Skip to content

Commit

Permalink
feat(auth): refresh expired token instead of logout immediately (#645)
Browse files Browse the repository at this point in the history
  • Loading branch information
cayacdev authored Jul 11, 2024
1 parent 04d81d1 commit b1bdc5e
Show file tree
Hide file tree
Showing 34 changed files with 672 additions and 672 deletions.
8 changes: 7 additions & 1 deletion backend/app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public function __construct()

public function register(Request $request)
{
if (User::where('email', $request->email)->exists()) {
return response()->json([
'message' => 'Internal Server Error',
], 500);
}

$this->validate($request, [
'name' => 'required',
'email' => 'required|email',
Expand Down Expand Up @@ -102,7 +108,7 @@ public function logout()
*/
public function refresh()
{
return $this->respondWithToken(auth()->refresh());
return $this->respondWithToken(auth()->refresh(true));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion backend/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
Route::post('login', 'AuthController@login');
Route::put('changePassword', 'AuthController@changePassword');
Route::post('logout', 'AuthController@logout');
Route::post('refresh', 'AuthController@refresh');
Route::post('refresh', ['uses' => 'AuthController@refresh', 'middleware' => 'jwt.refresh']);
Route::get('me', 'AuthController@me');
});

Expand Down
25 changes: 18 additions & 7 deletions backend/sample-requests.http
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
### Authorization by token, part 1. Retrieve and save token.
### Register a new user
POST {{domain}}/v1/auth/register
Content-Type: application/json

{
"email": "test@mailinator.com",
"email": "test1@test.com",
"name": "Test",
"password": "123456"
}

### Authorization by token, part 1. Retrieve and save token.
### Login with the registered user
POST {{domain}}/v1/auth/login
Content-Type: application/json

{
"email": "test@mailinator.com",
"password": "123456"
"email": "test1@test.com",
"password": "12345678"
}

> {% client.global.set("auth_token", response.body.access_token); %}
> {%
client.global.set("auth_token", response.body.access_token);
%}

### Refresh token
POST {{domain}}/v1/auth/refresh
Accept: application/json
Authorization: Bearer {{auth_token}}

> {%
client.global.set("auth_token", response.body.access_token);
%}

### Authorization by token, part 2. Use token to authorize.
### Get user information
GET {{domain}}/v1/auth/me
Accept: application/json
Authorization: Bearer {{auth_token}}
Expand Down
63 changes: 48 additions & 15 deletions frontend/src/app/auth/auth-interceptor.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { exhaustMap, map, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import * as fromApp from '../store/app.reducer';
import { Injectable } from '@angular/core'
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http'
import { Observable, throwError } from 'rxjs'
import { catchError, filter, map, switchMap, take, takeWhile } from 'rxjs/operators'
import { Store } from '@ngrx/store'
import * as fromApp from '../store/app.reducer'
import * as AuthActions from './store/auth.actions'

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
Expand All @@ -13,17 +14,49 @@ export class AuthInterceptorService implements HttpInterceptor {
return this.store.select('auth').pipe(
take(1),
map((authState) => {
return authState.user;
return authState.user
}),
exhaustMap((user) => {
switchMap((user) => {
if (!user) {
return next.handle(req);
return next.handle(req)
}
const modifiedReq = req.clone({
headers: new HttpHeaders().set('Authorization', `Bearer ${user.token}`),
});
return next.handle(modifiedReq);
})
);
const modifiedReq = this.modifyRequest(req, user.token)
return next.handle(modifiedReq).pipe(catchError((x) => this.handleAuthError(x, next, modifiedReq)))
}),
)
}

private handleAuthError(err: HttpErrorResponse, next: HttpHandler, modifiedReq: HttpRequest<unknown>): Observable<HttpEvent<unknown>> {
if (err.status === 401 && err.url?.includes('refresh') === false) {
return this.refreshToken$().pipe(
switchMap((token) => {
const newReq = this.modifyRequest(modifiedReq, token)
return next.handle(newReq).pipe(
catchError((err) => {
this.store.dispatch(new AuthActions.Logout())
return throwError(err)
}),
)
}),
)
}

return throwError(err)
}

private modifyRequest(request: HttpRequest<unknown>, token: string) {
return request.clone({
headers: new HttpHeaders().set('Authorization', `Bearer ${token}`),
})
}

private refreshToken$(): Observable<string> {
this.store.dispatch(new AuthActions.RefreshToken())

return this.store.select('auth').pipe(
takeWhile((authState) => authState.loading, true),
filter((authState) => !authState.loading),
map((authState) => authState.user?.token ?? ''),
)
}
}
38 changes: 19 additions & 19 deletions frontend/src/app/auth/auth.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import * as AuthActions from './store/auth.actions';
import { Store } from '@ngrx/store';
import * as fromApp from '../store/app.reducer';
import { Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core'
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'
import * as AuthActions from './store/auth.actions'
import { Store } from '@ngrx/store'
import * as fromApp from '../store/app.reducer'
import { Subscription } from 'rxjs'

@Component({
selector: 'app-auth',
Expand All @@ -14,30 +14,30 @@ export class AuthComponent implements OnInit {
form: UntypedFormGroup = new UntypedFormGroup({
email: new UntypedFormControl(''),
password: new UntypedFormControl(''),
});
isLoginMode = true;
isLoading = false;
error: string;
private storeSub: Subscription;
})
isLoginMode = true
isLoading = false
error?: string
private storeSub: Subscription

constructor(private store: Store<fromApp.AppState>) {}

ngOnInit(): void {
this.storeSub = this.store.select('auth').subscribe((authState) => {
this.isLoading = authState.loading;
this.error = authState.authError;
});
this.isLoading = authState.loading
this.error = authState.authError
})
}

onSubmit(): void {
if (!this.form.valid) {
return;
return
}
const email = this.form.value.email;
const password = this.form.value.password;
const email = this.form.value.email
const password = this.form.value.password

this.store.dispatch(new AuthActions.LoginStart({ email, password }));
this.store.dispatch(new AuthActions.LoginStart({ email, password }))

this.form.reset();
this.form.reset()
}
}
2 changes: 2 additions & 0 deletions frontend/src/app/auth/auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('AuthGuard', () => {
it('should return true if user is authenticated', (done) => {
store.setState({ auth: { user: { id: '1', email: '[email protected]' } } })

// @ts-ignore
const canActivate = guard.canActivate(null, null) as Observable<boolean>

canActivate.subscribe((isAuth) => {
Expand All @@ -51,6 +52,7 @@ describe('AuthGuard', () => {
const urlTree = router.createUrlTree(['/auth'])

createUrlTreeSpy.and.returnValue(urlTree)
// @ts-ignore
const canActivate = guard.canActivate(null, null) as Observable<boolean | UrlTree>

canActivate.subscribe((result) => {
Expand Down
30 changes: 10 additions & 20 deletions frontend/src/app/auth/auth.model.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
// FILEPATH: /Users/pmueller/workspace/intern/cashbox/frontend/src/app/auth/auth.model.spec.ts

import { Auth } from './auth.model';
import { Auth } from './auth.model'

describe('Auth', () => {
let auth: Auth;
let auth: Auth

beforeEach(() => {
auth = new Auth('[email protected]', 'token', new Date());
});
auth = new Auth('[email protected]', 'token', new Date())
})

it('should be created', () => {
expect(auth).toBeTruthy();
});
expect(auth).toBeTruthy()
})

it('should return token if token expiration date is in the future', () => {
auth = new Auth('[email protected]', 'token', new Date(new Date().getTime() + 10000));
expect(auth.token).toEqual('token');
});

it('should return null if token expiration date is in the past', () => {
auth = new Auth('[email protected]', 'token', new Date(new Date().getTime() - 10000));
expect(auth.token).toBeNull();
});

it('should return null if token expiration date is not set', () => {
auth = new Auth('[email protected]', 'token', null);
expect(auth.token).toBeNull();
});
});
auth = new Auth('[email protected]', 'token', new Date(new Date().getTime() + 10000))
expect(auth.token).toEqual('token')
})
})
3 changes: 0 additions & 3 deletions frontend/src/app/auth/auth.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ export class Auth {
) {}

get token(): string {
if (!this._tokenExpirationDate || new Date() > this._tokenExpirationDate) {
return null
}
return this._token
}
}
62 changes: 0 additions & 62 deletions frontend/src/app/auth/auth.service.spec.ts

This file was deleted.

25 changes: 0 additions & 25 deletions frontend/src/app/auth/auth.service.ts

This file was deleted.

Loading

0 comments on commit b1bdc5e

Please sign in to comment.