Sliding session implemented by using jwt interceptor to refresh the token 10 minutes before expiry
This commit is contained in:
parent
8ae67863eb
commit
7edac38435
brewman/routers
overlord/src/app
@ -1,13 +1,13 @@
|
|||||||
from datetime import timedelta
|
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 fastapi.security import OAuth2PasswordRequestForm
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from ..core.security import (
|
from ..core.security import (
|
||||||
Token,
|
Token,
|
||||||
authenticate_user,
|
authenticate_user,
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES,
|
ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||||
create_access_token, get_user,
|
create_access_token, get_current_active_user,
|
||||||
)
|
)
|
||||||
from ..db.session import SessionLocal
|
from ..db.session import SessionLocal
|
||||||
from ..schemas.auth import UserToken
|
from ..schemas.auth import UserToken
|
||||||
@ -59,8 +59,7 @@ async def login_for_access_token(
|
|||||||
|
|
||||||
@router.post("/refresh", response_model=Token)
|
@router.post("/refresh", response_model=Token)
|
||||||
async def refresh_token(
|
async def refresh_token(
|
||||||
db: Session = Depends(get_db),
|
user: UserToken = Security(get_current_active_user)
|
||||||
user: UserToken = Depends(get_user)
|
|
||||||
):
|
):
|
||||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
access_token = create_access_token(
|
access_token = create_access_token(
|
||||||
|
@ -31,7 +31,6 @@ from brewman.models.voucher import (
|
|||||||
)
|
)
|
||||||
from brewman.routers import get_lock_info
|
from brewman.routers import get_lock_info
|
||||||
from brewman.core.session import get_first_day
|
from brewman.core.session import get_first_day
|
||||||
from ..schemas import to_camel
|
|
||||||
from ..schemas.auth import UserToken
|
from ..schemas.auth import UserToken
|
||||||
from ..core.security import get_current_active_user as get_user
|
from ..core.security import get_current_active_user as get_user
|
||||||
from ..db.session import SessionLocal
|
from ..db.session import SessionLocal
|
||||||
|
@ -6,6 +6,9 @@ import {map} from 'rxjs/operators';
|
|||||||
import {User} from '../core/user';
|
import {User} from '../core/user';
|
||||||
|
|
||||||
const loginUrl = '/token';
|
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'})
|
@Injectable({providedIn: 'root'})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -13,7 +16,7 @@ export class AuthService {
|
|||||||
public currentUser: Observable<User>;
|
public currentUser: Observable<User>;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
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();
|
this.currentUser = this.currentUserSubject.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +34,7 @@ export class AuthService {
|
|||||||
.pipe(map(u => this.parseJwt(u)))
|
.pipe(map(u => this.parseJwt(u)))
|
||||||
.pipe(map(user => {
|
.pipe(map(user => {
|
||||||
// store user details and jwt token in local storage to keep user logged in between page refreshes
|
// 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);
|
this.currentUserSubject.next(user);
|
||||||
return 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() {
|
logout() {
|
||||||
// remove user from local storage to log user out
|
// remove user from local storage to log user out
|
||||||
localStorage.removeItem('currentUser');
|
localStorage.removeItem(JWT_USER);
|
||||||
this.currentUserSubject.next(null);
|
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;
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,30 +10,42 @@ import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ErrorInterceptor implements HttpInterceptor {
|
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>> {
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
return next.handle(request).pipe(catchError(err => {
|
return next.handle(request).pipe(catchError(err => {
|
||||||
if (err.status === 401) {
|
// We don't want to refresh token for some requests like login or refresh token itself
|
||||||
console.log('caught 401 in error interceptor');
|
// So we verify url and we throw an error if it's the case
|
||||||
// auto logout if 401 response returned from api
|
if (request.url.includes('/refresh') || request.url.includes('/token')) {
|
||||||
this.authService.logout();
|
// We do another check to see if refresh token failed
|
||||||
this.toaster.show('Danger', 'User has been logged out');
|
// In this case we want to logout user and to redirect it to login page
|
||||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
if (request.url.includes('/refresh')) {
|
||||||
width: '250px',
|
this.authService.logout();
|
||||||
data: {
|
}
|
||||||
title: 'Logged out!',
|
return throwError(err);
|
||||||
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.'
|
}
|
||||||
}
|
// 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
|
||||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
if (err.status !== 401) {
|
||||||
if (result) {
|
const error = err.error.message || err.error.detail || err.statusText;
|
||||||
this.router.navigate(['login']);
|
return throwError(error);
|
||||||
}
|
}
|
||||||
});
|
// auto logout if 401 response returned from api
|
||||||
}
|
this.authService.logout();
|
||||||
const error = err.error.message || err.statusText;
|
this.toaster.show('Danger', 'User has been logged out');
|
||||||
return throwError(error);
|
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']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,20 @@ import {AuthService} from '../auth/auth.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtInterceptor implements HttpInterceptor {
|
export class JwtInterceptor implements HttpInterceptor {
|
||||||
|
private isRefreshing = false;
|
||||||
|
|
||||||
constructor(private authService: AuthService) {
|
constructor(private authService: AuthService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
// add authorization header with jwt token if available
|
// 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;
|
const currentUser = this.authService.user;
|
||||||
if (currentUser?.access_token) {
|
if (currentUser?.access_token) {
|
||||||
request = request.clone({
|
request = request.clone({
|
||||||
@ -20,6 +29,7 @@ export class JwtInterceptor implements HttpInterceptor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return next.handle(request);
|
return next.handle(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export class User {
|
|||||||
perms: string[];
|
perms: string[];
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
access_token?: string;
|
access_token?: string;
|
||||||
exp?: bigint;
|
exp?: number;
|
||||||
|
|
||||||
public constructor(init?: Partial<User>) {
|
public constructor(init?: Partial<User>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user