diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 62906a566fee..464bc9b7cf1a 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -110,6 +110,7 @@ org.springframework.kafka:spring-kafka;2.5.7.RELEASE org.springframework.security:spring-security-config;5.3.5.RELEASE org.springframework.security:spring-security-core;5.3.5.RELEASE org.springframework.security:spring-security-oauth2-client;5.3.5.RELEASE +org.springframework.security:spring-security-oauth2-resource-server;5.3.5.RELEASE org.springframework.security:spring-security-oauth2-core;5.3.5.RELEASE org.springframework.security:spring-security-oauth2-jose;5.3.5.RELEASE org.springframework.security:spring-security-web;5.3.5.RELEASE diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 1596d5cab52e..db3a301d74f0 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -81,6 +81,12 @@ 5.3.5.RELEASE true + + org.springframework.security + spring-security-oauth2-resource-server + 5.3.5.RELEASE + true + org.springframework.security spring-security-oauth2-jose @@ -300,6 +306,7 @@ org.springframework.security:spring-security-config:[5.3.5.RELEASE] org.springframework.security:spring-security-core:[5.3.5.RELEASE] org.springframework.security:spring-security-oauth2-client:[5.3.5.RELEASE] + org.springframework.security:spring-security-oauth2-resource-server:[5.3.5.RELEASE] org.springframework.security:spring-security-oauth2-jose:[5.3.5.RELEASE] org.springframework.security:spring-security-web:[5.3.5.RELEASE] org.apache.qpid:qpid-jms-client:[0.53.0] diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureActiveDirectoryResourceServerConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureActiveDirectoryResourceServerConfiguration.java new file mode 100644 index 000000000000..8fd6086ef8bb --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureActiveDirectoryResourceServerConfiguration.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.aad.resource.server; + + +import com.azure.spring.aad.implementation.AuthorizationServerEndpoints; +import com.azure.spring.aad.resource.server.validator.AzureJwtAudienceValidator; +import com.azure.spring.aad.resource.server.validator.AzureJwtIssuerValidator; +import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.util.StringUtils; + +/** + *

+ * The configuration will not be activated if no {@link BearerTokenAuthenticationToken} class provided. + *

+ * By default, creating a JwtDecoder through JwkKeySetUri will be auto-configured. + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({AADAuthenticationProperties.class}) +@ConditionalOnClass(BearerTokenAuthenticationToken.class) +public class AzureActiveDirectoryResourceServerConfiguration { + + @Autowired + private AADAuthenticationProperties aadAuthenticationProperties; + + /** + * Use JwkKeySetUri to create JwtDecoder + * + * @return JwtDecoder bean + */ + @Bean + @ConditionalOnMissingBean(JwtDecoder.class) + public JwtDecoder jwtDecoderByJwkKeySetUri() { + if (StringUtils.isEmpty(aadAuthenticationProperties.getTenantId())) { + aadAuthenticationProperties.setTenantId("common"); + } + AuthorizationServerEndpoints identityEndpoints = new AuthorizationServerEndpoints( + aadAuthenticationProperties.getAuthorizationServerUri()); + NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder + .withJwkSetUri(identityEndpoints.jwkSetEndpoint(aadAuthenticationProperties.getTenantId())).build(); + List> validators = createDefaultValidator(); + nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); + return nimbusJwtDecoder; + } + + public List> createDefaultValidator() { + List> validators = new ArrayList<>(); + if (!StringUtils.isEmpty(aadAuthenticationProperties.getAppIdUri())) { + List validAudiences = new ArrayList<>(); + validAudiences.add(aadAuthenticationProperties.getAppIdUri()); + validators.add(new AzureJwtAudienceValidator(validAudiences)); + } + validators.add(new AzureJwtIssuerValidator()); + validators.add(new JwtTimestampValidator()); + return validators; + } + + /** + * Default configuration class for using AAD authentication and authorization. User can write another configuration + * bean to override it. + */ + @Configuration + @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) + @EnableWebSecurity + public static class DefaultAzureOAuth2ResourceServerWebSecurityConfigurerAdapter extends + WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().authenticated()) + .oauth2ResourceServer() + .jwt() + .jwtAuthenticationConverter(new AzureJwtBearerTokenAuthenticationConverter()); + } + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureJwtBearerTokenAuthenticationConverter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureJwtBearerTokenAuthenticationConverter.java new file mode 100644 index 000000000000..f5f7de17bc69 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureJwtBearerTokenAuthenticationConverter.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.aad.resource.server; + +import java.util.Collection; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.util.Assert; + +/** + * A {@link Converter} that takes a {@link Jwt} and converts it into a {@link BearerTokenAuthentication}. + */ +public class AzureJwtBearerTokenAuthenticationConverter implements Converter { + + private static final String DEFAULT_AUTHORITY_PREFIX = "SCOPE_"; + + private Converter> jwtGrantedConverter + = new JwtGrantedAuthoritiesConverter(); + + public AzureJwtBearerTokenAuthenticationConverter() { + } + + public AzureJwtBearerTokenAuthenticationConverter(String authoritiesClaimName) { + this(authoritiesClaimName, DEFAULT_AUTHORITY_PREFIX); + } + + public AzureJwtBearerTokenAuthenticationConverter(String authoritiesClaimName, String authorityPrefix) { + Assert.notNull(authoritiesClaimName, "authoritiesClaimName cannot be null"); + Assert.notNull(authorityPrefix, "authorityPrefix cannot be null"); + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(authoritiesClaimName); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(authorityPrefix); + this.jwtGrantedConverter = jwtGrantedAuthoritiesConverter; + } + + protected Collection extractAuthorities(Jwt jwt) { + return this.jwtGrantedConverter.convert(jwt); + } + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt()); + Collection authorities = extractAuthorities(jwt); + AzureOAuth2AuthenticatedPrincipal principal = new AzureOAuth2AuthenticatedPrincipal( + jwt.getHeaders(), jwt.getClaims(), authorities, jwt.getTokenValue()); + return new BearerTokenAuthentication(principal, accessToken, authorities); + } + + public void setJwtGrantedAuthoritiesConverter( + Converter> jwtGrantedAuthoritiesConverter) { + this.jwtGrantedConverter = jwtGrantedAuthoritiesConverter; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureOAuth2AuthenticatedPrincipal.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureOAuth2AuthenticatedPrincipal.java new file mode 100644 index 000000000000..e7f345a1a6dd --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/AzureOAuth2AuthenticatedPrincipal.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.aad.resource.server; + +import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTClaimsSet.Builder; +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.util.Assert; + +/** + * entity class of AzureOAuth2AuthenticatedPrincipal + */ +public class AzureOAuth2AuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal, Serializable { + + private static final long serialVersionUID = -3625690847771476854L; + + private final Collection authorities; + + private final Map headers; + + private final Map attributes; + + private final String tokenValue; + + private JWTClaimsSet jwtClaimsSet; + + public AzureOAuth2AuthenticatedPrincipal(Map headers, Map attributes, + Collection authorities, String tokenValue) { + Assert.notEmpty(attributes, "attributes cannot be empty"); + Assert.notEmpty(headers, "headers cannot be empty"); + this.headers = headers; + this.tokenValue = tokenValue; + this.attributes = Collections.unmodifiableMap(attributes); + this.authorities = authorities == null ? NO_AUTHORITIES : Collections.unmodifiableCollection(authorities); + toJwtClaimsSet(attributes); + } + + private void toJwtClaimsSet(Map attributes) { + JWTClaimsSet.Builder builder = new Builder(); + for (Entry entry : attributes.entrySet()) { + builder.claim(entry.getKey(), entry.getValue()); + } + this.jwtClaimsSet = builder.build(); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("name"); + } + + public String getTokenValue() { + return tokenValue; + } + + public Map getHeaders() { + return headers; + } + + public JWTClaimsSet getJwtClaimsSet() { + return jwtClaimsSet; + } + + public String getIssuer() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getIssuer(); + } + + public String getSubject() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getSubject(); + } + + public Map getClaims() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaims(); + } + + public Object getClaim(String name) { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim(name); + } + + public String getTenantId() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("tid"); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/package-info.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/package-info.java new file mode 100644 index 000000000000..962b2f77134b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.aad.resource.server + */ +package com.azure.spring.aad.resource.server; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/AzureJwtAudienceValidator.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/AzureJwtAudienceValidator.java new file mode 100644 index 000000000000..5084a434352a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/AzureJwtAudienceValidator.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.aad.resource.server.validator; + +import com.azure.spring.autoconfigure.aad.AADTokenClaim; +import java.util.List; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.util.Assert; + +/** + * Validates the "aud" claim in a {@link Jwt}, that is matches a configured value + */ +public class AzureJwtAudienceValidator implements OAuth2TokenValidator { + + private final JwtClaimValidator> validator; + + /** + * Constructs a {@link AzureJwtAudienceValidator} using the provided parameters + * + * @param audiences - The audience that each {@link Jwt} should have. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public AzureJwtAudienceValidator(List audiences) { + Assert.notNull(audiences, "audiences cannot be null"); + this.validator = new JwtClaimValidator(AADTokenClaim.AUD, aud -> audiences.containsAll((List) aud)); + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + return this.validator.validate(token); + } + + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/AzureJwtIssuerValidator.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/AzureJwtIssuerValidator.java new file mode 100644 index 000000000000..e0acb05c22b3 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/AzureJwtIssuerValidator.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.aad.resource.server.validator; + +import com.azure.spring.autoconfigure.aad.AADTokenClaim; +import java.util.function.Predicate; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.util.Assert; + +/** + * Validates the "iss" claim in a {@link Jwt}, that is matches a configured value + */ +public class AzureJwtIssuerValidator implements OAuth2TokenValidator { + + private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; + private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; + private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; + private final JwtClaimValidator validator; + + /** + * Constructs a {@link AzureJwtIssuerValidator} using the provided parameters + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public AzureJwtIssuerValidator() { + this.validator = new JwtClaimValidator(AADTokenClaim.ISS, validIssuer()); + } + + private Predicate validIssuer() { + return iss -> { + if (iss == null) { + return false; + } + return iss.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) + || iss.startsWith(STS_WINDOWS_ISSUER) + || iss.startsWith(STS_CHINA_CLOUD_API_ISSUER); + }; + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + return this.validator.validate(token); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/package-info.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/package-info.java new file mode 100644 index 000000000000..99713f2a5f68 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/resource/server/validator/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package com.azure.spring.aad.resource.server.validator + */ +package com.azure.spring.aad.resource.server.validator; diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java index 13c7787d713f..099e5957bd65 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java @@ -5,14 +5,6 @@ import com.azure.spring.aad.implementation.AuthorizationProperties; import com.nimbusds.jose.jwk.source.RemoteJWKSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; -import org.springframework.validation.annotation.Validated; - -import javax.annotation.PostConstruct; -import javax.validation.constraints.NotEmpty; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -21,6 +13,13 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotEmpty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.validation.annotation.Validated; /** * Configuration properties for Azure Active Directory Authentication. diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADTokenClaim.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADTokenClaim.java index aa27ba1ec63a..057aa2da34f7 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADTokenClaim.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADTokenClaim.java @@ -11,4 +11,6 @@ public class AADTokenClaim { public static final String NAME = "name"; public static final String TID = "tid"; public static final String ROLES = "roles"; + public static final String ISS = "iss"; + public static final String AUD = "aud"; } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java index fea3c0d87d12..e1a323352ff4 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/UserPrincipal.java @@ -105,6 +105,10 @@ public String getName() { return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("name"); } + public String getTenantId() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("tid"); + } + public String getUserPrincipalName() { return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("preferred_username"); } diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories index 53fc6120ab35..f85219dcf712 100644 --- a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories @@ -2,6 +2,8 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.azure.spring.cloudfoun org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration,\ com.azure.spring.autoconfigure.aad.AADOAuth2AutoConfiguration, \ +com.azure.spring.aad.implementation.AzureActiveDirectoryConfiguration,\ +com.azure.spring.aad.resource.server.AzureActiveDirectoryResourceServerConfiguration, \ com.azure.spring.autoconfigure.b2c.AADB2CAutoConfiguration,\ com.azure.spring.autoconfigure.cosmos.CosmosAutoConfiguration,\ com.azure.spring.autoconfigure.cosmos.CosmosHealthConfiguration,\ diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureActiveDirectoryResourceServerConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureActiveDirectoryResourceServerConfigurationTest.java new file mode 100644 index 000000000000..4119850edd1e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureActiveDirectoryResourceServerConfigurationTest.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.autoconfigure.aad; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.azure.spring.aad.resource.server.AzureActiveDirectoryResourceServerConfiguration; +import com.azure.spring.aad.resource.server.AzureActiveDirectoryResourceServerConfiguration.DefaultAzureOAuth2ResourceServerWebSecurityConfigurerAdapter; +import java.util.List; +import org.junit.Test; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +public class AzureActiveDirectoryResourceServerConfigurationTest { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withPropertyValues("azure.activedirectory.user-group.allowed-groups=User"); + + @Test + public void testNotExistBearerTokenAuthenticationToken() { + this.contextRunner + .withUserConfiguration(AzureActiveDirectoryResourceServerConfiguration.class) + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .run(context -> { + assertThat(context).doesNotHaveBean("jwtDecoderByJwkKeySetUri"); + }); + } + + @Test + public void testCreateJwtDecoderByJwkKeySetUri() { + this.contextRunner + .withUserConfiguration(AzureActiveDirectoryResourceServerConfiguration.class) + .run(context -> { + final JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + assertThat(jwtDecoder).isNotNull(); + assertThat(jwtDecoder).isExactlyInstanceOf(NimbusJwtDecoder.class); + }); + } + + @Test + public void testNotAudienceDefaultValidator() { + this.contextRunner + .withUserConfiguration(AzureActiveDirectoryResourceServerConfiguration.class) + .run(context -> { + AzureActiveDirectoryResourceServerConfiguration bean = context + .getBean(AzureActiveDirectoryResourceServerConfiguration.class); + List> defaultValidator = bean.createDefaultValidator(); + assertThat(defaultValidator).isNotNull(); + assertThat(defaultValidator).hasSize(2); + }); + } + + @Test + public void testExistAudienceDefaultValidator() { + this.contextRunner + .withUserConfiguration(AzureActiveDirectoryResourceServerConfiguration.class) + .withPropertyValues("azure.activedirectory.app-id-uri=fake-app-id-uri") + .run(context -> { + AzureActiveDirectoryResourceServerConfiguration bean = context + .getBean(AzureActiveDirectoryResourceServerConfiguration.class); + List> defaultValidator = bean.createDefaultValidator(); + assertThat(defaultValidator).isNotNull(); + assertThat(defaultValidator).hasSize(3); + }); + } + + @Test + public void testCreateWebSecurityConfigurerAdapter() { + this.contextRunner + .withUserConfiguration(AzureActiveDirectoryResourceServerConfiguration.class) + .run(context -> { + WebSecurityConfigurerAdapter webSecurityConfigurerAdapter = context + .getBean(DefaultAzureOAuth2ResourceServerWebSecurityConfigurerAdapter.class); + assertThat(webSecurityConfigurerAdapter).isNotNull(); + }); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtAudienceValidatorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtAudienceValidatorTest.java new file mode 100644 index 000000000000..f841faaf79df --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtAudienceValidatorTest.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.autoconfigure.aad; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.azure.spring.aad.resource.server.validator.AzureJwtAudienceValidator; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +public class AzureJwtAudienceValidatorTest { + + final AADAuthenticationProperties aadAuthenticationProperties = mock(AADAuthenticationProperties.class); + final Jwt jwt = mock(Jwt.class); + final List audiences = new ArrayList<>(); + final List claimAudience = new ArrayList<>(); + + @Test + public void testClientIdExistAndSuccessVerify() { + when(aadAuthenticationProperties.getClientId()).thenReturn("fake-client-id"); + when(jwt.getClaim(AADTokenClaim.AUD)).thenReturn(claimAudience); + claimAudience.add("fake-client-id"); + audiences.add(aadAuthenticationProperties.getClientId()); + AzureJwtAudienceValidator azureJwtAudienceValidator = new AzureJwtAudienceValidator(audiences); + OAuth2TokenValidatorResult result = azureJwtAudienceValidator.validate(jwt); + + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testAppIdUriExistAndSuccessVerify() { + when(aadAuthenticationProperties.getClientId()).thenReturn("fake-app-id-uri"); + when(jwt.getClaim(AADTokenClaim.AUD)).thenReturn(claimAudience); + claimAudience.add("fake-app-id-uri"); + audiences.add(aadAuthenticationProperties.getClientId()); + AzureJwtAudienceValidator azureJwtAudienceValidator = new AzureJwtAudienceValidator(audiences); + OAuth2TokenValidatorResult result = azureJwtAudienceValidator.validate(jwt); + + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testAppIdUriExistAndClientIdAndSuccessVerify() { + when(aadAuthenticationProperties.getClientId()).thenReturn("fake-app-id-uri"); + when(aadAuthenticationProperties.getAppIdUri()).thenReturn("fake-client-id"); + when(jwt.getClaim(AADTokenClaim.AUD)).thenReturn(claimAudience); + //claimAudience.add("fake-client-id"); + claimAudience.add("fake-app-id-uri"); + audiences.add(aadAuthenticationProperties.getClientId()); + audiences.add(aadAuthenticationProperties.getAppIdUri()); + AzureJwtAudienceValidator azureJwtAudienceValidator = new AzureJwtAudienceValidator(audiences); + OAuth2TokenValidatorResult result = azureJwtAudienceValidator.validate(jwt); + + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtBearerTokenAuthenticationConverterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtBearerTokenAuthenticationConverterTest.java new file mode 100644 index 000000000000..cef153b3ac9b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtBearerTokenAuthenticationConverterTest.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.autoconfigure.aad; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.azure.spring.aad.resource.server.AzureJwtBearerTokenAuthenticationConverter; +import com.azure.spring.aad.resource.server.AzureOAuth2AuthenticatedPrincipal; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; + +public class AzureJwtBearerTokenAuthenticationConverterTest { + + private Jwt jwt = mock(Jwt.class); + private Map claims = new HashMap<>(); + private Map headers = new HashMap<>(); + + @Before + public void init() { + claims.put("iss", "fake-issuer"); + claims.put("tid", "fake-tid"); + headers.put("kid", "kg2LYs2T0CTjIfj4rt6JIynen38"); + when(jwt.getClaim("scp")).thenReturn("Order.read Order.write"); + when(jwt.getClaim("roles")).thenReturn("User.read User.write"); + when(jwt.getTokenValue()).thenReturn("fake-token-value"); + when(jwt.getIssuedAt()).thenReturn(Instant.now()); + when(jwt.getHeaders()).thenReturn(headers); + when(jwt.getExpiresAt()).thenReturn(Instant.MAX); + when(jwt.getClaims()).thenReturn(claims); + when(jwt.containsClaim("scp")).thenReturn(true); + } + + @Test + public void testCreateUserPrincipal() { + AzureJwtBearerTokenAuthenticationConverter azureJwtBearerTokenAuthenticationConverter + = new AzureJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = azureJwtBearerTokenAuthenticationConverter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AzureOAuth2AuthenticatedPrincipal.class); + AzureOAuth2AuthenticatedPrincipal principal = (AzureOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getClaims()).isNotEmpty(); + assertThat(principal.getIssuer()).isEqualTo(claims.get("iss")); + assertThat(principal.getTenantId()).isEqualTo(claims.get("tid")); + } + + @Test + public void testExtractDefaultScopeAuthorities() { + AzureJwtBearerTokenAuthenticationConverter azureJwtBearerTokenAuthenticationConverter + = new AzureJwtBearerTokenAuthenticationConverter(); + AbstractAuthenticationToken authenticationToken = azureJwtBearerTokenAuthenticationConverter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AzureOAuth2AuthenticatedPrincipal.class); + AzureOAuth2AuthenticatedPrincipal principal = (AzureOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + } + + @Test + public void testExtractCustomScopeAuthorities() { + when(jwt.containsClaim("roles")).thenReturn(true); + AzureJwtBearerTokenAuthenticationConverter azureJwtBearerTokenAuthenticationConverter + = new AzureJwtBearerTokenAuthenticationConverter("roles", "ROLE_"); + AbstractAuthenticationToken authenticationToken = azureJwtBearerTokenAuthenticationConverter.convert(jwt); + assertThat(authenticationToken.getPrincipal()).isExactlyInstanceOf(AzureOAuth2AuthenticatedPrincipal.class); + AzureOAuth2AuthenticatedPrincipal principal = (AzureOAuth2AuthenticatedPrincipal) authenticationToken + .getPrincipal(); + assertThat(principal.getAttributes()).isNotEmpty(); + assertThat(principal.getAttributes()).hasSize(2); + } + + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtIssuerValidatorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtIssuerValidatorTest.java new file mode 100644 index 000000000000..b1390dff0149 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AzureJwtIssuerValidatorTest.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.autoconfigure.aad; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.azure.spring.aad.resource.server.validator.AzureJwtIssuerValidator; +import org.junit.Test; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +public class AzureJwtIssuerValidatorTest { + + final AADAuthenticationProperties aadAuthenticationProperties = mock(AADAuthenticationProperties.class); + final Jwt jwt = mock(Jwt.class); + + @Test + public void testIssuerSuccessVerify() { + when(aadAuthenticationProperties.getTenantId()).thenReturn("fake-tenant-id"); + when(jwt.getClaim(AADTokenClaim.ISS)).thenReturn("https://sts.windows.net/fake-tenant-id/v2.0"); + + AzureJwtIssuerValidator azureJwtIssuerValidator = new AzureJwtIssuerValidator(); + OAuth2TokenValidatorResult result = azureJwtIssuerValidator.validate(jwt); + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void testIssuerFailureVerify() { + when(aadAuthenticationProperties.getTenantId()).thenReturn("common"); + when(jwt.getClaim(AADTokenClaim.ISS)).thenReturn("https://sts.failure.net/fake-tenant-id/v2.0"); + + AzureJwtIssuerValidator azureJwtIssuerValidator = new AzureJwtIssuerValidator(); + OAuth2TokenValidatorResult result = azureJwtIssuerValidator.validate(jwt); + assertThat(result).isNotNull(); + assertThat(result.getErrors()).isNotEmpty(); + } + +}