Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DNCR-107] Add token refreshing #31

Merged
merged 3 commits into from
Oct 31, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions app/Http/Controllers/Auth/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,26 @@ 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);
}
}
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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message to user is correct for both scenarios, but maybe error could be auth.token_error? seeing create in error with refresh made me go and check error msgs. if you think it's not important you can leave it as it is

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is message also used by JWT library, I don't want to change it in any way.

}

Expand Down
19 changes: 8 additions & 11 deletions frontend/src/app/_commons/auth/auth-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ import { Observable } from 'rxjs/Observable';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private router: Router, private authService: AuthService) {
constructor(private router: Router) {
}

canActivate(): Observable<boolean> {
let check = this.authService.check();
check.subscribe(
(result) => {
if (!result) {
// TODO: Add "login required" message ;)
this.router.navigate(['/']);
}
}
);
let isLoggedIn = AuthService.isLoggedIn();

return check;
if (!isLoggedIn){
// TODO: Add "login required" message ;)
this.router.navigate(['/']);
}

return Observable.of(isLoggedIn);
}
}
97 changes: 58 additions & 39 deletions frontend/src/app/_commons/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,74 @@
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, JwtHelper } from 'angular2-jwt';
import { Observable } from 'rxjs/Observable';
import * as moment from 'moment';
import 'rxjs/add/operator/do';
import { LoginModel } from 'app/homepage/login';
import { AuthHttp } from './http';

@Injectable()
export class AuthService {
public KNOWN_USER = 'known_user';
private TOKEN = 'id_token';

private loggedIn = false;
private storage: Storage;

constructor(private router: Router, private cookies: CookieService, private http: AuthHttp) {
this.storage = localStorage;
this.loggedIn = tokenNotExpired();
}

public login(model: LoginModel): Promise<Response> {
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
);
private static TOKEN = 'id_token';
private static KNOWN_USER = 'known_user';
private static refreshTimeoutId: any;

return request.toPromise();
constructor(private cookies: CookieService, private http: AuthHttp) {
this.scheduleTokenRefreshing();
}

public logout() {
this.http.post('/api/logout', {}).subscribe(
() => {
this.loggedIn = false;
this.storage.removeItem(this.TOKEN);
this.router.navigate(['/']);
}
);
public static clear() {
clearTimeout(AuthService.refreshTimeoutId);
localStorage.removeItem(AuthService.TOKEN);
}

public login(model: LoginModel): Observable<Response> {
return this.http.post('/api/authorize', model)
.do((response) => this.saveToken(response));
}

public logout(): Observable<Response> {
return this.http.post('/api/logout', {})
.do(() => AuthService.clear());
}

public check() {
return Observable.of(this.loggedIn);
public static isLoggedIn(): boolean {
try {
return tokenNotExpired();
} catch (e) {
return false;
}
}

public isKnownUser(): boolean {
return this.cookies.get(this.KNOWN_USER) === 'true';
return this.cookies.get(AuthService.KNOWN_USER) === 'true';
}

private saveToken(response: Response) {
localStorage.setItem(AuthService.TOKEN, response.json().token);
this.cookies.put(AuthService.KNOWN_USER, 'true');
this.scheduleTokenRefreshing();
}

private scheduleTokenRefreshing() {
if (!AuthService.isLoggedIn()) {
AuthService.clear();
return;
}

let token = localStorage.getItem(AuthService.TOKEN);
let timeout = AuthService.getTokenTimeout(token);

AuthService.refreshTimeoutId = setTimeout(
() => this.http.post('/api/refresh-token', {}).subscribe((response) => this.saveToken(response)), timeout
);
}

private static getTokenTimeout(token: string): number {
let expiry = moment(new JwtHelper().getTokenExpirationDate(token));
let now = moment();
// Subtract 1 minute to be sure token is still valid
return moment.duration(expiry.diff(now)).subtract(1, 'minute').asMilliseconds();
}
}
27 changes: 27 additions & 0 deletions frontend/src/app/_commons/auth/http.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this! till now we didn't log some 500 errors and this'll be helpful. and it's done just in one place, brilliant :).

console.error(response);
}
return Observable.of(response);
});
}
}
1 change: 1 addition & 0 deletions frontend/src/app/_commons/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './auth-guard';
export * from './auth.service';
export * from './http';
19 changes: 17 additions & 2 deletions frontend/src/app/_commons/commons.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from 'app/_commons/auth';

/*
Expand All @@ -13,14 +14,16 @@ import { AuthService } from 'app/_commons/auth';
}
)
export class App {
constructor(private authService: AuthService) {
constructor(private router: Router, private authService: AuthService) {
}

logout() {
this.authService.logout();
public logout() {
this.authService.logout().subscribe(() => {
this.router.navigate(['/']);
});
}

isLoggedIn() {
return this.authService.check();
public isLoggedIn() {
return AuthService.isLoggedIn();
}
}
21 changes: 4 additions & 17 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
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';
import { CommonsModule } from './_commons/commons.module';
import { App } from './app.component';
import { APP_RESOLVER_PROVIDERS } from './app.resolver';
import { NoContent } from './no-content';
import { Homepage } from './homepage/homepage.component';
import { Homepage, HomepageGuard } from './homepage';

// Application wide providers
const APP_PROVIDERS = [
...APP_RESOLVER_PROVIDERS, CookieService, ...AUTH_PROVIDERS, {
...APP_RESOLVER_PROVIDERS, CookieService, HomepageGuard, ...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]
}
];

Expand Down
8 changes: 5 additions & 3 deletions frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Routes } from '@angular/router';
import { NoContent } from './no-content';
import { Homepage } from './homepage/homepage.component';
import { AuthGuard } from 'app/_commons/auth';
import { NoContent } from './no-content';
import { Homepage, HomepageGuard } from './homepage';

export const ROUTES: Routes = [
{
path: '',
component: Homepage
pathMatch: 'full',
component: Homepage,
canActivate: [HomepageGuard]
},
{
path: 'reception',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/app.template.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<nav class="navbar navbar-fixed-top navbar-dark bg-inverse" *ngIf="isLoggedIn() | async">
<nav class="navbar navbar-fixed-top navbar-dark bg-inverse" *ngIf="isLoggedIn()">
<a class="navbar-brand" [routerLink]="['/']">DNCR</a>
<button class="navbar-toggler hidden-md-up" type="button" data-toggle="collapse" data-target="#navbar-header"
aria-controls="navbar-header" aria-expanded="false" aria-label="Rozwiń nawigację">
&#9776;
</button>
<div class="collapse navbar-toggleable-sm" id="navbar-header">
<ul class="nav navbar-nav" *ngIf="isLoggedIn() | async">
<ul class="nav navbar-nav" *ngIf="isLoggedIn()">
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" [routerLink]="['reception']">Recepcja</a>
</li>
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/app/attendee/attendee.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { Response } from '@angular/http';
import { AuthHttp } from 'angular2-jwt';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import { Attendee } from './attendee';
import { Course } from 'app/course';
import { AuthHttp } from 'app/_commons/auth';

@Injectable()
export class AttendeeService {
Expand All @@ -17,11 +17,7 @@ export class AttendeeService {
let url = `api/courses/${course.id}/attendees`;
return this.http.get(url)
.map((response: Response) => response.json())
.catch(
() => {
return Observable.throw('Błąd pobierania kursantów.');
}
);
.catch(() => Observable.throw('Błąd pobierania kursantów.'));
}

create(attendee: Attendee): Observable<Attendee> {
Expand Down
Loading