diff --git a/README.adoc b/README.adoc index ae202d74..fc3befa2 100644 --- a/README.adoc +++ b/README.adoc @@ -4,7 +4,7 @@ image:https://github.com/spring-projects/spring-authorization-server/workflows/C = Spring Authorization Server -The Spring Authorization Server project, led by the https://spring.io/projects/spring-security/[Spring Security] team, is focused on delivering https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-06#section-1.1[OAuth 2.1 Authorization Server] support to the Spring community. +The Spring Authorization Server project, led by the https://spring.io/projects/spring-security/[Spring Security] team, is focused on delivering https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-1.1[OAuth 2.1 Authorization Server] support to the Spring community. This project replaces the Authorization Server support provided by https://spring.io/projects/spring-security-oauth/[Spring Security OAuth]. @@ -20,7 +20,7 @@ The Spring Authorization Server project provides software support through the ht https://tanzu.vmware.com/spring-runtime[Commercial support], which offers an extended support period, is also available from VMware. == Getting Started -The first place to start is to read the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01[OAuth 2.1 Authorization Framework] to gain an in-depth understanding on how to build an Authorization Server. +The first place to start is to read the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[OAuth 2.1 Authorization Framework] to gain an in-depth understanding on how to build an Authorization Server. It is a critically important first step as the implementation must conform to the specification defined in the OAuth 2.1 Authorization Framework and the https://github.com/spring-projects/spring-authorization-server/wiki/OAuth-2.0-Specifications[related specifications]. The second place to start is to become very familiar with the codebase in the following Spring Security modules: diff --git a/docs/src/docs/asciidoc/overview.adoc b/docs/src/docs/asciidoc/overview.adoc index 641c223f..e85ba590 100644 --- a/docs/src/docs/asciidoc/overview.adoc +++ b/docs/src/docs/asciidoc/overview.adoc @@ -6,7 +6,7 @@ This site contains reference documentation and how-to guides for Spring Authoriz [[introducing-spring-authorization-server]] == Introducing Spring Authorization Server -Spring Authorization Server is a framework that provides implementations of the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05[OAuth 2.1] and https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect 1.0] specifications and other related specifications. +Spring Authorization Server is a framework that provides implementations of the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[OAuth 2.1] and https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect 1.0] specifications and other related specifications. It is built on top of https://spring.io/projects/spring-security[Spring Security] to provide a secure, light-weight, and customizable foundation for building OpenID Connect 1.0 Identity Providers and OAuth2 Authorization Server products. [[feature-list]] @@ -25,10 +25,10 @@ Spring Authorization Server supports the following features: * Client Credentials * Refresh Token | -* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05[draft]) -** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-4.1[Authorization Code Grant] -** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-4.2[Client Credentials Grant] -** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-4.3[Refresh Token Grant] +* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[draft]) +** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-4.1[Authorization Code Grant] +** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-4.2[Client Credentials Grant] +** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-4.3[Refresh Token Grant] * OpenID Connect Core 1.0 (https://openid.net/specs/openid-connect-core-1_0.html[spec]) ** https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[Authorization Code Flow] @@ -48,7 +48,7 @@ Spring Authorization Server supports the following features: * `private_key_jwt` * `none` (public clients) | -* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-2.4[Client Authentication]) +* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-2.4[Client Authentication]) * JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication (https://tools.ietf.org/html/rfc7523[RFC 7523]) * Proof Key for Code Exchange by OAuth Public Clients (PKCE) (https://tools.ietf.org/html/rfc7636[RFC 7636]) @@ -64,9 +64,9 @@ Spring Authorization Server supports the following features: * xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo Endpoint] * xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration Endpoint] | -* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05[draft]) -** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-3.1[Authorization Endpoint] -** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-3.2[Token Endpoint] +* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[draft]) +** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-3.1[Authorization Endpoint] +** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-3.2[Token Endpoint] * OAuth 2.0 Token Introspection (https://tools.ietf.org/html/rfc7662[RFC 7662]) * OAuth 2.0 Token Revocation (https://tools.ietf.org/html/rfc7009[RFC 7009]) * OAuth 2.0 Authorization Server Metadata (https://tools.ietf.org/html/rfc8414[RFC 8414]) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java index 43e1c237..742325b3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java @@ -102,7 +102,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator impleme String requestedRedirectHost = requestedRedirect.getHost(); if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1 + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7.1 // While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}") // function similarly to loopback IP redirects described in Section 10.3.3, // the use of "localhost" is NOT RECOMMENDED. @@ -110,13 +110,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator impleme OAuth2ErrorCodes.INVALID_REQUEST, "localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " + "Use the IP literal (127.0.0.1) instead.", - "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1"); + "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7.1"); throwError(error, OAuth2ParameterNames.REDIRECT_URI, authorizationCodeRequestAuthentication, registeredClient); } if (!isLoopbackAddress(requestedRedirectHost)) { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7 + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7 // When comparing client redirect URIs against pre-registered URIs, // authorization servers MUST utilize exact string matching. if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) { @@ -124,7 +124,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator impleme authorizationCodeRequestAuthentication, registeredClient); } } else { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3 + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-10.3.3 // The authorization server MUST allow any port to be specified at the // time of the request for loopback IP redirect URIs, to accommodate // clients that obtain an available ephemeral port from the operating diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java index 7c0a7707..6926314d 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java @@ -118,6 +118,7 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter this.authenticationDetailsSource.buildDetails(request)); } if (authenticationRequest != null) { + validateClientIdentifier(authenticationRequest); Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest); this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult); } @@ -201,4 +202,25 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter this.errorHttpResponseConverter.write(errorResponse, null, httpResponse); } + private static void validateClientIdentifier(Authentication authentication) { + if (!(authentication instanceof OAuth2ClientAuthenticationToken)) { + return; + } + + // As per spec, in Appendix A.1. + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#appendix-A.1 + // The syntax for client_id is *VSCHAR (%x20-7E): + // -> Hex 20 -> ASCII 32 -> space + // -> Hex 7E -> ASCII 126 -> tilde + + OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication; + String clientId = (String) clientAuthentication.getPrincipal(); + for (int i = 0; i < clientId.length(); i++) { + char charAt = clientId.charAt(i); + if (!(charAt >= 32 && charAt <= 126)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); + } + } + } + } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java index cdd0d67f..5ed5829a 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java @@ -15,6 +15,8 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import java.nio.charset.StandardCharsets; + import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -33,6 +35,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.codec.Hex; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -130,6 +133,7 @@ public class OAuth2ClientAuthenticationFilterTests { this.filter.doFilter(request, response, filterChain); verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + verifyNoInteractions(this.authenticationConverter); } @Test @@ -142,6 +146,7 @@ public class OAuth2ClientAuthenticationFilterTests { this.filter.doFilter(request, response, filterChain); verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + verifyNoInteractions(this.authenticationManager); } @Test @@ -164,6 +169,46 @@ public class OAuth2ClientAuthenticationFilterTests { assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); } + // gh-889 + @Test + public void doFilterWhenRequestMatchesAndClientIdContainsNonPrintableASCIIThenInvalidRequestError() throws Exception { + // Hex 00 -> null + String clientId = new String(Hex.decode("00"), StandardCharsets.UTF_8); + assertWhenInvalidClientIdThenInvalidRequestError(clientId); + + // Hex 0a61 -> line feed + a + clientId = new String(Hex.decode("0a61"), StandardCharsets.UTF_8); + assertWhenInvalidClientIdThenInvalidRequestError(clientId); + + // Hex 1b -> escape + clientId = new String(Hex.decode("1b"), StandardCharsets.UTF_8); + assertWhenInvalidClientIdThenInvalidRequestError(clientId); + + // Hex 1b61 -> escape + a + clientId = new String(Hex.decode("1b61"), StandardCharsets.UTF_8); + assertWhenInvalidClientIdThenInvalidRequestError(clientId); + } + + private void assertWhenInvalidClientIdThenInvalidRequestError(String clientId) throws Exception { + when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( + new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null)); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", this.filterProcessesUrl); + request.setServletPath(this.filterProcessesUrl); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verifyNoInteractions(filterChain); + verifyNoInteractions(this.authenticationManager); + + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + OAuth2Error error = readError(response); + assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); + } + @Test public void doFilterWhenRequestMatchesAndBadCredentialsThenInvalidClientError() throws Exception { when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( @@ -179,6 +224,7 @@ public class OAuth2ClientAuthenticationFilterTests { this.filter.doFilter(request, response, filterChain); verifyNoInteractions(filterChain); + verify(this.authenticationManager).authenticate(any()); assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());