diff --git a/samples/tutorials/bff/angular/bff-gateway.openapi.json b/samples/tutorials/bff/angular/bff-gateway.openapi.json index 7dcbb2a8c..2741a7c13 100644 --- a/samples/tutorials/bff/angular/bff-gateway.openapi.json +++ b/samples/tutorials/bff/angular/bff-gateway.openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{"/logout":{"put":{"tags":["logout","Gateway"],"operationId":"logout","responses":{"204":{"description":"No Content"}}}},"/backchannel_logout":{"post":{"tags":["back-channel-logout-controller"],"operationId":"backChannelLogout","responses":{"200":{"description":"OK"}}}},"/me":{"get":{"tags":["getMe","Gateway"],"operationId":"getMe","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDto"}}}}}}},"/login-options":{"get":{"tags":["Gateway","getLoginOptions"],"operationId":"getLoginOptions","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LoginOptionDto"}}}}}}}}},"components":{"schemas":{"UserDto":{"type":"object","properties":{"subject":{"type":"string"},"issuer":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}}}},"LoginOptionDto":{"required":["label","loginUri"],"type":"object","properties":{"label":{"type":"string"},"loginUri":{"type":"string"}}}}}} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{"/logout":{"put":{"tags":["logout","Gateway"],"operationId":"logout","responses":{"204":{"description":"No Content"}}}},"/login-options":{"get":{"tags":["Gateway","getLoginOptions"],"operationId":"getLoginOptions","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LoginOptionDto"}}}}}}}}},"components":{"schemas":{"LoginOptionDto":{"required":["label","loginUri"],"type":"object","properties":{"label":{"type":"string"},"loginUri":{"type":"string"}}}}}} \ No newline at end of file diff --git a/samples/tutorials/bff/angular/bff-greetings-api.openapi.json b/samples/tutorials/bff/angular/bff-greetings-api.openapi.json index 101d62312..6552100d7 100644 --- a/samples/tutorials/bff/angular/bff-greetings-api.openapi.json +++ b/samples/tutorials/bff/angular/bff-greetings-api.openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"Users API","version":"1.0.0"},"servers":[{"url":"http://localhost:6443","description":"Generated server url"}],"security":[{"OAuth2":[]}],"paths":{"/greetings":{"get":{"tags":["get","Greetings"],"operationId":"getGreeting","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GreetingDto"}}}}}}}},"components":{"schemas":{"GreetingDto":{"required":["message"],"type":"object","properties":{"message":{"type":"string"}}}}}} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Users API","version":"1.0.0"},"servers":[{"url":"http://localhost:7443","description":"Generated server url"}],"security":[{"OAuth2":[]}],"paths":{"/users/me":{"get":{"tags":["getMe","Users"],"operationId":"getMe","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}}}}},"/greetings/public":{"get":{"tags":["getPublicGreeting","Greetings"],"operationId":"getGreeting","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GreetingDto"}}}}}}},"/greetings/nice":{"get":{"tags":["getNiceGreeting","Greetings"],"operationId":"getNiceGreeting","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GreetingDto"}}}}}}}},"components":{"schemas":{"UserInfo":{"required":["email","exp","iss","name","roles"],"type":"object","properties":{"name":{"type":"string"},"iss":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"exp":{"minimum":0,"type":"integer","format":"int64"},"email":{"type":"string"}}},"GreetingDto":{"required":["message"],"type":"object","properties":{"message":{"type":"string"}}}}}} \ No newline at end of file diff --git a/samples/tutorials/bff/angular/projects/bff-ui/src/app/user.service.ts b/samples/tutorials/bff/angular/projects/bff-ui/src/app/user.service.ts index e0bf16fdd..8ce17464d 100644 --- a/samples/tutorials/bff/angular/projects/bff-ui/src/app/user.service.ts +++ b/samples/tutorials/bff/angular/projects/bff-ui/src/app/user.service.ts @@ -1,33 +1,42 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http' import { GatewayApi, LoginOptionDto } from '@c4-soft/gateway-api'; +import { UsersApi } from '@c4-soft/greetings-api'; +import { interval } from 'rxjs'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { lastValueFrom } from 'rxjs/internal/lastValueFrom'; import { Observable } from 'rxjs/internal/Observable'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { lastValueFrom } from 'rxjs/internal/lastValueFrom'; @Injectable({ providedIn: 'root', }) export class UserService { private user$ = new BehaviorSubject(User.ANONYMOUS); + private refreshSub?: Subscription; - constructor(private gatewayApi: GatewayApi, private http: HttpClient) { + constructor( + private gatewayApi: GatewayApi, + private usersApi: UsersApi, + private http: HttpClient + ) { this.refresh(); } refresh(): void { - this.gatewayApi.getMe().subscribe({ + this.refreshSub?.unsubscribe() + this.usersApi.getMe().subscribe({ next: (user) => { - console.info(user); this.user$.next( - user.subject - ? new User( - user.subject, - user.issuer || '', - user.roles || [] - ) + user.name + ? new User(user.name, user.iss || '', user.roles || []) : User.ANONYMOUS ); + const now = Date.now(); + const delay = (1000 * user.exp - now) * 0.8; + if (delay > 10000) { + this.refreshSub = interval(delay).subscribe(() => this.refresh()); + } }, error: (error) => { console.warn(error); @@ -41,14 +50,16 @@ export class UserService { } async logout() { - lastValueFrom(this.gatewayApi.logout('response')).then((resp) => { - const logoutUri = resp.headers.get('location') || ''; - if (logoutUri) { - window.location.href = logoutUri; - } - }).finally(() => { - this.user$.next(User.ANONYMOUS) - }); + lastValueFrom(this.gatewayApi.logout('response')) + .then((resp) => { + const logoutUri = resp.headers.get('location') || ''; + if (logoutUri) { + window.location.href = logoutUri; + } + }) + .finally(() => { + this.user$.next(User.ANONYMOUS); + }); } get loginOptions(): Observable> { diff --git a/samples/tutorials/bff/angular/projects/c4-soft/gateway-api/.openapi-generator/FILES b/samples/tutorials/bff/angular/projects/c4-soft/gateway-api/.openapi-generator/FILES index 06aabd281..c547f8d23 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/gateway-api/.openapi-generator/FILES +++ b/samples/tutorials/bff/angular/projects/c4-soft/gateway-api/.openapi-generator/FILES @@ -1,14 +1,10 @@ README.md api.module.ts api/api.ts -api/backChannelLogoutController.service.ts -api/backChannelLogoutController.serviceInterface.ts api/gateway.service.ts api/gateway.serviceInterface.ts api/getLoginOptions.service.ts api/getLoginOptions.serviceInterface.ts -api/getMe.service.ts -api/getMe.serviceInterface.ts api/logout.service.ts api/logout.serviceInterface.ts configuration.ts @@ -16,7 +12,6 @@ encoder.ts index.ts model/loginOptionDto.ts model/models.ts -model/userDto.ts package.json param.ts variables.ts diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.gitignore b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.gitignore index 149b57654..449e168f5 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.gitignore +++ b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.gitignore @@ -2,3 +2,18 @@ wwwroot/*.js node_modules typings dist + +api/ +model/ + +api.module.ts +configuration.ts +encoder.ts +git_push.sh +index.ts +karma.conf.js +package.json +package-lock.json +param.ts +README.md +variables.ts diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.openapi-generator/FILES b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.openapi-generator/FILES index 82a11ad0f..60fa6e4de 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.openapi-generator/FILES +++ b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/.openapi-generator/FILES @@ -1,16 +1,23 @@ README.md api.module.ts api/api.ts -api/get.service.ts -api/get.serviceInterface.ts +api/getMe.service.ts +api/getMe.serviceInterface.ts +api/getNiceGreeting.service.ts +api/getNiceGreeting.serviceInterface.ts +api/getPublicGreeting.service.ts +api/getPublicGreeting.serviceInterface.ts api/greetings.service.ts api/greetings.serviceInterface.ts +api/users.service.ts +api/users.serviceInterface.ts configuration.ts encoder.ts git_push.sh index.ts model/greetingDto.ts model/models.ts +model/userInfo.ts package.json param.ts variables.ts diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/api.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/api.ts index 6d848f72e..69fe88583 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/api.ts +++ b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/api.ts @@ -1,7 +1,16 @@ -export * from './get.service'; -import { GetApi } from './get.service'; -export * from './get.serviceInterface'; +export * from './getMe.service'; +import { GetMeApi } from './getMe.service'; +export * from './getMe.serviceInterface'; +export * from './getNiceGreeting.service'; +import { GetNiceGreetingApi } from './getNiceGreeting.service'; +export * from './getNiceGreeting.serviceInterface'; +export * from './getPublicGreeting.service'; +import { GetPublicGreetingApi } from './getPublicGreeting.service'; +export * from './getPublicGreeting.serviceInterface'; export * from './greetings.service'; import { GreetingsApi } from './greetings.service'; export * from './greetings.serviceInterface'; -export const APIS = [GetApi, GreetingsApi]; +export * from './users.service'; +import { UsersApi } from './users.service'; +export * from './users.serviceInterface'; +export const APIS = [GetMeApi, GetNiceGreetingApi, GetPublicGreetingApi, GreetingsApi, UsersApi]; diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/get.service.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/get.service.ts deleted file mode 100644 index 13b565c03..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/get.service.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -/* tslint:disable:no-unused-variable member-ordering */ - -import { Inject, Injectable, Optional } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams, - HttpResponse, HttpEvent, HttpParameterCodec, HttpContext - } from '@angular/common/http'; -import { CustomHttpParameterCodec } from '../encoder'; -import { Observable } from 'rxjs'; - -// @ts-ignore -import { GreetingDto } from '../model/greetingDto'; - -// @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; -import { Configuration } from '../configuration'; -import { - GetApiInterface -} from './get.serviceInterface'; - - - -@Injectable({ - providedIn: 'root' -}) -export class GetApi implements GetApiInterface { - - protected basePath = 'http://localhost:6443'; - public defaultHeaders = new HttpHeaders(); - public configuration = new Configuration(); - public encoder: HttpParameterCodec; - - constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { - if (configuration) { - this.configuration = configuration; - } - if (typeof this.configuration.basePath !== 'string') { - if (Array.isArray(basePath) && basePath.length > 0) { - basePath = basePath[0]; - } - - if (typeof basePath !== 'string') { - basePath = this.basePath; - } - this.configuration.basePath = basePath; - } - this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); - } - - - // @ts-ignore - private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { - if (typeof value === "object" && value instanceof Date === false) { - httpParams = this.addToHttpParamsRecursive(httpParams, value); - } else { - httpParams = this.addToHttpParamsRecursive(httpParams, value, key); - } - return httpParams; - } - - private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { - if (value == null) { - return httpParams; - } - - if (typeof value === "object") { - if (Array.isArray(value)) { - (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); - } else if (value instanceof Date) { - if (key != null) { - httpParams = httpParams.append(key, (value as Date).toISOString().substr(0, 10)); - } else { - throw Error("key may not be null if value is Date"); - } - } else { - Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( - httpParams, value[k], key != null ? `${key}.${k}` : k)); - } - } else if (key != null) { - httpParams = httpParams.append(key, value); - } else { - throw Error("key may not be null if value is not object or array"); - } - return httpParams; - } - - /** - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public getGreeting(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; - public getGreeting(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public getGreeting(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public getGreeting(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { - - let localVarHeaders = this.defaultHeaders; - - let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; - if (localVarHttpHeaderAcceptSelected === undefined) { - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); - } - if (localVarHttpHeaderAcceptSelected !== undefined) { - localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); - } - - let localVarHttpContext: HttpContext | undefined = options && options.context; - if (localVarHttpContext === undefined) { - localVarHttpContext = new HttpContext(); - } - - - let responseType_: 'text' | 'json' | 'blob' = 'json'; - if (localVarHttpHeaderAcceptSelected) { - if (localVarHttpHeaderAcceptSelected.startsWith('text')) { - responseType_ = 'text'; - } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { - responseType_ = 'json'; - } else { - responseType_ = 'blob'; - } - } - - let localVarPath = `/greetings`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, - { - context: localVarHttpContext, - responseType: responseType_, - withCredentials: this.configuration.withCredentials, - headers: localVarHeaders, - observe: observe, - reportProgress: reportProgress - } - ); - } - -} diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/get.serviceInterface.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/get.serviceInterface.ts deleted file mode 100644 index 63b45d7a4..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/get.serviceInterface.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { HttpHeaders } from '@angular/common/http'; - -import { Observable } from 'rxjs'; - -import { GreetingDto } from '../model/models'; - - -import { Configuration } from '../configuration'; - - - -export interface GetApiInterface { - defaultHeaders: HttpHeaders; - configuration: Configuration; - - /** - * - * - */ - getGreeting(extraHttpRequestParams?: any): Observable; - -} diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.service.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.service.ts index 1ada8ba10..ef8faee2d 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.service.ts +++ b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.service.ts @@ -35,7 +35,7 @@ import { }) export class GreetingsApi implements GreetingsApiInterface { - protected basePath = 'http://localhost:6443'; + protected basePath = 'http://localhost:7443'; public defaultHeaders = new HttpHeaders(); public configuration = new Configuration(); public encoder: HttpParameterCodec; @@ -134,7 +134,60 @@ export class GreetingsApi implements GreetingsApiInterface { } } - let localVarPath = `/greetings`; + let localVarPath = `/greetings/public`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getNiceGreeting(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public getNiceGreeting(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public getNiceGreeting(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public getNiceGreeting(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/greetings/nice`; return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.serviceInterface.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.serviceInterface.ts index faf0253f1..b3a08ffcd 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.serviceInterface.ts +++ b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/greetings.serviceInterface.ts @@ -30,4 +30,10 @@ export interface GreetingsApiInterface { */ getGreeting(extraHttpRequestParams?: any): Observable; + /** + * + * + */ + getNiceGreeting(extraHttpRequestParams?: any): Observable; + } diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/usersController.service.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/usersController.service.ts deleted file mode 100644 index 5cf59cbb7..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/usersController.service.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -/* tslint:disable:no-unused-variable member-ordering */ - -import { Inject, Injectable, Optional } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams, - HttpResponse, HttpEvent, HttpParameterCodec, HttpContext - } from '@angular/common/http'; -import { CustomHttpParameterCodec } from '../encoder'; -import { Observable } from 'rxjs'; - -// @ts-ignore -import { GreetingDto } from '../model/greetingDto'; - -// @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; -import { Configuration } from '../configuration'; -import { - UsersControllerApiInterface -} from './usersController.serviceInterface'; - - - -@Injectable({ - providedIn: 'root' -}) -export class UsersControllerApi implements UsersControllerApiInterface { - - protected basePath = 'http://localhost:6443'; - public defaultHeaders = new HttpHeaders(); - public configuration = new Configuration(); - public encoder: HttpParameterCodec; - - constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { - if (configuration) { - this.configuration = configuration; - } - if (typeof this.configuration.basePath !== 'string') { - if (Array.isArray(basePath) && basePath.length > 0) { - basePath = basePath[0]; - } - - if (typeof basePath !== 'string') { - basePath = this.basePath; - } - this.configuration.basePath = basePath; - } - this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); - } - - - // @ts-ignore - private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { - if (typeof value === "object" && value instanceof Date === false) { - httpParams = this.addToHttpParamsRecursive(httpParams, value); - } else { - httpParams = this.addToHttpParamsRecursive(httpParams, value, key); - } - return httpParams; - } - - private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { - if (value == null) { - return httpParams; - } - - if (typeof value === "object") { - if (Array.isArray(value)) { - (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); - } else if (value instanceof Date) { - if (key != null) { - httpParams = httpParams.append(key, (value as Date).toISOString().substr(0, 10)); - } else { - throw Error("key may not be null if value is Date"); - } - } else { - Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( - httpParams, value[k], key != null ? `${key}.${k}` : k)); - } - } else if (key != null) { - httpParams = httpParams.append(key, value); - } else { - throw Error("key may not be null if value is not object or array"); - } - return httpParams; - } - - /** - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public getGreeting(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; - public getGreeting(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public getGreeting(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public getGreeting(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { - - let localVarHeaders = this.defaultHeaders; - - let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; - if (localVarHttpHeaderAcceptSelected === undefined) { - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); - } - if (localVarHttpHeaderAcceptSelected !== undefined) { - localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); - } - - let localVarHttpContext: HttpContext | undefined = options && options.context; - if (localVarHttpContext === undefined) { - localVarHttpContext = new HttpContext(); - } - - - let responseType_: 'text' | 'json' | 'blob' = 'json'; - if (localVarHttpHeaderAcceptSelected) { - if (localVarHttpHeaderAcceptSelected.startsWith('text')) { - responseType_ = 'text'; - } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { - responseType_ = 'json'; - } else { - responseType_ = 'blob'; - } - } - - let localVarPath = `/greetings`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, - { - context: localVarHttpContext, - responseType: responseType_, - withCredentials: this.configuration.withCredentials, - headers: localVarHeaders, - observe: observe, - reportProgress: reportProgress - } - ); - } - -} diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/usersController.serviceInterface.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/usersController.serviceInterface.ts deleted file mode 100644 index 3e1776bf2..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/api/usersController.serviceInterface.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { HttpHeaders } from '@angular/common/http'; - -import { Observable } from 'rxjs'; - -import { GreetingDto } from '../model/models'; - - -import { Configuration } from '../configuration'; - - - -export interface UsersControllerApiInterface { - defaultHeaders: HttpHeaders; - configuration: Configuration; - - /** - * - * - */ - getGreeting(extraHttpRequestParams?: any): Observable; - -} diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/addressStandardClaim.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/addressStandardClaim.ts deleted file mode 100644 index 4b7871d79..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/addressStandardClaim.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -export interface AddressStandardClaim { - country?: string; - region?: string; - locality?: string; - streetAddress?: string; - formatted?: string; - postalCode?: string; -} - diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/models.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/models.ts index f288de58b..0024fbc60 100644 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/models.ts +++ b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/models.ts @@ -1 +1,2 @@ export * from './greetingDto'; +export * from './userInfo'; diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/openidClaimSet.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/openidClaimSet.ts deleted file mode 100644 index cf0de7335..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/openidClaimSet.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { AddressStandardClaim } from './addressStandardClaim'; - - -export interface OpenidClaimSet { - [key: string]: object | any; - - - delegate?: { [key: string]: object; }; - name?: string; - claims?: { [key: string]: object; }; - nonce?: string; - issuer?: string; - subject?: string; - authenticatedAt?: string; - authorizedParty?: string; - accessTokenHash?: string; - expiresAt?: string; - issuedAt?: string; - audience?: Array; - authenticationContextClass?: string; - authorizationCodeHash?: string; - authenticationMethods?: Array; - address?: AddressStandardClaim; - locale?: string; - fullName?: string; - zoneInfo?: string; - givenName?: string; - phoneNumber?: string; - nickName?: string; - updatedAt?: string; - gender?: string; - emailVerified?: boolean; - middleName?: string; - birthdate?: string; - website?: string; - familyName?: string; - picture?: string; - profile?: string; - email?: string; - preferredUsername?: string; - phoneNumberVerified?: boolean; - empty?: boolean; -} - diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/userDto.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/userDto.ts deleted file mode 100644 index 927702d2e..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/model/userDto.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Users API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -export interface UserDto { - username?: string; - roles?: Array; - email?: string; -} - diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.component.spec.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.component.spec.ts deleted file mode 100644 index 51c389840..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { UsersApiComponent } from './greetings-api.component'; - -describe('UsersApiComponent', () => { - let component: UsersApiComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ UsersApiComponent ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(UsersApiComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.component.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.component.ts deleted file mode 100644 index f9f04fa73..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'lib-greetings-api', - template: ` -

- greetings-api works! -

- `, - styles: [ - ] -}) -export class UsersApiComponent { - -} diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.module.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.module.ts deleted file mode 100644 index 74ac5bb01..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import { UsersApiComponent } from './greetings-api.component'; - - - -@NgModule({ - declarations: [ - UsersApiComponent - ], - imports: [ - ], - exports: [ - UsersApiComponent - ] -}) -export class UsersApiModule { } diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.service.spec.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.service.spec.ts deleted file mode 100644 index 521435c45..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { UsersApiService } from './greetings-api.service'; - -describe('UsersApiService', () => { - let service: UsersApiService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(UsersApiService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.service.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.service.ts deleted file mode 100644 index 6442f56b8..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/lib/users-api.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class UsersApiService { - - constructor() { } -} diff --git a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/public-api.ts b/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/public-api.ts deleted file mode 100644 index d6a0a9357..000000000 --- a/samples/tutorials/bff/angular/projects/c4-soft/greetings-api/src/public-api.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Public API Surface of greetings-api - */ - -export * from './lib/greetings-api.service'; -export * from './lib/greetings-api.component'; -export * from './lib/greetings-api.module'; diff --git a/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java b/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java index cdb1d3288..48786207a 100644 --- a/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java +++ b/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java @@ -2,14 +2,11 @@ import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; import java.util.List; -import java.util.Optional; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; @@ -21,7 +18,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; -import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; import com.c4_soft.springaddons.security.oidc.starter.LogoutRequestUriBuilder; import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties; import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; @@ -68,21 +64,6 @@ public Mono> getLoginOptions(Authentication auth) throws UR return Mono.just(isAuthenticated ? List.of() : this.loginOptions); } - @GetMapping(path = "/me", produces = "application/json") - @Tag(name = "getMe") - @Operation(responses = { @ApiResponse(responseCode = "200") }) - public Mono getMe(Authentication auth) { - if (auth instanceof OAuth2AuthenticationToken oauth && oauth.getPrincipal() instanceof OidcUser user) { - final var claims = new OpenidClaimSet(user.getClaims()); - return Mono.just( - new UserDto( - claims.getSubject(), - Optional.ofNullable(claims.getIssuer()).map(URL::toString).orElse(""), - oauth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())); - } - return Mono.just(UserDto.ANONYMOUS); - } - @PutMapping(path = "/logout", produces = "application/json") @Tag(name = "logout") @Operation(responses = { @ApiResponse(responseCode = "204") }) @@ -104,10 +85,6 @@ public Mono> logout(ServerWebExchange exchange, Authenticat }); } - static record UserDto(String subject, String issuer, List roles) { - static final UserDto ANONYMOUS = new UserDto("", "", List.of()); - } - static record LoginOptionDto(@NotEmpty String label, @NotEmpty String loginUri) { } } diff --git a/samples/tutorials/bff/gateway/src/main/resources/application.yml b/samples/tutorials/bff/gateway/src/main/resources/application.yml index 7a18050f8..f50770c4e 100644 --- a/samples/tutorials/bff/gateway/src/main/resources/application.yml +++ b/samples/tutorials/bff/gateway/src/main/resources/application.yml @@ -1,7 +1,7 @@ scheme: http keycloak-port: 8442 -keycloak-issuer: https://oidc.c4-soft.com/auth/realms/master -keycloak-client-id: spring-addons-confidential +keycloak-issuer: https://oidc.c4-soft.com/auth/realms/spring-addons +keycloak-client-id: spring-addons-bff keycloak-secret: change-me cognito-client-id: 12olioff63qklfe9nio746es9f cognito-issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl @@ -148,12 +148,10 @@ com: - /login/** - /oauth2/** - /logout - - /me - /bff/** permit-all: - /login/** - /oauth2/** - - /me # The Angular app needs access to the CSRF cookie (to return its value as X-XSRF-TOKEN header) csrf: cookie-accessible-from-js login-path: /ui/ diff --git a/samples/tutorials/bff/greetings-api/pom.xml b/samples/tutorials/bff/greetings-api/pom.xml index bf0d71256..14b3fafc3 100644 --- a/samples/tutorials/bff/greetings-api/pom.xml +++ b/samples/tutorials/bff/greetings-api/pom.xml @@ -12,7 +12,7 @@ Sample resource-server providing current user with a personalized greeting - 6443 + 7443 diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/RestApiApplication.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/RestApiApplication.java new file mode 100644 index 000000000..97c3a4085 --- /dev/null +++ b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/RestApiApplication.java @@ -0,0 +1,24 @@ +package com.c4soft.springaddons.samples.bff.users; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "Users API", version = "1.0.0"), security = { @SecurityRequirement(name = "OAuth2") }) +public class RestApiApplication { + + public static void main(String[] args) { + SpringApplication.run(RestApiApplication.class, args); + } + + @Configuration + @EnableMethodSecurity + public static class WebSecurityConfig { + } +} diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java deleted file mode 100644 index 5f5436d36..000000000 --- a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.c4soft.springaddons.samples.bff.users; - -import java.util.Collection; -import java.util.Map; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.jwt.JwtClaimNames; - -import com.c4_soft.springaddons.security.oidc.OAuthentication; -import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; -import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; -import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "Users API", version = "1.0.0"), security = { @SecurityRequirement(name = "OAuth2") }) -public class UsersApiApplication { - - public static void main(String[] args) { - SpringApplication.run(UsersApiApplication.class, args); - } - - @Configuration - @EnableMethodSecurity - public static class WebSecurityConfig { - @Bean - JwtAbstractAuthenticationTokenConverter authenticationConverter( - Converter, Collection> authoritiesConverter, - SpringAddonsOidcProperties addonsProperties) { - return jwt -> new OAuthentication<>( - new OpenidClaimSet(jwt.getClaims(), addonsProperties.getOpProperties(jwt.getClaims().get(JwtClaimNames.ISS)).getUsernameClaim()), - authoritiesConverter.convert(jwt.getClaims()), - jwt.getTokenValue()); - }; - } -} diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingDto.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingDto.java deleted file mode 100644 index 266931223..000000000 --- a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.c4soft.springaddons.samples.bff.users.web; - -import jakarta.validation.constraints.NotEmpty; - -public record GreetingDto(@NotEmpty String message) { -} \ No newline at end of file diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java index 3bc885869..e8fd14956 100644 --- a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java +++ b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java @@ -1,25 +1,43 @@ package com.c4soft.springaddons.samples.bff.users.web; import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.c4_soft.springaddons.security.oidc.OAuthentication; -import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; - import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotEmpty; @RestController -@RequestMapping(path = "/greetings", produces = MediaType.APPLICATION_JSON_VALUE) @Tag(name = "Greetings") @Observed(name = "GreetingsController") public class GreetingsController { - @GetMapping() - @Tag(name = "get") - public GreetingDto getGreeting(OAuthentication auth) { + + @GetMapping(path = "/greetings/public", produces = MediaType.APPLICATION_JSON_VALUE) + @Tag(name = "getPublicGreeting") + @PreAuthorize("isAuthenticated()") + public GreetingDto getGreeting(JwtAuthenticationToken auth) { return new GreetingDto( - "Hi %s! You are authenticated by %s and granted with: %s.".formatted(auth.getName(), auth.getAttributes().getIssuer(), auth.getAuthorities())); + "Hi %s! You are authenticated by %s and granted with: %s." + .formatted(auth.getName(), auth.getTokenAttributes().get(JwtClaimNames.ISS), auth.getAuthorities())); + } + + @GetMapping(path = "/greetings/nice", produces = MediaType.APPLICATION_JSON_VALUE) + @Tag(name = "getNiceGreeting") + @PreAuthorize("hasAuthority('NICE')") + public GreetingDto getNiceGreeting(JwtAuthenticationToken auth) { + return new GreetingDto( + "Dear %s! You are authenticated by %s and granted with: %s." + .formatted(auth.getName(), auth.getTokenAttributes().get(JwtClaimNames.ISS), auth.getAuthorities())); + } + + /** + * @param message the greeting body + * @author Jerome Wacongne ch4mp@c4-soft.com + */ + static record GreetingDto(@NotEmpty String message) { } } diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/PublicController.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/PublicController.java deleted file mode 100644 index ab760bda9..000000000 --- a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/PublicController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.c4soft.springaddons.samples.bff.users.web; - -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import io.micrometer.observation.annotation.Observed; -import io.swagger.v3.oas.annotations.tags.Tag; - -@RestController -@RequestMapping(path = "/public", produces = MediaType.APPLICATION_JSON_VALUE) -@Tag(name = "Greetings") -@Observed(name = "PublicController") -public class PublicController { - @GetMapping("/hello") - @Tag(name = "get") - public GreetingDto getHello() { - return new GreetingDto("Hello Wrold!"); - } -} diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/UsersController.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/UsersController.java new file mode 100644 index 000000000..4a115b1ea --- /dev/null +++ b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/UsersController.java @@ -0,0 +1,48 @@ +package com.c4soft.springaddons.samples.bff.users.web; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; + +import io.micrometer.observation.annotation.Observed; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +@RestController +@Tag(name = "Users") +@Observed(name = "UsersController") +public class UsersController { + + @GetMapping(path = "/users/me", produces = MediaType.APPLICATION_JSON_VALUE) + @Tag(name = "getMe") + public UserInfo getMe(Authentication auth) { + if (auth instanceof JwtAuthenticationToken jwt) { + final var claims = new OpenidClaimSet(jwt.getTokenAttributes()); + return new UserInfo( + auth.getName(), + claims.getIssuer().toString(), + auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(), + claims.getExpiresAt().toEpochMilli() / 1000, + claims.getEmail()); + } + return UserInfo.ANONYMOUS; + } + + /** + * @param name user name + * @param roles user roles + * @param exp expierztion time (in seconds since epoch) + * @author Jerome Wacongne ch4mp@c4-soft.com + */ + static record UserInfo(@NotNull String name, @NotNull String iss, @NotNull List roles, @NotNull @Min(0L) Long exp, @NotNull String email) { + static final UserInfo ANONYMOUS = new UserInfo("", "", List.of(), Long.MAX_VALUE, ""); + } +} diff --git a/samples/tutorials/bff/greetings-api/src/main/resources/application.yml b/samples/tutorials/bff/greetings-api/src/main/resources/application.yml index 5facbcb46..7c45efec3 100644 --- a/samples/tutorials/bff/greetings-api/src/main/resources/application.yml +++ b/samples/tutorials/bff/greetings-api/src/main/resources/application.yml @@ -1,7 +1,7 @@ scheme: http origins: ${scheme}://localhost:8080 keycloak-port: 8442 -keycloak-issuer: https://oidc.c4-soft.com/auth/realms/master +keycloak-issuer: https://oidc.c4-soft.com/auth/realms/spring-addons cognito-issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl auth0-issuer: https://dev-ch4mpy.eu.auth0.com/ @@ -30,7 +30,6 @@ com: username-claim: preferred_username authorities: - path: $.realm_access.roles - - path: $.resource_access.*.roles - iss: ${cognito-issuer} username-claim: username authorities: @@ -46,7 +45,7 @@ com: - path: /** allowed-origin-patterns: ${origins} permit-all: - - "/public/**" + - "/users/me" - "/actuator/health/readiness" - "/actuator/health/liveness" - "/v3/api-docs/**" diff --git a/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java b/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java index 0822c9c0a..eec2f6a6a 100644 --- a/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java +++ b/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java @@ -1,32 +1,75 @@ package com.c4soft.springaddons.samples.bff.users.web; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.test.context.support.WithAnonymousUser; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @WebMvcTest(controllers = GreetingsController.class) @AutoConfigureAddonsWebmvcResourceServerSecurity class GreetingsControllerTest { + @Autowired MockMvcSupport api; + @Autowired + WithJwt.AuthenticationFactory authFactory; + + @Test + @WithAnonymousUser + void givenRequestIsAnonymous_whenGetPublicGreeting_thenUnauthorized() throws Exception { + api.get("/greetings/public").andExpect(status().isUnauthorized()); + } + @Test @WithAnonymousUser - void givenRequestIsAnonymous_whenGetGreetings_thenUnauthorized() throws Exception { - api.get("/greetings").andExpect(status().isUnauthorized()); + void givenRequestIsAnonymous_whenGetNiceGreeting_thenUnauthorized() throws Exception { + api.get("/greetings/nice").andExpect(status().isUnauthorized()); + } + + @ParameterizedTest + @MethodSource("allIdentities") + void givenUserIsAuthenticated_whenGetPublicGreeting_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception { + api.get("/greetings/public").andExpect(status().isOk()).andExpect( + jsonPath("$.message").value( + "Hi %s! You are authenticated by %s and granted with: %s.".formatted( + auth.getName(), + ((JwtAuthenticationToken) auth).getTokenAttributes().get(JwtClaimNames.ISS), + auth.getAuthorities()))); } @Test - @WithJwt("ch4mp_auth0.json") - void givenUserIsAuthenticated_whenGetGreetings_thenOk() throws Exception { - api.get("/greetings").andExpect(status().isOk()); + @WithJwt("ch4mp.json") + void givenUserIsCh4mp_whenGetNiceGreeting_thenOk() throws Exception { + api.get("/greetings/nice").andExpect(status().isOk()).andExpect( + jsonPath("$.message") + .value("Dear ch4mp! You are authenticated by https://oidc.c4-soft.com/auth/realms/spring-addons and granted with: [NICE, AUTHOR].")); } + @Test + @WithJwt("tonton-pirate.json") + void givenUserIsTontonPirate_whenGetNiceGreeting_thenForbidden() throws Exception { + api.get("/greetings/nice").andExpect(status().isForbidden()); + } + + private Stream allIdentities() { + final var authentications = authFactory.authenticationsFrom("ch4mp.json", "tonton-pirate.json").toList(); + return authentications.stream(); + } } diff --git a/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/UsersControllerTest.java b/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/UsersControllerTest.java new file mode 100644 index 000000000..2e484d9b9 --- /dev/null +++ b/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/UsersControllerTest.java @@ -0,0 +1,70 @@ +package com.c4soft.springaddons.samples.bff.users.web; + +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.test.context.support.WithAnonymousUser; + +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; + +@WebMvcTest(controllers = UsersController.class) +@AutoConfigureAddonsWebmvcResourceServerSecurity +class UsersControllerTest { + + @Autowired + MockMvcSupport api; + + @Autowired + WithJwt.AuthenticationFactory authFactory; + + @Test + @WithAnonymousUser + void givenRequestIsAnonymous_whenGetMe_thenOk() throws Exception { + // @formatter:off + api.get("/users/me") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("")) + .andExpect(jsonPath("$.exp").value(Long.MAX_VALUE)) + .andExpect(jsonPath("$.email").value("")) + .andExpect(jsonPath("$.roles").isEmpty()); + // @formatter:on + } + + @ParameterizedTest + @MethodSource("allIdentities") + void givenUserIsAuthenticated_whenGetMe_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception { + final var claims = new OpenidClaimSet(((JwtAuthenticationToken) auth).getTokenAttributes()); + final var authorities = auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toArray(); + + // @formatter:off + api.get("/users/me") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value(claims.getPreferredUsername())) + .andExpect(jsonPath("$.exp").value(claims.getExpiresAt().getEpochSecond())) + .andExpect(jsonPath("$.email").value(claims.getEmail())) + .andExpect(jsonPath("$.roles").value(containsInAnyOrder(authorities))); + // @formatter:on + } + + private Stream allIdentities() { + final var authentications = authFactory.authenticationsFrom("ch4mp.json", "tonton-pirate.json").toList(); + return authentications.stream(); + } + +} diff --git a/samples/tutorials/bff/greetings-api/src/test/resources/ch4mp.json b/samples/tutorials/bff/greetings-api/src/test/resources/ch4mp.json new file mode 100644 index 000000000..eada2a27b --- /dev/null +++ b/samples/tutorials/bff/greetings-api/src/test/resources/ch4mp.json @@ -0,0 +1,16 @@ +{ + "iss": "https://oidc.c4-soft.com/auth/realms/spring-addons", + "sub": "281c4558-550c-413b-9972-2d2e5bde6b9b", + "iat": 1695992542, + "exp": 1695992642, + "realm_access": { + "roles": [ + "NICE", + "AUTHOR" + ] + }, + "scope": "openid offline_access email profile", + "preferred_username": "ch4mp", + "email": "ch4mp@c4-soft.com", + "email_verified": true +} \ No newline at end of file diff --git a/samples/tutorials/bff/greetings-api/src/test/resources/ch4mp_auth0.json b/samples/tutorials/bff/greetings-api/src/test/resources/ch4mp_auth0.json deleted file mode 100644 index fa7f45fdf..000000000 --- a/samples/tutorials/bff/greetings-api/src/test/resources/ch4mp_auth0.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "https://c4-soft.com/user": { - "app_metadata": {}, - "created_at": "2023-06-01T01:21:37.810Z", - "email": "ch4mp@c4-soft.com", - "email_verified": true, - "identities": [ - { - "connection": "c4-soft", - "isSocial": true, - "provider": "oauth2", - "userId": "c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", - "user_id": "c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94" - } - ], - "multifactor": [], - "name": "ch4mp", - "nickname": "ch4mp", - "picture": "https://s.gravatar.com/avatar/f4d00b0a82e9307b1d68b29867fee4e5?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fch.png", - "roles": [ - "USER_ROLES_EDITOR" - ], - "updated_at": "2023-06-23T04:53:53.057Z", - "user_id": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", - "user_metadata": {} - }, - "permissions": [ - "NICE", "AUTHOR" - ], - "iss": "https://dev-ch4mpy.eu.auth0.com/", - "sub": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", - "aud": [ - "demo.c4-soft.com", - "https://dev-ch4mpy.eu.auth0.com/userinfo" - ], - "iat": 1687633329, - "exp": 1687719729, - "azp": "pDy3JpZoenbLk9MqXYCfJK1mpxeUwkKL", - "scope": "openid email" -} \ No newline at end of file diff --git a/samples/tutorials/bff/greetings-api/src/test/resources/tonton-pirate.json b/samples/tutorials/bff/greetings-api/src/test/resources/tonton-pirate.json new file mode 100644 index 000000000..121d17973 --- /dev/null +++ b/samples/tutorials/bff/greetings-api/src/test/resources/tonton-pirate.json @@ -0,0 +1,14 @@ +{ + "iss": "https://oidc.c4-soft.com/auth/realms/spring-addons", + "sub": "2d2e5bde6b9b-550c-413b-9972-281c4558", + "iat": 1695992551, + "exp": 1695992651, + "realm_access": { + "roles": [ + ] + }, + "scope": "openid offline_access email profile", + "preferred_username": "tonton-pirate", + "email": "tonton-pirate@c4-soft.com", + "email_verified": false +} \ No newline at end of file