diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index 578130822754..b307663f1cc9 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -9,7 +9,13 @@ "com\\.azure\\..+\\.implementation(\\..+)?", "com\\.fasterxml\\.jackson\\..+", "reactor\\.netty\\..+", - "io\\.netty\\..+" + "io\\.netty\\..+", + "com\\.nimbusds(\\..+)?", + "com\\.microsoft\\.azure\\..+", + "com\\.microsoft\\.spring\\..+", + "javax\\.jms(\\..+)?", + "javax\\.servlet(\\..+)?", + "io\\.micrometer(\\..+)?" ] } } diff --git a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml index 6f2cbc9b70e9..5039ffa779da 100755 --- a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml +++ b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml @@ -1970,5 +1970,17 @@ + + + + + + + + + diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 46800e66a2a8..43ef0ad50463 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -37,6 +37,9 @@ io.netty:netty-transport-native-unix-common;4.1.45.Final io.projectreactor.netty:reactor-netty;0.9.5.RELEASE io.projectreactor:reactor-core;3.3.3.RELEASE io.reactivex:rxjava;1.2.4 +javax.annotation:javax.annotation-api;1.3.2 +javax.servlet:javax.servlet-api;4.0.1 +javax.validation:validation-api;2.0.1.Final org.apache.httpcomponents:httpclient;4.3.6 org.apache.logging.log4j:log4j-api;2.11.1 org.apache.logging.log4j:log4j-core;2.11.1 @@ -45,10 +48,28 @@ org.apache.qpid:proton-j;0.33.2 org.asynchttpclient:async-http-client;2.10.5 org.codehaus.groovy:groovy-eclipse-batch;2.5.8-01 org.codehaus.groovy:groovy-eclipse-compiler;3.4.0-01 +org.hibernate.validator:hibernate-validator;6.0.17.Final org.powermock:powermock-api-mockito2;2.0.2 org.powermock:powermock-module-junit4;2.0.2 org.slf4j:slf4j-api;1.7.28 org.slf4j:slf4j-simple;1.7.25 +org.springframework.boot:spring-boot-actuator-autoconfigure;2.2.0.RELEASE +org.springframework.boot:spring-boot-autoconfigure;2.2.0.RELEASE +org.springframework.boot:spring-boot-autoconfigure-processor;2.2.0.RELEASE +org.springframework.boot:spring-boot-configuration-processor;2.2.0.RELEASE +org.springframework.boot:spring-boot-dependencies;2.2.0.RELEASE +org.springframework.boot:spring-boot-starter;2.2.0.RELEASE +org.springframework.boot:spring-boot-starter-test;2.2.0.RELEASE +org.springframework.boot:spring-boot-starter-validation;2.2.0.RELEASE +org.springframework.boot:spring-boot-starter-web;2.2.0.RELEASE +org.springframework.security:spring-security-config;5.2.0.RELEASE +org.springframework.security:spring-security-core;5.2.0.RELEASE +org.springframework.security:spring-security-web;5.2.0.RELEASE +org.springframework.security:spring-security-oauth2-client;5.2.0.RELEASE +org.springframework.security:spring-security-oauth2-core;5.2.0.RELEASE +org.springframework.security:spring-security-oauth2-jose;5.2.0.RELEASE +org.springframework:spring-web;5.2.0.RELEASE +pl.pragmatists:JUnitParams;1.1.1 ## Test dependency versions cglib:cglib-nodep;3.2.7 @@ -103,7 +124,7 @@ uk.org.lidalia:slf4j-test;1.2.0 com.azure:sdk-build-tools;1.0.0 com.beust:jcommander;1.58 com.google.code.findbugs:jsr305;3.0.2 -com.nimbusds:nimbus-jose-jwt;6.0.1 +com.nimbusds:nimbus-jose-jwt;7.9 com.nimbusds:oauth2-oidc-sdk;6.14 com.puppycrawl.tools:checkstyle;8.29 commons-io:commons-io;2.5 @@ -189,6 +210,9 @@ eventgrid_commons-io:commons-io;2.6 # sdk\eventhubs\microsoft-azure-eventhubs-extensions\pom.xml eventhubs_com.microsoft.azure:msal4j;0.4.0-preview +# sdk\eventhubs\microsoft-azure-eventhubs\pom.xml +eventhubs_com.nimbusds:nimbus-jose-jwt;6.0.1 + # sdk\keyvault\microsoft-azure-keyvault-extensions\pom.xml keyvault_org.mockito:mockito-core;1.10.19 # sdk\keyvault\microsoft-azure-keyvault-test\pom.xml diff --git a/eng/versioning/pom_file_version_scanner.ps1 b/eng/versioning/pom_file_version_scanner.ps1 index 02e33c902087..08b91364e6a4 100644 --- a/eng/versioning/pom_file_version_scanner.ps1 +++ b/eng/versioning/pom_file_version_scanner.ps1 @@ -46,8 +46,8 @@ $StartTime = $(get-date) # This is the for the bannedDependencies include exceptions. All entries need to be of the # form groupId:artifactId:[version] which locks to a specific version. The exception -# to this is the blanket, wildcard include for com.azure libraries. -$ComAzureWhitelistInclude = "com.azure:*" +# to this is the blanket, wildcard include for com.azure and com.microsoft.azure libraries. +$ComAzureWhitelistIncludes = ("com.azure:*", "com.microsoft.azure:*") function Write-Error-With-Color([string]$msg) { @@ -561,10 +561,11 @@ Get-ChildItem -Path $Path -Filter pom*.xml -Recurse -File | ForEach-Object { # These entries will not and should not have an update tag elseif ($split.Count -eq 2) { - if ($rawIncludeText -ne $ComAzureWhitelistInclude) + if ($ComAzureWhitelistIncludes -notcontains $rawIncludeText) { $script:FoundError = $true - Write-Error-With-Color "Error: $($rawIncludeText) is not a valid entry. With the exception of the $($ComAzureWhitelistInclude), every entry must be of the form groupId:artifactId:[version]" + $WhiteListIncludeForError = $ComAzureWhitelistIncludes -join " and " + Write-Error-With-Color "Error: $($rawIncludeText) is not a valid entry. With the exception of the $($WhiteListIncludeForError), every entry must be of the form groupId:artifactId:[version]" } } else diff --git a/eng/versioning/version_data.txt b/eng/versioning/version_data.txt index ede4c90f4c76..18c7e225f055 100644 --- a/eng/versioning/version_data.txt +++ b/eng/versioning/version_data.txt @@ -40,3 +40,6 @@ com.microsoft.azure.msi_auth_token_provider:azure-authentication-msi-token-provi com.microsoft.azure:azure-eventgrid;1.4.0-beta.1;1.4.0-beta.1 com.microsoft.azure:azure-loganalytics;1.0.0-beta-2;1.0.0-beta.2 com.microsoft.azure:azure-media;1.0.0-beta.1;1.0.0-beta.1 +com.microsoft.azure:azure-spring-boot;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-spring-boot-starter;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-active-directory-spring-boot-starter;2.2.4;2.2.5-beta.1 diff --git a/pom.xml b/pom.xml index 81cac0995c6e..da436a3d829d 100644 --- a/pom.xml +++ b/pom.xml @@ -31,5 +31,6 @@ sdk/storage sdk/template sdk/textanalytics + sdk/spring diff --git a/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml b/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml index 5722c008b1c2..b9f6c110e632 100644 --- a/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml +++ b/sdk/eventhubs/microsoft-azure-eventhubs/pom.xml @@ -58,7 +58,7 @@ com.nimbusds nimbus-jose-jwt - 6.0.1 + 6.0.1 diff --git a/sdk/spring/README.md b/sdk/spring/README.md new file mode 100644 index 000000000000..16b0600e60fd --- /dev/null +++ b/sdk/spring/README.md @@ -0,0 +1,83 @@ +# Azure Spring Boot client library for Java + +## Getting started +### Introduction + +This repo is for Spring Boot Starters of Azure services. It helps Spring Boot developers to adopt Azure services. + +### Support Spring Boot +This repository supports both Spring Boot 2.1.x and 2.2.x. Please read [Spring Boot Dependency Mapping](https://github.com/Azure/azure-sdk-for-java/wiki/Spring-Boot-Dependency-Mapping) for dependency mapping. + +### Prerequisites +- JDK 1.8 and above +- [Maven](http://maven.apache.org/) 3.0 and above + +## Key concepts +### Usage + +Below starters are available with latest release version. We recommend users to leverage latest version for bug fix and new features. +You can find them in [Maven Central Repository](https://search.maven.org/). +The first three starters are also available in [Spring Initializr](http://start.spring.io/). + +Starter Name | Version for Spring Boot 2.2.x | Version for Spring Boot 2.1.x | Version for Spring Boot 2.0.x +---|:---:|:---:|:---: +[azure-active-directory-spring-boot-starter](azure-spring-boot-starter-active-directory/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-active-directory-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-active-directory-spring-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-active-directory-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-active-directory-spring-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-active-directory-spring-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-active-directory-spring-boot-starter%20AND%20v:2.0.*) +[azure-storage-spring-boot-starter](azure-spring-boot-starter-storage/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-storage-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-storage-spring-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-storage-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-storage-spring-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-storage-spring-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-storage-spring-boot-starter%20AND%20v:2.0.*) +[azure-keyvault-secrets-spring-boot-starter](azure-spring-boot-starter-keyvault-secrets/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-keyvault-secrets-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-keyvault-secrets-spring-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-keyvault-secrets-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-keyvault-secrets-spring-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-keyvault-secrets-spring-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-keyvault-secrets-spring-boot-starter%20AND%20v:2.0.*) +[azure-active-directory-b2c-spring-boot-starter](azure-spring-boot-starter-active-directory-b2c/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-active-directory-b2c-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-active-directory-b2c-spring-boot-starter%22) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-active-directory-b2c-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-active-directory-b2c-spring-boot-starter%20AND%20v:2.1.*) | N/A +[azure-cosmosdb-spring-boot-starter](azure-spring-boot-starter-cosmosdb/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-cosmosdb-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-cosmosdb-spring-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-cosmosdb-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-cosmosdb-spring-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-cosmosdb-spring-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-cosmosdb-spring-boot-starter%20AND%20v:2.0.*) +[azure-mediaservices-spring-boot-starter](azure-spring-boot-starter-mediaservices/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-mediaservices-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-mediaservices-spring-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-mediaservices-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-mediaservices-spring-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-mediaservices-spring-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-mediaservices-spring-boot-starter%20AND%20v:2.0.*) +[azure-servicebus-spring-boot-starter](azure-spring-boot-starter-servicebus/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-servicebus-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-servicebus-spring-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-servicebus-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-servicebus-spring-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-servicebus-spring-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-servicebus-spring-boot-starter%20AND%20v:2.0.*) +[spring-data-gremlin-boot-starter](azure-spring-boot-starter-data-gremlin/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-gremlin-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22spring-data-gremlin-boot-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-gremlin-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:spring-data-gremlin-boot-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/spring-data-gremlin-boot-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:spring-data-gremlin-boot-starter%20AND%20v:2.0.*) +[azure-spring-boot-metrics-starter](azure-spring-boot-starter-metrics) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-spring-boot-metrics-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-spring-boot-metrics-starter%22) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-spring-boot-metrics-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-spring-boot-metrics-starter%20AND%20v:2.1.*) | [![](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-spring-boot-metrics-starter/2.0.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-spring-boot-metrics-starter%20AND%20v:2.0.*) +[azure-servicebus-jms-spring-boot-starter](azure-spring-boot-starter-servicebus-jms/README.md) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-servicebus-jms-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.microsoft.azure%22%20AND%20a%3A%22azure-servicebus-jms-spring-boot-starter%22) | [![Maven Central](https://img.shields.io/maven-central/v/com.microsoft.azure/azure-servicebus-jms-spring-boot-starter/2.1.svg)](https://search.maven.org/search?q=g:com.microsoft.azure%20AND%20a:azure-servicebus-jms-spring-boot-starter%20AND%20v:2.1.*) | N/A + +### Snapshots +[![Nexus OSS](https://img.shields.io/nexus/snapshots/https/oss.sonatype.org/com.microsoft.azure/azure-spring-boot.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/microsoft/azure/azure-spring-boot/) + +Snapshots built from `master` branch are available, add [maven repositories](https://maven.apache.org/settings.html#Repositories) configuration to your pom file as below. +```xml + + + nexus-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + true + always + + + +``` +## Examples +You could check below articles to learn more on usage of specific starters. + +[How to use the Spring Boot Starter with Azure Cosmos DB API](https://docs.microsoft.com/java/azure/spring-framework/configure-spring-boot-starter-java-app-with-cosmos-db). + +## Troubleshooting +## Next steps + +## Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. + +Please follow [instructions here](https://github.com/Azure/azure-sdk-for-java/blob/master/CONTRIBUTING.md) to build from source or contribute. + +### Filing Issues + +If you encounter any bug, please file an issue [here](https://github.com/Azure/azure-sdk-for-java/issues). + +To suggest a new feature or changes that could be made, file an issue the same way you would for a bug. + +You can participate community driven [![Gitter](https://badges.gitter.im/Microsoft/spring-on-azure.svg)](https://gitter.im/Microsoft/spring-on-azure) + +### Pull Requests + +Pull requests are welcome. To open your own pull request, click [here](https://github.com/Microsoft/azure-spring-boot/compare). When creating a pull request, make sure you are pointing to the fork and branch that your changes were made in. + +### Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### Data/Telemetry + +This project collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy](https://privacy.microsoft.com/privacystatement) statement to learn more. + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/README.md b/sdk/spring/azure-spring-boot-starter-active-directory/README.md new file mode 100644 index 000000000000..407bf79f59f0 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory/README.md @@ -0,0 +1,242 @@ +# Azure AD Spring Boot Starter client library for Java + +## Overview +With Spring Starter for Azure Active Directory, now you can get started quickly to build the authentication workflow for a web application that uses Azure AD and OAuth 2.0 to secure its back end. It also enables developers to create a role based authorization workflow for a Web API secured by Azure AD, with the power of the Spring Security Filter Chain. + +### Key concepts +This package provides 2 ways to integrate with Spring Security and authenticate with Azure Active Directory. +* Authenticate in backend, auto configuration for common Azure Active Directory OAuth2 properties and `OAuth2UserService` to map authorities are provided. +* Authenticate in frontend, sends bearer authorization code to backend, in backend a Spring Security filter validates the Jwt token from Azure AD and save authentication. 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. Below is a diagram that shows the layers and typical flow for Single Page Application with Spring Boot web API backend that uses the filter for Authentication and Authorization. +![Single Page Application + Spring Boot Web API + Azure AD](resource/spring-aad.png) + +The authorization flow is composed of 3 phrases: +* Login with credentials and validate id_token from Azure AD +* Get On-Behalf-Of token and membership info from Azure AD Graph API +* Evaluate the permission based on membership info to grant or deny access + +##Getting started +#### Register the Application in Azure AD +* **Register a new application**: Go to Azure Portal - Azure Active Directory - App registrations - New application registration to register the application in Azure Active Directory. `Application ID` is `client-id` in `application.properties`. +* **Grant permissions to the application**: 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). +* **Create a client secret key for the application**: Go to API ACCESS - Keys to create a secret key (`client-secret`). + +#### Add Maven Dependency + +`azure-spring-boot-starter-active-directory` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +[//]: # "{x-version-update-start;com.azure:azure-spring-boot-starter-active-directory;dependency}" +```xml + + com.azure + azure-active-directory-spring-boot-starter + 2.2.5-beta.1 + +``` +[//]: # "{x-version-update-end}" + +## Examples +#### Configure application.properties and autowire beans + +Refer to different samples for different authentication ways. + +##### Authenticate in backend + +Please refer to [azure-spring-boot-sample-active-directory-backend](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend/README-a.md) for authenticate in backend. + +Configure application.properties: +```properties +spring.security.oauth2.client.registration.azure.client-id=xxxxxx-your-client-id-xxxxxx +spring.security.oauth2.client.registration.azure.client-secret=xxxxxx-your-client-secret-xxxxxx +azure.activedirectory.tenant-id=xxxxxx-your-tenant-id-xxxxxx +azure.activedirectory.active-directory-groups=group1, group2 +``` + +Autowire `OAuth2UserService` bean in `WebSecurityConfigurerAdapter`: + +```java +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADOAuth2LoginConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private OAuth2UserService oidcUserService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService); + } +} +``` + +##### Authenticate in frontend + +Please refer to [azure-active-directory-spring-boot-sample](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory/README-a.md) for how to integrate Spring Security and Azure AD for authentication and authorization in a Single Page Application (SPA) scenario. + +Configure application.properties: +```properties +azure.activedirectory.client-id=Application-ID-in-AAD-App-registrations +azure.activedirectory.client-secret=Key-in-AAD-API-ACCESS +azure.activedirectory.active-directory-groups=Aad-groups e.g. group1,group2,group3 +``` + +If you're using [Azure China](https://docs.microsoft.com/azure/china/china-welcome), please append an extra line to the `application.properties` file: +```properties +azure.activedirectory.environment=cn +``` + +* Autowire `AADAuthenticationFilter` in `WebSecurityConfig.java` file + +```java +@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) +public class AADAuthenticationFilterConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private AADAuthenticationFilter aadAuthFilter; + +} +``` + +* Role-based Authorization with annotation `@PreAuthorize("hasRole('GROUP_NAME')")` +* Role-based Authorization with method `isMemberOf()` + +##### Authenticate stateless APIs using AAD app roles +This scenario fits best for stateless Spring backends exposing an API to SPAs ([OAuth 2.0 implicit grant flow](https://docs.microsoft.com/azure/active-directory/develop/v1-oauth2-implicit-grant-flow)) +or service-to-service access using the [client credentials grant flow](https://docs.microsoft.com/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow). + +The stateless processing can be activated with the `azure.activedirectory.session-stateless` property. +The authorization is using the [AAD AppRole feature](https://docs.microsoft.com/azure/architecture/multitenant-identity/app-roles#roles-using-azure-ad-app-roles), +so instead of using the `groups` claim the token has a `roles` claim which contains roles [configured in your manifest](https://docs.microsoft.com/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps#examples). + +Configure your `application properties`: + +```properties +azure.activedirectory.session-stateless=true +azure.activedirectory.client-id=xxxxxx-your-client-id-xxxxxx +``` + +Define your roles in your application registration manifest: +```json + "appRoles": [ + { + "allowedMemberTypes": [ + "User" + ], + "displayName": "My demo", + "id": "00000000-0000-0000-0000-000000000000", + "isEnabled": true, + "description": "My demo role.", + "value": "MY_ROLE" + } + ], +``` + +Autowire the auth filter and attach it to the filter chain: + +```java +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADAppRoleStatelessAuthenticationFilterConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private AADAppRoleStatelessAuthenticationFilter appRoleAuthFilter; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterBefore(appRoleAuthFilter, UsernamePasswordAuthenticationFilter.class); + } + +} +``` + +* Role-based Authorization with annotation `@PreAuthorize("hasRole('MY_ROLE')")` +* Role-based Authorization with method `isMemberOf()` + +The roles you want to use within your application have to be [set up in the manifest of your +application registration](https://docs.microsoft.com/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps). + +##### Using The Microsoft Graph API +By default, azure-spring-boot is set up to utilize the Azure AD Graph. If you would prefer, it can be set up to utilize the Microsoft Graph instead. In order to do this, you will need to update the app registration in Azure to grant the application permissions to the Microsoft Graph API and add some properties to the application.properties file. + +* **Grant permissions to the application**: After application registration succeeded, go to API permissions - Add a permission, select `Microsoft Graph`, select Delegated permissions, tick `Directory.AccessAsUser.All - Access the directory as the signed-in user` and `Use.Read - Sign in and read user profile`. Click `Add Permissions` (Note: you will need administrator privilege to grant permission). Furthermore, you can remove the API permissions to the Azure Active Directory Graph, as these will not be needed. + +* **Configure your `application properties`**: +```properties +azure.activedirectory.environment=global-v2-graph +azure.activedirectory.user-group.key=@odata.type +azure.activedirectory.user-group.value=#microsoft.graph.group +azure.activedirectory.user-group.object-id-key=id +``` + +If you're using [Azure China](https://docs.microsoft.com/azure/china/china-welcome), please set the environment property in the `application.properties` file to: +```properties +azure.activedirectory.environment=cn-v2-graph +``` + +Please refer to [azure-spring-boot-sample-active-directory-backend-v2](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/) to see a sample configured to use the Microsoft Graph API. +### Using Microsoft identity platform endpoints +If you want to use v2 version endpoints to do authorization and authentication, please pay attention to the attributes of claims, because there are some attributes exits in v1 version id-token by default but not in v2 version id-token, if you have to get that attribute, please make sure to add it into your scope. +There is the doc [Difference between v1 and v2](https://docs.microsoft.com/azure/active-directory/develop/azure-ad-endpoint-comparison), For example, the name attribute doesn't exist in v2 token, if you want it, you need add `profile` to your scope, like this: +```properties +spring.security.oauth2.client.registration.azure.scope=openid, https://graph.microsoft.com/user.read, profile +``` +You can see more details in this link: [details](https://docs.microsoft.com/azure/active-directory/develop/id-tokens) + +### AAD Conditional Access Policy +Now azure-active-directory-spring-boot-starter has supported AAD conditional access policy, if you are using this policy, you need add **AADOAuth2AuthorizationRequestResolver** and **AADAuthenticationFailureHandler** to your WebSecurityConfigurerAdapter. + +```java +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADOAuth2LoginConditionalPolicyConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private OAuth2UserService oidcUserService; + + @Autowired + ApplicationContext applicationContext; + + @Override + protected void configure(HttpSecurity http) throws Exception { + final ClientRegistrationRepository clientRegistrationRepository = + applicationContext.getBean(ClientRegistrationRepository.class); + + http.authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService) + .and() + .authorizationEndpoint() + .authorizationRequestResolver(new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository)) + .and() + .failureHandler(new AADAuthenticationFailureHandler()); + } +} +``` +## Next steps +#### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. +Microsoft uses this information to improve our tooling experience. Participation is voluntary. +If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +```properties +azure.activedirectory.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/privacystatement/OnlineServices/Default.aspx). + +## Key concepts + +## Troubleshooting + +## Contributing + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml new file mode 100644 index 000000000000..711803dd33e6 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-active-directory-spring-boot-starter + 2.2.5-beta.1 + + Azure AD Spring Security Integration Spring Boot Starter + Spring Boot Starter for Azure AD and Spring Security Integration + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + org.springframework + spring-web + 5.2.0.RELEASE + + + org.springframework.security + spring-security-core + 5.2.0.RELEASE + + + org.springframework.security + spring-security-web + 5.2.0.RELEASE + + + org.springframework.security + spring-security-config + 5.2.0.RELEASE + + + com.microsoft.azure + msal4j + 1.3.0 + + + com.nimbusds + nimbus-jose-jwt + 7.9 + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + com.fasterxml.jackson.core:jackson-databind:[2.10.1] + com.microsoft.azure:msal4j:[1.3.0] + com.nimbusds:nimbus-jose-jwt:[7.9] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + org.springframework.security:spring-security-config:[5.2.0.RELEASE] + org.springframework.security:spring-security-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-web:[5.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/resource/spa-oauth2.png b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spa-oauth2.png new file mode 100644 index 000000000000..46b6e1323787 Binary files /dev/null and b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spa-oauth2.png differ diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/resource/spring-aad.png b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spring-aad.png new file mode 100644 index 000000000000..42782d8bce12 Binary files /dev/null and b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spring-aad.png differ diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/src/main/resources/aad.enable.config b/sdk/spring/azure-spring-boot-starter-active-directory/src/main/resources/aad.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory/src/main/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/README.md b/sdk/spring/azure-spring-boot-starter/README.md new file mode 100644 index 000000000000..c7c200f18f9b --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/README.md @@ -0,0 +1,8 @@ +# Azure Spring Boot Starters client library for Java + +## Getting started +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter/pom.xml b/sdk/spring/azure-spring-boot-starter/pom.xml new file mode 100644 index 000000000000..772fe5b86f80 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter + 2.2.5-beta.1 + + Azure Spring Boot Starter + Core starter, including auto-configuration support + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/aad.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/aad.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/README.md b/sdk/spring/azure-spring-boot/README.md new file mode 100644 index 000000000000..c8ddb56a9aba --- /dev/null +++ b/sdk/spring/azure-spring-boot/README.md @@ -0,0 +1,54 @@ +#Azure Spring Boot client library for Java + +## Key concepts +This project provides auto-configuration for the following Azure services: + +- [Azure Active Directory](../azure-spring-boot-starter-active-directory) +- [Azure Active Directory B2C](../azure-spring-boot-starter-active-directory-b2c) +- [Cosmos DB SQL API](../azure-spring-boot-starter-cosmosdb) +- [Key Vault](../azure-spring-boot-starter-keyvault-secrets) +- [Media Service](../azure-spring-boot-starter-mediaservices) +- [Service Bus](../azure-spring-boot-starter-servicebus) +- [Storage](../azure-spring-boot-starter-storage) + +This module also provides the ability to automatically inject credentials from Cloud Foundry into your +applications consuming Azure services. It does this by reading the `VCAP_SERVICES` environment +variable and setting the appropriate properties used by auto-configuration code. + +For details, please see sample code in the [azure-spring-boot-sample-cloud-foundry](../azure-spring-boot-samples/azure-spring-boot-sample-cloud-foundry) + +## Getting started +To start a new project using Azure, go on [start.spring.io](https://start.spring.io) and select "Azure +Support": this will configure the project to make sure you can integrate easily with Azure service. + +For instance, let's assume that you want to use Service Bus, you can add the usual `azure-servicebus` +dependency to your project and the Spring Boot auto-configuration will kick-in: + +```xml + + com.microsoft.azure + azure-servicebus + +``` + +Note that there is no need to add a `version` as those are managed already by the project. + +Alternatively you may want to use the [starters](../azure-spring-boot-starters) + +[//]: # ({x-version-update-start;com.azure:azure-servicebus-spring-boot-starter;dependency}) +```xml + + com.azure + azure-servicebus-spring-boot-starter + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +## Examples +Please refer to the [samples](../azure-spring-boot-samples) for more getting started instructions. + +## Troubleshooting +## Next steps +## Contributing + diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml new file mode 100644 index 000000000000..8865bab2aad1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -0,0 +1,227 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + jar + + Azure Spring Boot AutoConfigure + Azure Spring Boot AutoConfigure + https://github.com/Azure/azure-sdk-for-java + + + 0.22 + + + + + org.springframework.boot + spring-boot-autoconfigure + 2.2.0.RELEASE + + + + org.slf4j + slf4j-api + 1.7.28 + + + + org.springframework + spring-web + 5.2.0.RELEASE + + + + org.springframework.boot + spring-boot-configuration-processor + 2.2.0.RELEASE + true + + + + javax.validation + validation-api + 2.0.1.Final + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + org.springframework.security + spring-security-core + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-web + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-oauth2-client + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-oauth2-jose + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-config + 5.2.0.RELEASE + true + + + com.nimbusds + nimbus-jose-jwt + 7.9 + true + + + javax.servlet + javax.servlet-api + 4.0.1 + true + + + + + com.microsoft.azure + msal4j + 1.3.0 + true + + + + + org.springframework.boot + spring-boot-autoconfigure-processor + 2.2.0.RELEASE + true + + + + org.hibernate.validator + hibernate-validator + 6.0.17.Final + true + + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + + + org.springframework.boot + spring-boot-starter-test + 2.2.0.RELEASE + test + + + com.vaadin.external.google + android-json + + + + + org.springframework.boot + spring-boot-starter-web + 2.2.0.RELEASE + test + + + org.mockito + mockito-core + 3.0.0 + test + + + com.github.tomakehurst + wiremock-standalone + 2.24.1 + test + + + pl.pragmatists + JUnitParams + 1.1.1 + test + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.azure:* + com.fasterxml.jackson.core:jackson-databind:[2.10.1] + com.google.code.findbugs:jsr305:[3.0.2] + com.microsoft.azure:msal4j:[1.3.0] + com.nimbusds:nimbus-jose-jwt:[7.9] + javax.servlet:javax.servlet-api:[4.0.1] + javax.annotation:javax.annotation-api:[1.3.2] + javax.validation:validation-api:[2.0.1.Final] + org.slf4j:slf4j-api:[1.7.28] + org.hibernate.validator:hibernate-validator:[6.0.17.Final] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework.boot:spring-boot-actuator-autoconfigure:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-autoconfigure-processor:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-autoconfigure:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-configuration-processor:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-test:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-web:[2.2.0.RELEASE] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework.security:spring-security-config:[5.2.0.RELEASE] + org.springframework.security:spring-security-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-client:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-jose:[5.2.0.RELEASE] + org.springframework.security:spring-security-web:[5.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java new file mode 100644 index 000000000000..8b4e2c6696d8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.proc.BadJWTException; +import net.minidev.json.JSONArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.util.StringUtils.hasText; + +/** + * A stateless authentication filter which uses app roles feature of Azure Active Directory. Since it's a stateless + * implementation so the principal will not be stored in session. By using roles claim in the token it will not call + * Microsoft Graph to retrieve users' groups. + */ +public class AADAppRoleStatelessAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADAppRoleStatelessAuthenticationFilter.class); + private static final String TOKEN_TYPE = "Bearer "; + private static final JSONArray DEFAULT_ROLE_CLAIM = new JSONArray().appendElement("USER"); + private static final String ROLE_PREFIX = "ROLE_"; + + private final UserPrincipalManager principalManager; + + public AADAppRoleStatelessAuthenticationFilter(UserPrincipalManager principalManager) { + this.principalManager = principalManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + boolean cleanupRequired = false; + + if (hasText(authHeader) && authHeader.startsWith(TOKEN_TYPE)) { + try { + final String token = authHeader.replace(TOKEN_TYPE, ""); + final UserPrincipal principal = principalManager.buildUserPrincipal(token); + final JSONArray roles = Optional.ofNullable((JSONArray) principal.getClaims().get("roles")) + .filter(r -> !r.isEmpty()) + .orElse(DEFAULT_ROLE_CLAIM); + final Authentication authentication = new PreAuthenticatedAuthenticationToken( + principal, null, rolesToGrantedAuthorities(roles)); + authentication.setAuthenticated(true); + LOGGER.info("Request token verification success. {}", authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + cleanupRequired = true; + } catch (BadJWTException ex) { + final String errorMessage = "Invalid JWT. Either expired or not yet valid. " + ex.getMessage(); + LOGGER.warn(errorMessage); + throw new ServletException(errorMessage, ex); + } catch (ParseException | BadJOSEException | JOSEException ex) { + LOGGER.error("Failed to initialize UserPrincipal.", ex); + throw new ServletException(ex); + } + } + + try { + filterChain.doFilter(request, response); + } finally { + if (cleanupRequired) { + //Clear context after execution + SecurityContextHolder.clearContext(); + } + } + } + + protected Set rolesToGrantedAuthorities(JSONArray roles) { + return roles.stream() + .filter(Objects::nonNull) + .map(s -> new SimpleGrantedAuthority(ROLE_PREFIX + s)) + .collect(Collectors.toSet()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java new file mode 100644 index 000000000000..907ad8f9d834 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.aad.msal4j.MsalServiceException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.savedrequest.DefaultSavedRequest; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Strategy used to handle a failed authentication attempt. + *

+ * To redirect the user to the authentication page to allow them to try again when conditional access policy is + * configured on Azure Active Directory. + */ +public class AADAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private AuthenticationFailureHandler defaultHandler; + + public AADAuthenticationFailureHandler() { + this.defaultHandler = new SimpleUrlAuthenticationFailureHandler(AADConstantsHelper.FAILURE_DEFAULT_URL); + } + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + final OAuth2AuthenticationException targetException = (OAuth2AuthenticationException) exception; + //handle conditional access policy + if (AADConstantsHelper.CONDITIONAL_ACCESS_POLICY.equals((targetException.getError().getErrorCode()))) { + //get infos + final Throwable cause = targetException.getCause(); + if (cause instanceof MsalServiceException) { + final MsalServiceException e = (MsalServiceException) cause; + final String claims = e.claims(); + + final DefaultSavedRequest savedRequest = (DefaultSavedRequest) request.getSession() + .getAttribute(AADConstantsHelper.SAVED_REQUEST); + final String savedRequestUrl = savedRequest.getRedirectUrl(); + //put claims into session + request.getSession().setAttribute(AADConstantsHelper.CAP_CLAIMS, claims); + //redirect + response.setStatus(302); + response.sendRedirect(savedRequestUrl); + return; + } + } + //default handle logic + defaultHandler.onAuthenticationFailure(request, response, exception); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java new file mode 100644 index 000000000000..d0a4de95ab4a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.aad.msal4j.MsalServiceException; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.util.ResourceRetriever; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.naming.ServiceUnavailableException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.MalformedURLException; +import java.text.ParseException; + +/** + * A stateful authentication filter which uses Microsoft Graph groups to authorize. Both ID token and access token are + * supported. In the case of access token, only access token issued for the exact same application this filter used for + * could be accepted, e.g. access token issued for Microsoft Graph could not be processed by users' application. + */ +public class AADAuthenticationFilter extends OncePerRequestFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationFilter.class); + + private static final String CURRENT_USER_PRINCIPAL = "CURRENT_USER_PRINCIPAL"; + private static final String CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN = "CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN"; + private static final String CURRENT_USER_PRINCIPAL_JWT_TOKEN = "CURRENT_USER_PRINCIPAL_JWT_TOKEN"; + + private static final String TOKEN_HEADER = "Authorization"; + private static final String TOKEN_TYPE = "Bearer "; + + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties serviceEndpointsProps; + private UserPrincipalManager principalManager; + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps, + ResourceRetriever resourceRetriever) { + this.aadAuthProps = aadAuthProps; + this.serviceEndpointsProps = serviceEndpointsProps; + this.principalManager = new UserPrincipalManager(serviceEndpointsProps, aadAuthProps, resourceRetriever, false); + } + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps, + ResourceRetriever resourceRetriever, + JWKSetCache jwkSetCache) { + this.aadAuthProps = aadAuthProps; + this.serviceEndpointsProps = serviceEndpointsProps; + this.principalManager = new UserPrincipalManager(serviceEndpointsProps, + aadAuthProps, + resourceRetriever, + false, + jwkSetCache); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader(TOKEN_HEADER); + + if (authHeader != null && authHeader.startsWith(TOKEN_TYPE)) { + try { + final String idToken = authHeader.replace(TOKEN_TYPE, ""); + UserPrincipal principal = (UserPrincipal) request + .getSession().getAttribute(CURRENT_USER_PRINCIPAL); + String graphApiToken = (String) request + .getSession().getAttribute(CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN); + final String currentToken = (String) request + .getSession().getAttribute(CURRENT_USER_PRINCIPAL_JWT_TOKEN); + + final AzureADGraphClient client = new AzureADGraphClient(aadAuthProps.getClientId(), + aadAuthProps.getClientSecret(), aadAuthProps, serviceEndpointsProps); + + if (principal == null + || graphApiToken == null + || graphApiToken.isEmpty() + || !idToken.equals(currentToken)) { + principal = principalManager.buildUserPrincipal(idToken); + + final String tenantId = principal.getClaim().toString(); + graphApiToken = client.acquireTokenForGraphApi(idToken, tenantId).accessToken(); + + principal.setUserGroups(client.getGroups(graphApiToken)); + + request.getSession().setAttribute(CURRENT_USER_PRINCIPAL, principal); + request.getSession().setAttribute(CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN, graphApiToken); + request.getSession().setAttribute(CURRENT_USER_PRINCIPAL_JWT_TOKEN, idToken); + } + + final Authentication authentication = new PreAuthenticatedAuthenticationToken( + principal, null, client.convertGroupsToGrantedAuthorities(principal.getUserGroups())); + + authentication.setAuthenticated(true); + LOGGER.info("Request token verification success. {}", authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (MalformedURLException | ParseException | BadJOSEException | JOSEException ex) { + LOGGER.error("Failed to initialize UserPrincipal.", ex); + throw new ServletException(ex); + } catch (ServiceUnavailableException ex) { + LOGGER.error("Failed to acquire graph api token.", ex); + throw new ServletException(ex); + } catch (MsalServiceException ex) { + if (ex.claims() != null && !ex.claims().isEmpty()) { + throw new ServletException("Handle conditional access policy", ex); + } else { + throw ex; + } + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java new file mode 100644 index 000000000000..6856d63a1e0d --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +import com.microsoft.azure.telemetry.TelemetrySender; +import com.nimbusds.jose.jwk.source.DefaultJWKSetCache; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.util.ClassUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Azure Active Authentication filters . + *

+ * The configuration will not be activated if no {@literal azure.activedirectory.client-id} property provided. + *

+ * A stateless filter {@link AADAppRoleStatelessAuthenticationFilter} will be auto-configured by specifying + * {@literal azure.activedirectory.session-stateless=true}. Otherwise, {@link AADAuthenticationFilter} will be + * configured. + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@ConditionalOnProperty(prefix = AADAuthenticationFilterAutoConfiguration.PROPERTY_PREFIX, value = {"client-id"}) +@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class}) +@PropertySource(value = "classpath:serviceEndpoints.properties") +public class AADAuthenticationFilterAutoConfiguration { + private static final Logger LOG = LoggerFactory.getLogger(AADAuthenticationProperties.class); + + public static final String PROPERTY_PREFIX = "azure.activedirectory"; + private static final String PROPERTY_SESSION_STATELESS = "session-stateless"; + + private final AADAuthenticationProperties aadAuthProps; + + private final ServiceEndpointsProperties serviceEndpointsProps; + + public AADAuthenticationFilterAutoConfiguration(AADAuthenticationProperties aadAuthFilterProps, + ServiceEndpointsProperties serviceEndpointsProps) { + this.aadAuthProps = aadAuthFilterProps; + this.serviceEndpointsProps = serviceEndpointsProps; + } + + /** + * Declare AADAuthenticationFilter bean. + * + * @return AADAuthenticationFilter bean + */ + @Bean + @ConditionalOnMissingBean(AADAuthenticationFilter.class) + @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = {"client-id", "client-secret"}) + @ConditionalOnExpression("${azure.activedirectory.session-stateless:false} == false") + public AADAuthenticationFilter azureADJwtTokenFilter() { + LOG.info("AzureADJwtTokenFilter Constructor."); + return new AADAuthenticationFilter(aadAuthProps, + serviceEndpointsProps, + getJWTResourceRetriever(), + getJWKSetCache()); + } + + @Bean + @ConditionalOnMissingBean(AADAppRoleStatelessAuthenticationFilter.class) + @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = PROPERTY_SESSION_STATELESS, havingValue = "true") + public AADAppRoleStatelessAuthenticationFilter azureADStatelessAuthFilter(ResourceRetriever resourceRetriever) { + LOG.info("Creating AzureADStatelessAuthFilter bean."); + final boolean useExplicitAudienceCheck = true; + return new AADAppRoleStatelessAuthenticationFilter(new UserPrincipalManager(serviceEndpointsProps, aadAuthProps, + resourceRetriever, useExplicitAudienceCheck)); + } + + @Bean + @ConditionalOnMissingBean(ResourceRetriever.class) + public ResourceRetriever getJWTResourceRetriever() { + return new DefaultResourceRetriever(aadAuthProps.getJwtConnectTimeout(), aadAuthProps.getJwtReadTimeout(), + aadAuthProps.getJwtSizeLimit()); + } + + @Bean + @ConditionalOnMissingBean(JWKSetCache.class) + public JWKSetCache getJWKSetCache() { + return new DefaultJWKSetCache(aadAuthProps.getJwkSetCacheLifespan(), TimeUnit.MILLISECONDS); + } + + @PostConstruct + private void sendTelemetry() { + if (aadAuthProps.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(AADAuthenticationFilterAutoConfiguration.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java new file mode 100644 index 000000000000..70429c9469dd --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotEmpty; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Configuration properties for Azure Active Directory Authentication. + */ +@Validated +@ConfigurationProperties("azure.activedirectory") +public class AADAuthenticationProperties { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationProperties.class); + + private static final String DEFAULT_SERVICE_ENVIRONMENT = "global"; + + private static final long DEFAULT_JWKSETCACHE_LIFESPAN = TimeUnit.MINUTES.toMillis(5); + + /** + * Default UserGroup configuration. + */ + private UserGroupProperties userGroup = new UserGroupProperties(); + + + /** + * Azure service environment/region name, e.g., cn, global + */ + private String environment = DEFAULT_SERVICE_ENVIRONMENT; + /** + * Registered application ID in Azure AD. + * Must be configured when OAuth2 authentication is done in front end + */ + private String clientId; + /** + * API Access Key of the registered application. + * Must be configured when OAuth2 authentication is done in front end + */ + private String clientSecret; + + /** + * Azure AD groups. + */ + private List activeDirectoryGroups = new ArrayList<>(); + + /** + * App ID URI which might be used in the "aud" claim of an id_token. + */ + private String appIdUri; + + /** + * Connection Timeout for the JWKSet Remote URL call. + */ + private int jwtConnectTimeout = RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT; /* milliseconds */ + + /** + * Read Timeout for the JWKSet Remote URL call. + */ + private int jwtReadTimeout = RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT; /* milliseconds */ + + /** + * Size limit in Bytes of the JWKSet Remote URL call. + */ + private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */ + + /** + * The lifespan of the cached JWK set before it expires, default is 5 minutes. + */ + private long jwkSetCacheLifespan = DEFAULT_JWKSETCACHE_LIFESPAN; + + /** + * Azure Tenant ID. + */ + private String tenantId; + + /** + * If Telemetry events should be published to Azure AD. + */ + private boolean allowTelemetry = true; + + /** + * If true activates the stateless auth filter {@link AADAppRoleStatelessAuthenticationFilter}. + * The default is false which activates {@link AADAuthenticationFilter}. + */ + private Boolean sessionStateless = false; + + @DeprecatedConfigurationProperty(reason = "Configuration moved to UserGroup class to keep UserGroup properties " + + "together", replacement = "azure.activedirectory.user-group.allowed-groups") + public List getActiveDirectoryGroups() { + return activeDirectoryGroups; + } + /** + * Properties dedicated to changing the behavior of how the groups are mapped from the Azure AD response. Depending + * on the graph API used the object will not be the same. + */ + public static class UserGroupProperties { + + /** + * Expected UserGroups that an authority will be granted to if found in the response from the MemeberOf Graph + * API Call. + */ + private List allowedGroups = new ArrayList<>(); + + /** + * Key of the JSON Node to get from the Azure AD response object that will be checked to contain the {@code + * azure.activedirectory.user-group.value} to signify that this node is a valid {@code UserGroup}. + */ + @NotEmpty + private String key = "objectType"; + + /** + * Value of the JSON Node identified by the {@code azure.activedirectory.user-group.key} to validate the JSON + * Node is a UserGroup. + */ + @NotEmpty + private String value = "Group"; + + /** + * Key of the JSON Node containing the Azure Object ID for the {@code UserGroup}. + */ + @NotEmpty + private String objectIDKey = "objectId"; + + public List getAllowedGroups() { + return allowedGroups; + } + + public void setAllowedGroups(List allowedGroups) { + this.allowedGroups = allowedGroups; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getObjectIDKey() { + return objectIDKey; + } + + public void setObjectIDKey(String objectIDKey) { + this.objectIDKey = objectIDKey; + } + + @Override + public String toString() { + return "UserGroupProperties{" + + "allowedGroups=" + allowedGroups + + ", key='" + key + '\'' + + ", value='" + value + '\'' + + ", objectIDKey='" + objectIDKey + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserGroupProperties that = (UserGroupProperties) o; + return Objects.equals(allowedGroups, that.allowedGroups) + && Objects.equals(key, that.key) + && Objects.equals(value, that.value) + && Objects.equals(objectIDKey, that.objectIDKey); + } + + @Override + public int hashCode() { + return Objects.hash(allowedGroups, key, value, objectIDKey); + } + } + + + /** + * Validates at least one of the user group properties are populated. + */ + @PostConstruct + public void validateUserGroupProperties() { + if (this.sessionStateless) { + if (!this.activeDirectoryGroups.isEmpty()) { + LOGGER.warn("Group names are not supported if you set 'sessionSateless' to 'true'."); + } + } else if (this.activeDirectoryGroups.isEmpty() && this.getUserGroup().getAllowedGroups().isEmpty()) { + throw new IllegalStateException("One of the User Group Properties must be populated. " + + "Please populate azure.activedirectory.user-group.allowed-groups"); + } + } + + public UserGroupProperties getUserGroup() { + return userGroup; + } + + public void setUserGroup(UserGroupProperties userGroup) { + this.userGroup = userGroup; + } + + public String getEnvironment() { + return environment; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + + 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 void setActiveDirectoryGroups(List activeDirectoryGroups) { + this.activeDirectoryGroups = activeDirectoryGroups; + } + + public String getAppIdUri() { + return appIdUri; + } + + public void setAppIdUri(String appIdUri) { + this.appIdUri = appIdUri; + } + + public int getJwtConnectTimeout() { + return jwtConnectTimeout; + } + + public void setJwtConnectTimeout(int jwtConnectTimeout) { + this.jwtConnectTimeout = jwtConnectTimeout; + } + + public int getJwtReadTimeout() { + return jwtReadTimeout; + } + + public void setJwtReadTimeout(int jwtReadTimeout) { + this.jwtReadTimeout = jwtReadTimeout; + } + + public int getJwtSizeLimit() { + return jwtSizeLimit; + } + + public void setJwtSizeLimit(int jwtSizeLimit) { + this.jwtSizeLimit = jwtSizeLimit; + } + + public long getJwkSetCacheLifespan() { + return jwkSetCacheLifespan; + } + + public void setJwkSetCacheLifespan(long jwkSetCacheLifespan) { + this.jwkSetCacheLifespan = jwkSetCacheLifespan; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public Boolean getSessionStateless() { + return sessionStateless; + } + + public void setSessionStateless(Boolean sessionStateless) { + this.sessionStateless = sessionStateless; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java new file mode 100644 index 000000000000..58fd05b2e1d1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +public class AADConstantsHelper { + public static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy"; + public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; + public static final String CAP_CLAIMS = "CAP_Claims"; + public static final String CLAIMS = "claims"; + public static final String FAILURE_DEFAULT_URL = "/login?error"; +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java new file mode 100644 index 000000000000..87038b1f0d06 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.StringUtils; + +/** + * To add conditional policy claims to authorization URL. + */ +public class AADOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private OAuth2AuthorizationRequestResolver defaultResolver; + + public AADOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + return addClaims(request, defaultResolver.resolve(request)); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + return addClaims(request, defaultResolver.resolve(request, clientRegistrationId)); + } + + //add claims to authorization-url + private OAuth2AuthorizationRequest addClaims(HttpServletRequest request, + OAuth2AuthorizationRequest req) { + if (req == null || request == null) { + return req; + } + + final String conditionalAccessPolicyClaims = getConditionalAccessPolicyClaims(request); + if (StringUtils.isEmpty(conditionalAccessPolicyClaims)) { + return req; + } + + final Map extraParams = new HashMap<>(); + if (req.getAdditionalParameters() != null) { + extraParams.putAll(req.getAdditionalParameters()); + } + extraParams.put(AADConstantsHelper.CLAIMS, conditionalAccessPolicyClaims); + return OAuth2AuthorizationRequest + .from(req) + .additionalParameters(extraParams) + .build(); + } + + private String getConditionalAccessPolicyClaims(HttpServletRequest request) { + //claims just for one use + final String claims = request.getSession() + .getAttribute(AADConstantsHelper.CAP_CLAIMS) == null ? "" : (String) request + .getSession() + .getAttribute(AADConstantsHelper.CAP_CLAIMS); + //remove claims in session + if (!StringUtils.isEmpty(claims)) { + request.getSession().removeAttribute(AADConstantsHelper.CAP_CLAIMS); + } + return claims; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java new file mode 100644 index 000000000000..47345fb454f2 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.azure.telemetry.TelemetrySender; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Azure Active Authentication OAuth 2.0. + *

+ * The configuration will not be activated if no {@literal azure.activedirectory.tenant-id} property provided. + *

+ * A OAuth2 user service {@link AADOAuth2UserService} will be auto-configured by specifying + * {@literal azure.activedirectory.active-directory-groups} property. + */ +@Configuration +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnProperty(prefix = "azure.activedirectory", value = "tenant-id") +@PropertySource("classpath:/aad-oauth2-common.properties") +@PropertySource(value = "classpath:serviceEndpoints.properties") +@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class}) +public class AADOAuth2AutoConfiguration { + + private final AADAuthenticationProperties aadAuthProps; + + private final ServiceEndpointsProperties serviceEndpointsProps; + + public AADOAuth2AutoConfiguration(AADAuthenticationProperties aadAuthProperties, + ServiceEndpointsProperties serviceEndpointsProps) { + this.aadAuthProps = aadAuthProperties; + this.serviceEndpointsProps = serviceEndpointsProps; + } + + @Bean + @ConditionalOnProperty(prefix = "azure.activedirectory", value = "active-directory-groups") + public OAuth2UserService oidcUserService() { + return new AADOAuth2UserService(aadAuthProps, serviceEndpointsProps); + } + + @PostConstruct + private void sendTelemetry() { + if (aadAuthProps.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(AADOAuth2AutoConfiguration.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java new file mode 100644 index 000000000000..16b4863a39bf --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.aad.msal4j.MsalServiceException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.StringUtils; + +import javax.naming.ServiceUnavailableException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Set; + +/** + * This implementation will retrieve group info of user from Microsoft Graph and map groups to {@link GrantedAuthority}. + */ +public class AADOAuth2UserService implements OAuth2UserService { + private static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy"; + private static final String INVALID_REQUEST = "invalid_request"; + private static final String SERVER_ERROR = "server_error"; + private static final String DEFAULT_USERNAME_ATTR_NAME = "name"; + + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties serviceEndpointsProps; + + public AADOAuth2UserService(AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps) { + this.aadAuthProps = aadAuthProps; + this.serviceEndpointsProps = serviceEndpointsProps; + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + final OidcUserService delegate = new OidcUserService(); + + // Delegate to the default implementation for loading a user + OidcUser oidcUser = delegate.loadUser(userRequest); + final OidcIdToken idToken = userRequest.getIdToken(); + + final String graphApiToken; + final Set mappedAuthorities; + + try { + // https://github.com/MicrosoftDocs/azure-docs/issues/8121#issuecomment-387090099 + // In AAD App Registration configure oauth2AllowImplicitFlow to true + final ClientRegistration registration = userRequest.getClientRegistration(); + + final AzureADGraphClient graphClient = new AzureADGraphClient(registration.getClientId(), + registration.getClientSecret(), aadAuthProps, serviceEndpointsProps); + + graphApiToken = graphClient.acquireTokenForGraphApi(idToken.getTokenValue(), + aadAuthProps.getTenantId()).accessToken(); + + mappedAuthorities = graphClient.getGrantedAuthorities(graphApiToken); + } catch (MalformedURLException e) { + throw wrapException(INVALID_REQUEST, "Failed to acquire token for Graph API.", null, e); + } catch (ServiceUnavailableException e) { + throw wrapException(SERVER_ERROR, "Failed to acquire token for Graph API.", null, e); + } catch (IOException e) { + throw wrapException(SERVER_ERROR, "Failed to map group to authorities.", null, e); + } catch (MsalServiceException e) { + if (e.claims() != null && !e.claims().isEmpty()) { + throw wrapException(CONDITIONAL_ACCESS_POLICY, "Handle conditional access policy", null, e); + } else { + throw e; + } + } + + // Create a copy of oidcUser but use the mappedAuthorities instead + oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), getUserNameAttrName(userRequest)); + + return oidcUser; + } + + private OAuth2AuthenticationException wrapException(String errorCode, String errDesc, String uri, Exception e) { + final OAuth2Error oAuth2Error = new OAuth2Error(errorCode, errDesc, uri); + throw new OAuth2AuthenticationException(oAuth2Error, e); + } + + private String getUserNameAttrName(OAuth2UserRequest userRequest) { + String userNameAttrName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + if (StringUtils.isEmpty(userNameAttrName)) { + userNameAttrName = DEFAULT_USERNAME_ATTR_NAME; + } + + return userNameAttrName; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java new file mode 100644 index 000000000000..500d96d3aa29 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientCredential; +import com.microsoft.aad.msal4j.MsalServiceException; +import com.microsoft.aad.msal4j.OnBehalfOfParameters; +import com.microsoft.aad.msal4j.UserAssertion; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import javax.naming.ServiceUnavailableException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Microsoft Graph client encapsulation. + */ +public class AzureADGraphClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(AzureADGraphClient.class); + private static final SimpleGrantedAuthority DEFAULT_AUTHORITY = new SimpleGrantedAuthority("ROLE_USER"); + private static final String DEFAULT_ROLE_PREFIX = "ROLE_"; + private static final String MICROSOFT_GRAPH_SCOPE = "https://graph.microsoft.com/user.read"; + private static final String AAD_GRAPH_API_SCOPE = "https://graph.windows.net/user.read"; + + private final String clientId; + private final String clientSecret; + private final ServiceEndpoints serviceEndpoints; + private final AADAuthenticationProperties aadAuthenticationProperties; + + private static final String V2_VERSION_ENV_FLAG = "v2-graph"; + private boolean aadMicrosoftGraphApiBool; + + public AzureADGraphClient(String clientId, String clientSecret, AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.aadAuthenticationProperties = aadAuthProps; + this.serviceEndpoints = serviceEndpointsProps.getServiceEndpoints(aadAuthProps.getEnvironment()); + + this.initAADMicrosoftGraphApiBool(aadAuthProps.getEnvironment()); + } + + private void initAADMicrosoftGraphApiBool(String endpointEnv) { + this.aadMicrosoftGraphApiBool = endpointEnv.contains(V2_VERSION_ENV_FLAG); + } + + private String getUserMembershipsV1(String accessToken) throws IOException { + final URL url = new URL(serviceEndpoints.getAadMembershipRestUri()); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // Set the appropriate header fields in the request header. + + if (this.aadMicrosoftGraphApiBool) { + conn.setRequestMethod(HttpMethod.GET.toString()); + conn.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)); + conn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + conn.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } else { + conn.setRequestMethod(HttpMethod.GET.toString()); + conn.setRequestProperty("api-version", "1.6"); + conn.setRequestProperty(HttpHeaders.AUTHORIZATION, accessToken); + conn.setRequestProperty(HttpHeaders.ACCEPT, "application/json;odata=minimalmetadata"); + } + final String responseInJson = getResponseStringFromConn(conn); + final int responseCode = conn.getResponseCode(); + if (responseCode == HTTPResponse.SC_OK) { + return responseInJson; + } else { + throw new IllegalStateException("Response is not " + + HTTPResponse.SC_OK + ", response json: " + responseInJson); + } + } + + private static String getResponseStringFromConn(HttpURLConnection conn) throws IOException { + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + final StringBuilder stringBuffer = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuffer.append(line); + } + return stringBuffer.toString(); + } + } + + public List getGroups(String graphApiToken) throws IOException { + return loadUserGroups(graphApiToken); + } + + private List loadUserGroups(String graphApiToken) throws IOException { + final String responseInJson = getUserMembershipsV1(graphApiToken); + final List lUserGroups = new ArrayList<>(); + final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); + final JsonNode rootNode = objectMapper.readValue(responseInJson, JsonNode.class); + final JsonNode valuesNode = rootNode.get("value"); + + if (valuesNode != null) { + lUserGroups + .addAll(StreamSupport.stream(valuesNode.spliterator(), false).filter(this::isMatchingUserGroupKey) + .map(node -> { + final String objectID = node. + get(aadAuthenticationProperties.getUserGroup().getObjectIDKey()).asText(); + final String displayName = node.get("displayName").asText(); + return new UserGroup(objectID, displayName); + }).collect(Collectors.toList())); + + } + + return lUserGroups; + } + + /** + * Checks that the JSON Node is a valid User Group to extract User Groups from + * + * @param node - json node to look for a key/value to equate against the + * {@link AADAuthenticationProperties.UserGroupProperties} + * @return true if the json node contains the correct key, and expected value to identify a user group. + */ + private boolean isMatchingUserGroupKey(final JsonNode node) { + return node.get(aadAuthenticationProperties.getUserGroup().getKey()).asText() + .equals(aadAuthenticationProperties.getUserGroup().getValue()); + } + + public Set getGrantedAuthorities(String graphApiToken) throws IOException { + // Fetch the authority information from the protected resource using accessToken + final List groups = getGroups(graphApiToken); + + // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + return convertGroupsToGrantedAuthorities(groups); + } + + + /** + * Converts UserGroup list to Set of GrantedAuthorities + * + * @param groups user groups + * @return granted authorities + */ + public Set convertGroupsToGrantedAuthorities(final List groups) { + // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + final Set mappedAuthorities = groups.stream().filter(this::isValidUserGroupToGrantAuthority) + .map(userGroup -> new SimpleGrantedAuthority(DEFAULT_ROLE_PREFIX + userGroup.getDisplayName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (mappedAuthorities.isEmpty()) { + mappedAuthorities.add(DEFAULT_AUTHORITY); + } + + return mappedAuthorities; + } + + /** + * Determines if this is a valid {@link UserGroup} to build to a GrantedAuthority. + *

+ * If the {@link AADAuthenticationProperties.UserGroupProperties#getAllowedGroups()} or the {@link + * AADAuthenticationProperties#getActiveDirectoryGroups()} contains the {@link UserGroup#getDisplayName()} return + * true. + * + * @param group - User Group to check if valid to grant an authority to. + * @return true if either of the allowed-groups or active-directory-groups contains the UserGroup display name + */ + private boolean isValidUserGroupToGrantAuthority(final UserGroup group) { + return aadAuthenticationProperties.getUserGroup().getAllowedGroups().contains(group.getDisplayName()) + || aadAuthenticationProperties.getActiveDirectoryGroups().contains(group.getDisplayName()); + } + + public IAuthenticationResult acquireTokenForGraphApi(String idToken, String tenantId) + throws ServiceUnavailableException { + final IClientCredential clientCredential = ClientCredentialFactory.createFromSecret(clientSecret); + final UserAssertion assertion = new UserAssertion(idToken); + + IAuthenticationResult result = null; + ExecutorService service = null; + try { + service = Executors.newFixedThreadPool(1); + + final ConfidentialClientApplication application = ConfidentialClientApplication + .builder(clientId, clientCredential) + .authority(serviceEndpoints.getAadSigninUri() + tenantId + "/") + .build(); + + final Set scopes = new HashSet<>(); + scopes.add(aadMicrosoftGraphApiBool ? MICROSOFT_GRAPH_SCOPE : AAD_GRAPH_API_SCOPE); + + final OnBehalfOfParameters onBehalfOfParameters = OnBehalfOfParameters + .builder(scopes, assertion) + .build(); + + final CompletableFuture future = application.acquireToken(onBehalfOfParameters); + result = future.get(); + } catch (ExecutionException | InterruptedException | MalformedURLException e) { + // handle conditional access policy + final Throwable cause = e.getCause(); + if (cause instanceof MsalServiceException) { + final MsalServiceException exception = (MsalServiceException) cause; + if (exception.claims() != null && !exception.claims().isEmpty()) { + throw exception; + } + } + LOGGER.error("acquire on behalf of token for graph api error", e); + } finally { + if (service != null) { + service.shutdown(); + } + } + + if (result == null) { + throw new ServiceUnavailableException("unable to acquire on-behalf-of token for client " + clientId); + } + return result; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java new file mode 100644 index 000000000000..9b2e8101be88 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class JacksonObjectMapperFactory { + + private JacksonObjectMapperFactory() { + } + + public static ObjectMapper getInstance() { + return SingletonHelper.INSTANCE; + } + + private static class SingletonHelper { + private static final ObjectMapper INSTANCE = new ObjectMapper(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java new file mode 100644 index 000000000000..157f0d4e6fa7 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + + +/** + * Pojo file to store the service urls for different azure services. + */ +public class ServiceEndpoints { + private String aadSigninUri; + private String aadGraphApiUri; + private String aadKeyDiscoveryUri; + private String aadMembershipRestUri; + + public String getAadSigninUri() { + return aadSigninUri; + } + + public void setAadSigninUri(String aadSigninUri) { + this.aadSigninUri = aadSigninUri; + } + + public String getAadGraphApiUri() { + return aadGraphApiUri; + } + + public void setAadGraphApiUri(String aadGraphApiUri) { + this.aadGraphApiUri = aadGraphApiUri; + } + + public String getAadKeyDiscoveryUri() { + return aadKeyDiscoveryUri; + } + + public void setAadKeyDiscoveryUri(String aadKeyDiscoveryUri) { + this.aadKeyDiscoveryUri = aadKeyDiscoveryUri; + } + + public String getAadMembershipRestUri() { + return aadMembershipRestUri; + } + + public void setAadMembershipRestUri(String aadMembershipRestUri) { + this.aadMembershipRestUri = aadMembershipRestUri; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java new file mode 100644 index 000000000000..e8cbb168d3bf --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConfigurationProperties("azure.service") +public class ServiceEndpointsProperties { + private Map endpoints = new HashMap<>(); + + public Map getEndpoints() { + return endpoints; + } + + /** + * Get ServiceEndpoints data for the given environment. + * + * @param environment the environment of the cloud service + * @return The ServiceEndpoints data for the given azure service environment + */ + public ServiceEndpoints getServiceEndpoints(String environment) { + Assert.notEmpty(endpoints, "No service endpoints found"); + + if (!endpoints.containsKey(environment)) { + throw new IllegalArgumentException(environment + " is not found in the configuration," + + " only following environments are supported: " + endpoints.keySet()); + } + + return endpoints.get(environment); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java new file mode 100644 index 000000000000..360ae47ebb29 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.io.Serializable; +import java.util.Objects; + +public class UserGroup implements Serializable { + private static final long serialVersionUID = 9064197572478554735L; + + private String objectID; + private String displayName; + + public UserGroup(String objectID, String displayName) { + this.objectID = objectID; + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public String getObjectID() { + return objectID; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof UserGroup)) { + return false; + } + final UserGroup group = (UserGroup) o; + return this.getDisplayName().equals(group.getDisplayName()) + && this.getObjectID().equals(group.getObjectID()); + } + + @Override + public int hashCode() { + return Objects.hash(objectID, displayName); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java new file mode 100644 index 000000000000..0d7fa096bee6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class UserPrincipal implements Serializable { + private static final long serialVersionUID = -3725690847771476854L; + + private JWSObject jwsObject; + private JWTClaimsSet jwtClaimsSet; + private List userGroups = new ArrayList<>(); + + public UserPrincipal(JWSObject jwsObject, JWTClaimsSet jwtClaimsSet) { + this.jwsObject = jwsObject; + this.jwtClaimsSet = jwtClaimsSet; + } + + // claimset + public String getIssuer() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getIssuer(); + } + + public String getSubject() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getSubject(); + } + + public Map getClaims() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaims(); + } + + public Object getClaim() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim("tid"); + } + + public Object getClaim(String name) { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim(name); + } + + public String getUpn() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("upn"); + } + + public String getUniqueName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("unique_name"); + } + + public String getName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("name"); + } + + // header + public String getKid() { + return jwsObject == null ? null : jwsObject.getHeader().getKeyID(); + } + + public void setUserGroups(List groups) { + this.userGroups = groups; + } + + public List getUserGroups() { + return this.userGroups; + } + + public boolean isMemberOf(UserGroup group) { + return !(userGroups == null || userGroups.isEmpty()) && userGroups.contains(group); + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java new file mode 100644 index 000000000000..0a1353693e45 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.jwk.source.JWKSetCache; +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.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTClaimsSetVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * A user principal manager to load user info from JWT. + */ +public class UserPrincipalManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserPrincipalManager.class); + + private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; + private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; + private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; + + private final JWKSource keySource; + private final AADAuthenticationProperties aadAuthProps; + private final Boolean explicitAudienceCheck; + private final Set validAudiences = new HashSet<>(); + + /**ø + * Creates a new {@link UserPrincipalManager} with a predefined {@link JWKSource}. + *

+ * This is helpful in cases the JWK is not a remote JWKSet or for unit testing. + * + * @param keySource - {@link JWKSource} containing at least one key + */ + public UserPrincipalManager(JWKSource keySource) { + this.keySource = keySource; + this.explicitAudienceCheck = false; + this.aadAuthProps = null; + } + + /** + * Create a new {@link UserPrincipalManager} based of the {@link ServiceEndpoints#getAadKeyDiscoveryUri()} and + * {@link AADAuthenticationProperties#getEnvironment()}. + * + * @param serviceEndpointsProps - used to retrieve the JWKS URL + * @param aadAuthProps - used to retrieve the environment. + * @param resourceRetriever - configures the {@link RemoteJWKSet} call. + * @param explicitAudienceCheck - explicit audience check + */ + public UserPrincipalManager(ServiceEndpointsProperties serviceEndpointsProps, + AADAuthenticationProperties aadAuthProps, + ResourceRetriever resourceRetriever, + boolean explicitAudienceCheck) { + this.aadAuthProps = aadAuthProps; + this.explicitAudienceCheck = explicitAudienceCheck; + if (explicitAudienceCheck) { + // client-id for "normal" check + this.validAudiences.add(this.aadAuthProps.getClientId()); + // app id uri for client credentials flow (server to server communication) + this.validAudiences.add(this.aadAuthProps.getAppIdUri()); + } + try { + keySource = new RemoteJWKSet<>(new URL(serviceEndpointsProps + .getServiceEndpoints(aadAuthProps.getEnvironment()).getAadKeyDiscoveryUri()), resourceRetriever); + } catch (MalformedURLException e) { + LOGGER.error("Failed to parse active directory key discovery uri.", e); + throw new IllegalStateException("Failed to parse active directory key discovery uri.", e); + } + } + + /** + * Create a new {@link UserPrincipalManager} based of the {@link ServiceEndpoints#getAadKeyDiscoveryUri()} and + * {@link AADAuthenticationProperties#getEnvironment()}. + * + * @param serviceEndpointsProps - used to retrieve the JWKS URL + * @param aadAuthProps - used to retrieve the environment. + * @param resourceRetriever - configures the {@link RemoteJWKSet} call. + * @param jwkSetCache - used to cache the JWK set for a finite time, default set to 5 minutes + * which matches constructor above if no jwkSetCache is passed in + * @param explicitAudienceCheck - explicit audience check + */ + public UserPrincipalManager(ServiceEndpointsProperties serviceEndpointsProps, + AADAuthenticationProperties aadAuthProps, + ResourceRetriever resourceRetriever, + boolean explicitAudienceCheck, + JWKSetCache jwkSetCache) { + this.aadAuthProps = aadAuthProps; + this.explicitAudienceCheck = explicitAudienceCheck; + if (explicitAudienceCheck) { + // client-id for "normal" check + this.validAudiences.add(this.aadAuthProps.getClientId()); + // app id uri for client credentials flow (server to server communication) + this.validAudiences.add(this.aadAuthProps.getAppIdUri()); + } + try { + keySource = new RemoteJWKSet<>(new URL(serviceEndpointsProps + .getServiceEndpoints(aadAuthProps.getEnvironment()).getAadKeyDiscoveryUri()), + resourceRetriever, + jwkSetCache); + } catch (MalformedURLException e) { + LOGGER.error("Failed to parse active directory key discovery uri.", e); + throw new IllegalStateException("Failed to parse active directory key discovery uri.", e); + } + } + + public UserPrincipal buildUserPrincipal(String idToken) throws ParseException, JOSEException, BadJOSEException { + final JWSObject jwsObject = JWSObject.parse(idToken); + final ConfigurableJWTProcessor validator = + getAadJwtTokenValidator(jwsObject.getHeader().getAlgorithm()); + final JWTClaimsSet jwtClaimsSet = validator.process(idToken, null); + final JWTClaimsSetVerifier verifier = validator.getJWTClaimsSetVerifier(); + verifier.verify(jwtClaimsSet, null); + + return new UserPrincipal(jwsObject, jwtClaimsSet); + } + + private ConfigurableJWTProcessor getAadJwtTokenValidator(JWSAlgorithm jwsAlgorithm) { + final ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + + final JWSKeySelector keySelector = + new JWSVerificationKeySelector<>(jwsAlgorithm, keySource); + jwtProcessor.setJWSKeySelector(keySelector); + + //TODO: would it make sense to inject it? and make it configurable or even allow to provide own implementation + 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.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) + || issuer.startsWith(STS_WINDOWS_ISSUER) + || issuer.startsWith(STS_CHINA_CLOUD_API_ISSUER))) { + throw new BadJWTException("Invalid token issuer"); + } + if (explicitAudienceCheck) { + final Optional matchedAudience = claimsSet.getAudience().stream() + .filter(UserPrincipalManager.this.validAudiences::contains).findFirst(); + if (matchedAudience.isPresent()) { + LOGGER.debug("Matched audience [{}]", matchedAudience.get()); + } else { + throw new BadJWTException("Invalid token audience. Provided value " + claimsSet.getAudience() + + "does not match neither client-id nor AppIdUri."); + } + } + } + }); + return jwtProcessor; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java new file mode 100644 index 000000000000..16e6f692712b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* + * Disclaimer: + * This class is copied from https://github.com/Microsoft/azure-tools-for-java/ with minor modification (fixing + * static analysis error). + * Location in the repo: /Utils/azuretools-core/src/com/microsoft/azuretools/azurecommons/util/GetHashMac.java + */ + +package com.microsoft.azure.spring.support; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class GetHashMac { + public static final String MAC_REGEX = "([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}"; + public static final String MAC_REGEX_ZERO = "([0]{2}[:-]){5}[0]{2}"; + public static final String HASHED_MAC_REGEX = "[0-9a-f]{64}"; + + private GetHashMac() { + super(); + } + + public static boolean isValidHashMacFormat(String hashMac) { + if (hashMac == null || hashMac.isEmpty()) { + return false; + } + + final Pattern hashedMacPattern = Pattern.compile(HASHED_MAC_REGEX); + final Matcher matcher = hashedMacPattern.matcher(hashMac); + return matcher.matches(); + } + + public static String getHashMac() { + final String rawMac = getRawMac(); + if (rawMac == null || rawMac.isEmpty()) { + return null; + } + + final Pattern pattern = Pattern.compile(MAC_REGEX); + final Pattern patternZero = Pattern.compile(MAC_REGEX_ZERO); + final Matcher matcher = pattern.matcher(rawMac); + String mac = ""; + while (matcher.find()) { + mac = matcher.group(0); + if (!patternZero.matcher(mac).matches()) { + break; + } + } + + return hash(mac); + } + + private static String getRawMac() { + final StringBuilder ret = new StringBuilder(); + + final String os = System.getProperty("os.name"); + String[] command = {"ifconfig", "-a"}; + if (os != null && !os.isEmpty() && os.toLowerCase(Locale.US).startsWith("win")) { + command = new String[]{"getmac"}; + } + + try { + final ProcessBuilder builder = new ProcessBuilder(command); + final Process process = builder.start(); + + try (InputStream inputStream = process.getInputStream(); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(inputStreamReader)) { + String tmp; + while ((tmp = br.readLine()) != null) { + ret.append(tmp); + } + } + } catch (IOException e) { + return null; + } + + return ret.toString(); + } + + private static String hash(String mac) { + if (mac == null || mac.isEmpty()) { + return null; + } + + final String ret; + try { + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + final byte[] bytes = mac.getBytes(StandardCharsets.UTF_8); + md.update(bytes); + final byte[] bytesAfterDigest = md.digest(); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytesAfterDigest.length; i++) { + sb.append(Integer.toString((bytesAfterDigest[i] & 0xff) + 0x100, 16).substring(1)); + } + + ret = sb.toString(); + } catch (NoSuchAlgorithmException ex) { + return null; + } + + return ret; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java new file mode 100644 index 000000000000..7dd24b4da4d4 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.support; + +/** + * Util class to generate user agent. + */ +public class UserAgent { + /** + * Generate UserAgent string for given service. + * + * @param serviceName Name of the service from which called this method. + * @param allowTelemetry Whether allows telemtry + * @return generated UserAgent string + */ + public static String getUserAgent(String serviceName, boolean allowTelemetry) { + String macAddress = "Not Collected"; + if (allowTelemetry) { + macAddress = GetHashMac.getHashMac(); + } + + return String.format(serviceName + " MacAddressHash:%s", macAddress); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java new file mode 100644 index 000000000000..2c590973fb48 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.telemetry; + +/** + * This class contains constants like telemetry keys and methods to retrieve telemetry info. + */ +public class TelemetryData { + + public static final String INSTALLATION_ID = "installationId"; + public static final String PROJECT_VERSION = "version"; + public static final String SERVICE_NAME = "serviceName"; + public static final String HASHED_ACCOUNT_NAME = "hashedAccountName"; + public static final String HASHED_NAMESPACE = "hashedNamespace"; + public static final String TENANT_NAME = "tenantName"; + + public static String getClassPackageSimpleName(Class clazz) { + if (clazz == null) { + return "unknown"; + } + + return clazz.getPackage().getName().replaceAll("\\w+\\.", ""); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java new file mode 100644 index 000000000000..d77319258a7c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.telemetry; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.azure.utils.PropertyLoader; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.time.Instant; +import java.util.Map; + +/** + * Telemetry data will be sent to Application Insights. + */ +public class TelemetryEventData { + + private final String name; + + @JsonProperty("iKey") + private final String instrumentationKey; + + private final Tags tags = new Tags("Spring-on-azure", "Java-maven-plugin"); + + private final EventData data = new EventData("EventData"); + + private final String time; + + public TelemetryEventData(String eventName, @NonNull Map properties) { + Assert.hasText(eventName, "Event name should contain text."); + + name = "Microsoft.ApplicationInsights.Event"; + instrumentationKey = PropertyLoader.getTelemetryInstrumentationKey(); + + data.getBaseData().setName(eventName); + data.getBaseData().setProperties(properties); + time = Instant.now().toString(); + } + + private static class Tags { + + @JsonProperty("ai.cloud.roleInstance") + private final String aiCloudRoleInstance; + + @JsonProperty("ai.internal.sdkVersion") + private final String aiInternalSdkVersion; + + Tags(String instance, String sdkVersion) { + aiCloudRoleInstance = instance; + aiInternalSdkVersion = sdkVersion; + } + + public String getAiCloudRoleInstance() { + return aiCloudRoleInstance; + } + + public String getAiInternalSdkVersion() { + return aiInternalSdkVersion; + } + } + + private static class EventData { + + private final String baseType; + + private final CustomData baseData = new CustomData(); + + EventData(String baseType) { + this.baseType = baseType; + } + + private static class CustomData { + + private final Integer ver = 2; + + private String name; + + private Map properties; + + public Integer getVer() { + return ver; + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + private void setName(String name) { + this.name = name; + } + + private void setProperties(Map properties) { + this.properties = properties; + } + } + + public String getBaseType() { + return baseType; + } + + public CustomData getBaseData() { + return baseData; + } + } + + public String getName() { + return name; + } + + public String getInstrumentationKey() { + return instrumentationKey; + } + + public Tags getTags() { + return tags; + } + + public EventData getData() { + return data; + } + + public String getTime() { + return time; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java new file mode 100644 index 000000000000..858a866b8eaf --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.telemetry; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Telemetry configuration's properties. + */ +@ConfigurationProperties("telemetry") +public class TelemetryProperties { + + private String instrumentationKey; + + public String getInstrumentationKey() { + return instrumentationKey; + } + + public void setInstrumentationKey(String instrumentationKey) { + this.instrumentationKey = instrumentationKey; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java new file mode 100644 index 000000000000..4037d650645c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.telemetry; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.support.GetHashMac; +import com.microsoft.azure.utils.PropertyLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON; + +/** + * Client used for sending telemetry info. + */ +public class TelemetrySender { + + private static final Logger LOGGER = LoggerFactory.getLogger(TelemetrySender.class); + + private static final String TELEMETRY_TARGET_URL = "https://dc.services.visualstudio.com/v2/track"; + + private static final String PROJECT_INFO = "spring-boot-starter/" + PropertyLoader.getProjectVersion(); + + private static final int RETRY_LIMIT = 3; // Align the retry times with sdk + + private static final RestTemplate REST_TEMPLATE = new RestTemplate(); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final HttpHeaders HEADERS = new HttpHeaders(); + + static { + HEADERS.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON.toString()); + } + + private ResponseEntity executeRequest(final TelemetryEventData eventData) { + try { + final HttpEntity body = new HttpEntity<>(MAPPER.writeValueAsString(eventData), HEADERS); + + return REST_TEMPLATE.exchange(TELEMETRY_TARGET_URL, HttpMethod.POST, body, String.class); + } catch (RestClientException | JsonProcessingException e) { + LOGGER.warn("Failed to exchange telemetry request, {}.", e.getMessage()); + } + + return null; + } + + private void sendTelemetryData(@NonNull TelemetryEventData eventData) { + ResponseEntity response = null; + + for (int i = 0; i < RETRY_LIMIT; i++) { + response = executeRequest(eventData); + + if (response != null && response.getStatusCode() == HttpStatus.OK) { + return; + } + } + + if (response != null && response.getStatusCode() != HttpStatus.OK) { + LOGGER.warn("Failed to send telemetry data, response status code {}.", response.getStatusCode().toString()); + } + } + + public void send(String name, @NonNull Map properties) { + Assert.hasText(name, "Event name should contain text."); + + properties.putIfAbsent(TelemetryData.INSTALLATION_ID, GetHashMac.getHashMac()); + properties.putIfAbsent(TelemetryData.PROJECT_VERSION, PROJECT_INFO); + + sendTelemetryData(new TelemetryEventData(name, properties)); + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java new file mode 100644 index 000000000000..f9d951046811 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Util class to load property files. + */ +public class PropertyLoader { + private static final String PROJECT_PROPERTY_FILE = "/META-INF/project.properties"; + + private static final String TELEMETRY_CONFIG_FILE = "/telemetry.config"; + + private static String getProperty(String file, String property) { + try (InputStream inputStream = PropertyLoader.class.getResourceAsStream(file)) { + if (inputStream != null) { + final Properties properties = new Properties(); + properties.load(inputStream); + + return properties.getProperty(property); + } + } catch (IOException e) { + // Omitted + } + + return "unknown"; + } + + public static String getProjectVersion() { + return getProperty(PROJECT_PROPERTY_FILE, "project.version"); + } + + public static String getTelemetryInstrumentationKey() { + return getProperty(TELEMETRY_CONFIG_FILE, "telemetry.instrumentationKey"); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties new file mode 100644 index 000000000000..90d42083480f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties @@ -0,0 +1 @@ +project.version=@project.version@ diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..603d44a26bf0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,15 @@ +org.springframework.boot.env.EnvironmentPostProcessor=com.microsoft.azure.spring.cloudfoundry.environment.VcapProcessor,\ +com.microsoft.azure.spring.keyvault.KeyVaultEnvironmentPostProcessor +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosDbRepositoriesAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosDbReactiveRepositoriesAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.gremlin.GremlinAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.gremlin.GremlinRepositoriesAutoConfiguration,\ +com.azure.spring.autoconfigure.mediaservices.MediaServicesAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.servicebus.ServiceBusAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.storage.StorageAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.aad.AADOAuth2AutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.btoc.AADB2CAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.metrics.AzureMonitorMetricsExportAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.jms.ServiceBusJMSAutoConfiguration diff --git a/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties b/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties new file mode 100644 index 000000000000..ff355a06c5de --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties @@ -0,0 +1,12 @@ +spring.security.oauth2.client.provider.azure.authorization-uri=https://login.microsoftonline.com/common/oauth2/authorize +spring.security.oauth2.client.provider.azure.token-uri=https://login.microsoftonline.com/common/oauth2/token +spring.security.oauth2.client.provider.azure.user-info-uri=https://login.microsoftonline.com/common/openid/userinfo +spring.security.oauth2.client.provider.azure.jwk-set-uri=https://login.microsoftonline.com/common/discovery/keys +spring.security.oauth2.client.provider.azure.user-name-attribute=name + +spring.security.oauth2.client.registration.azure.client-authentication-method=post +spring.security.oauth2.client.registration.azure.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.azure.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} +spring.security.oauth2.client.registration.azure.scope=openid, https://graph.microsoft.com/user.read +spring.security.oauth2.client.registration.azure.client-name=Azure +spring.security.oauth2.client.registration.azure.provider=azure diff --git a/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties b/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties new file mode 100644 index 000000000000..236d2ff8b308 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties @@ -0,0 +1,16 @@ +azure.service.endpoints.cn.aadSigninUri=https://login.partner.microsoftonline.cn/ +azure.service.endpoints.cn.aadGraphApiUri=https://graph.chinacloudapi.cn/ +azure.service.endpoints.cn.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys +azure.service.endpoints.cn.aadMembershipRestUri=https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6 +azure.service.endpoints.cn-v2-graph.aadSigninUri=https://login.partner.microsoftonline.cn/ +azure.service.endpoints.cn-v2-graph.aadGraphApiUri=https://microsoftgraph.chinacloudapi.cn/ +azure.service.endpoints.cn-v2-graph.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys +azure.service.endpoints.cn-v2-graph.aadMembershipRestUri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf +azure.service.endpoints.global.aadSigninUri=https://login.microsoftonline.com/ +azure.service.endpoints.global.aadGraphApiUri=https://graph.windows.net/ +azure.service.endpoints.global.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/ +azure.service.endpoints.global.aadMembershipRestUri=https://graph.windows.net/me/memberOf?api-version=1.6 +azure.service.endpoints.global-v2-graph.aadSigninUri=https://login.microsoftonline.com/ +azure.service.endpoints.global-v2-graph.aadGraphApiUri=https://graph.microsoft.com/ +azure.service.endpoints.global-v2-graph.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/ +azure.service.endpoints.global-v2-graph.aadMembershipRestUri=https://graph.microsoft.com/v1.0/me/memberOf diff --git a/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config b/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config new file mode 100644 index 000000000000..61b615fa0ef1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config @@ -0,0 +1 @@ +telemetry.instrumentationKey=@telemetry.instrumentationKey@ \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAppRoleStatelessAuthenticationFilterConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAppRoleStatelessAuthenticationFilterConfigSample.java new file mode 100644 index 000000000000..94ee2cd2e2e5 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAppRoleStatelessAuthenticationFilterConfigSample.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad; + +import com.microsoft.azure.spring.autoconfigure.aad.AADAppRoleStatelessAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + *

+ * Code samples for the AADAppRoleStatelessAuthenticationFilter in README.md + */ +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADAppRoleStatelessAuthenticationFilterConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private AADAppRoleStatelessAuthenticationFilter appRoleAuthFilter; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterBefore(appRoleAuthFilter, UsernamePasswordAuthenticationFilter.class); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAuthenticationFilterConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAuthenticationFilterConfigSample.java new file mode 100644 index 000000000000..2e0747dedb8f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAuthenticationFilterConfigSample.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad; + +import com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + *

+ * Code samples for AADAuthenticationFilter in README.md + */ +@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) +public class AADAuthenticationFilterConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private AADAuthenticationFilter aadAuthFilter; + +} diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java new file mode 100644 index 000000000000..1ca114f1d768 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad; + +import com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFailureHandler; +import com.microsoft.azure.spring.autoconfigure.aad.AADOAuth2AuthorizationRequestResolver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + *

+ * Code samples for the AAD OAuth2.0 login in README.md, including handling for AAD conditional policy + */ +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADOAuth2LoginConditionalPolicyConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private OAuth2UserService oidcUserService; + + @Autowired + ApplicationContext applicationContext; + + @Override + protected void configure(HttpSecurity http) throws Exception { + final ClientRegistrationRepository clientRegistrationRepository = + applicationContext.getBean(ClientRegistrationRepository.class); + + http.authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService) + .and() + .authorizationEndpoint() + .authorizationRequestResolver(new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository)) + .and() + .failureHandler(new AADAuthenticationFailureHandler()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConfigSample.java new file mode 100644 index 000000000000..402b0953d87b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConfigSample.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + *

+ * Code samples for the AAD OAuth2.0 login in README.md + */ +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADOAuth2LoginConfigSample extends WebSecurityConfigurerAdapter { + + @Autowired + private OAuth2UserService oidcUserService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java new file mode 100644 index 000000000000..ea893e087099 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader.Builder; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.BadJWTException; +import net.minidev.json.JSONArray; +import org.assertj.core.api.Assertions; +import org.hamcrest.CoreMatchers; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AADAppRoleAuthenticationFilterTest { + + public static final String TOKEN = "dummy-token"; + + private final UserPrincipalManager userPrincipalManager; + private final HttpServletRequest request; + private final HttpServletResponse response; + private final SimpleGrantedAuthority roleAdmin; + private final SimpleGrantedAuthority roleUser; + private final AADAppRoleStatelessAuthenticationFilter filter; + + private UserPrincipal createUserPrincipal(Collection roles) { + final JSONArray claims = new JSONArray(); + claims.addAll(roles); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .subject("john doe") + .claim("roles", claims) + .build(); + final JWSObject jwsObject = new JWSObject(new Builder(JWSAlgorithm.RS256).build(), + new Payload(jwtClaimsSet.toString())); + return new UserPrincipal(jwsObject, jwtClaimsSet); + } + + public AADAppRoleAuthenticationFilterTest() { + userPrincipalManager = mock(UserPrincipalManager.class); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + roleAdmin = new SimpleGrantedAuthority("ROLE_admin"); + roleUser = new SimpleGrantedAuthority("ROLE_user"); + filter = new AADAppRoleStatelessAuthenticationFilter(userPrincipalManager); + } + + @Test + public void testDoFilterGoodCase() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + final UserPrincipal dummyPrincipal = createUserPrincipal(Arrays.asList("user", "admin")); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal); + + // Check in subsequent filter that authentication is available! + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNotNull(authentication); + assertTrue("User should be authenticated!", authentication.isAuthenticated()); + assertEquals(dummyPrincipal, authentication.getPrincipal()); + + @SuppressWarnings("unchecked") + final Collection authorities = (Collection) authentication + .getAuthorities(); + Assertions.assertThat(authorities).containsExactlyInAnyOrder(roleAdmin, roleUser); + }; + + filter.doFilterInternal(request, response, filterChain); + + verify(userPrincipalManager).buildUserPrincipal(TOKEN); + assertNull("Authentication has not been cleaned up!", SecurityContextHolder.getContext().getAuthentication()); + } + + @Test(expected = ServletException.class) + public void testDoFilterShouldRethrowJWTException() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(any())).thenThrow(new BadJWTException("bad token")); + + filter.doFilterInternal(request, response, mock(FilterChain.class)); + } + + @Test + public void testDoFilterAddsDefaultRole() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + + final UserPrincipal dummyPrincipal = createUserPrincipal(Collections.emptyList()); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal); + + // Check in subsequent filter that authentication is available and default roles are filled. + final FilterChain filterChain = (request, response) -> { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNotNull(authentication); + assertTrue("User should be authenticated!", authentication.isAuthenticated()); + final SimpleGrantedAuthority expectedDefaultRole = new SimpleGrantedAuthority("ROLE_USER"); + + @SuppressWarnings("unchecked") + final Collection authorities = (Collection) authentication + .getAuthorities(); + Assertions.assertThat(authorities).containsExactlyInAnyOrder(expectedDefaultRole); + }; + + filter.doFilterInternal(request, response, filterChain); + + verify(userPrincipalManager).buildUserPrincipal(TOKEN); + assertNull("Authentication has not been cleaned up!", SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + public void testRolesToGrantedAuthoritiesShouldConvertRolesAndFilterNulls() { + final JSONArray roles = new JSONArray().appendElement("user").appendElement(null).appendElement("ADMIN"); + final AADAppRoleStatelessAuthenticationFilter filter = new AADAppRoleStatelessAuthenticationFilter(null); + final Set result = filter.rolesToGrantedAuthorities(roles); + assertThat("Set should contain the two granted authority 'ROLE_user' and 'ROLE_ADMIN'", result, + CoreMatchers.hasItems(new SimpleGrantedAuthority("ROLE_user"), + new SimpleGrantedAuthority("ROLE_ADMIN"))); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java new file mode 100644 index 000000000000..24c137fc77f4 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.core.env.Environment; + +import java.util.Map; + +import static com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationProperties.UserGroupProperties; +import static org.assertj.core.api.Assertions.assertThat; + +public class AADAuthenticationAutoConfigurationTest { + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) + .withPropertyValues("azure.activedirectory.client-id=fake-client-id", + "azure.activedirectory.client-secret=fake-client-secret", + "azure.activedirectory.active-directory-groups=fake-group", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri", + Constants.ALLOW_TELEMETRY_PROPERTY + "=false"); + + @Test + public void createAADAuthenticationFilter() { + this.contextRunner.run(context -> { + final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class); + assertThat(azureADJwtTokenFilter).isNotNull(); + assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); + }); + } + + @Test + public void serviceEndpointsCanBeOverridden() { + this.contextRunner.withPropertyValues("azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadSigninUri=https://test/", + "azure.service.endpoints.global.aadGraphApiUri=https://test/", + "azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadMembershipRestUri=https://test/") + .run(context -> { + final Environment environment = context.getEnvironment(); + assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadGraphApiUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadKeyDiscoveryUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadMembershipRestUri")) + .isEqualTo("https://test/"); + final ServiceEndpointsProperties serviceEndpointsProperties = + context.getBean(ServiceEndpointsProperties.class); + assertThat(serviceEndpointsProperties).isNotNull(); + assertThat(serviceEndpointsProperties.getEndpoints()).isNotEmpty(); + + final Map endpoints = serviceEndpointsProperties.getEndpoints(); + assertThat(endpoints).hasSize(4); + assertThat(endpoints.get("cn")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://graph.chinacloudapi.cn/", + "https://login.partner.microsoftonline.cn/common/discovery/keys", + "https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6", + "https://login.partner.microsoftonline.cn/"); + assertThat(endpoints.get("global")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://test/", "https://test/", "https://test/", "https://test/"); + }); + } + + @Test + public void testUserGroupPropertiesAreOverridden() { + contextRunner.withPropertyValues("azure.activedirectory.user-group.allowed-groups=another_group,third_group", + "azure.activedirectory.user-group.key=key", "azure.activedirectory.user-group.value=value", + "azure.activedirectory.user-group.object-id-key=objidk") + .run(context -> { + assertThat(context.getBean(AADAuthenticationProperties.class)).isNotNull(); + + final UserGroupProperties userGroupProperties = context + .getBean(AADAuthenticationProperties.class).getUserGroup(); + + assertThat(userGroupProperties).hasNoNullFieldsOrProperties() + .extracting(UserGroupProperties::getKey, UserGroupProperties::getValue, + UserGroupProperties::getObjectIDKey).containsExactly("key", "value", "objidk"); + + assertThat(userGroupProperties.getAllowedGroups()).isNotEmpty() + .hasSize(2).containsExactly("another_group", "third_group"); + } + ); + + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java new file mode 100644 index 000000000000..85604cca1e36 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.ObjectError; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static com.microsoft.azure.spring.autoconfigure.aad.Constants.ALLOW_TELEMETRY_PROPERTY; +import static com.microsoft.azure.spring.autoconfigure.aad.Constants.CLIENT_ID_PROPERTY; +import static com.microsoft.azure.spring.autoconfigure.aad.Constants.CLIENT_SECRET_PROPERTY; +import static com.microsoft.azure.spring.autoconfigure.aad.Constants.TARGETED_GROUPS_PROPERTY; +import static org.assertj.core.api.Assertions.assertThat; + +public class AADAuthenticationFilterPropertiesTest { + @After + public void clearAllProperties() { + System.clearProperty(Constants.SERVICE_ENVIRONMENT_PROPERTY); + System.clearProperty(CLIENT_ID_PROPERTY); + System.clearProperty(CLIENT_SECRET_PROPERTY); + System.clearProperty(TARGETED_GROUPS_PROPERTY); + } + + @Test + public void canSetProperties() { + configureAllRequiredProperties(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + + final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + + assertThat(properties.getClientId()).isEqualTo(Constants.CLIENT_ID); + assertThat(properties.getClientSecret()).isEqualTo(Constants.CLIENT_SECRET); + assertThat(properties.getActiveDirectoryGroups() + .toString()).isEqualTo(Constants.TARGETED_GROUPS.toString()); + } + } + + @Test + public void defaultEnvironmentIsGlobal() { + configureAllRequiredProperties(); + assertThat(System.getProperty(Constants.SERVICE_ENVIRONMENT_PROPERTY)).isNullOrEmpty(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + + final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + + assertThat(properties.getEnvironment()).isEqualTo(Constants.DEFAULT_ENVIRONMENT); + } + } + + private void configureAllRequiredProperties() { + System.setProperty(CLIENT_ID_PROPERTY, Constants.CLIENT_ID); + System.setProperty(CLIENT_SECRET_PROPERTY, Constants.CLIENT_SECRET); + System.setProperty(TARGETED_GROUPS_PROPERTY, + Constants.TARGETED_GROUPS.toString().replace("[", "").replace("]", "")); + System.setProperty(ALLOW_TELEMETRY_PROPERTY, "false"); + } + + @Test + @Ignore // TODO (wepa) clientId and clientSecret can also be configured in oauth2 config, test to be refactored + public void emptySettingsNotAllowed() { + System.setProperty(CLIENT_ID_PROPERTY, ""); + System.setProperty(CLIENT_SECRET_PROPERTY, ""); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + Exception exception = null; + + context.register(Config.class); + + try { + context.refresh(); + } catch (Exception e) { + exception = e; + } + + assertThat(exception).isNotNull(); + assertThat(exception).isExactlyInstanceOf(ConfigurationPropertiesBindException.class); + + final BindValidationException bindException = (BindValidationException) exception.getCause().getCause(); + final List errors = bindException.getValidationErrors().getAllErrors(); + + final List errorStrings = errors.stream().map(e -> e.toString()).collect(Collectors.toList()); + + final List errorStringsExpected = Arrays.asList( + "Field error in object 'azure.activedirectory' on field 'activeDirectoryGroups': " + + "rejected value [null];", + "Field error in object 'azure.activedirectory' on field 'clientId': rejected value [];", + "Field error in object 'azure.activedirectory' on field 'clientSecret': rejected value [];" + ); + + Collections.sort(errorStrings); + + assertThat(errors.size()).isEqualTo(errorStringsExpected.size()); + + for (int i = 0; i < errorStrings.size(); i++) { + assertThat(errorStrings.get(i)).contains(errorStringsExpected.get(i)); + } + } + } + + @Configuration + @EnableConfigurationProperties(AADAuthenticationProperties.class) + static class Config { + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java new file mode 100644 index 000000000000..70954873eb73 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AADAuthenticationFilterTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)); + + @Before + @Ignore + public void beforeEveryMethod() { + Assume.assumeTrue(!Constants.CLIENT_ID.contains("real_client_id")); + Assume.assumeTrue(!Constants.CLIENT_SECRET.contains("real_client_secret")); + Assume.assumeTrue(!Constants.BEARER_TOKEN.contains("real_jtw_bearer_token")); + } + + //TODO (Zhou Liu): current test case is out of date, a new test case need to cover here, do it later. + @Test + @Ignore + public void doFilterInternal() { + this.contextRunner.withPropertyValues(Constants.CLIENT_ID_PROPERTY, Constants.CLIENT_ID) + .withPropertyValues(Constants.CLIENT_SECRET_PROPERTY, Constants.CLIENT_SECRET) + .withPropertyValues(Constants.TARGETED_GROUPS_PROPERTY, + Constants.TARGETED_GROUPS.toString() + .replace("[", "").replace("]", "")); + + this.contextRunner.run(context -> { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(Constants.TOKEN_HEADER)).thenReturn(Constants.BEARER_TOKEN); + + final HttpServletResponse response = mock(HttpServletResponse.class); + final FilterChain filterChain = mock(FilterChain.class); + + + final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class); + assertThat(azureADJwtTokenFilter).isNotNull(); + assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); + + azureADJwtTokenFilter.doFilterInternal(request, response, filterChain); + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication.getPrincipal()).isNotNull(); + assertThat(authentication.getPrincipal()).isExactlyInstanceOf(UserPrincipal.class); + assertThat(authentication.getAuthorities()).isNotNull(); + assertThat(authentication.getAuthorities().size()).isEqualTo(2); + assertThat(authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group1")) + && authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group2")) + ).isTrue(); + + final UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + assertThat(principal.getIssuer()).isNotNull().isNotEmpty(); + assertThat(principal.getKid()).isNotNull().isNotEmpty(); + assertThat(principal.getSubject()).isNotNull().isNotEmpty(); + + assertThat(principal.getClaims()).isNotNull().isNotEmpty(); + final Map claims = principal.getClaims(); + assertThat(claims.get("iss")).isEqualTo(principal.getIssuer()); + assertThat(claims.get("sub")).isEqualTo(principal.getSubject()); + }); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java new file mode 100644 index 000000000000..8181b0580ad5 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePropertySource; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AADOAuth2ConfigTest { + private static final String AAD_OAUTH2_MINIMUM_PROPS = "aad-backend-oauth2-minimum.properties"; + private Resource testResource; + private ResourcePropertySource testPropResource; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private AnnotationConfigWebApplicationContext testContext; + + @Before + public void setup() throws Exception { + testResource = new ClassPathResource(AAD_OAUTH2_MINIMUM_PROPS); + testPropResource = new ResourcePropertySource("test", testResource); + } + + @After + public void clear() { + if (testContext != null) { + testContext.close(); + } + } + + @Test + public void noOAuth2UserServiceBeanCreatedIfPropsNotConfigured() { + final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(AADOAuth2AutoConfiguration.class); + context.refresh(); + + exception.expect(NoSuchBeanDefinitionException.class); + context.getBean(OAuth2UserService.class); + } + + @Test + public void testOAuth2UserServiceBeanCreatedIfPropsConfigured() { + testContext = initTestContext(); + Assert.assertNotNull(testContext.getBean(OAuth2UserService.class)); + } + + @Test + public void noOAuth2UserServiceBeanCreatedIfTenantIdNotConfigured() { + testPropResource.getSource().remove(Constants.TENANT_ID_PROPERTY); + testContext = initTestContext(); + + exception.expect(NoSuchBeanDefinitionException.class); + testContext.getBean(OAuth2UserService.class); + } + + @Test + public void testEndpointsPropertiesLoadAndOverridable() { + testContext = initTestContext("azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadSigninUri=https://test/", + "azure.service.endpoints.global.aadGraphApiUri=https://test/", + "azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadMembershipRestUri=https://test/", + Constants.ALLOW_TELEMETRY_PROPERTY + "=false"); + + + final Environment environment = testContext.getEnvironment(); + assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadGraphApiUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadKeyDiscoveryUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadMembershipRestUri")) + .isEqualTo("https://test/"); + final ServiceEndpointsProperties serviceEndpointsProperties = + testContext.getBean(ServiceEndpointsProperties.class); + assertThat(serviceEndpointsProperties).isNotNull(); + assertThat(serviceEndpointsProperties.getEndpoints()).isNotEmpty(); + + final Map endpoints = serviceEndpointsProperties.getEndpoints(); + assertThat(endpoints).hasSize(4); + assertThat(endpoints.get("cn")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://graph.chinacloudapi.cn/", + "https://login.partner.microsoftonline.cn/common/discovery/keys", + "https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6", + "https://login.partner.microsoftonline.cn/"); + assertThat(endpoints.get("global")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://test/", "https://test/", "https://test/", "https://test/"); + + } + + private AnnotationConfigWebApplicationContext initTestContext(String... environment) { + final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + + context.getEnvironment().getPropertySources().addLast(testPropResource); + context.getEnvironment().getPropertySources().addLast(new MockPropertySource() + .withProperty(Constants.ALLOW_TELEMETRY_PROPERTY, "false")); + if (environment.length > 0) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, environment); + } + + context.register(AADOAuth2AutoConfiguration.class); + context.refresh(); + + return context; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java new file mode 100644 index 000000000000..fc4ecee8ecd9 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThatCode; + +public class AADUserGroupsPropertyValidatorTest { + + private AADAuthenticationProperties aadAuthenticationProperties; + + @Before + public void setUp() { + aadAuthenticationProperties = new AADAuthenticationProperties(); + } + + @Test + public void isValidNoGroupsDefined() { + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()) + .isInstanceOf(IllegalStateException.class).hasMessage( + "One of the User Group Properties must be populated. " + + "Please populate azure.activedirectory.user-group.allowed-groups"); + } + + @Test + public void isValidDeprecatedPropertySet() { + aadAuthenticationProperties.setActiveDirectoryGroups(Collections.singletonList("user-group")); + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException(); + } + + @Test + public void isValidUserGroupPropertySet() { + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("user-group")); + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException(); + } + + @Test + public void isValidBothUserGroupPropertiesSet() { + aadAuthenticationProperties.setActiveDirectoryGroups(Collections.singletonList("user-group")); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("user-group")); + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java new file mode 100644 index 000000000000..34b8ecd7b9e1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.GrantedAuthority; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class AzureADGraphClientTest { + + private AzureADGraphClient adGraphClient; + + private AADAuthenticationProperties aadAuthProps; + + @Mock + private ServiceEndpointsProperties endpointsProps; + + @Before + public void setup() { + final List activeDirectoryGroups = new ArrayList<>(); + activeDirectoryGroups.add("Test_Group"); + aadAuthProps = new AADAuthenticationProperties(); + aadAuthProps.setActiveDirectoryGroups(activeDirectoryGroups); + adGraphClient = new AzureADGraphClient("client", "pass", aadAuthProps, endpointsProps); + } + + @Test + public void testConvertGroupToGrantedAuthorities() { + + final List userGroups = Collections.singletonList( + new UserGroup("testId", "Test_Group")); + + final Set authorities = adGraphClient.convertGroupsToGrantedAuthorities(userGroups); + assertThat(authorities).hasSize(1).extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_Test_Group"); + } + + @Test + public void testConvertGroupToGrantedAuthoritiesUsingAllowedGroups() { + final List userGroups = Arrays + .asList(new UserGroup("testId", "Test_Group"), + new UserGroup("testId", "Another_Group")); + aadAuthProps.getUserGroup().getAllowedGroups().add("Another_Group"); + final Set authorities = adGraphClient.convertGroupsToGrantedAuthorities(userGroups); + assertThat(authorities).hasSize(2).extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_Test_Group", "ROLE_Another_Group"); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java new file mode 100644 index 000000000000..84dc5e4c0452 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.util.Arrays; +import java.util.List; + +public class Constants { + public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment"; + public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id"; + public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret"; + public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.active-directory-groups"; + public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id"; + public static final String ALLOW_TELEMETRY_PROPERTY = "azure.activedirectory.allow-telemetry"; + + + public static final String DEFAULT_ENVIRONMENT = "global"; + public static final String CLIENT_ID = "real_client_id"; + public static final String CLIENT_SECRET = "real_client_secret"; + public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3"); + + public static final String TOKEN_HEADER = "Authorization"; + public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token"; + + /** Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens */ + public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1" + + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu" + + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD" + + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p" + + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm" + + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs" + + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K" + + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn" + + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh" + + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO" + + "xzxaQybzYpQ"; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java new file mode 100644 index 000000000000..58d0ba2cc390 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.util.Arrays; +import java.util.List; + +public class MicrosoftGraphConstants { + + public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment"; + public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id"; + public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret"; + public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.active-directory-groups"; + public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id"; + + public static final String DEFAULT_ENVIRONMENT = "global"; + public static final String CLIENT_ID = "real_client_id"; + public static final String CLIENT_SECRET = "real_client_secret"; + public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3"); + + public static final String TOKEN_HEADER = "Authorization"; + public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token"; + + /** Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens */ + public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1" + + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu" + + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD" + + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p" + + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm" + + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs" + + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K" + + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn" + + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh" + + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO" + + "xzxaQybzYpQ"; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java new file mode 100644 index 000000000000..a6959a20d933 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ResourceRetrieverTest { + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) + .withPropertyValues("azure.activedirectory.client-id=fake-client-id", + "azure.activedirectory.client-secret=fake-client-secret", + "azure.activedirectory.active-directory-groups=fake-group", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri"); + + @Test + public void resourceRetrieverDefaultConfig() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(ResourceRetriever.class); + final ResourceRetriever retriever = context.getBean(ResourceRetriever.class); + assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class); + + final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever; + assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT); + assertThat(defaultRetriever.getReadTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT); + assertThat(defaultRetriever.getSizeLimit()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT); + }); + } + + @Test + public void resourceRetriverIsConfigurable() { + this.contextRunner.withPropertyValues("azure.activedirectory.jwt-connect-timeout=1234", + "azure.activedirectory.jwt-read-timeout=1234", + "azure.activedirectory.jwt-size-limit=123400", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri") + .run(context -> { + assertThat(context).hasSingleBean(ResourceRetriever.class); + final ResourceRetriever retriever = context.getBean(ResourceRetriever.class); + assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class); + + final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever; + assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(1234); + assertThat(defaultRetriever.getReadTimeout()).isEqualTo(1234); + assertThat(defaultRetriever.getSizeLimit()).isEqualTo(123400); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java new file mode 100644 index 000000000000..08906553218c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Assert; +import org.junit.Test; + +public class UserGroupTest { + private static final UserGroup GROUP_1 = new UserGroup("12345", "test"); + + @Test + public void getDisplayName() { + Assert.assertEquals("test", GROUP_1.getDisplayName()); + } + + @Test + public void getObjectID() { + Assert.assertEquals("12345", GROUP_1.getObjectID()); + } + + @Test + public void equals() { + final UserGroup group2 = new UserGroup("12345", "test"); + Assert.assertEquals(GROUP_1, group2); + } + + @Test + public void hashCodeTest() { + final UserGroup group2 = new UserGroup("12345", "test"); + Assert.assertEquals(GROUP_1.hashCode(), group2.hashCode()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java new file mode 100644 index 000000000000..5826ad4fb4d0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + + +public class UserPrincipalAzureADGraphTest { + @Rule + public WireMockRule wireMockRule = new WireMockRule(9519); + + private AzureADGraphClient graphClientMock; + private String clientId; + private String clientSecret; + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties endpointsProps; + private String accessToken; + private static String userGroupsJson; + + static { + try { + final ObjectMapper objectMapper = new ObjectMapper(); + final Map json = objectMapper.readValue(UserPrincipalAzureADGraphTest.class.getClassLoader() + .getResourceAsStream("aad/azure-ad-graph-user-groups.json"), + new TypeReference>() { }); + userGroupsJson = objectMapper.writeValueAsString(json); + } catch (IOException e) { + e.printStackTrace(); + userGroupsJson = null; + } + Assert.assertNotNull(userGroupsJson); + } + + @Before + public void setup() { + accessToken = Constants.BEARER_TOKEN; + aadAuthProps = new AADAuthenticationProperties(); + endpointsProps = new ServiceEndpointsProperties(); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf"); + endpointsProps.getEndpoints().put("global", serviceEndpoints); + clientId = "client"; + clientSecret = "pass"; + } + + @Test + public void getAuthoritiesByUserGroups() throws Exception { + aadAuthProps.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + assertThat(graphClientMock.getGrantedAuthorities(Constants.BEARER_TOKEN)).isNotEmpty() + .extracting(GrantedAuthority::getAuthority).containsExactly("ROLE_group1"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("%s", accessToken))) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .withHeader("api-version", equalTo("1.6"))); + } + + @Test + public void getGroups() throws Exception { + aadAuthProps.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + final Collection authorities = graphClientMock + .getGrantedAuthorities(Constants.BEARER_TOKEN); + + assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("%s", accessToken))) + .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .withHeader("api-version", equalTo("1.6"))); + } + + @Test + public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException { + final File tmpOutputFile = File.createTempFile("test-user-principal", "txt"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + FileInputStream fileInputStream = new FileInputStream(tmpOutputFile); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + + final JWSObject jwsObject = JWSObject.parse(Constants.JWT_TOKEN); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build(); + final UserPrincipal principal = new UserPrincipal(jwsObject, jwtClaimsSet); + + objectOutputStream.writeObject(principal); + + final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject(); + + Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); + Assert.assertFalse("Serialized UserPrincipal kid not empty", + StringUtils.isEmpty(serializedPrincipal.getKid())); + Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); + Assert.assertTrue("Serialized UserPrincipal claims not empty.", + serializedPrincipal.getClaims().size() > 0); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java new file mode 100644 index 000000000000..8a3b23caac0f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Resource; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +public class UserPrincipalManagerAudienceTest { + + private static final String FAKE_CLIENT_ID = "dsflkjsdflkjsdf"; + private static final String FAKE_APPLICATION_URI = "https://oihiugjuzfvbhg"; + + private JWSSigner signer; + private String jwkString; + private ResourceRetriever resourceRetriever; + + private ServiceEndpointsProperties serviceEndpointsProperties; + private AADAuthenticationProperties aadAuthenticationProperties; + private UserPrincipalManager userPrincipalManager; + + @Before + public void setupKeys() throws NoSuchAlgorithmException, IOException { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + + final KeyPair kp = kpg.genKeyPair(); + final RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate(); + + signer = new RSASSASigner(privateKey); + + final RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey) kp.getPublic()) + .privateKey((RSAPrivateKey) kp.getPrivate()) + .keyID("1") + .build(); + final JWKSet jwkSet = new JWKSet(rsaJWK); + jwkString = jwkSet.toString(); + + resourceRetriever = url -> new Resource(jwkString, "application/json"); + + serviceEndpointsProperties = mock(ServiceEndpointsProperties.class); + aadAuthenticationProperties = new AADAuthenticationProperties(); + aadAuthenticationProperties.setClientId(FAKE_CLIENT_ID); + aadAuthenticationProperties.setAppIdUri(FAKE_APPLICATION_URI); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadKeyDiscoveryUri("file://dummy"); + when(serviceEndpointsProperties.getServiceEndpoints(anyString())).thenReturn(serviceEndpoints); + } + + @Test + public void allowApplicationUriAsAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_CLIENT_ID) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .doesNotThrowAnyException(); + } + + @Test + public void allowClientIdAsAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_APPLICATION_URI) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .doesNotThrowAnyException(); + } + + @Test + public void failWithUnkownAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience("unknown audience") + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .hasMessageContaining("Invalid token audience."); + } + + @Test + public void failOnInvalidSiganture() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_APPLICATION_URI) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + final String invalidToken = orderTwo.substring(0, orderTwo.length() - 5); + + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(invalidToken)) + .hasMessageContaining("JWT rejected: Invalid signature"); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java new file mode 100644 index 000000000000..ebff6c0c5080 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.BadJWTException; +import junitparams.FileParameters; +import junitparams.JUnitParamsRunner; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@RunWith(JUnitParamsRunner.class) +public class UserPrincipalManagerTest { + + private static ImmutableJWKSet immutableJWKSet; + + @BeforeClass + public static void setupClass() throws Exception { + final X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(Files.newInputStream(Paths.get("src/test/resources/test-public-key.txt"))); + immutableJWKSet = new ImmutableJWKSet<>(new JWKSet(JWK.parse( + cert))); + } + + private UserPrincipalManager userPrincipalManager; + + + @Test + public void testAlgIsTakenFromJWT() throws Exception { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + final UserPrincipal userPrincipal = userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-signed.txt")), StandardCharsets.UTF_8)); + assertThat(userPrincipal).isNotNull().extracting(UserPrincipal::getIssuer, UserPrincipal::getSubject) + .containsExactly("https://sts.windows.net/test", "test@example.com"); + } + + @Test + public void invalidIssuer() { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-bad-issuer.txt")), StandardCharsets.UTF_8))) + .isInstanceOf(BadJWTException.class); + } + + @Test + //TODO: add more generated tokens with other valid issuers to this file. Didn't manage to generate them + @FileParameters("src/test/resources/jwt-valid-issuer.txt") + public void validIssuer(final String token) { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(token)) + .doesNotThrowAnyException(); + } + + @Test + public void nullIssuer() { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-null-issuer.txt")), StandardCharsets.UTF_8))) + .isInstanceOf(BadJWTException.class); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java new file mode 100644 index 000000000000..88fd36f442d6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +public class UserPrincipalMicrosoftGraphTest { + @Rule + public WireMockRule wireMockRule = new WireMockRule(9519); + + private AzureADGraphClient graphClientMock; + private String clientId; + private String clientSecret; + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties endpointsProps; + private String accessToken; + private static String userGroupsJson; + + static { + try { + final ObjectMapper objectMapper = new ObjectMapper(); + final Map json = objectMapper.readValue(UserPrincipalMicrosoftGraphTest.class + .getClassLoader().getResourceAsStream("aad/microsoft-graph-user-groups.json"), + new TypeReference>() { }); + userGroupsJson = objectMapper.writeValueAsString(json); + } catch (IOException e) { + e.printStackTrace(); + userGroupsJson = null; + } + Assert.assertNotNull(userGroupsJson); + } + + @Before + public void setup() { + accessToken = MicrosoftGraphConstants.BEARER_TOKEN; + aadAuthProps = new AADAuthenticationProperties(); + aadAuthProps.setEnvironment("global-v2-graph"); + aadAuthProps.getUserGroup().setKey("@odata.type"); + aadAuthProps.getUserGroup().setValue("#microsoft.graph.group"); + aadAuthProps.getUserGroup().setObjectIDKey("id"); + endpointsProps = new ServiceEndpointsProperties(); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf"); + endpointsProps.getEndpoints().put("global-v2-graph", serviceEndpoints); + clientId = "client"; + clientSecret = "pass"; + } + + @Test + public void getAuthoritiesByUserGroups() throws Exception { + aadAuthProps.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + assertThat(graphClientMock.getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN)) + .isNotEmpty() + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); + } + + @Test + public void getGroups() throws Exception { + aadAuthProps.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .withBody(userGroupsJson))); + + final Collection authorities = graphClientMock + .getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN); + + assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))); + } + + @Test + public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException { + final File tmpOutputFile = File.createTempFile("test-user-principal", "txt"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + FileInputStream fileInputStream = new FileInputStream(tmpOutputFile); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + + final JWSObject jwsObject = JWSObject.parse(MicrosoftGraphConstants.JWT_TOKEN); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build(); + final UserPrincipal principal = new UserPrincipal(jwsObject, jwtClaimsSet); + + objectOutputStream.writeObject(principal); + + final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject(); + + Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); + Assert.assertFalse("Serialized UserPrincipal kid not empty", + StringUtils.isEmpty(serializedPrincipal.getKid())); + Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); + Assert.assertTrue("Serialized UserPrincipal claims not empty.", + serializedPrincipal.getClaims().size() > 0); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java new file mode 100644 index 000000000000..2bfae28b6710 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.utils; + +public final class TestUtils { + + private TestUtils() { + } + + /** + * + * @param propName property name + * @param propValue value of property + * @return property name and value pair. e.g., prop.name=prop.value + */ + public static String propPair(String propName, String propValue) { + return propName + "=" + propValue; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties b/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties new file mode 100644 index 000000000000..0931172601a1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.client.registration.azure.client-id=abcd +spring.security.oauth2.client.registration.azure.client-secret=password + +azure.activedirectory.tenant-id=my-tanant-id +azure.activedirectory.active-directory-groups=my-aad-group1, my-aad-group2 diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad/azure-ad-graph-user-groups.json b/sdk/spring/azure-spring-boot/src/test/resources/aad/azure-ad-graph-user-groups.json new file mode 100644 index 000000000000..baf41c7408b2 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aad/azure-ad-graph-user-groups.json @@ -0,0 +1,65 @@ +{ + "odata.metadata": "https://graph.windows.net/myorganization/$metadata#directoryObjects", + "value": [ + { + "odata.type": "Microsoft.DirectoryServices.Group", + "objectType": "Group", + "objectId": "12345678-7baf-48ce-96f4-a2d60c26391e", + "deletionTimestamp": null, + "description": "this is group1", + "dirSyncEnabled": true, + "displayName": "group1", + "lastDirSyncTime": "2017-08-02T12:54:37Z", + "mail": null, + "mailNickname": "something", + "mailEnabled": false, + "onPremisesDomainName": null, + "onPremisesNetBiosName": null, + "onPremisesSamAccountName": null, + "onPremisesSecurityIdentifier": "S-1-5-21-1234567885-903363285-719344707-285039", + "provisioningErrors": [], + "proxyAddresses": [], + "securityEnabled": true + }, + { + "odata.type": "Microsoft.DirectoryServices.Group", + "objectType": "Group", + "objectId": "12345678-e757-4474-b9c4-3f00a9ac17a0", + "deletionTimestamp": null, + "description": null, + "dirSyncEnabled": true, + "displayName": "group2", + "lastDirSyncTime": "2017-08-09T13:45:03Z", + "mail": null, + "mailNickname": "somethingelse", + "mailEnabled": false, + "onPremisesDomainName": null, + "onPremisesNetBiosName": null, + "onPremisesSamAccountName": null, + "onPremisesSecurityIdentifier": "S-1-5-21-1234567885-903363285-719344707-28565", + "provisioningErrors": [], + "proxyAddresses": [], + "securityEnabled": true + }, + { + "odata.type": "Microsoft.DirectoryServices.Group", + "objectType": "Group", + "objectId": "12345678-86a4-4237-aeb0-60bad29c1de0", + "deletionTimestamp": null, + "description": "this is group3", + "dirSyncEnabled": true, + "displayName": "group3", + "lastDirSyncTime": "2017-08-09T05:41:43Z", + "mail": null, + "mailNickname": "somethingelse", + "mailEnabled": false, + "onPremisesDomainName": null, + "onPremisesNetBiosName": null, + "onPremisesSamAccountName": null, + "onPremisesSecurityIdentifier": "S-1-5-21-1234567884-1604012920-1887927527-14401381", + "provisioningErrors": [], + "proxyAddresses": [], + "securityEnabled": true + }], + "odata.nextLink": "directoryObjects/$/Microsoft.DirectoryServices.User/12345678-2898-434a-a370-8ec974c2fb57/memberOf?$skiptoken=X'445370740700010000000000000000100000009D29CBA7B45D854A84FF7F9B636BD9DC000000000000000000000017312E322E3834302E3131333535362E312E342E3233333100000000'" +} diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad/microsoft-graph-user-groups.json b/sdk/spring/azure-spring-boot/src/test/resources/aad/microsoft-graph-user-groups.json new file mode 100644 index 000000000000..93497a317b70 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aad/microsoft-graph-user-groups.json @@ -0,0 +1,80 @@ +{ + "odata.metadata": "https://graph.windows.net/myorganization/$metadata#directoryObjects", + "value": [ + { + "@odata.type": "#microsoft.graph.group", + "id": "12345678-7baf-48ce-96f4-a2d60c26391e", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-08-02T12:54:37Z", + "creationOptions": [], + "description": "this is group1", + "displayName": "group1", + "groupTypes": [], + "mail": null, + "mailEnabled": false, + "mailNickname": "something", + "onPremisesLastSyncDateTime": null, + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [], + "renewedDateTime": "2017-08-02T12:54:37Z", + "resourceBehaviorOptions": [], + "resourceProvisioningOptions": [], + "securityEnabled": true, + "visibility": null, + "onPremisesProvisioningErrors": [] + }, + { + "@odata.type": "#microsoft.graph.group", + "id": "12345678-e757-4474-b9c4-3f00a9ac17a0", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-08-09T13:45:03Z", + "creationOptions": [], + "description": "this is group2", + "displayName": "group2", + "groupTypes": [], + "mail": null, + "mailEnabled": false, + "mailNickname": "somethingelse", + "onPremisesLastSyncDateTime": null, + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [], + "renewedDateTime": "2017-08-09T13:45:03Z", + "resourceBehaviorOptions": [], + "resourceProvisioningOptions": [], + "securityEnabled": true, + "visibility": null, + "onPremisesProvisioningErrors": [] + }, + { + "@odata.type": "#microsoft.graph.group", + "id": "12345678-86a4-4237-aeb0-60bad29c1de0", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-08-09T05:41:43Z", + "creationOptions": [], + "description": "this is group3", + "displayName": "group3", + "groupTypes": [], + "mail": null, + "mailEnabled": false, + "mailNickname": "somethingelse", + "onPremisesLastSyncDateTime": null, + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [], + "renewedDateTime": "2017-08-09T05:41:43Z", + "resourceBehaviorOptions": [], + "resourceProvisioningOptions": [], + "securityEnabled": true, + "visibility": null, + "onPremisesProvisioningErrors": [] + }], + "odata.nextLink": "directoryObjects/$/Microsoft.DirectoryServices.User/12345678-2898-434a-a370-8ec974c2fb57/memberOf?$skiptoken=X'445370740700010000000000000000100000009D29CBA7B45D854A84FF7F9B636BD9DC000000000000000000000017312E322E3834302E3131333535362E312E342E3233333100000000'" +} diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx b/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx new file mode 100644 index 000000000000..ea44792776c8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx @@ -0,0 +1 @@ +for test file extension only \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt new file mode 100644 index 000000000000..28afc8898855 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3dyb25naXNzdWVyLm5ldHQvdGVzdCIsInN1YiI6InRlc3RAZXhhbXBsZS5jb20iLCJuYmYiOjE1NDUwMDg5MDYsImV4cCI6OTk5OTk5OTk5OTksImlhdCI6MTU0NTAwODkwNiwianRpIjoidGVzdGlkIiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.lK-pSsNMHHui8IxSCYs7e4gH9I29ug7CyNnT8GIJBWEZCeLs3IRnlkLG923Zn7eT_4A0aWFyEnjCBYIKtqX7AoaLBwCa08yB9x7c0DbJQvdKwFjGzc5zkpNxnzBZxXLJr7D9nMAjeQrYLkgoy4XXHL_m_Z6PTf9Jwl6tTYqUS06gd5ZokV1DtBTTPeDJj7KKzNhY3PQ1Hh_-RLoCspqIiZFZ8dfPgDCc2OXVCsH8_2tUFCktuPuVYD11Ws7_hFG6sq8AF1jyugrtYnwMhbzpMCtkL-SoZsmBtmAUFW20vTNYV6Vri-VEqz5VkHef9ZqZmlNPR0vH8hcZVj0IX8t7yA \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt new file mode 100644 index 000000000000..ac1382b4f5eb --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmJmIjoxNTQ1MDA5MjQ5LCJleHAiOjk5OTk5OTk5OTk5LCJpYXQiOjE1NDUwMDkyNDksImp0aSI6InRlc3RpZCIsInR5cCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcmVnaXN0ZXIifQ.2rHU8-_UWjE0U2Vq9KDmtt1ztlj_G9OW777O-kZ_di7dOBZPt6H2eMba34Qf5wILfs1bHBubMNIs64B9mLffJzXp_FKyMdcCsYecJAOaSscrSLjHYdnZqhRIETOloz-nbxiH_AhaJP6Hb482Hu7It4XhcxWU_tZ9kRD1brfoyb_-8Qh4vmrR4eddtfLZDlr3xFfTSD9FKDeECDWu59wGLBVS_32Y42XYV82f5PD1FsAG62vC-t2XdVS-y6aQIT1QElsKcc66xY21XgXq4fkFGxyoYPB1hCLIPz_QMJxRXql7AnVoxkueQxMzH4NCT64i1Aj7texhHbZh4-_jG29-zg \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt new file mode 100644 index 000000000000..0ca89f3afca7 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt new file mode 100644 index 000000000000..0ca89f3afca7 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml b/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..47e7acf72ba1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000000..1f0955d450f0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx b/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx new file mode 100644 index 000000000000..fd42b8ccd78e Binary files /dev/null and b/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx differ diff --git a/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt b/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt new file mode 100644 index 000000000000..458bcc5fe8cc --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIBATANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJVUzEL +MAkGA1UECgwCWjQwHhcNMTMwODI4MTgyODM0WhcNMjMwODI4MTgyODM0WjAaMQsw +CQYDVQQGEwJVUzELMAkGA1UECgwCWjQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDfdOqotHd55SYO0dLz2oXengw/tZ+q3ZmOPeVmMuOMIYO/Cv1wk2U0 +OK4pug4OBSJPhl09Zs6IwB8NwPOU7EDTgMOcQUYB/6QNCI1J7Zm2oLtuchzz4pIb ++o4ZAhVprLhRyvqi8OTKQ7kfGfs5Tuwmn1M/0fQkfzMxADpjOKNgf0uy6lN6utjd +TrPKKFUQNdc6/Ty8EeTnQEwUlsT2LAXCfEKxTn5RlRljDztS7Sfgs8VL0FPy1Qi8 +B+dFcgRYKFrcpsVaZ1lBmXKsXDRu5QR/Rg3f9DRq4GR1sNH8RLY9uApMl2SNz+sR +4zRPG85R/se5Q06Gu0BUQ3UPm67ETVZLAgMBAAGjUDBOMB0GA1UdDgQWBBQHZPTE +yQVu/0I/3QWhlTyW7WoTzTAfBgNVHSMEGDAWgBQHZPTEyQVu/0I/3QWhlTyW7WoT +zTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDHxqJ9y8alTH7agVMW +Zfic/RbrdvHwyq+IOrgDToqyo0w+IZ6BCn9vjv5iuhqu4ForOWDAFpQKZW0DLBJE +Qy/7/0+9pk2DPhK1XzdOovlSrkRt+GcEpGnUXnzACXDBbO0+Wrk+hcjEkQRRK1bW +2rknARIEJG9GS+pShP9Bq/0BmNsMepdNcBa0z3a5B0fzFyCQoUlX6RTqxRw1h1Qt +5F00pfsp7SjXVIvYcewHaNASbto1n5hrSz1VY9hLba11ivL1N4WoWbmzAL6BWabs +C2D/MenST2/X6hTKyGXpg3Eg2h3iLvUtwcNny0hRKstc73Jl9xR3qXfXKJH0ThTl +q0gq +-----END CERTIFICATE----- diff --git a/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx b/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx new file mode 100644 index 000000000000..7754333d91e0 Binary files /dev/null and b/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx differ diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 9c31a0e88d2c..450120cde9ef 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -10,11 +10,12 @@ resources: type: github name: Azure/azure-sdk-tools endpoint: azure - + trigger: branches: include: - master + - feature/* - hotfix/* - release/* paths: @@ -31,11 +32,23 @@ pr: paths: include: - sdk/spring/ - + stages: - template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml parameters: ServiceDirectory: spring Artifacts: - - name: azure-spring-something - safeName: azurespringsomething + - name: azure-spring-boot + groupId: com.microsoft.azure + safeName: azurespringboot + - name: azure-spring-boot-starter + groupId: com.microsoft.azure + safeName: azurespringbootstarter + - name: azure-active-directory-spring-boot-starter + groupId: com.microsoft.azure + safeName: azurespringbootstarteractivedirectory + + + + + diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml new file mode 100644 index 000000000000..84980d7d61a7 --- /dev/null +++ b/sdk/spring/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + com.azure + azure-spring-boot-service + pom + 1.0.0 + + azure-spring-boot + azure-spring-boot-starter + azure-spring-boot-starter-active-directory + + +