diff --git a/src/app/auth/authentication.service.spec.ts b/src/app/auth/authentication.service.spec.ts index 9203b40..b820333 100644 --- a/src/app/auth/authentication.service.spec.ts +++ b/src/app/auth/authentication.service.spec.ts @@ -9,14 +9,14 @@ import { AuthInterceptor } from '../shared/auth.interceptor'; import { SSO_API_URL } from '../shared/sso-api'; import { AUTH_API_URL } from '../shared/auth-api'; import { REALM } from '../shared/realm-token'; +import { WIT_API_URL } from '../shared/wit-api'; describe('Service: Authentication service', () => { - const authUrl: string = 'http://example.com/'; + const authUrl: string = 'http://auth.example.com/'; let authenticationService: AuthenticationService; let broadcaster: Broadcaster; let httpClient: HttpClient; - let httpClientTestingModule: HttpClientTestingModule; let httpTestingController: HttpTestingController; beforeEach(() => { @@ -31,18 +31,10 @@ describe('Service: Authentication service', () => { useClass: AuthInterceptor, multi: true }, - { - provide: AUTH_API_URL, - useValue: 'http://example.com/' - }, - { - provide: REALM, - useValue: 'fabric8' - }, - { - provide: SSO_API_URL, - useValue: 'http://example.com/auth' - }, + { provide: REALM, useValue: 'fabric8' }, + { provide: AUTH_API_URL, useValue: 'http://auth.example.com/' }, + { provide: SSO_API_URL, useValue: 'http://sso.example.com/auth' }, + { provide: WIT_API_URL, useValue: 'http://wit.example.com'}, Broadcaster ] }); diff --git a/src/app/auth/authentication.service.ts b/src/app/auth/authentication.service.ts index 0d1459a..7535927 100644 --- a/src/app/auth/authentication.service.ts +++ b/src/app/auth/authentication.service.ts @@ -9,7 +9,6 @@ import { AUTH_API_URL } from '../shared/auth-api'; import { SSO_API_URL } from '../shared/sso-api'; import { REALM } from '../shared/realm-token'; import { Token } from '../user/token'; -import { isAuthenticationError } from '../shared/isAuthenticationError'; import { ConnectableObservable } from 'rxjs/internal/observable/ConnectableObservable'; export interface ProcessTokenResponse { @@ -100,8 +99,7 @@ export class AuthenticationService { let tokenUrl = this.apiUrl + `token?force_pull=true&for=` + encodeURIComponent(cluster); const httpOptions = { headers: new HttpHeaders({ - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.getToken()}` + 'Content-Type': 'application/json' }) }; return this.http.get(tokenUrl, httpOptions) diff --git a/src/app/shared/auth.interceptor.spec.ts b/src/app/shared/auth.interceptor.spec.ts index bd0c340..58e8b81 100644 --- a/src/app/shared/auth.interceptor.spec.ts +++ b/src/app/shared/auth.interceptor.spec.ts @@ -6,10 +6,15 @@ import { import { TestBed } from '@angular/core/testing'; import { Broadcaster } from 'ngx-base'; import { AuthInterceptor } from './auth.interceptor'; +import { WIT_API_URL } from './wit-api'; +import { AUTH_API_URL } from './auth-api'; +import { SSO_API_URL } from './sso-api'; describe(`AuthHttpInterceptor`, () => { - const testUrl: string = 'http://localhost/test'; + const testUrl: string = 'http://auth.example.com/test'; + const otherUrl: string = 'http://other.example.com/test'; const testToken: string = 'test_token'; + const rptToken = 'new_token_with_rpt_data'; let httpMock: HttpTestingController; let httpClient: HttpClient; @@ -24,6 +29,9 @@ describe(`AuthHttpInterceptor`, () => { useClass: AuthInterceptor, multi: true }, + { provide: WIT_API_URL, useValue: 'http://wit.example.com'}, + { provide: AUTH_API_URL, useValue: 'http://auth.example.com'}, + { provide: SSO_API_URL, useValue: 'http://sso.example.com'}, Broadcaster ] }); @@ -31,9 +39,7 @@ describe(`AuthHttpInterceptor`, () => { httpClient = TestBed.get(HttpClient); broadcaster = TestBed.get(Broadcaster); - spyOn(localStorage, 'getItem').and.callFake( (key: string): string => { - return key === 'auth_token' ? testToken : null; - }); + localStorage.setItem('auth_token', testToken); }); afterEach(() => { httpMock.verify(); @@ -44,8 +50,39 @@ describe(`AuthHttpInterceptor`, () => { const req = httpMock.expectOne(testUrl); + expect(req.request.headers.has('Authorization')).toBeTruthy(); + expect(req.request.headers.get('Authorization')).toBe(`Bearer ${testToken}`); + }); + + it('should not intercept request if the URL is not valid auth endpoint', () => { + httpClient.get(otherUrl).subscribe(() => {}); + + const req = httpMock.expectOne(otherUrl); + + // should not add authorization header + expect(req.request.headers.has('Authorization')).toBeFalsy(); + }); + + it('should update auth_token if there is a new RPT token in response header', () => { + httpClient.get(testUrl, { observe: 'response' }).subscribe(res => { + expect(res.headers.has('Authorization')).toBeTruthy(); + expect(res.headers.get('Authorization')).toBe(`Bearer ${rptToken}`); + }); + + const req = httpMock.expectOne(testUrl); + + // mock response + req.flush( + { data: 'mock-data' }, + { headers: { 'Authorization': `Bearer ${rptToken}` } } + ); + + // check if request sends proper auth headers expect(req.request.headers.has('Authorization')); expect(req.request.headers.get('Authorization')).toBe(`Bearer ${testToken}`); + + // check if localStorage is updated with new RPT token + expect(localStorage.getItem('auth_token')).toBe(rptToken); }); it('should broadcast authenticationError event on 401 with code jwt_security_error', () => { diff --git a/src/app/shared/auth.interceptor.ts b/src/app/shared/auth.interceptor.ts index 29544a8..7a38796 100644 --- a/src/app/shared/auth.interceptor.ts +++ b/src/app/shared/auth.interceptor.ts @@ -13,17 +13,26 @@ import { tap } from 'rxjs/operators'; import { Broadcaster } from 'ngx-base'; -import { isAuthenticationError } from './isAuthenticationError'; +import { WIT_API_URL } from './wit-api'; +import { AUTH_API_URL } from './auth-api'; +import { SSO_API_URL } from './sso-api'; @Injectable() export class AuthInterceptor implements HttpInterceptor { - constructor(@Inject(forwardRef(() => Broadcaster)) private broadcaster: Broadcaster) { - } + constructor( + @Inject(forwardRef(() => Broadcaster)) private broadcaster: Broadcaster, + @Inject(WIT_API_URL) private witApiUrl: string, + @Inject(AUTH_API_URL) private authApiUrl: string, + @Inject(SSO_API_URL) private ssoUrl: string + ) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { + if (!this.isAuthUrl(request.url)) { + return next.handle(request); + } + let token = localStorage.getItem('auth_token'); - let ok: string; if (token !== null) { request = request.clone({ setHeaders: { @@ -35,20 +44,45 @@ export class AuthInterceptor implements HttpInterceptor { .pipe( tap( // Succeeds when there is a response; ignore other events - event => ok = event instanceof HttpResponse ? 'succeeded' : '', + event => event instanceof HttpResponse ? this.refreshRPT(event) : '', // Operation failed; error is an HttpErrorResponse - error => { - if (error instanceof HttpErrorResponse) { - const res: HttpErrorResponse = error; - if (res.status === 403 || isAuthenticationError(res)) { - this.broadcaster.broadcast('authenticationError', res); - } else if (res.status === 500) { - this.broadcaster.broadcast('communicationError', res); - } - } - return throwError(error); - } + error => error instanceof HttpErrorResponse ? this.catchError(error) : throwError(error) ) ); } + + private refreshRPT(res: HttpResponse) { + if (res.headers.has('Authorization')) { + const token = localStorage.getItem('auth_token'); + const newToken = res.headers.get('Authorization').replace('Bearer ', ''); + if (token !== newToken) { + localStorage.setItem('auth_token', newToken); + } + } + } + + private catchError(res: HttpErrorResponse) { + if (res.status === 403 || this.isAuthenticationError(res)) { + this.broadcaster.broadcast('authenticationError', res); + } else if (res.status === 500) { + this.broadcaster.broadcast('communicationError', res); + } + } + + private isAuthenticationError(res: HttpErrorResponse): boolean { + if (res.status === 401) { + const json: any = res.error; + const hasErrors: boolean = json && Array.isArray(json.errors); + const isJwtError: boolean = hasErrors && + json.errors.filter((e: any) => e.code === 'jwt_security_error').length >= 1; + const authHeader = res.headers.get('www-authenticate'); + const isLoginHeader = authHeader && authHeader.toLowerCase().includes('login'); + return isJwtError || isLoginHeader; + } + return false; + } + + private isAuthUrl(url: string) { + return url.startsWith(this.witApiUrl) || url.startsWith(this.authApiUrl) || url.startsWith(this.ssoUrl); + } } diff --git a/src/app/shared/check-auth-error.ts b/src/app/shared/check-auth-error.ts deleted file mode 100644 index 2dc7588..0000000 --- a/src/app/shared/check-auth-error.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HttpResponse } from '@angular/common/http'; - -export function isAuthenticationError(res: HttpResponse): boolean { - if (res.status === 401) { - const json: any = res.body; - const hasErrors: boolean = json && Array.isArray(json.errors); - const isJwtError: boolean = hasErrors && - json.errors.filter((e: any) => e.code === 'jwt_security_error').length >= 1; - const authHeader = res.headers.get('www-authenticate'); - const isLoginHeader = authHeader && authHeader.toLowerCase().includes('login'); - return isJwtError || isLoginHeader; - } - return false; -} diff --git a/src/app/shared/isAuthenticationError.ts b/src/app/shared/isAuthenticationError.ts deleted file mode 100644 index 927a6ec..0000000 --- a/src/app/shared/isAuthenticationError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; - -export function isAuthenticationError(res: HttpErrorResponse): boolean { - if (res.status === 401) { - const json: any = res.error; - const hasErrors: boolean = json && Array.isArray(json.errors); - const isJwtError: boolean = hasErrors && - json.errors.filter((e: any) => e.code === 'jwt_security_error').length >= 1; - const authHeader = res.headers.get('www-authenticate'); - const isLoginHeader = authHeader && authHeader.toLowerCase().includes('login'); - return isJwtError || isLoginHeader; - } - return false; -} diff --git a/src/app/shared/wit-api.ts b/src/app/shared/wit-api.ts new file mode 100644 index 0000000..514981a --- /dev/null +++ b/src/app/shared/wit-api.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export let WIT_API_URL = new InjectionToken('fabric8.wit.api.url'); diff --git a/src/app/user/user.service.spec.ts b/src/app/user/user.service.spec.ts index 558512d..b711021 100644 --- a/src/app/user/user.service.spec.ts +++ b/src/app/user/user.service.spec.ts @@ -1,4 +1,4 @@ -import { HttpHeaders } from '@angular/common/http'; +import { HttpHeaders, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; @@ -6,6 +6,9 @@ import { Broadcaster, Logger } from 'ngx-base'; import { AUTH_API_URL } from '../shared/auth-api'; import { UserService } from './user.service'; +import { AuthInterceptor } from '../shared/auth.interceptor'; +import { SSO_API_URL } from '../shared/sso-api'; +import { WIT_API_URL } from '../shared/wit-api'; describe('Service: User service', () => { @@ -20,9 +23,13 @@ describe('Service: User service', () => { providers: [ UserService, { - provide: AUTH_API_URL, - useValue: 'http://example.com/' + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true }, + { provide: AUTH_API_URL, useValue: 'http://auth.example.com/' }, + { provide: SSO_API_URL, useValue: 'http://sso.example.com/auth' }, + { provide: WIT_API_URL, useValue: 'http://wit.example.com'}, Broadcaster, Logger ] diff --git a/src/app/user/user.service.ts b/src/app/user/user.service.ts index a426031..a012d52 100644 --- a/src/app/user/user.service.ts +++ b/src/app/user/user.service.ts @@ -6,16 +6,14 @@ import { ConnectableObservable, merge, of, - ReplaySubject, - throwError + ReplaySubject } from 'rxjs'; -import { catchError, map, multicast, switchMap, tap } from 'rxjs/operators'; +import { map, multicast, switchMap, tap } from 'rxjs/operators'; import { cloneDeep } from 'lodash'; import { Broadcaster, Logger } from 'ngx-base'; import { AUTH_API_URL } from '../shared/auth-api'; -import { isAuthenticationError } from '../shared/check-auth-error'; import { User } from './user'; /** @@ -74,8 +72,11 @@ export class UserService { switchMap((val: any) => { // If it's a login event, then we need to retrieve the user's details if (val === 'loggedIn') { - return this.http.get(this.userUrl, { headers: this.headers }) - .pipe(map((response: {data: User}) => cloneDeep(response.data))); + return this.http + .get(this.userUrl, { headers: this.headers }) + .pipe( + map((response: {data: User}) => cloneDeep(response.data)) + ); } else { // Otherwise, we clear the user return of({}); @@ -101,10 +102,7 @@ export class UserService { return this.http .get(`${this.usersUrl}/${encodeURIComponent(userId)}`, { headers: this.headers }) .pipe( - map((response: {data: User}) => { - return response.data; - }), - catchError(this.catchRequestError) + map((response: {data: User}) => response.data) ); } @@ -131,10 +129,7 @@ export class UserService { return this.http .get(this.searchUrl + '/users?q=' + encodeURIComponent(search), {headers: this.headers}) .pipe( - map((response: {data: User[]}) => { - return response.data; - }), - catchError(this.catchRequestError) + map((response: {data: User[]}) => response.data) ); } return of([]); @@ -174,12 +169,9 @@ export class UserService { return this.http .get(this.usersUrl, { headers: this.headers }) .pipe( - map((response: {data: User[]}) => { - return response.data; - }), - catchError(this.catchRequestError), - // TODO remove this - tap(val => this.allUserData = val) + map((response: {data: User[]}) => response.data), + // TODO remove this + tap(val => this.allUserData = val) ); } @@ -194,22 +186,17 @@ export class UserService { return this.http .get( `${this.usersUrl}?filter[username]=${encodeURIComponent(username)}`, { headers: this.headers }) .pipe( - map((response: {data: User[]}) => { - return response.data; - }), - catchError(this.catchRequestError) + map((response: {data: User[]}) => response.data) ); } /** * Send email verification link to user. */ - sendEmailVerificationLink(): Observable { + sendEmailVerificationLink(): Observable> { + const url = this.usersUrl + '/verificationcode'; return this.http - .post(this.usersUrl + '/verificationcode', '', { headers: this.headers }) - .pipe(map((response: Response) => { - return response; - })); + .post(url, null, { headers: this.headers, observe: 'response' , responseType: 'text'}); } /** @@ -219,11 +206,4 @@ export class UserService { resetUser(): void { this.userData = {} as User; } - - private catchRequestError = (response: HttpResponse) => { - if (isAuthenticationError(response)) { - this.broadcaster.broadcast('authenticationError', response); - } - return throwError(response); - } }