test(portal-self-service): DigiD-guarded registration submit page (refs #67)

Scaffold libs/ui (NL Design System via Utrecht components) and libs/auth (DigiD OIDC
over angular-auth-oidc-client: mockable AuthService, provider, token interceptor,
authenticated guard). Failing component + axe tests for the RegistrationPage: it must
show the signed-in BSN, submit to the BFF (mocked api-client) and confirm, with no
WCAG 2.1 AA violations. The page is a stub, so the behaviour tests fail; green follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 13:02:29 +02:00
parent 311aab0aba
commit 72c2bdfae7
27 changed files with 725 additions and 4 deletions

View File

@@ -0,0 +1,16 @@
import { Signal } from '@angular/core';
/**
* The portal's view of the signed-in user. An abstraction over the OIDC library so components and
* guards depend on a small, mockable surface (the real implementation is DigiadAuthService).
*/
export abstract class AuthService {
/** Whether a DigiD session is active. */
abstract readonly isAuthenticated: Signal<boolean>;
/** The citizen-service number from the DigiD token, once authenticated. */
abstract readonly bsn: Signal<string | undefined>;
/** Start the DigiD login (redirects to Keycloak). */
abstract login(): void;
/** End the session. */
abstract logout(): void;
}

View File

@@ -0,0 +1,13 @@
import { inject } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
/** Allow the route only when a DigiD session is active; otherwise start the login. */
export const authenticatedGuard: CanActivateFn = () => {
const auth = inject(AuthService);
if (auth.isAuthenticated()) {
return true;
}
auth.login();
return false;
};

View File

@@ -0,0 +1,40 @@
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { authInterceptor, LogLevel, provideAuth } from 'angular-auth-oidc-client';
import { AuthService } from './auth.service';
import { DigiadAuthService } from './digid-auth.service';
export interface DigiadAuthOptions {
/** The Keycloak `digid` realm issuer, as reachable from the browser. */
authority: string;
/** Where Keycloak redirects back to after login (usually the app origin). */
redirectUrl: string;
/** The BFF origin whose requests get the bearer token attached (secure route). */
secureApiOrigin: string;
}
/**
* Configure DigiD login (Keycloak `digid` realm, public client `big-portal`, auth-code + PKCE) and
* bind {@link AuthService} to the OIDC-backed implementation. Register {@link authInterceptor} in the
* app's HttpClient so BFF calls carry the token.
*/
export function provideDigiadAuth(options: DigiadAuthOptions): EnvironmentProviders {
return makeEnvironmentProviders([
provideAuth({
config: {
authority: options.authority,
redirectUrl: options.redirectUrl,
postLogoutRedirectUri: options.redirectUrl,
clientId: 'big-portal',
scope: 'openid profile',
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
secureRoutes: [options.secureApiOrigin],
logLevel: LogLevel.Warn,
},
}),
{ provide: AuthService, useClass: DigiadAuthService },
]);
}
export { authInterceptor };

View File

@@ -0,0 +1,29 @@
import { inject, Injectable, Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { map } from 'rxjs';
import { AuthService } from './auth.service';
/** DigiD-backed AuthService over angular-auth-oidc-client (Keycloak `digid` realm). */
@Injectable()
export class DigiadAuthService extends AuthService {
private readonly oidc = inject(OidcSecurityService);
readonly isAuthenticated: Signal<boolean> = toSignal(
this.oidc.isAuthenticated$.pipe(map((result) => result.isAuthenticated)),
{ initialValue: false },
);
readonly bsn: Signal<string | undefined> = toSignal(
this.oidc.userData$.pipe(map((data) => (data.userData as { bsn?: string } | null)?.bsn)),
{ initialValue: undefined },
);
override login(): void {
this.oidc.authorize();
}
override logout(): void {
this.oidc.logoff().subscribe();
}
}