Add demo project for AAD stateless app-role filter (PR #512) (#689)

* Add demo project for AAD stateless app-role filter.
* Add introduction to the README.md
This commit is contained in:
Wladislaw Mitzel 2019-07-09 05:10:36 +02:00 коммит произвёл Pan Li
Родитель ca45e4e68f
Коммит 483116987b
9 изменённых файлов: 410 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,107 @@
## About this sample
### Overview
This demo project explains the usage of the stateless authentication filter `AADAppRoleStatelessAuthenticationFilter`.
This project is composed of a vue.js frontend and a simple backend with three endpoints
* `/public` (accessible by anyone)
* `/authorized` (role "user" required)
* `/admin/demo` (role "admin" required).
### Get started
The sample is composed of two layers: vue.js client and Spring Boot RESTful Web Service. You need to make some changes
to get it working with your Azure AD tenant on both sides.
### How to configure
#### Register your application with your Azure Active Directory Tenant
Follow the guide [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-openid-connect-code#register-your-application-with-your-ad-tenant).
#### Configure appRoles
In order to use only the `id_token` for our authentication and authorization purposes we will use the
`appRoles` feature which AAD provides. Follow the guide
[Add app roles in your application](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps)
For the test SPA provided with this example you should create the following roles in your manifest:
```
"appRoles": [
{
"allowedMemberTypes": [
"User"
],
"displayName": "Admin",
"id": "2fa848d0-8054-4e11-8c73-7af5f1171001",
"isEnabled": true,
"description": "Full admin access",
"value": "Admin"
},
{
"allowedMemberTypes": [
"User"
],
"displayName": "User",
"id": "f8ed78b5-fabc-488e-968b-baa48a570001",
"isEnabled": true,
"description": "Normal user access",
"value": "User"
}
],
```
After you've created the roles go to your Enterprise Application in Azure Portal, select "Users and groups" and
assign the new roles to your Users (assignment of roles to groups is not available in the free tier of AAD).
Furthermore enable the implicit flow in the manifest for the demo application
(or if you have SPAs calling you):
```
"oauth2AllowImplicitFlow": "true",
```
#### Configure application.properties
You have to activate the stateless app-role auth filter and configure the `client-id`of your application registration:
```properties
azure.activedirectory.session-stateless=true
azure.aad.app-role.client-id=xxxxxx-your-client-id-xxxxxx
```
#### Configure Webapp
Add your `tenant-id` and `client-id` in `src/main/resources/static/index.html`:
```
data: {
clientId: 'xxxxxxxx-your-client-id-xxxxxxxxxxxx',
tenantId: 'xxxxxxxx-your-tenant-id-xxxxxxxxxxxx',
tokenType: 'id_token',
token: null,
log: null
},
```
### How to run
- Use Maven
```
mvn clean package spring-boot:run
```
### Check the authentication and authorization
1. Access http://localhost:8080
2. Without logging in try the three endpoints (public, authorized and admin). While the public
endpoint should work without a token the other two will return a 403.
3. Insert your `client-id` and `tenant-id` and perform a log in. If successfull the token textarea
should get populated. Also the token header and token payload field will be populated.
4. Again access the three endpoints. Depending on your user and the assigned `appRoles` you should
be able to call the authorized and admin endpoints.
#### Demo
![demoonstration video](docs/demo.webp "Demo Video")

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 628 KiB

Просмотреть файл

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-spring-boot-samples</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>azure-active-directory-spring-boot-stateless-sample</artifactId>
<name>Azure AD Stateless Spring Security Integration Spring Boot Sample</name>
<description>Sample project using the AAD stateless app-role filter for AAD integration in Spring Security
</description>
<url>https://github.com/Microsoft/azure-spring-boot</url>
<properties>
<project.rootdir>${project.basedir}/../..</project.rootdir>
</properties>
<dependencies>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-active-directory-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
</dependencies>
</project>

Просмотреть файл

@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for
* license information.
*/
package sample.aad;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AzureADStatelessBackendSampleApplication {
public static void main(String[] args) {
SpringApplication.run(AzureADStatelessBackendSampleApplication.class, args);
}
}

Просмотреть файл

@ -0,0 +1,36 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for
* license information.
*/
package sample.aad.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MainController {
@GetMapping("/public")
@ResponseBody
public String publicMethod() {
return "public endpoint response";
}
@GetMapping("/authorized")
@ResponseBody
@PreAuthorize("hasRole('ROLE_User')")
public String onlyAuthorizedUsers() {
return "authorized endpoint response";
}
@GetMapping("/admin/demo")
@ResponseBody
// For demo purposes for this endpoint we configure the required role in the AADWebSecurityConfig class.
// However, it is advisable to use method level security with @PreAuthorize("hasRole('xxx')")
public String onlyForAdmins() {
return "admin endpoint";
}
}

Просмотреть файл

@ -0,0 +1,38 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for
* license information.
*/
package sample.aad.security;
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;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AADWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AADAppRoleStatelessAuthenticationFilter aadAuthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("Admin")
.antMatchers("/", "/index.html", "/public").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(aadAuthFilter, UsernamePasswordAuthenticationFilter.class);
}
}

Просмотреть файл

@ -0,0 +1,6 @@
# By default, azure.activedirectory.environment property has value `global`,
# supported value is global, cn. Please refer to the README for details.
# azure.activedirectory.environment=global
azure.activedirectory.session-stateless=true
azure.activedirectory.client-id=xxxxxxxx-your-client-id-xxxxxxxxxxxx

Просмотреть файл

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>Stateless Backend Demo</title>
</head>
<body>
<div id="app" class="p-1 m-1">
<nav class="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">Stateless Backend Demo</a>
</nav>
<div id="auth" class="container">
<div class="row p-1">
<label for="clientId" class="col">Client-ID:</label>
<input id="clientId" v-model="clientId" placeholder="clientId" class="col-4">
<label for="tenantId" class="col">Tenant-ID:</label>
<input id="tenantId" v-model="tenantId" placeholder="tenantId" class="col-3">
<select v-model="tokenType" class="col-2">
<option disabled value="">Select token type</option>
<option>id_token</option>
</select>
<a :href="signInUrl" class="btn btn-primary" v-if="!token" class="col-2">Login</a>
<a @click="logout()" href="#" class="btn btn-danger" v-if="token" class="col-2">Logout</a>
</div>
<div class="row p-1">
<label for="token" class="col">Token:</label>
<textarea v-model="token" class="col-11"></textarea>
</div>
<div class="row p-1">
<label for="tokenheader" class="col">Token header:</label>
<textarea v-model="tokenheader" class="col-5" readonly="true" rows="5"></textarea>
<label for="tokenpayload" class="col">Token payload:</label>
<textarea v-model="tokenpayload" class="col-5" readonly="true" rows="5"></textarea>
</div>
<div class="row p-1">
<button type="button" class="btn btn-primary" @click="publicEndpoint()">Public Endpoint</button>
<button type="button" class="btn btn-secondary" @click="authorized()">Authorized</button>
<button type="button" class="btn btn-danger" @click="admin()">Admin Endpoint</button>
</div>
<div class="row p-1">
<textarea class="col-12" v-model="log" rows="7"></textarea>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js" integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js" integrity="sha384-U/+EF1mNzvy5eahP9DeB32duTkAmXrePwnRWtuSh1C/bHHhyR1KZCr/aGZBkctpY" crossorigin="anonymous"></script>
<script src="https://unpkg.com/vue-axios@2.1.4/dist/vue-axios.min.js" integrity="sha384-YoadHAhGpFWrsa9D2SlBUOEYtnmKUkFLN8bYEirDkn7Fg0VwuWyn/8JlkfeupmdP" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script>
var app = new Vue({
el: '#app',
data: {
clientId: 'xxxxxxxx-your-client-id-xxxxxxxxxxxx',
tenantId: 'xxxxxxxx-your-tenant-id-xxxxxxxxxxxx',
tokenType: 'id_token',
token: null,
log: null
},
computed: {
signInUrl: function() {
return "https://login.microsoftonline.com/" + this.tenantId +
"/oauth2/authorize?" +
"client_id=" + this.clientId +
"&response_type=" + this.tokenType +
"&redirect_uri=" + this.redirect +
"&response_mode=fragment" +
"&scope=openid" +
"&state=12345" +
"&nonce=dummy123";
},
tokenheader: function () {
return this.getTokenPart(0)
},
tokenpayload: function () {
return this.getTokenPart(1)
},
redirect: function () {
return location.protocol + '//' + location.host + location.pathname
}
},
beforeMount: function () {
if(window.location.hash) {
var params = window.location.hash.substr(1).split('&').reduce(function (result, item) {
var parts = item.split('=');
result[parts[0]] = parts[1];
return result;
}, {});
this.token = params['id_token'];
if (!this.token) {
this.token = params['access_token'];
}
}
var comp = this
axios.interceptors.request.use(
function (config) {
if (comp.token) {
config.headers['Authorization'] = `Bearer ${ comp.token }`;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
},
methods: {
getTokenPart: function (i) {
var result = null
if (this.token) {
var splitted = this.token.split(".")
if (splitted.length === 3) {
return atob(splitted[i])
}
}
return result
},
publicEndpoint: function () {
this.callApi("/public")
},
authorized: function () {
this.callApi("/authorized")
},
admin: function () {
this.callApi("/admin/demo")
},
logout: function () {
this.token = ""
},
callApi: function (p) {
axios.get(p).then(
(response) => {
this.log = JSON.stringify(response)
},
(error) => {
this.log = JSON.stringify(error.response)
})
}
}
})
</script>
</body>
</html>

Просмотреть файл

@ -145,6 +145,7 @@
<module>azure-active-directory-b2c-oidc-spring-boot-sample</module>
<module>azure-active-directory-spring-boot-backend-sample</module>
<module>azure-active-directory-spring-boot-sample</module>
<module>azure-active-directory-spring-boot-stateless-sample</module>
<module>azure-cosmosdb-spring-boot-sample</module>
<module>azure-keyvault-secrets-spring-boot-sample</module>
<module>azure-mediaservices-spring-boot-sample</module>