-
Notifications
You must be signed in to change notification settings - Fork 0
task#25 create an authentication service
The goal of this task is to create an authentication service, which handles the login and logout procedure. The successful login will store the current user, the token and the expired date in the local store or session store of the browser depending on the remember me flag.
Overview about the keys in the local/session store
| key | description |
|---|---|
| currentUser | The current user object |
| authenticationToken | the jwtToken (without Bearer) |
| expires_at | the expires date of the token |
- Create the files
browser-storage-service.tsandbrowser-storage-service.spec.tsin the foldersrc/app/shared/service. - Write functionality to abstract the call to
SessionStorageorLocalStoragedepending on therememberMeflag. If remeberMe is true then use the LocalStore. - Create the file
authentication.service.tsin the foldersrc/app/shared/service. - Create the login method which takes the argument
login(username, password, rememberMe=true). - Call the backend
${this.authApiUrl}/users/authenticatewith a post and submit username and password as an object. - Save the
authenticationToken, the expired dateexpires_atin thecurrentUserobject in the local store or session store depending of therememberMeflag. Update the User class with the new fieldsexpires,token,refreshToken,rolesandpublicKey. - Add a
refresh_tokenfunctionality. One minute before the token expires, therefresh_token apiis delivering a new token and a new refresh token. - Add the
refresh_tokenfunctionality to the mock backend. - Create a new MockUser class to save tokens and refresh tokens. Create conversion functions between User and MockUser.
- Create a
logout()method, which removes thecurrentUserobject from the local/session store. - Create a unit test
authentication.service.spec.tsin the foldersrc/app/shared/serviceto verify the functionality. - Expand the
user modelto store theexpires,roles,token,refreshTokenin the user object
Verify the result by running the unit tests
Generate the service:
ng generate service shared/service/browser-storage
The service browser-storage.service.ts has been created in the folder src/app/shared/service.
The browser storage service abstrcts the call to session or local storage in the browser depending on the remeberMe flag. Another use case is... if the user has accepted the data safety regulations allowing storing information longer than the session.
The following source code shows the implementation of the described functionality:
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class BrowserStorageService {
// Remember-me is influenced by the user signing the data protection contract
// or can be part of the login dialog
private _rememberMe: boolean = false;
constructor() {
}
get rememberMe(): boolean {
return this._rememberMe;
}
set rememberMe(value: boolean) {
this._rememberMe = value;
}
removeItem(name: string) {
if (this._rememberMe) localStorage.removeItem(name);
else sessionStorage.removeItem(name)
}
getItem(name: string): string {
if (this._rememberMe) return localStorage.getItem(name);
else return sessionStorage.getItem(name)
}
setItem(name: string, value: string) {
if (this._rememberMe) localStorage.setItem(name, value);
else sessionStorage.setItem(name, value)
}
clear() {
if (this._rememberMe) localStorage.clear();
else sessionStorage.clear();
}
}Browser Storage Unit Test
The unit test is verifying the correct use of Local and Session store:
import { TestBed } from '@angular/core/testing';
import { BrowserStorageService } from './browser-storage.service';
describe('BrowserStorageService', () => {
let service: BrowserStorageService;
beforeEach(() => {
TestBed.configureTestingModule({})
service = TestBed.inject(BrowserStorageService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should write to local store', () => {
service.rememberMe = true;
service.setItem('dummyTestItem', 'item');
expect(service.getItem('dummyTestItem')).toEqual('item');
expect(localStorage.getItem('dummyTestItem')).toEqual('item');
});
it('should write to session store', () => {
service.rememberMe = false;
service.setItem('dummyTestItem', 'item');
expect(service.getItem('dummyTestItem')).toEqual('item');
expect(sessionStorage.getItem('dummyTestItem')).toEqual('item');
});
});Generate the service:
ng generate service shared/service/authentication
The service authentication.service.ts has been created in the folder src/app/shared/service.
The authentication service handles communication between the angular app and the backend api for everything related to authentication. It contains methods for login, logout and refresh token, and contains properties for accessing the current user.
The user property exposes an RxJS observable (Observable) so any component can subscribe to be notified when a user logs in, logs out or has their token refreshed, the notification is triggered by the call to this.currentUserSubject.next() from each method in the service.
The currentUserValue getter allows other components to quickly get the value of the current user without having to subscribe to the user observable.
The login() method POSTs the username and password to the API for authentication, on success the api returns the user details and a JWT token which are published to all subscribers with the call to this.currentUserValue.next(user), the api also returns a refresh token cookie which is stored by the browser. The method then starts a countdown timer by calling this.startRefreshTokenTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires so the user stays logged in.
The logout() method makes a POST request to the API to revoke the refresh token that is stored in a browser storage, then cancels the silent refresh running in the background by calling this.stopRefreshTokenTimer(), then logs the user out by publishing a null value to all subscriber components (this.currentUserValue.next(null)), and finally redirects the user to the login page (still disabled...will be enabled if a login form exists).
The refreshToken() method is similar to the login() method, they both perform authentication, but this method does it by making a POST request to the API that includes a refresh token. On success the api returns the user details and a JWT token which are published to all subscribers with the call to this.currentUserSubject.next(user), the api also returns a new refresh token cookie which replaces the old one in the browser. The method then starts a countdown timer by calling this.startRefreshTokenTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires so the user stays logged in.
The following source code shows the implementation of the described functionality:
import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {BehaviorSubject, Observable} from 'rxjs';
import {map, retry} from 'rxjs/operators';
import {User} from '../model/user';
import {environment} from '../../../environments/environment';
import {JwtTokenService} from './jwt-token.service';
import {Jwt} from '../helper';
import {BrowserStorageService} from './browser-storage.service';
@Injectable({providedIn: 'root'})
export class AuthenticationService {
private currentUserSubject: BehaviorSubject<User>;
public currentUser: Observable<User>;
private authApiUrl: string;
constructor(
private browserStorageService: BrowserStorageService,
private http: HttpClient,
private router: Router,
private tokenService: JwtTokenService
) {
this.authApiUrl = environment.endpoints.backendAuthUrl;
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(browserStorageService.getItem('currentUser')));
this.currentUser = this.currentUserSubject.asObservable();
}
public get currentUserValue(): User {
return this.currentUserSubject.value;
}
login(username: string, password: string, rememberMe = true): Observable<any> {
const data = {
username: username,
password: password
};
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'}),
observe: 'response' as 'response'
};
return this.http.post<any>(`${this.authApiUrl}/users/authenticate`, data, httpOptions)
.pipe(
map(response => {
console.log(response);
const user = response.body;
if (this.tokenService.verifyToken(user.token, user.publicKey)) {
this.browserStorageService.rememberMe = rememberMe;
return this.saveUser(user);
}
}),
retry(2)
);
}
logout() {
// remove user from local storage and set current user to null
this.browserStorageService.removeItem('currentUser');
this.currentUserSubject.next(null);
this.stopRefreshTokenTimer();
// this.router.navigate(['/login']); // is needed after we have a login dialog
}
getBasicAuthHeader(username: string, password: string) {
return `Basic ${btoa(username + ':' + password)}`;
}
refreshToken() {
const jwtToken = this.currentUserSubject.value.token;
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + jwtToken
}), observe: 'response' as 'response'
};
const data = {
username: this.currentUserValue.username,
refreshToken: this.currentUserValue.refreshToken
};
return this.http.post<any>(`$${this.authApiUrl}/users/refresh_token`, data, httpOptions)
.pipe(map(response => {
console.log(response);
const user = response.body;
if (this.tokenService.verifyToken(user.token, user.publicKey)) {
return this.saveUser(user);
} else {
console.log('refresh token not successful')
}
}));
}
private refreshTokenTimeout;
private startRefreshTokenTimer() {
// set a timeout to refresh the token a minute before it expires
const expires = new Date(this.currentUserValue.expires * 1000);
const timeout = expires.getTime() - Date.now() - (60 * 1000); // debug 59 minutes before (every minute a new token)
console.log('timeout=' + timeout, 'expires=' + expires);
this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(), timeout);
}
private stopRefreshTokenTimer() {
clearTimeout(this.refreshTokenTimeout);
}
private saveUser(user: User): User {
this.extractTokenInfo(user);
this.browserStorageService.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
console.log(this.currentUserValue.roles, user.roles);
this.startRefreshTokenTimer();
return user;
}
public extractTokenInfo(user: User) {
const jwt = this.tokenService.decodeToken(user.token);
if (jwt instanceof Jwt) {
user.expires = +JSON.stringify(jwt.getExpiration());
user.roles = jwt.body['roles'];
}
}
}The unit test authentication.service.spec.ts of the AuthenticationService
has been created in the folder src/app/shared/service.
The following source code shows the implementation of the described functionality. The unit tests verify the login, logout and refresh token functionality.
import {inject, TestBed} from '@angular/core/testing';
import { AuthenticationService } from './authentication.service';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { MockBackendInterceptor } from '../helper/mock/mock-backend-interceptor.service';
import { CreateUserService } from '../helper/create-user.service';
import {decodeToken, Jwt, verifyToken} from '../helper';
describe('AuthenticationService', () => {
let service: AuthenticationService;
let singletonService: AuthenticationService;
let userService: CreateUserService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
HttpClientTestingModule,
],
providers: [
AuthenticationService,
CreateUserService,
{
provide: HTTP_INTERCEPTORS,
useClass: MockBackendInterceptor,
multi: true
}
]
});
service = TestBed.inject(AuthenticationService);
userService = TestBed.inject(CreateUserService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('Service injected via inject(...) and TestBed.inject(...) should be the same instance',
inject([AuthenticationService], (injectService: AuthenticationService) => {
expect(injectService).toBe(service);
})
);
it('should authenticate an admin', (done) => {
service.login('admin','admin').subscribe(
(data) => {
console.log('authenticateResponse', data, service.currentUserValue);
expect(service.currentUserValue.username).toBe('admin');
// use the same service for further tests
singletonService = service;
checkTheTokens();
checkTheRoles(['admin','user']);
checkCurrentUserStoreExist();
done();
},
(err) => {
console.log('authenticateResponse', err);
fail();
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should refresh a token', (done) => {
const oldRefreshToken = singletonService.currentUserValue.refreshToken;
singletonService.refreshToken().subscribe(
(data) => {
console.log('refreshTokenResponse', data, service.currentUserValue);
expect(singletonService.currentUserValue.username).toBe('admin');
const newRefreshToken = singletonService.currentUserValue.refreshToken;
expect(newRefreshToken).not.toEqual(oldRefreshToken);
checkTheTokens();
checkTheRoles(['admin','user']);
checkCurrentUserStoreExist();
done();
},
(err) => {
console.log('authenticateResponse', err);
fail();
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should logout', (done) => {
singletonService.logout()
expect(singletonService.currentUserValue).toBeNull();
checkCurrentUserStoreNotExist();
done();
});
function checkCurrentUserStoreExist() {
expect(localStorage.getItem('currentUser')).toBeTruthy();
}
function checkCurrentUserStoreNotExist() {
expect(localStorage.getItem('currentUser')).toBeFalsy();
}
function checkTheTokens() {
const refreshToken = decodeToken(singletonService.currentUserValue.refreshToken);
if (refreshToken instanceof Jwt) {
expect(verifyToken(singletonService.currentUserValue.refreshToken, singletonService.currentUserValue.publicKey)).toBeTrue();
expect(refreshToken.body.sub).toEqual(singletonService.currentUserValue.username);
}
const token = decodeToken(singletonService.currentUserValue.token);
if (token instanceof Jwt) {
expect(verifyToken(singletonService.currentUserValue.token, singletonService.currentUserValue.publicKey)).toBeTrue();
expect(token.body.sub).toEqual(singletonService.currentUserValue.username);
}
}
function checkTheRoles(checkRoles: Array<string> ) {
singletonService.currentUserValue.roles.forEach(role => {
expect(checkRoles.includes(role)).toBeTrue();
});
}
});The following source code shows the implementation of the refresh_token functionality. The mock backend has been improved by
- refresh_token api
- new mock user class
- new error returns from the api
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpResponse,
HttpHandler,
HttpEvent,
HttpInterceptor,
HTTP_INTERCEPTORS, HttpHeaders
} from '@angular/common/http';
import {Observable, of, throwError} from 'rxjs';
import {delay, mergeMap, materialize, dematerialize, tap} from 'rxjs/operators';
import {AUCTION_DATA} from '../../../auction/shared/auction-data';
import {
createMockUser,
createRsaJwtToken,
createTestRefreshToken,
createTestToken, createUser, mockAdmin, MockUser, mockUser, nowEpochSeconds,
publicKey,
verifyRsaJwtToken
} from './jwt-backend.data';
import {decodeToken, IJwtStdPayload, Jwt} from '../helper.jwt';
import {CreateUserService} from '../create-user.service';
import {UserService} from '../../service/user.service';
import {User} from '../../model/user';
/**
* The mock backend interceptor is used to simulate a backend. The interceptor allows
* to write individual route functions in order to support all different http verbs (GET, POST, PUT, GET)
* The interceptor simulated a backend delay of 500ms. The traffic to the interceptor
* is visible in the console of the browser.
*
* At the end of this file you will find a method mockBackendProvider which can be used in the module provider
* to activate the interceptor
*
* Based on: https://jasonwatmore.com/post/2019/05/02/angular-7-mock-backend-example-for-backendless-development
*
*/
// array in local storage for users
const usersKey = 'users';
// const users = JSON.parse(localStorage.getItem(usersKey)) || [];
const users = [];
users.push(mockAdmin);
users.push(mockUser);
localStorage.setItem(usersKey, JSON.stringify(users));
@Injectable()
export class MockBackendInterceptor implements HttpInterceptor {
/**
* Overwritten method of HttpInterceptor
* @param request
* @param next
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const {url, method, headers, body} = request;
// wrap in delayed observable to simulate server api call
return of(null)
.pipe(mergeMap(() => handleRoute()))
.pipe(materialize()) // call materialize and dematerialize to ensure delay
.pipe(delay(100))
.pipe(dematerialize())
.pipe(tap({
next: data => {
console.log('mockResponse', data);
},
error: error => {
console.log('mockResponse', JSON.stringify(error));
},
complete: () => console.log('mockResponse: on complete')
}));
/**
* The handle route function is used to individually support the
* different end points
*/
function handleRoute() {
console.log('mockRequest: ' + url, method, headers, body);
let response: Observable<HttpEvent<any>>;
switch (true) {
case url.endsWith('/users/authenticate') && method === 'POST':
response = authenticate();
break;
case url.endsWith('/users/register') && method === 'POST':
response = register();
break;
case url.endsWith('/users/refresh_token') && method === 'POST':
response = refreshToken();
break;
case url.endsWith('/users') && method === 'GET':
response = getUsers();
break;
case url.match(/(\/users[\/])/) && method === 'GET':
if (isNumber(nameFromUrl())) response = getUser();
else response = getUserByName();
break;
case url.match(/\/users\/\d+$/) && method === 'DELETE':
response = deleteUser();
break;
case url.endsWith('/users') && method === 'POST':
response = register();
break;
case url.match(/\/users\/\d+$/) && method === 'PUT':
response = changeUser();
break;
case url.match(/\/auctions\/\d+$/) && method === 'GET':
response = getAuction();
break;
case url.endsWith('/auctions') && method === 'GET':
response = getAuctions();
break;
default:
// pass through any requests not handled above
response = next.handle(request);
}
// response.subscribe(data => {console.log('mockResponse', response);})
return response;
}
// --- route functions ---
function authenticate() {
const {username, password} = body;
let mockUsers: Array<MockUser> = JSON.parse(localStorage.getItem(usersKey)) || [];
const mockUser: MockUser = mockUsers.find(x => x.username === username && x.password === password);
if (!mockUser) {
return notFound('Username or password is incorrect');
} else {
mockUser.tokens.push(createTestToken(username));
mockUser.refreshTokens.push(createTestRefreshToken(username));
localStorage.setItem(usersKey, JSON.stringify(mockUsers));
const user: User = createUser(mockUser);
user.publicKey = publicKey;
const responseHeader: HttpHeaders = createHeader( user.token);
return ok(user, responseHeader);
}
}
function register() {
const user = body;
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
if (mockUsers.find(x => x.username === user.username)) {
return error('Username "' + user.username + '" is already taken')
}
user.id = mockUsers.length ? Math.max(...mockUsers.map(x => x.id)) + 1 : 1;
const mockUser: MockUser = createMockUser(user);
mockUsers.push(mockUser);
localStorage.setItem(usersKey, JSON.stringify(mockUsers));
const responseHeaders = createResisterHeader();
// const responseHeaders: HttpHeaders = createHeader( 'ok');
return ok(user, responseHeaders);
}
function refreshToken() {
if (!isLoggedIn()) return unauthorized();
if (!isTokenExpired()) return expired();
const {username, refreshToken} = body;
if (!isRefreshTokenExpired(refreshToken)) return expired();
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
const mockUser: MockUser = mockUsers.find(x => x.username === username);
if (!mockUser) {
return error(`Username ${mockUser} not found`);
} else {
const refreshTok = mockUser.refreshTokens.find(x => x === refreshToken);
if (!refreshTok) {
return error('refreshToken not found');
} else {
mockUser.tokens.push(createTestToken(username));
mockUser.refreshTokens.push(createTestRefreshToken(username));
localStorage.setItem(usersKey, JSON.stringify(mockUsers));
const user: User = createUser(mockUser);
user.publicKey = publicKey;
const responseHeader: HttpHeaders = createHeader(user.token);
JSON.stringify(responseHeader);
return ok(user, responseHeader);
}
}
}
function getUsers() {
if (!isLoggedIn()) return unauthorized();
if (!isTokenExpired()) return expired();
if (!isInRole('admin')) return notInRole();
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
if (!isLoggedIn()) return unauthorized();
const users = [];
mockUsers.forEach(mockUser => users.push(createUser(mockUser)))
return ok(users);
}
function getUser() {
if (!isLoggedIn()) return unauthorized();
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
mockUsers = mockUsers.filter(x => x.id === idFromUrl());
if (mockUsers.length > 0) {
return ok(createUser(mockUsers[0]));
} else {
return noContent('User with id ' + idFromUrl() + ' not found.')
}
}
function getUserByName() {
if (!isLoggedIn()) return unauthorized();
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
mockUsers = mockUsers.filter(x => x.username === nameFromUrl());
if (mockUsers.length > 0) {
return ok(createUser(mockUsers[0]));
} else {
return noContent('User with name ' + nameFromUrl() + ' not found.')
}
}
function deleteUser() {
if (!isLoggedIn()) return unauthorized();
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
mockUsers = mockUsers.filter(x => x.id !== idFromUrl());
localStorage.setItem(usersKey, JSON.stringify(mockUsers));
return ok();
}
function changeUser() {
if (!isLoggedIn()) return unauthorized();
const user = body;
if (!user.id) user.id = idFromUrl();
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
mockUsers = mockUsers.filter(x => x.id === idFromUrl());
if (mockUsers.length > 0) {
// delete this user
let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
mockUsers = mockUsers.filter(x => x.id !== idFromUrl());
// add changed user
mockUsers.push(user);
localStorage.setItem(usersKey, JSON.stringify(mockUsers));
return ok(user);
} else {
return noContent('User with id ' + idFromUrl() + ' not found.')
}
}
function getAuctions() {
return ok(AUCTION_DATA);
}
function getAuction() {
let auctions = AUCTION_DATA.filter(x => x.id === idFromUrl());
if (auctions.length > 0) {
return ok(auctions[0]);
} else {
return noContent('Auction item with id ' + idFromUrl() + ' not found.')
}
}
// helper functions
function ok(body?, headers?: HttpHeaders) {
const resp = new HttpResponse({body: body, headers: headers, status: 200});
return of(new HttpResponse(resp));
}
function error(message) {
const resp = new HttpResponse({body: message, headers: headers, status: 404});
return of(new HttpResponse(resp));
}
function unauthorized() {
const resp = new HttpResponse({body: 'Unauthorised', headers: headers, status: 401});
return of(new HttpResponse(resp));
}
function expired() {
const resp = new HttpResponse({body: 'Unauthorised - Token expired', headers: headers, status: 401});
return of(new HttpResponse(resp));
}
function notInRole() {
const resp = new HttpResponse({body: 'Forbidden - not correct role', headers: headers, status: 403});
return of(new HttpResponse(resp));
}
function notFound(message) {
const resp = new HttpResponse({body: message, headers: headers, status: 404});
return of(new HttpResponse(resp));
}
function noContent(message) {
const resp = new HttpResponse({body: message, headers: headers, status: 204});
return of(new HttpResponse(resp));
}
function isLoggedIn() {
const bearerToken = headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
const jwtToken = bearerToken.slice(7, bearerToken.length);
try {
if (verifyRsaJwtToken(jwtToken)) return true;
} catch (e) {
if (e instanceof Error) {
throwError({status: 401, error: {message: 'Unauthorised - Token invalid'}});
}
}
}
return (headers.get('Authorization') === 'Bearer fake-jwt-token')
}
function isTokenExpired() {
const bearerToken = headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
const jwtToken = bearerToken.slice(7, bearerToken.length);
const token = decodeToken(jwtToken);
if (token instanceof Jwt) {
if (token.body.exp >= nowEpochSeconds()) return true;
}
}
return false;
}
function isRefreshTokenExpired(refreshToken: string) {
const token = decodeToken(refreshToken);
if (token instanceof Jwt) {
if (token.body.exp >= nowEpochSeconds()) return true;
}
return false;
}
function isInRole(role) {
const bearerToken = headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
const jwtToken = bearerToken.slice(7, bearerToken.length);
const token = decodeToken(jwtToken);
if (token instanceof Jwt && token.body['roles']) {
const roles: Array<string> = token.body['roles'];
console.log('roles', roles, role);
if (roles.indexOf(role) > -1) return true;
}
}
return false;
}
function idFromUrl() {
const urlParts = url.split('/');
return parseInt(urlParts[urlParts.length - 1]);
}
function nameFromUrl() {
const urlParts = url.split('/');
return urlParts[urlParts.length - 1];
}
function createHeader(token: string): HttpHeaders {
return new HttpHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${token}`
});
}
function createResisterHeader(): HttpHeaders {
return new HttpHeaders({
'Content-Type': 'application/json',
Accept: '"application/json'
});
}
function addTokenToHeader(headers: HttpHeaders, token: string): HttpHeaders {
return addItemToHeader(headers, 'Authorization', `Bearer ${token}`);
}
function addContentTypeToHeader(headers: HttpHeaders): HttpHeaders {
return addItemToHeader(headers, 'Content-Type', 'application/json');
}
function addAcceptToHeader(headers: HttpHeaders): HttpHeaders {
return addItemToHeader(headers, 'Accept', 'application/json');
}
function addItemToHeader(headers: HttpHeaders, key: string, item: string): HttpHeaders {
return headers.append(key, item);
}
function isNumber(value: string | number): boolean {
return ((value != null) && !isNaN(Number(value.toString())));
}
}
}
/**
* Put the method call to the provider section of your NgModule
*/
export const mockBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: MockBackendInterceptor,
multi: true
};The following source code shows the test implementation of the refresh_token functionality:
import {TestBed} from '@angular/core/testing';
import {MockBackendInterceptor} from './mock-backend-interceptor.service';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {HTTP_INTERCEPTORS, HttpClient, HttpHeaders} from '@angular/common/http';
import {Auction} from '../../../auction/shared/auction';
import {decodeToken, Jwt} from '../helper.jwt';
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'my-auth-token'
}),
observe: 'response' as 'response'
};
let httpOptionsJwtToken = {
headers: null,
observe: 'response' as 'response'
};
let httpOptionsExpiredJwtToken = {
headers: null,
observe: 'response' as 'response'
};
let httpOptionsInvalidJwtToken = {
headers: null,
observe: 'response' as 'response'
};
class User {
id?: number;
username: string;
password: string;
firstName?: string;
lastName?: string;
email?: string;
thresholdOpenPayment?: number;
locked?: boolean;
token?: string;
refreshToken?: string;
expires?: number;
}
const postUser: User = {
firstName: 'post',
lastName: 'post',
username: 'post',
password: 'post',
email: 'post' + '@mail.com',
thresholdOpenPayment: 1000,
locked: false,
token: null,
refreshToken: null,
expires: null
};
let regUserId: number = 0;
let postUserId: number = 0;
let adminUserId: number = 0;
const regUser: User = {
id: 0,
firstName: 'default',
lastName: 'default',
username: 'default',
password: 'default',
email: 'default1' + '@mail.com',
thresholdOpenPayment: 1000,
locked: false,
token: null,
refreshToken: null,
expires: null
};
const adminRegUser: User = {
id: 0,
firstName: 'admin',
lastName: 'admin',
username: 'admin',
password: 'admin',
email: 'admin' + '@mail.com',
thresholdOpenPayment: 1000,
locked: false,
token: null,
refreshToken: null,
expires: null
};
const loginUser: User = {
username: 'default',
password: 'default'
};
const loginUserRefresh = {
username: 'default',
refreshToken: 'default'
};
const loginAdminUser: User = {
username: 'admin',
password: 'admin'
};
const unknowLoginUser: User = {
username: 'unknown',
password: 'default'
};
const wrongPasswordLoginUser: User = {
username: 'default',
password: 'unknown'
};
const expiredRsa256Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.cJF7Z_4dbFkHdbx-2TogMqppa3MoLzjj7O0XOyl7ZMDSZDiyRvSZhwKOT40gdYO1iW65ZYnpeumEcCrYM_KnfMV3i9d9LOPBDYakerpA-lHD_tfaB2rNWFgjjtg1IhvI-_1tSYfTjosPB2KB110t3Jz_iTSAFV8AxM02UubddDo';
describe('MockBackendInterceptor', () => {
let service: MockBackendInterceptor;
let http: HttpTestingController;
let httpClient: HttpClient;
beforeEach(() => {
const testBed = TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MockBackendInterceptor,
multi: true
}
]
});
http = testBed.get(HttpTestingController);
httpClient = testBed.get(HttpClient);
});
it('should catch 401', (done) => {
httpClient.get('/error', httpOptions)
.subscribe((data) => {
expect(data.status).toBe(401);
done();
}, (err) => {
expect(err.status).toBe(401);
// Perform test
done();
});
http.expectOne('/error').error(new ErrorEvent('Unauthorized error'), {
status: 401
});
http.verify();
});
it('should catch 401 at get users', (done) => {
httpClient.get('/users', httpOptions)
.subscribe((data) => {
expect(data.status).toBe(401);
done();
}, (err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(401);
// Perform test
done();
});
});
it('should return an auction', (done) => {
httpClient.get<Auction>('/auctions/1', httpOptions)
.subscribe((data) => {
const auction: Auction = data.body;
const status: number = data.status;
expect(status).toBe(200);
expect(auction.id).toBe(1);
done();
},
(err) => {
// console.log('mockTestErrorResponse', err);
fail();
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should register a user', (done) => {
httpClient.post<User>('/users/register', regUser, httpOptions)
.subscribe((data) => {
const user: User = data.body;
const status: number = data.status;
expect(status).toBe(200);
expect(user.username).toEqual(regUser.username);
done();
},
(err) => {
// console.log('mockTestResponse', err);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should not register an admin because already registered', (done) => {
httpClient.post<User>('/users/register', adminRegUser, httpOptions)
.subscribe((data) => {
console.log('mockTestResponse', data);
const status: number = data.status;
expect(status).toBe(404);
const user: User = data.body;
// expect(user.username).toEqual(adminRegUser.username);
done();
},
(err) => {
console.log('mockTestResponse', err);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should authenticate an admin', (done) => {
httpClient.post<User>('/users/authenticate', loginAdminUser, httpOptions)
.subscribe((data) => {
console.log('mockTestResponse', data);
const bearerToken = data.headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
const jwtToken = bearerToken.slice(7, bearerToken.length);
const token = decodeToken(jwtToken);
if (token instanceof Jwt) {
expect(token.body.sub).toEqual(loginAdminUser.username);
httpOptionsJwtToken.headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + jwtToken
});
// console.log('roles=' + token.body['roles']);
// expect(token.body['roles']).toEqual(loginAdminUser.username);
} else fail();
} else fail();
const user: User = data.body;
const status: number = data.status;
expect(status).toBe(200);
expect(user.username).toEqual(loginAdminUser.username);
done();
},
(err) => {
console.log('mockTestResponse', err);
fail();
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should not authenticate an unknown user', (done) => {
httpClient.post<User>('/users/authenticate', unknowLoginUser, httpOptions)
.subscribe(data => {
console.log('mockTestResponse1', data);
const status: number = data.status;
expect(status).toBe(404);
// fail();
done();
},
(err) => {
if (err.headers) {
const bearerToken = err.headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
fail();
}
}
console.log('mockTestResponse', err);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should not authenticate a user with wrong password', (done) => {
httpClient.post<User>('/users/authenticate', wrongPasswordLoginUser, httpOptions)
.subscribe((data) => {
console.log('mockTestResponse', data);
const status: number = data.status;
expect(status).toBe(404);
// fail();
done();
},
(err) => {
if (err.headers) {
const bearerToken = err.headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
fail();
}
}
console.log('mockTestResponse', err);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should get all users with valid token and role admin', (done) => {
httpClient.get<User>('/users', httpOptionsJwtToken)
.subscribe((data) => {
expect(data.status).toBe(200);
console.log('mockTestResponse', data.body[0]);
expect(data.body[0].id).toBeGreaterThan(0);
done();
},
(err) => {
console.log('mockTestResponse', err);
fail();
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should NOT get all users with an EXPIRED token and role admin with return 401', (done) => {
httpOptionsExpiredJwtToken.headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + expiredRsa256Token
});
httpClient.get<User>('/users', httpOptionsExpiredJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(401);
done();
},
(err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(401);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should NOT get all users with an INVALID token and role admin with return 401', (done) => {
let bearerToken = httpOptionsJwtToken.headers.get('Authorization');
bearerToken = bearerToken.slice(0, -1) + '0';
httpOptionsInvalidJwtToken.headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearerToken
});
httpClient.get<User>('/users', httpOptionsInvalidJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(401);
done();
},
(err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(401);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should create a postUser with valid token and role admin', (done) => {
httpClient.post<User>('/users', postUser, httpOptionsJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(200);
expect(data.body.id).toBeGreaterThan(0);
postUserId = data.body.id;
done();
},
(err) => {
console.log('mockTestResponse', err);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should change a postUser with valid token and role admin', (done) => {
postUser.firstName = "changed";
httpClient.put<User>('/users/' + postUserId, postUser, httpOptionsJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(200);
expect(data.body.id).toBeGreaterThan(0);
expect(data.body.id).toBe(postUser.id);
expect(data.body.firstName).toEqual ("changed");
done();
},
(err) => {
console.log('mockTestResponse', err);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should delete the postUser with valid token and role admin', (done) => {
httpClient.delete<User>('/users/' + postUserId, httpOptionsJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(200);
done();
},
(err) => {
console.log('mockTestResponse', err);
done();
fail();
}, () => {
// console.log('Test complete');
done();
});
});
it('should authenticate a user', (done) => {
httpClient.post<User>('/users/authenticate', loginUser, httpOptions)
.subscribe((data) => {
console.log('mockTestResponse', data);
const bearerToken = data.headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
const jwtToken = bearerToken.slice(7, bearerToken.length);
const token = decodeToken(jwtToken);
if (token instanceof Jwt) {
expect(token.body.sub).toEqual(loginUser.username);
httpOptionsJwtToken.headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + jwtToken
});
loginUserRefresh.refreshToken = data.body.refreshToken;
console.log('refreshToken=' + loginUserRefresh.refreshToken);
} else fail();
} else fail();
const user: User = data.body;
const status: number = data.status;
expect(status).toBe(200);
expect(user.username).toEqual(loginUser.username);
done();
},
(err) => {
console.log('mockTestResponse', err);
fail();
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should NOT get all users with valid token and role user with return 403 forbidden', (done) => {
httpClient.get<User>('/users', httpOptionsJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(403);
//fail();
done();
},
(err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(403);
done();
}, () => {
// console.log('Test complete');
done();
});
});
it('should send a refresh token', (done) => {
httpClient.post<User>('/users/refresh_token', loginUserRefresh, httpOptionsJwtToken)
.subscribe((data) => {
console.log('mockTestResponse', data);
const bearerToken = data.headers.get('Authorization');
if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
const jwtToken = bearerToken.slice(7, bearerToken.length);
const token = decodeToken(jwtToken);
if (token instanceof Jwt) {
expect(token.body.sub).toEqual(loginUser.username);
httpOptionsJwtToken.headers = new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + jwtToken
});
const refreshToken = decodeToken(data.body.refreshToken);
if (refreshToken instanceof Jwt) {
expect(refreshToken.body.sub).toEqual(loginUser.username);
} else fail();
loginUserRefresh.refreshToken = data.body.refreshToken;
} else fail();
} else fail();
const user: User = data.body;
const status: number = data.status;
expect(status).toBe(200);
expect(user.username).toEqual(loginUser.username);
done();
},
(err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(200);
done();
}, () => {
// console.log('Test complete');
done();
});
});
});The following source code shows the implementation of the refresh_token functionality.
The backend is working with a different User class: MockUser.
Therefore some new functions were created to convert between the User and the MockUser.
The api is working with the user object. The backend is saving MockUser to the localStore.
import {IJwtHeader, IJwtStdPayload, createToken, verifyToken} from '../helper.jwt';
import {User} from '../../model/user';
const issuer = 'ZHAW';
const subject = 'Auction-App';
const audience = 'ASE2';
export const sharedSecret = 'sharedsecret';
export const privateKey =
`-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAICVyX5OguWsNi8lxzDEtVbFLeuW5pmQVPKdOY3FPPTrofDeWYVy
bMT7/3fcyP+L/CH4T7z9suw4c4FBB2SOso8CAwEAAQJAbKinFdIYsSbevubItYB0
0PddP6lMAtbBwid0nEXhpgFKHpBW3xVn7wv1dmzya8O8t7KNhMI/529+ScJ1PLca
SQIhALpptXYzj/3b6sACfeFKbwooHMHPk3kdTrZymBmiz/azAiEAsJXX5jrnl4bF
GBUAyV8Fv0Yu+eFhA8IvdhiBzJD4orUCIQCQnsolNcOUUzVAWa6HRlP3MT9+LShg
Yhha+3R9Dw8AeQIhAK/EJseKoFTKF8q1tTe7dowCPuYIuTk1g2poUGKfdmz1AiBg
JhyKTosBk1OzKz5DWD6k11pA9qpAXcMmvpUUcduCVw==
-----END RSA PRIVATE KEY-----`;
export const publicKey =
`-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAICVyX5OguWsNi8lxzDEtVbFLeuW5pmQ
VPKdOY3FPPTrofDeWYVybMT7/3fcyP+L/CH4T7z9suw4c4FBB2SOso8CAwEAAQ==
-----END PUBLIC KEY-----`;
const hmacHeader: IJwtHeader = {
"typ": "JWT",
"alg": "HS256"
};
const rsaHeader: IJwtHeader = {
"typ": "JWT",
"alg": "RS256"
};
export function createRsaJwtToken(payLoad: IJwtStdPayload, claims) {
return createToken(rsaHeader, payLoad, claims, privateKey);
}
export function createHmacJwtToken(payLoad: IJwtStdPayload, claims) {
return createToken(hmacHeader, payLoad, claims, sharedSecret);
}
export function verifyRsaJwtToken(token: string) {
return verifyToken(token, publicKey);
}
export function verifyHmacJwtToken(token: string) {
return verifyToken(token, sharedSecret);
}
export function createTestToken(username: string) {
const roles: Array<string> = username === 'admin' ? ['admin', 'user'] : ['user'];
const payLoad: IJwtStdPayload = {
iat: 0,
exp: createExpiresDateTime(),
iss: "",
aud: "",
sub: username,
};
const claims = {
roles: roles,
accessToken: 'secretaccesstoken',
};
return createRsaJwtToken(payLoad, claims);
// console.log (verifyRsaJwtToken(token));
// return token;
}
export function createTestRefreshToken(username: string) {
const payLoad: IJwtStdPayload = {
iat: 0,
exp: createExpiresDateTime(),
iss: "",
aud: "",
sub: username,
};
const claims = {
accessToken: 'secretaccesstoken',
};
return createRsaJwtToken(payLoad, claims);
// console.log (verifyRsaJwtToken(token));
// return token;
}
export function nowEpochSeconds() {
return Math.floor(new Date().getTime() / 1000);
}
export function createExpiresDateTime() {
const exp = (nowEpochSeconds() + (60 * 60)) * 1000
return Math.floor(new Date(exp).getTime() / 1000);
}
export class MockUser {
id?: number;
username: string;
password: string;
firstName: string;
lastName: string;
email?: string;
thresholdOpenPayment?: number;
locked?: boolean;
tokens?: Array<string>;
refreshTokens?: Array<string>;
expires?: number;
}
export const mockAdmin: MockUser = {
id: 1,
firstName: 'admin',
lastName: 'admin',
username: 'admin',
password: 'admin',
email: 'admin' + '@mail.com',
thresholdOpenPayment: 1000,
locked: false,
tokens: [createTestToken('admin')],
refreshTokens: [createTestRefreshToken('admin')],
expires: createExpiresDateTime()
};
export const mockUser: MockUser = {
id: 2,
firstName: 'user',
lastName: 'user',
username: 'user',
password: 'user',
email: 'user' + '@mail.com',
thresholdOpenPayment: 1000,
locked: false,
tokens: [createTestToken('user')],
refreshTokens: [createTestRefreshToken('user')],
expires: createExpiresDateTime()
};
export function createUser(mockUser: MockUser): User {
const user = new User();
user.id = mockUser.id;
user.firstName = mockUser.firstName;
user.lastName = mockUser.lastName;
user.username = mockUser.username;
user.password = mockUser.password;
user.email = mockUser.email;
user.thresholdOpenPayment = mockUser.thresholdOpenPayment;
user.locked = mockUser.locked;
if (mockUser.tokens) user.token = mockUser.tokens[mockUser.tokens.length - 1];
if (mockUser.refreshTokens) user.refreshToken = mockUser.refreshTokens[mockUser.refreshTokens.length - 1];
user.expires = mockUser.expires;
return user;
}
export function createMockUser(user: User): MockUser {
const mockUser= new MockUser();
mockUser.id = user.id;
mockUser.firstName = user.firstName;
mockUser.lastName = user.lastName;
mockUser.username = user.username;
mockUser.password = user.password;
mockUser.email = user.email;
mockUser.thresholdOpenPayment = user.thresholdOpenPayment;
mockUser.locked = user.locked;
(user.token) ? mockUser.tokens.push(user.token) : mockUser.tokens = [];
(user.refreshToken) ? mockUser.refreshTokens.push(user.refreshToken) : mockUser.refreshTokens = [];
mockUser.expires = user.expires;
return mockUser;
}The following source code shows the test implementation of the new fields in the user class functionality:
export class User {
id?: number;
username: string;
password: string;
firstName: string;
lastName: string;
email?: string;
thresholdOpenPayment?: number;
locked?: boolean;
token?: string;
refreshToken?: string;
roles?: Array<string>;
expires?: number;
publicKey?: string;
}The following source code shows the test implementation of the new fields in the user class functionality:
import {first} from 'rxjs/operators';
import {Injectable} from '@angular/core';
import {User} from '../model/user';
import {UserService} from '../service/user.service';
@Injectable({providedIn: 'root'})
export class CreateUserService {
constructor(
// private router: Router,
private userService: UserService
) {
}
private registerUser(user: User) {
this.userService.register(user)
.pipe(first())
.subscribe(
data => {},
error => {}
);
}
checkAndRegisterUser(userName) {
let users = JSON.parse(localStorage.getItem('users')) || [];
const user: User = users.find(x => x.username === userName );
if (users.length === 0 || !user) {
// user not yet in localStorage
const user = {
firstName: userName,
lastName: userName,
username: userName,
password: userName,
email: userName + '@mail.com',
thresholdOpenPayment: 1000,
locked: false,
token: null,
refreshToken: null,
expires: null
};
this.registerUser(user);
}
}
/**
* Creates default users for test purposes
* Used only in development environment
*/
createDefaultUsers() {
this.checkAndRegisterUser('admin');
this.checkAndRegisterUser('user');
}
}