From 5b5bc57375c01de1cf789a1c5b26111fe75ed227 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 19 Jul 2017 12:47:24 +0200 Subject: [PATCH] Auth improved. --- .../interceptors/auth.interceptor.spec.ts | 20 ++++++- .../shared/interceptors/auth.interceptor.ts | 52 +++++++++++-------- .../app/shared/services/auth.service.ts | 30 ++++++++--- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts b/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts index 9f5d379ef..13bf0ba28 100644 --- a/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts +++ b/src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts @@ -104,7 +104,25 @@ describe('AuthInterceptor', () => { authService.verify(x => x.logoutRedirect(), Times.once()); })); - [401, 403].forEach(statusCode => { + it(`should logout for 401 status code`, + inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { + + authService.setup(x => x.userChanges).returns(() => { return Observable.of({ authToken: 'letmein' }); }); + authService.setup(x => x.loginSilent()).returns(() => { return Observable.of({ authToken: 'letmereallyin' }); }); + + http.get('http://service/p/apps').subscribe( + _ => { /* NOOP */ }, + _ => { /* NOOP */ }); + + // const req = httpMock.expectOne('http://service/p/apps'); + + httpMock.expectOne('http://service/p/apps').error({}, { status: 401 }); + httpMock.expectOne('http://service/p/apps').error({}, { status: 401 }); + + authService.verify(x => x.logoutRedirect(), Times.once()); + })); + + [403].forEach(statusCode => { it(`should logout for ${statusCode} status code`, inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/interceptors/auth.interceptor.ts b/src/Squidex/app/shared/interceptors/auth.interceptor.ts index e5efce3cc..0cdd8f79a 100644 --- a/src/Squidex/app/shared/interceptors/auth.interceptor.ts +++ b/src/Squidex/app/shared/interceptors/auth.interceptor.ts @@ -9,7 +9,7 @@ import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse import { Injectable} from '@angular/core'; import { Observable } from 'rxjs'; -import { AuthService } from './../services/auth.service'; +import { AuthService, Profile } from './../services/auth.service'; import { ApiUrlConfig } from 'framework'; @Injectable() @@ -25,31 +25,37 @@ export class AuthInterceptor implements HttpInterceptor { public intercept(req: HttpRequest, next: HttpHandler): Observable> { if (req.url.indexOf(this.baseUrl) === 0 && !req.headers.has('NoAuth')) { return this.authService.userChanges.first().switchMap(user => { - const token = user ? user.authToken : ''; - - const authReq = req.clone({ - headers: req.headers - .set('Authorization', token) - .set('Accept-Language', '*') - .set('Pragma', 'no-cache') - }); - - return next.handle(authReq) - .catch((error: HttpErrorResponse) => { - if (error.status === 404 && (!user || user.isExpired)) { - this.authService.logoutRedirect(); - - return Observable.empty(); - } else if (error.status === 401 || error.status === 403) { - this.authService.logoutRedirect(); - - return Observable.empty(); - } - return Observable.throw(error); - }); + return this.makeRequest(req, next, user, true); }); } else { return next.handle(req); } } + + private makeRequest(req: HttpRequest, next: HttpHandler, user: Profile, renew = false): Observable> { + const token = user ? user.authToken : ''; + + const authReq = req.clone({ + headers: req.headers + .set('Authorization', token) + .set('Accept-Language', '*') + .set('Pragma', 'no-cache') + }); + + return next.handle(authReq) + .catch((error: HttpErrorResponse) => { + if (error.status === 401 && renew) { + return this.authService.loginSilent().switchMap(u => this.makeRequest(req, next, u)); + } else if (error.status === 404 && (!user || user.isExpired)) { + this.authService.logoutRedirect(); + + return Observable.empty(); + }else if (error.status === 401 || error.status === 403) { + this.authService.logoutRedirect(); + + return Observable.empty(); + } + return Observable.throw(error); + }); + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index 4caef7624..308bce9e2 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -11,7 +11,8 @@ import { Observable, ReplaySubject } from 'rxjs'; import { Log, User, - UserManager + UserManager, + WebStorageStateStore } from 'oidc-client'; import { ApiUrlConfig } from 'framework'; @@ -81,6 +82,7 @@ export class AuthService { silent_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-silent/'), popup_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-popup/'), authority: apiUrl.buildUrl('identity-server/'), + userStore: new WebStorageStateStore({ store: window.localStorage || window.sessionStorage }), automaticSilentRenew: true }); @@ -96,7 +98,7 @@ export class AuthService { this.currentUser = user; }); - this.checkState(this.userManager.signinSilent()); + this.checkState(this.userManager.getUser()); } public logoutRedirect(): Observable { @@ -107,20 +109,32 @@ export class AuthService { return Observable.fromPromise(this.userManager.signoutRedirectCallback()); } - public loginPopup(): Observable { - return Observable.fromPromise(this.userManager.signinPopup()); + public loginPopup(): Observable { + return Observable.fromPromise(this.userManager.signinPopup()).map(u => this.createProfile(u)); + } + + public loginSilent(): Observable { + return Observable.fromPromise(this.userManager.signinSilent()).map(u => this.createProfile(u)); } public loginRedirect(): Observable { return Observable.fromPromise(this.userManager.signinRedirect()); } - public loginRedirectComplete(): Observable { - return Observable.fromPromise(this.userManager.signinRedirectCallback()); + public loginRedirectComplete(): Observable { + return Observable.fromPromise(this.userManager.signinRedirectCallback()).map(u => this.createProfile(u)); } - private checkState(promise: Promise) { - promise.catch((err) => { + private createProfile(user: User) { + return user ? new Profile(user) : null; + } + + private checkState(promise: Promise) { + promise.then(user => { + this.user$.next(this.createProfile(user)); + + return true; + }, err => { this.user$.next(null); return false;