Sliding session implemented by using jwt interceptor to refresh the token 10 minutes before expiry

This commit is contained in:
Amritanshu Agrawal 2020-05-30 14:09:38 +05:30
parent 8ae67863eb
commit 7edac38435
6 changed files with 84 additions and 34 deletions

View File

@ -1,13 +1,13 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Security
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from ..core.security import (
Token,
authenticate_user,
ACCESS_TOKEN_EXPIRE_MINUTES,
create_access_token, get_user,
create_access_token, get_current_active_user,
)
from ..db.session import SessionLocal
from ..schemas.auth import UserToken
@ -59,8 +59,7 @@ async def login_for_access_token(
@router.post("/refresh", response_model=Token)
async def refresh_token(
db: Session = Depends(get_db),
user: UserToken = Depends(get_user)
user: UserToken = Security(get_current_active_user)
):
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(

View File

@ -31,7 +31,6 @@ from brewman.models.voucher import (
)
from brewman.routers import get_lock_info
from brewman.core.session import get_first_day
from ..schemas import to_camel
from ..schemas.auth import UserToken
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionLocal

View File

@ -6,6 +6,9 @@ import {map} from 'rxjs/operators';
import {User} from '../core/user';
const loginUrl = '/token';
const refreshUrl = '/refresh';
const JWT_USER = 'JWT_USER';
const ACCESS_TOKEN_REFRESH_MINUTES = 10; // refresh token 10 minutes before expiry
@Injectable({providedIn: 'root'})
export class AuthService {
@ -13,7 +16,7 @@ export class AuthService {
public currentUser: Observable<User>;
constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem(JWT_USER)));
this.currentUser = this.currentUserSubject.asObservable();
}
@ -31,7 +34,7 @@ export class AuthService {
.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));
localStorage.setItem(JWT_USER, JSON.stringify(user));
this.currentUserSubject.next(user);
return user;
}));
@ -55,9 +58,36 @@ export class AuthService {
});
}
needsRefreshing(): boolean {
// We use this line to debug token refreshing
// console.log("\n", Date.now(), ": Date.now()\n", this.user.exp * 1000, ": user.exp\n",(this.user.exp - (ACCESS_TOKEN_REFRESH_MINUTES * 60)) * 1000, ": comp");
return Date.now() > (this.user.exp - (ACCESS_TOKEN_REFRESH_MINUTES * 60)) * 1000;
}
expired(): boolean {
return Date.now() > this.user.exp * 1000;
}
logout() {
// remove user from local storage to log user out
localStorage.removeItem('currentUser');
localStorage.removeItem(JWT_USER);
this.currentUserSubject.next(null);
}
getJwtToken() {
return JSON.parse(localStorage.getItem(JWT_USER)).access_token;
}
refreshToken() {
return this.http.post<any>(refreshUrl, {})
.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(JWT_USER, JSON.stringify(user));
this.currentUserSubject.next(user);
return user;
}));
}
}

View File

@ -10,30 +10,42 @@ import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private router: Router, private dialog: MatDialog, private toaster: ToasterService) { }
constructor(private authService: AuthService, private router: Router, private dialog: MatDialog, private toaster: ToasterService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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: {
title: 'Logged out!',
content: 'You have been logged out.\nYou can press Cancel to stay on page and login in another tab to resume here, or you can press Ok to navigate to the login page.'
}
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.router.navigate(['login']);
}
});
}
const error = err.error.message || err.statusText;
return throwError(error);
}));
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
// We don't want to refresh token for some requests like login or refresh token itself
// So we verify url and we throw an error if it's the case
if (request.url.includes('/refresh') || request.url.includes('/token')) {
// We do another check to see if refresh token failed
// In this case we want to logout user and to redirect it to login page
if (request.url.includes('/refresh')) {
this.authService.logout();
}
return throwError(err);
}
// If error status is different than 401 we want to skip refresh token
// So we check that and throw the error if it's the case
if (err.status !== 401) {
const error = err.error.message || err.error.detail || err.statusText;
return throwError(error);
}
// 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: {
title: 'Logged out!',
content: 'You have been logged out.\nYou can press Cancel to stay on page and login in another tab to resume here, or you can press Ok to navigate to the login page.'
}
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.router.navigate(['login']);
}
});
}));
}
}

View File

@ -6,11 +6,20 @@ import {AuthService} from '../auth/auth.service';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
private isRefreshing = false;
constructor(private authService: AuthService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add authorization header with jwt token if available
// We use this line to debug token refreshing
// console.log("intercepting:\nisRefreshing: ", this.isRefreshing, "\n user: ", this.authService.user,"\n needsRefreshing: ", this.authService.needsRefreshing());
if (!this.isRefreshing && this.authService.user && this.authService.needsRefreshing()) {
this.isRefreshing = true;
this.authService.refreshToken().subscribe( x=> this.isRefreshing = false);
}
const currentUser = this.authService.user;
if (currentUser?.access_token) {
request = request.clone({
@ -20,6 +29,7 @@ export class JwtInterceptor implements HttpInterceptor {
});
}
return next.handle(request);
}
}

View File

@ -7,7 +7,7 @@ export class User {
perms: string[];
isAuthenticated: boolean;
access_token?: string;
exp?: bigint;
exp?: number;
public constructor(init?: Partial<User>) {
Object.assign(this, init);