diff --git a/apps/self-service/src/app/app.config.spec.ts b/apps/self-service/src/app/app.config.spec.ts new file mode 100644 index 0000000..439e411 --- /dev/null +++ b/apps/self-service/src/app/app.config.spec.ts @@ -0,0 +1,65 @@ +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { BffApiV1Service } from 'api-client'; +import { authInterceptor } from 'auth'; +import { AbstractSecurityStorage, ConfigurationService } from 'angular-auth-oidc-client'; +import { SECURE_API_ROUTES } from './app.config'; + +// Guards the DigiD token wiring end-to-end. The api-client calls the BFF with RELATIVE URLs, and the +// angular-auth-oidc-client interceptor attaches the token only when `req.url` starts with a configured +// secureRoute. A regression to an absolute origin (as once shipped) makes the relative URL never match, +// so the submit goes out unauthenticated and fails silently. This drives the REAL interceptor and the +// REAL api-client against the REAL production route value (SECURE_API_ROUTES); only the config source +// and the token storage are faked, so the assertion turns on the actual route-matching. +describe('self-service DigiD token wiring', () => { + let http: HttpTestingController; + let bff: BffApiV1Service; + const token = 'digid-access-token'; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptors([authInterceptor()])), + provideHttpClientTesting(), + { + provide: ConfigurationService, + useValue: { + hasAtLeastOneConfig: () => true, + getAllConfigurations: () => [{ configId: 'digid', secureRoutes: SECURE_API_ROUTES }], + }, + }, + { + // A signed-in session: the storage the interceptor's token lookup reads from. + provide: AbstractSecurityStorage, + useValue: { + read: () => JSON.stringify({ authzData: token, authnResult: { id_token: 'id-token' } }), + write: () => undefined, + remove: () => undefined, + clear: () => undefined, + }, + }, + ], + }); + http = TestBed.inject(HttpTestingController); + bff = TestBed.inject(BffApiV1Service); + }); + + afterEach(() => http.verify()); + + it('attaches the bearer token to the relative self-service BFF call', () => { + bff.postSelfServiceRegistrations().subscribe(); + + const req = http.expectOne('/self-service/registrations'); + expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`); + req.flush({ registrationId: 'reg-1', status: 'Ingediend' }); + }); + + it('leaves the anonymous openbaar register call unauthenticated', () => { + bff.getOpenbaarRegister().subscribe(); + + const req = http.expectOne((r) => r.url === '/openbaar/register'); + expect(req.request.headers.has('Authorization')).toBe(false); + req.flush([]); + }); +}); diff --git a/apps/self-service/src/app/app.config.ts b/apps/self-service/src/app/app.config.ts index c60e7bf..06382d2 100644 --- a/apps/self-service/src/app/app.config.ts +++ b/apps/self-service/src/app/app.config.ts @@ -13,12 +13,17 @@ export interface RuntimeConfig { authority: string; } +/** + * Route prefixes whose requests carry the DigiD token. These MUST match the **relative** URLs the + * api-client actually calls (same-origin via the nginx proxy) — the interceptor matches on `req.url`, + * which stays relative, so an absolute origin would never match and the token would go unattached. + * `/openbaar/` is deliberately excluded: it is the anonymous public register. + */ +export const SECURE_API_ROUTES = ['/self-service/']; + /** * Build the app providers from runtime config. `redirectUrl` is the app's own origin (where Keycloak - * redirects back). The app is served same-origin as the BFF (nginx proxies /self-service + /openbaar), - * so the api-client uses **relative** URLs — hence `secureRoutes` is the relative `/self-service/` - * prefix (the guarded BFF route), not the origin: the interceptor matches on `req.url`, which stays - * relative, so an origin would never match and the token would not be attached. + * redirects back). `secureRoutes` uses {@link SECURE_API_ROUTES} — relative prefixes, not the origin. */ export function appConfig(runtime: RuntimeConfig): ApplicationConfig { const origin = typeof window !== 'undefined' ? window.location.origin : '/'; @@ -30,7 +35,7 @@ export function appConfig(runtime: RuntimeConfig): ApplicationConfig { provideDigiadAuth({ authority: runtime.authority, redirectUrl: origin, - secureRoutes: ['/self-service/'], + secureRoutes: SECURE_API_ROUTES, }), ], };