Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/create sample about login authenticate using private key jwt #196

Original file line number Diff line number Diff line change
@@ -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?



1 change: 1 addition & 0 deletions aad/spring-security/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<module>reactive/webflux/oauth2/spring-cloud-gateway/resource-server-1</module>
<module>reactive/webflux/oauth2/spring-cloud-gateway/resource-server-2</module>
<module>servlet/oauth2/login</module>
<module>servlet/oauth2/login-authenticate-using-private-key-jwt</module>
<module>servlet/oauth2/client-access-resource-server/client</module>
<module>servlet/oauth2/client-access-resource-server/resource-server</module>
<module>servlet/oauth2/resource-server-check-permissions-by-claims-in-access-token/client</module>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.azure.spring</groupId>
<artifactId>azure-spring-boot-samples</artifactId>
<version>1.0.0</version>
<relativePath>../../../../../pom.xml</relativePath>
</parent>

<artifactId>servlet-oauth2-login-authenticate-using-private-key-jwt</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>Spring-Security Sample: Servlet: OAuth2: Login</name>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials">
* Certificate credentials</a>
* @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();
}


}
Original file line number Diff line number Diff line change
@@ -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<T extends AbstractOAuth2AuthorizationGrantRequest>
implements Converter<T, MultiValueMap<String, String>> {

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<String, AzureActiveDirectoryCertificateSignedJwtAssertionFactory> factories;

public AzureActiveDirectoryJwtClientAuthenticationParametersConverter(
Map<String, AzureActiveDirectoryCertificateSignedJwtAssertionFactory> factories) {
this.factories = factories;
}

@Override
public MultiValueMap<String, String> 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<String, String> createParameters(ClientRegistration registration) throws AzureActiveDirectoryAssertionException {
MultiValueMap<String, String> 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();
}
}
Loading