From ae8c46084c832a0daebd6c168de61066df2f9d9b Mon Sep 17 00:00:00 2001 From: tanshu Date: Mon, 11 May 2020 23:45:52 +0530 Subject: [PATCH] Working as a drop-in replacement for the last --- overlord/src/app/auth/auth-guard.service.ts | 48 ++++----- overlord/src/app/auth/auth.service.ts | 101 ++++++++---------- overlord/src/app/core/core.module.ts | 12 +-- .../src/app/core/http-auth-interceptor.ts | 43 ++++---- overlord/src/app/core/jwt.interceptor.ts | 25 +++++ 5 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 overlord/src/app/core/jwt.interceptor.ts diff --git a/overlord/src/app/auth/auth-guard.service.ts b/overlord/src/app/auth/auth-guard.service.ts index e1763d13..42bba6a6 100644 --- a/overlord/src/app/auth/auth-guard.service.ts +++ b/overlord/src/app/auth/auth-guard.service.ts @@ -1,41 +1,33 @@ import {Injectable} from '@angular/core'; -import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; +import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router'; + import {AuthService} from './auth.service'; -import {Observable} from 'rxjs/internal/Observable'; -import {map} from 'rxjs/operators'; import {ToasterService} from '../core/toaster.service'; -import {User} from '../core/user'; -@Injectable({ - providedIn: 'root' -}) +@Injectable({providedIn: 'root'}) export class AuthGuard implements CanActivate { - - constructor(private auth: AuthService, private router: Router, private toaster: ToasterService) { + constructor( + private router: Router, + private authService: AuthService, + private toaster: ToasterService + ) { } - static checkUser(permission: string, user: User, router: Router, state: RouterStateSnapshot, toaster: ToasterService): boolean { - if (!user.isAuthenticated) { - router.navigate(['login'], {queryParams: {returnUrl: state.url}}); - toaster.show('Danger', 'User is not authenticated'); + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + const user = this.authService.user; + const permission = route.data['permission'].replace(/ /g, '-').toLowerCase(); + if (!user) { + // not logged in so redirect to login page with the return url + this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}}); return false; } - const hasPermission = permission === undefined || user.perms.indexOf(permission) !== -1; - if (!hasPermission) { - toaster.show('Danger', 'You do not have the permission to access this area.'); + console.log(permission, user.perms.indexOf(permission)); + if (permission !== undefined && user.perms.indexOf(permission) === -1) { + this.toaster.show('Danger', 'You do not have the permission to access this area.'); + return false; } - return hasPermission; - } + // logged in so return true + return true; - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { - if (this.auth.user === undefined) { - return this.auth.userObservable - .pipe( - map((value: User) => AuthGuard.checkUser( - route.data['permission'], value, this.router, state, this.toaster - )) - ); - } - return AuthGuard.checkUser(route.data['permission'], this.auth.user, this.router, state, this.toaster); } } diff --git a/overlord/src/app/auth/auth.service.ts b/overlord/src/app/auth/auth.service.ts index 0d5d8046..78dbcf18 100644 --- a/overlord/src/app/auth/auth.service.ts +++ b/overlord/src/app/auth/auth.service.ts @@ -1,72 +1,63 @@ import {Injectable} from '@angular/core'; -import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {Observable} from 'rxjs/internal/Observable'; -import {catchError, tap} from 'rxjs/operators'; -import {Subject} from 'rxjs/internal/Subject'; -import {ErrorLoggerService} from '../core/error-logger.service'; +import {HttpClient} from '@angular/common/http'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; + import {User} from '../core/user'; +const loginUrl = '/token'; -const httpOptions = { - headers: new HttpHeaders({'Content-Type': 'application/json'}) -}; - -const loginUrl = '/api/login'; -const logoutUrl = '/api/logout'; -const checkUrl = '/api/auth'; - -@Injectable({ - providedIn: 'root' -}) +@Injectable({providedIn: 'root'}) export class AuthService { - public userObservable = new Subject(); + private currentUserSubject: BehaviorSubject; + public currentUser: Observable; - constructor(private http: HttpClient, private log: ErrorLoggerService) { - this.check().subscribe(); + constructor(private http: HttpClient) { + this.currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('currentUser'))); + this.currentUser = this.currentUserSubject.asObservable(); } - private _user: User; - - get user() { - return this._user; + public get user(): User { + return this.currentUserSubject.value; } - set user(user: User) { - this._user = user; - this.userObservable.next(user); + login(username: string, password: string) { + const formData: FormData = new FormData(); + formData.append('username', username); + formData.append('password', password); + formData.append('grant_type', 'password'); + return this.http.post(loginUrl, formData) + .pipe(map(u => u.access_token)) + .pipe(map(u => this.parseJwt(u))) + .pipe(map(user => { + // store user details and jwt token in local storage to keep user logged in between page refreshes + localStorage.setItem('currentUser', JSON.stringify(user)); + this.currentUserSubject.next(user); + return user; + })); } - login(name: string, password: string, otp?: string, clientName?: string): Observable { - const data = {name: name, password: password}; - if (otp) { - data['otp'] = otp; - } + parseJwt(token): User { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); - if (clientName) { - data['clientName'] = clientName; - } - - return this.http.post(loginUrl, data, httpOptions) - .pipe( - tap((user: User) => this.user = user), - catchError(this.log.handleError('AuthService', 'login')) - ); + const decoded = JSON.parse(jsonPayload); + return new User({ + id: decoded.userId, + name: decoded.sub, + lockedOut: decoded.lockedOut, + perms: decoded.scopes, + access_token: token, + exp: decoded.exp + }); } - logout(): Observable { - return this.http.post(logoutUrl, {}, httpOptions) - .pipe( - tap((user: User) => this.user = user), - catchError(this.log.handleError('AuthService', 'logout')) - ); + logout() { + // remove user from local storage to log user out + localStorage.removeItem('currentUser'); + this.currentUserSubject.next(null); } - - check(): Observable { - return this.http.get(checkUrl, httpOptions) - .pipe( - tap((user: User) => this.user = user), - catchError(this.log.handleError('AuthService', 'check')) - ); - } - } diff --git a/overlord/src/app/core/core.module.ts b/overlord/src/app/core/core.module.ts index 265f511a..e0c80de0 100644 --- a/overlord/src/app/core/core.module.ts +++ b/overlord/src/app/core/core.module.ts @@ -7,9 +7,10 @@ import { MatMenuModule } from '@angular/material/menu'; import { MatToolbarModule } from '@angular/material/toolbar'; import {RouterModule} from '@angular/router'; import {HTTP_INTERCEPTORS} from '@angular/common/http'; -import {HttpAuthInterceptor} from './http-auth-interceptor'; import {LoadingBarHttpClientModule} from '@ngx-loading-bar/http-client'; import {LoadingBarRouterModule} from '@ngx-loading-bar/router'; +import {JwtInterceptor} from './jwt.interceptor'; +import {ErrorInterceptor} from './http-auth-interceptor'; @NgModule({ imports: [ @@ -30,11 +31,10 @@ import {LoadingBarRouterModule} from '@ngx-loading-bar/router'; LoadingBarHttpClientModule, LoadingBarRouterModule ], - providers: [[{ - provide: HTTP_INTERCEPTORS, - useClass: HttpAuthInterceptor, - multi: true - }]] + providers: [ + {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, + {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}, + ] }) export class CoreModule { } diff --git a/overlord/src/app/core/http-auth-interceptor.ts b/overlord/src/app/core/http-auth-interceptor.ts index 2d8b99f9..f4f9a139 100644 --- a/overlord/src/app/core/http-auth-interceptor.ts +++ b/overlord/src/app/core/http-auth-interceptor.ts @@ -1,26 +1,24 @@ -import {Injectable} from '@angular/core'; -import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; - -import {Observable, throwError} from 'rxjs'; -import {catchError} from 'rxjs/operators'; -import {ToasterService} from './toaster.service'; -import {Router} from '@angular/router'; -import {ConfirmDialogComponent} from '../shared/confirm-dialog/confirm-dialog.component'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; +import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { AuthService } from '../auth/auth.service'; +import { ToasterService } from './toaster.service'; +import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component'; @Injectable() -export class HttpAuthInterceptor implements HttpInterceptor { +export class ErrorInterceptor implements HttpInterceptor { + constructor(private authService: AuthService, private router: Router, private dialog: MatDialog, private toaster: ToasterService) { } - constructor(private router: Router, private dialog: MatDialog, private toaster: ToasterService) { - } - - intercept(req: HttpRequest, next: HttpHandler): - Observable> { - return next.handle(req) - .pipe( - catchError((error: any) => { - if (error.status === 401) { - this.toaster.show('Danger', 'User has been logged out'); + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe(catchError(err => { + if (err.status === 401) { + console.log('caught 401 in error interceptor'); + // auto logout if 401 response returned from api + this.authService.logout(); + this.toaster.show('Danger', 'User has been logged out'); const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '250px', data: { @@ -34,9 +32,8 @@ export class HttpAuthInterceptor implements HttpInterceptor { } }); } + const error = err.error.message || err.statusText; return throwError(error); - } - ) - ); - } + })); + } } diff --git a/overlord/src/app/core/jwt.interceptor.ts b/overlord/src/app/core/jwt.interceptor.ts new file mode 100644 index 00000000..8deb5d65 --- /dev/null +++ b/overlord/src/app/core/jwt.interceptor.ts @@ -0,0 +1,25 @@ +import {Injectable} from '@angular/core'; +import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@angular/common/http'; +import {Observable} from 'rxjs'; + +import {AuthService} from '../auth/auth.service'; + +@Injectable() +export class JwtInterceptor implements HttpInterceptor { + constructor(private authService: AuthService) { + } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // add authorization header with jwt token if available + const currentUser = this.authService.user; + if (currentUser && currentUser.access_token) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${currentUser.access_token}` + } + }); + } + + return next.handle(request); + } +}