From 8538600448a04c1679f885163fddb35ae834c307 Mon Sep 17 00:00:00 2001 From: Gladwin Burboz <8951010+gburboz@users.noreply.github.com> Date: Sun, 14 Apr 2019 17:17:39 -0400 Subject: [PATCH] Resource Server - Multi-Tenant Jwt Decoder by Issuer --- .../jwt/MultiTenantDelegatingJwtDecoder.java | 76 ++++++++++++++++++ .../sample/OAuth2ResourceServerConfig.java | 77 +++++++++++++++++++ ...h2ResourceServerSecurityConfiguration.java | 29 ++++--- .../src/main/resources/application.yml | 8 ++ 4 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MultiTenantDelegatingJwtDecoder.java create mode 100644 samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerConfig.java diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MultiTenantDelegatingJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MultiTenantDelegatingJwtDecoder.java new file mode 100644 index 00000000000..c3bad5f38f8 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MultiTenantDelegatingJwtDecoder.java @@ -0,0 +1,76 @@ +package org.springframework.security.oauth2.jwt; + +import com.nimbusds.jose.JOSEObject; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jose.util.JSONObjectUtils; +import net.minidev.json.JSONObject; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.text.ParseException; +import java.util.Map; + +public class MultiTenantDelegatingJwtDecoder implements JwtDecoder { + private static final String DECODING_ERROR_MESSAGE_TEMPLATE = + "An error occurred while attempting to decode the Jwt: %s"; + + private JwtDecoder decoderDefault; + + private Map decoderByIssuer; + + public MultiTenantDelegatingJwtDecoder(JwtDecoder decoderDefault) { + this(decoderDefault, null); + } + + public MultiTenantDelegatingJwtDecoder( + Map decoderByIssuer) { + this(null, decoderByIssuer); + } + + public MultiTenantDelegatingJwtDecoder( + JwtDecoder decoderDefault, + Map decoderByIssuer) { + Assert.isTrue(decoderDefault != null || !CollectionUtils.isEmpty(decoderByIssuer), + "At least one of decoderDefault or decoderByIssuer must be provided"); + this.decoderDefault = decoderDefault; + this.decoderByIssuer = decoderByIssuer; + } + + @Override + public Jwt decode(String token) throws JwtException { + JwtDecoder jwtDecoder = null; + if (!CollectionUtils.isEmpty(decoderByIssuer)) { + String issuer = parseAndFindIssuer(token); + if (issuer == null && decoderDefault == null) { + throw new JwtException( + "Unable to determine issuer for the token"); + } else { + jwtDecoder = decoderByIssuer.get(issuer); + if (jwtDecoder == null && decoderDefault == null) { + throw new JwtException(String.format( + "JwtDecoder has not been configured for issuer %s", issuer)); + } + } + } + if (jwtDecoder == null && decoderDefault != null) { + jwtDecoder = decoderDefault; + } else { + throw new JwtException(String.format("Unable to determine JwtDecoder")); + } + return jwtDecoder.decode(token); + } + + private String parseAndFindIssuer(String token) { + try { + Base64URL[] parts = JOSEObject.split(token); + JSONObject payload = JSONObjectUtils.parse(parts[1].decodeToString()); + return payload.getAsString("iss"); + } catch (ArrayIndexOutOfBoundsException + | NullPointerException + | ParseException ex) { + throw new JwtException(String.format( + DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } + } + +} diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerConfig.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerConfig.java new file mode 100644 index 00000000000..d1a1dac3623 --- /dev/null +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerConfig.java @@ -0,0 +1,77 @@ +package sample; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +import java.util.HashSet; +import java.util.Set; + +@Configuration +@ConfigurationProperties("spring.security.oauth2.resourceserver") +public class OAuth2ResourceServerConfig { + + @NestedConfigurationProperty + private JwtConfig jwt; + + @NestedConfigurationProperty + private Set multiTenantJwt = new HashSet<>(); + + @NestedConfigurationProperty + private OpaqueConfig opaque; + + public JwtConfig getJwt() { + return jwt; + } + + public void setJwt(JwtConfig jwt) { + this.jwt = jwt; + } + + public Set getMultiTenantJwt() { + return multiTenantJwt; + } + + public OpaqueConfig getOpaque() { + return opaque; + } + + public void setOpaque(OpaqueConfig opaque) { + this.opaque = opaque; + } + + public static class JwtConfig { + private String issuerUri; + private String jwkSetUri; + + public String getIssuerUri() { + return issuerUri; + } + + public void setIssuerUri(String issuerUri) { + this.issuerUri = issuerUri; + } + + public String getJwkSetUri() { + return jwkSetUri; + } + + public void setJwkSetUri(String jwkSetUri) { + this.jwkSetUri = jwkSetUri; + } + } + + public static class OpaqueConfig { + + private String introspectionUri; + + public String getIntrospectionUri() { + return introspectionUri; + } + + public void setIntrospectionUri(String introspectionUri) { + this.introspectionUri = introspectionUri; + } + + } +} diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java index 7977d92fa90..fa3b778cb8d 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java @@ -18,16 +18,18 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; - -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.MultiTenantDelegatingJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationProvider; @@ -35,14 +37,12 @@ /** * @author Josh Cummings */ +@Configuration @EnableWebSecurity public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter { - @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") - String jwkSetUri; - - @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}") - String introspectionUri; + @Autowired + OAuth2ResourceServerConfig config; @Override protected void configure(HttpSecurity http) throws Exception { @@ -70,11 +70,22 @@ AuthenticationManagerResolver multitenantAuthenticationManag } AuthenticationManager jwt() { - JwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); + JwtDecoder jwtDecoder = null; + if (this.config.getJwt() != null) { + jwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.config.getJwt().getJwkSetUri()).build(); + } + if (!this.config.getMultiTenantJwt().isEmpty()) { + Map jwtDecoderByIssuer = this.config.getMultiTenantJwt().stream() + .collect(Collectors.toMap(e -> e.getIssuerUri(), + e -> NimbusJwtDecoder.withJwkSetUri(e.getJwkSetUri()).build())); + jwtDecoder = new MultiTenantDelegatingJwtDecoder(jwtDecoder, jwtDecoderByIssuer); + } return new JwtAuthenticationProvider(jwtDecoder)::authenticate; } AuthenticationManager opaque() { - return new OAuth2IntrospectionAuthenticationProvider(this.introspectionUri, "client", "secret")::authenticate; + return new OAuth2IntrospectionAuthenticationProvider( + this.config.getOpaque().getIntrospectionUri(), "client", "secret")::authenticate; } + } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml b/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml index 52aff11b1aa..04f8549a297 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml @@ -3,6 +3,14 @@ spring: oauth2: resourceserver: jwt: + issuer-uri: ${mockwebserver.url} jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json + multi-tenant-jwt: + - + issuer-uri: "https://accounts.google.com" + jwk-set-uri: "https://www.googleapis.com/oauth2/v3/certs" + - + issuer-uri: "https://contoso.auth0.com/" + jwk-set-uri: "https://contoso.auth0.com/.well-known/jwks.json" opaque: introspection-uri: ${mockwebserver.url}/introspect