From fcbeff0306752d9ea032d29aaaafcfb5e39b4364 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 18 Feb 2022 17:23:41 +0800 Subject: [PATCH 1/6] Create sample: servlet/oauth2/login-authenticate-using-private-key-jwt. --- aad/spring-security/pom.xml | 1 + .../pom.xml | 41 ++++++ .../oauth2/login/jwt/SampleApplication.java | 12 ++ ...yCertificateSignedJwtAssertionFactory.java | 121 ++++++++++++++++++ ...ientAuthenticationParametersConverter.java | 60 +++++++++ .../WebSecurityConfiguration.java | 109 ++++++++++++++++ .../login/jwt/controller/HomeController.java | 13 ++ .../src/main/resources/application.yml | 28 ++++ ...CertificateSignedAssertionFactoryTest.java | 91 +++++++++++++ .../WebSecurityConfigurationTest.java | 18 +++ .../encrypted-private-key-and-certificate.pem | 53 ++++++++ 11 files changed, 547 insertions(+) create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/pom.xml create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/SampleApplication.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/controller/HomeController.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfigurationTest.java create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/resources/encrypted-private-key-and-certificate.pem diff --git a/aad/spring-security/pom.xml b/aad/spring-security/pom.xml index 1e4693c4e..72c1079a0 100644 --- a/aad/spring-security/pom.xml +++ b/aad/spring-security/pom.xml @@ -28,6 +28,7 @@ reactive/webflux/oauth2/spring-cloud-gateway/resource-server-1 reactive/webflux/oauth2/spring-cloud-gateway/resource-server-2 servlet/oauth2/login + servlet/oauth2/login-authenticate-using-private-key-jwt servlet/oauth2/client-access-resource-server/client servlet/oauth2/client-access-resource-server/resource-server servlet/oauth2/resource-server-check-permissions-by-claims-in-access-token/client diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/pom.xml b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/pom.xml new file mode 100644 index 000000000..3e389eacc --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + com.azure.spring + azure-spring-boot-samples + 1.0.0 + ../../../../../pom.xml + + + servlet-oauth2-login-authenticate-using-private-key-jwt + 1.0.0 + jar + + Spring-Security Sample: Servlet: OAuth2: Login + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-test + test + + + jakarta.xml.bind + jakarta.xml.bind-api + test + + + + diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/SampleApplication.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/SampleApplication.java new file mode 100644 index 000000000..971d1a400 --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/SampleApplication.java @@ -0,0 +1,12 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java new file mode 100644 index 000000000..89043bb9c --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java @@ -0,0 +1,121 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.springframework.util.Assert; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import java.util.UUID; + +/** + * A factory used to create Azure AD JWT assertion signed with a certificate. + * + * @author Rujun Chen + * @see + * Certificate credentials + * @since 2022-02-18 + */ +public class AzureActiveDirectoryCertificateSignedJwtAssertionFactory { + + private final JWSSigner signer; + private final JWSHeader header; + private final JWTClaimsSet templateClaims; + + /** + * @param file Path of file. The file should contain encrypted private key and certificate. And the file name should + * have ".pfx" as suffix. + * @param password The password of the encrypted private key. + */ + public AzureActiveDirectoryCertificateSignedJwtAssertionFactory(String file, String password, String tenantId, + String clientId) + throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, + UnrecoverableKeyException, JOSEException { + String fileExtension = file.substring(file.lastIndexOf(".") + 1); + Assert.isTrue("pfx".equals(fileExtension), "Only support file with '.pfx' extension."); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream(file), password.toCharArray()); + String alias = keyStore.aliases().nextElement(); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray()); + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias); + PublicKey publicKey = x509Certificate.getPublicKey(); + signer = createJWSSigner(publicKey, privateKey); + header = createJWSHeader(x509Certificate); + templateClaims = createTemplateJWTClaims(tenantId, clientId); + } + + public String createJwtAssertion() throws JOSEException { + JWTClaimsSet claims = createJWTClaimsSet(); + SignedJWT signedJwt = new SignedJWT(header, claims); + signedJwt.sign(signer); + return signedJwt.serialize(); + } + + private JWSSigner createJWSSigner(PublicKey publicKey, PrivateKey privateKey) throws JOSEException { + // Azure AD currently supports only RSA. + // Refs: https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-self-signed-certificate + JWK jwk = new RSAKey.Builder((RSAPublicKey) publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + return new DefaultJWSSignerFactory().createJWSSigner(jwk); + } + + @SuppressWarnings("deprecation") + private JWSHeader createJWSHeader(X509Certificate x509Certificate) throws CertificateEncodingException, + NoSuchAlgorithmException { + return new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .x509CertThumbprint(Base64URL.encode(getX5t(x509Certificate))) + .build(); + } + + private byte[] getX5t(X509Certificate cert) + throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] der = cert.getEncoded(); + digest.update(der); + return digest.digest(); + } + + private JWTClaimsSet createTemplateJWTClaims(String tenantId, String clientId) { + return new JWTClaimsSet.Builder() + .audience(String.format("https://login.microsoftonline.com/%s/v2.0", tenantId)) + .issuer(clientId) + .subject(clientId) + .build(); + } + + private JWTClaimsSet createJWTClaimsSet() { + Date currentTime = new Date(); + return new JWTClaimsSet.Builder(templateClaims) + .expirationTime(Date.from(currentTime.toInstant().plusSeconds(300))) // 5 minutes after currentTime. + .jwtID(UUID.randomUUID().toString()) + .notBeforeTime(currentTime) + .issueTime(currentTime) + .build(); + } + + +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java new file mode 100644 index 000000000..04e8406ce --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java @@ -0,0 +1,60 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory; + +import com.nimbusds.jose.JOSEException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.Map; + +public class AzureActiveDirectoryJwtClientAuthenticationParametersConverter + implements Converter> { + + private final static Logger LOGGER = + LoggerFactory.getLogger(AzureActiveDirectoryJwtClientAuthenticationParametersConverter.class); + private static final String CLIENT_ASSERTION_TYPE_VALUE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + + private final Map factories; + + public AzureActiveDirectoryJwtClientAuthenticationParametersConverter( + Map factories) { + this.factories = factories; + } + + @Override + public MultiValueMap convert(T authorizationGrantRequest) { + Assert.notNull(authorizationGrantRequest, "authorizationGrantRequest cannot be null"); + + ClientRegistration registration = authorizationGrantRequest.getClientRegistration(); + ClientAuthenticationMethod method = registration.getClientAuthenticationMethod(); + if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(registration.getClientAuthenticationMethod()) + && !ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) { + return null; + } + + try { + return createParameters(registration); + } catch (JOSEException e) { + LOGGER.error("Failed to create parameters.", e); + } + return null; + } + + private MultiValueMap createParameters(ClientRegistration registration) throws JOSEException { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE); + parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION, createAssertion(registration)); + return parameters; + } + + private String createAssertion(ClientRegistration registration) throws JOSEException { + return factories.get(registration.getRegistrationId()).createJwtAssertion(); + } +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java new file mode 100644 index 000000000..8c59c72a6 --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java @@ -0,0 +1,109 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.configuration; + +import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryCertificateSignedJwtAssertionFactory; +import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryJwtClientAuthenticationParametersConverter; +import com.nimbusds.jose.JOSEException; +import org.springframework.core.env.Environment; +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.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@EnableWebSecurity +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + private static final Pattern ISSUER_URI_PATTERN = Pattern.compile("https://login.microsoftonline.com/(.*?)/v2.0"); + + private final Environment environment; + private final ClientRegistrationRepository repository; + + public WebSecurityConfiguration(Environment environment, ClientRegistrationRepository repository) { + this.environment = environment; + this.repository = repository; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.oauth2Login() + .tokenEndpoint() + .accessTokenResponseClient(accessTokenResponseClient(Collections.singletonList("client-1"), repository)) + .and() + .and() + .authorizeRequests() + .anyRequest().authenticated(); + // @formatter:off + } + + private DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient( + List registrationIds, ClientRegistrationRepository repository + ) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, + JOSEException { + OAuth2AuthorizationCodeGrantRequestEntityConverter converter = + new OAuth2AuthorizationCodeGrantRequestEntityConverter(); + converter.addParametersConverter( + new AzureActiveDirectoryJwtClientAuthenticationParametersConverter<>(createFactoryMap(registrationIds, repository))); + DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient(); + client.setRequestEntityConverter(converter); + return client; + } + + private Map createFactoryMap( + List registrationIds, ClientRegistrationRepository repository + ) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, + JOSEException { + Map factories = new HashMap<>(); + for (String registrationId: registrationIds) { + AzureActiveDirectoryCertificateSignedJwtAssertionFactory factory = createFactory(registrationId, repository); + if (factory != null) { + factories.put(registrationId, factory); + } + } + return factories; + } + + private AzureActiveDirectoryCertificateSignedJwtAssertionFactory createFactory( + String registrationId, ClientRegistrationRepository repository + ) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, + JOSEException { + String clientCertificatePath = environment.getProperty( + String.format("spring.security.oauth2.client.registration.%s.client-certificate-path", registrationId)); + if (!StringUtils.hasText(clientCertificatePath)) { + return null; + } + String clientCertificatePassword = environment.getProperty( + String.format("spring.security.oauth2.client.registration.%s.client-certificate-password", registrationId)); + if (!StringUtils.hasText(clientCertificatePassword)) { + return null; + } + ClientRegistration registration = repository.findByRegistrationId(registrationId); + String tenantId = getTenantIdFromIssuerUri(registration.getProviderDetails().getIssuerUri()); + String clientId = registration.getClientId(); + return new AzureActiveDirectoryCertificateSignedJwtAssertionFactory( + clientCertificatePath, clientCertificatePassword, tenantId, clientId); + } + + static String getTenantIdFromIssuerUri(String issuerUri) { + Matcher matcher = ISSUER_URI_PATTERN.matcher(issuerUri); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/controller/HomeController.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/controller/HomeController.java new file mode 100644 index 000000000..eac08789f --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/controller/HomeController.java @@ -0,0 +1,13 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HomeController { + + @GetMapping("/") + public String home() { + return "Hello, this is client-1."; + } +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml new file mode 100644 index 000000000..c58e4f30a --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml @@ -0,0 +1,28 @@ +# Please fill these placeholders before running this application: +# 1. ${tenant-id} +# 2. ${client-1-client-id} +# 4. ${resource-server-1-client-id} +# 5. ${client-1-certificate-password} + +logging: + level: + root: DEBUG +server: + port: 8080 +spring: + security: + oauth2: + client: + provider: # Refs: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2login-common-oauth2-provider + azure-active-directory: + issuer-uri: https://login.microsoftonline.com/${tenant-id}/v2.0 # Refs: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webflux-oauth2-login-openid-provider-configuration + registration: + client-1: + provider: azure-active-directory + client-id: ${client-1-client-id} + client-certificate-path: ${client-1-certificate-path} + client-certificate-password: ${client-1-certificate-password} + scope: openid, profile + redirect-uri: http://localhost:8080/login/oauth2/code/ + profiles: + active: develop diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java new file mode 100644 index 000000000..182c78df5 --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java @@ -0,0 +1,91 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import javax.xml.bind.DatatypeConverter; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class AzureActiveDirectoryCertificateSignedAssertionFactoryTest { + + private static final String TEST_TENANT_ID = "test-tenant-id"; + private static final String TEST_CLIENT_ID = "test-client-id"; + + @Test + public void testPfx() throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, + NoSuchAlgorithmException, JOSEException { + test(new AzureActiveDirectoryCertificateSignedJwtAssertionFactory( + "src/test/resources/encrypted-private-key-and-certificate.pfx", "myPassword1", TEST_TENANT_ID, + TEST_CLIENT_ID)); + } + + // TODO support pem file. + @Disabled("Pem file is not supported now.") + @Test + public void testPem() throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, + NoSuchAlgorithmException, JOSEException { + test(new AzureActiveDirectoryCertificateSignedJwtAssertionFactory( + "src/test/resources/encrypted-private-key-and-certificate.pem", "myPassword1", TEST_TENANT_ID, + TEST_CLIENT_ID)); + } + + public void test(AzureActiveDirectoryCertificateSignedJwtAssertionFactory factory) throws JOSEException, + JsonProcessingException { + String assertion = factory.createJwtAssertion(); + String[] chunks = assertion.split("\\."); + Base64.Decoder decoder = Base64.getUrlDecoder(); + String header = new String(decoder.decode(chunks[0])); + String payload = new String(decoder.decode(chunks[1])); + String signature = new String(decoder.decode(chunks[2])); + assertNotNull(header); + assertNotNull(payload); + assertNotNull(signature); + + ObjectMapper mapper = new ObjectMapper(); + AssertionHeader assertionHeader = mapper.readValue(header, AssertionHeader.class); + assertNotNull(assertionHeader); + assertEquals("RS256", assertionHeader.alg); + assertEquals("JWT", assertionHeader.typ); + // The value of "x5t" is same to the "Thumbprint" in the Azure Portal. + assertEquals("D829DB4885D1C22B1207F72F533DFA8125861174", + DatatypeConverter.printHexBinary(Base64.getUrlDecoder().decode(assertionHeader.x5t))); + + AssertionClaims assertionClaims = mapper.readValue(payload, AssertionClaims.class); + assertNotNull(assertionClaims); + assertEquals(String.format("https://login.microsoftonline.com/%s/v2.0", TEST_TENANT_ID), assertionClaims.aud); + assertNotNull(assertionClaims.exp); + assertEquals(TEST_CLIENT_ID, assertionClaims.iss); + assertNotNull(assertionClaims.jti); + assertNotNull(assertionClaims.nbf); + assertEquals(TEST_CLIENT_ID, assertionClaims.sub); + assertNotNull(assertionClaims.iat); + } + + private static class AssertionHeader { + public String alg; + public String typ; + public String x5t; + } + + private static class AssertionClaims { + public String aud; + public String exp; + public String iss; + public String jti; + public String nbf; + public String sub; + public String iat; + } + +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfigurationTest.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfigurationTest.java new file mode 100644 index 000000000..b62188397 --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfigurationTest.java @@ -0,0 +1,18 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.configuration; + +import org.junit.jupiter.api.Test; + +import static com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.configuration.WebSecurityConfiguration.getTenantIdFromIssuerUri; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WebSecurityConfigurationTest { + + @Test + public void getTenantFromIssuerUriTest() { + String tenantId = getTenantIdFromIssuerUri("https://login.microsoftonline.com/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaa/v2.0"); + assertEquals("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaa", tenantId); + + tenantId = getTenantIdFromIssuerUri("https://login.microsoftonline.com/11111111-1111-1111-1111-11111111/v2.0"); + assertEquals("11111111-1111-1111-1111-11111111", tenantId); + } +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/resources/encrypted-private-key-and-certificate.pem b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/resources/encrypted-private-key-and-certificate.pem new file mode 100644 index 000000000..bf8d06692 --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/resources/encrypted-private-key-and-certificate.pem @@ -0,0 +1,53 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQICY05lJ0wtIECAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECEZSNDV/wmuOBIIEyAzGoFPLy3fv +LVzrCZKCdebI+sUe7ivYhPRMHMOCpr9tKtaYgKzWNBrcwqzz59HQW3c0IZE9pavb +6sB8szAk5xwEfUGBX0hSDJP6dZByhds6w5jhnl+QKt4+4MfQOR1wZHuBhV1+X85e +TWZWV8QOurPMCUvM/qp+wSRw6EOEIIqYJuYbEnpt66zXbsvyv0cnzER4KSGe1QZt +iIM3gVAKTDoP+gr1HdAqtSj2C2bxkzZzRfnVJSkxnqbg8lWeXXkZ/tzjJeoTCL1z +TExVEIc6ZXzkM/cRD1ABXBRDb6yV9Sc0MO2FlVFkJEWAh5Rxpj7A7ovK9GvbL2XC +qJjmqTXNHhEzAE5FIsKYfpkd8wrNX21K/Z+3ahUEB2jFdjEfJl1HaKN8zqV1jLt1 +rw2St2sb2djSnTFqQSVroUOSSShEt7j6vz/7JJ4MrL2jnw6/SP7yjR/PH1PKri/1 +dxT13/1Q80M/tcIHOgF7b4NCY1NhUAvME7RWRGUIFEkvHE1Tvbna8SeMoq88eqT7 +IKBpMPDqXvVwo3+Inpf5/a6a/p/HdHW8S+kCCdqeMBeJkZ2prthFUtQWrnNbdyCx +OFPWwGmwLK06bZO4vn3Zqf9oO7suNQfWb3R0koJJWE6UEHf6utYfe6S+3Cmv8p5Z +snMYV5qVfRaKeSEq7YwQN+NfiTfsCy2vtKTM+4B8128JW3mwQOxdMswJ/0ORrmFa +X8H4uQjY+GRu4EiBE4hP8VBv2Y/AJyOKVXl3VZGh2kUyrfHAdBpTrk0p4N+6os4e +rY2xLQ6jBLbLdVAIFJYJu60wfGyjxhjIpTV72TjBLBm4wFBXW3crGw9DTaD0FC1a +tGmCWp5w+VsPKHLvXcfnRuntpeyXi8/7eHQCt0GrKHtrHZuIOB5lWDKKS+DmG7Xk +Uma1HwUSKshL1tq+SCu9wMOW8qQuASyBX5eonWs0ML5iVIvlIs+bCyb3Tjf+75V0 +Psyo7y3QDBSDyoID5yOYn1oltg5njPlcRpa7KvDeFJSBb+DDTsM+c1W61yhFJf31 +dQyECdoFoGjJYAMOuBamdB49eP4rITFR3oipBg/+KBsRrSR0dSj2lR9vo3D3APCP +2ik8O/TlcW7StLyA3zQ2Ke8n40vXc8GjVivKpv/4vSgwYf2remk0zD2RKarm/nzF +NYAn6R+7HarL+x1NBQd2wtKm0vvztb7zXdqI3OG3SszZTU3qW+AAkGwdLhYppjk5 +piEzHHagdf7kZ3IZbO4KnnrMhTMZdgLGWkH+hUjk9yBucyhpMtgc+UuUjd0pILDq +pxrpCGjjv04StpwUok0Z4VUNWuELXfPLn8EkbZWVToS8SRNk4D/IBKV1BsKqN4Re +b530DzGI/eKZqe8irjVzFQytP54Ebkmm8jU/j/hSDKb0+COlou1w4LSQive4MZT2 +QkTeOIfHE8ARYCDmfJbZn7NUe8nF7KNRH0FwznRPqAHyHOksVDUcLTQSZan7xEnV +DZncyhtpLjuCW+rUCCwRueU/e+MUdMgZvHcdjDZr27xHnJD8AFkGaxZBdrEm/IXQ +CFQ+epmkG5rfVjHSThuW5vSVcw/NQ4LyqZWVoB26XQ1u81VA8PQc8qe8z6rpU83/ +x0x6DOb+3dipqr1wTAbNXw== +-----END ENCRYPTED PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIUYCzXhaSrYqN4EGW5OI/dxdUrUfQwDQYJKoZIhvcNAQEL +BQAwdzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u +ZG9uMRgwFgYDVQQKDA9HbG9iYWwgU2VjdXJpdHkxFjAUBgNVBAsMDUlUIERlcGFy +dG1lbnQxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTIyMDIxODA3MDEwOFoXDTIz +MDIxODA3MDEwOFowdzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0G +A1UEBwwGTG9uZG9uMRgwFgYDVQQKDA9HbG9iYWwgU2VjdXJpdHkxFjAUBgNVBAsM +DUlUIERlcGFydG1lbnQxFDASBgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyIITpNkH+7IwpgHmUC2T+M42aC9FModDBt26 +JEBwcfpys4cmlg42QlDcJVXXF1avoYQbGnFW3T9q77xHdftUrSqg0iUn5pDbSytY +Kmd16SD3n4NySJnpI+NgrTQobzLS+NjrsaKKuXhNkXNmZ4lRw9hZ2FnCVqWlylLs +FQkc9Jkkrn+Le2wX74EXUjB5FfKMaeJyXcCHztYhuCqPs3jzTCnE//3Iz6Cuew9s +JanwB1yNIUiBstaKs2h2lB3YsXNPkyIRvHDX0PXWbQ9p+FE1jYDiwhijamHF4yeM +shKDD6PY8fv8dFCuLitpuUYwBAFsckWwoq5K0/FX6xMW/yjHHwIDAQABo1MwUTAd +BgNVHQ4EFgQUUbMM550NJZhKpebdnbXvCstzXrIwHwYDVR0jBBgwFoAUUbMM550N +JZhKpebdnbXvCstzXrIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEAyAB3YL7vf+SB8+zZpiTnG8x3d8ki3OZDTcYyNf44cxbwO6RyubrxTAIBzKfD +8zs+q/i37IRa6sSGDCef77Bl39PcWyCzYM1IJwTbijZRjLfTxHh6J1yaa1ts8FXi +ISDy++FuPMffIHU4c5Ct64pLBIzu6nbhCSd59YVM0tSvhkfnkp7byNgk3sGF8g8E +rtM6Ioja1uC5tcL2U95Yaj58MAoG4nYpFZZPsymoroRtxG0FUuzeuphAOsJNQDxt +5O4bNgnX4/ZUg6QEJzhALLuZKY1TeN9pvEzHvRkns87Ogqx+7Y6uaenFLf9J+PsH +A0akkAzd8wteguDyI0RltuKZOg== +-----END CERTIFICATE----- From 79be83a9732427071f4b47da7b3e32091ad96a76 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 18 Feb 2022 17:39:11 +0800 Subject: [PATCH 2/6] Create README for sample: servlet/oauth2/login-authenticate-using-private-key-jwt. --- ...ogin-authenticate-using-private-key-jwt.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 aad/spring-security/docs/servlet/oauth2/login-authenticate-using-private-key-jwt.md diff --git a/aad/spring-security/docs/servlet/oauth2/login-authenticate-using-private-key-jwt.md b/aad/spring-security/docs/servlet/oauth2/login-authenticate-using-private-key-jwt.md new file mode 100644 index 000000000..8bdc81cb7 --- /dev/null +++ b/aad/spring-security/docs/servlet/oauth2/login-authenticate-using-private-key-jwt.md @@ -0,0 +1,100 @@ +- [1. About](#1-about) +- [2. Get sample applications](#2-get-sample-applications) +- [3. Create resources in Azure](#3-create-resources-in-azure) + * [3.1. Create a tenant](#31-create-a-tenant) + * [3.2. Add a new user](#32-add-a-new-user) + * [3.3. Register client-1](#33-register-client-1) + * [3.4. Add a redirect URI for client-1](#34-add-a-redirect-uri-for-client-1) + * [3.5. Add a certificate for client-1](#35-add-a-certificate-for-client-1) + + [3.5.1. Create certificate.pem and encrypted-private-key-and-certificate.pfx](#351-create-certificatepem-and-encrypted-private-key-and-certificatepfx) + + [3.5.2. Upload certificate](#352-upload-certificate) +- [4. Run sample applications](#4-run-sample-applications) +- [5. Homework](#5-homework) + + + + + + + + +# 1. About + +This section shows the basic scenario: +1. Login by [OAuth 2.0 authorization code flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) and [request an access token with a certificate credential](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential). + +# 2. Get sample applications +Get samples applications from in GitHub: [login-authenticate-using-private-key-jwt](../../../servlet/oauth2/login-authenticate-using-private-key-jwt). + +# 3. Create resources in Azure + +## 3.1. Create a tenant +Read [document about creating an Azure AD tenant](https://docs.microsoft.com/azure/active-directory/develop/quickstart-create-new-tenant#create-a-new-azure-ad-tenant), create a new tenant. Get the tenant-id: **${tenant-id}**. + +## 3.2. Add a new user +Read [document about adding users](https://docs.microsoft.com/azure/active-directory/fundamentals/add-users-azure-active-directory), add a new user: **user-1@${tenant-name}.com**. Get the user's password. + +## 3.3. Register client-1 +Read [document about registering an application](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app), register an application named **client-1**. Get the client-id: **${client-1-client-id}**. + +## 3.4. Add a redirect URI for client-1 +Read [document about adding a redirect URI](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app#add-a-redirect-uri), add redirect URI: **http://localhost:8080/login/oauth2/code/**. + +## 3.5. Add a certificate for client-1 + +### 3.5.1. Create certificate.pem and encrypted-private-key-and-certificate.pfx + +Use this command to create **encrypted-private-key.pem**: +```shell +openssl genrsa \ +-aes128 \ +-passout pass:myPassword1 \ +-out encrypted-private-key.pem \ +2048 +``` + +Use this command to create **certificate.pem**: +```shell +openssl req \ +-new -x509 \ +-sha256 \ +-subj '/C=GB/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com' \ +-days 365 \ +-key encrypted-private-key.pem \ +-passin pass:myPassword1 \ +-out certificate.pem +``` + +Use this command to create **encrypted-private-key-and-certificate.pfx**: +```shell +openssl pkcs12 \ +-inkey encrypted-private-key.pem \ +-passin pass:myPassword1 \ +-passout pass:myPassword1 \ +-in certificate.pem \ +-export \ +-out encrypted-private-key-and-certificate.pfx +``` + +You can refer to [document about create self-signed certificate](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-self-signed-certificate) to get more information. + +### 3.5.2. Upload certificate +Read [document about adding a certificate](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-certificate), upload certificate **certificate.pem**. + +# 4. Run sample applications + 1. Open sample application: [login-authenticate-using-private-key-jwt](../../../servlet/oauth2/login-authenticate-using-private-key-jwt), fill the placeholders in **application.yml**, then run the application. + 2. . Open browser(for example: [Edge](https://www.microsoft.com/edge?r=1)), close all [InPrivate window](https://support.microsoft.com/microsoft-edge/browse-inprivate-in-microsoft-edge-cd2c9a48-0bc4-b98e-5e46-ac40c84e27e2), and open a new InPrivate window. + 3. Access **http://localhost:8080**, it will redirect to Microsoft login page. Input username and password (update password if it requests you to), it will return permission request page. click **Accept**, then it will return **Hello, this is client-1.**. This means we log in successfully. + 4. Access **http://localhost:8080/**, it will return **Hello, this is client-1.**, which means login successfully. + +# 5. Homework + 1. Read [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749). + 2. Read [document about OAuth 2.0 and OpenID Connect protocols on the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/active-directory-v2-protocols). + 3. Read [document about Microsoft identity platform and OpenID Connect protocol](https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc) + 4. Read [document about Microsoft identity platform and OAuth 2.0 authorization code flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow). + 5. Read [document about application authentication certificate credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials). + 6. . Investigate each item's purpose in the sample projects' **application.yml**. + 7. In [login-authenticate-using-private-key-jwt](../../../servlet/oauth2/login-authenticate-using-private-key-jwt)'s **application.yml**, the property **spring.security.oauth2.client.registration.scope** contains **openid**, **profile**. what will happen if we delete these scopes? + + + From 31a12bfce59e0786268bd58cf1a7fe895bf934c9 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 18 Feb 2022 17:44:26 +0800 Subject: [PATCH 3/6] Fix error in application.yml. --- .../src/main/resources/application.yml | 4 ++-- .../servlet/oauth2/login/src/main/resources/application.yml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml index c58e4f30a..1a4fe2938 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml @@ -1,8 +1,8 @@ # Please fill these placeholders before running this application: # 1. ${tenant-id} # 2. ${client-1-client-id} -# 4. ${resource-server-1-client-id} -# 5. ${client-1-certificate-password} +# 3. ${client-1-certificate-path} +# 4. ${client-1-certificate-password} logging: level: diff --git a/aad/spring-security/servlet/oauth2/login/src/main/resources/application.yml b/aad/spring-security/servlet/oauth2/login/src/main/resources/application.yml index 04d2b3d8f..a5fbd2990 100644 --- a/aad/spring-security/servlet/oauth2/login/src/main/resources/application.yml +++ b/aad/spring-security/servlet/oauth2/login/src/main/resources/application.yml @@ -2,7 +2,6 @@ # 1. ${tenant-id} # 2. ${client-1-client-id} # 3. ${client-1-client-secret} -# 4. ${resource-server-1-client-id} logging: level: From 47eef1af3db8de97881e0ea03295fac770fb2fed Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 22 Feb 2022 17:24:24 +0800 Subject: [PATCH 4/6] Delete CLIENT_SECRET_JWT in AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java. --- ...iveDirectoryJwtClientAuthenticationParametersConverter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java index 04e8406ce..502547528 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java @@ -34,8 +34,7 @@ public MultiValueMap convert(T authorizationGrantRequest) { ClientRegistration registration = authorizationGrantRequest.getClientRegistration(); ClientAuthenticationMethod method = registration.getClientAuthenticationMethod(); - if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(registration.getClientAuthenticationMethod()) - && !ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) { + if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(registration.getClientAuthenticationMethod())) { return null; } From 3ec778c48e08ae7ffd154ba20d79b2ef841fde31 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 22 Feb 2022 17:25:55 +0800 Subject: [PATCH 5/6] Add "client-authentication-method" in application.yml. --- .../src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml index 1a4fe2938..da65496ff 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml @@ -20,6 +20,7 @@ spring: client-1: provider: azure-active-directory client-id: ${client-1-client-id} + client-authentication-method: private_key_jwt client-certificate-path: ${client-1-certificate-path} client-certificate-password: ${client-1-certificate-password} scope: openid, profile From 172060c8dced8759cb847ce2ca54cf3ab456acc6 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 23 Feb 2022 14:36:03 +0800 Subject: [PATCH 6/6] Create AzureActiveDirectoryAssertionException to hold all exceptions about assertion. --- ...zureActiveDirectoryAssertionException.java | 22 ++++++++ ...yCertificateSignedJwtAssertionFactory.java | 51 ++++++++++++------- ...ientAuthenticationParametersConverter.java | 9 ++-- .../WebSecurityConfiguration.java | 16 ++---- ...CertificateSignedAssertionFactoryTest.java | 32 +++++++----- 5 files changed, 80 insertions(+), 50 deletions(-) create mode 100644 aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryAssertionException.java diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryAssertionException.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryAssertionException.java new file mode 100644 index 000000000..f38793089 --- /dev/null +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryAssertionException.java @@ -0,0 +1,22 @@ +package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory; + +/** + * This exception is thrown when failed to create Azure Active Directory jwt assertion. + * + * @author Rujun Chen + * @since 2022-02-23 + */ +public class AzureActiveDirectoryAssertionException extends Exception { + + /** + * Creates a {@code AzureActiveDirectoryAssertionException} with the specified detail message and cause. + * + * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code + * null} value is permitted, and indicates that the cause is nonexistent or unknown.) + * @since 2022-02-23 + */ + public AzureActiveDirectoryAssertionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java index 89043bb9c..773ced1ce 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedJwtAssertionFactory.java @@ -44,31 +44,44 @@ public class AzureActiveDirectoryCertificateSignedJwtAssertionFactory { private final JWTClaimsSet templateClaims; /** - * @param file Path of file. The file should contain encrypted private key and certificate. And the file name should - * have ".pfx" as suffix. - * @param password The password of the encrypted private key. + * @param file Path of certificate file. The file should contain encrypted private key and certificate. + * And the file name should have ".pfx" as suffix. + * @param password The password of the encrypted private key in certificate file. + * @throws AzureActiveDirectoryAssertionException if failed to create factory. */ public AzureActiveDirectoryCertificateSignedJwtAssertionFactory(String file, String password, String tenantId, - String clientId) - throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, - UnrecoverableKeyException, JOSEException { - String fileExtension = file.substring(file.lastIndexOf(".") + 1); - Assert.isTrue("pfx".equals(fileExtension), "Only support file with '.pfx' extension."); - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream(file), password.toCharArray()); - String alias = keyStore.aliases().nextElement(); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray()); - X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias); - PublicKey publicKey = x509Certificate.getPublicKey(); - signer = createJWSSigner(publicKey, privateKey); - header = createJWSHeader(x509Certificate); - templateClaims = createTemplateJWTClaims(tenantId, clientId); + String clientId) throws AzureActiveDirectoryAssertionException { + try { + String fileExtension = file.substring(file.lastIndexOf(".") + 1); + Assert.isTrue("pfx".equals(fileExtension), "Only support file with '.pfx' extension."); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream(file), password.toCharArray()); + String alias = keyStore.aliases().nextElement(); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray()); + X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate(alias); + PublicKey publicKey = x509Certificate.getPublicKey(); + signer = createJWSSigner(publicKey, privateKey); + header = createJWSHeader(x509Certificate); + templateClaims = createTemplateJWTClaims(tenantId, clientId); + } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException | + UnrecoverableKeyException | JOSEException exception) { + throw new AzureActiveDirectoryAssertionException("Failed to create factory.", exception); + } } - public String createJwtAssertion() throws JOSEException { + /** + * Create JWT assertion + * + * @throws AzureActiveDirectoryAssertionException If failed to create assertion. + */ + public String createJwtAssertion() throws AzureActiveDirectoryAssertionException { JWTClaimsSet claims = createJWTClaimsSet(); SignedJWT signedJwt = new SignedJWT(header, claims); - signedJwt.sign(signer); + try { + signedJwt.sign(signer); + } catch (JOSEException exception) { + throw new AzureActiveDirectoryAssertionException("Failed to sign JWT.", exception); + } return signedJwt.serialize(); } diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java index 502547528..decedbc85 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryJwtClientAuthenticationParametersConverter.java @@ -1,6 +1,5 @@ package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory; -import com.nimbusds.jose.JOSEException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.convert.converter.Converter; @@ -40,20 +39,20 @@ public MultiValueMap convert(T authorizationGrantRequest) { try { return createParameters(registration); - } catch (JOSEException e) { - LOGGER.error("Failed to create parameters.", e); + } catch (AzureActiveDirectoryAssertionException exception) { + LOGGER.error("Failed to create parameters.", exception); } return null; } - private MultiValueMap createParameters(ClientRegistration registration) throws JOSEException { + private MultiValueMap createParameters(ClientRegistration registration) throws AzureActiveDirectoryAssertionException { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE); parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION, createAssertion(registration)); return parameters; } - private String createAssertion(ClientRegistration registration) throws JOSEException { + private String createAssertion(ClientRegistration registration) throws AzureActiveDirectoryAssertionException { return factories.get(registration.getRegistrationId()).createJwtAssertion(); } } diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java index 8c59c72a6..9e04a6fe1 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/configuration/WebSecurityConfiguration.java @@ -1,8 +1,8 @@ package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.configuration; +import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryAssertionException; import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryCertificateSignedJwtAssertionFactory; import com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory.AzureActiveDirectoryJwtClientAuthenticationParametersConverter; -import com.nimbusds.jose.JOSEException; import org.springframework.core.env.Environment; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -13,11 +13,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.util.StringUtils; -import java.io.IOException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -52,8 +47,7 @@ protected void configure(HttpSecurity http) throws Exception { private DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient( List registrationIds, ClientRegistrationRepository repository - ) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, - JOSEException { + ) throws AzureActiveDirectoryAssertionException { OAuth2AuthorizationCodeGrantRequestEntityConverter converter = new OAuth2AuthorizationCodeGrantRequestEntityConverter(); converter.addParametersConverter( @@ -65,8 +59,7 @@ private DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient( private Map createFactoryMap( List registrationIds, ClientRegistrationRepository repository - ) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, - JOSEException { + ) throws AzureActiveDirectoryAssertionException { Map factories = new HashMap<>(); for (String registrationId: registrationIds) { AzureActiveDirectoryCertificateSignedJwtAssertionFactory factory = createFactory(registrationId, repository); @@ -79,8 +72,7 @@ private Map cr private AzureActiveDirectoryCertificateSignedJwtAssertionFactory createFactory( String registrationId, ClientRegistrationRepository repository - ) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, - JOSEException { + ) throws AzureActiveDirectoryAssertionException { String clientCertificatePath = environment.getProperty( String.format("spring.security.oauth2.client.registration.%s.client-certificate-path", registrationId)); if (!StringUtils.hasText(clientCertificatePath)) { diff --git a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java index 182c78df5..1ff08d6c6 100644 --- a/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java +++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/test/java/com/azure/spring/sample/reactive/servlet/oauth2/login/jwt/azure/activedirectory/AzureActiveDirectoryCertificateSignedAssertionFactoryTest.java @@ -2,16 +2,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.JOSEException; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import javax.xml.bind.DatatypeConverter; -import java.io.IOException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.util.Base64; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -23,8 +17,7 @@ public class AzureActiveDirectoryCertificateSignedAssertionFactoryTest { private static final String TEST_CLIENT_ID = "test-client-id"; @Test - public void testPfx() throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, - NoSuchAlgorithmException, JOSEException { + public void testPfx() throws AzureActiveDirectoryAssertionException { test(new AzureActiveDirectoryCertificateSignedJwtAssertionFactory( "src/test/resources/encrypted-private-key-and-certificate.pfx", "myPassword1", TEST_TENANT_ID, TEST_CLIENT_ID)); @@ -33,15 +26,14 @@ public void testPfx() throws UnrecoverableKeyException, CertificateException, Ke // TODO support pem file. @Disabled("Pem file is not supported now.") @Test - public void testPem() throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, - NoSuchAlgorithmException, JOSEException { + public void testPem() throws AzureActiveDirectoryAssertionException { test(new AzureActiveDirectoryCertificateSignedJwtAssertionFactory( "src/test/resources/encrypted-private-key-and-certificate.pem", "myPassword1", TEST_TENANT_ID, TEST_CLIENT_ID)); } - public void test(AzureActiveDirectoryCertificateSignedJwtAssertionFactory factory) throws JOSEException, - JsonProcessingException { + public void test(AzureActiveDirectoryCertificateSignedJwtAssertionFactory factory) + throws AzureActiveDirectoryAssertionException { String assertion = factory.createJwtAssertion(); String[] chunks = assertion.split("\\."); Base64.Decoder decoder = Base64.getUrlDecoder(); @@ -53,7 +45,13 @@ public void test(AzureActiveDirectoryCertificateSignedJwtAssertionFactory factor assertNotNull(signature); ObjectMapper mapper = new ObjectMapper(); - AssertionHeader assertionHeader = mapper.readValue(header, AssertionHeader.class); + AssertionHeader assertionHeader = null; + try { + assertionHeader = mapper.readValue(header, AssertionHeader.class); + } catch (JsonProcessingException exception) { + // It's OK to print stacktrace here, because it's just test code. + exception.printStackTrace(); + } assertNotNull(assertionHeader); assertEquals("RS256", assertionHeader.alg); assertEquals("JWT", assertionHeader.typ); @@ -61,7 +59,13 @@ public void test(AzureActiveDirectoryCertificateSignedJwtAssertionFactory factor assertEquals("D829DB4885D1C22B1207F72F533DFA8125861174", DatatypeConverter.printHexBinary(Base64.getUrlDecoder().decode(assertionHeader.x5t))); - AssertionClaims assertionClaims = mapper.readValue(payload, AssertionClaims.class); + AssertionClaims assertionClaims = null; + try { + assertionClaims = mapper.readValue(payload, AssertionClaims.class); + } catch (JsonProcessingException exception) { + // It's OK to print stacktrace here, because it's just test code. + exception.printStackTrace(); + } assertNotNull(assertionClaims); assertEquals(String.format("https://login.microsoftonline.com/%s/v2.0", TEST_TENANT_ID), assertionClaims.aud); assertNotNull(assertionClaims.exp);