Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): refresh expired token instead of logout immediately #645

Merged
merged 3 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 register(Request $request)
{
if (User::where('email', $request->email)->exists()) {
return response()->json([
'message' => 'Internal Server Error',
], 500);

Check warning on line 25 in backend/app/Http/Controllers/AuthController.php

View check run for this annotation

Codecov / codecov/patch

backend/app/Http/Controllers/AuthController.php#L23-L25

Added lines #L23 - L25 were not covered by tests
}

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

Check warning on line 111 in backend/app/Http/Controllers/AuthController.php

View check run for this annotation

Codecov / codecov/patch

backend/app/Http/Controllers/AuthController.php#L111

Added line #L111 was not covered by tests
}

/**
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
Loading