fix(core): limit DateHeaderTimeSyncInterceptor to IAM API responses only

This commit is contained in:
copilot-swe-agent[bot]
2026-07-03 11:24:18 +00:00
committed by GitHub
parent 958f8c908e
commit 31d7396c71
2 changed files with 59 additions and 18 deletions
@@ -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<TimeSyncService>;
let httpClient: HttpClient;
let appConfigServiceMock: jasmine.SpyObj<AppConfigService>;
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' });
});
});
@@ -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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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);
}
}