import { HttpClient } from '@angular/common/http'; import { Component, Inject, OnInit, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { apiErrorMessage } from '../shared/api-error'; import { API_BASE_URL } from '../shared/api-base-url'; interface OidcClient { id: string; clientId: string; clientName: string; tokenEndpointAuthMethod: string; redirectUris: string[]; postLogoutRedirectUris: string[]; grantTypes: string[]; responseTypes: string[]; scope: string; firstParty: boolean; enabled: boolean; includeGroups: boolean; } interface CreatedOidcClient extends OidcClient { clientSecret?: string; } @Component({ selector: 'app-admin-oidc-clients', imports: [ReactiveFormsModule], template: `

OIDC Clients

Clients fuer OpenID Connect Web-SSO verwalten.

Neuer Client

@if (message()) {

{{ message() }}

}
@if (createdSecret()) {
Client Secret {{ createdSecret() }} Dieses Secret wird nur einmal angezeigt.
}

Discovery

Configuration
{{ oidcBaseUrl }}/.well-known/openid-configuration
Authorize
{{ oidcBaseUrl }}/oidc/auth
Token
{{ oidcBaseUrl }}/oidc/token
UserInfo
{{ oidcBaseUrl }}/oidc/me
JWKS
{{ oidcBaseUrl }}/oidc/jwks

Registrierte Clients

@if (clients().length) {
@for (client of clients(); track client.id) {
@if (editingClientId() === client.id) {
Client bearbeiten {{ client.clientId }}
{{ client.enabled ? 'aktiv' : 'deaktiviert' }} {{ client.tokenEndpointAuthMethod }}
} @else {
{{ client.clientName }} {{ client.clientId }}
{{ client.enabled ? 'aktiv' : 'deaktiviert' }} {{ client.tokenEndpointAuthMethod }} @if (client.firstParty) { first-party } @if (client.includeGroups) { groups }
Redirect URIs
{{ client.redirectUris.join(', ') }}
Logout Redirect URIs
{{ client.postLogoutRedirectUris.join(', ') || '-' }}
Scopes
{{ client.scope }}
@if (canRotateSecret(client)) { } @else { Public Client ohne Secret }
}
}
} @else {

Noch keine OIDC-Clients vorhanden.

}
`, }) export class AdminOidcClientsComponent implements OnInit { readonly clients = signal([]); readonly loading = signal(false); readonly failed = signal(false); readonly message = signal(''); readonly createdSecret = signal(''); readonly editingClientId = signal(null); readonly oidcBaseUrl: string; readonly form; readonly editForm; constructor( private readonly fb: FormBuilder, private readonly http: HttpClient, @Inject(API_BASE_URL) readonly apiBaseUrl: string, ) { this.oidcBaseUrl = apiBaseUrl.endsWith('/api') ? apiBaseUrl.slice(0, -4) : apiBaseUrl; this.form = this.fb.nonNullable.group({ clientName: ['', Validators.required], redirectUris: ['http://localhost:8080/callback', Validators.required], postLogoutRedirectUris: [''], scope: ['openid profile email groups'], publicClient: [false], firstParty: [false], includeGroups: [true], }); this.editForm = this.fb.nonNullable.group({ clientName: ['', Validators.required], redirectUris: ['', Validators.required], postLogoutRedirectUris: [''], scope: ['openid profile email groups'], firstParty: [false], includeGroups: [true], }); } ngOnInit(): void { this.load(); } create(): void { if (this.form.invalid) { return; } this.loading.set(true); this.failed.set(false); this.message.set(''); this.createdSecret.set(''); const value = this.form.getRawValue(); this.http .post(`${this.apiBaseUrl}/admin/oidc/clients`, { ...value, redirectUris: this.lines(value.redirectUris), postLogoutRedirectUris: this.lines(value.postLogoutRedirectUris), }) .subscribe({ next: (client) => { this.createdSecret.set(client.clientSecret ?? ''); this.message.set('Client wurde erstellt.'); this.load(); }, error: (error) => { this.failed.set(true); this.message.set(apiErrorMessage(error)); this.loading.set(false); }, complete: () => this.loading.set(false), }); } toggle(client: OidcClient): void { this.http .patch(`${this.apiBaseUrl}/admin/oidc/clients/${client.id}`, { enabled: !client.enabled }) .subscribe({ next: () => this.load(), error: (error) => this.message.set(apiErrorMessage(error)) }); } canRotateSecret(client: OidcClient): boolean { return client.tokenEndpointAuthMethod !== 'none'; } rotateSecret(client: OidcClient): void { const confirmed = window.confirm( 'Das Client Secret wird neu erzeugt und nur einmal angezeigt. Bestehende Apps muessen danach das neue Secret verwenden.', ); if (!confirmed) { return; } this.loading.set(true); this.failed.set(false); this.message.set(''); this.createdSecret.set(''); this.http .post(`${this.apiBaseUrl}/admin/oidc/clients/${client.id}/secret/rotate`, {}) .subscribe({ next: (updatedClient) => { this.createdSecret.set(updatedClient.clientSecret ?? ''); this.message.set('Client Secret wurde rotiert.'); this.load(); }, error: (error) => { this.failed.set(true); this.message.set(apiErrorMessage(error)); this.loading.set(false); }, complete: () => this.loading.set(false), }); } startEdit(client: OidcClient): void { this.failed.set(false); this.message.set(''); this.createdSecret.set(''); this.editingClientId.set(client.id); this.editForm.setValue({ clientName: client.clientName, redirectUris: this.multiline(client.redirectUris), postLogoutRedirectUris: this.multiline(client.postLogoutRedirectUris), scope: client.scope, firstParty: client.firstParty, includeGroups: client.includeGroups, }); } cancelEdit(): void { this.editingClientId.set(null); } save(client: OidcClient): void { if (this.editForm.invalid) { return; } this.loading.set(true); this.failed.set(false); this.message.set(''); const value = this.editForm.getRawValue(); this.http .patch(`${this.apiBaseUrl}/admin/oidc/clients/${client.id}`, { ...value, redirectUris: this.lines(value.redirectUris), postLogoutRedirectUris: this.lines(value.postLogoutRedirectUris), }) .subscribe({ next: () => { this.editingClientId.set(null); this.message.set('Client wurde gespeichert.'); this.load(); }, error: (error) => { this.failed.set(true); this.message.set(apiErrorMessage(error)); this.loading.set(false); }, complete: () => this.loading.set(false), }); } delete(client: OidcClient): void { this.http .delete(`${this.apiBaseUrl}/admin/oidc/clients/${client.id}`) .subscribe({ next: () => { if (this.editingClientId() === client.id) { this.editingClientId.set(null); } this.load(); }, error: (error) => this.message.set(apiErrorMessage(error)), }); } private load(): void { this.http.get(`${this.apiBaseUrl}/admin/oidc/clients`).subscribe({ next: (clients) => this.clients.set(clients), error: (error) => { this.failed.set(true); this.message.set(apiErrorMessage(error)); }, }); } private lines(value: string): string[] { return value .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); } private multiline(values: string[]): string { return values.join('\n'); } }