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?
+
+
+
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/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
new file mode 100644
index 000000000..773ced1ce
--- /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,134 @@
+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 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 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);
+ }
+ }
+
+ /**
+ * Create JWT assertion
+ *
+ * @throws AzureActiveDirectoryAssertionException If failed to create assertion.
+ */
+ public String createJwtAssertion() throws AzureActiveDirectoryAssertionException {
+ JWTClaimsSet claims = createJWTClaimsSet();
+ SignedJWT signedJwt = new SignedJWT(header, claims);
+ try {
+ signedJwt.sign(signer);
+ } catch (JOSEException exception) {
+ throw new AzureActiveDirectoryAssertionException("Failed to sign JWT.", exception);
+ }
+ 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..decedbc85
--- /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,58 @@
+package com.azure.spring.sample.reactive.servlet.oauth2.login.jwt.azure.activedirectory;
+
+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())) {
+ return null;
+ }
+
+ try {
+ return createParameters(registration);
+ } catch (AzureActiveDirectoryAssertionException exception) {
+ LOGGER.error("Failed to create parameters.", exception);
+ }
+ return null;
+ }
+
+ 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 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
new file mode 100644
index 000000000..9e04a6fe1
--- /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,101 @@
+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 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.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 AzureActiveDirectoryAssertionException {
+ 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 AzureActiveDirectoryAssertionException {
+ 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 AzureActiveDirectoryAssertionException {
+ 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..da65496ff
--- /dev/null
+++ b/aad/spring-security/servlet/oauth2/login-authenticate-using-private-key-jwt/src/main/resources/application.yml
@@ -0,0 +1,29 @@
+# Please fill these placeholders before running this application:
+# 1. ${tenant-id}
+# 2. ${client-1-client-id}
+# 3. ${client-1-certificate-path}
+# 4. ${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-authentication-method: private_key_jwt
+ 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..1ff08d6c6
--- /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,95 @@
+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 org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import javax.xml.bind.DatatypeConverter;
+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 AzureActiveDirectoryAssertionException {
+ 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 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 AzureActiveDirectoryAssertionException {
+ 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 = 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);
+ // The value of "x5t" is same to the "Thumbprint" in the Azure Portal.
+ assertEquals("D829DB4885D1C22B1207F72F533DFA8125861174",
+ DatatypeConverter.printHexBinary(Base64.getUrlDecoder().decode(assertionHeader.x5t)));
+
+ 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);
+ 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-----
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: