├── 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 |
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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------