* Add demo project for AAD stateless app-role filter. * Add introduction to the README.md
This commit is contained in:
Родитель
ca45e4e68f
Коммит
483116987b
|
@ -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")
|
||||
|
||||
|
Двоичные данные
azure-spring-boot-samples/azure-active-directory-spring-boot-stateless-sample/docs/demo.webp
Normal file
Двоичные данные
azure-spring-boot-samples/azure-active-directory-spring-boot-stateless-sample/docs/demo.webp
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче