├── ui-application ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── types.ts │ │ ├── add-new-book │ │ │ ├── add-new-book.component.css │ │ │ ├── add-new-book.component.html │ │ │ ├── add-new-book.component.ts │ │ │ └── add-new-book.component.spec.ts │ │ ├── edit-book │ │ │ ├── edit-book.component.css │ │ │ ├── edit-book.component.html │ │ │ ├── edit-book.component.spec.ts │ │ │ └── edit-book.component.ts │ │ ├── fake-data.ts │ │ ├── app.component.css │ │ ├── auth.service.spec.ts │ │ ├── book.service.spec.ts │ │ ├── http.interceptor.spec.ts │ │ ├── book-list │ │ │ ├── book-list.component.css │ │ │ ├── book-list.component.spec.ts │ │ │ ├── book-list.component.ts │ │ │ └── book-list.component.html │ │ ├── book-form │ │ │ ├── book-form.component.css │ │ │ ├── book-form.component.spec.ts │ │ │ ├── book-form.component.ts │ │ │ └── book-form.component.html │ │ ├── app-routing.module.ts │ │ ├── auth.service.ts │ │ ├── book.service.ts │ │ ├── http.interceptor.ts │ │ ├── app.component.ts │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ └── app.module.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── styles.css │ ├── main.ts │ ├── index.html │ ├── test.ts │ └── polyfills.ts ├── proxy.config.json ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .browserslistrc ├── .gitignore ├── tsconfig.json ├── README.md ├── build.gradle ├── package.json ├── karma.conf.js ├── gradlew.bat ├── angular.json └── gradlew ├── book-service ├── settings.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ ├── main │ │ ├── resources │ │ │ └── application.yml │ │ └── java │ │ │ └── com │ │ │ └── thomasvitale │ │ │ └── bookservice │ │ │ └── BookServiceApplication.java │ └── test │ │ └── java │ │ └── com │ │ └── thomasvitale │ │ └── bookservice │ │ └── BookServiceApplicationTests.java ├── .gitignore ├── build.gradle ├── gradlew.bat └── gradlew ├── edge-service ├── settings.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── src │ ├── main │ │ ├── resources │ │ │ └── application.yml │ │ └── java │ │ │ └── com │ │ │ └── thomasvitale │ │ │ └── edgeservice │ │ │ └── EdgeServiceApplication.java │ └── test │ │ └── java │ │ └── com │ │ └── thomasvitale │ │ └── edgeservice │ │ ├── WelcomeControllerTests.java │ │ └── EdgeServiceApplicationTests.java ├── build.gradle ├── gradlew.bat └── gradlew ├── .idea └── .gitignore ├── docker-compose.yml ├── README.md ├── .gitignore ├── LICENSE └── platform └── keycloak └── realm-export.json /ui-application/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /book-service/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'book-service' 2 | -------------------------------------------------------------------------------- /edge-service/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'edge-service' 2 | -------------------------------------------------------------------------------- /ui-application/src/app/types.ts: -------------------------------------------------------------------------------- 1 | export interface Book { 2 | isbn: string; 3 | title: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui-application/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /ui-application/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasVitale/securing-apps-oauth2-oidc-spring-security-devoxx-ua-2021/HEAD/ui-application/src/favicon.ico -------------------------------------------------------------------------------- /ui-application/src/app/add-new-book/add-new-book.component.css: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: auto; 3 | max-width: 600px; 4 | } 5 | 6 | .mat-h1 { 7 | text-align: center; 8 | } 9 | -------------------------------------------------------------------------------- /book-service/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasVitale/securing-apps-oauth2-oidc-spring-security-devoxx-ua-2021/HEAD/book-service/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /edge-service/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasVitale/securing-apps-oauth2-oidc-spring-security-devoxx-ua-2021/HEAD/edge-service/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ui-application/proxy.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "target": "http://localhost:9000", 4 | "secure": false, 5 | "logLevel": "debug", 6 | "changeOrigin": true 7 | } 8 | } -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ui-application/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /book-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8000 3 | spring: 4 | security: 5 | oauth2: 6 | resourceserver: 7 | jwt: 8 | issuer-uri: http://localhost:8080/auth/realms/PolarBookshop 9 | -------------------------------------------------------------------------------- /ui-application/src/app/edit-book/edit-book.component.css: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: auto; 3 | max-width: 600px; 4 | } 5 | 6 | .mat-h1 { 7 | text-align: center; 8 | } 9 | 10 | .mat-spinner { 11 | margin: auto; 12 | } -------------------------------------------------------------------------------- /book-service/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /edge-service/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ui-application/src/app/add-new-book/add-new-book.component.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /ui-application/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ui-application/src/app/fake-data.ts: -------------------------------------------------------------------------------- 1 | import { Book } from "./types"; 2 | 3 | export const fakeBooks: Book[] = [{ 4 | isbn: '1234567890', 5 | title: 'Harry Potter' 6 | }, { 7 | isbn: '1234567891', 8 | title: 'The Lord of the Rings' 9 | }, { 10 | isbn: '1234567892', 11 | title: 'His Dark Materials' 12 | }]; 13 | -------------------------------------------------------------------------------- /ui-application/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /ui-application/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .toolbar-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | .sidenav-container { 6 | height: 100%; 7 | } 8 | 9 | .sidenav { 10 | width: 200px; 11 | } 12 | 13 | .sidenav .mat-toolbar { 14 | background: inherit; 15 | } 16 | 17 | .mat-toolbar.mat-primary { 18 | position: sticky; 19 | top: 0; 20 | z-index: 1; 21 | } 22 | -------------------------------------------------------------------------------- /ui-application/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ui-application/src/app/edit-book/edit-book.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | 10 |
11 | 12 |
-------------------------------------------------------------------------------- /ui-application/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ui-application/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /ui-application/src/app/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /ui-application/src/app/book.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { BookService } from './book.service'; 4 | 5 | describe('BookService', () => { 6 | let service: BookService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(BookService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /ui-application/src/app/http.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { HttpInterceptorImpl } from './http.interceptor'; 4 | 5 | describe('HttpInterceptor', () => { 6 | beforeEach(() => TestBed.configureTestingModule({ 7 | providers: [ 8 | HttpInterceptorImpl 9 | ] 10 | })); 11 | 12 | it('should be created', () => { 13 | const interceptor: HttpInterceptorImpl = TestBed.inject(HttpInterceptorImpl); 14 | expect(interceptor).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /ui-application/src/app/book-list/book-list.component.css: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: 20px; 3 | } 4 | 5 | .dashboard-card { 6 | position: absolute; 7 | top: 15px; 8 | left: 15px; 9 | right: 15px; 10 | bottom: 15px; 11 | } 12 | 13 | .more-button { 14 | position: absolute; 15 | top: 5px; 16 | right: 10px; 17 | } 18 | 19 | .dashboard-card-content { 20 | text-align: left; 21 | } 22 | 23 | .mat-h1 { 24 | text-align: center; 25 | } 26 | 27 | .card-actions { 28 | text-align: right; 29 | } 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | 4 | redis: 5 | image: redis:6.2 6 | container_name: redis 7 | ports: 8 | - 6379:6379 9 | 10 | keycloak: 11 | image: thomasvitale/keycloak-m1:15.0.1 # Use jboss/keycloak:15.0.1 on Intel processors 12 | container_name: "keycloak" 13 | volumes: 14 | - ./platform/keycloak:/opt/jboss/keycloak/imports 15 | environment: 16 | KEYCLOAK_USER: user 17 | KEYCLOAK_PASSWORD: password 18 | KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm-export.json 19 | ports: 20 | - 8080:8080 21 | -------------------------------------------------------------------------------- /book-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /edge-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /ui-application/src/app/book-form/book-form.component.css: -------------------------------------------------------------------------------- 1 | .full-width { 2 | width: 100%; 3 | } 4 | 5 | .book-card { 6 | min-width: 120px; 7 | margin: 20px auto; 8 | } 9 | 10 | .mat-radio-button { 11 | display: block; 12 | margin: 5px 0; 13 | } 14 | 15 | .row { 16 | display: flex; 17 | flex-direction: row; 18 | } 19 | 20 | .col { 21 | flex: 1; 22 | margin-right: 20px; 23 | } 24 | 25 | .col:last-child { 26 | margin-right: 0; 27 | } 28 | 29 | .card-actions { 30 | text-align: right; 31 | } 32 | 33 | .mat-card-header { 34 | display: block; 35 | margin: auto; 36 | text-align: center; 37 | } 38 | -------------------------------------------------------------------------------- /ui-application/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UiApplication 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Securing applications with OAuth2 and OIDC using Spring Security (Devoxx Ukraine 2021) 2 | 3 | Source code and examples from my presentation at Devoxx Ukraine 2021 4 | 5 | ## Prerequisites 6 | 7 | To run all the examples, you need to install the following tools: 8 | 9 | * [Java 17](https://adoptium.net) 10 | * [Docker](https://www.docker.com) 11 | 12 | ## Usage 13 | 14 | The sample applications rely on Redis and Keycloak. You can run them as containers with the following command: 15 | 16 | ```shell 17 | $ docker-compose up -d 18 | ``` 19 | 20 | Both Spring Boot applications can be run locally with this command: 21 | 22 | ```shell 23 | $ ./gradlew bootRun 24 | ``` 25 | -------------------------------------------------------------------------------- /edge-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9000 3 | 4 | spring: 5 | cloud: 6 | gateway: 7 | routes: 8 | - id: book-route 9 | uri: http://localhost:8000 10 | predicates: 11 | - Path=/books/** 12 | default-filters: 13 | - TokenRelay 14 | - SaveSession 15 | security: 16 | oauth2: 17 | client: 18 | registration: 19 | keycloak: 20 | client-id: edge-service 21 | client-secret: polar-keycloak-secret 22 | scope: openid 23 | provider: 24 | keycloak: 25 | issuer-uri: http://localhost:8080/auth/realms/PolarBookshop 26 | session: 27 | store-type: redis 28 | -------------------------------------------------------------------------------- /ui-application/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /ui-application/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AddNewBookComponent } from './add-new-book/add-new-book.component'; 4 | import { BookListComponent } from './book-list/book-list.component'; 5 | import { EditBookComponent } from './edit-book/edit-book.component'; 6 | 7 | const routes: Routes = [ 8 | { path: 'browse-books', component: BookListComponent, pathMatch: 'full' }, 9 | { path: 'add-book', component: AddNewBookComponent }, 10 | { path: 'edit-book/:isbn', component: EditBookComponent }, 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forRoot(routes, {useHash: true})], 15 | exports: [RouterModule] 16 | }) 17 | export class AppRoutingModule { } 18 | -------------------------------------------------------------------------------- /ui-application/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /ui-application/src/app/add-new-book/add-new-book.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { BookService } from '../book.service'; 4 | import { Book } from '../types'; 5 | 6 | @Component({ 7 | selector: 'app-add-new-book', 8 | templateUrl: './add-new-book.component.html', 9 | styleUrls: ['./add-new-book.component.css'] 10 | }) 11 | export class AddNewBookComponent implements OnInit { 12 | 13 | constructor(private bookService: BookService, private router: Router) { } 14 | 15 | ngOnInit(): void { 16 | } 17 | 18 | onSubmit(book: Book): void { 19 | this.bookService.addBook(book) 20 | .subscribe(() => { 21 | this.router.navigateByUrl('/browse-books'); 22 | }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ui-application/src/app/book-form/book-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BookFormComponent } from './book-form.component'; 4 | 5 | describe('BookFormComponent', () => { 6 | let component: BookFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BookFormComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BookFormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui-application/src/app/book-list/book-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BookListComponent } from './book-list.component'; 4 | 5 | describe('BookListComponent', () => { 6 | let component: BookListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BookListComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BookListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui-application/src/app/edit-book/edit-book.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditBookComponent } from './edit-book.component'; 4 | 5 | describe('EditBookComponent', () => { 6 | let component: EditBookComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ EditBookComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(EditBookComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui-application/src/app/add-new-book/add-new-book.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddNewBookComponent } from './add-new-book.component'; 4 | 5 | describe('AddNewBookComponent', () => { 6 | let component: AddNewBookComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AddNewBookComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddNewBookComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /ui-application/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /ui-application/src/app/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpXsrfTokenExtractor } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Location } from '@angular/common'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthService { 10 | 11 | constructor(private httpClient: HttpClient, private location: Location, private httpXsrfTokenExtractor: HttpXsrfTokenExtractor) { } 12 | 13 | authenticate(): Observable { 14 | return this.httpClient.get('/welcome'); 15 | } 16 | 17 | login(): void { 18 | window.open('/oauth2/authorization/keycloak', '_self'); 19 | } 20 | 21 | logout(): Observable { 22 | const formData: any = new FormData(); 23 | formData.append('_csrf', this.httpXsrfTokenExtractor.getToken()); 24 | return this.httpClient.post('/logout', formData); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui-application/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), { 21 | teardown: { destroyAfterEach: false } 22 | } 23 | ); 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /ui-application/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "es2015", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | HELP.md 26 | .gradle 27 | build/ 28 | !gradle/wrapper/gradle-wrapper.jar 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### STS ### 33 | .apt_generated 34 | .classpath 35 | .factorypath 36 | .project 37 | .settings 38 | .springBeans 39 | .sts4-cache 40 | bin/ 41 | !**/src/main/**/bin/ 42 | !**/src/test/**/bin/ 43 | 44 | ### IntelliJ IDEA ### 45 | .idea 46 | *.iws 47 | *.iml 48 | *.ipr 49 | out/ 50 | !**/src/main/**/out/ 51 | !**/src/test/**/out/ 52 | 53 | ### NetBeans ### 54 | /nbproject/private/ 55 | /nbbuild/ 56 | /dist/ 57 | /nbdist/ 58 | /.nb-gradle/ 59 | 60 | ### VS Code ### 61 | .vscode/ -------------------------------------------------------------------------------- /ui-application/src/app/book-form/book-form.component.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@angular/core'; 2 | import { Output } from '@angular/core'; 3 | import { Input } from '@angular/core'; 4 | import { Component, OnInit } from '@angular/core'; 5 | import { Book } from '../types'; 6 | 7 | @Component({ 8 | selector: 'app-book-form', 9 | templateUrl: './book-form.component.html', 10 | styleUrls: ['./book-form.component.css'] 11 | }) 12 | export class BookFormComponent implements OnInit { 13 | 14 | @Input() buttonText = 'Submit'; 15 | @Input() titleText = 'Book Information'; 16 | @Input() currentBook: Book | undefined; 17 | @Output() onSubmit = new EventEmitter(); 18 | 19 | book: Book = { 20 | isbn: '', 21 | title: '', 22 | }; 23 | 24 | constructor() { } 25 | 26 | ngOnInit(): void { 27 | if (this.currentBook) { 28 | this.book = this.currentBook; 29 | } 30 | } 31 | 32 | onButtonClicked(): void { 33 | this.onSubmit.emit(this.book); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ui-application/src/app/edit-book/edit-book.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { BookService } from '../book.service'; 4 | import { Book } from '../types'; 5 | 6 | @Component({ 7 | selector: 'app-edit-book', 8 | templateUrl: './edit-book.component.html', 9 | styleUrls: ['./edit-book.component.css'] 10 | }) 11 | export class EditBookComponent implements OnInit { 12 | 13 | book!: Book; 14 | 15 | constructor(private route: ActivatedRoute, private bookService: BookService, private router: Router) { } 16 | 17 | ngOnInit(): void { 18 | const isbn = this.route.snapshot.paramMap.get('isbn') as string; 19 | this.bookService.getBookByIsbn(isbn) 20 | .subscribe(book => this.book = book); 21 | } 22 | 23 | onSubmit(book: Book): void { 24 | this.bookService.editBook(book) 25 | .subscribe(() => { 26 | this.router.navigateByUrl('/browse-books'); 27 | }); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ui-application/src/app/book.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Book } from './types'; 5 | 6 | const httpOptions = { 7 | headers: new HttpHeaders({ 8 | 'Content-Type': 'application/json' 9 | }) 10 | }; 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class BookService { 16 | 17 | constructor(private httpClient: HttpClient) { } 18 | 19 | getBooks(): Observable { 20 | return this.httpClient.get('/books'); 21 | } 22 | 23 | getBookByIsbn(isbn: string): Observable { 24 | return this.httpClient.get(`/books/${isbn}`); 25 | } 26 | 27 | addBook(book: Book): Observable { 28 | return this.httpClient.post(`/books`, 29 | book, 30 | httpOptions 31 | ); 32 | } 33 | 34 | editBook(book: Book): Observable { 35 | return this.httpClient.put(`/books/${book.isbn}`, 36 | book, 37 | httpOptions 38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /ui-application/src/app/book-list/book-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { map } from 'rxjs/operators'; 3 | import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; 4 | import { Book } from '../types'; 5 | import { BookService } from '../book.service'; 6 | import { Router } from '@angular/router'; 7 | import { MatDialog } from '@angular/material/dialog'; 8 | 9 | @Component({ 10 | selector: 'app-book-list', 11 | templateUrl: './book-list.component.html', 12 | styleUrls: ['./book-list.component.css'] 13 | }) 14 | export class BookListComponent implements OnInit { 15 | 16 | books: Book[] = []; 17 | 18 | colNumber = this.breakpointObserver.observe(Breakpoints.Handset).pipe( 19 | map(({ matches }) => matches ? 2 : 3)); 20 | 21 | constructor( 22 | public dialog: MatDialog, 23 | private bookService: BookService, 24 | private breakpointObserver: BreakpointObserver, 25 | private router: Router 26 | ) {} 27 | 28 | ngOnInit(): void { 29 | this.bookService.getBooks().subscribe(books => this.books = books); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ui-application/src/app/book-form/book-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {{ titleText }} 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /ui-application/README.md: -------------------------------------------------------------------------------- 1 | # UiApplication 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.0.5. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /ui-application/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.github.node-gradle.node" version "3.1.1" 3 | } 4 | 5 | task lintAngular(type: NpxTask) { 6 | command = "ng" 7 | args = ["lint"] 8 | dependsOn(tasks.npmInstall) 9 | inputs.dir("src") 10 | inputs.dir("node_modules") 11 | inputs.files("angular.json", ".browserslistrc", "tsconfig.json", "tsconfig.app.json", "tsconfig.spec.json", 12 | "tslint.json") 13 | outputs.upToDateWhen { true } 14 | } 15 | 16 | task testAngular(type: NpxTask) { 17 | command = "ng" 18 | args = ["test"] 19 | dependsOn(tasks.npmInstall) 20 | inputs.dir("src") 21 | inputs.dir("node_modules") 22 | inputs.files("angular.json", ".browserslistrc", "tsconfig.json", "tsconfig.spec.json", "karma.conf.js") 23 | outputs.upToDateWhen { true } 24 | } 25 | 26 | task buildAngular(type: NpxTask) { 27 | command = "ng" 28 | args = ["build"] 29 | dependsOn(tasks.npmInstall) 30 | inputs.dir(project.fileTree("src").exclude("**/*.spec.ts")) 31 | inputs.dir("node_modules") 32 | inputs.files("angular.json", ".browserslistrc", "tsconfig.json", "tsconfig.app.json") 33 | } 34 | -------------------------------------------------------------------------------- /book-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.6.0' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'com.thomasvitale' 8 | version = '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | ext { 16 | set('testcontainersVersion', "1.16.0") 17 | set('testKeycloakVersion', "1.8.1") 18 | } 19 | 20 | dependencies { 21 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 22 | implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' 23 | implementation 'org.springframework.boot:spring-boot-starter-web' 24 | 25 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 26 | testImplementation 'org.springframework.boot:spring-boot-starter-webflux' 27 | testImplementation 'org.testcontainers:junit-jupiter' 28 | testImplementation "com.github.dasniko:testcontainers-keycloak:${testKeycloakVersion}" 29 | } 30 | 31 | dependencyManagement { 32 | imports { 33 | mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}" 34 | } 35 | } 36 | 37 | test { 38 | useJUnitPlatform() 39 | } 40 | -------------------------------------------------------------------------------- /ui-application/src/app/book-list/book-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Browse Books

3 | 4 | 5 | 6 | 7 | 8 | {{book.title}} 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ISBN: {{ book.isbn }} 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /ui-application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-application", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~13.0.2", 16 | "@angular/cdk": "~13.0.2", 17 | "@angular/common": "~13.0.2", 18 | "@angular/compiler": "~13.0.2", 19 | "@angular/core": "~13.0.2", 20 | "@angular/forms": "~13.0.2", 21 | "@angular/material": "~13.0.2", 22 | "@angular/platform-browser": "~13.0.2", 23 | "@angular/platform-browser-dynamic": "~13.0.2", 24 | "@angular/router": "~13.0.2", 25 | "rxjs": "~7.4.0", 26 | "tslib": "^2.3.0", 27 | "zone.js": "~0.11.4" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~13.0.3", 31 | "@angular/cli": "~13.0.3", 32 | "@angular/compiler-cli": "~13.0.2", 33 | "@types/jasmine": "~3.10.0", 34 | "@types/node": "^12.11.1", 35 | "jasmine-core": "~3.10.0", 36 | "karma": "~6.3.4", 37 | "karma-chrome-launcher": "~3.1.0", 38 | "karma-coverage": "~2.0.3", 39 | "karma-jasmine": "~4.0.0", 40 | "karma-jasmine-html-reporter": "~1.7.0", 41 | "typescript": "~4.4.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui-application/src/app/http.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | HttpInterceptor, 7 | HttpErrorResponse 8 | } from '@angular/common/http'; 9 | import { Observable } from 'rxjs'; 10 | import { AuthService } from './auth.service'; 11 | import { tap } from 'rxjs/operators'; 12 | 13 | @Injectable() 14 | export class HttpInterceptorImpl implements HttpInterceptor { 15 | 16 | constructor(private authService: AuthService) {} 17 | 18 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 19 | const finalRequest = this.prepareRequest(request); 20 | return next.handle(finalRequest).pipe(tap(() => {}, 21 | (err: any) => { 22 | if (err instanceof HttpErrorResponse) { 23 | if (!request.url.endsWith('/welcome') && err.status === 401) { 24 | this.authService.login(); 25 | } else if (request.url.endsWith('/welcome') && err.status === 401) { 26 | console.log('Unauthenticated session'); 27 | } 28 | } 29 | } 30 | )); 31 | } 32 | 33 | prepareRequest(request: HttpRequest): HttpRequest { 34 | if (request.url.endsWith('post')) { 35 | return request.clone(); 36 | } 37 | return request.clone({ 38 | headers: request.headers.set('X-Requested-With', 'XMLHttpRequest') 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui-application/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 3 | import { Observable } from 'rxjs'; 4 | import { map, shareReplay } from 'rxjs/operators'; 5 | import { OnInit } from '@angular/core'; 6 | import { AuthService } from './auth.service'; 7 | import { HttpXsrfTokenExtractor } from '@angular/common/http'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | styleUrls: ['./app.component.css'] 13 | }) 14 | export class AppComponent implements OnInit { 15 | 16 | title = 'Polar Bookshop'; 17 | 18 | isAuthenticated = false; 19 | 20 | isHandset$: Observable = this.breakpointObserver.observe(Breakpoints.Handset) 21 | .pipe( 22 | map(result => result.matches), 23 | shareReplay() 24 | ); 25 | 26 | constructor( 27 | private authService: AuthService, 28 | private breakpointObserver: BreakpointObserver, 29 | private httpXsrfTokenExtractor: HttpXsrfTokenExtractor, 30 | ) {} 31 | 32 | ngOnInit(): void { 33 | this.authService.authenticate().subscribe(result => { 34 | if (result) { 35 | this.isAuthenticated = true; 36 | } 37 | }); 38 | } 39 | 40 | logInClicked(): void { 41 | this.authService.login(); 42 | } 43 | 44 | csrfToken(): string | null { 45 | return this.httpXsrfTokenExtractor.getToken(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /edge-service/src/test/java/com/thomasvitale/edgeservice/WelcomeControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.thomasvitale.edgeservice; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; 6 | import org.springframework.boot.test.mock.mockito.MockBean; 7 | import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; 8 | import org.springframework.test.web.reactive.server.WebTestClient; 9 | 10 | import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOidcLogin; 11 | 12 | @WebFluxTest(WelcomeController.class) 13 | public class WelcomeControllerTests { 14 | 15 | @Autowired 16 | WebTestClient webClient; 17 | 18 | @MockBean 19 | ReactiveClientRegistrationRepository clientRegistrationRepository; 20 | 21 | @Test 22 | void whenNotAuthenticatedThen401() { 23 | webClient 24 | .get().uri("/welcome") 25 | .exchange() 26 | .expectStatus().isUnauthorized(); 27 | } 28 | 29 | @Test 30 | void whenAuthenticatedThenReturnPrincipal() { 31 | 32 | webClient 33 | .mutateWith(mockOidcLogin()) 34 | .get().uri("/welcome") 35 | .exchange() 36 | .expectStatus().is2xxSuccessful() 37 | .expectBody(String.class).isEqualTo("Welcome to Polar Bookshop!"); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /edge-service/src/test/java/com/thomasvitale/edgeservice/EdgeServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.thomasvitale.edgeservice; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.boot.test.mock.mockito.MockBean; 6 | import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; 7 | import org.springframework.test.context.DynamicPropertyRegistry; 8 | import org.springframework.test.context.DynamicPropertySource; 9 | import org.testcontainers.containers.GenericContainer; 10 | import org.testcontainers.junit.jupiter.Container; 11 | import org.testcontainers.junit.jupiter.Testcontainers; 12 | import org.testcontainers.utility.DockerImageName; 13 | 14 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 15 | @Testcontainers 16 | class EdgeServiceApplicationTests { 17 | 18 | private static final int REDIS_PORT = 6379; 19 | 20 | @Container 21 | static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6.2")) 22 | .withExposedPorts(REDIS_PORT); 23 | 24 | @MockBean 25 | ReactiveClientRegistrationRepository clientRegistrationRepository; 26 | 27 | @DynamicPropertySource 28 | static void redisProperties(DynamicPropertyRegistry registry) { 29 | registry.add("spring.redis.host", () -> redis.getHost()); 30 | registry.add("spring.redis.port", () -> redis.getMappedPort(REDIS_PORT)); 31 | } 32 | 33 | @Test 34 | void verifyThatSpringContextLoads() { 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ui-application/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ui-application'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /edge-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.6.0' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'com.thomasvitale' 8 | version = '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | maven { url 'https://repo.spring.io/milestone' } 14 | } 15 | 16 | ext { 17 | set('springCloudVersion', "2021.0.0-RC1") 18 | set('testcontainersVersion', "1.16.0") 19 | } 20 | 21 | dependencies { 22 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 23 | implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' 24 | implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' 25 | implementation 'org.springframework.cloud:spring-cloud-starter-gateway' 26 | implementation 'org.springframework.session:spring-session-data-redis' 27 | 28 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 29 | testImplementation 'org.springframework.security:spring-security-test' 30 | testImplementation 'io.projectreactor:reactor-test' 31 | testImplementation 'org.testcontainers:junit-jupiter' 32 | } 33 | 34 | dependencyManagement { 35 | imports { 36 | mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}" 37 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 38 | } 39 | } 40 | 41 | test { 42 | useJUnitPlatform() 43 | } 44 | 45 | processResources { 46 | dependsOn 'compileFrontend' 47 | from ('../ui-application/dist/ui-application') { 48 | into 'static' 49 | } 50 | } 51 | 52 | task compileFrontend(type: GradleBuild) { 53 | dir = '../ui-application' 54 | tasks = ['buildAngular'] 55 | } 56 | -------------------------------------------------------------------------------- /ui-application/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | Menu 6 | 7 | 8 | menu_book 9 | Browse Books 10 | 11 | 12 | import_contacts 13 | Add Book 14 | 15 | 16 | 17 | 18 | 19 | 26 | Polar Bookshop 27 | 28 | 31 |
32 | 33 | 36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /ui-application/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { LayoutModule } from '@angular/cdk/layout'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatListModule } from '@angular/material/list'; 6 | import { MatSidenavModule } from '@angular/material/sidenav'; 7 | import { MatToolbarModule } from '@angular/material/toolbar'; 8 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { RouterTestingModule } from '@angular/router/testing'; 10 | import { AppComponent } from './app.component'; 11 | 12 | describe('AppComponent', () => { 13 | 14 | beforeEach(waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [AppComponent], 17 | imports: [ 18 | NoopAnimationsModule, 19 | LayoutModule, 20 | MatButtonModule, 21 | MatIconModule, 22 | MatListModule, 23 | MatSidenavModule, 24 | MatToolbarModule, 25 | RouterTestingModule 26 | ] 27 | }).compileComponents(); 28 | })); 29 | 30 | it('should create the app', () => { 31 | const fixture = TestBed.createComponent(AppComponent); 32 | const app = fixture.componentInstance; 33 | expect(app).toBeTruthy(); 34 | }); 35 | 36 | it(`should have as title 'Polar Bookshop'`, () => { 37 | const fixture = TestBed.createComponent(AppComponent); 38 | const app = fixture.componentInstance; 39 | expect(app.title).toEqual('Polar Bookshop'); 40 | }); 41 | 42 | it('should render title', () => { 43 | const fixture = TestBed.createComponent(AppComponent); 44 | fixture.detectChanges(); 45 | const compiled = fixture.nativeElement; 46 | expect(compiled.querySelector('.content span').textContent).toContain('Polar Bookshop app is running!'); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /book-service/src/main/java/com/thomasvitale/bookservice/BookServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.thomasvitale.bookservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; 10 | import org.springframework.security.web.SecurityFilterChain; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.Collection; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | 18 | @SpringBootApplication 19 | @EnableWebSecurity 20 | public class BookServiceApplication { 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(BookServiceApplication.class, args); 24 | } 25 | 26 | @Bean 27 | SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 28 | return http 29 | .authorizeRequests(authorize -> authorize.anyRequest().authenticated()) 30 | .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) 31 | .build(); 32 | } 33 | 34 | } 35 | 36 | record Book(String isbn, String title){} 37 | 38 | @RestController 39 | @RequestMapping("books") 40 | class BookController { 41 | 42 | private static final Map books = new HashMap<>(); 43 | 44 | static { 45 | var book1 = new Book("1234567891", "The Hobbit"); 46 | var book2 = new Book("1234567892", "His Dark Materials"); 47 | books.put(book1.isbn(), book1); 48 | books.put(book2.isbn(), book2); 49 | } 50 | 51 | @GetMapping 52 | Collection getBooks() { 53 | return books.values(); 54 | } 55 | 56 | @GetMapping("{isbn}") 57 | Optional getBookIsbn(@PathVariable String isbn) { 58 | return Optional.of(books.get(isbn)); 59 | } 60 | 61 | @PostMapping 62 | @ResponseStatus(HttpStatus.CREATED) 63 | Book createBook(@RequestBody Book book) { 64 | books.put(book.isbn(), book); 65 | return book; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /ui-application/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js/dist/zone'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /ui-application/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 6 | 7 | import { LayoutModule } from '@angular/cdk/layout'; 8 | import { MatDialogModule } from '@angular/material/dialog'; 9 | import { MatButtonModule } from '@angular/material/button'; 10 | import { MatFormFieldModule } from '@angular/material/form-field'; 11 | import { MatIconModule } from '@angular/material/icon'; 12 | import { MatToolbarModule } from '@angular/material/toolbar'; 13 | import { MatSidenavModule } from '@angular/material/sidenav'; 14 | import { MatListModule } from '@angular/material/list'; 15 | import { MatGridListModule } from '@angular/material/grid-list'; 16 | import { MatCardModule } from '@angular/material/card'; 17 | import { MatMenuModule } from '@angular/material/menu'; 18 | import { MatInputModule } from '@angular/material/input'; 19 | import { MatSelectModule } from '@angular/material/select'; 20 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 21 | import { MatRadioModule } from '@angular/material/radio'; 22 | 23 | import { AppRoutingModule } from './app-routing.module'; 24 | import { AppComponent } from './app.component'; 25 | import { BookListComponent } from './book-list/book-list.component'; 26 | import { BookFormComponent } from './book-form/book-form.component'; 27 | import { HttpInterceptorImpl } from './http.interceptor'; 28 | import { AddNewBookComponent } from './add-new-book/add-new-book.component'; 29 | import { EditBookComponent } from './edit-book/edit-book.component'; 30 | 31 | @NgModule({ 32 | declarations: [ 33 | AppComponent, 34 | BookListComponent, 35 | BookFormComponent, 36 | AddNewBookComponent, 37 | EditBookComponent 38 | ], 39 | imports: [ 40 | BrowserModule, 41 | HttpClientModule, 42 | AppRoutingModule, 43 | BrowserAnimationsModule, 44 | FormsModule, 45 | MatDialogModule, 46 | MatButtonModule, 47 | MatFormFieldModule, 48 | MatIconModule, 49 | MatToolbarModule, 50 | LayoutModule, 51 | MatSidenavModule, 52 | MatListModule, 53 | MatGridListModule, 54 | MatCardModule, 55 | MatMenuModule, 56 | MatInputModule, 57 | MatSelectModule, 58 | MatProgressSpinnerModule, 59 | MatRadioModule, 60 | ReactiveFormsModule 61 | ], 62 | providers: [{provide: HTTP_INTERCEPTORS, useClass: HttpInterceptorImpl, multi: true}], 63 | bootstrap: [AppComponent] 64 | }) 65 | export class AppModule { } 66 | -------------------------------------------------------------------------------- /book-service/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /edge-service/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ui-application/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /edge-service/src/main/java/com/thomasvitale/edgeservice/EdgeServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.thomasvitale.edgeservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.security.config.Customizer; 8 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 9 | import org.springframework.security.config.web.server.ServerHttpSecurity; 10 | import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler; 11 | import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; 12 | import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; 13 | import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; 14 | import org.springframework.security.web.server.SecurityWebFilterChain; 15 | import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; 16 | import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; 17 | import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository; 18 | import org.springframework.security.web.server.csrf.CsrfToken; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.RestController; 21 | import org.springframework.web.server.WebFilter; 22 | import reactor.core.publisher.Mono; 23 | 24 | @SpringBootApplication 25 | @EnableWebFluxSecurity 26 | public class EdgeServiceApplication { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(EdgeServiceApplication.class, args); 30 | } 31 | 32 | @Bean 33 | SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) { 34 | return http 35 | .authorizeExchange(authorize -> authorize 36 | .pathMatchers("/", "/*.css", "/*.js", "/favicon.ico").permitAll() 37 | .anyExchange().authenticated()) 38 | .oauth2Login(Customizer.withDefaults()) 39 | .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository))) 40 | .exceptionHandling(exceptionHandling -> exceptionHandling 41 | .authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))) 42 | .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())) 43 | .build(); 44 | } 45 | 46 | private ServerLogoutSuccessHandler oidcLogoutSuccessHandler(ReactiveClientRegistrationRepository clientRegistrationRepository) { 47 | var oidcLogoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository); 48 | oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); 49 | return oidcLogoutSuccessHandler; 50 | } 51 | 52 | @Bean 53 | ServerOAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() { 54 | return new WebSessionServerOAuth2AuthorizedClientRepository(); 55 | } 56 | 57 | @Bean 58 | WebFilter csrfWebFilter() { 59 | // Required because of https://github.com/spring-projects/spring-security/issues/5766 60 | return (exchange, chain) -> { 61 | exchange.getResponse().beforeCommit(() -> Mono.defer(() -> { 62 | Mono csrfToken = exchange.getAttribute(CsrfToken.class.getName()); 63 | return csrfToken != null ? csrfToken.then() : Mono.empty(); 64 | })); 65 | return chain.filter(exchange); 66 | }; 67 | } 68 | 69 | } 70 | 71 | @RestController 72 | class WelcomeController { 73 | 74 | @GetMapping("welcome") 75 | Mono getWelcome() { 76 | return Mono.just(new Welcome("Welcome to Polar Bookshop!")); 77 | } 78 | 79 | } 80 | 81 | record Welcome(String message){} 82 | -------------------------------------------------------------------------------- /book-service/src/test/java/com/thomasvitale/bookservice/BookServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.thomasvitale.bookservice; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import dasniko.testcontainers.keycloak.KeycloakContainer; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.DynamicPropertyRegistry; 13 | import org.springframework.test.context.DynamicPropertySource; 14 | import org.springframework.test.web.reactive.server.WebTestClient; 15 | import org.springframework.web.reactive.function.BodyInserters; 16 | import org.springframework.web.reactive.function.client.WebClient; 17 | import org.testcontainers.junit.jupiter.Container; 18 | import org.testcontainers.junit.jupiter.Testcontainers; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | 22 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 23 | @Testcontainers 24 | class BookServiceApplicationTests { 25 | 26 | private static KeycloakToken userTokens; 27 | 28 | @Autowired 29 | private WebTestClient webTestClient; 30 | 31 | @Container 32 | private static final KeycloakContainer keycloakContainer = new KeycloakContainer("thomasvitale/keycloak-m1:15.0.1") 33 | .withRealmImportFile("keycloak_config.json"); 34 | 35 | @DynamicPropertySource 36 | static void dynamicProperties(DynamicPropertyRegistry registry) { 37 | registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri", 38 | () -> keycloakContainer.getAuthServerUrl() + "/realms/PolarBookshop"); 39 | } 40 | 41 | @BeforeAll 42 | static void generateAccessTokens() { 43 | WebClient webClient = WebClient.builder() 44 | .baseUrl(keycloakContainer.getAuthServerUrl() + "/realms/PolarBookshop/protocol/openid-connect/token") 45 | .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) 46 | .build(); 47 | 48 | userTokens = authenticateWith("isabelle", "password", webClient); 49 | } 50 | 51 | @Test 52 | void whenPostRequestThenBookCreated() { 53 | var expectedBook = new Book("1231231231", "Title"); 54 | 55 | webTestClient 56 | .post() 57 | .uri("/books") 58 | .headers(headers -> headers.setBearerAuth(userTokens.accessToken())) 59 | .bodyValue(expectedBook) 60 | .exchange() 61 | .expectStatus().isCreated() 62 | .expectBody(Book.class).value(actualBook -> { 63 | assertThat(actualBook).isNotNull(); 64 | assertThat(actualBook.isbn()).isEqualTo(expectedBook.isbn()); 65 | }); 66 | } 67 | 68 | @Test 69 | void whenPostRequestUnauthenticatedThen401() { 70 | var expectedBook = new Book("1231231232", "Title"); 71 | 72 | webTestClient 73 | .post() 74 | .uri("/books") 75 | .bodyValue(expectedBook) 76 | .exchange() 77 | .expectStatus().isUnauthorized(); 78 | } 79 | 80 | private static KeycloakToken authenticateWith(String username, String password, WebClient webClient) { 81 | return webClient 82 | .post() 83 | .body(BodyInserters.fromFormData("grant_type", "password") 84 | .with("client_id", "polar-test") 85 | .with("username", username) 86 | .with("password", password) 87 | ) 88 | .retrieve() 89 | .bodyToMono(KeycloakToken.class) 90 | .block(); 91 | } 92 | 93 | private record KeycloakToken(String accessToken) { 94 | 95 | @JsonCreator 96 | private KeycloakToken(@JsonProperty("access_token") final String accessToken) { 97 | this.accessToken = accessToken; 98 | } 99 | 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /ui-application/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ui-application": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:application": { 10 | "strict": true 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ui-application", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 32 | "src/styles.css" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "namedChunks": false, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "900kb", 55 | "maximumError": "1mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "2kb", 60 | "maximumError": "4kb" 61 | } 62 | ] 63 | }, 64 | "development": {} 65 | }, 66 | "defaultConfiguration": "production" 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "options": { 71 | "proxyConfig": "proxy.config.json" 72 | }, 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "ui-application:build:production" 76 | }, 77 | "development": { 78 | "browserTarget": "ui-application:build:development" 79 | } 80 | }, 81 | "defaultConfiguration": "development" 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "ui-application:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "assets": [ 97 | "src/favicon.ico", 98 | "src/assets" 99 | ], 100 | "styles": [ 101 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 102 | "src/styles.css" 103 | ], 104 | "scripts": [] 105 | } 106 | }, 107 | "e2e": { 108 | "builder": "@angular-devkit/build-angular:protractor", 109 | "options": { 110 | "protractorConfig": "e2e/protractor.conf.js" 111 | }, 112 | "configurations": { 113 | "production": { 114 | "devServerTarget": "ui-application:serve:production" 115 | }, 116 | "development": { 117 | "devServerTarget": "ui-application:serve:development" 118 | } 119 | }, 120 | "defaultConfiguration": "development" 121 | } 122 | } 123 | } 124 | }, 125 | "defaultProject": "ui-application" 126 | } 127 | -------------------------------------------------------------------------------- /book-service/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /edge-service/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /ui-application/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /platform/keycloak/realm-export.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb", 3 | "realm" : "PolarBookshop", 4 | "notBefore" : 0, 5 | "defaultSignatureAlgorithm" : "RS256", 6 | "revokeRefreshToken" : false, 7 | "refreshTokenMaxReuse" : 0, 8 | "accessTokenLifespan" : 300, 9 | "accessTokenLifespanForImplicitFlow" : 900, 10 | "ssoSessionIdleTimeout" : 1800, 11 | "ssoSessionMaxLifespan" : 36000, 12 | "ssoSessionIdleTimeoutRememberMe" : 0, 13 | "ssoSessionMaxLifespanRememberMe" : 0, 14 | "offlineSessionIdleTimeout" : 2592000, 15 | "offlineSessionMaxLifespanEnabled" : false, 16 | "offlineSessionMaxLifespan" : 5184000, 17 | "clientSessionIdleTimeout" : 0, 18 | "clientSessionMaxLifespan" : 0, 19 | "clientOfflineSessionIdleTimeout" : 0, 20 | "clientOfflineSessionMaxLifespan" : 0, 21 | "accessCodeLifespan" : 60, 22 | "accessCodeLifespanUserAction" : 300, 23 | "accessCodeLifespanLogin" : 1800, 24 | "actionTokenGeneratedByAdminLifespan" : 43200, 25 | "actionTokenGeneratedByUserLifespan" : 300, 26 | "oauth2DeviceCodeLifespan" : 600, 27 | "oauth2DevicePollingInterval" : 5, 28 | "enabled" : true, 29 | "sslRequired" : "external", 30 | "registrationAllowed" : false, 31 | "registrationEmailAsUsername" : false, 32 | "rememberMe" : false, 33 | "verifyEmail" : false, 34 | "loginWithEmailAllowed" : true, 35 | "duplicateEmailsAllowed" : false, 36 | "resetPasswordAllowed" : false, 37 | "editUsernameAllowed" : false, 38 | "bruteForceProtected" : false, 39 | "permanentLockout" : false, 40 | "maxFailureWaitSeconds" : 900, 41 | "minimumQuickLoginWaitSeconds" : 60, 42 | "waitIncrementSeconds" : 60, 43 | "quickLoginCheckMilliSeconds" : 1000, 44 | "maxDeltaTimeSeconds" : 43200, 45 | "failureFactor" : 30, 46 | "roles" : { 47 | "realm" : [ { 48 | "id" : "d793c9e1-74f6-4f44-b8e7-069ccd5f0112", 49 | "name" : "customer", 50 | "composite" : false, 51 | "clientRole" : false, 52 | "containerId" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb", 53 | "attributes" : { } 54 | }, { 55 | "id" : "79769cda-df2d-4faa-9b2c-556fb65e1858", 56 | "name" : "employee", 57 | "composite" : false, 58 | "clientRole" : false, 59 | "containerId" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb", 60 | "attributes" : { } 61 | }, { 62 | "id" : "21e45e64-6952-4325-ad9c-546cafdda5dc", 63 | "name" : "default-roles-polarbookshop", 64 | "description" : "${role_default-roles}", 65 | "composite" : true, 66 | "composites" : { 67 | "realm" : [ "offline_access", "uma_authorization" ], 68 | "client" : { 69 | "account" : [ "manage-account", "view-profile" ] 70 | } 71 | }, 72 | "clientRole" : false, 73 | "containerId" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb", 74 | "attributes" : { } 75 | }, { 76 | "id" : "4c57bed4-affb-4c57-a323-8600c040e0af", 77 | "name" : "offline_access", 78 | "description" : "${role_offline-access}", 79 | "composite" : false, 80 | "clientRole" : false, 81 | "containerId" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb", 82 | "attributes" : { } 83 | }, { 84 | "id" : "770217d7-a143-466a-abad-2b870e34fb8b", 85 | "name" : "uma_authorization", 86 | "description" : "${role_uma_authorization}", 87 | "composite" : false, 88 | "clientRole" : false, 89 | "containerId" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb", 90 | "attributes" : { } 91 | } ], 92 | "client" : { 93 | "realm-management" : [ { 94 | "id" : "1acb1e67-d8c8-4fa4-ae80-da049700fb80", 95 | "name" : "query-realms", 96 | "description" : "${role_query-realms}", 97 | "composite" : false, 98 | "clientRole" : true, 99 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 100 | "attributes" : { } 101 | }, { 102 | "id" : "70939670-dbd5-43c3-a019-1687f757b3cc", 103 | "name" : "view-authorization", 104 | "description" : "${role_view-authorization}", 105 | "composite" : false, 106 | "clientRole" : true, 107 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 108 | "attributes" : { } 109 | }, { 110 | "id" : "9cea1f92-1a9a-4ae4-aad9-c0c0ec886b58", 111 | "name" : "manage-authorization", 112 | "description" : "${role_manage-authorization}", 113 | "composite" : false, 114 | "clientRole" : true, 115 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 116 | "attributes" : { } 117 | }, { 118 | "id" : "2d102cf7-276b-434c-83d1-ef96c59c3ae1", 119 | "name" : "impersonation", 120 | "description" : "${role_impersonation}", 121 | "composite" : false, 122 | "clientRole" : true, 123 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 124 | "attributes" : { } 125 | }, { 126 | "id" : "1f5a550a-5e82-47d6-a093-b8295a3e62d6", 127 | "name" : "manage-clients", 128 | "description" : "${role_manage-clients}", 129 | "composite" : false, 130 | "clientRole" : true, 131 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 132 | "attributes" : { } 133 | }, { 134 | "id" : "0e0813aa-ff69-4654-9c33-6326e81f2129", 135 | "name" : "query-users", 136 | "description" : "${role_query-users}", 137 | "composite" : false, 138 | "clientRole" : true, 139 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 140 | "attributes" : { } 141 | }, { 142 | "id" : "714aa8a5-286b-4701-963c-56d881fc6a22", 143 | "name" : "create-client", 144 | "description" : "${role_create-client}", 145 | "composite" : false, 146 | "clientRole" : true, 147 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 148 | "attributes" : { } 149 | }, { 150 | "id" : "1f4a7b78-4243-4ec6-88a6-a093cb4bd2e3", 151 | "name" : "manage-events", 152 | "description" : "${role_manage-events}", 153 | "composite" : false, 154 | "clientRole" : true, 155 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 156 | "attributes" : { } 157 | }, { 158 | "id" : "2a446abe-e91b-4d23-91bd-38c4df2bac31", 159 | "name" : "query-clients", 160 | "description" : "${role_query-clients}", 161 | "composite" : false, 162 | "clientRole" : true, 163 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 164 | "attributes" : { } 165 | }, { 166 | "id" : "abf34bf5-645d-42fd-8196-baf3e63d90d8", 167 | "name" : "realm-admin", 168 | "description" : "${role_realm-admin}", 169 | "composite" : true, 170 | "composites" : { 171 | "client" : { 172 | "realm-management" : [ "query-realms", "view-authorization", "manage-authorization", "query-users", "manage-clients", "impersonation", "manage-events", "query-clients", "create-client", "view-events", "query-groups", "manage-users", "manage-realm", "manage-identity-providers", "view-realm", "view-clients", "view-users", "view-identity-providers" ] 173 | } 174 | }, 175 | "clientRole" : true, 176 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 177 | "attributes" : { } 178 | }, { 179 | "id" : "7c55f95f-74e9-41f9-8d00-b52eb6c8454b", 180 | "name" : "view-events", 181 | "description" : "${role_view-events}", 182 | "composite" : false, 183 | "clientRole" : true, 184 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 185 | "attributes" : { } 186 | }, { 187 | "id" : "c154361b-e686-4f21-b96f-dfc2b9d9566f", 188 | "name" : "manage-users", 189 | "description" : "${role_manage-users}", 190 | "composite" : false, 191 | "clientRole" : true, 192 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 193 | "attributes" : { } 194 | }, { 195 | "id" : "9273108b-547b-4535-b82a-a8b270f1751d", 196 | "name" : "query-groups", 197 | "description" : "${role_query-groups}", 198 | "composite" : false, 199 | "clientRole" : true, 200 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 201 | "attributes" : { } 202 | }, { 203 | "id" : "649ca356-0ea3-4775-be22-154f3c29ca8e", 204 | "name" : "manage-realm", 205 | "description" : "${role_manage-realm}", 206 | "composite" : false, 207 | "clientRole" : true, 208 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 209 | "attributes" : { } 210 | }, { 211 | "id" : "61b9e2de-9d92-43ad-a878-a1914fef8754", 212 | "name" : "manage-identity-providers", 213 | "description" : "${role_manage-identity-providers}", 214 | "composite" : false, 215 | "clientRole" : true, 216 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 217 | "attributes" : { } 218 | }, { 219 | "id" : "635e8a99-5a60-4ac0-b241-77d0cfde6113", 220 | "name" : "view-realm", 221 | "description" : "${role_view-realm}", 222 | "composite" : false, 223 | "clientRole" : true, 224 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 225 | "attributes" : { } 226 | }, { 227 | "id" : "39bf0f07-6027-49e1-9ce1-0dc1fea8fbb0", 228 | "name" : "view-clients", 229 | "description" : "${role_view-clients}", 230 | "composite" : true, 231 | "composites" : { 232 | "client" : { 233 | "realm-management" : [ "query-clients" ] 234 | } 235 | }, 236 | "clientRole" : true, 237 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 238 | "attributes" : { } 239 | }, { 240 | "id" : "accedb9a-b39a-4942-93fe-2aa2c28216bd", 241 | "name" : "view-users", 242 | "description" : "${role_view-users}", 243 | "composite" : true, 244 | "composites" : { 245 | "client" : { 246 | "realm-management" : [ "query-users", "query-groups" ] 247 | } 248 | }, 249 | "clientRole" : true, 250 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 251 | "attributes" : { } 252 | }, { 253 | "id" : "1db2ea79-3ab0-4a46-989c-4e6fa580cd0b", 254 | "name" : "view-identity-providers", 255 | "description" : "${role_view-identity-providers}", 256 | "composite" : false, 257 | "clientRole" : true, 258 | "containerId" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 259 | "attributes" : { } 260 | } ], 261 | "edge-service" : [ ], 262 | "security-admin-console" : [ ], 263 | "admin-cli" : [ ], 264 | "account-console" : [ ], 265 | "broker" : [ { 266 | "id" : "c310a693-61fb-49ba-afd2-c33130afbffd", 267 | "name" : "read-token", 268 | "description" : "${role_read-token}", 269 | "composite" : false, 270 | "clientRole" : true, 271 | "containerId" : "ac27b810-3fde-4e8a-8094-efc17dbe8947", 272 | "attributes" : { } 273 | } ], 274 | "account" : [ { 275 | "id" : "db00fe3c-6abd-4cd5-9377-a526dd7fe30e", 276 | "name" : "view-applications", 277 | "description" : "${role_view-applications}", 278 | "composite" : false, 279 | "clientRole" : true, 280 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 281 | "attributes" : { } 282 | }, { 283 | "id" : "ad90771f-3dcc-46ab-a0e7-761f24e7fcbe", 284 | "name" : "delete-account", 285 | "description" : "${role_delete-account}", 286 | "composite" : false, 287 | "clientRole" : true, 288 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 289 | "attributes" : { } 290 | }, { 291 | "id" : "06f3c924-2474-4085-9e60-8a1de9d976af", 292 | "name" : "manage-account", 293 | "description" : "${role_manage-account}", 294 | "composite" : true, 295 | "composites" : { 296 | "client" : { 297 | "account" : [ "manage-account-links" ] 298 | } 299 | }, 300 | "clientRole" : true, 301 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 302 | "attributes" : { } 303 | }, { 304 | "id" : "0c785922-d070-4889-8e81-b3a94ed5c946", 305 | "name" : "view-consent", 306 | "description" : "${role_view-consent}", 307 | "composite" : false, 308 | "clientRole" : true, 309 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 310 | "attributes" : { } 311 | }, { 312 | "id" : "ecb3a79a-629d-413c-bf1e-f04c8a19d8c6", 313 | "name" : "view-profile", 314 | "description" : "${role_view-profile}", 315 | "composite" : false, 316 | "clientRole" : true, 317 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 318 | "attributes" : { } 319 | }, { 320 | "id" : "3a07608a-6805-42f5-80b3-6698be87e266", 321 | "name" : "manage-consent", 322 | "description" : "${role_manage-consent}", 323 | "composite" : true, 324 | "composites" : { 325 | "client" : { 326 | "account" : [ "view-consent" ] 327 | } 328 | }, 329 | "clientRole" : true, 330 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 331 | "attributes" : { } 332 | }, { 333 | "id" : "407d8b2e-d291-49fd-9bd8-b912cb10f86d", 334 | "name" : "manage-account-links", 335 | "description" : "${role_manage-account-links}", 336 | "composite" : false, 337 | "clientRole" : true, 338 | "containerId" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 339 | "attributes" : { } 340 | } ] 341 | } 342 | }, 343 | "groups" : [ ], 344 | "defaultRole" : { 345 | "id" : "21e45e64-6952-4325-ad9c-546cafdda5dc", 346 | "name" : "default-roles-polarbookshop", 347 | "description" : "${role_default-roles}", 348 | "composite" : true, 349 | "clientRole" : false, 350 | "containerId" : "2c8a8c6c-32f7-4461-97bf-2fa0051a3bcb" 351 | }, 352 | "requiredCredentials" : [ "password" ], 353 | "otpPolicyType" : "totp", 354 | "otpPolicyAlgorithm" : "HmacSHA1", 355 | "otpPolicyInitialCounter" : 0, 356 | "otpPolicyDigits" : 6, 357 | "otpPolicyLookAheadWindow" : 1, 358 | "otpPolicyPeriod" : 30, 359 | "otpSupportedApplications" : [ "FreeOTP", "Google Authenticator" ], 360 | "webAuthnPolicyRpEntityName" : "keycloak", 361 | "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], 362 | "webAuthnPolicyRpId" : "", 363 | "webAuthnPolicyAttestationConveyancePreference" : "not specified", 364 | "webAuthnPolicyAuthenticatorAttachment" : "not specified", 365 | "webAuthnPolicyRequireResidentKey" : "not specified", 366 | "webAuthnPolicyUserVerificationRequirement" : "not specified", 367 | "webAuthnPolicyCreateTimeout" : 0, 368 | "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, 369 | "webAuthnPolicyAcceptableAaguids" : [ ], 370 | "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", 371 | "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], 372 | "webAuthnPolicyPasswordlessRpId" : "", 373 | "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", 374 | "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", 375 | "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", 376 | "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", 377 | "webAuthnPolicyPasswordlessCreateTimeout" : 0, 378 | "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, 379 | "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], 380 | "users" : [ { 381 | "id" : "a512f22b-da00-41c3-83fb-c7e9c8e11c6a", 382 | "createdTimestamp" : 1631734312411, 383 | "username" : "bjorn", 384 | "enabled" : true, 385 | "totp" : false, 386 | "emailVerified" : false, 387 | "firstName" : "Bjorn", 388 | "lastName" : "Vinterberg", 389 | "credentials" : [ { 390 | "id" : "e694b099-ecb3-41e9-a119-c9cae28bf49e", 391 | "type" : "password", 392 | "createdDate" : 1631734347429, 393 | "secretData" : "{\"value\":\"22y8PR4TVm7w0HqzcVEZhEMkXj78eRrDSdA3EFYIKvx81AYL5E25k7kfH/RNU7ul6+wiT1robgNaUlqwmjZD/w==\",\"salt\":\"UIzWYySJbqYAgpr1MJuAjw==\",\"additionalParameters\":{}}", 394 | "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" 395 | } ], 396 | "disableableCredentialTypes" : [ ], 397 | "requiredActions" : [ ], 398 | "realmRoles" : [ "customer", "default-roles-polarbookshop" ], 399 | "notBefore" : 0, 400 | "groups" : [ ] 401 | }, { 402 | "id" : "fab48ff8-05bb-428a-b3fd-934ebc291123", 403 | "createdTimestamp" : 1631734281404, 404 | "username" : "isabelle", 405 | "enabled" : true, 406 | "totp" : false, 407 | "emailVerified" : false, 408 | "firstName" : "Isabelle", 409 | "lastName" : "Dahl", 410 | "credentials" : [ { 411 | "id" : "5120a212-d018-4c5c-8e5f-e9db43051d27", 412 | "type" : "password", 413 | "createdDate" : 1631734340263, 414 | "secretData" : "{\"value\":\"BqHyaJH2VQPNVckxt3f6irzMAkYAPwN2y91c5nYVxnnvBVx3vHGgyBwuy5c/cst4lbco3vTCoMz4wNcHRN7m7g==\",\"salt\":\"Nsr//DpiMWlzi52/FrTRAA==\",\"additionalParameters\":{}}", 415 | "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" 416 | } ], 417 | "disableableCredentialTypes" : [ ], 418 | "requiredActions" : [ ], 419 | "realmRoles" : [ "customer", "employee", "default-roles-polarbookshop" ], 420 | "notBefore" : 0, 421 | "groups" : [ ] 422 | } ], 423 | "scopeMappings" : [ { 424 | "clientScope" : "offline_access", 425 | "roles" : [ "offline_access" ] 426 | } ], 427 | "clientScopeMappings" : { 428 | "account" : [ { 429 | "client" : "account-console", 430 | "roles" : [ "manage-account" ] 431 | } ] 432 | }, 433 | "clients" : [ { 434 | "id" : "7d424681-ee92-4cae-bad2-0ca2edc67d3a", 435 | "clientId" : "account", 436 | "name" : "${client_account}", 437 | "rootUrl" : "${authBaseUrl}", 438 | "baseUrl" : "/realms/PolarBookshop/account/", 439 | "surrogateAuthRequired" : false, 440 | "enabled" : true, 441 | "alwaysDisplayInConsole" : false, 442 | "clientAuthenticatorType" : "client-secret", 443 | "redirectUris" : [ "/realms/PolarBookshop/account/*" ], 444 | "webOrigins" : [ ], 445 | "notBefore" : 0, 446 | "bearerOnly" : false, 447 | "consentRequired" : false, 448 | "standardFlowEnabled" : true, 449 | "implicitFlowEnabled" : false, 450 | "directAccessGrantsEnabled" : false, 451 | "serviceAccountsEnabled" : false, 452 | "publicClient" : true, 453 | "frontchannelLogout" : false, 454 | "protocol" : "openid-connect", 455 | "attributes" : { }, 456 | "authenticationFlowBindingOverrides" : { }, 457 | "fullScopeAllowed" : false, 458 | "nodeReRegistrationTimeout" : 0, 459 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 460 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 461 | }, { 462 | "id" : "333b0cc6-b5b7-4614-8f32-2dd56ec9f288", 463 | "clientId" : "account-console", 464 | "name" : "${client_account-console}", 465 | "rootUrl" : "${authBaseUrl}", 466 | "baseUrl" : "/realms/PolarBookshop/account/", 467 | "surrogateAuthRequired" : false, 468 | "enabled" : true, 469 | "alwaysDisplayInConsole" : false, 470 | "clientAuthenticatorType" : "client-secret", 471 | "redirectUris" : [ "/realms/PolarBookshop/account/*" ], 472 | "webOrigins" : [ ], 473 | "notBefore" : 0, 474 | "bearerOnly" : false, 475 | "consentRequired" : false, 476 | "standardFlowEnabled" : true, 477 | "implicitFlowEnabled" : false, 478 | "directAccessGrantsEnabled" : false, 479 | "serviceAccountsEnabled" : false, 480 | "publicClient" : true, 481 | "frontchannelLogout" : false, 482 | "protocol" : "openid-connect", 483 | "attributes" : { 484 | "pkce.code.challenge.method" : "S256" 485 | }, 486 | "authenticationFlowBindingOverrides" : { }, 487 | "fullScopeAllowed" : false, 488 | "nodeReRegistrationTimeout" : 0, 489 | "protocolMappers" : [ { 490 | "id" : "d79c4c9f-a1bb-4837-a7df-4bcd3311a983", 491 | "name" : "audience resolve", 492 | "protocol" : "openid-connect", 493 | "protocolMapper" : "oidc-audience-resolve-mapper", 494 | "consentRequired" : false, 495 | "config" : { } 496 | } ], 497 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 498 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 499 | }, { 500 | "id" : "d1ba56e0-a2dd-43c6-a561-68f2047c836e", 501 | "clientId" : "admin-cli", 502 | "name" : "${client_admin-cli}", 503 | "surrogateAuthRequired" : false, 504 | "enabled" : true, 505 | "alwaysDisplayInConsole" : false, 506 | "clientAuthenticatorType" : "client-secret", 507 | "redirectUris" : [ ], 508 | "webOrigins" : [ ], 509 | "notBefore" : 0, 510 | "bearerOnly" : false, 511 | "consentRequired" : false, 512 | "standardFlowEnabled" : false, 513 | "implicitFlowEnabled" : false, 514 | "directAccessGrantsEnabled" : true, 515 | "serviceAccountsEnabled" : false, 516 | "publicClient" : true, 517 | "frontchannelLogout" : false, 518 | "protocol" : "openid-connect", 519 | "attributes" : { }, 520 | "authenticationFlowBindingOverrides" : { }, 521 | "fullScopeAllowed" : false, 522 | "nodeReRegistrationTimeout" : 0, 523 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 524 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 525 | }, { 526 | "id" : "ac27b810-3fde-4e8a-8094-efc17dbe8947", 527 | "clientId" : "broker", 528 | "name" : "${client_broker}", 529 | "surrogateAuthRequired" : false, 530 | "enabled" : true, 531 | "alwaysDisplayInConsole" : false, 532 | "clientAuthenticatorType" : "client-secret", 533 | "redirectUris" : [ ], 534 | "webOrigins" : [ ], 535 | "notBefore" : 0, 536 | "bearerOnly" : true, 537 | "consentRequired" : false, 538 | "standardFlowEnabled" : true, 539 | "implicitFlowEnabled" : false, 540 | "directAccessGrantsEnabled" : false, 541 | "serviceAccountsEnabled" : false, 542 | "publicClient" : false, 543 | "frontchannelLogout" : false, 544 | "protocol" : "openid-connect", 545 | "attributes" : { }, 546 | "authenticationFlowBindingOverrides" : { }, 547 | "fullScopeAllowed" : false, 548 | "nodeReRegistrationTimeout" : 0, 549 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 550 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 551 | }, { 552 | "id" : "d374caf6-d082-40bc-a9d6-19190f3b82d8", 553 | "clientId" : "edge-service", 554 | "surrogateAuthRequired" : false, 555 | "enabled" : true, 556 | "alwaysDisplayInConsole" : false, 557 | "clientAuthenticatorType" : "client-secret", 558 | "secret" : "polar-keycloak-secret", 559 | "redirectUris" : [ "http://localhost:9000/login/oauth2/code/*", "http://localhost:9000" ], 560 | "webOrigins" : [ "http://localhost:9000" ], 561 | "notBefore" : 0, 562 | "bearerOnly" : false, 563 | "consentRequired" : false, 564 | "standardFlowEnabled" : true, 565 | "implicitFlowEnabled" : false, 566 | "directAccessGrantsEnabled" : false, 567 | "serviceAccountsEnabled" : false, 568 | "publicClient" : false, 569 | "frontchannelLogout" : false, 570 | "protocol" : "openid-connect", 571 | "attributes" : { }, 572 | "authenticationFlowBindingOverrides" : { }, 573 | "fullScopeAllowed" : true, 574 | "nodeReRegistrationTimeout" : -1, 575 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 576 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 577 | }, { 578 | "id" : "49a4e7fe-3b7f-430e-a8e4-3a95c0c76e40", 579 | "clientId" : "realm-management", 580 | "name" : "${client_realm-management}", 581 | "surrogateAuthRequired" : false, 582 | "enabled" : true, 583 | "alwaysDisplayInConsole" : false, 584 | "clientAuthenticatorType" : "client-secret", 585 | "redirectUris" : [ ], 586 | "webOrigins" : [ ], 587 | "notBefore" : 0, 588 | "bearerOnly" : true, 589 | "consentRequired" : false, 590 | "standardFlowEnabled" : true, 591 | "implicitFlowEnabled" : false, 592 | "directAccessGrantsEnabled" : false, 593 | "serviceAccountsEnabled" : false, 594 | "publicClient" : false, 595 | "frontchannelLogout" : false, 596 | "protocol" : "openid-connect", 597 | "attributes" : { }, 598 | "authenticationFlowBindingOverrides" : { }, 599 | "fullScopeAllowed" : false, 600 | "nodeReRegistrationTimeout" : 0, 601 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 602 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 603 | }, { 604 | "id" : "2c70cd00-783c-47fb-a50b-4db537a72bdf", 605 | "clientId" : "security-admin-console", 606 | "name" : "${client_security-admin-console}", 607 | "rootUrl" : "${authAdminUrl}", 608 | "baseUrl" : "/admin/PolarBookshop/console/", 609 | "surrogateAuthRequired" : false, 610 | "enabled" : true, 611 | "alwaysDisplayInConsole" : false, 612 | "clientAuthenticatorType" : "client-secret", 613 | "redirectUris" : [ "/admin/PolarBookshop/console/*" ], 614 | "webOrigins" : [ "+" ], 615 | "notBefore" : 0, 616 | "bearerOnly" : false, 617 | "consentRequired" : false, 618 | "standardFlowEnabled" : true, 619 | "implicitFlowEnabled" : false, 620 | "directAccessGrantsEnabled" : false, 621 | "serviceAccountsEnabled" : false, 622 | "publicClient" : true, 623 | "frontchannelLogout" : false, 624 | "protocol" : "openid-connect", 625 | "attributes" : { 626 | "pkce.code.challenge.method" : "S256" 627 | }, 628 | "authenticationFlowBindingOverrides" : { }, 629 | "fullScopeAllowed" : false, 630 | "nodeReRegistrationTimeout" : 0, 631 | "protocolMappers" : [ { 632 | "id" : "a96b0dd9-576c-4bd3-a5a3-de6b8628223f", 633 | "name" : "locale", 634 | "protocol" : "openid-connect", 635 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 636 | "consentRequired" : false, 637 | "config" : { 638 | "userinfo.token.claim" : "true", 639 | "user.attribute" : "locale", 640 | "id.token.claim" : "true", 641 | "access.token.claim" : "true", 642 | "claim.name" : "locale", 643 | "jsonType.label" : "String" 644 | } 645 | } ], 646 | "defaultClientScopes" : [ "web-origins", "roles", "profile", "email" ], 647 | "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] 648 | } ], 649 | "clientScopes" : [ { 650 | "id" : "1fe68fff-5f30-4b07-a2e9-be7e919d8b19", 651 | "name" : "roles", 652 | "description" : "OpenID Connect scope for add user roles to the access token", 653 | "protocol" : "openid-connect", 654 | "attributes" : { 655 | "include.in.token.scope" : "false", 656 | "display.on.consent.screen" : "true", 657 | "consent.screen.text" : "${rolesScopeConsentText}" 658 | }, 659 | "protocolMappers" : [ { 660 | "id" : "5fb66ac7-3cd6-437b-b4af-20894bb541df", 661 | "name" : "realm roles", 662 | "protocol" : "openid-connect", 663 | "protocolMapper" : "oidc-usermodel-realm-role-mapper", 664 | "consentRequired" : false, 665 | "config" : { 666 | "multivalued" : "true", 667 | "user.attribute" : "foo", 668 | "id.token.claim" : "true", 669 | "access.token.claim" : "true", 670 | "claim.name" : "roles", 671 | "jsonType.label" : "String" 672 | } 673 | }, { 674 | "id" : "287872cc-fe34-4394-b1f6-c2624fbcc4d1", 675 | "name" : "client roles", 676 | "protocol" : "openid-connect", 677 | "protocolMapper" : "oidc-usermodel-client-role-mapper", 678 | "consentRequired" : false, 679 | "config" : { 680 | "user.attribute" : "foo", 681 | "access.token.claim" : "true", 682 | "claim.name" : "resource_access.${client_id}.roles", 683 | "jsonType.label" : "String", 684 | "multivalued" : "true" 685 | } 686 | }, { 687 | "id" : "2c1b5381-5360-47a7-a3a3-b7b93396b95e", 688 | "name" : "audience resolve", 689 | "protocol" : "openid-connect", 690 | "protocolMapper" : "oidc-audience-resolve-mapper", 691 | "consentRequired" : false, 692 | "config" : { } 693 | } ] 694 | }, { 695 | "id" : "dc9ce4d4-8cbb-4982-976b-fb99a86c6f6f", 696 | "name" : "web-origins", 697 | "description" : "OpenID Connect scope for add allowed web origins to the access token", 698 | "protocol" : "openid-connect", 699 | "attributes" : { 700 | "include.in.token.scope" : "false", 701 | "display.on.consent.screen" : "false", 702 | "consent.screen.text" : "" 703 | }, 704 | "protocolMappers" : [ { 705 | "id" : "c5e0a572-cf7c-4fe7-ab3a-48fe7cdf25af", 706 | "name" : "allowed web origins", 707 | "protocol" : "openid-connect", 708 | "protocolMapper" : "oidc-allowed-origins-mapper", 709 | "consentRequired" : false, 710 | "config" : { } 711 | } ] 712 | }, { 713 | "id" : "5308a7e2-b024-43f4-b9a7-7f16d3eecf08", 714 | "name" : "address", 715 | "description" : "OpenID Connect built-in scope: address", 716 | "protocol" : "openid-connect", 717 | "attributes" : { 718 | "include.in.token.scope" : "true", 719 | "display.on.consent.screen" : "true", 720 | "consent.screen.text" : "${addressScopeConsentText}" 721 | }, 722 | "protocolMappers" : [ { 723 | "id" : "3b77c47e-a0c4-4e8a-b5f6-053d83b74cfc", 724 | "name" : "address", 725 | "protocol" : "openid-connect", 726 | "protocolMapper" : "oidc-address-mapper", 727 | "consentRequired" : false, 728 | "config" : { 729 | "user.attribute.formatted" : "formatted", 730 | "user.attribute.country" : "country", 731 | "user.attribute.postal_code" : "postal_code", 732 | "userinfo.token.claim" : "true", 733 | "user.attribute.street" : "street", 734 | "id.token.claim" : "true", 735 | "user.attribute.region" : "region", 736 | "access.token.claim" : "true", 737 | "user.attribute.locality" : "locality" 738 | } 739 | } ] 740 | }, { 741 | "id" : "be5bb807-784e-45f1-a757-9c3d41f7b2a9", 742 | "name" : "profile", 743 | "description" : "OpenID Connect built-in scope: profile", 744 | "protocol" : "openid-connect", 745 | "attributes" : { 746 | "include.in.token.scope" : "true", 747 | "display.on.consent.screen" : "true", 748 | "consent.screen.text" : "${profileScopeConsentText}" 749 | }, 750 | "protocolMappers" : [ { 751 | "id" : "33a99069-f001-4b44-9739-52fb542c0a2c", 752 | "name" : "gender", 753 | "protocol" : "openid-connect", 754 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 755 | "consentRequired" : false, 756 | "config" : { 757 | "userinfo.token.claim" : "true", 758 | "user.attribute" : "gender", 759 | "id.token.claim" : "true", 760 | "access.token.claim" : "true", 761 | "claim.name" : "gender", 762 | "jsonType.label" : "String" 763 | } 764 | }, { 765 | "id" : "bc980074-4d09-4472-9121-741e1d9e8aff", 766 | "name" : "middle name", 767 | "protocol" : "openid-connect", 768 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 769 | "consentRequired" : false, 770 | "config" : { 771 | "userinfo.token.claim" : "true", 772 | "user.attribute" : "middleName", 773 | "id.token.claim" : "true", 774 | "access.token.claim" : "true", 775 | "claim.name" : "middle_name", 776 | "jsonType.label" : "String" 777 | } 778 | }, { 779 | "id" : "bcd0fa9e-06e1-46a7-bb49-07ab1b872269", 780 | "name" : "updated at", 781 | "protocol" : "openid-connect", 782 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 783 | "consentRequired" : false, 784 | "config" : { 785 | "userinfo.token.claim" : "true", 786 | "user.attribute" : "updatedAt", 787 | "id.token.claim" : "true", 788 | "access.token.claim" : "true", 789 | "claim.name" : "updated_at", 790 | "jsonType.label" : "String" 791 | } 792 | }, { 793 | "id" : "b1e6ce39-f2c6-4467-8910-6fd115b832da", 794 | "name" : "nickname", 795 | "protocol" : "openid-connect", 796 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 797 | "consentRequired" : false, 798 | "config" : { 799 | "userinfo.token.claim" : "true", 800 | "user.attribute" : "nickname", 801 | "id.token.claim" : "true", 802 | "access.token.claim" : "true", 803 | "claim.name" : "nickname", 804 | "jsonType.label" : "String" 805 | } 806 | }, { 807 | "id" : "6f736386-e00d-4d46-a8df-947d8fc34dfa", 808 | "name" : "zoneinfo", 809 | "protocol" : "openid-connect", 810 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 811 | "consentRequired" : false, 812 | "config" : { 813 | "userinfo.token.claim" : "true", 814 | "user.attribute" : "zoneinfo", 815 | "id.token.claim" : "true", 816 | "access.token.claim" : "true", 817 | "claim.name" : "zoneinfo", 818 | "jsonType.label" : "String" 819 | } 820 | }, { 821 | "id" : "e1f2c1f3-b4ed-4aa1-afc3-7ad40aeebaaf", 822 | "name" : "website", 823 | "protocol" : "openid-connect", 824 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 825 | "consentRequired" : false, 826 | "config" : { 827 | "userinfo.token.claim" : "true", 828 | "user.attribute" : "website", 829 | "id.token.claim" : "true", 830 | "access.token.claim" : "true", 831 | "claim.name" : "website", 832 | "jsonType.label" : "String" 833 | } 834 | }, { 835 | "id" : "4f3390b3-ac61-47cf-9fd4-b7fc71f421bc", 836 | "name" : "locale", 837 | "protocol" : "openid-connect", 838 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 839 | "consentRequired" : false, 840 | "config" : { 841 | "userinfo.token.claim" : "true", 842 | "user.attribute" : "locale", 843 | "id.token.claim" : "true", 844 | "access.token.claim" : "true", 845 | "claim.name" : "locale", 846 | "jsonType.label" : "String" 847 | } 848 | }, { 849 | "id" : "f58590ec-e9df-46c0-b8f0-434e33126ee8", 850 | "name" : "full name", 851 | "protocol" : "openid-connect", 852 | "protocolMapper" : "oidc-full-name-mapper", 853 | "consentRequired" : false, 854 | "config" : { 855 | "id.token.claim" : "true", 856 | "access.token.claim" : "true", 857 | "userinfo.token.claim" : "true" 858 | } 859 | }, { 860 | "id" : "64ae371f-f8b0-47bd-9645-57fc0ce2bd01", 861 | "name" : "profile", 862 | "protocol" : "openid-connect", 863 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 864 | "consentRequired" : false, 865 | "config" : { 866 | "userinfo.token.claim" : "true", 867 | "user.attribute" : "profile", 868 | "id.token.claim" : "true", 869 | "access.token.claim" : "true", 870 | "claim.name" : "profile", 871 | "jsonType.label" : "String" 872 | } 873 | }, { 874 | "id" : "3dd54a4e-7eeb-4368-b7df-e21996f28184", 875 | "name" : "given name", 876 | "protocol" : "openid-connect", 877 | "protocolMapper" : "oidc-usermodel-property-mapper", 878 | "consentRequired" : false, 879 | "config" : { 880 | "userinfo.token.claim" : "true", 881 | "user.attribute" : "firstName", 882 | "id.token.claim" : "true", 883 | "access.token.claim" : "true", 884 | "claim.name" : "given_name", 885 | "jsonType.label" : "String" 886 | } 887 | }, { 888 | "id" : "7e088a0c-9df1-4721-bd4f-ff09d57f76f9", 889 | "name" : "picture", 890 | "protocol" : "openid-connect", 891 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 892 | "consentRequired" : false, 893 | "config" : { 894 | "userinfo.token.claim" : "true", 895 | "user.attribute" : "picture", 896 | "id.token.claim" : "true", 897 | "access.token.claim" : "true", 898 | "claim.name" : "picture", 899 | "jsonType.label" : "String" 900 | } 901 | }, { 902 | "id" : "c77e33ec-673a-4d6c-9989-a10ad7f3208a", 903 | "name" : "birthdate", 904 | "protocol" : "openid-connect", 905 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 906 | "consentRequired" : false, 907 | "config" : { 908 | "userinfo.token.claim" : "true", 909 | "user.attribute" : "birthdate", 910 | "id.token.claim" : "true", 911 | "access.token.claim" : "true", 912 | "claim.name" : "birthdate", 913 | "jsonType.label" : "String" 914 | } 915 | }, { 916 | "id" : "f104ba8b-d70a-448c-972d-043c54b5b488", 917 | "name" : "username", 918 | "protocol" : "openid-connect", 919 | "protocolMapper" : "oidc-usermodel-property-mapper", 920 | "consentRequired" : false, 921 | "config" : { 922 | "userinfo.token.claim" : "true", 923 | "user.attribute" : "username", 924 | "id.token.claim" : "true", 925 | "access.token.claim" : "true", 926 | "claim.name" : "preferred_username", 927 | "jsonType.label" : "String" 928 | } 929 | }, { 930 | "id" : "7bd018a1-ed07-4c70-a6f2-fcca78ca3e21", 931 | "name" : "family name", 932 | "protocol" : "openid-connect", 933 | "protocolMapper" : "oidc-usermodel-property-mapper", 934 | "consentRequired" : false, 935 | "config" : { 936 | "userinfo.token.claim" : "true", 937 | "user.attribute" : "lastName", 938 | "id.token.claim" : "true", 939 | "access.token.claim" : "true", 940 | "claim.name" : "family_name", 941 | "jsonType.label" : "String" 942 | } 943 | } ] 944 | }, { 945 | "id" : "b80792cd-2fa4-4f1f-9320-398f64c8a7fa", 946 | "name" : "phone", 947 | "description" : "OpenID Connect built-in scope: phone", 948 | "protocol" : "openid-connect", 949 | "attributes" : { 950 | "include.in.token.scope" : "true", 951 | "display.on.consent.screen" : "true", 952 | "consent.screen.text" : "${phoneScopeConsentText}" 953 | }, 954 | "protocolMappers" : [ { 955 | "id" : "2a4118d5-4108-4958-ab90-06ced1b88333", 956 | "name" : "phone number verified", 957 | "protocol" : "openid-connect", 958 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 959 | "consentRequired" : false, 960 | "config" : { 961 | "userinfo.token.claim" : "true", 962 | "user.attribute" : "phoneNumberVerified", 963 | "id.token.claim" : "true", 964 | "access.token.claim" : "true", 965 | "claim.name" : "phone_number_verified", 966 | "jsonType.label" : "boolean" 967 | } 968 | }, { 969 | "id" : "2a3b1bb8-cde4-4b1f-8d56-f723fdb98013", 970 | "name" : "phone number", 971 | "protocol" : "openid-connect", 972 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 973 | "consentRequired" : false, 974 | "config" : { 975 | "userinfo.token.claim" : "true", 976 | "user.attribute" : "phoneNumber", 977 | "id.token.claim" : "true", 978 | "access.token.claim" : "true", 979 | "claim.name" : "phone_number", 980 | "jsonType.label" : "String" 981 | } 982 | } ] 983 | }, { 984 | "id" : "da8807ce-a9ce-4b25-befe-91a9116fb86c", 985 | "name" : "microprofile-jwt", 986 | "description" : "Microprofile - JWT built-in scope", 987 | "protocol" : "openid-connect", 988 | "attributes" : { 989 | "include.in.token.scope" : "true", 990 | "display.on.consent.screen" : "false" 991 | }, 992 | "protocolMappers" : [ { 993 | "id" : "85d74c3d-ed44-4553-b96a-ea4e264e74da", 994 | "name" : "groups", 995 | "protocol" : "openid-connect", 996 | "protocolMapper" : "oidc-usermodel-realm-role-mapper", 997 | "consentRequired" : false, 998 | "config" : { 999 | "multivalued" : "true", 1000 | "userinfo.token.claim" : "true", 1001 | "user.attribute" : "foo", 1002 | "id.token.claim" : "true", 1003 | "access.token.claim" : "true", 1004 | "claim.name" : "groups", 1005 | "jsonType.label" : "String" 1006 | } 1007 | }, { 1008 | "id" : "d4f6026f-87ee-49b5-99b0-d68768bbabf0", 1009 | "name" : "upn", 1010 | "protocol" : "openid-connect", 1011 | "protocolMapper" : "oidc-usermodel-property-mapper", 1012 | "consentRequired" : false, 1013 | "config" : { 1014 | "userinfo.token.claim" : "true", 1015 | "user.attribute" : "username", 1016 | "id.token.claim" : "true", 1017 | "access.token.claim" : "true", 1018 | "claim.name" : "upn", 1019 | "jsonType.label" : "String" 1020 | } 1021 | } ] 1022 | }, { 1023 | "id" : "0a691078-fc0f-4eed-a16c-bd5b6a0a0b6c", 1024 | "name" : "offline_access", 1025 | "description" : "OpenID Connect built-in scope: offline_access", 1026 | "protocol" : "openid-connect", 1027 | "attributes" : { 1028 | "consent.screen.text" : "${offlineAccessScopeConsentText}", 1029 | "display.on.consent.screen" : "true" 1030 | } 1031 | }, { 1032 | "id" : "a7445721-9c39-4c75-b693-e2bdbc3f9204", 1033 | "name" : "email", 1034 | "description" : "OpenID Connect built-in scope: email", 1035 | "protocol" : "openid-connect", 1036 | "attributes" : { 1037 | "include.in.token.scope" : "true", 1038 | "display.on.consent.screen" : "true", 1039 | "consent.screen.text" : "${emailScopeConsentText}" 1040 | }, 1041 | "protocolMappers" : [ { 1042 | "id" : "197718f1-0c47-4380-861e-1a0dfaa38a1e", 1043 | "name" : "email", 1044 | "protocol" : "openid-connect", 1045 | "protocolMapper" : "oidc-usermodel-property-mapper", 1046 | "consentRequired" : false, 1047 | "config" : { 1048 | "userinfo.token.claim" : "true", 1049 | "user.attribute" : "email", 1050 | "id.token.claim" : "true", 1051 | "access.token.claim" : "true", 1052 | "claim.name" : "email", 1053 | "jsonType.label" : "String" 1054 | } 1055 | }, { 1056 | "id" : "03daa671-977d-47e2-a3d5-a1158b901432", 1057 | "name" : "email verified", 1058 | "protocol" : "openid-connect", 1059 | "protocolMapper" : "oidc-usermodel-property-mapper", 1060 | "consentRequired" : false, 1061 | "config" : { 1062 | "userinfo.token.claim" : "true", 1063 | "user.attribute" : "emailVerified", 1064 | "id.token.claim" : "true", 1065 | "access.token.claim" : "true", 1066 | "claim.name" : "email_verified", 1067 | "jsonType.label" : "boolean" 1068 | } 1069 | } ] 1070 | }, { 1071 | "id" : "6953a40a-a838-4f81-a524-e91cfd9de2be", 1072 | "name" : "role_list", 1073 | "description" : "SAML role list", 1074 | "protocol" : "saml", 1075 | "attributes" : { 1076 | "consent.screen.text" : "${samlRoleListScopeConsentText}", 1077 | "display.on.consent.screen" : "true" 1078 | }, 1079 | "protocolMappers" : [ { 1080 | "id" : "e688e7ba-c45f-4db7-a978-6cc851a8fa10", 1081 | "name" : "role list", 1082 | "protocol" : "saml", 1083 | "protocolMapper" : "saml-role-list-mapper", 1084 | "consentRequired" : false, 1085 | "config" : { 1086 | "single" : "false", 1087 | "attribute.nameformat" : "Basic", 1088 | "attribute.name" : "Role" 1089 | } 1090 | } ] 1091 | } ], 1092 | "defaultDefaultClientScopes" : [ "roles", "role_list", "email", "profile", "web-origins" ], 1093 | "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], 1094 | "browserSecurityHeaders" : { 1095 | "contentSecurityPolicyReportOnly" : "", 1096 | "xContentTypeOptions" : "nosniff", 1097 | "xRobotsTag" : "none", 1098 | "xFrameOptions" : "SAMEORIGIN", 1099 | "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", 1100 | "xXSSProtection" : "1; mode=block", 1101 | "strictTransportSecurity" : "max-age=31536000; includeSubDomains" 1102 | }, 1103 | "smtpServer" : { }, 1104 | "eventsEnabled" : false, 1105 | "eventsListeners" : [ "jboss-logging" ], 1106 | "enabledEventTypes" : [ ], 1107 | "adminEventsEnabled" : false, 1108 | "adminEventsDetailsEnabled" : false, 1109 | "identityProviders" : [ ], 1110 | "identityProviderMappers" : [ ], 1111 | "components" : { 1112 | "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { 1113 | "id" : "28b277a7-1eb1-4b5e-a91f-feb8019aeda3", 1114 | "name" : "Trusted Hosts", 1115 | "providerId" : "trusted-hosts", 1116 | "subType" : "anonymous", 1117 | "subComponents" : { }, 1118 | "config" : { 1119 | "host-sending-registration-request-must-match" : [ "true" ], 1120 | "client-uris-must-match" : [ "true" ] 1121 | } 1122 | }, { 1123 | "id" : "152a6142-e70c-4898-9322-a24d8144b17e", 1124 | "name" : "Allowed Protocol Mapper Types", 1125 | "providerId" : "allowed-protocol-mappers", 1126 | "subType" : "authenticated", 1127 | "subComponents" : { }, 1128 | "config" : { 1129 | "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper" ] 1130 | } 1131 | }, { 1132 | "id" : "fd26fbaa-4e3e-4a5f-bb06-3b8964a39e92", 1133 | "name" : "Allowed Client Scopes", 1134 | "providerId" : "allowed-client-templates", 1135 | "subType" : "authenticated", 1136 | "subComponents" : { }, 1137 | "config" : { 1138 | "allow-default-scopes" : [ "true" ] 1139 | } 1140 | }, { 1141 | "id" : "8908dd5c-58ea-4c56-a1c9-bddffd6967ee", 1142 | "name" : "Full Scope Disabled", 1143 | "providerId" : "scope", 1144 | "subType" : "anonymous", 1145 | "subComponents" : { }, 1146 | "config" : { } 1147 | }, { 1148 | "id" : "08c068cd-b76a-4a76-a379-b20f67e3af98", 1149 | "name" : "Allowed Client Scopes", 1150 | "providerId" : "allowed-client-templates", 1151 | "subType" : "anonymous", 1152 | "subComponents" : { }, 1153 | "config" : { 1154 | "allow-default-scopes" : [ "true" ] 1155 | } 1156 | }, { 1157 | "id" : "cfccd784-81cb-4586-ad55-e339fe7db6d6", 1158 | "name" : "Max Clients Limit", 1159 | "providerId" : "max-clients", 1160 | "subType" : "anonymous", 1161 | "subComponents" : { }, 1162 | "config" : { 1163 | "max-clients" : [ "200" ] 1164 | } 1165 | }, { 1166 | "id" : "e2b3c6e6-460d-4fd4-9eb4-7ce605b18949", 1167 | "name" : "Consent Required", 1168 | "providerId" : "consent-required", 1169 | "subType" : "anonymous", 1170 | "subComponents" : { }, 1171 | "config" : { } 1172 | }, { 1173 | "id" : "d1c10cbc-b2e4-4983-b8b9-a4afacac1aad", 1174 | "name" : "Allowed Protocol Mapper Types", 1175 | "providerId" : "allowed-protocol-mappers", 1176 | "subType" : "anonymous", 1177 | "subComponents" : { }, 1178 | "config" : { 1179 | "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper" ] 1180 | } 1181 | } ], 1182 | "org.keycloak.keys.KeyProvider" : [ { 1183 | "id" : "acc0fe3c-987e-406b-8690-8c72230feb5a", 1184 | "name" : "rsa-enc-generated", 1185 | "providerId" : "rsa-generated", 1186 | "subComponents" : { }, 1187 | "config" : { 1188 | "privateKey" : [ "MIIEowIBAAKCAQEAoua885vBAbqI2/18hp28ZjLKgEg9c2k5jglqV87do+gq6BYPFW3mua0TrnVg7WGlYqWofoLuSHgmOlgTh32YKVuvEe/ooTB8s24A/wU+kuAZ8rJ9GxySMDhTu57aXO6bCS+qNiBAFrxIOQvtvRZX0Elc/hczzFBkf0MVniTp7pYmTCCndI94trMUTSWYiM0fRQCrLuPdolVPgVBb8iOHi6T0zAqVws6ZQi35wvEWQjpOaai/i1HRSi8cxT2Z+wnbsQ/lt9KQrP0JLqzBf9pvuzLoKY57R4Iqkv2EovxYngQUiqGsDwxuBjqoySSQ0G8ckS2YwrywJT6sz0f/9gP7AwIDAQABAoIBAHVtMsPAgO7IEiPSpFM9nTNmZYb3jCSHs49pxhip7CddEqDeVd0yreEmnEtkHYzAmMF1fPJFW5Mt2RCxsNI4fNlDV/3sHJslIp8NKvaqRyMFHG9QrtI+VDsiP2m95taawXNrwpzlvo0lCELnEGb/lil02O3ot9QOF4ONTiYDogXzXASvE0Y6YHuNUEjXWDu8N83CEy6/SbvqUm/qz0VTsmONdfvgW4X4Ik5EC6FQz6Ri2omTD2pPCNAvvX3NLcHWcYCuXdVurd1SsPUgIQpTQNHRLHp+kaGNsRn3JIuvXOTH8kzx6Q+s2Mv1cSRgLqCrjn+2Vi7BA4XZM2Jmm/JsYdECgYEA0RXd8ASpeGR8wr6IIdA8IwCZoJRY1cP0cXhSlrjtava6vRI87i569KL0ganFAPqcZRNF3zv2txOsgfSlwR2jYs0tNbFGzUtARTspgo/i08xpUdfzdqrZ3rbr7pInQUzC8HmAT8e13xB8yPUnTy4rQ4Y6SIQY4FbHd2xajJfH3F0CgYEAx3P/afjhJF47f/xGK5DSVwXvGuY4kvRjwlUWcREunQkY2vUryMYbTn3f5+/rzIIYUc/IY36KkOFgS0HE0MvG45UVn15y3JnzOVNjkOa7WDvmBiUna9SjbTWex0lfrpGzz1fH57WaP7Wft7JXbrFfN8Il+E/EmwAAWrmQp8j2vt8CgYBSqJduDRnGe8uK3fJJk95CZDlX723TTL21okDcT02lDNe0aaJm8z8uucF6BVjZ0znDB8SGxVj69a8sTUC+QO5X4A9we54nCQnmlMYLVMDyxIwajAo3LsIlNyG81lDokuU6Vn5wi96NyiHa/HhG6FS4RDeEdZy0C/qdbtu5W0PrDQKBgQCJsz39LcRSVXSCserIHR360q/3REZ+vpc5DYGG4jPqWuN8F5P48zza/fBdsrh64r5jjS5t8mk8CT1v96In/Of9K2pYMt7eCrNvOnEdEw74pbHzQCdtPMrvlPtf3vpeDV20oofzIt+xaHUyX8AZPf/dAP8x5fB4ipG0ETUbgZVIxQKBgFvV5nfPpXlr8axw7P85x3YgWpAbNXtsiX5mvZYlXxfoROSJ77kpFJGJEox/pnAiQpAQ3Y0FlhOgVyCXmnHeB87zWAUj9HYQk2VINMb4PQz2eo8IcqU54USplyxIW/Wls89AqUWAPqA6zKB+yqgHpE1WwEcae1ht/A2rCMTSoJka" ], 1189 | "keyUse" : [ "enc" ], 1190 | "certificate" : [ "MIICqTCCAZECBgF76vCDHTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1Qb2xhckJvb2tzaG9wMB4XDTIxMDkxNTE5Mjg1NVoXDTMxMDkxNTE5MzAzNVowGDEWMBQGA1UEAwwNUG9sYXJCb29rc2hvcDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKLmvPObwQG6iNv9fIadvGYyyoBIPXNpOY4JalfO3aPoKugWDxVt5rmtE651YO1hpWKlqH6C7kh4JjpYE4d9mClbrxHv6KEwfLNuAP8FPpLgGfKyfRsckjA4U7ue2lzumwkvqjYgQBa8SDkL7b0WV9BJXP4XM8xQZH9DFZ4k6e6WJkwgp3SPeLazFE0lmIjNH0UAqy7j3aJVT4FQW/Ijh4uk9MwKlcLOmUIt+cLxFkI6Tmmov4tR0UovHMU9mfsJ27EP5bfSkKz9CS6swX/ab7sy6CmOe0eCKpL9hKL8WJ4EFIqhrA8MbgY6qMkkkNBvHJEtmMK8sCU+rM9H//YD+wMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAHgSGP3x/36hUOrTsjMcFmaD0WMhm4eVxDzL2DLw4tFu1iErlalfQa5SrnFIE0mcjeeVb8Em1JXxKQF7uuLPPhRE/E0nm7rDYMiN2e2R8IyHvKub1pn319MA0RSuzj2kaJsg5N2yqv5SUeKHfnr6s9MU73x9I58XPMVmKQEId2++j4ZZawf2JuXVDg3dXP7Y94B5ordM1vCAU1sBGOZB1afUuatXOt0kj12pa8eC03cSrngA1rrvFvoJ3kOSLzdQCTFjPsP9XguGI6YjPANMSWZwPQrDQuJ1cgenHoERtJq8C07Bd0ufJEvKm4x0m2q+BpzXZpUEmOJWQP1Wv/NPxjQ==" ], 1191 | "priority" : [ "100" ] 1192 | } 1193 | }, { 1194 | "id" : "d3ae88da-7ff3-41f7-8057-c39934a4068b", 1195 | "name" : "rsa-generated", 1196 | "providerId" : "rsa-generated", 1197 | "subComponents" : { }, 1198 | "config" : { 1199 | "privateKey" : [ "MIIEowIBAAKCAQEApv3fIZ9lnvA3TRt7+OQw5ZLcM7mzhJJnP7kifUX0NmlLS7Ix6SY4qi/Pcawth/H/vZcvkuOiVELJlw5UEFbcZIObOecGPklz4+kC22xci9g6y4Xy/Mn6QZgMqikEghNRLzMTDBWK4906rL87lgTomgqmdQr5kr1x9W1c1F2MO7FSXkDONH0uhCa8SUT+t44D95fgasUBrZ5t4C5w/cGTXODMCoCpiMgkB48SysSVQW322377SW4SZbrkRvuZDuosucSe4OPj0QbN+6ibKSQnQe5l36eUcUKMnXUBaUSJnfzISyNBDMLaK0SSh6kmHs+yad9ayjon8b2USWNSznWnawIDAQABAoIBAAhyYP3/ZRGEDxgMYtn1GPAT3KWVKBp1mVlk0PTLPA4gh1l92v5zG3yoTHLwhaERwaDia9qfPw/KDQ5iACiPbDh9W7WzFOLDrmQB3k4hDhFYz6iJzv+wa8MzoLheuManmXystbLvyAW8pAIm7impd+aK0V6sPnGIuaHzdTWcwcInIfyDYa0++wHfWJ+CfqjMXmkf00AdnwfpR5eOhOmtDIyK28+NAnOM0Z6CxzmJWkjHdzs/VF248ToITOw3PDTBrn8Rg2R8uc2tlca+/MhOMbMbkQBrFkQz7j0hRWmq5oiDtHIConO7dL0c5GkGF7ye31wsTHslscHAi4VhTShS2EkCgYEA5k/KVwJIDqjs7NEpfcicLZQgiEC/DpYfrkNj9pJfLl7Iy1TaLynJXv/zm7USoVBtKIu1Wqo+rVYsTeNDt2/hcN5BSA5bo1+RgMvMYzGCsuY7xvo1DCZE1JAsLEPwO/Kdzaa036SeZHiinuBD/npHaE39QotYViKtI6yG0N3iz3UCgYEAuZ4Shap5EAlhAJhg1qFU5pFRyMMreC+kePbnUvFLhMNy3nenGHvvALLwM30htf87V2hhBIvurzLO14b/b6NeDDDEtoD5SUr4ZN6PkiSnjgz8NRl9cgeiypTGR+gQ3uWoRLkLQ/Sy02eZQx/nc5OmfGDyzU2eVSI2QkifdWGNn18CgYB/eZZzBk4/3vwUL8kZV9op2B0RjVSyjCFI4QAJUAbisY/OC08gX66XqG97tYN0SgHBOWLNpgE/C71F9w5aD9DmGf5kFZ5fFNyCP0sHdDX1Mz3O6lpGOUBLrujkL05ev/DcDPN7/a+1yJep/FNmHOQ9NT2CgYIJWvQeFLYiEU4iqQKBgQCH5Pfwjn91wzgWhHkZiiIrcCctUd6IbnD+8t0nN2uvtpxpzbYzUcEIhPkg6TL+GO/sLYoiFwTXld4joz1uEXAwgp/ycfiQtWeoCd2ZBGb7s/wOMCxMiNShdky4pml7ly4bqlJaHVOzPPzTTQ9Z6baRBzWcI4CRGawZ2SuNd6+FBwKBgDFxdpCCU1DOMZQ5qZnTfgSgiN3en8+12jriWqR8Z1ScObmz3C/hUqFyJG59hNeCSzSo3VNWYJQVCiAEHH5AjLynsP+bj82FCUE9Lw3J37yNfCuTh9eL+uAMV//Evt3RQLNMAxaVKowSgCP2nKtBP1uepwnMzSFEyaVQn1Ag9GZs" ], 1200 | "keyUse" : [ "sig" ], 1201 | "certificate" : [ "MIICqTCCAZECBgF76vCCSzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1Qb2xhckJvb2tzaG9wMB4XDTIxMDkxNTE5Mjg1NVoXDTMxMDkxNTE5MzAzNVowGDEWMBQGA1UEAwwNUG9sYXJCb29rc2hvcDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKb93yGfZZ7wN00be/jkMOWS3DO5s4SSZz+5In1F9DZpS0uyMekmOKovz3GsLYfx/72XL5LjolRCyZcOVBBW3GSDmznnBj5Jc+PpAttsXIvYOsuF8vzJ+kGYDKopBIITUS8zEwwViuPdOqy/O5YE6JoKpnUK+ZK9cfVtXNRdjDuxUl5AzjR9LoQmvElE/reOA/eX4GrFAa2ebeAucP3Bk1zgzAqAqYjIJAePEsrElUFt9tt++0luEmW65Eb7mQ7qLLnEnuDj49EGzfuomykkJ0HuZd+nlHFCjJ11AWlEiZ38yEsjQQzC2itEkoepJh7PsmnfWso6J/G9lEljUs51p2sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOb40qVhHAHHxrDl2WY2qVCMVtJV8U5DAFUqjDqx7n2SRNzNSTyJPGmshLaZBoFgX60bmZjsVBK9Mohw4iCDAaGLuyAGSsoh1lbGr15McAd+9TVkrR/z+UOv9u4+5ojHZSbHCMAv8yA7pDzWK63kwKSr5oPkoN9qG7LYEZPTXCrS9Hj7E2kGG7GgnaV7cj/j6Bm1OHMKO3tkfzJrIRK6RTyq4Sw00fqpIbHG9l0ogV8ToGEFi9u+Qe07kb/0zI2Go+FM8t7OTpexpyxPx+6EPL9PJbOQj0uMAIlE9G+QMJhUe6lm3nozGsFCRRC6j7oCiGYHwxW/Zzus+otlrXPFT8g==" ], 1202 | "priority" : [ "100" ] 1203 | } 1204 | }, { 1205 | "id" : "af733457-a921-415f-a16e-84bef8bdb0d7", 1206 | "name" : "hmac-generated", 1207 | "providerId" : "hmac-generated", 1208 | "subComponents" : { }, 1209 | "config" : { 1210 | "kid" : [ "2129fd32-9afb-4160-8445-6d6049df582f" ], 1211 | "secret" : [ "Wpnt7di-aREnWMaDWJZrf6Cudl0Xo_WX4vnzd7VZBFWiN83INYeJz3gay4tBd_BU15zGtrzp8sAqL_U6Lumseg" ], 1212 | "priority" : [ "100" ], 1213 | "algorithm" : [ "HS256" ] 1214 | } 1215 | }, { 1216 | "id" : "0e8e26d3-7214-4ff5-bdf5-1db6bd9cca4e", 1217 | "name" : "aes-generated", 1218 | "providerId" : "aes-generated", 1219 | "subComponents" : { }, 1220 | "config" : { 1221 | "kid" : [ "a75f962b-f538-4544-b6bc-0da5bea5b072" ], 1222 | "secret" : [ "IwMSJsIlPFSAkkeJ5FjYTw" ], 1223 | "priority" : [ "100" ] 1224 | } 1225 | } ] 1226 | }, 1227 | "internationalizationEnabled" : false, 1228 | "supportedLocales" : [ ], 1229 | "authenticationFlows" : [ { 1230 | "id" : "8a681513-96d3-487a-806d-c10f6fa30194", 1231 | "alias" : "Account verification options", 1232 | "description" : "Method with which to verity the existing account", 1233 | "providerId" : "basic-flow", 1234 | "topLevel" : false, 1235 | "builtIn" : true, 1236 | "authenticationExecutions" : [ { 1237 | "authenticator" : "idp-email-verification", 1238 | "authenticatorFlow" : false, 1239 | "requirement" : "ALTERNATIVE", 1240 | "priority" : 10, 1241 | "userSetupAllowed" : false, 1242 | "autheticatorFlow" : false 1243 | }, { 1244 | "authenticatorFlow" : true, 1245 | "requirement" : "ALTERNATIVE", 1246 | "priority" : 20, 1247 | "flowAlias" : "Verify Existing Account by Re-authentication", 1248 | "userSetupAllowed" : false, 1249 | "autheticatorFlow" : true 1250 | } ] 1251 | }, { 1252 | "id" : "06b2ab4e-11dd-4736-9c1f-455a812c933d", 1253 | "alias" : "Authentication Options", 1254 | "description" : "Authentication options.", 1255 | "providerId" : "basic-flow", 1256 | "topLevel" : false, 1257 | "builtIn" : true, 1258 | "authenticationExecutions" : [ { 1259 | "authenticator" : "basic-auth", 1260 | "authenticatorFlow" : false, 1261 | "requirement" : "REQUIRED", 1262 | "priority" : 10, 1263 | "userSetupAllowed" : false, 1264 | "autheticatorFlow" : false 1265 | }, { 1266 | "authenticator" : "basic-auth-otp", 1267 | "authenticatorFlow" : false, 1268 | "requirement" : "DISABLED", 1269 | "priority" : 20, 1270 | "userSetupAllowed" : false, 1271 | "autheticatorFlow" : false 1272 | }, { 1273 | "authenticator" : "auth-spnego", 1274 | "authenticatorFlow" : false, 1275 | "requirement" : "DISABLED", 1276 | "priority" : 30, 1277 | "userSetupAllowed" : false, 1278 | "autheticatorFlow" : false 1279 | } ] 1280 | }, { 1281 | "id" : "5a9cec8e-da5d-488c-94b9-8d01603de650", 1282 | "alias" : "Browser - Conditional OTP", 1283 | "description" : "Flow to determine if the OTP is required for the authentication", 1284 | "providerId" : "basic-flow", 1285 | "topLevel" : false, 1286 | "builtIn" : true, 1287 | "authenticationExecutions" : [ { 1288 | "authenticator" : "conditional-user-configured", 1289 | "authenticatorFlow" : false, 1290 | "requirement" : "REQUIRED", 1291 | "priority" : 10, 1292 | "userSetupAllowed" : false, 1293 | "autheticatorFlow" : false 1294 | }, { 1295 | "authenticator" : "auth-otp-form", 1296 | "authenticatorFlow" : false, 1297 | "requirement" : "REQUIRED", 1298 | "priority" : 20, 1299 | "userSetupAllowed" : false, 1300 | "autheticatorFlow" : false 1301 | } ] 1302 | }, { 1303 | "id" : "884700c9-97e2-49c9-a3c5-239488a8283f", 1304 | "alias" : "Direct Grant - Conditional OTP", 1305 | "description" : "Flow to determine if the OTP is required for the authentication", 1306 | "providerId" : "basic-flow", 1307 | "topLevel" : false, 1308 | "builtIn" : true, 1309 | "authenticationExecutions" : [ { 1310 | "authenticator" : "conditional-user-configured", 1311 | "authenticatorFlow" : false, 1312 | "requirement" : "REQUIRED", 1313 | "priority" : 10, 1314 | "userSetupAllowed" : false, 1315 | "autheticatorFlow" : false 1316 | }, { 1317 | "authenticator" : "direct-grant-validate-otp", 1318 | "authenticatorFlow" : false, 1319 | "requirement" : "REQUIRED", 1320 | "priority" : 20, 1321 | "userSetupAllowed" : false, 1322 | "autheticatorFlow" : false 1323 | } ] 1324 | }, { 1325 | "id" : "5cc1e8ec-77f5-4190-8b56-598aabf5cc1f", 1326 | "alias" : "First broker login - Conditional OTP", 1327 | "description" : "Flow to determine if the OTP is required for the authentication", 1328 | "providerId" : "basic-flow", 1329 | "topLevel" : false, 1330 | "builtIn" : true, 1331 | "authenticationExecutions" : [ { 1332 | "authenticator" : "conditional-user-configured", 1333 | "authenticatorFlow" : false, 1334 | "requirement" : "REQUIRED", 1335 | "priority" : 10, 1336 | "userSetupAllowed" : false, 1337 | "autheticatorFlow" : false 1338 | }, { 1339 | "authenticator" : "auth-otp-form", 1340 | "authenticatorFlow" : false, 1341 | "requirement" : "REQUIRED", 1342 | "priority" : 20, 1343 | "userSetupAllowed" : false, 1344 | "autheticatorFlow" : false 1345 | } ] 1346 | }, { 1347 | "id" : "9a1e9378-f838-4eff-88cb-56c40bfc7072", 1348 | "alias" : "Handle Existing Account", 1349 | "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", 1350 | "providerId" : "basic-flow", 1351 | "topLevel" : false, 1352 | "builtIn" : true, 1353 | "authenticationExecutions" : [ { 1354 | "authenticator" : "idp-confirm-link", 1355 | "authenticatorFlow" : false, 1356 | "requirement" : "REQUIRED", 1357 | "priority" : 10, 1358 | "userSetupAllowed" : false, 1359 | "autheticatorFlow" : false 1360 | }, { 1361 | "authenticatorFlow" : true, 1362 | "requirement" : "REQUIRED", 1363 | "priority" : 20, 1364 | "flowAlias" : "Account verification options", 1365 | "userSetupAllowed" : false, 1366 | "autheticatorFlow" : true 1367 | } ] 1368 | }, { 1369 | "id" : "5d52bbee-fa72-4b71-8cca-6a32acb7aacb", 1370 | "alias" : "Reset - Conditional OTP", 1371 | "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", 1372 | "providerId" : "basic-flow", 1373 | "topLevel" : false, 1374 | "builtIn" : true, 1375 | "authenticationExecutions" : [ { 1376 | "authenticator" : "conditional-user-configured", 1377 | "authenticatorFlow" : false, 1378 | "requirement" : "REQUIRED", 1379 | "priority" : 10, 1380 | "userSetupAllowed" : false, 1381 | "autheticatorFlow" : false 1382 | }, { 1383 | "authenticator" : "reset-otp", 1384 | "authenticatorFlow" : false, 1385 | "requirement" : "REQUIRED", 1386 | "priority" : 20, 1387 | "userSetupAllowed" : false, 1388 | "autheticatorFlow" : false 1389 | } ] 1390 | }, { 1391 | "id" : "48b114a6-4be6-4936-ad58-5af93b1754a9", 1392 | "alias" : "User creation or linking", 1393 | "description" : "Flow for the existing/non-existing user alternatives", 1394 | "providerId" : "basic-flow", 1395 | "topLevel" : false, 1396 | "builtIn" : true, 1397 | "authenticationExecutions" : [ { 1398 | "authenticatorConfig" : "create unique user config", 1399 | "authenticator" : "idp-create-user-if-unique", 1400 | "authenticatorFlow" : false, 1401 | "requirement" : "ALTERNATIVE", 1402 | "priority" : 10, 1403 | "userSetupAllowed" : false, 1404 | "autheticatorFlow" : false 1405 | }, { 1406 | "authenticatorFlow" : true, 1407 | "requirement" : "ALTERNATIVE", 1408 | "priority" : 20, 1409 | "flowAlias" : "Handle Existing Account", 1410 | "userSetupAllowed" : false, 1411 | "autheticatorFlow" : true 1412 | } ] 1413 | }, { 1414 | "id" : "c94ea5cd-d8fb-49ac-a7ff-b2c70df27643", 1415 | "alias" : "Verify Existing Account by Re-authentication", 1416 | "description" : "Reauthentication of existing account", 1417 | "providerId" : "basic-flow", 1418 | "topLevel" : false, 1419 | "builtIn" : true, 1420 | "authenticationExecutions" : [ { 1421 | "authenticator" : "idp-username-password-form", 1422 | "authenticatorFlow" : false, 1423 | "requirement" : "REQUIRED", 1424 | "priority" : 10, 1425 | "userSetupAllowed" : false, 1426 | "autheticatorFlow" : false 1427 | }, { 1428 | "authenticatorFlow" : true, 1429 | "requirement" : "CONDITIONAL", 1430 | "priority" : 20, 1431 | "flowAlias" : "First broker login - Conditional OTP", 1432 | "userSetupAllowed" : false, 1433 | "autheticatorFlow" : true 1434 | } ] 1435 | }, { 1436 | "id" : "496fa06c-b4c3-488f-8db0-38ced2c3d2cf", 1437 | "alias" : "browser", 1438 | "description" : "browser based authentication", 1439 | "providerId" : "basic-flow", 1440 | "topLevel" : true, 1441 | "builtIn" : true, 1442 | "authenticationExecutions" : [ { 1443 | "authenticator" : "auth-cookie", 1444 | "authenticatorFlow" : false, 1445 | "requirement" : "ALTERNATIVE", 1446 | "priority" : 10, 1447 | "userSetupAllowed" : false, 1448 | "autheticatorFlow" : false 1449 | }, { 1450 | "authenticator" : "auth-spnego", 1451 | "authenticatorFlow" : false, 1452 | "requirement" : "DISABLED", 1453 | "priority" : 20, 1454 | "userSetupAllowed" : false, 1455 | "autheticatorFlow" : false 1456 | }, { 1457 | "authenticator" : "identity-provider-redirector", 1458 | "authenticatorFlow" : false, 1459 | "requirement" : "ALTERNATIVE", 1460 | "priority" : 25, 1461 | "userSetupAllowed" : false, 1462 | "autheticatorFlow" : false 1463 | }, { 1464 | "authenticatorFlow" : true, 1465 | "requirement" : "ALTERNATIVE", 1466 | "priority" : 30, 1467 | "flowAlias" : "forms", 1468 | "userSetupAllowed" : false, 1469 | "autheticatorFlow" : true 1470 | } ] 1471 | }, { 1472 | "id" : "db19a77f-23c3-4a90-adca-4c5bb65f60ee", 1473 | "alias" : "clients", 1474 | "description" : "Base authentication for clients", 1475 | "providerId" : "client-flow", 1476 | "topLevel" : true, 1477 | "builtIn" : true, 1478 | "authenticationExecutions" : [ { 1479 | "authenticator" : "client-secret", 1480 | "authenticatorFlow" : false, 1481 | "requirement" : "ALTERNATIVE", 1482 | "priority" : 10, 1483 | "userSetupAllowed" : false, 1484 | "autheticatorFlow" : false 1485 | }, { 1486 | "authenticator" : "client-jwt", 1487 | "authenticatorFlow" : false, 1488 | "requirement" : "ALTERNATIVE", 1489 | "priority" : 20, 1490 | "userSetupAllowed" : false, 1491 | "autheticatorFlow" : false 1492 | }, { 1493 | "authenticator" : "client-secret-jwt", 1494 | "authenticatorFlow" : false, 1495 | "requirement" : "ALTERNATIVE", 1496 | "priority" : 30, 1497 | "userSetupAllowed" : false, 1498 | "autheticatorFlow" : false 1499 | }, { 1500 | "authenticator" : "client-x509", 1501 | "authenticatorFlow" : false, 1502 | "requirement" : "ALTERNATIVE", 1503 | "priority" : 40, 1504 | "userSetupAllowed" : false, 1505 | "autheticatorFlow" : false 1506 | } ] 1507 | }, { 1508 | "id" : "a2134c18-8c8a-4dcb-87e1-e2c76b5ffb91", 1509 | "alias" : "direct grant", 1510 | "description" : "OpenID Connect Resource Owner Grant", 1511 | "providerId" : "basic-flow", 1512 | "topLevel" : true, 1513 | "builtIn" : true, 1514 | "authenticationExecutions" : [ { 1515 | "authenticator" : "direct-grant-validate-username", 1516 | "authenticatorFlow" : false, 1517 | "requirement" : "REQUIRED", 1518 | "priority" : 10, 1519 | "userSetupAllowed" : false, 1520 | "autheticatorFlow" : false 1521 | }, { 1522 | "authenticator" : "direct-grant-validate-password", 1523 | "authenticatorFlow" : false, 1524 | "requirement" : "REQUIRED", 1525 | "priority" : 20, 1526 | "userSetupAllowed" : false, 1527 | "autheticatorFlow" : false 1528 | }, { 1529 | "authenticatorFlow" : true, 1530 | "requirement" : "CONDITIONAL", 1531 | "priority" : 30, 1532 | "flowAlias" : "Direct Grant - Conditional OTP", 1533 | "userSetupAllowed" : false, 1534 | "autheticatorFlow" : true 1535 | } ] 1536 | }, { 1537 | "id" : "12be718a-fa36-4111-a156-b9706d0c65da", 1538 | "alias" : "docker auth", 1539 | "description" : "Used by Docker clients to authenticate against the IDP", 1540 | "providerId" : "basic-flow", 1541 | "topLevel" : true, 1542 | "builtIn" : true, 1543 | "authenticationExecutions" : [ { 1544 | "authenticator" : "docker-http-basic-authenticator", 1545 | "authenticatorFlow" : false, 1546 | "requirement" : "REQUIRED", 1547 | "priority" : 10, 1548 | "userSetupAllowed" : false, 1549 | "autheticatorFlow" : false 1550 | } ] 1551 | }, { 1552 | "id" : "63b802c1-1bd0-4305-8892-2fd348baf129", 1553 | "alias" : "first broker login", 1554 | "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", 1555 | "providerId" : "basic-flow", 1556 | "topLevel" : true, 1557 | "builtIn" : true, 1558 | "authenticationExecutions" : [ { 1559 | "authenticatorConfig" : "review profile config", 1560 | "authenticator" : "idp-review-profile", 1561 | "authenticatorFlow" : false, 1562 | "requirement" : "REQUIRED", 1563 | "priority" : 10, 1564 | "userSetupAllowed" : false, 1565 | "autheticatorFlow" : false 1566 | }, { 1567 | "authenticatorFlow" : true, 1568 | "requirement" : "REQUIRED", 1569 | "priority" : 20, 1570 | "flowAlias" : "User creation or linking", 1571 | "userSetupAllowed" : false, 1572 | "autheticatorFlow" : true 1573 | } ] 1574 | }, { 1575 | "id" : "ac3a4785-7a53-4b9a-be86-e028e3bf084d", 1576 | "alias" : "forms", 1577 | "description" : "Username, password, otp and other auth forms.", 1578 | "providerId" : "basic-flow", 1579 | "topLevel" : false, 1580 | "builtIn" : true, 1581 | "authenticationExecutions" : [ { 1582 | "authenticator" : "auth-username-password-form", 1583 | "authenticatorFlow" : false, 1584 | "requirement" : "REQUIRED", 1585 | "priority" : 10, 1586 | "userSetupAllowed" : false, 1587 | "autheticatorFlow" : false 1588 | }, { 1589 | "authenticatorFlow" : true, 1590 | "requirement" : "CONDITIONAL", 1591 | "priority" : 20, 1592 | "flowAlias" : "Browser - Conditional OTP", 1593 | "userSetupAllowed" : false, 1594 | "autheticatorFlow" : true 1595 | } ] 1596 | }, { 1597 | "id" : "4bc694fd-17b6-49cd-ac2e-efba826ffde3", 1598 | "alias" : "http challenge", 1599 | "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", 1600 | "providerId" : "basic-flow", 1601 | "topLevel" : true, 1602 | "builtIn" : true, 1603 | "authenticationExecutions" : [ { 1604 | "authenticator" : "no-cookie-redirect", 1605 | "authenticatorFlow" : false, 1606 | "requirement" : "REQUIRED", 1607 | "priority" : 10, 1608 | "userSetupAllowed" : false, 1609 | "autheticatorFlow" : false 1610 | }, { 1611 | "authenticatorFlow" : true, 1612 | "requirement" : "REQUIRED", 1613 | "priority" : 20, 1614 | "flowAlias" : "Authentication Options", 1615 | "userSetupAllowed" : false, 1616 | "autheticatorFlow" : true 1617 | } ] 1618 | }, { 1619 | "id" : "779b6ee2-32e1-4a2c-ae72-a920e1f0ae40", 1620 | "alias" : "registration", 1621 | "description" : "registration flow", 1622 | "providerId" : "basic-flow", 1623 | "topLevel" : true, 1624 | "builtIn" : true, 1625 | "authenticationExecutions" : [ { 1626 | "authenticator" : "registration-page-form", 1627 | "authenticatorFlow" : true, 1628 | "requirement" : "REQUIRED", 1629 | "priority" : 10, 1630 | "flowAlias" : "registration form", 1631 | "userSetupAllowed" : false, 1632 | "autheticatorFlow" : true 1633 | } ] 1634 | }, { 1635 | "id" : "b2c5a86d-8802-4d25-8973-47c7346a3d6a", 1636 | "alias" : "registration form", 1637 | "description" : "registration form", 1638 | "providerId" : "form-flow", 1639 | "topLevel" : false, 1640 | "builtIn" : true, 1641 | "authenticationExecutions" : [ { 1642 | "authenticator" : "registration-user-creation", 1643 | "authenticatorFlow" : false, 1644 | "requirement" : "REQUIRED", 1645 | "priority" : 20, 1646 | "userSetupAllowed" : false, 1647 | "autheticatorFlow" : false 1648 | }, { 1649 | "authenticator" : "registration-profile-action", 1650 | "authenticatorFlow" : false, 1651 | "requirement" : "REQUIRED", 1652 | "priority" : 40, 1653 | "userSetupAllowed" : false, 1654 | "autheticatorFlow" : false 1655 | }, { 1656 | "authenticator" : "registration-password-action", 1657 | "authenticatorFlow" : false, 1658 | "requirement" : "REQUIRED", 1659 | "priority" : 50, 1660 | "userSetupAllowed" : false, 1661 | "autheticatorFlow" : false 1662 | }, { 1663 | "authenticator" : "registration-recaptcha-action", 1664 | "authenticatorFlow" : false, 1665 | "requirement" : "DISABLED", 1666 | "priority" : 60, 1667 | "userSetupAllowed" : false, 1668 | "autheticatorFlow" : false 1669 | } ] 1670 | }, { 1671 | "id" : "d90970da-c727-4f9e-8d63-6a7c3e488bce", 1672 | "alias" : "reset credentials", 1673 | "description" : "Reset credentials for a user if they forgot their password or something", 1674 | "providerId" : "basic-flow", 1675 | "topLevel" : true, 1676 | "builtIn" : true, 1677 | "authenticationExecutions" : [ { 1678 | "authenticator" : "reset-credentials-choose-user", 1679 | "authenticatorFlow" : false, 1680 | "requirement" : "REQUIRED", 1681 | "priority" : 10, 1682 | "userSetupAllowed" : false, 1683 | "autheticatorFlow" : false 1684 | }, { 1685 | "authenticator" : "reset-credential-email", 1686 | "authenticatorFlow" : false, 1687 | "requirement" : "REQUIRED", 1688 | "priority" : 20, 1689 | "userSetupAllowed" : false, 1690 | "autheticatorFlow" : false 1691 | }, { 1692 | "authenticator" : "reset-password", 1693 | "authenticatorFlow" : false, 1694 | "requirement" : "REQUIRED", 1695 | "priority" : 30, 1696 | "userSetupAllowed" : false, 1697 | "autheticatorFlow" : false 1698 | }, { 1699 | "authenticatorFlow" : true, 1700 | "requirement" : "CONDITIONAL", 1701 | "priority" : 40, 1702 | "flowAlias" : "Reset - Conditional OTP", 1703 | "userSetupAllowed" : false, 1704 | "autheticatorFlow" : true 1705 | } ] 1706 | }, { 1707 | "id" : "bdf533f8-14c7-4b9e-b975-0099b2caebca", 1708 | "alias" : "saml ecp", 1709 | "description" : "SAML ECP Profile Authentication Flow", 1710 | "providerId" : "basic-flow", 1711 | "topLevel" : true, 1712 | "builtIn" : true, 1713 | "authenticationExecutions" : [ { 1714 | "authenticator" : "http-basic-authenticator", 1715 | "authenticatorFlow" : false, 1716 | "requirement" : "REQUIRED", 1717 | "priority" : 10, 1718 | "userSetupAllowed" : false, 1719 | "autheticatorFlow" : false 1720 | } ] 1721 | } ], 1722 | "authenticatorConfig" : [ { 1723 | "id" : "4c9baaa5-3a90-4f90-ba1a-1e6153c30d67", 1724 | "alias" : "create unique user config", 1725 | "config" : { 1726 | "require.password.update.after.registration" : "false" 1727 | } 1728 | }, { 1729 | "id" : "dbdc9255-a7e3-4223-83ba-c6a9187e267c", 1730 | "alias" : "review profile config", 1731 | "config" : { 1732 | "update.profile.on.first.login" : "missing" 1733 | } 1734 | } ], 1735 | "requiredActions" : [ { 1736 | "alias" : "CONFIGURE_TOTP", 1737 | "name" : "Configure OTP", 1738 | "providerId" : "CONFIGURE_TOTP", 1739 | "enabled" : true, 1740 | "defaultAction" : false, 1741 | "priority" : 10, 1742 | "config" : { } 1743 | }, { 1744 | "alias" : "terms_and_conditions", 1745 | "name" : "Terms and Conditions", 1746 | "providerId" : "terms_and_conditions", 1747 | "enabled" : false, 1748 | "defaultAction" : false, 1749 | "priority" : 20, 1750 | "config" : { } 1751 | }, { 1752 | "alias" : "UPDATE_PASSWORD", 1753 | "name" : "Update Password", 1754 | "providerId" : "UPDATE_PASSWORD", 1755 | "enabled" : true, 1756 | "defaultAction" : false, 1757 | "priority" : 30, 1758 | "config" : { } 1759 | }, { 1760 | "alias" : "UPDATE_PROFILE", 1761 | "name" : "Update Profile", 1762 | "providerId" : "UPDATE_PROFILE", 1763 | "enabled" : true, 1764 | "defaultAction" : false, 1765 | "priority" : 40, 1766 | "config" : { } 1767 | }, { 1768 | "alias" : "VERIFY_EMAIL", 1769 | "name" : "Verify Email", 1770 | "providerId" : "VERIFY_EMAIL", 1771 | "enabled" : true, 1772 | "defaultAction" : false, 1773 | "priority" : 50, 1774 | "config" : { } 1775 | }, { 1776 | "alias" : "delete_account", 1777 | "name" : "Delete Account", 1778 | "providerId" : "delete_account", 1779 | "enabled" : false, 1780 | "defaultAction" : false, 1781 | "priority" : 60, 1782 | "config" : { } 1783 | }, { 1784 | "alias" : "update_user_locale", 1785 | "name" : "Update User Locale", 1786 | "providerId" : "update_user_locale", 1787 | "enabled" : true, 1788 | "defaultAction" : false, 1789 | "priority" : 1000, 1790 | "config" : { } 1791 | } ], 1792 | "browserFlow" : "browser", 1793 | "registrationFlow" : "registration", 1794 | "directGrantFlow" : "direct grant", 1795 | "resetCredentialsFlow" : "reset credentials", 1796 | "clientAuthenticationFlow" : "clients", 1797 | "dockerAuthenticationFlow" : "docker auth", 1798 | "attributes" : { 1799 | "cibaBackchannelTokenDeliveryMode" : "poll", 1800 | "cibaExpiresIn" : "120", 1801 | "cibaAuthRequestedUserHint" : "login_hint", 1802 | "oauth2DeviceCodeLifespan" : "600", 1803 | "clientOfflineSessionMaxLifespan" : "0", 1804 | "oauth2DevicePollingInterval" : "5", 1805 | "clientSessionIdleTimeout" : "0", 1806 | "clientSessionMaxLifespan" : "0", 1807 | "parRequestUriLifespan" : "60", 1808 | "clientOfflineSessionIdleTimeout" : "0", 1809 | "cibaInterval" : "5" 1810 | }, 1811 | "keycloakVersion" : "15.0.1", 1812 | "userManagedAccessAllowed" : false, 1813 | "clientProfiles" : { 1814 | "profiles" : [ ] 1815 | }, 1816 | "clientPolicies" : { 1817 | "policies" : [ ] 1818 | } 1819 | } --------------------------------------------------------------------------------