JHipster - OAuth2/OIDC need to read groups from access token - spring-security

The JHipster OAuth2/OIDC default configuration expects the "groups' to be found in the idToken. Can anyone explain how to read the "groups" from the access token instead?

Here are the changes made to retrieve the user's groups / granted authorities from the access token.
Note that for my case the Access Token (JSON) that the auth code is exchanged for contains an "access_token" field as a peer to the idToken. The "access_token" field is an ID or reference to the actual access token with the user's groups. An extra http request is needed to retrieve that "actual" access token.
For Okta the access token is a JWT similar to the idToken so if for some reason you need to configure Okta to add the groups to the access token instead of the idToken you will find them there.
Solution was based in this Spring doc:
Delegation-based strategy with OAuth2UserService
In your WebSecurityConfigurerAdapter class edit your oauth2Login config:
.oauth2Login().userInfoEndpoint().oidcUserService(this.oidcUserService());
Then create the custom oidcUserService():
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
// The access token will be a reference to the actual token
// ( for Okta this would be the actual JWT access token )
String accessTokenRef = userRequest.getAccessToken().getTokenValue();
// Call the end point to get the actual access_token
// ( httpClient is just a RestTemplate impl w/the required configs )
String[] groups = httpClient.fetchGroups(accessTokenRef);
// Create the GrantedAuthority objs & add to mappedAuthorities set
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
for (String group: groups) {
mappedAuthorities.add(new SimpleGrantedAuthority(group));
}
// Create a copy of oidcUser but use the mappedAuthorities instead
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
If you are using JHipster there will be a GrantedAuthoritiesMapper that will need to be updated to map the authorities passed in directly to your application roles rather than reading them from the idToken. Something like:
#Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
Collection<String> roles = new HashSet();
authorities.forEach(authority -> {
roles.add(authority.getAuthority());
});
List<GrantedAuthority> list = SecurityUtils.mapRolesToGrantedAuthorities(roles);
mappedAuthorities = new HashSet<GrantedAuthority>(list);
return mappedAuthorities;
};
}
There are likely some other ways to do this and I would be happy to hear any advice.
Thanks to the commenters for their help.

Related

How to set modify the access token request entity for Client Credentials grant when using Spring Security OAuth2 framrwork

I'm writing client for a 3rd party service that doesn't have the standard request format for getting an access token. The access token request body is a JSON with two attributes and the client_id and client_secret needs to be sent as a basic auth header. How do I build the custom request entity and headers converter to appropriately set these values in the access token request?
I have the client configuration with the client manager and responseclient.
public class RestClientConfig {
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient){
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(configurer -> configurer.accessTokenResponseClient(accessTokenResponseClient))
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient(){
OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
requestEntityConverter.setParametersConverter(null); --> this is where I'm stuck. Need to build a request entity converter bean to pass to this method
DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return accessTokenResponseClient;
}
}

Blazor and OAuth JWT Bearer Token Storage

We're creating a small Blazor app which accesses a 3rd party api which we have no control over. The API is secured using OAuth with a client_id and client_secret on a machine to machine basis. There is no authentication from the user.
Initially we hit the token endpoint with the id and secret and it returns a token with an expiry date and time. Currently we just repeat the initial token request before every call to the api so that the token is always brand new and has never expired.
Is this practice accepted or should I be storing the bearer token and it's expiration date and time somewhere secure on the client to check the expiration before requesting a new token?
I'm conscious that if the app is widely used internally at our business we could start to reach the api usage limits, but also, if we store it locally on the client, could I mistakenly leave us vulnerable to XSS attacks or similar.
I'm looking for best practice guidance really, and, if the advice is to store the token and check against expiry, where and how should I be storing the token securely?
I found a nice solution at:
https://referbruv.com/blog/using-imemorycache-for-token-caching-in-an-aspnet-core-application/
The author gives an excellent demonstration of how to use IMemoryCache for my exact scenario. I implement a Token Service which checks the expiry of the cache item. If it's in date, I just used the cached token. If the token has expired, I fetch a new token from the API.
public class TokenService : ITokenService
{
private readonly IApiAccountService _apiAccountService;
private readonly IMemoryCache _cache;
public TokenService(IMemoryCache cache, IApiAccountService apiAccountService)
{
_cache = cache;
_apiAccountService = apiAccountService;
}
public async Task<string> FetchTokenAsync()
{
string token;
if (!_cache.TryGetValue("Token", out token))
{
var authResult = await _apiAccountService.GetToken();
var options = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds((authResult.expires_at - DateTime.Now).TotalSeconds));
_cache.Set("Token", authResult.access_token, options);
token = authResult.access_token;
}
return token;
}
}
Then, when calling my API I just call the Token Service:
private readonly ITokenService _tokenService;
public ApiService(HttpClient httpClient, IConfiguration configuration, ITokenService tokenService)
{
_httpClient = httpClient;
_configuration = configuration;
_tokenService = tokenService;
}
...
private async Task<List<T>> FetchListAsync<T>(string uri)
{
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenService.FetchTokenAsync());
var response = await _httpClient.GetAsync(uri);
response.EnsureSuccessStatusCode();
using var responseStream = await
response.Content.ReadAsStreamAsync();
var reader = new StreamReader(responseStream).ReadToEnd();
var responseObject = JsonConvert.DeserializeObject<List<T>>(reader);
return responseObject;
}

OAuth2 v2.0 authorize - error AADSTS90009 Application is requesting a token for itself

Requirement:
Use OAuth2 Implicit Grant flow do authorization in swagger ui page.
Swagger configuration code:
#Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("*"))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securitySchema()))
.securityContexts(Collections.singletonList(securityContext()));
}
private SecurityScheme securitySchema() {
String scopeForAppIdUri = "http://xxxxxxx/.default";
AuthorizationScope authorizationScopeForAppIdUri = new AuthorizationScope(scopeForAppIdUri, "scope to restrict access to data protected by APIs");
List<AuthorizationScope> authorizationScopes = Collections.singletonList(authorizationScopeForAppIdUri);
LoginEndpoint loginEndpoint = new LoginEndpoint("https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize");
List<GrantType> grantTypes = Collections.singletonList(new ImplicitGrant(loginEndpoint, "Authorization"));
return new OAuth("oauth2", authorizationScopes, grantTypes, null);
}
private SecurityContext securityContext() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
List<SecurityReference> securityReferences = Collections.singletonList(
new SecurityReference("oauth2", new AuthorizationScope[]{authorizationScope}));
return SecurityContext.builder().securityReferences(securityReferences).build();
}
Sent request:
https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
?response_type=token
&client_id={client id GUID}
&redirect_uri=http://localhost:8080/webjars/springfox-swagger-ui/oauth2-redirect.html
&scope=https://xxxxxxx/.default
&state=xxxxxxxxx
Error:
AADSTS90009: Application is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App identifier
Question:
There's a similar question about this error: OAuth 2.0 and Azure Active Directory - error AADSTS90009, which suggests to set 'resource' parameter to be client id for v1.0, so for v2.0, it can clear the error by setting 'scope' value to be client id.
But for my case, Our endpoints will validate 'aud' value in token that should equals to the application id uri 'http://xxxxxxx'(this feature cannot change), which means i cannot just set 'scope' to be '{client-id}/.default', or else even though it can return token successfully but the token cannot pass the validation of endpoint.

Spring Oauth2 Client, automatically refresh expired access_token

Let me explain my use case.
I need to have a spring boot oauth2 client application (not a resource server As we already have a separate resource server). Also I have following requirements:
For each out going request to resource server, we need to send id_token. (Done by customizing resttemplate).
For any request, no matter if it invokes resource server or not, If access token is expired my application must refresh it automatically (without any user intervention like any popup or redirection.).
If refresh_token is also expired, user must be logged out.
Questions:
For point 2 and 3, I have spent many hours reading documents and code and Stack Overflow but was not able to find the solution (or did not understand). So I decided to put all pieces together which I found on many blogs and documents, and come up with my solution. Below is my solution for point 2.
Can we please have a look to below code and suggest if there could be any problem with this approach?
How to solve point 3 I am thinking of extending solution for point 2 but not sure what code I need to write, can anyone guide me?
/**
*
* #author agam
*
*/
#Component
public class ExpiredTokenFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(ExpiredTokenFilter.class);
private Duration accessTokenExpiresSkew = Duration.ofMillis(1000);
private Clock clock = Clock.systemUTC();
#Autowired
private OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
#Autowired
CustomOidcUserService userService;
private DefaultRefreshTokenTokenResponseClient accessTokenResponseClient;
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory;
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
public ExpiredTokenFilter() {
super();
this.accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
this.jwtDecoderFactory = new OidcIdTokenDecoderFactory();
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.debug("my custom filter called ");
/**
* check if authentication is done.
*/
if (null != SecurityContextHolder.getContext().getAuthentication()) {
OAuth2AuthenticationToken currentUser = (OAuth2AuthenticationToken) SecurityContextHolder.getContext()
.getAuthentication();
OAuth2AuthorizedClient authorizedClient = this.oAuth2AuthorizedClientService
.loadAuthorizedClient(currentUser.getAuthorizedClientRegistrationId(), currentUser.getName());
/**
* Check if token existing token is expired.
*/
if (isExpired(authorizedClient.getAccessToken())) {
/*
* do something to get new access token
*/
log.debug(
"=========================== Token Expired !! going to refresh ================================================");
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
/*
* Call Auth server token endpoint to refresh token.
*/
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
clientRegistration, authorizedClient.getAccessToken(), authorizedClient.getRefreshToken());
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient
.getTokenResponse(refreshTokenGrantRequest);
/*
* Convert id_token to OidcToken.
*/
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
/*
* Since I have already implemented a custom OidcUserService, reuse existing
* code to get new user.
*/
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
accessTokenResponse.getAccessToken(), idToken, accessTokenResponse.getAdditionalParameters()));
log.debug(
"=========================== Token Refresh Done !! ================================================");
/*
* Print old and new id_token, just in case.
*/
DefaultOidcUser user = (DefaultOidcUser) currentUser.getPrincipal();
log.debug("new id token is " + oidcUser.getIdToken().getTokenValue());
log.debug("old id token was " + user.getIdToken().getTokenValue());
/*
* Create new authentication(OAuth2AuthenticationToken).
*/
OAuth2AuthenticationToken updatedUser = new OAuth2AuthenticationToken(oidcUser,
oidcUser.getAuthorities(), currentUser.getAuthorizedClientRegistrationId());
/*
* Update access_token and refresh_token by saving new authorized client.
*/
OAuth2AuthorizedClient updatedAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
currentUser.getName(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken());
this.oAuth2AuthorizedClientService.saveAuthorizedClient(updatedAuthorizedClient, updatedUser);
/*
* Set new authentication in SecurityContextHolder.
*/
SecurityContextHolder.getContext().setAuthentication(updatedUser);
}
}
filterChain.doFilter(request, response);
}
private Boolean isExpired(OAuth2AccessToken oAuth2AccessToken) {
Instant now = this.clock.instant();
Instant expiresAt = oAuth2AccessToken.getExpiresAt();
return now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew));
}
private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
OAuth2AccessTokenResponse accessTokenResponse) {
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
Jwt jwt;
try {
jwt = jwtDecoder
.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
} catch (JwtException ex) {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
}
OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
return idToken;
}
}
I am open for any suggestion to improve my code. Thanks.
There are not enough details to understand your use-case fully. It would be great to understand:
Spring security is rapidly evolving around OAuth2, consider mentioning the version you are using. My answer assumes 5.2+
Are you in servlet (user logged in somehow) or non-servlet (like #Scheduled method) environment
From the limited information and my limited knowledge I have following hints:
Consider using WebClient instead of RestTemplate, this is they way to go for the future. It is reactive but don't be scared. It can be used in "blocking" environment as well, you will not use it's full potential but you can still benefit from its better support for OAuth2
WebClient itself has a ServletOAuth2AuthorizedClientExchangeFilterFunction which does pretty much what you are trying to achieve
When creating ServletOAuth2AuthorizedClientExchangeFilterFunction you pass in AuthorizedClientServiceOAuth2AuthorizedClientManager which is a strategy on how to (re)authenticate client.
Sample configuration may look as follows:
#Bean
public WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
manager.setAuthorizedClientProvider(new DelegatingOAuth2AuthorizedClientProvider(
new RefreshTokenOAuth2AuthorizedClientProvider(),
new ClientCredentialsOAuth2AuthorizedClientProvider()));
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(manager);
oauth2.setDefaultClientRegistrationId("your-client-registratioin-id");
return WebClient.builder()
.filter(oauth2)
.apply(oauth2.oauth2Configuration())
.build();
}
And use it as:
#Autowire
private final WebClient webClient;
...
webClient.get()
.uri("http://localhost:8081/api/message")
.retrieve()
.bodyToMono(String.class)
.map(string -> "Retrieved using password grant: " + string)
.subscribe(log::info);
Hope this helps to move in the right direction! Have fun

Authenticating users with auth token in query string with ASP.NET MVC

[This question relates to ASP.NET MVC4, and it is about best-practice approach - so please, don't suggest hacks.]
I want to authenticate users using an auth token sent in the request URL. It works similarly to a password reset token, except in this case it does not go to a reset page but instead grants access to some portion of the site. The idea is to send the URL with the auth token to a verified email address of the user. Users can click the link and perform some actions without typing their password.
Out-of-the-box, ASP.NET has the [Authorize] attribute and the SimpleMembershipProvider - these seem to work great, but they do some voodoo magic under the hood (like auto-generating database tables), so I don't know how to extend them to add this link-based auth token.
I don't expect an exact answer, but please do point me to the right direction.
Thanks!
Uf, broad question. But I will try at least to direct you to a right direction.
So first if suggest that you use Forms Authentication as a base, but you will have to customize using of it. And I presume that you do not want to use cookies for the authentication as this is native behaviour of the Forms Authentication.
The most important point you should consider to have it you custom query string token based authentication.
Create a login action and in this action you will authorize the user, if he have granted access you ask FormsAuthentication to create AuthCookie. For the further on you just take the httpCookie.Value as your auth token that you will carry in query string.
You need to implement the Application_BeginRequest in the Global.asax that will handle this query string tokens and translate it into the cookie. With this approach you can leverage all the ASP.NET Forms Authentication infrastructure.
This is quite high level picture w/o code. If you need more detail help I can also provide it to you.
You should just use a regular Action that accepts HttpGet.
Upon receiving the token, immediately invalid it so it can't be used again.
Also, only accept tokens that are within your pre-defined range of time period, like 24 or 72 hours.
Thank you Peter for idea.
If smb need to create JWT token authorization for old ASP.NET MVC5.I wrote small example. I don't serialize cookie to JWT. I create a JWT and after I am checking it in the BeginRequest. If everything is ok, I create a cookie and set it to the httpContext.Request. I used authentication mode="Forms" for application and it require cookies.
For create JWT token:
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
[AllowAnonymous]
[HttpPost]
public ActionResult LoginJWT(LoginViewModel model)
{
ActionResult response = null;
if (ModelState.IsValid)
{
if (true) //todo: check user login&password
{
var payload = new Dictionary<string, object>
{
{ "iss", "subject" },
{ "sub", "api" },
{ "exp", DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds()},
{ "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds()},
{ "jti", Guid.NewGuid() },
{ "uid", "64" } //custom field for identificate user
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(payload, secret);
response = Content(token);
}
else
{
response = new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest, "Login or password are not found");
}
}
else
{
response = new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest, "Errors in Model");
}
return response;
}
For check JWT token in Global.asax:
public override void Init()
{
this.BeginRequest += this.BeginRequestHandler;
base.Init();
}
private void BeginRequestHandler(object sender, EventArgs e)
{
var bearerToken = this.Context.Request.Headers["Authorization"];
if (bearerToken != null)
{
var token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
int userId = 0;
try
{
IJsonSerializer serializer = new JsonNetSerializer();
var provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
var json = decoder.DecodeToObject<IDictionary<string, string>>(token, secret, verify: true);
if (json.TryGetValue("uid", out var uid))
{
userId = Convert.ToInt32(uid);
}
}
catch (TokenExpiredException)
{
Console.WriteLine("Token has expired");
}
catch (SignatureVerificationException)
{
Console.WriteLine("Token has invalid signature");
}
if (userId != 0)
{
// check user by id, if found create cookie.
}
}
}
I used:
jwt-dotnet/jwt library 7.2.1

Resources