14 Commits
0.3.0 ... 0.3.1

Author SHA1 Message Date
Joe Grandja
931aaa4e94 Release 0.3.1 2022-06-17 11:56:51 -04:00
Daniel Garnier-Moiroux
ec7ab5c956 Add authenticationDetailsSource to AuthorizationEndpointFilter
Closes gh-768
2022-06-16 16:27:39 -04:00
Joe Grandja
fdf0a2f94c Access token is available when customizing ID Token
Closes gh-744
2022-06-16 10:38:09 -04:00
Joe Grandja
b37d4dd31e Describe error message when redirect_uri contains localhost
Closes gh-680
2022-06-15 17:38:54 -04:00
Joe Grandja
4199ab0172 Unsupported code_challenge_method parameter should return invalid_grant
Issue gh-770
2022-06-15 09:24:42 -04:00
Joe Grandja
7dfdcf3a27 Validate code_challenge_method parameter
Issue gh-756

Closes gh-770
2022-06-15 09:09:05 -04:00
Joe Grandja
0cae3c693e OpenID Provider Configuration response returns introspection_endpoint
Closes gh-779
2022-06-10 12:04:31 -04:00
Gyeongwon, Do
d6ff0f3fc7 Add token revocation endpoint to OIDC Provider Configuration endpoint
Closes gh-687
2022-06-10 11:47:22 -04:00
Joe Grandja
c75b8a1cb9 Build PR with Java 8
Issue gh-761
2022-06-10 10:45:53 -04:00
Steve Riesenberg
77d665fe97 Build with Java 8, 11 or 17
Issue gh-761
2022-06-08 18:08:41 -05:00
Steve Riesenberg
dc79172a4b Downgrade to hsqldb 2.5.2
Closes gh-771
2022-06-08 17:40:56 -05:00
Steve Riesenberg
e204b6bced Downgrade to Java 8
Closes gh-761
2022-06-08 17:40:33 -05:00
Joe Grandja
d46bdfc80b Update link to feature list 2022-05-25 16:24:17 -04:00
Steve Riesenberg
421a9723ea Next Development Version 2022-05-25 10:45:22 -05:00
30 changed files with 277 additions and 133 deletions

View File

@@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
jdk: [11]
jdk: [8,11,17]
fail-fast: false
runs-on: ${{ matrix.os }}
if: needs.prerequisites.outputs.runjobs
@@ -70,7 +70,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 11
java-version: 8
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Snapshot Tests
@@ -90,7 +90,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 11
java-version: 8
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Deploy Artifacts
@@ -114,7 +114,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 11
java-version: 8
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Deploy Docs

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
jdk: [11]
jdk: [8]
fail-fast: false
steps:
- uses: actions/checkout@v2

View File

@@ -13,7 +13,7 @@ This project uses https://www.zenhub.com/[ZenHub] to prioritize the feature road
The project board can be accessed https://app.zenhub.com/workspaces/authorization-server-5e8f3182b5e8f5841bfc4902/board?repos=248032165[here].
It is recommended to install the ZenHub https://www.zenhub.com/extension[browser extension] as it integrates natively within GitHub's user interface.
The completed and upcoming feature list can be viewed in the https://github.com/spring-projects/spring-authorization-server/wiki/Feature-List[wiki].
The feature list can be viewed in the https://docs.spring.io/spring-authorization-server/docs/current/reference/html/overview.html#feature-list[reference documentation].
== Support Policy
The Spring Authorization Server project provides software support and is documented in its link:SUPPORT_POLICY.adoc[support policy].
@@ -52,9 +52,9 @@ In the instructions below, https://vimeo.com/34436402[`./gradlew`] is invoked fr
a cross-platform, self-contained bootstrap mechanism for the build.
=== Prerequisites
https://help.github.com/set-up-git-redirect[Git] and the https://www.oracle.com/technetwork/java/javase/downloads[JDK11 build].
https://help.github.com/set-up-git-redirect[Git] and the https://www.oracle.com/technetwork/java/javase/downloads[JDK8 build].
Be sure that your `JAVA_HOME` environment variable points to the `jdk11` folder extracted from the JDK download.
Be sure that your `JAVA_HOME` environment variable points to the `jdk1.8.0` folder extracted from the JDK download.
=== Check out sources
[indent=0]

View File

@@ -4,7 +4,7 @@ plugins {
id "groovy"
}
sourceCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
repositories {
gradlePluginPortal()

View File

@@ -73,14 +73,16 @@ public class SpringJavaPlugin implements Plugin<Project> {
// Apply Java source compatibility version
JavaPluginExtension java = project.getExtensions().getByType(JavaPluginExtension.class);
java.setTargetCompatibility(JavaVersion.VERSION_11);
java.setTargetCompatibility(JavaVersion.VERSION_1_8);
// Configure Java tasks
project.getTasks().withType(JavaCompile.class, (javaCompile) -> {
CompileOptions options = javaCompile.getOptions();
options.setEncoding("UTF-8");
options.getCompilerArgs().add("-parameters");
options.getRelease().set(11);
if (JavaVersion.current().isJava11Compatible()) {
options.getRelease().set(8);
}
});
project.getTasks().withType(Jar.class, (jar) -> jar.manifest((manifest) -> {
Map<String, String> attributes = new HashMap<>();

View File

@@ -54,7 +54,7 @@ public class SpringJavadocApiPlugin implements Plugin<Project> {
api.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
if (JavaVersion.current().isJava11Compatible()) {
if (JavaVersion.current().isJava8Compatible()) {
project.copy((copy) -> copy.from(api.getDestinationDir())
.into(api.getDestinationDir())
.include("element-list")

View File

@@ -30,8 +30,6 @@ public class SpringJavadocOptionsPlugin implements Plugin<Project> {
project.getTasks().withType(Javadoc.class, (javadoc) -> {
StandardJavadocDocletOptions options = (StandardJavadocDocletOptions) javadoc.getOptions();
options.addStringOption("Xdoclint:none", "-quiet");
// Workaround for Java 11 javadoc search bug. Can be removed with Java 17.
options.addBooleanOption("-no-module-directories", true);
});
}
}

View File

@@ -19,6 +19,6 @@ dependencies {
api "com.squareup.okhttp3:mockwebserver:4.9.3"
api "com.squareup.okhttp3:okhttp:4.9.3"
api "com.jayway.jsonpath:json-path:2.7.0"
api "org.hsqldb:hsqldb:2.6.1"
api "org.hsqldb:hsqldb:2.5.2"
}
}

View File

@@ -4,7 +4,7 @@ plugins {
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "11"
sourceCompatibility = "1.8"
repositories {
mavenCentral()

View File

@@ -6,7 +6,7 @@ If you are just getting started with Spring Authorization Server, the following
[[system-requirements]]
== System Requirements
Spring Authorization Server requires a Java 11 or higher Runtime Environment.
Spring Authorization Server requires a Java 8 or higher Runtime Environment.
[[installing-spring-authorization-server]]
== Installing Spring Authorization Server

View File

@@ -1,5 +1,5 @@
version=0.3.0
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
version=0.3.1
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
springFrameworkVersion=5.3.20

View File

@@ -24,7 +24,7 @@ package org.springframework.security.oauth2.core;
public final class Version {
private static final int MAJOR = 0;
private static final int MINOR = 3;
private static final int PATCH = 0;
private static final int PATCH = 1;
/**
* Global Serialization value for Spring Security Authorization Server classes.

View File

@@ -122,9 +122,10 @@ final class CodeVerifierAuthenticator {
} catch (NoSuchAlgorithmException ex) {
// It is unlikely that SHA-256 is not available on the server. If it is not available,
// there will likely be bigger issues as well. We default to SERVER_ERROR.
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
}
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
return false;
}
private static void throwInvalidGrant(String parameterName) {

View File

@@ -179,7 +179,12 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
// ----- ID token -----
OidcIdToken idToken;
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).build();
// @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,

View File

@@ -209,11 +209,9 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
if (StringUtils.hasText(codeChallenge)) {
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod)) {
if (!"S256".equals(codeChallengeMethod)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
} else if (registeredClient.getClientSettings().isRequireProofKey()) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
@@ -477,69 +475,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
return true;
}
private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) {
UriComponents requestedRedirect;
try {
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
if (requestedRedirect.getFragment() != null) {
return false;
}
} catch (Exception ex) {
return false;
}
String requestedRedirectHost = requestedRedirect.getHost();
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#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.
return false;
}
if (!isLoopbackAddress(requestedRedirectHost)) {
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7
// When comparing client redirect URIs against pre-registered URIs,
// authorization servers MUST utilize exact string matching.
return registeredClient.getRedirectUris().contains(requestedRedirectUri);
}
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#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
// system at the time of the request.
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
registeredRedirect.port(requestedRedirect.getPort());
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
return true;
}
}
return false;
}
private static boolean isLoopbackAddress(String host) {
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
return true;
}
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
String[] ipv4Octets = host.split("\\.");
if (ipv4Octets.length != 4) {
return false;
}
try {
int[] address = new int[ipv4Octets.length];
for (int i=0; i < ipv4Octets.length; i++) {
address[i] = Integer.parseInt(ipv4Octets[i]);
}
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
} catch (NumberFormatException ex) {
return false;
}
}
private static boolean isPrincipalAuthenticated(Authentication principal) {
return principal != null &&
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
@@ -562,9 +497,16 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
private static void throwError(String errorCode, String parameterName, String errorUri,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
private static void throwError(OAuth2Error error, String parameterName,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
boolean redirectOnError = true;
if (errorCode.equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
parameterName.equals(OAuth2ParameterNames.STATE))) {
@@ -589,7 +531,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
}
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
}
@@ -639,16 +580,95 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
authenticationContext.getAuthentication();
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
if (StringUtils.hasText(requestedRedirectUri)) {
// ***** redirect_uri is available in authorization request
UriComponents requestedRedirect = null;
try {
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
} catch (Exception ex) { }
if (requestedRedirect == null || requestedRedirect.getFragment() != null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
} else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
registeredClient.getRedirectUris().size() != 1) {
// redirect_uri is REQUIRED for OpenID Connect
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
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
// 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.
OAuth2Error error = new OAuth2Error(
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");
throwError(error, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
if (!isLoopbackAddress(requestedRedirectHost)) {
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7
// When comparing client redirect URIs against pre-registered URIs,
// authorization servers MUST utilize exact string matching.
if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
} else {
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#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
// system at the time of the request.
boolean validRedirectUri = false;
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
registeredRedirect.port(requestedRedirect.getPort());
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
validRedirectUri = true;
break;
}
}
if (!validRedirectUri) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
}
} else {
// ***** redirect_uri is NOT available in authorization request
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
registeredClient.getRedirectUris().size() != 1) {
// redirect_uri is REQUIRED for OpenID Connect
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
}
}
private static boolean isLoopbackAddress(String host) {
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
return true;
}
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
String[] ipv4Octets = host.split("\\.");
if (ipv4Octets.length != 4) {
return false;
}
try {
int[] address = new int[ipv4Octets.length];
for (int i=0; i < ipv4Octets.length; i++) {
address[i] = Integer.parseInt(ipv4Octets[i]);
}
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
} catch (NumberFormatException ex) {
return false;
}
}

View File

@@ -176,7 +176,12 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
// ----- ID token -----
OidcIdToken idToken;
if (authorizedScopes.contains(OidcScopes.OPENID)) {
tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).build();
// @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,

View File

@@ -93,6 +93,10 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
.tokenRevocationEndpoint(asUrl(issuer, this.providerSettings.getTokenRevocationEndpoint()))
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
.tokenIntrospectionEndpoint(asUrl(issuer, this.providerSettings.getTokenIntrospectionEndpoint()))
.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
.subjectType("public")
.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
.scope(OidcScopes.OPENID)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -45,6 +46,7 @@ import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.RedirectUrlBuilder;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
@@ -82,6 +84,7 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
private final AuthenticationManager authenticationManager;
private final RequestMatcher authorizationEndpointMatcher;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAuthorizationResponse;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
@@ -144,6 +147,7 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
try {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request);
authorizationCodeRequestAuthentication.setDetails(this.authenticationDetailsSource.buildDetails(request));
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication);
@@ -169,6 +173,17 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
}
}
/**
* Sets the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}.
*
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}
* @since 0.3.1
*/
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,24 +48,29 @@ public class TestOAuth2Authorizations {
}
public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
OAuth2AccessToken accessToken, Map<String, Object> accessTokenClaims) {
return authorization(registeredClient, accessToken, accessTokenClaims, Collections.emptyMap());
}
public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
Map<String, Object> authorizationRequestAdditionalParameters) {
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300));
return authorization(registeredClient, accessToken, Collections.emptyMap(), authorizationRequestAdditionalParameters);
}
private static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
OAuth2AccessToken accessToken, Map<String, Object> accessTokenClaims,
Map<String, Object> authorizationRequestAdditionalParameters) {
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
"code", Instant.now(), Instant.now().plusSeconds(120));
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
"refresh-token", Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS));
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300));
return authorization(registeredClient, authorizationCode, accessToken, Collections.emptyMap(), authorizationRequestAdditionalParameters);
}
public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
OAuth2AuthorizationCode authorizationCode) {
return authorization(registeredClient, authorizationCode, null, Collections.emptyMap(), Collections.emptyMap());
}
public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
OAuth2AccessToken accessToken, Map<String, Object> accessTokenClaims) {
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
"code", Instant.now(), Instant.now().plusSeconds(120));
return authorization(registeredClient, authorizationCode, accessToken, accessTokenClaims, Collections.emptyMap());
}
private static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
OAuth2AuthorizationCode authorizationCode, OAuth2AccessToken accessToken,
Map<String, Object> accessTokenClaims, Map<String, Object> authorizationRequestAdditionalParameters) {
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://provider.com/oauth2/authorize")
.clientId(registeredClient.getClientId())
@@ -74,18 +79,25 @@ public class TestOAuth2Authorizations {
.additionalParameters(authorizationRequestAdditionalParameters)
.state("state")
.build();
return OAuth2Authorization.withRegisteredClient(registeredClient)
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
.id("id")
.principalName("principal")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.token(authorizationCode)
.token(accessToken, (metadata) -> metadata.putAll(tokenMetadata(accessTokenClaims)))
.refreshToken(refreshToken)
.attribute(OAuth2ParameterNames.STATE, "state")
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
.attribute(Principal.class.getName(),
new TestingAuthenticationToken("principal", null, "ROLE_A", "ROLE_B"))
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes());
if (accessToken != null) {
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
"refresh-token", Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS));
builder
.token(accessToken, (metadata) -> metadata.putAll(tokenMetadata(accessTokenClaims)))
.refreshToken(refreshToken);
}
return builder;
}
private static Map<String, Object> tokenMetadata(Map<String, Object> tokenClaims) {

View File

@@ -443,7 +443,9 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
@Test
public void authenticateWhenValidCodeAndAuthenticationRequestThenReturnIdToken() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
"code", Instant.now(), Instant.now().plusSeconds(120));
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient, authorizationCode).build();
when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
.thenReturn(authorization);
@@ -466,6 +468,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(accessTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(accessTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(accessTokenContext.getAuthorization()).isEqualTo(authorization);
assertThat(accessTokenContext.getAuthorization().getAccessToken()).isNull();
assertThat(accessTokenContext.getAuthorizedScopes())
.isEqualTo(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME));
assertThat(accessTokenContext.getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN);
@@ -481,7 +484,8 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1);
assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(idTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(idTokenContext.getAuthorization()).isEqualTo(authorization);
assertThat(idTokenContext.getAuthorization()).isNotEqualTo(authorization);
assertThat(idTokenContext.getAuthorization().getAccessToken()).isNotNull();
assertThat(idTokenContext.getAuthorizedScopes())
.isEqualTo(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME));
assertThat(idTokenContext.getTokenType().getValue()).isEqualTo(OidcParameterNames.ID_TOKEN);
@@ -503,8 +507,8 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(accessTokenAuthentication.getAccessToken().getScopes()).isEqualTo(accessTokenScopes);
assertThat(accessTokenAuthentication.getRefreshToken()).isNotNull();
assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getRefreshToken().getToken());
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
assertThat(authorizationCode.isInvalidated()).isTrue();
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCodeToken = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
assertThat(authorizationCodeToken.isInvalidated()).isTrue();
OAuth2Authorization.Token<OidcIdToken> idToken = updatedAuthorization.getToken(OidcIdToken.class);
assertThat(idToken).isNotNull();
assertThat(accessTokenAuthentication.getAdditionalParameters())

View File

@@ -207,7 +207,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
);
)
.extracting(ex -> ((OAuth2AuthorizationCodeRequestAuthenticationException) ex).getError())
.satisfies(error ->
assertThat(error.getDescription()).isEqualTo("localhost is not allowed for the redirect_uri (https://localhost:5000). Use the IP literal (127.0.0.1) instead."));
}
@Test
@@ -377,6 +380,26 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
);
}
// gh-770
@Test
public void authenticateWhenPkceMissingCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.additionalParameters(additionalParameters)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies(ex ->
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, authentication.getRedirectUri())
);
}
@Test
public void authenticateWhenPrincipalNotAuthenticatedThenReturnAuthorizationCodeRequest() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

View File

@@ -233,7 +233,8 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1);
assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(idTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(idTokenContext.getAuthorization()).isEqualTo(authorization);
assertThat(idTokenContext.getAuthorization()).isNotEqualTo(authorization);
assertThat(idTokenContext.getAuthorization().getAccessToken()).isNotEqualTo(authorization.getAccessToken());
assertThat(idTokenContext.getAuthorizedScopes())
.isEqualTo(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME));
assertThat(idTokenContext.getTokenType().getValue()).isEqualTo(OidcParameterNames.ID_TOKEN);

View File

@@ -275,7 +275,7 @@ public class PublicClientAuthenticationProviderTests {
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
}
private static Map<String, Object> createAuthorizationCodeTokenParameters() {

View File

@@ -95,6 +95,8 @@ public class OidcProviderConfigurationEndpointFilterTests {
String tokenEndpoint = "/oauth2/v1/token";
String jwkSetEndpoint = "/oauth2/v1/jwks";
String userInfoEndpoint = "/userinfo";
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
ProviderSettings providerSettings = ProviderSettings.builder()
.issuer(issuer)
@@ -102,6 +104,8 @@ public class OidcProviderConfigurationEndpointFilterTests {
.tokenEndpoint(tokenEndpoint)
.jwkSetEndpoint(jwkSetEndpoint)
.oidcUserInfoEndpoint(userInfoEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
.build();
ProviderContextHolder.setProviderContext(new ProviderContext(providerSettings, null));
OidcProviderConfigurationEndpointFilter filter =
@@ -126,6 +130,10 @@ public class OidcProviderConfigurationEndpointFilterTests {
assertThat(providerConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
assertThat(providerConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\"");
assertThat(providerConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
assertThat(providerConfigurationResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/v1/introspect\"");
assertThat(providerConfigurationResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/issuer1/userinfo\"");

View File

@@ -32,10 +32,12 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -55,10 +57,12 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
@@ -78,6 +82,7 @@ import static org.mockito.Mockito.when;
*/
public class OAuth2AuthorizationEndpointFilterTests {
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private static final String REMOTE_ADDRESS = "remote-address";
private AuthenticationManager authenticationManager;
private OAuth2AuthorizationEndpointFilter filter;
private TestingAuthenticationToken principal;
@@ -116,6 +121,13 @@ public class OAuth2AuthorizationEndpointFilterTests {
.hasMessage("authorizationEndpointUri cannot be empty");
}
@Test
public void setAuthenticationDetailsSourceWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationDetailsSource(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationDetailsSource cannot be null");
}
@Test
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null))
@@ -364,6 +376,32 @@ public class OAuth2AuthorizationEndpointFilterTests {
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), same(authenticationException));
}
@Test
public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal).build();
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource =
mock(AuthenticationDetailsSource.class);
WebAuthenticationDetails webAuthenticationDetails = new WebAuthenticationDetails(request);
when(authenticationDetailsSource.buildDetails(request)).thenReturn(webAuthenticationDetails);
this.filter.setAuthenticationDetailsSource(authenticationDetailsSource);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authorizationCodeRequestAuthentication);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationDetailsSource).buildDetails(any());
verify(this.authenticationManager).authenticate(any());
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestPrincipalNotAuthenticatedThenCommenceAuthentication() throws Exception {
this.principal.setAuthenticated(false);
@@ -507,9 +545,15 @@ public class OAuth2AuthorizationEndpointFilterTests {
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
ArgumentCaptor<OAuth2AuthorizationCodeRequestAuthenticationToken> authorizationCodeRequestAuthenticationCaptor =
ArgumentCaptor.forClass(OAuth2AuthorizationCodeRequestAuthenticationToken.class);
verify(this.authenticationManager).authenticate(authorizationCodeRequestAuthenticationCaptor.capture());
verifyNoInteractions(filterChain);
assertThat(authorizationCodeRequestAuthenticationCaptor.getValue().getDetails())
.asInstanceOf(type(WebAuthenticationDetails.class))
.extracting(WebAuthenticationDetails::getRemoteAddress)
.isEqualTo(REMOTE_ADDRESS);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?code=code&state=state");
}
@@ -578,6 +622,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
String requestUri = DEFAULT_AUTHORIZATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.setRemoteAddr(REMOTE_ADDRESS);
request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
@@ -593,6 +638,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
String requestUri = DEFAULT_AUTHORIZATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.setRemoteAddr(REMOTE_ADDRESS);
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
registeredClient.getScopes().forEach((scope) -> request.addParameter(OAuth2ParameterNames.SCOPE, scope));

View File

@@ -6,7 +6,7 @@ plugins {
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "11"
sourceCompatibility = "1.8"
repositories {
mavenCentral()

View File

@@ -6,7 +6,7 @@ plugins {
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "11"
sourceCompatibility = "1.8"
repositories {
mavenCentral()

View File

@@ -6,7 +6,7 @@ plugins {
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "11"
sourceCompatibility = "1.8"
repositories {
mavenCentral()

View File

@@ -6,7 +6,7 @@ plugins {
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "11"
sourceCompatibility = "1.8"
repositories {
mavenCentral()

View File

@@ -6,7 +6,7 @@ plugins {
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "11"
sourceCompatibility = "1.8"
repositories {
mavenCentral()