Skip to content

Commit

Permalink
Add support for multiple issuers per host using the path component
Browse files Browse the repository at this point in the history
  • Loading branch information
jgrandja committed Jan 15, 2024
1 parent 6ec92a0 commit 168077b
Show file tree
Hide file tree
Showing 33 changed files with 583 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -28,6 +31,7 @@
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;

Expand All @@ -42,57 +46,92 @@
*/
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
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
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 {
AuthorizationServerContextHolder.resetContext();
}
}

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<String> 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<String, Object> 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<String> issuerSupplier;
private final String issuer;
private final AuthorizationServerSettings authorizationServerSettings;

private DefaultAuthorizationServerContext(Supplier<String> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -51,6 +51,8 @@
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.
*
Expand Down Expand Up @@ -209,13 +211,10 @@ void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthe
@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<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
Expand All @@ -234,7 +233,7 @@ void configure(HttpSecurity httpSecurity) {
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
authenticationManager,
authorizationServerSettings.getAuthorizationEndpoint());
withMultipleIssuerPattern(authorizationServerSettings.getAuthorizationEndpoint()));
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.authorizationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.authorizationRequestConverters);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -56,6 +56,8 @@
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.
*
Expand Down Expand Up @@ -314,7 +316,7 @@ public void init(HttpSecurity httpSecurity) {
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<HttpSecurity> exceptionHandling = httpSecurity.getConfigurer(ExceptionHandlingConfigurer.class);
Expand Down Expand Up @@ -342,7 +344,7 @@ public void configure(HttpSecurity httpSecurity) {
JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
if (jwkSource != null) {
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
jwkSource, authorizationServerSettings.getJwkSetEndpoint());
jwkSource, withMultipleIssuerPattern(authorizationServerSettings.getJwkSetEndpoint()));
httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -70,7 +70,7 @@ void addDefaultAuthorizationServerMetadataCustomizer(
@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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -51,6 +51,8 @@
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.
*
Expand Down Expand Up @@ -161,16 +163,16 @@ void init(HttpSecurity httpSecurity) {
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<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -43,6 +43,7 @@
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;

/**
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -45,6 +45,8 @@
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.
*
Expand Down Expand Up @@ -166,7 +168,7 @@ public void init(HttpSecurity builder) {
AuthorizationServerSettings authorizationServerSettings =
OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
this.requestMatcher = new AntPathRequestMatcher(
authorizationServerSettings.getDeviceAuthorizationEndpoint(), HttpMethod.POST.name());
withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()), HttpMethod.POST.name());

List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
if (!this.authenticationProviders.isEmpty()) {
Expand All @@ -184,7 +186,7 @@ public void configure(HttpSecurity builder) {

OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter =
new OAuth2DeviceAuthorizationEndpointFilter(
authenticationManager, authorizationServerSettings.getDeviceAuthorizationEndpoint());
authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()));

List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.deviceAuthorizationRequestConverters.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -50,6 +50,8 @@
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.
*
Expand Down Expand Up @@ -195,13 +197,10 @@ public OAuth2DeviceVerificationEndpointConfigurer consentPage(String consentPage
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<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
if (!this.authenticationProviders.isEmpty()) {
Expand All @@ -221,7 +220,7 @@ public void configure(HttpSecurity builder) {
OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter =
new OAuth2DeviceVerificationEndpointFilter(
authenticationManager,
authorizationServerSettings.getDeviceVerificationEndpoint());
withMultipleIssuerPattern(authorizationServerSettings.getDeviceVerificationEndpoint()));
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.deviceVerificationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.deviceVerificationRequestConverters);
Expand Down
Loading

0 comments on commit 168077b

Please sign in to comment.