-
Notifications
You must be signed in to change notification settings - Fork 460
Azure AD Integration #97
Changes from 12 commits
5f3ed9c
90b6db2
bd66c2f
a56832d
d37d24b
c44444b
a2dca25
0dd6672
694371d
3902f9a
e4ad658
39e8d7a
0621ed3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
## Overview | ||
This package provides a Spring Security filter to validate the Jwt token from Azure AD. The Jwt token is also used to acquire a On-Behalf-Of token for Azure AD Graph API so that authenticated user's membership information is available for authorization of access of API resources. | ||
|
||
### Register the application in Azure AD | ||
* Go to Azure Portal - Azure Active Directory - App registrations - New application registration to register the application in Azure Active Directory. `Application ID` is `clientId` in `application.properties`. | ||
* After application registration succeeded, go to API ACCESS - Required permissions - DELEGATED PERMISSIONS, tick `Access the directory as the signed-in user` and `Sign in and read user profile`. | ||
* Click `Grant Permissions` (Note: you will need administrator privilege to grant permission). | ||
* Go to API ACCESS - Keys to create a secret key (`clientSecret`). | ||
|
||
### Add the dependency | ||
|
||
`azure-ad-integration-spring-boot-autoconfigure` is published on Maven Central Repository. | ||
If you are using Maven, add the following dependency. | ||
|
||
```xml | ||
<dependency> | ||
<groupId>com.microsoft.azure</groupId> | ||
<artifactId>azure-ad-integration-spring-boot-autoconfigure</artifactId> | ||
<version>0.1.4</version> | ||
</dependency> | ||
``` | ||
|
||
### Add application properties | ||
|
||
Open `application.properties` file and add below properties. | ||
|
||
``` | ||
azure.activedirectory.clientId=Application-ID-in-AAD-App-registrations | ||
azure.activedirectory.clientSecret=Key-in-AAD-API-ACCESS | ||
azure.activedirectory.allowedRolesGroups=roles-groups-allowed-to-access-API-resource e.g. group1,group2,group3 | ||
``` | ||
|
||
### Configure WebSecurityConfigurerAdapter class to use `AzureADJwtTokenFilter` | ||
|
||
``` | ||
@Autowired | ||
private AzureADJwtTokenFilter aadJwtFilter; | ||
``` | ||
|
||
You can refer to [azure-ad-integration-spring-boot-autoconfigure-sample]() for how to integrate Spring Security and Azure AD for authentication and authorization in a Single Page Application (SPA) scenario. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<parent> | ||
<groupId>com.microsoft.azure</groupId> | ||
<artifactId>azure-spring-boot-starter-parent</artifactId> | ||
<version>0.1.4</version> | ||
<relativePath>../../common/azure-spring-boot-starter-parent/pom.xml</relativePath> | ||
</parent> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<artifactId>azure-ad-integration-spring-boot-autoconfigure</artifactId> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. normally service name + spring boot autoconfiguration, no need integration, |
||
<packaging>jar</packaging> | ||
|
||
<name>Azure AD Spring Security Integration Spring Boot Autoconfigure</name> | ||
<description>Spring Boot auto configuration package for Azure AD and Spring Security Integration</description> | ||
<url>https://github.com/Microsoft/azure-spring-boot-starters</url> | ||
|
||
<licenses> | ||
<license> | ||
<name>MIT</name> | ||
<url>https://github.com/Microsoft/azure-spring-boot-starters/blob/master/LICENSE</url> | ||
<distribution>repo</distribution> | ||
</license> | ||
</licenses> | ||
|
||
<developers> | ||
<developer> | ||
<id>yaweiw</id> | ||
<name>Yawei Wang</name> | ||
<email>yaweiw@microsoft.com</email> | ||
</developer> | ||
</developers> | ||
|
||
<scm> | ||
<connection>scm:git:git://github.com/Microsoft/azure-spring-boot-starters.git</connection> | ||
<developerConnection>scm:git:ssh://github.com:Microsoft/azure-spring-boot-starters.git</developerConnection> | ||
<url>https://github.com/Microsoft/azure-spring-boot-starters/tree/master</url> | ||
</scm> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-configuration-processor</artifactId> | ||
<optional>true</optional> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.microsoft.azure</groupId> | ||
<artifactId>azure-spring-common</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-validation</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.security</groupId> | ||
<artifactId>spring-security-test</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.nimbusds</groupId> | ||
<artifactId>nimbus-jose-jwt</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.microsoft.azure</groupId> | ||
<artifactId>adal4j</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework</groupId> | ||
<artifactId>spring-web</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.security</groupId> | ||
<artifactId>spring-security-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.security</groupId> | ||
<artifactId>spring-security-web</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>javax.servlet</groupId> | ||
<artifactId>javax.servlet-api</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.fasterxml.jackson.core</groupId> | ||
<artifactId>jackson-databind</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-core</artifactId> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. spring-boot-starter-test contains mockito already, are you demanding different version of mockito here? |
||
</dependency> | ||
</dependencies> | ||
|
||
|
||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See LICENSE in the project root for | ||
* license information. | ||
*/ | ||
package com.microsoft.azure.autoconfigure.aad; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||
import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.Scope; | ||
|
||
@Configuration | ||
@ConditionalOnMissingBean(AzureADJwtTokenFilter.class) | ||
@EnableConfigurationProperties(AzureADJwtFilterProperties.class) | ||
public class AzureADJwtFilterAutoConfiguration { | ||
private static final Logger LOG = LoggerFactory.getLogger(AzureADJwtFilterProperties.class); | ||
|
||
private final AzureADJwtFilterProperties aadJwtFilterProperties; | ||
|
||
public AzureADJwtFilterAutoConfiguration(AzureADJwtFilterProperties aadJwtFilterProperties) { | ||
this.aadJwtFilterProperties = aadJwtFilterProperties; | ||
} | ||
|
||
/** | ||
* Declare AzureADJwtFilter bean. | ||
* | ||
* @return AzureADJwtFilter bean | ||
*/ | ||
@Bean | ||
@Scope("prototype") | ||
public AzureADJwtTokenFilter azureADJwtFilter() { | ||
LOG.info("AzureADJwtTokenFilter Constructor."); | ||
return new AzureADJwtTokenFilter(aadJwtFilterProperties); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See LICENSE in the project root for | ||
* license information. | ||
*/ | ||
package com.microsoft.azure.autoconfigure.aad; | ||
|
||
import org.hibernate.validator.constraints.NotEmpty; | ||
import org.springframework.boot.context.properties.ConfigurationProperties; | ||
import org.springframework.validation.annotation.Validated; | ||
|
||
import java.util.List; | ||
|
||
@Validated | ||
@ConfigurationProperties("azure.activedirectory") | ||
public class AzureADJwtFilterProperties { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please reference this commit 86aa124 to add Java doc for each field. There will be hints when user use the properties. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
/** | ||
* Registered application ID in Azure AD. | ||
*/ | ||
@NotEmpty | ||
private String clientId; | ||
/** | ||
* API Access Key of the registered application. | ||
*/ | ||
@NotEmpty | ||
private String clientSecret; | ||
/** | ||
* Allowed roles and groups in Azure AD. | ||
*/ | ||
@NotEmpty | ||
private List<String> allowedRolesGroups; | ||
|
||
private static final String aadSignInUri = "https://login.microsoftonline.com/"; | ||
private static final String aadGraphAPIUri = "https://graph.windows.net/"; | ||
|
||
public String getClientId() { | ||
return clientId; | ||
} | ||
public void setClientId(String clientId) { | ||
this.clientId = clientId; | ||
} | ||
public String getClientSecret() { | ||
return clientSecret; | ||
} | ||
public void setClientSecret(String clientSecret) { | ||
this.clientSecret = clientSecret; | ||
} | ||
public String getAadSignInUri() { | ||
return aadSignInUri; | ||
} | ||
public String getAadGraphAPIUri() { | ||
return aadGraphAPIUri; | ||
} | ||
|
||
public List<String> getAllowedRolesGroups() { | ||
return allowedRolesGroups; | ||
} | ||
public void setAllowedRolesGroups(List<String> allowedRolesGroups) { | ||
this.allowedRolesGroups = allowedRolesGroups; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/** | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See LICENSE in the project root for | ||
* license information. | ||
*/ | ||
package com.microsoft.azure.autoconfigure.aad; | ||
|
||
import com.nimbusds.jose.JOSEException; | ||
import com.nimbusds.jose.JWSAlgorithm; | ||
import com.nimbusds.jose.JWSObject; | ||
import com.nimbusds.jose.jwk.JWK; | ||
import com.nimbusds.jose.jwk.JWKSet; | ||
import com.nimbusds.jose.jwk.source.JWKSource; | ||
import com.nimbusds.jose.jwk.source.RemoteJWKSet; | ||
import com.nimbusds.jose.proc.BadJOSEException; | ||
import com.nimbusds.jose.proc.JWSKeySelector; | ||
import com.nimbusds.jose.proc.JWSVerificationKeySelector; | ||
import com.nimbusds.jose.proc.SecurityContext; | ||
import com.nimbusds.jwt.JWTClaimsSet; | ||
import com.nimbusds.jwt.proc.*; | ||
|
||
import java.io.IOException; | ||
import java.net.MalformedURLException; | ||
import java.net.URL; | ||
import java.text.ParseException; | ||
import java.util.Map; | ||
|
||
public final class AzureADJwtToken { | ||
private final JWSObject jwsObject; | ||
private final JWTClaimsSet jwsClaimsSet; | ||
private final JWKSet jwsKeySet; | ||
|
||
public AzureADJwtToken(String bearerToken) throws Exception { | ||
final ConfigurableJWTProcessor validator = getAadJwtTokenValidator(bearerToken); | ||
jwsClaimsSet = validator.process(bearerToken, null); | ||
final JWTClaimsSetVerifier verifier = validator.getJWTClaimsSetVerifier(); | ||
verifier.verify(jwsClaimsSet, null); | ||
jwsObject = JWSObject.parse(bearerToken); | ||
jwsKeySet = loadAadPublicKeys(); | ||
} | ||
|
||
private ConfigurableJWTProcessor getAadJwtTokenValidator( | ||
String bearerToken) throws ParseException, JOSEException, BadJOSEException, MalformedURLException { | ||
final ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor(); | ||
final JWKSource keySource = new RemoteJWKSet( | ||
new URL("https://login.microsoftonline.com/common/discovery/keys")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make this constant a static field? |
||
final JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256; | ||
final JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource); | ||
jwtProcessor.setJWSKeySelector(keySelector); | ||
|
||
jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier(){ | ||
@Override | ||
public void verify(JWTClaimsSet claimsSet, SecurityContext ctx) throws BadJWTException { | ||
super.verify(claimsSet, ctx); | ||
final String issuer = claimsSet.getIssuer(); | ||
if (issuer == null || !issuer.contains("https://sts.windows.net/")) { | ||
throw new BadJWTException("Invalid token issuer"); | ||
} | ||
} | ||
}); | ||
return jwtProcessor; | ||
} | ||
|
||
private JWKSet loadAadPublicKeys() throws IOException, ParseException { | ||
final int connectTimeoutinMS = 1000; | ||
final int readTimeoutinMS = 1000; | ||
final int sizeLimitinBytes = 10000; | ||
return JWKSet.load( | ||
new URL("https://login.microsoftonline.com/common/discovery/keys"), | ||
connectTimeoutinMS, | ||
readTimeoutinMS, | ||
sizeLimitinBytes); | ||
} | ||
|
||
// claimset | ||
public String getIssuer() { | ||
return jwsClaimsSet == null ? null : jwsClaimsSet.getIssuer(); | ||
} | ||
public String getSubject() { | ||
return jwsClaimsSet == null ? null : jwsClaimsSet.getSubject(); | ||
} | ||
public Map<String, Object> getClaims() { | ||
return jwsClaimsSet == null ? null : jwsClaimsSet.getClaims(); | ||
} | ||
public Object getClaim(String name) { | ||
return jwsClaimsSet == null ? null : jwsClaimsSet.getClaim(name); | ||
} | ||
|
||
// header | ||
public String getKid() { | ||
return jwsObject == null ? null : jwsObject.getHeader().getKeyID(); | ||
} | ||
|
||
// JWK | ||
public JWK getJWKByKid(String kid) { | ||
return jwsKeySet == null ? null : jwsKeySet.getKeyByKeyId(kid); | ||
} | ||
|
||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should insert a paragraph that describes the scenario that this enables, ie using Azure AD to implement authN/Z for spring-boot based Rest APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure