Skip to content

Simplify customizing the access token response #925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Hccake opened this issue Oct 11, 2022 · 8 comments
Closed

Simplify customizing the access token response #925

Hccake opened this issue Oct 11, 2022 · 8 comments
Assignees
Labels
status: duplicate A duplicate of another issue

Comments

@Hccake
Copy link

Hccake commented Oct 11, 2022

In OAuth2.1 draft,Token Response can be added with additional parameters.

It can currently be solved by replacing the default AuthenticationSuccessHandler.

Hope there is a component that can be easily implemented, like TokenEnhancer in spring-security-oauth2.

@Hccake Hccake added the type: enhancement A general enhancement label Oct 11, 2022
@javamachr
Copy link

There is org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer example here

@Hccake
Copy link
Author

Hccake commented Oct 18, 2022

There is org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer example here

I think token endpoint response additional parameters and token claims are different things, and by default token claims are not in the response.

For example, the idToken attribute of OIDC is officially stored in additionalParameters:
see OAuth2AuthorizationCodeAuthenticationProvider line 179 - 213

		// ----- ID token -----
		OidcIdToken idToken;
		if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
			// @formatter:off
			tokenContext = tokenContextBuilder
					.tokenType(ID_TOKEN_TOKEN_TYPE)
					.authorization(authorizationBuilder.build())	// ID token customizer may need access to the access token and/or refresh token
					.build();
			// @formatter:on
			OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
			if (!(generatedIdToken instanceof Jwt)) {
				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
						"The token generator failed to generate the ID token.", ERROR_URI);
				throw new OAuth2AuthenticationException(error);
			}
			idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
					generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
			authorizationBuilder.token(idToken, (metadata) ->
					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
		} else {
			idToken = null;
		}

		authorization = authorizationBuilder.build();

		// Invalidate the authorization code as it can only be used once
		authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());

		this.authorizationService.save(authorization);

		Map<String, Object> additionalParameters = Collections.emptyMap();
		if (idToken != null) {
			additionalParameters = new HashMap<>();
			additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
		}

@jgrandja
Copy link
Collaborator

@Hccake As mentioned by @javamachr, the OAuth2TokenCustomizer is the equivalent to TokenEnhancer in spring-security-oauth2.

authorizationServerConfigurer.tokenEndpoint.accessTokenResponseHandler() provides the ability to customize the OAuth 2.0 Access Token Response parameters, however, the parameters are not necessarily the same as the claims contained in the access token.

Can you be more specific on what type of parameters you want to customize (add?) to the OAuth 2.0 Access Token Response?

@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue and removed type: enhancement A general enhancement labels Oct 24, 2022
@jgrandja jgrandja self-assigned this Oct 24, 2022
@Hccake
Copy link
Author

Hccake commented Oct 25, 2022

For some clients, when returning access_token, want to directly return some user information, such as user_id.

When using spring-security-oauth2, it is possible to do this with TokenEnhancer, but not with OAuth2TokenCustomizer in spring-authorization-server.

Using JWT type tokens can extend claims through OAuth2TokenCustomizer, but some old systems use Opaque type tokens, and the claims extended by OAuth2TokenCustomizer cannot be obtained in the response.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Oct 25, 2022
@jgrandja
Copy link
Collaborator

@Hccake

but some old systems use Opaque type tokens, and the claims extended by OAuth2TokenCustomizer cannot be obtained in the response.

As per the reference documentation for OAuth2TokenCustomizer<OAuth2TokenClaimsContext>:

An OAuth2TokenCustomizer declared with a generic type of OAuth2TokenClaimsContext (implements OAuth2TokenContext) provides the ability to customize the claims of an "opaque" OAuth2AccessToken.

You can register a OAuth2TokenCustomizer<OAuth2TokenClaimsContext> @Bean that adds user information as claims.

The client can obtain the user information claims from the OAuth2 Token Introspection Endpoint on a subsequent call.

If you need the user information returned in the access token response, then you can supply a custom AuthenticationSuccessHandler to OAuth2TokenEndpointFilter. The following implementation is similar to the default OAuth2TokenEndpointFilter.sendAccessTokenResponse() but it can also add claims from the opaque token.

public static class CustomAccessTokenResponseHandler implements AuthenticationSuccessHandler {
	private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
			new OAuth2AccessTokenResponseHttpMessageConverter();
	private OAuth2AuthorizationService authorizationService;

	public CustomAccessTokenResponseHandler(OAuth2AuthorizationService authorizationService) {
		this.authorizationService = authorizationService;
	}

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
				(OAuth2AccessTokenAuthenticationToken) authentication;

		OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
		OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
		Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();

		// Lookup the authorization using the access token
		OAuth2Authorization authorization = this.authorizationService.findByToken(
				accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);

		Map<String, Object> opaqueTokenClaims = authorization.getAccessToken().getClaims();
		Authentication userPrincipal = authorization.getAttribute(Principal.class.getName());

		OAuth2AccessTokenResponse.Builder builder =
				OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
						.tokenType(accessToken.getTokenType())
						.scopes(accessToken.getScopes());
		if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
			builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
		}
		if (refreshToken != null) {
			builder.refreshToken(refreshToken.getTokenValue());
		}
		if (!CollectionUtils.isEmpty(additionalParameters)) {
			builder.additionalParameters(additionalParameters);
		}

		// TODO Add custom response parameters using `opaqueTokenClaims` and/or `userPrincipal`


		OAuth2AccessTokenResponse accessTokenResponse = builder.build();
		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
		this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
	}

}

I'll close this as either solution provided will work.

@jgrandja jgrandja added status: invalid An issue that we don't feel is valid for: stackoverflow A question that's better suited to stackoverflow.com and removed status: feedback-provided Feedback has been provided status: invalid An issue that we don't feel is valid labels Oct 25, 2022
@Hccake
Copy link
Author

Hccake commented Oct 26, 2022

As I said at the beginning, a custom AuthenticationSuccessHandler can extend the access token response, but users don't actually need to care about the conversion of OAuth2AccessTokenResponse.

If we have a TokenResponseEnhancer component that allows users to customize the response, it will be easier for users to use, modify the default AuthenticationSuccessHandler, which is OAuth2TokenEndpointFilter::sendAccessTokenResponse:

private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
		Authentication authentication) throws IOException {

	OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
			(OAuth2AccessTokenAuthenticationToken) authentication;

	OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
	OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();

	OAuth2AccessTokenResponse.Builder builder =
			OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
					.tokenType(accessToken.getTokenType())
					.scopes(accessToken.getScopes());
	if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
		builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
	}
	if (refreshToken != null) {
		builder.refreshToken(refreshToken.getTokenValue());
	}

	Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
	// The user only needs to register a tokenResponseEnhancer to implement 
	// the attribute extension of the access token response
	if(tokenResponseEnhancer != null) {
		additionalParameters = tokenResponseEnhancer.enhance(accessTokenAuthentication, additionalParameters);
	}
	if (!CollectionUtils.isEmpty(additionalParameters)) {
		builder.additionalParameters(additionalParameters);
	}

	OAuth2AccessTokenResponse accessTokenResponse = builder.build();
	ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
	this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}

Or separate the construction and writing of OAuth2AccessTokenResponse:

private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
		Authentication authentication) throws IOException {
	OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseBuilder.build(authentication);
	ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
	this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}

accessTokenResponseBuilder is used to build and accessTokenHttpResponseConverter is used to write out

@jgrandja
Copy link
Collaborator

jgrandja commented Nov 1, 2022

@Hccake The solution provided with CustomAccessTokenResponseHandler will move you forward. I feel the duplication of initializing the defaults for OAuth2AccessTokenResponse.Builder is quite minimal and don't think there is much value for reuse at this point.

However, I will re-open this ticket and we may consider adding an improvement later on at some point.

@jgrandja jgrandja removed the status: on-hold We can't start working on this issue yet label May 27, 2023
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Oct 30, 2023
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Nov 16, 2023
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Dec 13, 2023
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Dec 13, 2023
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Jan 5, 2024
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Jan 16, 2024
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Jan 16, 2024
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Jan 19, 2024
ddubson pushed a commit to ddubson/spring-authorization-server that referenced this issue Jan 22, 2024
@jgrandja jgrandja self-assigned this Jan 29, 2024
@jgrandja jgrandja added status: duplicate A duplicate of another issue and removed type: enhancement A general enhancement labels Jan 29, 2024
@jgrandja
Copy link
Collaborator

Closing as duplicate of gh-1429

jgrandja pushed a commit that referenced this issue Jan 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants