Skip to content

Commit d4ae69b

Browse files
Dmitriy Dubsonjgrandja
Dmitriy Dubson
authored andcommitted
Add ability to customize the access token response
Issue gh-925 Closes gh-1429
1 parent 4bcae2b commit d4ae69b

File tree

6 files changed

+468
-42
lines changed

6 files changed

+468
-42
lines changed

docs/modules/ROOT/pages/protocol-endpoints.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ The supported https://datatracker.ietf.org/doc/html/rfc6749#section-1.3[authoriz
263263

264264
* `*AuthenticationConverter*` -- A `DelegatingAuthenticationConverter` composed of `OAuth2AuthorizationCodeAuthenticationConverter`, `OAuth2RefreshTokenAuthenticationConverter`, `OAuth2ClientCredentialsAuthenticationConverter`, and `OAuth2DeviceCodeAuthenticationConverter`.
265265
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeAuthenticationProvider`, `OAuth2RefreshTokenAuthenticationProvider`, `OAuth2ClientCredentialsAuthenticationProvider`, and `OAuth2DeviceCodeAuthenticationProvider`.
266-
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an `OAuth2AccessTokenAuthenticationToken` and returns the `OAuth2AccessTokenResponse`.
266+
* `*AuthenticationSuccessHandler*` -- An `OAuth2AccessTokenResponseAuthenticationSuccessHandler`.
267267
* `*AuthenticationFailureHandler*` -- An `OAuth2ErrorAuthenticationFailureHandler`.
268268

269269
[[oauth2-token-endpoint-customizing-client-credentials-grant-request-validation]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2020-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.springframework.lang.Nullable;
19+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
20+
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AccessTokenResponseAuthenticationSuccessHandler;
21+
import org.springframework.util.Assert;
22+
23+
import java.util.Collections;
24+
import java.util.HashMap;
25+
import java.util.Map;
26+
import java.util.function.Consumer;
27+
28+
/**
29+
* An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2AccessTokenResponse.Builder}
30+
* and is used when customizing the building of the {@link OAuth2AccessTokenResponse}.
31+
*
32+
* @author Dmitriy Dubson
33+
* @see OAuth2AuthenticationContext
34+
* @see OAuth2AccessTokenResponse
35+
* @see OAuth2AccessTokenResponseAuthenticationSuccessHandler#setAccessTokenResponseCustomizer(Consumer)
36+
* @since 1.3
37+
*/
38+
public final class OAuth2AccessTokenAuthenticationContext implements OAuth2AuthenticationContext {
39+
private final Map<Object, Object> context;
40+
41+
private OAuth2AccessTokenAuthenticationContext(Map<Object, Object> context) {
42+
this.context = Collections.unmodifiableMap(new HashMap<>(context));
43+
}
44+
45+
@SuppressWarnings("unchecked")
46+
@Nullable
47+
@Override
48+
public <V> V get(Object key) {
49+
return hasKey(key) ? (V) this.context.get(key) : null;
50+
}
51+
52+
@Override
53+
public boolean hasKey(Object key) {
54+
Assert.notNull(key, "key cannot be null");
55+
return this.context.containsKey(key);
56+
}
57+
58+
/**
59+
* Returns the {@link OAuth2AccessTokenResponse.Builder} access token response builder
60+
* @return the {@link OAuth2AccessTokenResponse.Builder}
61+
*/
62+
public OAuth2AccessTokenResponse.Builder getAccessTokenResponse() {
63+
return get(OAuth2AccessTokenResponse.Builder.class);
64+
}
65+
66+
/**
67+
* Constructs a new {@link Builder} with the provided {@link OAuth2AccessTokenAuthenticationToken}.
68+
*
69+
* @param authentication the {@link OAuth2AccessTokenAuthenticationToken}
70+
* @return the {@link Builder}
71+
*/
72+
public static OAuth2AccessTokenAuthenticationContext.Builder with(OAuth2AccessTokenAuthenticationToken authentication) {
73+
return new OAuth2AccessTokenAuthenticationContext.Builder(authentication);
74+
}
75+
76+
/**
77+
* A builder for {@link OAuth2AccessTokenAuthenticationContext}
78+
*/
79+
public static final class Builder extends AbstractBuilder<OAuth2AccessTokenAuthenticationContext, Builder> {
80+
private Builder(OAuth2AccessTokenAuthenticationToken authentication) {
81+
super(authentication);
82+
}
83+
84+
/**
85+
* Sets the {@link OAuth2AccessTokenResponse.Builder} access token response builder
86+
* @param accessTokenResponse the {@link OAuth2AccessTokenResponse.Builder}
87+
* @return the {@link Builder} for further configuration
88+
*/
89+
public Builder accessTokenResponse(OAuth2AccessTokenResponse.Builder accessTokenResponse) {
90+
return put(OAuth2AccessTokenResponse.Builder.class, accessTokenResponse);
91+
}
92+
93+
/**
94+
* Builds a new {@link OAuth2AccessTokenAuthenticationContext}.
95+
*
96+
* @return the {@link OAuth2AccessTokenAuthenticationContext}
97+
*/
98+
public OAuth2AccessTokenAuthenticationContext build() {
99+
Assert.notNull(get(OAuth2AccessTokenResponse.Builder.class), "accessTokenResponse cannot be null");
100+
101+
return new OAuth2AccessTokenAuthenticationContext(getContext());
102+
}
103+
}
104+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java

+4-41
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,32 +16,24 @@
1616
package org.springframework.security.oauth2.server.authorization.web;
1717

1818
import java.io.IOException;
19-
import java.time.temporal.ChronoUnit;
2019
import java.util.Arrays;
21-
import java.util.Map;
2220

2321
import jakarta.servlet.FilterChain;
2422
import jakarta.servlet.ServletException;
2523
import jakarta.servlet.http.HttpServletRequest;
2624
import jakarta.servlet.http.HttpServletResponse;
27-
2825
import org.springframework.core.log.LogMessage;
2926
import org.springframework.http.HttpMethod;
30-
import org.springframework.http.converter.HttpMessageConverter;
31-
import org.springframework.http.server.ServletServerHttpResponse;
3227
import org.springframework.security.authentication.AbstractAuthenticationToken;
3328
import org.springframework.security.authentication.AuthenticationDetailsSource;
3429
import org.springframework.security.authentication.AuthenticationManager;
3530
import org.springframework.security.core.Authentication;
3631
import org.springframework.security.core.context.SecurityContextHolder;
37-
import org.springframework.security.oauth2.core.OAuth2AccessToken;
3832
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
3933
import org.springframework.security.oauth2.core.OAuth2Error;
4034
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
41-
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
4235
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
4336
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
44-
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
4537
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
4638
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
4739
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
@@ -54,14 +46,14 @@
5446
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
5547
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
5648
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
49+
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AccessTokenResponseAuthenticationSuccessHandler;
5750
import org.springframework.security.web.authentication.AuthenticationConverter;
5851
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
5952
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
6053
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
6154
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
6255
import org.springframework.security.web.util.matcher.RequestMatcher;
6356
import org.springframework.util.Assert;
64-
import org.springframework.util.CollectionUtils;
6557
import org.springframework.web.filter.OncePerRequestFilter;
6658

6759
/**
@@ -86,6 +78,7 @@
8678
* @author Joe Grandja
8779
* @author Madhu Bhat
8880
* @author Daniel Garnier-Moiroux
81+
* @author Dmitriy Dubson
8982
* @since 0.0.1
9083
* @see AuthenticationManager
9184
* @see OAuth2AuthorizationCodeAuthenticationProvider
@@ -103,12 +96,10 @@ public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
10396
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
10497
private final AuthenticationManager authenticationManager;
10598
private final RequestMatcher tokenEndpointMatcher;
106-
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
107-
new OAuth2AccessTokenResponseHttpMessageConverter();
10899
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
109100
new WebAuthenticationDetailsSource();
110101
private AuthenticationConverter authenticationConverter;
111-
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAccessTokenResponse;
102+
private AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2AccessTokenResponseAuthenticationSuccessHandler();
112103
private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler();
113104

114105
/**
@@ -218,34 +209,6 @@ public void setAuthenticationFailureHandler(AuthenticationFailureHandler authent
218209
this.authenticationFailureHandler = authenticationFailureHandler;
219210
}
220211

221-
private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
222-
Authentication authentication) throws IOException {
223-
224-
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
225-
(OAuth2AccessTokenAuthenticationToken) authentication;
226-
227-
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
228-
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
229-
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
230-
231-
OAuth2AccessTokenResponse.Builder builder =
232-
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
233-
.tokenType(accessToken.getTokenType())
234-
.scopes(accessToken.getScopes());
235-
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
236-
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
237-
}
238-
if (refreshToken != null) {
239-
builder.refreshToken(refreshToken.getTokenValue());
240-
}
241-
if (!CollectionUtils.isEmpty(additionalParameters)) {
242-
builder.additionalParameters(additionalParameters);
243-
}
244-
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
245-
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
246-
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
247-
}
248-
249212
private static void throwError(String errorCode, String parameterName) {
250213
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
251214
throw new OAuth2AuthenticationException(error);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2020-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.web.authentication;
17+
18+
import java.io.IOException;
19+
import java.time.temporal.ChronoUnit;
20+
import java.util.Map;
21+
import java.util.function.Consumer;
22+
23+
import jakarta.servlet.ServletException;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
28+
import org.springframework.http.converter.HttpMessageConverter;
29+
import org.springframework.http.server.ServletServerHttpResponse;
30+
import org.springframework.security.core.Authentication;
31+
import org.springframework.security.oauth2.core.*;
32+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
33+
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
34+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationContext;
35+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
36+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.CollectionUtils;
39+
40+
/**
41+
* An implementation of an {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
42+
* and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
43+
*
44+
* @author Dmitriy Dubson
45+
* @see AuthenticationSuccessHandler
46+
* @see OAuth2AccessTokenResponseHttpMessageConverter
47+
* @since 1.3
48+
*/
49+
public final class OAuth2AccessTokenResponseAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
50+
private final Log logger = LogFactory.getLog(getClass());
51+
52+
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseConverter =
53+
new OAuth2AccessTokenResponseHttpMessageConverter();
54+
55+
private Consumer<OAuth2AccessTokenAuthenticationContext> accessTokenResponseCustomizer;
56+
57+
@Override
58+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
59+
if (!(authentication instanceof OAuth2AccessTokenAuthenticationToken accessTokenAuthentication)) {
60+
if (this.logger.isErrorEnabled()) {
61+
this.logger.error(Authentication.class.getSimpleName() + " must be of type " +
62+
OAuth2AccessTokenAuthenticationToken.class.getName() +
63+
" but was " + authentication.getClass().getName());
64+
}
65+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "Unable to process the access token response.", null);
66+
throw new OAuth2AuthenticationException(error);
67+
}
68+
69+
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
70+
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
71+
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
72+
73+
OAuth2AccessTokenResponse.Builder builder =
74+
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
75+
.tokenType(accessToken.getTokenType())
76+
.scopes(accessToken.getScopes());
77+
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
78+
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
79+
}
80+
if (refreshToken != null) {
81+
builder.refreshToken(refreshToken.getTokenValue());
82+
}
83+
if (!CollectionUtils.isEmpty(additionalParameters)) {
84+
builder.additionalParameters(additionalParameters);
85+
}
86+
87+
if (this.accessTokenResponseCustomizer != null) {
88+
// @formatter:off
89+
OAuth2AccessTokenAuthenticationContext accessTokenAuthenticationContext =
90+
OAuth2AccessTokenAuthenticationContext.with(accessTokenAuthentication)
91+
.accessTokenResponse(builder)
92+
.build();
93+
// @formatter:on
94+
this.accessTokenResponseCustomizer.accept(accessTokenAuthenticationContext);
95+
if (this.logger.isTraceEnabled()) {
96+
this.logger.trace("Customized access token response");
97+
}
98+
}
99+
100+
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
101+
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
102+
this.accessTokenResponseConverter.write(accessTokenResponse, null, httpResponse);
103+
}
104+
105+
/**
106+
* Sets the {@code Consumer} providing access to the {@link OAuth2AccessTokenAuthenticationContext}
107+
* containing an {@link OAuth2AccessTokenResponse.Builder} and additional context information.
108+
*
109+
* @param accessTokenResponseCustomizer the {@code Consumer} providing access to the {@link OAuth2AccessTokenAuthenticationContext} containing an {@link OAuth2AccessTokenResponse.Builder}
110+
*/
111+
public void setAccessTokenResponseCustomizer(Consumer<OAuth2AccessTokenAuthenticationContext> accessTokenResponseCustomizer) {
112+
Assert.notNull(accessTokenResponseCustomizer, "accessTokenResponseCustomizer cannot be null");
113+
this.accessTokenResponseCustomizer = accessTokenResponseCustomizer;
114+
}
115+
}

0 commit comments

Comments
 (0)