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,12 +10,27 @@ 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
|
||||||
|
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
|
// auto logout if 401 response returned from api
|
||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
this.toaster.show('Danger', 'User has been logged out');
|
this.toaster.show('Danger', 'User has been logged out');
|
||||||
@ -31,9 +46,6 @@ export class ErrorInterceptor implements HttpInterceptor {
|
|||||||
this.router.navigate(['login']);
|
this.router.navigate(['login']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const error = err.error.message || err.statusText;
|
|
||||||
return throwError(error);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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