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 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(
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user