Skip to content

Commit 63c7a45

Browse files
kratostainejgrandja
authored andcommitted
Add token endpoint implementation
Fixes spring-projectsgh-67
1 parent e1a64df commit 63c7a45

File tree

5 files changed

+408
-1
lines changed

5 files changed

+408
-1
lines changed

core/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*
3434
* @author Joe Grandja
3535
* @author Krisztian Toth
36+
* @author Madhu Bhat
3637
* @since 0.0.1
3738
* @see RegisteredClient
3839
* @see OAuth2AccessToken
@@ -74,6 +75,15 @@ public OAuth2AccessToken getAccessToken() {
7475
return this.accessToken;
7576
}
7677

78+
/**
79+
* Sets the access token {@link OAuth2AccessToken} in the {@link OAuth2Authorization}.
80+
*
81+
* @param accessToken the access token
82+
*/
83+
public final void setAccessToken(OAuth2AccessToken accessToken) {
84+
this.accessToken = accessToken;
85+
}
86+
7787
/**
7888
* Returns the attribute(s) associated to the authorization.
7989
*

core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
/**
2727
* @author Joe Grandja
28+
* @author Madhu Bhat
2829
*/
2930
public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken {
3031
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
@@ -49,4 +50,13 @@ public Object getCredentials() {
4950
public Object getPrincipal() {
5051
return null;
5152
}
53+
54+
/**
55+
* Returns the access token {@link OAuth2AccessToken}.
56+
*
57+
* @return the access token
58+
*/
59+
public OAuth2AccessToken getAccessToken() {
60+
return this.accessToken;
61+
}
5262
}

core/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
/**
2626
* @author Joe Grandja
27+
* @author Madhu Bhat
2728
*/
2829
public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenticationToken {
2930
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
@@ -57,4 +58,13 @@ public Object getCredentials() {
5758
public Object getPrincipal() {
5859
return null;
5960
}
61+
62+
/**
63+
* Returns the code.
64+
*
65+
* @return the code
66+
*/
67+
public String getCode() {
68+
return this.code;
69+
}
6070
}

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

+148-1
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,177 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.web;
1717

18+
import com.fasterxml.jackson.annotation.JsonInclude;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
1820
import org.springframework.core.convert.converter.Converter;
21+
import org.springframework.http.HttpHeaders;
22+
import org.springframework.http.HttpMethod;
23+
import org.springframework.http.HttpStatus;
24+
import org.springframework.http.MediaType;
1925
import org.springframework.security.authentication.AuthenticationManager;
2026
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.context.SecurityContextHolder;
28+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
29+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
30+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
31+
import org.springframework.security.oauth2.core.OAuth2Error;
32+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
33+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
34+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
2135
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
36+
import org.springframework.security.oauth2.server.authorization.TokenType;
37+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
38+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
39+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
40+
import org.springframework.security.web.util.matcher.RequestMatcher;
41+
import org.springframework.util.Assert;
42+
import org.springframework.util.StringUtils;
2243
import org.springframework.web.filter.OncePerRequestFilter;
2344

2445
import javax.servlet.FilterChain;
2546
import javax.servlet.ServletException;
2647
import javax.servlet.http.HttpServletRequest;
2748
import javax.servlet.http.HttpServletResponse;
2849
import java.io.IOException;
50+
import java.io.Writer;
2951

3052
/**
53+
* This {@code Filter} is used by the client to obtain an access token by presenting
54+
* its authorization grant.
55+
*
56+
* <p>
57+
* It converts the OAuth 2.0 Access Token Request to {@link OAuth2AuthorizationCodeAuthenticationToken},
58+
* which is then authenticated by the {@link AuthenticationManager} and gets back
59+
* {@link OAuth2AccessTokenAuthenticationToken} which has the {@link OAuth2AccessToken} if the request
60+
* was successfully authenticated. The {@link OAuth2AccessToken} is then updated in the in-flight {@link OAuth2Authorization}
61+
* and sent back to the client. In case the authentication fails, an HTTP 401 (Unauthorized) response is returned.
62+
*
63+
* <p>
64+
* By default, this {@code Filter} responds to access token requests
65+
* at the {@code URI} {@code /oauth2/token} and {@code HttpMethod} {@code POST}
66+
* using the default {@link AntPathRequestMatcher}.
67+
*
68+
* <p>
69+
* The default base {@code URI} {@code /oauth2/token} may be overridden
70+
* via the constructor {@link #OAuth2TokenEndpointFilter(OAuth2AuthorizationService, AuthenticationManager, String)}.
71+
*
3172
* @author Joe Grandja
73+
* @author Madhu Bhat
3274
*/
3375
public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
34-
private Converter<HttpServletRequest, Authentication> authorizationGrantConverter;
76+
/**
77+
* The default endpoint {@code URI} for access token requests.
78+
*/
79+
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
80+
81+
private Converter<HttpServletRequest, Authentication> authorizationGrantConverter = this::convert;
3582
private AuthenticationManager authenticationManager;
3683
private OAuth2AuthorizationService authorizationService;
84+
private RequestMatcher uriMatcher;
85+
private ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
86+
87+
/**
88+
* Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters.
89+
*
90+
* @param authorizationService the authorization service implementation
91+
* @param authenticationManager the authentication manager implementation
92+
*/
93+
public OAuth2TokenEndpointFilter(OAuth2AuthorizationService authorizationService, AuthenticationManager authenticationManager) {
94+
Assert.notNull(authorizationService, "authorizationService cannot be null");
95+
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
96+
this.authenticationManager = authenticationManager;
97+
this.authorizationService = authorizationService;
98+
this.uriMatcher = new AntPathRequestMatcher(DEFAULT_TOKEN_ENDPOINT_URI, HttpMethod.POST.name());
99+
}
100+
101+
/**
102+
* Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters.
103+
*
104+
* @param authorizationService the authorization service implementation
105+
* @param authenticationManager the authentication manager implementation
106+
* @param tokenEndpointUri the token endpoint's uri
107+
*/
108+
public OAuth2TokenEndpointFilter(OAuth2AuthorizationService authorizationService, AuthenticationManager authenticationManager,
109+
String tokenEndpointUri) {
110+
Assert.notNull(authorizationService, "authorizationService cannot be null");
111+
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
112+
Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty");
113+
this.authenticationManager = authenticationManager;
114+
this.authorizationService = authorizationService;
115+
this.uriMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
116+
}
37117

38118
@Override
39119
protected void doFilterInternal(HttpServletRequest request,
40120
HttpServletResponse response, FilterChain filterChain)
41121
throws ServletException, IOException {
122+
if (uriMatcher.matches(request)) {
123+
try {
124+
if (validateAccessTokenRequest(request)) {
125+
OAuth2AuthorizationCodeAuthenticationToken authCodeAuthToken =
126+
(OAuth2AuthorizationCodeAuthenticationToken) authorizationGrantConverter.convert(request);
127+
OAuth2AccessTokenAuthenticationToken accessTokenAuthenticationToken =
128+
(OAuth2AccessTokenAuthenticationToken) authenticationManager.authenticate(authCodeAuthToken);
129+
if (accessTokenAuthenticationToken.isAuthenticated()) {
130+
OAuth2Authorization authorization = authorizationService
131+
.findByTokenAndTokenType(authCodeAuthToken.getCode(), TokenType.AUTHORIZATION_CODE);
132+
authorization.setAccessToken(accessTokenAuthenticationToken.getAccessToken());
133+
authorizationService.save(authorization);
134+
writeSuccessResponse(response, accessTokenAuthenticationToken.getAccessToken());
135+
} else {
136+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
137+
}
138+
}
139+
} catch (OAuth2AuthenticationException exception) {
140+
SecurityContextHolder.clearContext();
141+
writeFailureResponse(response, exception.getError());
142+
}
143+
} else {
144+
filterChain.doFilter(request, response);
145+
}
146+
}
147+
148+
private boolean validateAccessTokenRequest(HttpServletRequest request) {
149+
if (StringUtils.isEmpty(request.getParameter(OAuth2ParameterNames.CODE))
150+
|| StringUtils.isEmpty(request.getParameter(OAuth2ParameterNames.REDIRECT_URI))
151+
|| StringUtils.isEmpty(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))) {
152+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
153+
} else if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))) {
154+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE));
155+
}
156+
return true;
157+
}
158+
159+
private OAuth2AuthorizationCodeAuthenticationToken convert(HttpServletRequest request) {
160+
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
161+
return new OAuth2AuthorizationCodeAuthenticationToken(
162+
request.getParameter(OAuth2ParameterNames.CODE),
163+
clientPrincipal,
164+
request.getParameter(OAuth2ParameterNames.REDIRECT_URI)
165+
);
166+
}
167+
168+
private void writeSuccessResponse(HttpServletResponse response, OAuth2AccessToken body) throws IOException {
169+
try (Writer out = response.getWriter()) {
170+
response.setStatus(HttpStatus.OK.value());
171+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
172+
response.setCharacterEncoding("UTF-8");
173+
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
174+
response.setHeader(HttpHeaders.PRAGMA, "no-cache");
175+
out.write(objectMapper.writeValueAsString(body));
176+
}
177+
}
42178

179+
private void writeFailureResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
180+
try (Writer out = response.getWriter()) {
181+
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_CLIENT)) {
182+
response.setStatus(HttpStatus.UNAUTHORIZED.value());
183+
} else {
184+
response.setStatus(HttpStatus.BAD_REQUEST.value());
185+
}
186+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
187+
response.setCharacterEncoding("UTF-8");
188+
out.write(objectMapper.writeValueAsString(error));
189+
}
43190
}
44191
}

0 commit comments

Comments
 (0)