diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilter.java index aa6cee69..21ef8bc0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -16,7 +16,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; import java.io.IOException; -import java.util.function.Supplier; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -28,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.context.Authoriz import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; @@ -42,10 +46,12 @@ import org.springframework.web.util.UriComponentsBuilder; */ final class AuthorizationServerContextFilter extends OncePerRequestFilter { private final AuthorizationServerSettings authorizationServerSettings; + private final IssuerResolver issuerResolver; AuthorizationServerContextFilter(AuthorizationServerSettings authorizationServerSettings) { Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null"); this.authorizationServerSettings = authorizationServerSettings; + this.issuerResolver = new IssuerResolver(authorizationServerSettings); } @Override @@ -53,10 +59,9 @@ final class AuthorizationServerContextFilter extends OncePerRequestFilter { throws ServletException, IOException { try { + String issuer = this.issuerResolver.resolve(request); AuthorizationServerContext authorizationServerContext = - new DefaultAuthorizationServerContext( - () -> resolveIssuer(this.authorizationServerSettings, request), - this.authorizationServerSettings); + new DefaultAuthorizationServerContext(issuer, this.authorizationServerSettings); AuthorizationServerContextHolder.setContext(authorizationServerContext); filterChain.doFilter(request, response); } finally { @@ -64,35 +69,69 @@ final class AuthorizationServerContextFilter extends OncePerRequestFilter { } } - private static String resolveIssuer(AuthorizationServerSettings authorizationServerSettings, HttpServletRequest request) { - return authorizationServerSettings.getIssuer() != null ? - authorizationServerSettings.getIssuer() : - getContextPath(request); - } + private static final class IssuerResolver { + private final String issuer; + private final Set endpointUris; + + private IssuerResolver(AuthorizationServerSettings authorizationServerSettings) { + if (authorizationServerSettings.getIssuer() != null) { + this.issuer = authorizationServerSettings.getIssuer(); + this.endpointUris = Collections.emptySet(); + } else { + this.issuer = null; + this.endpointUris = new HashSet<>(); + this.endpointUris.add("/.well-known/oauth-authorization-server"); + this.endpointUris.add("/.well-known/openid-configuration"); + for (Map.Entry setting : authorizationServerSettings.getSettings().entrySet()) { + if (setting.getKey().endsWith("-endpoint")) { + this.endpointUris.add((String) setting.getValue()); + } + } + } + } + + private String resolve(HttpServletRequest request) { + if (this.issuer != null) { + return this.issuer; + } + + // Resolve Issuer Identifier dynamically from request + String path = request.getRequestURI(); + if (!StringUtils.hasText(path)) { + path = ""; + } else { + for (String endpointUri : this.endpointUris) { + if (path.contains(endpointUri)) { + path = path.replace(endpointUri, ""); + break; + } + } + } + + // @formatter:off + return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(path) + .replaceQuery(null) + .fragment(null) + .build() + .toUriString(); + // @formatter:on + } - private static String getContextPath(HttpServletRequest request) { - // @formatter:off - return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) - .replacePath(request.getContextPath()) - .replaceQuery(null) - .fragment(null) - .build() - .toUriString(); - // @formatter:on } private static final class DefaultAuthorizationServerContext implements AuthorizationServerContext { - private final Supplier issuerSupplier; + private final String issuer; private final AuthorizationServerSettings authorizationServerSettings; - private DefaultAuthorizationServerContext(Supplier issuerSupplier, AuthorizationServerSettings authorizationServerSettings) { - this.issuerSupplier = issuerSupplier; + private DefaultAuthorizationServerContext(String issuer, AuthorizationServerSettings authorizationServerSettings) { + this.issuer = issuer; this.authorizationServerSettings = authorizationServerSettings; } @Override public String getIssuer() { - return this.issuerSupplier.get(); + return this.issuer; } @Override diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java index e1ae33e9..354b02b7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -51,6 +51,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for the OAuth 2.0 Authorization Endpoint. * @@ -209,13 +211,10 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C @Override void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); + String authorizationEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getAuthorizationEndpoint()); this.requestMatcher = new OrRequestMatcher( - new AntPathRequestMatcher( - authorizationServerSettings.getAuthorizationEndpoint(), - HttpMethod.GET.name()), - new AntPathRequestMatcher( - authorizationServerSettings.getAuthorizationEndpoint(), - HttpMethod.POST.name())); + new AntPathRequestMatcher(authorizationEndpointUri, HttpMethod.GET.name()), + new AntPathRequestMatcher(authorizationEndpointUri, HttpMethod.POST.name())); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); if (!this.authenticationProviders.isEmpty()) { @@ -234,7 +233,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C OAuth2AuthorizationEndpointFilter authorizationEndpointFilter = new OAuth2AuthorizationEndpointFilter( authenticationManager, - authorizationServerSettings.getAuthorizationEndpoint()); + withMultipleIssuerPattern(authorizationServerSettings.getAuthorizationEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.authorizationRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.authorizationRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java index 1b04ac15..ff0e7112 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -56,6 +56,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support. * @@ -314,7 +316,7 @@ public final class OAuth2AuthorizationServerConfigurer requestMatchers.add(configurer.getRequestMatcher()); }); requestMatchers.add(new AntPathRequestMatcher( - authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name())); + withMultipleIssuerPattern(authorizationServerSettings.getJwkSetEndpoint()), HttpMethod.GET.name())); this.endpointsMatcher = new OrRequestMatcher(requestMatchers); ExceptionHandlingConfigurer exceptionHandling = httpSecurity.getConfigurer(ExceptionHandlingConfigurer.class); @@ -342,7 +344,7 @@ public final class OAuth2AuthorizationServerConfigurer JWKSource jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity); if (jwkSource != null) { NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter( - jwkSource, authorizationServerSettings.getJwkSetEndpoint()); + jwkSource, withMultipleIssuerPattern(authorizationServerSettings.getJwkSetEndpoint())); httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java index 25c4fb39..ace3388f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -70,7 +70,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends A @Override void init(HttpSecurity httpSecurity) { this.requestMatcher = new AntPathRequestMatcher( - "/.well-known/oauth-authorization-server", HttpMethod.GET.name()); + "/.well-known/oauth-authorization-server/**", HttpMethod.GET.name()); } @Override diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java index 7e37982f..b4bc1f0e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -51,6 +51,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for OAuth 2.0 Client Authentication. * @@ -161,16 +163,16 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); this.requestMatcher = new OrRequestMatcher( new AntPathRequestMatcher( - authorizationServerSettings.getTokenEndpoint(), + withMultipleIssuerPattern(authorizationServerSettings.getTokenEndpoint()), HttpMethod.POST.name()), new AntPathRequestMatcher( - authorizationServerSettings.getTokenIntrospectionEndpoint(), + withMultipleIssuerPattern(authorizationServerSettings.getTokenIntrospectionEndpoint()), HttpMethod.POST.name()), new AntPathRequestMatcher( - authorizationServerSettings.getTokenRevocationEndpoint(), + withMultipleIssuerPattern(authorizationServerSettings.getTokenRevocationEndpoint()), HttpMethod.POST.name()), new AntPathRequestMatcher( - authorizationServerSettings.getDeviceAuthorizationEndpoint(), + withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()), HttpMethod.POST.name())); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java index 9d7ab7ad..2ff97378 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -43,6 +43,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Refr import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -56,6 +57,13 @@ final class OAuth2ConfigurerUtils { private OAuth2ConfigurerUtils() { } + static String withMultipleIssuerPattern(String endpointUri) { + Assert.hasText(endpointUri, "endpointUri cannot be empty"); + return endpointUri.startsWith("/") ? + "/**" + endpointUri : + "/**/" + endpointUri; + } + static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) { RegisteredClientRepository registeredClientRepository = httpSecurity.getSharedObject(RegisteredClientRepository.class); if (registeredClientRepository == null) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java index 765548f3..af86410c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -45,6 +45,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for the OAuth 2.0 Device Authorization Endpoint. * @@ -166,7 +168,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder); this.requestMatcher = new AntPathRequestMatcher( - authorizationServerSettings.getDeviceAuthorizationEndpoint(), HttpMethod.POST.name()); + withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()), HttpMethod.POST.name()); List authenticationProviders = createDefaultAuthenticationProviders(builder); if (!this.authenticationProviders.isEmpty()) { @@ -184,7 +186,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter = new OAuth2DeviceAuthorizationEndpointFilter( - authenticationManager, authorizationServerSettings.getDeviceAuthorizationEndpoint()); + authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.deviceAuthorizationRequestConverters.isEmpty()) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java index bc50f7b4..aa4d7b78 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -50,6 +50,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for the OAuth 2.0 Device Verification Endpoint. * @@ -195,13 +197,10 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA public void init(HttpSecurity builder) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder); + String deviceVerificationEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getDeviceVerificationEndpoint()); this.requestMatcher = new OrRequestMatcher( - new AntPathRequestMatcher( - authorizationServerSettings.getDeviceVerificationEndpoint(), - HttpMethod.GET.name()), - new AntPathRequestMatcher( - authorizationServerSettings.getDeviceVerificationEndpoint(), - HttpMethod.POST.name())); + new AntPathRequestMatcher(deviceVerificationEndpointUri, HttpMethod.GET.name()), + new AntPathRequestMatcher(deviceVerificationEndpointUri, HttpMethod.POST.name())); List authenticationProviders = createDefaultAuthenticationProviders(builder); if (!this.authenticationProviders.isEmpty()) { @@ -221,7 +220,7 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter = new OAuth2DeviceVerificationEndpointFilter( authenticationManager, - authorizationServerSettings.getDeviceVerificationEndpoint()); + withMultipleIssuerPattern(authorizationServerSettings.getDeviceVerificationEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.deviceVerificationRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.deviceVerificationRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java index 61e089f2..fa23a173 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -54,6 +54,8 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for the OAuth 2.0 Token Endpoint. * @@ -163,7 +165,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); this.requestMatcher = new AntPathRequestMatcher( - authorizationServerSettings.getTokenEndpoint(), HttpMethod.POST.name()); + withMultipleIssuerPattern(authorizationServerSettings.getTokenEndpoint()), HttpMethod.POST.name()); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); if (!this.authenticationProviders.isEmpty()) { @@ -182,7 +184,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure OAuth2TokenEndpointFilter tokenEndpointFilter = new OAuth2TokenEndpointFilter( authenticationManager, - authorizationServerSettings.getTokenEndpoint()); + withMultipleIssuerPattern(authorizationServerSettings.getTokenEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.accessTokenRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.accessTokenRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java index 08f9c0b3..96eb1062 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -43,6 +43,8 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for the OAuth 2.0 Token Introspection Endpoint. * @@ -152,7 +154,7 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); this.requestMatcher = new AntPathRequestMatcher( - authorizationServerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name()); + withMultipleIssuerPattern(authorizationServerSettings.getTokenIntrospectionEndpoint()), HttpMethod.POST.name()); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); if (!this.authenticationProviders.isEmpty()) { @@ -170,7 +172,7 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter = new OAuth2TokenIntrospectionEndpointFilter( - authenticationManager, authorizationServerSettings.getTokenIntrospectionEndpoint()); + authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getTokenIntrospectionEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.introspectionRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.introspectionRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java index e6570f42..9b89d8d0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -42,6 +42,8 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for the OAuth 2.0 Token Revocation Endpoint. * @@ -151,7 +153,7 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); this.requestMatcher = new AntPathRequestMatcher( - authorizationServerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name()); + withMultipleIssuerPattern(authorizationServerSettings.getTokenRevocationEndpoint()), HttpMethod.POST.name()); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); if (!this.authenticationProviders.isEmpty()) { @@ -169,7 +171,7 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth OAuth2TokenRevocationEndpointFilter revocationEndpointFilter = new OAuth2TokenRevocationEndpointFilter( - authenticationManager, authorizationServerSettings.getTokenRevocationEndpoint()); + authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getTokenRevocationEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.revocationRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.revocationRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationEndpointConfigurer.java index 082b2344..b030353e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -46,6 +46,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for OpenID Connect 1.0 Dynamic Client Registration Endpoint. * @@ -160,7 +162,7 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut @Override void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); - String clientRegistrationEndpointUri = authorizationServerSettings.getOidcClientRegistrationEndpoint(); + String clientRegistrationEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint()); this.requestMatcher = new OrRequestMatcher( new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.POST.name()), new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.GET.name()) @@ -183,7 +185,7 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter = new OidcClientRegistrationEndpointFilter( authenticationManager, - authorizationServerSettings.getOidcClientRegistrationEndpoint()); + withMultipleIssuerPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.clientRegistrationRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.clientRegistrationRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java index 04b356ff..f138f1e2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -44,6 +44,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint. * @@ -151,7 +153,7 @@ public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer @Override void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); - String logoutEndpointUri = authorizationServerSettings.getOidcLogoutEndpoint(); + String logoutEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getOidcLogoutEndpoint()); this.requestMatcher = new OrRequestMatcher( new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.GET.name()), new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.POST.name()) @@ -174,7 +176,7 @@ public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer OidcLogoutEndpointFilter oidcLogoutEndpointFilter = new OidcLogoutEndpointFilter( authenticationManager, - authorizationServerSettings.getOidcLogoutEndpoint()); + withMultipleIssuerPattern(authorizationServerSettings.getOidcLogoutEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.logoutRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.logoutRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java index 1141f330..9e02d84b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -70,7 +70,7 @@ public final class OidcProviderConfigurationEndpointConfigurer extends AbstractO @Override void init(HttpSecurity httpSecurity) { this.requestMatcher = new AntPathRequestMatcher( - "/.well-known/openid-configuration", HttpMethod.GET.name()); + "/**/.well-known/openid-configuration", HttpMethod.GET.name()); } @Override diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java index 0bd08f53..79f8883c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -49,6 +49,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern; + /** * Configurer for OpenID Connect 1.0 UserInfo Endpoint. * @@ -185,7 +187,7 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur @Override void init(HttpSecurity httpSecurity) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); - String userInfoEndpointUri = authorizationServerSettings.getOidcUserInfoEndpoint(); + String userInfoEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getOidcUserInfoEndpoint()); this.requestMatcher = new OrRequestMatcher( new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()), new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name())); @@ -207,7 +209,7 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter = new OidcUserInfoEndpointFilter( authenticationManager, - authorizationServerSettings.getOidcUserInfoEndpoint()); + withMultipleIssuerPattern(authorizationServerSettings.getOidcUserInfoEndpoint())); List authenticationConverters = createDefaultAuthenticationConverters(); if (!this.userInfoRequestConverters.isEmpty()) { authenticationConverters.addAll(0, this.userInfoRequestConverters); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java index a12ef305..3a3a556d 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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,9 +28,22 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori public interface AuthorizationServerContext { /** - * Returns the {@code URL} of the Authorization Server's issuer identifier. + * Returns {@link AuthorizationServerSettings#getIssuer()} if available, otherwise, + * resolves the issuer identifier from the "current" request. * - * @return the {@code URL} of the Authorization Server's issuer identifier + *

+ * The issuer identifier may contain a path component to support multiple issuers per host in a multi-tenant hosting configuration. + * + *

+ * For example: + *

    + *
  • {@code https://example.com/issuer1/oauth2/token} — resolves the issuer to {@code https://example.com/issuer1}
  • + *
  • {@code https://example.com/issuer2/oauth2/token} — resolves the issuer to {@code https://example.com/issuer2}
  • + *
  • {@code https://example.com/authz/issuer1/oauth2/token} — resolves the issuer to {@code https://example.com/authz/issuer1}
  • + *
  • {@code https://example.com/authz/issuer2/oauth2/token} — resolves the issuer to {@code https://example.com/authz/issuer2}
  • + *
+ * + * @return {@link AuthorizationServerSettings#getIssuer()} if available, otherwise, resolves the issuer identifier from the "current" request */ String getIssuer(); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java index 1cdc155f..3218b75e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -57,7 +57,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques /** * The default endpoint {@code URI} for OpenID Provider Configuration requests. */ - private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; + private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/**/.well-known/openid-configuration"; private final RequestMatcher requestMatcher = new AntPathRequestMatcher( DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java index c561260c..c26f1cfe 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -55,7 +55,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP /** * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Metadata requests. */ - private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server"; + private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server/**"; private final RequestMatcher requestMatcher = new AntPathRequestMatcher( DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java index b6b6db2f..3a51415b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -40,13 +40,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler; 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.UrlUtils; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -199,9 +199,8 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques OAuth2UserCode userCode = deviceAuthorizationRequestAuthentication.getUserCode(); // Generate the fully-qualified verification URI - String issuerUri = AuthorizationServerContextHolder.getContext().getIssuer(); - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(issuerUri) - .path(this.verificationUri); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(this.verificationUri); String verificationUri = uriComponentsBuilder.build().toUriString(); // @formatter:off String verificationUriComplete = uriComponentsBuilder diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilterTests.java new file mode 100644 index 00000000..17b500ee --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilterTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2020-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.FilterChain; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AuthorizationServerContextFilter}. + * + * @author Joe Grandja + */ +class AuthorizationServerContextFilterTests { + private static final String SCHEME = "https"; + private static final String HOST = "example.com"; + private static final int PORT = 8443; + private static final String DEFAULT_ISSUER = SCHEME + "://" + HOST + ":" + PORT; + private AuthorizationServerContextFilter filter; + + @Test + public void doFilterWhenDefaultEndpointsThenIssuerResolved() throws Exception { + AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build(); + this.filter = new AuthorizationServerContextFilter(authorizationServerSettings); + + String issuerPath = "/issuer1"; + String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath); + Set endpointUris = getEndpointUris(authorizationServerSettings); + + for (String endpointUri : endpointUris) { + assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath); + } + } + + @Test + public void doFilterWhenCustomEndpointsThenIssuerResolved() throws Exception { + AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder() + .authorizationEndpoint("/oauth2/v1/authorize") + .deviceAuthorizationEndpoint("/oauth2/v1/device_authorization") + .deviceVerificationEndpoint("/oauth2/v1/device_verification") + .tokenEndpoint("/oauth2/v1/token") + .jwkSetEndpoint("/oauth2/v1/jwks") + .tokenRevocationEndpoint("/oauth2/v1/revoke") + .tokenIntrospectionEndpoint("/oauth2/v1/introspect") + .oidcClientRegistrationEndpoint("/connect/v1/register") + .oidcUserInfoEndpoint("/v1/userinfo") + .oidcLogoutEndpoint("/connect/v1/logout") + .build(); + this.filter = new AuthorizationServerContextFilter(authorizationServerSettings); + + String issuerPath = "/issuer2"; + String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath); + Set endpointUris = getEndpointUris(authorizationServerSettings); + + for (String endpointUri : endpointUris) { + assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath); + } + } + + @Test + public void doFilterWhenIssuerHasMultiplePathsThenIssuerResolved() throws Exception { + AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build(); + this.filter = new AuthorizationServerContextFilter(authorizationServerSettings); + + String issuerPath = "/path1/path2/issuer3"; + String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath); + Set endpointUris = getEndpointUris(authorizationServerSettings); + + for (String endpointUri : endpointUris) { + assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath); + } + } + + private void assertResolvedIssuer(String requestUri, String expectedIssuer) throws Exception { + MockHttpServletRequest request = createRequest(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + + AtomicReference resolvedIssuer = new AtomicReference<>(); + FilterChain filterChain = (req, resp) -> + resolvedIssuer.set(AuthorizationServerContextHolder.getContext().getIssuer()); + + this.filter.doFilter(request, response, filterChain); + + assertThat(resolvedIssuer.get()).isEqualTo(expectedIssuer); + } + + private static Set getEndpointUris(AuthorizationServerSettings authorizationServerSettings) { + Set endpointUris = new HashSet<>(); + endpointUris.add("/.well-known/oauth-authorization-server"); + endpointUris.add("/.well-known/openid-configuration"); + for (Map.Entry setting : authorizationServerSettings.getSettings().entrySet()) { + if (setting.getKey().endsWith("-endpoint")) { + endpointUris.add((String) setting.getValue()); + } + } + return endpointUris; + } + + private static MockHttpServletRequest createRequest(String requestUri) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI(requestUri); + request.setScheme(SCHEME); + request.setServerName(HOST); + request.setServerPort(PORT); + return request; + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/JwkSetTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/JwkSetTests.java index 9c3dfae3..9c6fbfc5 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/JwkSetTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/JwkSetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -112,6 +112,14 @@ public class JwkSetTests { assertJwkSetRequestThenReturnKeys(authorizationServerSettings.getJwkSetEndpoint()); } + @Test + public void requestWhenJwkSetRequestIncludesIssuerPathThenReturnKeys() throws Exception { + this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire(); + + String issuer = "https://example.com:8443/issuer1"; + assertJwkSetRequestThenReturnKeys(issuer.concat(authorizationServerSettings.getJwkSetEndpoint())); + } + private void assertJwkSetRequestThenReturnKeys(String jwkSetEndpointUri) throws Exception { this.mvc.perform(get(jwkSetEndpointUri)) .andExpect(status().isOk()) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java index 15063311..ea64e23d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -826,6 +826,46 @@ public class OAuth2AuthorizationCodeGrantTests { assertThat(securityContext.getAuthentication()).isNull(); } + @Test + public void requestWhenAuthorizationAndTokenRequestIncludesIssuerPathThenIssuerResolvedWithPath() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithTokenGenerator.class).autowire(); + + RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build(); + this.registeredClientRepository.save(registeredClient); + + String issuer = "https://example.com:8443/issuer1"; + + MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI)) + .queryParams(getAuthorizationRequestParameters(registeredClient)) + .queryParam(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE) + .queryParam(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256") + .with(user("user"))) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + String authorizationCode = extractParameterFromRedirectUri(mvcResult.getResponse().getRedirectedUrl(), "code"); + OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); + + this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI)) + .params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization)) + .param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()) + .param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.token_type").isNotEmpty()) + .andExpect(jsonPath("$.expires_in").isNotEmpty()) + .andExpect(jsonPath("$.refresh_token").doesNotExist()) + .andExpect(jsonPath("$.scope").isNotEmpty()) + .andReturn(); + + ArgumentCaptor tokenContextCaptor = ArgumentCaptor.forClass(OAuth2TokenContext.class); + verify(tokenGenerator).generate(tokenContextCaptor.capture()); + OAuth2TokenContext tokenContext = tokenContextCaptor.getValue(); + assertThat(tokenContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer); + } + private static MultiValueMap getAuthorizationRequestParameters(RegisteredClient registeredClient) { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java index 083f8327..a7330a1d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -65,7 +65,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ExtendWith(SpringTestContextExtension.class) public class OAuth2AuthorizationServerMetadataTests { private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server"; - private static final String ISSUER_URL = "https://example.com"; + private static final String ISSUER = "https://example.com"; private static EmbeddedDatabase db; private static JWKSource jwkSource; @@ -105,19 +105,37 @@ public class OAuth2AuthorizationServerMetadataTests { public void requestWhenAuthorizationServerMetadataRequestAndIssuerSetThenUsed() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))) .andExpect(status().is2xxSuccessful()) - .andExpect(jsonPath("issuer").value(ISSUER_URL)) + .andExpect(jsonPath("issuer").value(ISSUER)) .andReturn(); } @Test - public void requestWhenAuthorizationServerMetadataRequestAndIssuerNotSetThenResolveFromRequest() throws Exception { + public void requestWhenAuthorizationServerMetadataRequestIncludesIssuerPathThenMetadataResponseHasIssuerPath() throws Exception { this.spring.register(AuthorizationServerConfigurationWithIssuerNotSet.class).autowire(); - this.mvc.perform(get("http://localhost".concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))) + String host = "https://example.com:8443"; + + String issuerPath = "/issuer1"; + String issuer = host.concat(issuerPath); + this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("issuer").value(issuer)) + .andReturn(); + + issuerPath = "/path1/issuer2"; + issuer = host.concat(issuerPath); + this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("issuer").value(issuer)) + .andReturn(); + + issuerPath = "/path1/path2/issuer3"; + issuer = host.concat(issuerPath); + this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath))) .andExpect(status().is2xxSuccessful()) - .andExpect(jsonPath("issuer").value("http://localhost")) + .andExpect(jsonPath("issuer").value(issuer)) .andReturn(); } @@ -126,7 +144,7 @@ public class OAuth2AuthorizationServerMetadataTests { public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() throws Exception { this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire(); - this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))) .andExpect(status().is2xxSuccessful()) .andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, hasItems("scope1", "scope2"))); @@ -156,7 +174,7 @@ public class OAuth2AuthorizationServerMetadataTests { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL).build(); + return AuthorizationServerSettings.builder().issuer(ISSUER).build(); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java index 9b097db8..f76be0cb 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -357,6 +357,30 @@ public class OAuth2ClientCredentialsGrantTests { verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(clientPrincipal)); } + @Test + public void requestWhenTokenRequestIncludesIssuerPathThenIssuerResolvedWithPath() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build(); + this.registeredClientRepository.save(registeredClient); + + String issuer = "https://example.com:8443/issuer1"; + + this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI)) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .param(OAuth2ParameterNames.SCOPE, "scope1 scope2") + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").value("scope1 scope2")); + + ArgumentCaptor jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class); + verify(jwtCustomizer).customize(jwtEncodingContextCaptor.capture()); + JwtEncodingContext jwtEncodingContext = jwtEncodingContextCaptor.getValue(); + assertThat(jwtEncodingContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer); + } + private static String encodeBasicAuth(String clientId, String secret) throws Exception { clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()); secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java index f8b03983..3e82ccc1 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -218,8 +218,10 @@ public class OAuth2DeviceCodeGrantTests { parameters.set(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " ")); + String issuer = "https://example.com:8443/issuer1"; + // @formatter:off - MvcResult mvcResult = this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI) + MvcResult mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI)) .params(parameters) .headers(withClientAuth(registeredClient))) .andExpect(status().isOk()) @@ -240,9 +242,9 @@ public class OAuth2DeviceCodeGrantTests { String userCode = deviceAuthorizationResponse.getUserCode().getTokenValue(); assertThat(userCode).matches("[A-Z]{4}-[A-Z]{4}"); assertThat(deviceAuthorizationResponse.getVerificationUri()) - .isEqualTo("http://localhost/oauth2/device_verification"); + .isEqualTo("https://example.com:8443/oauth2/device_verification"); assertThat(deviceAuthorizationResponse.getVerificationUriComplete()) - .isEqualTo("http://localhost/oauth2/device_verification?user_code=" + userCode); + .isEqualTo("https://example.com:8443/oauth2/device_verification?user_code=" + userCode); String deviceCode = deviceAuthorizationResponse.getDeviceCode().getTokenValue(); OAuth2Authorization authorization = this.authorizationService.findByToken(deviceCode, DEVICE_CODE_TOKEN_TYPE); @@ -311,8 +313,10 @@ public class OAuth2DeviceCodeGrantTests { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE); + String issuer = "https://example.com:8443/issuer1"; + // @formatter:off - MvcResult mvcResult = this.mvc.perform(get(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI) + MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)) .queryParams(parameters) .with(user("user"))) .andExpect(status().isOk()) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java index 728e29a0..faedfe6f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -30,6 +30,7 @@ import java.util.function.Consumer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -107,6 +108,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -167,6 +169,18 @@ public class OAuth2TokenIntrospectionTests { .build(); } + @SuppressWarnings("unchecked") + @BeforeEach + public void setup() { + reset(authenticationConverter); + reset(authenticationConvertersConsumer); + reset(authenticationProvider); + reset(authenticationProvidersConsumer); + reset(authenticationSuccessHandler); + reset(authenticationFailureHandler); + reset(accessTokenCustomizer); + } + @AfterEach public void tearDown() { jdbcOperations.update("truncate table oauth2_authorization"); @@ -395,6 +409,41 @@ public class OAuth2TokenIntrospectionTests { verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenIntrospectionAuthentication)); } + @Test + public void requestWhenIntrospectionRequestIncludesIssuerPathThenActive() throws Exception { + this.spring.register(AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint.class).autowire(); + + RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build(); + this.registeredClientRepository.save(introspectRegisteredClient); + + RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(authorizedRegisteredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build(); + this.authorizationService.save(authorization); + + OAuth2AccessToken accessToken = authorization.getAccessToken().getToken(); + + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken( + introspectRegisteredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, introspectRegisteredClient.getClientSecret()); + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, null, null); + + when(authenticationConverter.convert(any())).thenReturn(tokenIntrospectionAuthentication); + when(authenticationProvider.supports(eq(OAuth2TokenIntrospectionAuthenticationToken.class))).thenReturn(true); + when(authenticationProvider.authenticate(any())).thenReturn(tokenIntrospectionAuthentication); + + String issuer = "https://example.com:8443/issuer1"; + + // @formatter:off + this.mvc.perform(post(issuer.concat(authorizationServerSettings.getTokenIntrospectionEndpoint())) + .params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN)) + .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient))) + .andExpect(status().isOk()); + // @formatter:on + } + private static MultiValueMap getTokenIntrospectionRequestParameters(OAuth2Token token, OAuth2TokenType tokenType) { MultiValueMap parameters = new LinkedMultiValueMap<>(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java index 18777bf4..dc626a13 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -201,6 +201,35 @@ public class OAuth2TokenRevocationTests { assertThat(refreshToken.isInvalidated()).isFalse(); } + @Test + public void requestWhenRevokeAccessTokenAndRequestIncludesIssuerPathThenRevoked() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + this.registeredClientRepository.save(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + OAuth2AccessToken token = authorization.getAccessToken().getToken(); + OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN; + this.authorizationService.save(authorization); + + String issuer = "https://example.com:8443/issuer1"; + + // @formatter:off + this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI)) + .params(getTokenRevocationRequestParameters(token, tokenType)) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()); + // @formatter:on + + OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId()); + OAuth2Authorization.Token accessToken = updatedAuthorization.getAccessToken(); + assertThat(accessToken.isInvalidated()).isTrue(); + OAuth2Authorization.Token refreshToken = updatedAuthorization.getRefreshToken(); + assertThat(refreshToken.isInvalidated()).isFalse(); + } + @Test public void requestWhenTokenRevocationEndpointCustomizedThenUsed() throws Exception { this.spring.register(AuthorizationServerConfigurationCustomTokenRevocationEndpoint.class).autowire(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java index b4b33d8e..be1ea066 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -137,6 +137,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @ExtendWith(SpringTestContextExtension.class) public class OidcClientRegistrationTests { + private static final String ISSUER = "https://example.com:8443/issuer1"; private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token"; private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register"; private static final HttpMessageConverter accessTokenHttpResponseConverter = @@ -374,7 +375,7 @@ public class OidcClientRegistrationTests { when(authenticationProvider.authenticate(any())).thenThrow(new OAuth2AuthenticationException("error")); - this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)) .param(OAuth2ParameterNames.CLIENT_ID, "invalid").with(jwt())); verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any()); @@ -399,7 +400,7 @@ public class OidcClientRegistrationTests { OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration); - this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + this.mvc.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI)) .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .param(OAuth2ParameterNames.SCOPE, "scope1") .with(httpBasic(clientRegistrationResponse.getClientId(), clientRegistrationResponse.getClientSecret()))) @@ -436,7 +437,7 @@ public class OidcClientRegistrationTests { JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder() .issuer(clientRegistrationResponse.getClientId()) .subject(clientRegistrationResponse.getClientId()) - .audience(Collections.singletonList(asUrl(this.authorizationServerSettings.getIssuer(), this.authorizationServerSettings.getTokenEndpoint()))) + .audience(Collections.singletonList(asUrl(ISSUER, this.authorizationServerSettings.getTokenEndpoint()))) .issuedAt(issuedAt) .expiresAt(expiresAt) .build(); @@ -447,7 +448,7 @@ public class OidcClientRegistrationTests { Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); - this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + this.mvc.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI)) .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .param(OAuth2ParameterNames.SCOPE, "scope1") .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") @@ -520,7 +521,7 @@ public class OidcClientRegistrationTests { // @formatter:on Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); - MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + MvcResult mvcResult = this.mvc.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI)) .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .param(OAuth2ParameterNames.SCOPE, clientRegistrationScope) .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") @@ -539,7 +540,7 @@ public class OidcClientRegistrationTests { httpHeaders.setBearerAuth(accessToken.getTokenValue()); // Register the client - mvcResult = this.mvc.perform(post(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI) + mvcResult = this.mvc.perform(post(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)) .headers(httpHeaders) .contentType(MediaType.APPLICATION_JSON) .content(getClientRegistrationRequestContent(clientRegistration))) @@ -557,7 +558,7 @@ public class OidcClientRegistrationTests { return JwtClaimsSet.builder() .issuer(registeredClient.getClientId()) .subject(registeredClient.getClientId()) - .audience(Collections.singletonList(asUrl(this.authorizationServerSettings.getIssuer(), this.authorizationServerSettings.getTokenEndpoint()))) + .audience(Collections.singletonList(asUrl(ISSUER, this.authorizationServerSettings.getTokenEndpoint()))) .issuedAt(issuedAt) .expiresAt(expiresAt); } @@ -734,7 +735,6 @@ public class OidcClientRegistrationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() - .issuer("https://auth-server:9000") .build(); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java index 1a4a62b7..2efd8ed4 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -63,7 +63,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ExtendWith(SpringTestContextExtension.class) public class OidcProviderConfigurationTests { private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration"; - private static final String ISSUER_URL = "https://example.com"; + private static final String ISSUER = "https://example.com"; public final SpringTestContext spring = new SpringTestContext(); @@ -77,9 +77,29 @@ public class OidcProviderConfigurationTests { public void requestWhenConfigurationRequestAndIssuerSetThenReturnDefaultConfigurationResponse() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) .andExpect(status().is2xxSuccessful()) - .andExpectAll(defaultConfigurationMatchers()); + .andExpectAll(defaultConfigurationMatchers(ISSUER)); + } + + @Test + public void requestWhenConfigurationRequestIncludesIssuerPathThenConfigurationResponseHasIssuerPath() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithIssuerNotSet.class).autowire(); + + String issuer = "https://example.com:8443/issuer1"; + this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers(issuer)); + + issuer = "https://example.com:8443/path1/issuer2"; + this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers(issuer)); + + issuer = "https://example.com:8443/path1/path2/issuer3"; + this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers(issuer)); } // gh-632 @@ -87,10 +107,10 @@ public class OidcProviderConfigurationTests { public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) .with(user("user"))) .andExpect(status().is2xxSuccessful()) - .andExpectAll(defaultConfigurationMatchers()); + .andExpectAll(defaultConfigurationMatchers(ISSUER)); } // gh-616 @@ -98,7 +118,7 @@ public class OidcProviderConfigurationTests { public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception { this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire(); - this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) .andExpect(status().is2xxSuccessful()) .andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL))); @@ -108,35 +128,35 @@ public class OidcProviderConfigurationTests { public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint() throws Exception { this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire(); - this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) .andExpect(status().is2xxSuccessful()) - .andExpectAll(defaultConfigurationMatchers()) - .andExpect(jsonPath("$.registration_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint()))); + .andExpectAll(defaultConfigurationMatchers(ISSUER)) + .andExpect(jsonPath("$.registration_endpoint").value(ISSUER.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint()))); } - private ResultMatcher[] defaultConfigurationMatchers() { + private ResultMatcher[] defaultConfigurationMatchers(String issuer) { // @formatter:off return new ResultMatcher[] { - jsonPath("issuer").value(ISSUER_URL), - jsonPath("authorization_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getAuthorizationEndpoint())), - jsonPath("token_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenEndpoint())), + jsonPath("issuer").value(issuer), + jsonPath("authorization_endpoint").value(issuer.concat(this.authorizationServerSettings.getAuthorizationEndpoint())), + jsonPath("token_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenEndpoint())), jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), - jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())), - jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())), - jsonPath("end_session_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())), + jsonPath("jwks_uri").value(issuer.concat(this.authorizationServerSettings.getJwkSetEndpoint())), + jsonPath("userinfo_endpoint").value(issuer.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())), + jsonPath("end_session_endpoint").value(issuer.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())), jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()), jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()), jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()), - jsonPath("revocation_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())), + jsonPath("revocation_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())), jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), - jsonPath("introspection_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())), + jsonPath("introspection_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())), jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()), @@ -218,7 +238,18 @@ public class OidcProviderConfigurationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() - .issuer(ISSUER_URL) + .issuer(ISSUER) + .build(); + } + + } + + @EnableWebSecurity + static class AuthorizationServerConfigurationWithIssuerNotSet extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() .build(); } @@ -306,7 +337,7 @@ public class OidcProviderConfigurationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value").build(); + return AuthorizationServerSettings.builder().issuer(ISSUER + "?param=value").build(); } } @@ -315,7 +346,7 @@ public class OidcProviderConfigurationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#fragment").build(); + return AuthorizationServerSettings.builder().issuer(ISSUER + "#fragment").build(); } } @@ -324,7 +355,7 @@ public class OidcProviderConfigurationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value#fragment").build(); + return AuthorizationServerSettings.builder().issuer(ISSUER + "?param=value#fragment").build(); } } @@ -333,7 +364,7 @@ public class OidcProviderConfigurationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?").build(); + return AuthorizationServerSettings.builder().issuer(ISSUER + "?").build(); } } @@ -342,7 +373,7 @@ public class OidcProviderConfigurationTests { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#").build(); + return AuthorizationServerSettings.builder().issuer(ISSUER + "#").build(); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java index ebc4680c..a63475b0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -303,9 +303,11 @@ public class OidcTests { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); this.registeredClientRepository.save(registeredClient); + String issuer = "https://example.com:8443/issuer1"; + // Login MultiValueMap authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient); - MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI) + MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI)) .queryParams(authorizationRequestParameters) .with(user("user"))) .andExpect(status().is3xxRedirection()) @@ -319,7 +321,7 @@ public class OidcTests { OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); // Get ID Token - mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI)) .params(getTokenRequestParameters(registeredClient, authorization)) .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( registeredClient.getClientId(), registeredClient.getClientSecret()))) @@ -334,7 +336,7 @@ public class OidcTests { String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); // Logout - mvcResult = this.mvc.perform(post(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI) + mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI)) .param("id_token_hint", idToken) .session(session)) .andExpect(status().is3xxRedirection()) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java index 66f92761..4a0e01b0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 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. @@ -24,11 +24,12 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import jakarta.servlet.http.HttpServletResponse; + import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; -import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -193,6 +194,24 @@ public class OidcUserInfoTests { // @formatter:on } + @Test + public void requestWhenUserInfoRequestIncludesIssuerPathThenUserInfoResponse() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + OAuth2Authorization authorization = createAuthorization(); + this.authorizationService.save(authorization); + + String issuer = "https://example.com:8443/issuer1"; + + OAuth2AccessToken accessToken = authorization.getAccessToken().getToken(); + // @formatter:off + this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue())) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(userInfoResponse()); + // @formatter:on + } + @Test public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception { this.spring.register(CustomUserInfoConfiguration.class).autowire(); @@ -512,7 +531,6 @@ public class OidcUserInfoTests { @Bean AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() - .issuer("https://auth-server:9000") .build(); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilterTests.java index 7f8a5448..900bdd8e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -21,6 +21,7 @@ import java.time.temporal.ChronoUnit; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -404,6 +405,9 @@ public class OAuth2DeviceAuthorizationEndpointFilterTests { request.setRequestURI(AUTHORIZATION_URI); request.setServletPath(AUTHORIZATION_URI); request.setRemoteAddr(REMOTE_ADDRESS); + request.setScheme("https"); + request.setServerName("provider.com"); + request.setServerPort(-1); return request; } diff --git a/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java index 36682bb3..b868e234 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -43,8 +43,6 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; @@ -94,7 +92,7 @@ public class AuthorizationServerConfig { */ DeviceClientAuthenticationConverter deviceClientAuthenticationConverter = new DeviceClientAuthenticationConverter( - authorizationServerSettings.getDeviceAuthorizationEndpoint()); + "/**" + authorizationServerSettings.getDeviceAuthorizationEndpoint()); DeviceClientAuthenticationProvider deviceClientAuthenticationProvider = new DeviceClientAuthenticationProvider(registeredClientRepository);