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,12 +10,27 @@ 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');
// 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');
@ -31,9 +46,6 @@ export class ErrorInterceptor implements HttpInterceptor {
this.router.navigate(['login']);
}
});
}
const error = err.error.message || err.statusText;
return throwError(error);
}));
}
}

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);