From 280aa33b4671b83e1b0ca8b854e879705bc8f908 Mon Sep 17 00:00:00 2001 From: Amadeusz Starzykiewicz Date: Fri, 7 Oct 2016 17:52:00 +0200 Subject: [PATCH 1/3] [DNCR-107] Add refreshing token before it's timeout --- app/Http/Controllers/Auth/AuthController.php | 15 +++- .../src/app/_commons/auth/auth.service.ts | 70 ++++++++++++------- frontend/src/app/app.component.ts | 5 +- .../src/app/homepage/login/login.component.ts | 24 ++++--- routes/api.php | 1 + 5 files changed, 75 insertions(+), 40 deletions(-) diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index 1ed001d..44e5566 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -25,7 +25,6 @@ public function login(LoginRequest $request) try { - // attempt to verify the credentials and create a token for the user if(!$token = \JWTAuth::attempt($credentials)) { return response()->json(['error' => \Lang::get('auth.failed')], 401); @@ -33,7 +32,19 @@ public function login(LoginRequest $request) } catch(JWTException $e) { - // something went wrong whilst attempting to encode the token + return response()->json(['error' => \Lang::get('auth.could_not_create_token')], 500); + } + + return response()->json(['token' => $token]); + } + public function refresh() + { + try + { + $token = \JWTAuth::refresh(); + } + catch(JWTException $e) + { return response()->json(['error' => \Lang::get('auth.could_not_create_token')], 500); } diff --git a/frontend/src/app/_commons/auth/auth.service.ts b/frontend/src/app/_commons/auth/auth.service.ts index 44d69ad..bc58a94 100755 --- a/frontend/src/app/_commons/auth/auth.service.ts +++ b/frontend/src/app/_commons/auth/auth.service.ts @@ -1,12 +1,11 @@ -import 'rxjs/add/observable/of'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { Router } from '@angular/router'; -import { LoginModel } from 'app/homepage/login'; import { CookieService } from 'angular2-cookie/core'; import { Response } from '@angular/http'; -import { tokenNotExpired, AuthHttp } from 'angular2-jwt'; -import 'rxjs/add/operator/toPromise'; +import { tokenNotExpired, AuthHttp, JwtHelper } from 'angular2-jwt'; +import { Observable } from 'rxjs/Observable'; +import * as moment from 'moment'; +import 'rxjs/add/observable/of'; +import { LoginModel } from 'app/homepage/login'; @Injectable() export class AuthService { @@ -15,34 +14,32 @@ export class AuthService { private loggedIn = false; private storage: Storage; + private refreshTimeout: any; - constructor(private router: Router, private cookies: CookieService, private http: AuthHttp) { + constructor(private cookies: CookieService, private http: AuthHttp) { this.storage = localStorage; this.loggedIn = tokenNotExpired(); + this.scheduleTokenRefreshing(); } - public login(model: LoginModel): Promise { + public login(model: LoginModel): Observable { let request = this.http.post('/api/authorize', model); - request.subscribe( - (response) => { - this.storage.setItem(this.TOKEN, response.json().token); - this.loggedIn = tokenNotExpired(); - this.cookies.put(this.KNOWN_USER, 'true'); - this.router.navigate(['/reception']); - }, (error) => error - ); + request.subscribe((response) => this.saveToken(response)); - return request.toPromise(); + return request; } - public logout() { - this.http.post('/api/logout', {}).subscribe( - () => { - this.loggedIn = false; - this.storage.removeItem(this.TOKEN); - this.router.navigate(['/']); - } - ); + public logout(): Observable { + let request = this.http.post('/api/logout', {}); + request.subscribe(() => this.clear()); + + return request; + } + + public clear() { + clearTimeout(this.refreshTimeout); + this.loggedIn = false; + this.storage.removeItem(this.TOKEN); } public check() { @@ -52,4 +49,27 @@ export class AuthService { public isKnownUser(): boolean { return this.cookies.get(this.KNOWN_USER) === 'true'; } + + private saveToken(response: Response) { + this.storage.setItem(this.TOKEN, response.json().token); + this.loggedIn = tokenNotExpired(); + this.cookies.put(this.KNOWN_USER, 'true'); + this.scheduleTokenRefreshing(); + } + + private scheduleTokenRefreshing() { + if (!this.loggedIn) { + return; + } + + let helper = new JwtHelper(); + let token = this.storage.getItem(this.TOKEN); + let expiry = helper.decodeToken(token).exp * 1000; + let now = moment().valueOf(); + let timeout = expiry - now - 60000; // Subtract 1 minute to be sure token is still valid + + this.refreshTimeout = setTimeout( + () => this.http.post('/api/refresh-token', {}).subscribe((response) => this.saveToken(response)), timeout + ); + } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9ac2e63..55fcff9 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; import { AuthService } from 'app/_commons/auth'; /* @@ -13,11 +14,11 @@ import { AuthService } from 'app/_commons/auth'; } ) export class App { - constructor(private authService: AuthService) { + constructor(private router: Router, private authService: AuthService) { } logout() { - this.authService.logout(); + this.authService.logout().subscribe(() => this.router.navigate(['/'])); } isLoggedIn() { diff --git a/frontend/src/app/homepage/login/login.component.ts b/frontend/src/app/homepage/login/login.component.ts index 564f05f..9a3acba 100755 --- a/frontend/src/app/homepage/login/login.component.ts +++ b/frontend/src/app/homepage/login/login.component.ts @@ -1,7 +1,8 @@ import { Component } from '@angular/core'; +import { NgForm } from '@angular/common'; +import { Router } from '@angular/router'; import { AuthService } from 'app/_commons/auth'; import { LoginModel } from './login.model'; -import { NgForm } from '@angular/common'; @Component( { @@ -16,7 +17,7 @@ export class LoginComponent { error = ''; isKnownUser = false; - constructor(private service: AuthService) { + constructor(private router: Router, private service: AuthService) { } ngOnInit() { @@ -24,15 +25,16 @@ export class LoginComponent { } onSubmit() { - this.service.login(this.model).catch( - (body) => { - let response = body.json(); - if (response.hasOwnProperty('error')) { - this.error = response.error; - } else { - this.error = 'Nieprawidłowy login lub hasło.'; + this.service.login(this.model) + .subscribe( + () => this.router.navigate(['/reception']), (body) => { + let response = body.json(); + if (response.hasOwnProperty('error')) { + this.error = response.error; + } else { + this.error = 'Nieprawidłowy login lub hasło.'; + } } - } - ); + ); } } diff --git a/routes/api.php b/routes/api.php index 0ab88aa..737c8b6 100755 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ Route::post('authorize', 'Auth\AuthController@login'); Route::post('logout', 'Auth\AuthController@logout')->middleware('auth'); +Route::post('refresh-token', 'Auth\AuthController@refresh')->middleware('auth'); Route::group(['middleware' => 'auth'], function(){ // Attendees From 41164ed55d4aedfb41e589a40d7d35295611a43b Mon Sep 17 00:00:00 2001 From: Amadeusz Starzykiewicz Date: Sun, 16 Oct 2016 16:51:25 +0200 Subject: [PATCH 2/3] [DNCR-107] Improve authentication error handling --- frontend/src/app/_commons/auth/auth-guard.ts | 17 +++--- .../src/app/_commons/auth/auth.service.ts | 61 ++++++++++--------- frontend/src/app/_commons/auth/http.ts | 27 ++++++++ frontend/src/app/_commons/auth/index.ts | 1 + frontend/src/app/_commons/commons.module.ts | 19 +++++- frontend/src/app/app.component.ts | 6 +- frontend/src/app/app.module.ts | 17 +----- frontend/src/app/app.template.html | 4 +- frontend/src/app/attendee/attendee.service.ts | 8 +-- frontend/src/app/course/courses.service.ts | 29 ++++----- .../src/app/homepage/homepage.component.ts | 10 +-- .../src/app/homepage/login/login.component.ts | 4 +- .../instructors/instructors.service.ts | 4 +- .../manager/locations/locations.service.ts | 4 +- 14 files changed, 119 insertions(+), 92 deletions(-) create mode 100644 frontend/src/app/_commons/auth/http.ts diff --git a/frontend/src/app/_commons/auth/auth-guard.ts b/frontend/src/app/_commons/auth/auth-guard.ts index 91b8d55..2b4265d 100644 --- a/frontend/src/app/_commons/auth/auth-guard.ts +++ b/frontend/src/app/_commons/auth/auth-guard.ts @@ -9,16 +9,13 @@ export class AuthGuard implements CanActivate { } canActivate(): Observable { - let check = this.authService.check(); - check.subscribe( - (result) => { - if (!result) { - // TODO: Add "login required" message ;) - this.router.navigate(['/']); - } - } - ); + let isLoggedIn = this.authService.isLoggedIn(); - return check; + if (!isLoggedIn){ + // TODO: Add "login required" message ;) + this.router.navigate(['/']); + } + + return Observable.of(isLoggedIn); } } diff --git a/frontend/src/app/_commons/auth/auth.service.ts b/frontend/src/app/_commons/auth/auth.service.ts index bc58a94..da3cb31 100755 --- a/frontend/src/app/_commons/auth/auth.service.ts +++ b/frontend/src/app/_commons/auth/auth.service.ts @@ -1,49 +1,52 @@ import { Injectable } from '@angular/core'; import { CookieService } from 'angular2-cookie/core'; import { Response } from '@angular/http'; -import { tokenNotExpired, AuthHttp, JwtHelper } from 'angular2-jwt'; +import { Router } from '@angular/router'; +import { tokenNotExpired, JwtHelper } from 'angular2-jwt'; import { Observable } from 'rxjs/Observable'; import * as moment from 'moment'; -import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/do'; import { LoginModel } from 'app/homepage/login'; +import { AuthHttp } from './http'; @Injectable() export class AuthService { + private static TOKEN = 'id_token'; + private static refreshTimeout: any; public KNOWN_USER = 'known_user'; - private TOKEN = 'id_token'; - private loggedIn = false; - private storage: Storage; - private refreshTimeout: any; + public static clear() { + clearTimeout(AuthService.refreshTimeout); + localStorage.removeItem(AuthService.TOKEN); + } - constructor(private cookies: CookieService, private http: AuthHttp) { - this.storage = localStorage; - this.loggedIn = tokenNotExpired(); + constructor(private cookies: CookieService, private http: AuthHttp, private router: Router) { this.scheduleTokenRefreshing(); } public login(model: LoginModel): Observable { - let request = this.http.post('/api/authorize', model); - request.subscribe((response) => this.saveToken(response)); - - return request; + return this.http.post('/api/authorize', model) + .do((response) => this.saveToken(response)); } public logout(): Observable { - let request = this.http.post('/api/logout', {}); - request.subscribe(() => this.clear()); - - return request; + return this.http.post('/api/logout', {}) + .do(() => AuthService.clear()); } - public clear() { - clearTimeout(this.refreshTimeout); - this.loggedIn = false; - this.storage.removeItem(this.TOKEN); - } + public isLoggedIn(): boolean { + try { + if (!tokenNotExpired()){ + if (this.router.url !== '/') { + this.router.navigate(['/']); + } + return false; + } - public check() { - return Observable.of(this.loggedIn); + return true; + } catch (e) { + return false; + } } public isKnownUser(): boolean { @@ -51,24 +54,24 @@ export class AuthService { } private saveToken(response: Response) { - this.storage.setItem(this.TOKEN, response.json().token); - this.loggedIn = tokenNotExpired(); + localStorage.setItem(AuthService.TOKEN, response.json().token); this.cookies.put(this.KNOWN_USER, 'true'); this.scheduleTokenRefreshing(); } private scheduleTokenRefreshing() { - if (!this.loggedIn) { + if (!this.isLoggedIn()) { + AuthService.clear(); return; } let helper = new JwtHelper(); - let token = this.storage.getItem(this.TOKEN); + let token = localStorage.getItem(AuthService.TOKEN); let expiry = helper.decodeToken(token).exp * 1000; let now = moment().valueOf(); let timeout = expiry - now - 60000; // Subtract 1 minute to be sure token is still valid - this.refreshTimeout = setTimeout( + AuthService.refreshTimeout = setTimeout( () => this.http.post('/api/refresh-token', {}).subscribe((response) => this.saveToken(response)), timeout ); } diff --git a/frontend/src/app/_commons/auth/http.ts b/frontend/src/app/_commons/auth/http.ts new file mode 100644 index 0000000..495f465 --- /dev/null +++ b/frontend/src/app/_commons/auth/http.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { Http, RequestOptions, Request, RequestOptionsArgs, Response } from '@angular/http'; +import { AuthHttp as BasicAuthHttp, AuthConfig } from 'angular2-jwt'; +import { Observable } from 'rxjs'; +import 'rxjs/add/operator/do'; +import { AuthService } from './auth.service'; + +@Injectable() +export class AuthHttp extends BasicAuthHttp { + constructor(options: AuthConfig, http: Http, defOpts?: RequestOptions) { + super(options, http, defOpts); + } + + request(url: string | Request, options?: RequestOptionsArgs): Observable { + return super.request(url, options) + .catch((response: Response) => { + if ([401, 403].indexOf(response.status) !== -1) { + // TODO: Add notification about invalid response - logout + AuthService.clear(); + } + if (response.status === 500) { + console.error(response); + } + return Observable.of(response); + }); + } +} diff --git a/frontend/src/app/_commons/auth/index.ts b/frontend/src/app/_commons/auth/index.ts index 7f8160a..351e1de 100644 --- a/frontend/src/app/_commons/auth/index.ts +++ b/frontend/src/app/_commons/auth/index.ts @@ -1,2 +1,3 @@ export * from './auth-guard'; export * from './auth.service'; +export * from './http'; diff --git a/frontend/src/app/_commons/commons.module.ts b/frontend/src/app/_commons/commons.module.ts index 614023f..76c985c 100644 --- a/frontend/src/app/_commons/commons.module.ts +++ b/frontend/src/app/_commons/commons.module.ts @@ -2,10 +2,12 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { ScheduleModule } from 'primeng/components/schedule/schedule'; +import { Http } from '@angular/http'; +import { AuthConfig } from 'angular2-jwt'; import { FormField } from './form-field'; import { CalendarComponent } from './calendar'; import { AddItemButtonComponent } from './add-item-button'; -import { AuthService, AuthGuard } from './auth'; +import { AuthHttp, AuthService, AuthGuard } from './auth'; import { LabelledFormField } from './labelled-form-field'; @NgModule( @@ -17,7 +19,20 @@ import { LabelledFormField } from './labelled-form-field'; BrowserModule, FormsModule, ScheduleModule ], providers: [ - AuthService, AuthGuard + AuthService, AuthGuard, { + provide: AuthHttp, + useFactory: (http) => { + return new AuthHttp( + new AuthConfig( + { + globalHeaders: [{ 'Content-Type': 'application/json' }, { 'Accept': 'application/json' }], + noJwtError: true + } + ), http + ); + }, + deps: [Http] + } ], exports: [ FormField, LabelledFormField, CalendarComponent, AddItemButtonComponent diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 55fcff9..d6b96ec 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -18,10 +18,12 @@ export class App { } logout() { - this.authService.logout().subscribe(() => this.router.navigate(['/'])); + this.authService.logout().subscribe(() => { + this.router.navigate(['/']); + }); } isLoggedIn() { - return this.authService.check(); + return this.authService.isLoggedIn(); } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 7d65858..04cbb20 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; -import { HttpModule, XSRFStrategy, CookieXSRFStrategy, Http } from '@angular/http'; +import { HttpModule, XSRFStrategy, CookieXSRFStrategy } from '@angular/http'; import { RouterModule } from '@angular/router'; -import { AUTH_PROVIDERS, AuthHttp, AuthConfig } from 'angular2-jwt'; +import { AUTH_PROVIDERS } from 'angular2-jwt'; import { CookieService } from 'angular2-cookie/services/cookies.service'; import { ENV_PROVIDERS } from './environment'; import { ROUTES } from './app.routes'; @@ -18,19 +18,6 @@ const APP_PROVIDERS = [ ...APP_RESOLVER_PROVIDERS, CookieService, ...AUTH_PROVIDERS, { provide: XSRFStrategy, useValue: new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN') - }, { - provide: AuthHttp, - useFactory: (http) => { - return new AuthHttp( - new AuthConfig( - { - globalHeaders: [{ 'Content-Type': 'application/json' }, { 'Accept': 'application/json' }], - noJwtError: true - } - ), http - ); - }, - deps: [Http] } ]; diff --git a/frontend/src/app/app.template.html b/frontend/src/app/app.template.html index d0ada4f..8b7a9ec 100755 --- a/frontend/src/app/app.template.html +++ b/frontend/src/app/app.template.html @@ -1,11 +1,11 @@ -