diff --git a/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.spec.ts b/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.spec.ts index dbb96d87aa..3547d636a6 100644 --- a/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.spec.ts +++ b/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.spec.ts @@ -20,19 +20,27 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common import { TestBed } from '@angular/core/testing'; import { TimeSyncService } from '../services/time-sync.service'; import { DateHeaderTimeSyncInterceptor } from './date-header-time-sync.interceptor'; +import { AppConfigService } from '../../app-config/app-config.service'; describe('DateHeaderTimeSyncInterceptor', () => { let httpMock: HttpTestingController; let timeSyncServiceSpy: jasmine.SpyObj; let httpClient: HttpClient; + let appConfigServiceMock: jasmine.SpyObj; + + const IAM_HOST = 'https://iam.example.com/auth/realms/alfresco'; beforeEach(() => { timeSyncServiceSpy = jasmine.createSpyObj('TimeSyncService', ['updateClockOffsetFromDateHeader']); + appConfigServiceMock = jasmine.createSpyObj('AppConfigService', ['get'], { + oauth2: { host: IAM_HOST } + }); TestBed.configureTestingModule({ providers: [ DateHeaderTimeSyncInterceptor, { provide: TimeSyncService, useValue: timeSyncServiceSpy }, + { provide: AppConfigService, useValue: appConfigServiceMock }, { provide: HTTP_INTERCEPTORS, useClass: DateHeaderTimeSyncInterceptor, multi: true }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting() @@ -47,33 +55,56 @@ describe('DateHeaderTimeSyncInterceptor', () => { httpMock.verify(); }); - it('should call updateClockOffsetFromDateHeader when response contains a Date header', () => { + it('should call updateClockOffsetFromDateHeader when IAM response contains a Date header', () => { const requestStartTime = 1728911579000; spyOn(Date, 'now').and.returnValue(requestStartTime); + const iamUrl = `${IAM_HOST}/protocol/openid-connect/token`; + httpClient.get(iamUrl).subscribe(); + + const req = httpMock.expectOne(iamUrl); + req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } }); + + expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).toHaveBeenCalledWith('Mon, 14 Oct 2024 13:12:59 GMT', requestStartTime); + }); + + it('should not call updateClockOffsetFromDateHeader for non-IAM URLs', () => { + httpClient.get('/api/content/nodes').subscribe(); + + const req = httpMock.expectOne('/api/content/nodes'); + req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } }); + + expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled(); + }); + + it('should not call updateClockOffsetFromDateHeader when IAM response has no Date header', () => { + const iamUrl = `${IAM_HOST}/protocol/openid-connect/token`; + httpClient.get(iamUrl).subscribe(); + + const req = httpMock.expectOne(iamUrl); + req.flush({}); + + expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled(); + }); + + it('should not call updateClockOffsetFromDateHeader when oauth2 host is not configured', () => { + (Object.getOwnPropertyDescriptor(appConfigServiceMock, 'oauth2')?.get as jasmine.Spy).and.returnValue({ host: '' }); + httpClient.get('/test').subscribe(); const req = httpMock.expectOne('/test'); req.flush({}, { headers: { date: 'Mon, 14 Oct 2024 13:12:59 GMT' } }); - expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).toHaveBeenCalledWith('Mon, 14 Oct 2024 13:12:59 GMT', requestStartTime); - }); - - it('should not call updateClockOffsetFromDateHeader when response has no Date header', () => { - httpClient.get('/test').subscribe(); - - const req = httpMock.expectOne('/test'); - req.flush({}); - expect(timeSyncServiceSpy.updateClockOffsetFromDateHeader).not.toHaveBeenCalled(); }); it('should pass through the request unchanged', () => { - httpClient.get('/test').subscribe(); + const iamUrl = `${IAM_HOST}/protocol/openid-connect/token`; + httpClient.get(iamUrl).subscribe(); - const req = httpMock.expectOne('/test'); + const req = httpMock.expectOne(iamUrl); expect(req.request.method).toBe('GET'); - expect(req.request.url).toBe('/test'); + expect(req.request.url).toBe(iamUrl); req.flush({ data: 'value' }); }); }); diff --git a/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.ts b/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.ts index 041fe2b4a9..03e5289eed 100644 --- a/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.ts +++ b/lib/core/src/lib/auth/oidc/date-header-time-sync.interceptor.ts @@ -20,26 +20,31 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } fr import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { TimeSyncService } from '../services/time-sync.service'; +import { AppConfigService } from '../../app-config/app-config.service'; /** * HTTP interceptor that passively keeps the clock offset in `TimeSyncService` up-to-date - * by reading the standard `Date` response header (RFC 7231) from every HTTP response. + * by reading the standard `Date` response header (RFC 7231) from IAM API responses. * - * This removes the need for a dedicated `serverTimeUrl` endpoint: as long as HTTP responses - * include a `Date` header (all well-behaved HTTP/1.1 and HTTP/2 servers do), the clock drift - * correction will be applied transparently without an extra network round-trip. + * Only responses whose URL starts with the configured OAuth2 host (`oauth2.host`) are + * processed, avoiding unnecessary offset recalculations on every HTTP call. + * + * This removes the need for a dedicated `serverTimeUrl` endpoint: as long as IAM responses + * include a `Date` header, the clock drift correction will be applied transparently + * without an extra network round-trip. * * The interceptor is registered automatically when `provideCoreAuth()` is used. */ @Injectable() export class DateHeaderTimeSyncInterceptor implements HttpInterceptor { private readonly _timeSyncService = inject(TimeSyncService); + private readonly _appConfigService = inject(AppConfigService); intercept(request: HttpRequest, next: HttpHandler): Observable> { const requestStartTime = Date.now(); return next.handle(request).pipe( tap((event) => { - if (event instanceof HttpResponse) { + if (event instanceof HttpResponse && this.isIamRequest(event.url ?? request.url)) { const dateHeader = event.headers.get('date'); if (dateHeader) { this._timeSyncService.updateClockOffsetFromDateHeader(dateHeader, requestStartTime); @@ -48,4 +53,9 @@ export class DateHeaderTimeSyncInterceptor implements HttpInterceptor { }) ); } + + private isIamRequest(url: string): boolean { + const iamHost = this._appConfigService.oauth2?.host; + return !!iamHost && url.startsWith(iamHost); + } }