|
16 | 16 |
|
17 | 17 | package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource; |
18 | 18 |
|
19 | | -import java.util.Collections; |
20 | | -import java.util.LinkedHashMap; |
21 | | -import java.util.List; |
22 | | -import java.util.Map; |
23 | | -import java.util.regex.Matcher; |
24 | | -import java.util.regex.Pattern; |
25 | | - |
26 | 19 | import jakarta.servlet.http.HttpServletRequest; |
27 | | -import jakarta.servlet.http.HttpServletResponse; |
28 | 20 |
|
29 | | -import org.springframework.http.HttpHeaders; |
30 | | -import org.springframework.http.HttpStatus; |
31 | 21 | import org.springframework.security.authentication.AuthenticationManager; |
32 | 22 | import org.springframework.security.authentication.AuthenticationManagerResolver; |
33 | 23 | import org.springframework.security.config.annotation.web.HttpSecurityBuilder; |
34 | 24 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
35 | | -import org.springframework.security.core.Authentication; |
36 | | -import org.springframework.security.core.AuthenticationException; |
37 | | -import org.springframework.security.oauth2.core.OAuth2AccessToken; |
38 | | -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
39 | | -import org.springframework.security.oauth2.core.OAuth2Error; |
40 | | -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; |
41 | | -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
42 | | -import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; |
| 25 | +import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationConverter; |
43 | 26 | import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider; |
44 | | -import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken; |
45 | | -import org.springframework.security.web.AuthenticationEntryPoint; |
| 27 | +import org.springframework.security.oauth2.server.resource.web.DPoPAuthenticationEntryPoint; |
| 28 | +import org.springframework.security.oauth2.server.resource.web.DPoPRequestMatcher; |
46 | 29 | import org.springframework.security.web.authentication.AuthenticationConverter; |
47 | 30 | import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler; |
48 | 31 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
49 | 32 | import org.springframework.security.web.authentication.AuthenticationFilter; |
50 | 33 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
51 | 34 | import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; |
52 | 35 | import org.springframework.security.web.util.matcher.RequestMatcher; |
53 | | -import org.springframework.util.CollectionUtils; |
54 | | -import org.springframework.util.StringUtils; |
| 36 | +import org.springframework.util.Assert; |
55 | 37 | import org.springframework.web.context.request.RequestAttributes; |
56 | 38 | import org.springframework.web.context.request.RequestContextHolder; |
57 | 39 | import org.springframework.web.context.request.ServletRequestAttributes; |
|
61 | 43 | * (DPoP) support. |
62 | 44 | * |
63 | 45 | * @author Joe Grandja |
| 46 | + * @author Max Batischev |
64 | 47 | * @since 6.5 |
65 | 48 | * @see DPoPAuthenticationProvider |
66 | 49 | * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449 |
67 | 50 | * OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a> |
68 | 51 | */ |
69 | | -final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>> |
| 52 | +public final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>> |
70 | 53 | extends AbstractHttpConfigurer<DPoPAuthenticationConfigurer<B>, B> { |
71 | 54 |
|
72 | 55 | private RequestMatcher requestMatcher; |
@@ -108,6 +91,50 @@ private AuthenticationManager getTokenAuthenticationManager(B http) { |
108 | 91 | }; |
109 | 92 | } |
110 | 93 |
|
| 94 | + /** |
| 95 | + * Sets the {@link RequestMatcher} to use. |
| 96 | + * @param requestMatcher |
| 97 | + * @since 7.0 |
| 98 | + */ |
| 99 | + public DPoPAuthenticationConfigurer<B> requestMatcher(RequestMatcher requestMatcher) { |
| 100 | + Assert.notNull(requestMatcher, "requestMatcher cannot be null"); |
| 101 | + this.requestMatcher = requestMatcher; |
| 102 | + return this; |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * Sets the {@link AuthenticationConverter} to use. |
| 107 | + * @param authenticationConverter |
| 108 | + * @since 7.0 |
| 109 | + */ |
| 110 | + public DPoPAuthenticationConfigurer<B> authenticationConverter(AuthenticationConverter authenticationConverter) { |
| 111 | + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); |
| 112 | + this.authenticationConverter = authenticationConverter; |
| 113 | + return this; |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Sets the {@link AuthenticationFailureHandler} to use. |
| 118 | + * @param failureHandler |
| 119 | + * @since 7.0 |
| 120 | + */ |
| 121 | + public DPoPAuthenticationConfigurer<B> failureHandler(AuthenticationFailureHandler failureHandler) { |
| 122 | + Assert.notNull(failureHandler, "failureHandler cannot be null"); |
| 123 | + this.authenticationFailureHandler = failureHandler; |
| 124 | + return this; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Sets the {@link AuthenticationSuccessHandler} to use. |
| 129 | + * @param successHandler |
| 130 | + * @since 7.0 |
| 131 | + */ |
| 132 | + public DPoPAuthenticationConfigurer<B> successHandler(AuthenticationSuccessHandler successHandler) { |
| 133 | + Assert.notNull(successHandler, "successHandler cannot be null"); |
| 134 | + this.authenticationSuccessHandler = successHandler; |
| 135 | + return this; |
| 136 | + } |
| 137 | + |
111 | 138 | private RequestMatcher getRequestMatcher() { |
112 | 139 | if (this.requestMatcher == null) { |
113 | 140 | this.requestMatcher = new DPoPRequestMatcher(); |
@@ -139,101 +166,4 @@ private AuthenticationFailureHandler getAuthenticationFailureHandler() { |
139 | 166 | return this.authenticationFailureHandler; |
140 | 167 | } |
141 | 168 |
|
142 | | - private static final class DPoPRequestMatcher implements RequestMatcher { |
143 | | - |
144 | | - @Override |
145 | | - public boolean matches(HttpServletRequest request) { |
146 | | - String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); |
147 | | - if (!StringUtils.hasText(authorization)) { |
148 | | - return false; |
149 | | - } |
150 | | - return StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue()); |
151 | | - } |
152 | | - |
153 | | - } |
154 | | - |
155 | | - private static final class DPoPAuthenticationConverter implements AuthenticationConverter { |
156 | | - |
157 | | - private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^DPoP (?<token>[a-zA-Z0-9-._~+/]+=*)$", |
158 | | - Pattern.CASE_INSENSITIVE); |
159 | | - |
160 | | - @Override |
161 | | - public Authentication convert(HttpServletRequest request) { |
162 | | - List<String> authorizationList = Collections.list(request.getHeaders(HttpHeaders.AUTHORIZATION)); |
163 | | - if (CollectionUtils.isEmpty(authorizationList)) { |
164 | | - return null; |
165 | | - } |
166 | | - if (authorizationList.size() != 1) { |
167 | | - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, |
168 | | - "Found multiple Authorization headers.", null); |
169 | | - throw new OAuth2AuthenticationException(error); |
170 | | - } |
171 | | - String authorization = authorizationList.get(0); |
172 | | - if (!StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue())) { |
173 | | - return null; |
174 | | - } |
175 | | - Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization); |
176 | | - if (!matcher.matches()) { |
177 | | - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "DPoP access token is malformed.", |
178 | | - null); |
179 | | - throw new OAuth2AuthenticationException(error); |
180 | | - } |
181 | | - String accessToken = matcher.group("token"); |
182 | | - List<String> dPoPProofList = Collections |
183 | | - .list(request.getHeaders(OAuth2AccessToken.TokenType.DPOP.getValue())); |
184 | | - if (CollectionUtils.isEmpty(dPoPProofList) || dPoPProofList.size() != 1) { |
185 | | - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, |
186 | | - "DPoP proof is missing or invalid.", null); |
187 | | - throw new OAuth2AuthenticationException(error); |
188 | | - } |
189 | | - String dPoPProof = dPoPProofList.get(0); |
190 | | - return new DPoPAuthenticationToken(accessToken, dPoPProof, request.getMethod(), |
191 | | - request.getRequestURL().toString()); |
192 | | - } |
193 | | - |
194 | | - } |
195 | | - |
196 | | - private static final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint { |
197 | | - |
198 | | - @Override |
199 | | - public void commence(HttpServletRequest request, HttpServletResponse response, |
200 | | - AuthenticationException authenticationException) { |
201 | | - Map<String, String> parameters = new LinkedHashMap<>(); |
202 | | - if (authenticationException instanceof OAuth2AuthenticationException oauth2AuthenticationException) { |
203 | | - OAuth2Error error = oauth2AuthenticationException.getError(); |
204 | | - parameters.put(OAuth2ParameterNames.ERROR, error.getErrorCode()); |
205 | | - if (StringUtils.hasText(error.getDescription())) { |
206 | | - parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription()); |
207 | | - } |
208 | | - if (StringUtils.hasText(error.getUri())) { |
209 | | - parameters.put(OAuth2ParameterNames.ERROR_URI, error.getUri()); |
210 | | - } |
211 | | - } |
212 | | - parameters.put("algs", |
213 | | - JwsAlgorithms.RS256 + " " + JwsAlgorithms.RS384 + " " + JwsAlgorithms.RS512 + " " |
214 | | - + JwsAlgorithms.PS256 + " " + JwsAlgorithms.PS384 + " " + JwsAlgorithms.PS512 + " " |
215 | | - + JwsAlgorithms.ES256 + " " + JwsAlgorithms.ES384 + " " + JwsAlgorithms.ES512); |
216 | | - String wwwAuthenticate = toWWWAuthenticateHeader(parameters); |
217 | | - response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); |
218 | | - response.setStatus(HttpStatus.UNAUTHORIZED.value()); |
219 | | - } |
220 | | - |
221 | | - private static String toWWWAuthenticateHeader(Map<String, String> parameters) { |
222 | | - StringBuilder wwwAuthenticate = new StringBuilder(); |
223 | | - wwwAuthenticate.append(OAuth2AccessToken.TokenType.DPOP.getValue()); |
224 | | - if (!parameters.isEmpty()) { |
225 | | - wwwAuthenticate.append(" "); |
226 | | - int i = 0; |
227 | | - for (Map.Entry<String, String> entry : parameters.entrySet()) { |
228 | | - wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\""); |
229 | | - if (i++ != parameters.size() - 1) { |
230 | | - wwwAuthenticate.append(", "); |
231 | | - } |
232 | | - } |
233 | | - } |
234 | | - return wwwAuthenticate.toString(); |
235 | | - } |
236 | | - |
237 | | - } |
238 | | - |
239 | 169 | } |
0 commit comments