mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
fix(core): limit DateHeaderTimeSyncInterceptor to IAM API responses only
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user