-
Notifications
You must be signed in to change notification settings - Fork 0
task#23 create a jwt token service
Since we will use JWT to secure our application, we will need a service for signing, verifying and decoding the tokens. The mock server shall generate valid token data and the client application shall decode it. A testing environment can be found here:
Just past your token and shared key or public key in this website.
The final solution can be found here:
https://github.com/mbachmann/angular-tutorial-2020/tree/task%2323-create-a-jwt-token-service
- import the node-forge, crypto-js and moment library including the types
- Create a folder
servicein thesrc/app/shared. - Create a library for base64url encode and decode in the folder
src/app/shared/helper. - Create a jwt library for signing, verifying and parsing the token in the folder
src/app/shared/helper. - Integrate the own jwt library in the mock server
- Create a simple jwt-token-service to call the functions for signing, verifying and parsing the token
in the folder
src/app/shared/service. - Create an index.ts file in the folder
src/app/shared/helper.
The verification shall be done by unit tests.
Run successfully the unit tests for base64url, jwt and mock server.
The new folder structure after creating the service folder and the files in the helper folder:
src
\- app
\- auction
\- auction-list
\- auction-list.component.css
\- auction-list.component.html
\- auction-list.component.ts
\- auction-list.component.spec.ts
\- auction-list-detail
\- auction-list-detail.component.css
\- auction-list-detail.component.html
\- auction-list-detail.component.ts
\- auction-list-detail.component.spec.ts
\- mouse-event-display
\- mouse-event-display.component.css
\- mouse-event-display.component.html
\- mouse-event-display.component.ts
\- mouse-event-display.component.spec.ts
\- shared
\- auction.ts
\- auction-data.service.spec.ts
\- auction-data.service.ts
\- auction-data.ts
\- auction.module.ts
\- auction-routing.module.ts
\- home
\- home.component.css
\- home.component.html
\- home.component.ts
\- home.component.spec.ts
\- nav-bar
\- nav-bar.component.css
\- nav-bar.component.html
\- nav-bar.component.ts
\- nav-bar.component.spec.ts
\-shared
\- helper
\- mock
\- mock.module.ts
\- mock-backend-interceptor.service.ts
\- angular-date-http-interceptor.ts
\- base64url.spec.ts
\- base64url.ts
\- helper.jwt.spec.ts
\- helper.jwt.ts
\- helper.service.spec.ts
\- helper.service.ts
\- index.ts
\- service
\- jwt-token.service.ts
\- app.module.ts
\- app.component.html
\- app.component.spec.ts
\- app.component.ts
\- app.component.scss
\- app.routing.ts
\- assets
\- 01-yamaha-blue.png
\- 02-yamaha-aquamarine.png
\- 03-yamaha-red.png
\- environments
\- environment.prod.ts
\- environment.ts
\- environment.model.ts
\- _variables.scss
\- app.constansts.ts
\- favicon.ico
\- index.html
\- main.ts
\- polyfills.ts
\- styles.scss
\- test.ts
\- tsconfig.app.jsop
\- tsconfig.spec.json
\- typings.d.ts
The Hints section is structured equally to the task list.
One the best library for node.js is jsonwebtoken. Unfortunately jsonwebtoken is not working with
ng-cli. For decoding the jwt token the following libraries can be used:
-
jwt-decodeis a small browser library that helps decoding JWTs token which are Base64Url encoded. This library doesn't validate the token, any well formed JWT can be decoded. -
@auth0/angular-jwtis a library for decoding JWTs token. It includes functionality to verify the expiration date of the token
For our purpose we will use none of the mentioned jwt libraries.
We are using the moment library for date computations. The library crypto-js
will allow as to compute the signature for the HMAC algorithm. The library
node-forge can create the signature for the rsa based crypto algorithm.
npm install moment --save
npm install @types/moment --save-dev
npm install node-forge --save
npm install @types/node --save-dev
npm install @types/node-forge --save-dev
npm i crypto-js@3.1.9-1 --save
npm install @types/crypto-js --save-dev
Add to tsconfig.base.json the following two option to support synthetic default imports:
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
Copy the script into the head tag:
<script>
if (global === undefined) {
var global = window;
}
</script>Why do we need base64 and/or base64url?
Base64 is a group of similar binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation. The term Base64 originates from a specific MIME content transfer encoding. Each base64 digit represents exactly 6 bits of data. MIME's Base64 implementation uses A–Z, a–z, and 0–9 for the first 62 values. Base64 encoding can be helpful when fairly lengthy identifying information is used in an HTTP environment. For example, a database persistence framework for Java objects might use Base64 encoding to encode a relatively large unique id (generally 128-bit UUIDs) into a string for use as an HTTP parameter in HTTP forms or HTTP GET URLs.
Using standard Base64 in URL requires encoding of '+', '/' and '=' characters into special percent-encoded hexadecimal sequences ('+' becomes '%2B', '/' becomes '%2F' and '=' becomes '%3D'), which makes the string unnecessarily longer. For this reason, modified Base64 for URL variants exist, where the '+' and '/' characters of standard Base64 are respectively replaced by '-' and '_', so that using URL encoders/decoders is no longer necessary and have no impact on the length of the encoded value, leaving the same encoded form intact for use in relational databases, web forms, and object identifiers in general.
Some variants allow or require omitting the padding '=' signs to avoid them being confused with field separators, or require that any such padding be percent-encoded. Some libraries will encode '=' to '.'.
Source: https://en.wikipedia.org/wiki/Base64
There are several libraries for base64url encode and decode. Here you can find some detailed information:
https://www.npmjs.com/package/base64url
We will use this library and expand some of the functions plus we will change the unit tests to Jasmine syntax.
Create a file base64url.ts in the folder src/app/shared/helper.
import {Buffer} from "buffer";
/**
* encode data from string or a Butter with a default encoding of utf8 to
* base64url
* see https://www.npmjs.com/package/base64url
*
* @param input the input data
* @param encoding default utf8
*/
function encode(input: string | Buffer, encoding: BufferEncoding = "utf8"): string {
if (Buffer.isBuffer(input)) {
return fromBase64(input.toString("base64"));
}
return fromBase64(Buffer.from(input as string, encoding).toString("base64"));
}
function decode(base64url: string, encoding: string = "utf8"): string {
return Buffer.from(toBase64(base64url), "base64").toString(encoding);
}
function toBase64(base64url: string | Buffer): string {
// We this to be a string so we can do .replace on it. If it's
// already a string, this is a noop.
base64url = base64url.toString();
return padString(base64url)
.replace(/\-/g, "+")
.replace(/_/g, "/");
}
function fromBase64(base64: string): string {
return base64
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function toBuffer(base64url: string): Buffer {
return Buffer.from(toBase64(base64url), "base64");
}
function fromString(str: string): string {
return Buffer.from(str)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
export interface Base64Url {
(input: string | Buffer, encoding?: string): string;
encode(input: string | Buffer, encoding?: string): string;
decode(base64url: string, encoding?: string): string;
toBase64(base64url: string | Buffer): string;
fromBase64(base64: string): string;
toBuffer(base64url: string): Buffer;
fromString(str: string): string;
}
let base64url = encode as Base64Url;
base64url.encode = encode;
base64url.decode = decode;
base64url.toBase64 = toBase64;
base64url.fromBase64 = fromBase64;
base64url.toBuffer = toBuffer;
base64url.fromString = fromString;
export default base64url;
function padString(input: string): string {
let segmentLength = 4;
let stringLength = input.length;
let diff = stringLength % segmentLength;
if (!diff) {
return input;
}
let position = stringLength;
let padLength = segmentLength - diff;
let paddedStringLength = stringLength + padLength;
let buffer = Buffer.alloc(paddedStringLength);
buffer.write(input);
while (padLength--) {
buffer.write("=", position++);
}
return buffer.toString();
}
Unit testing the library
The library is united tested with Jasmine standard. Create a file base64url.spec.ts
in the folder src/app/shared/helper.
- from string to base64url
- from base64url to base64
- from base64 to base64url
- from base64url to string
- from base64url to string (buffer)
import base64url from './base64url';
import {Buffer} from "buffer";
function base64(s) {
return Buffer.from(s, 'binary').toString('base64');
}
describe('Helper Base64Url', () => {
let testBuffer: string;
beforeEach(() => {
let array = new Array<string>(256);
for (let i = 0 ; i < 256 ; i++) array[i] = String.fromCharCode(i);
let prev = '';
testBuffer = array.reduce((prev, curr) => {
prev += curr;
return prev;
}
);
});
it('from string to base64url', () => {
const b64 = base64(testBuffer);
const b64url = base64url(testBuffer, 'binary');
expect(b64.indexOf('+')).toEqual(83);
expect(b64.indexOf('/')).toEqual(255);
expect(b64.indexOf('=')).toEqual(342);
expect(b64url.indexOf('+')).toEqual(-1);
expect(b64url.indexOf('/')).toEqual(-1);
expect(b64url.indexOf('=')).toEqual(-1);
expect(b64.indexOf('+')).toEqual(b64url.indexOf('-'));
expect(b64.indexOf('/')).toEqual(b64url.indexOf('_'));
});
it('from base64url to base64', () => {
const b64 = base64(testBuffer);
const b64url = base64url(testBuffer, 'binary');
const result = base64url.toBase64(b64url);
expect(result).toBe(b64);
});
it('from base64 to base64url', () => {
const b64 = base64(testBuffer);
const b64url = base64url(testBuffer, 'binary');
const result = base64url.fromBase64(b64);
expect(result).toBe(b64url);
});
it('from base64url to string', () => {
const b64url = base64url(testBuffer, 'binary');
const result = base64url.decode(b64url, 'binary');
expect(result).toEqual(testBuffer);
});
it('from base64url to string (buffer)', () => {
const b64url = base64url(testBuffer, 'binary');
const result = base64url.decode(Buffer.from(b64url).toString(), 'binary');
expect(result).toEqual(testBuffer);
});
});Since the jsonwebtoken library is not easily working on client side, we can build an own functionality for this purpose.
In order to create a JSON web token, we will need three data types:
- Secret (RSA: Private key, or HMAC: shared key)
- Header
- Payload/claims (Standard payload and project specific payload)
If you are looking into using RSA for the secret we need to create a private/public key pair.
There are many ways of creating keys:
- Console with openssl
- Online RSA key generator
The following 2 commands are generating the key pair (privkey.pem/public.pem):
openssl genrsa -out privkey.pem 1024
openssl rsa -in privkey.pem -outform PEM -pubout -out public.pem
Online RSA key generators are fast and easy. For real world project use the console.
For education purposes you can choose 512 or 1024 bit key size.
Note the “key size” — 512 bit, there are other options too like 1024 bit, 2048 bit, 4096 bit. No doubt longer key lengths are better, but you should know that — with every doubling of the RSA key length, decryption gets at least 6 times slower. Also, it’s not quite easy to make a brute force search on a 256-bit key (but possible). If you are using an algorithm more the RSA256 (e.h. RSA384 or RSA512) the key size must be 1024 or higher.
if you see the generated Private Key and Public Key. They have headers and footers, like Private Key starts with
— — -BEGIN RSA PRIVATE KEY — — -
and ends with
— — -END RSA PRIVATE KEY — — -
Don’t miss those lines while copying, they are important.
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQCsXOj094/oLd8/89ArzH5+MJ5j8Pd1UX4+9LLURHILwcrwBAbi
0l9VohJ4IA04rsjkUqLpZSjGzikOhFwNNiBcJ1xQ9A9bgoVqYFvJpNcG0xRFeHoT
ZnX7jcBQAyrUKioI7OQ/pUnqtNfXlK7cVSi+80+Bvwaa2Hwhcg66xUzcqwIDAQAB
AoGBAIooMuZgJS5yznbhhGQHFwEpEVyEgqW7+5iU5V61ukBoRrVaVPasr5PhRDKb
Zl2f5BD3l/PCjQvFpi0ntO02DHrdXDlLzG89XPaCylyHJhOS1eohtcS19oRxtq78
jMQChZ/OxedCtmsUiT3yX2bl0pyvwzoDkwjNAfe4XLa4ZwfhAkEA8Yjo6LBaqxSC
ls0eP+xUSc5Jn9tRhGzYBNlhX6+duMcc1WwWHhvpekcqLj95feH9V7Yh4XQJ8Pw5
CLKdug65ewJBALavfeVwRO13qRLpOU62BetF+CTvzGpdkmRLCo6B/Ryi6/4FuNgk
QRp/RvrG7JHEDnGJuPSdAfmFtBwMJtf1CpECQQDVYaix+SsAvSvpYej5fCWy1oYA
rddEcIwfLJRz3kgut+lnOwgHLY2Es142YWlJpt8UIBmqfcNSnOEeJ/5kIyIFAkBA
7I9migbFCiC5Ss+GDKR/38b3gY15Q7XyFMU0rjfBBJmwFmKB1iiY/SDBoQ6UI0Qq
z5I+xMnd3smKqjrnxvsxAkEA2I3Xvzn2kuW6KmUYHthBJkFS4ufrVTlv7foFgjOY
J/BSWpYaEqCCTLhPTUzaEer5w9SvTbWF9UYcb4OLa2c5JQ==
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsXOj094/oLd8/89ArzH5+MJ5j
8Pd1UX4+9LLURHILwcrwBAbi0l9VohJ4IA04rsjkUqLpZSjGzikOhFwNNiBcJ1xQ
9A9bgoVqYFvJpNcG0xRFeHoTZnX7jcBQAyrUKioI7OQ/pUnqtNfXlK7cVSi+80+B
vwaa2Hwhcg66xUzcqwIDAQAB
-----END PUBLIC KEY-----
Te keypair has been generated with 1024 bits. Smaller is for testing purposes possible, but it is only supported by the RSA-SHA256 algorithm. For RSA-SHA384 and RSA-SHA512 a kepair with 1024 should be created. Since we are using the library for signing and verifying tokens in the mock server (running in the browser), do not use larger than 1024 Bits keypairs. 2048 Bits compared to 1024 Bits is 6x slower.
There are some useful jwt libraries available: jsonwebtoken, jwt-decode and @auth0/angular-jwt.
jsonwebtoken is running in node.js for signing, verifying and parsing the jwt-token but is
unfortunately not running in the browser with ng-cli. jwt-decode and @auth0/angular-jwt are purely for parsing
the tokens. Therefore we will write our
own library for signing, parsing and verifying the jwt tokens. Using such a library
only for the mock server is a bit an overkill. But for education purposes it is
nice to understand how we can implement it.
First of all we need some interfaces for header and payload. The payload consists of a
standard payload plus the project specific payload. The complete file is available
in the chapter The source code of the Jwt-library.
/**
* Interface JwtHeader
*/
export interface IJwtHeader {
typ: string;
alg: string
}
/**
* Interface for the standard claims
*/
export interface IJwtStdPayload {
iss?: string; // (issuer): Issuer of the JWT
sub?: string; // (subject): Subject of the JWT (the user)
aud?: string; // audience): Recipient for which the JWT is intended
exp?: number; // (expiration time): Time after which the JWT expires
nbf?: number; // (not before time): Time before which the JWT must not be accepted for processing
iat?: number; // issued at time): Time at which the JWT was issued; can be used to determine age of the JWT
jti?: string; // JWT ID): Unique identifier;
}
// Standard Payload definitions
const privateKey = ".. generated private key ..";
const publicKey = ".. generated public key ..";
const issuer = 'ZHAW';
const subject = 'Auction-App';
const audience = 'ASE2';
const payLoad: IJwtStdPayload = {
iat: 1568394275,
exp: 1568397875,
iss: issuer,
aud: audience,
sub: 'admin',
};
/**
* All supported crypto algorithms
*/
const algCryptoMap = {
HS256: 'SHA256',
HS384: 'SHA384',
HS512: 'SHA512',
RS256: 'RSA-SHA256',
RS384: 'RSA-SHA384',
RS512: 'RSA-SHA512',
none: 'none'
};
const hmacHeader: IJwtHeader = {
"typ": "JWT",
"alg": "HS256"
};
const rsaHeader: IJwtHeader = {
"typ": "JWT",
"alg": "RS256"
};
// Project specific claims
export interface JWTClaims {
roles: Array<string>;
accessToken: string;
}
const roles: Array<string> = ['admin','user'];
const claims: JWTClaims = {
roles: roles,
accessToken: 'secretaccesstoken',
};Keep the payload as small as possible in size, because:
- It has to be passed on each request. It will slow the your app
- Information is sensitive, even though JWT is encoded, yet it resides in unreliable client system.
There are standard fields for the payload. To make the JWT efficient we will be using only the minimum:
- iss (issuer): Issuer of the JWT
- sub (subject): Subject of the JWT (the user)
- aud (audience): Recipient for which the JWT is intended
- exp (expiration time): Time after which the JWT expires
- nbf (not before time): Time before which the JWT must not be accepted for processing
- iat (issued at time): Time at which the JWT was issued; can be used to determine age of the JWT
- jti (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once)
There are several options creating a jwt-library in the browser:
- using the nodejs crypto library by integration a custom ng-cli webpack configuration
- using other crpyto libraries like crpyto-js and node-forge.
We will use the crpyto-js library for the HMAC algorithm and node-forge for the RSA private and public key signing.
We want some easy usable functions for signing, decoding and verifying the tokens. The use of the functions can be reviewed in the following unit test source code.
const token = createToken(header, payLoad, claims, privateKey, false);
const valid:boolean = verifyToken(token, publicKey);
const token = decodeToken(token);The Jwt-library consists of 4 classes:
- Jwt: the main class to create, sign and verify the token
- JwtParser: is decoding the token
- JwtHeader: contains the 2 header attributes
- JwtBody: contains the payload (standard and customs claims)
The library is united tested with Jasmine standard. Create a file helper.jwt.ts
in the folder src/app/shared/helper.
import * as uuid from "uuid";
import {Buffer} from 'buffer';
import * as forge from 'node-forge';
import {pki} from 'node-forge';
import * as CryptoJS from 'crypto-js';
import base64url, {Base64Url} from './base64url';
/**
* Creates a signed JWT token
*
* @param header the header information (type and algorithm
* @param payload the standard claims to jwt
* @param claims the project specific claims
* @param signingKey private key or shared secret
*/
export function createToken(header: IJwtHeader, payload: IJwtStdPayload, claims: any, signingKey: string, enforceFields = true): string {
const union = {...payload, ...claims};
const jwt = new Jwt(header, union, enforceFields);
jwt.setSigningKey(signingKey);
return jwt.getSignedToken();
}
/**
* Decodes a token based and returns a Jwt object. The signature is NOT verified.
* @param token
*/
export function decodeToken(token: string): Jwt | JwtParseError {
return new JwtParser().parse(token);
}
/**
* Verifies the token signature
* @param token
* @param key
*/
export function verifyToken(token: string, key: string): boolean {
const jwt = new JwtParser().parse(token);
if (jwt instanceof Jwt) {
jwt.setPublicKey(key);
return jwt.verifySignature();
}
return false;
}
/**
* Encrypt a message with symemtric aes algorithm
* @param message
* @param secret
*/
export function encrypt(message: string, secret: string) {
return CryptoJS.AES.encrypt(message, secret).toString();
}
/**
* Decrypt a message with symmetric aes algorithm
* @param message
* @param secret
*/
export function decrypt(message: string, secret: string) {
const bytes = CryptoJS.AES.decrypt(message, secret);
return bytes.toString(CryptoJS.enc.Utf8);
}
/**
* Generates a keypair with a public and a private key
* @param bits 512, 1024, 2048
*
*/
export function createKeyPair(bits: number): pki.rsa.KeyPair {
return pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
}
/**
* Interface JwtHeader
*/
export interface IJwtHeader {
typ: string;
alg: string
}
/**
* Interface for the standard claims
*/
export interface IJwtStdPayload {
iss?: string; // (issuer): Issuer of the JWT
sub?: string; // (subject): Subject of the JWT (the user)
aud?: string; // audience): Recipient for which the JWT is intended
exp?: number; // (expiration time): Time after which the JWT expires
nbf?: number; // (not before time): Time before which the JWT must not be accepted for processing
iat?: number; // issued at time): Time at which the JWT was issued; can be used to determine age of the JWT
jti?: string; // JWT ID): Unique identifier;
}
/**
* All supported crypto algorithms
*/
const algCryptoMap = {
HS256: 'SHA256',
HS384: 'SHA384',
HS512: 'SHA512',
RS256: 'RSA-SHA256',
RS384: 'RSA-SHA384',
RS512: 'RSA-SHA512',
none: 'none'
};
/**
* Error text
*/
const properties = {
"errors": {
"PARSE_ERROR": "Jwt cannot be parsed",
"EXPIRED": "Jwt is expired",
"UNSUPPORTED_SIGNING_ALG": "Unsupported signing algorithm",
"SIGNING_KEY_REQUIRED": "Signing key is required",
"PUBLIC_KEY_REQUIRED": "Signing key is required",
"SIGNATURE_MISMTACH": "Signature verification failed",
"SIGNATURE_ALGORITHM_MISMTACH": "Unexpected signature algorithm",
"NOT_ACTIVE": "Jwt not active",
"KEY_RESOLVER_ERROR": "Error while resolving signing key for kid \"%s\""
}
};
/**
* base64 encoder for the rsa signature
* @param str
*/
function sigBase64EncodeUrl(str) {
return base64url.fromBase64(str);
}
/**
* Base64 encoder for body and header
* @param data
*/
function base64urlEncode(data) {
const str = typeof data === 'number' ? data.toString() : data;
return base64url.fromString(str);
}
/**
* Error class for the Jwt class
*/
class JwtError extends Error {
constructor(public message) {
super('JwtError: ' + message);
}
}
/**
* Error class for the parser class
*/
export class JwtParseError extends Error {
constructor(public message) {
super('JwtParseError: ' + message);
}
}
/**
* Represents a token consisting of 3 parts:
* header, body and signature
* The claims are stored in the body (standard claims and extra claims)
* For the signing is a private key or a shared secret necessary
* The function getSignedToken creates the final token string
*/
export class Jwt {
body: JwtBody;
header: JwtHeader; //
signingKey: string; // contains the private key in pem with -----BEGIN RSA PRIVATE KEY----- header
publicKey: string; // contains the private key in pem with -----BEGIN PUBLIC KEY----- header
signature: string; // contains the segement 3
verificationInput: string; // contains the segements 0 and 1
private algTypeMap = {
HS256: 'hmac',
HS384: 'hmac',
HS512: 'hmac',
RS256: 'sign',
RS384: 'sign',
RS512: 'sign',
};
constructor(header, claims, enforceDefaultFields) {
this.header = new JwtHeader(header);
this.body = new JwtBody(claims);
if (enforceDefaultFields === true) {
if (!this.body.jti) {
this.setJti(uuid.v4());
}
if (!this.body.iat) {
this.setIssuedAt(this.nowEpochSeconds());
}
if (this.body.exp) {
if (this.body.exp === 0) {
this.setExpiration((this.nowEpochSeconds() + (60 * 60)) * 1000);
}
} else {
this.setExpiration((this.nowEpochSeconds() + (60 * 60)) * 1000);
}
if (!this.isSupportedAlg(this.header.alg)) {
throw new Error(`ERROR algorithm ${this.header.alg} is not supported`);
}
}
}
nowEpochSeconds() {
return Math.floor(new Date().getTime() / 1000);
}
setClaim(claim, value) {
this.body[claim] = value;
return this;
}
setHeader(param, value) {
this.header[param] = value;
return this;
}
setJti(jti) {
this.body.jti = jti;
return this;
}
setSubject(sub) {
this.body.sub = sub;
return this;
}
setIssuer(iss) {
this.body.iss = iss;
return this;
}
setIssuedAt(iat) {
this.body.iat = iat;
return this;
}
setExpiration(exp) {
if (exp) {
this.body.exp = Math.floor((exp instanceof Date ? exp : new Date(exp)).getTime() / 1000);
}
}
getExpiration() {
return this.body.exp;
}
getExpirationDate(): Date {
const date = new Date(0);
date.setUTCSeconds(this.body.exp);
return date;
}
setNotBefore(nbf) {
if (nbf) {
this.body.nbf = Math.floor((nbf instanceof Date ? nbf : new Date(nbf)).getTime() / 1000);
}
}
isSupportedAlg(alg) {
return !!algCryptoMap[alg];
}
setSigningAlgorithm(alg) {
if (!this.isSupportedAlg(alg)) {
throw new JwtError(properties.errors.UNSUPPORTED_SIGNING_ALG);
}
this.header.alg = alg;
return this;
}
isExpired() {
return new Date(this.body.exp * 1000) < new Date();
}
isNotBefore() {
return new Date(this.body.nbf * 1000) >= new Date();
}
setSigningKey(privateKEY: string) {
this.signingKey = privateKEY;
}
setPublicKey(publicKEY: string) {
this.publicKey = publicKEY;
}
/**
* Signs the token
*
* @param payload the data to be signed (part 1 and part 2 of the token)
* @param algorithm HMAC or RAS algorithm
* @param cryptoInput privateKey of sharedSecret
*/
private sign(payload, algorithm, cryptoInput) {
let buffer;
const cryptoAlgName = algCryptoMap[algorithm];
const signingType = this.algTypeMap[algorithm];
if (!cryptoAlgName) {
throw new JwtError(properties.errors.UNSUPPORTED_SIGNING_ALG);
}
if (signingType === 'hmac') {
switch (algorithm) {
case 'HS256':
buffer = this.hmacBase64url(CryptoJS.HmacSHA256(payload, cryptoInput));
break;
case 'HS384':
buffer = this.hmacBase64url(CryptoJS.HmacSHA384(payload, cryptoInput));
break;
case 'HS512':
buffer = this.hmacBase64url(CryptoJS.HmacSHA512(payload, cryptoInput));
break;
}
} else {
const privateKey1 = forge.pki.privateKeyFromPem(cryptoInput) as pki.rsa.PrivateKey;
let md;
switch (algorithm) {
case 'RS256':
md = forge.md.sha256.create();
break;
case 'RS384':
md = forge.md.sha384.create();
break;
case 'RS512':
md = forge.md.sha512.create();
break;
}
md.update(payload, 'utf8');
buffer = sigBase64EncodeUrl(btoa(privateKey1.sign(md)));
}
return buffer;
}
private hmacBase64url(source) {
// Encode in classical base64
let base64 = CryptoJS.enc.Base64.stringify(source);
return base64url.fromBase64(base64);
}
/**
* The Jwt will be Signed and the 3-parts token is returns
*/
getSignedToken(): string {
var segments = [];
segments.push(this.header.compact());
segments.push(this.body.compact());
if (this.header.alg !== 'none') {
if (this.signingKey) {
this.signature = this.sign(segments.join('.'), this.header.alg, this.signingKey);
segments.push(this.signature);
} else {
throw new Error(properties.errors.SIGNING_KEY_REQUIRED);
}
}
this.verificationInput = segments[0] + '.' + segments[1];
return segments.join('.');
}
/**
* Verifies if a the Jwt token class has a valid token
*/
verifySignature(): boolean {
if (this.publicKey === 'undefined' || this.publicKey === '') throw new Error(properties.errors.PUBLIC_KEY_REQUIRED);
const signingType = this.algTypeMap[this.header.alg];
if (signingType === 'hmac') {
return (this.signature === this.sign(this.verificationInput, this.header.alg, this.publicKey));
} else if (signingType === 'sign') {
const publicKey = forge.pki.publicKeyFromPem(this.publicKey) as pki.rsa.PublicKey;
let md;
switch (this.header.alg) {
case 'RS256':
md = forge.md.sha256.create();
break;
case 'RS384':
md = forge.md.sha384.create();
break;
case 'RS512':
md = forge.md.sha512.create();
break;
}
md.update(this.verificationInput, 'utf8');
const decodedSignature = base64url.toBase64(this.signature);
return publicKey.verify(md.digest().bytes(), atob(decodedSignature));
} else throw new Error(properties.errors.SIGNATURE_ALGORITHM_MISMTACH);
}
}
/**
* The parser is used to inspect and decode a token
* The parse function returns a Jwt object for a valid token
*/
export class JwtParser {
private safeJsonParse(input: any) {
let result;
try {
result = JSON.parse(Buffer.from(base64url.toBase64(input), 'base64').toString());
} catch (e) {
return e;
}
return result;
}
/**
* parse is creating a Jwt token class
* @param jwtString the encoded 3-parts jwt token
*/
parse(jwtString) {
let segments = jwtString.split('.');
let signature;
if (segments.length < 2 || segments.length > 3) {
return new JwtParseError(properties.errors.PARSE_ERROR);
}
let header = this.safeJsonParse(segments[0]);
let body = this.safeJsonParse(segments[1]);
if (segments[2]) {
signature = segments[2];
}
if (header instanceof Error) {
return new JwtParseError(properties.errors.PARSE_ERROR);
}
if (body instanceof Error) {
return new JwtParseError(properties.errors.PARSE_ERROR);
}
var jwt = new Jwt(header, body, false);
jwt.setSigningAlgorithm(header.alg);
jwt.signature = signature;
jwt.verificationInput = segments[0] + '.' + segments[1];
jwt.header = new JwtHeader(header);
return jwt;
}
}
/**
* Represents the body of the jwt token, which consists of the standard payload
* and the project specific claims
*/
class JwtBody implements IJwtStdPayload {
iss?: string; // (issuer): Issuer of the JWT
sub?: string; // (subject): Subject of the JWT (the user)
aud?: string; // audience): Recipient for which the JWT is intended
exp?: number; // (expiration time): Time after which the JWT expires
nbf?: number; // (not before time): Time before which the JWT must not be accepted for processing
iat?: number; // issued at time): Time at which the JWT was issued; can be used to determine age of the JWT
jti?: string; // JWT ID): Unique identifier;
constructor(claims) {
var self = this;
if (claims) {
Object.keys(claims).forEach(function (k) {
self[k] = claims[k];
});
}
}
toJSON() {
let acc = {};
Object.keys(this).forEach(key => {
if (typeof (this[key]) !== 'undefined' && this[key] !== '') {
acc[key] = this[key];
}
});
return acc;
}
compact() {
return base64urlEncode(JSON.stringify(this));
}
}
/**
* Represents the header of the JWT token
* Contains the type and the algorithm
* The default is HMAC 256
*/
class JwtHeader implements IJwtHeader {
typ;
alg;
constructor(header: IJwtHeader) {
this.typ = header && header.typ || 'JWT';
this.alg = header && header.alg || 'HS256';
}
compact() {
return base64urlEncode(JSON.stringify(this));
};
}The library is united tested with Jasmine standard. Create a file helper.jwt.spec.ts
in the folder src/app/shared/helper.
The following tests are implemented:
- RSA256 token with key 512 should be created
- RSA256 token should be created
- RSA256 token with small payload should be created
- RSA384 token should be created
- RSA512 token should be created
- HMAC256 token should be created
- HMAC384 token should be created
- HMAC512 token should be created
- RSA256 token with key 512 should be verified
- RSA256 token should be verified
- RSA256 token with small payload should be verified
- RSA384 token should be verified
- RSA512 token should be verified
- HMAC256 token should be verified
- HMAC384 token should be verified
- HMAC512 token should be verified
- HMAC512 token should not be verified
import {createToken, verifyToken, IJwtHeader, IJwtStdPayload, JwtParseError, Jwt} from './helper.jwt';
const issuer = 'ZHAW';
const subject = 'Auction-App';
const audience = 'ASE2';
const sharedTestSecret = 'sharedsecret';
const private512TestKey =
`-----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-----`;
const public512TestKey =
`-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAICVyX5OguWsNi8lxzDEtVbFLeuW5pmQ
VPKdOY3FPPTrofDeWYVybMT7/3fcyP+L/CH4T7z9suw4c4FBB2SOso8CAwEAAQ==
-----END PUBLIC KEY-----`;
const private1024TestKey =
`-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQCsXOj094/oLd8/89ArzH5+MJ5j8Pd1UX4+9LLURHILwcrwBAbi
0l9VohJ4IA04rsjkUqLpZSjGzikOhFwNNiBcJ1xQ9A9bgoVqYFvJpNcG0xRFeHoT
ZnX7jcBQAyrUKioI7OQ/pUnqtNfXlK7cVSi+80+Bvwaa2Hwhcg66xUzcqwIDAQAB
AoGBAIooMuZgJS5yznbhhGQHFwEpEVyEgqW7+5iU5V61ukBoRrVaVPasr5PhRDKb
Zl2f5BD3l/PCjQvFpi0ntO02DHrdXDlLzG89XPaCylyHJhOS1eohtcS19oRxtq78
jMQChZ/OxedCtmsUiT3yX2bl0pyvwzoDkwjNAfe4XLa4ZwfhAkEA8Yjo6LBaqxSC
ls0eP+xUSc5Jn9tRhGzYBNlhX6+duMcc1WwWHhvpekcqLj95feH9V7Yh4XQJ8Pw5
CLKdug65ewJBALavfeVwRO13qRLpOU62BetF+CTvzGpdkmRLCo6B/Ryi6/4FuNgk
QRp/RvrG7JHEDnGJuPSdAfmFtBwMJtf1CpECQQDVYaix+SsAvSvpYej5fCWy1oYA
rddEcIwfLJRz3kgut+lnOwgHLY2Es142YWlJpt8UIBmqfcNSnOEeJ/5kIyIFAkBA
7I9migbFCiC5Ss+GDKR/38b3gY15Q7XyFMU0rjfBBJmwFmKB1iiY/SDBoQ6UI0Qq
z5I+xMnd3smKqjrnxvsxAkEA2I3Xvzn2kuW6KmUYHthBJkFS4ufrVTlv7foFgjOY
J/BSWpYaEqCCTLhPTUzaEer5w9SvTbWF9UYcb4OLa2c5JQ==
-----END RSA PRIVATE KEY-----`;
const public1024TestKey =
`-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsXOj094/oLd8/89ArzH5+MJ5j
8Pd1UX4+9LLURHILwcrwBAbi0l9VohJ4IA04rsjkUqLpZSjGzikOhFwNNiBcJ1xQ
9A9bgoVqYFvJpNcG0xRFeHoTZnX7jcBQAyrUKioI7OQ/pUnqtNfXlK7cVSi+80+B
vwaa2Hwhcg66xUzcqwIDAQAB
-----END PUBLIC KEY-----`;
const hmac256Header: IJwtHeader = {
"typ": "JWT",
"alg": "HS256"
};
const hmac384Header: IJwtHeader = {
"typ": "JWT",
"alg": "HS384"
};
const hmac512Header: IJwtHeader = {
"typ": "JWT",
"alg": "HS512"
};
const rsa256Header: IJwtHeader = {
"typ": "JWT",
"alg": "RS256"
};
const rsa384Header: IJwtHeader = {
"typ": "JWT",
"alg": "RS384"
};
const rsa512Header: IJwtHeader = {
"typ": "JWT",
"alg": "RS512"
};
const payLoad: IJwtStdPayload = {
iat: 1568394275,
exp: 1568397875,
iss: issuer,
aud: audience,
sub: 'admin',
};
const smallPayLoad: IJwtStdPayload = {
iat: 1568394275,
exp: 1568397875,
sub: 'a',
};
const roles: Array<string> = ['admin','user'];
const claims = {
roles: roles,
accessToken: 'secretaccesstoken',
};
const rsa256Token512key = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.NrV6aPYIr0yffWIGuLWRUPUNcUAJ14aaUteYgjDj92kGlGatacud9sLGeesZVlbz_ftwmFvaC2EGw-issPKT3w';
const rsa256Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.cJF7Z_4dbFkHdbx-2TogMqppa3MoLzjj7O0XOyl7ZMDSZDiyRvSZhwKOT40gdYO1iW65ZYnpeumEcCrYM_KnfMV3i9d9LOPBDYakerpA-lHD_tfaB2rNWFgjjtg1IhvI-_1tSYfTjosPB2KB110t3Jz_iTSAFV8AxM02UubddDo';
const rsaSmallPayload256Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwic3ViIjoiYSJ9.DZXu8tq3b-H0RL5zAO9pjWe9b4k1tOeTYh4on_Y050UdPUDrlqs-Cv1oasB8R4gwece-PG3_NbF2vWTfuDn1aQEUXCneCaHACu821am6AHhBZMkM99hVyZozWAvw3ORl3YyW2ZOvTsD1ohhtANRBePDT60iWurHocQs-yKi9J54';
const rsa384Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.UtwZwkwHunSP-OI-Pa0O9Mk4SAP32oPEd-uSWadpDA8JN3m99azOwjdRKEA5t073nkEwj8aogG0Zgb4qi7VvLalzsX7fKtu5I0SncLCIVq4aATd-u4l-Tfs3lfcESEwJ3dJgZ1dFWDFsfxWNxmBhVHPeeRKZglg-3pLkYgv6-VQ';
const rsa512Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.U-pVG60rIETwAIbwU3kHtq4bJzqsxpIrEhhgpnd8eOVjOnj6PyjDpP2pUn6G7-QdmjQ1aQq2qafXmIvDsCGcPIq8hT62kBMywi0uKnxmDOKX7iEvD-8KYVezEUs-gynIOJUe_ONLeAjwlWs5mMi0Wi4jSakz_JQ9Dl-V3rn5pas';
const hmac256Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.4LVHe-BndqFFe6hTm-DH6K8KHZtE9tH67iEOpk81gaw';
const hmac384Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.hdJ5z95tUb-NzeS_jKclAxoQV32Bb8x8KRAGKh-RJviOIr2o35-88bk0Aed1DMSS';
const hmac512Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.EiIsWGjsTSpxtD0CBhQy4PxRr2ZcCkdVXR7OaV6-8dZp0jDBdjFxlwVFJ-X4hhZ8fcnb4L3LL1dR5-OCPEXJBA';
function createRsaJwtToken(header: IJwtHeader, payLoad: IJwtStdPayload, claims): string {
return createToken(header, payLoad, claims, private1024TestKey, false);
}
function createHmacJwtToken(header: IJwtHeader, payLoad: IJwtStdPayload, claims): string {
return createToken(header, payLoad, claims, sharedTestSecret, false);
}
function verifyRsaJwtToken(token: string): boolean {
return verifyToken(token, public1024TestKey);
}
export function verifyHmacJwtToken(token: string): boolean {
return verifyToken(token, sharedTestSecret);
}
describe('Helper JWT', () => {
// beforeEach(() => TestBed.configureTestingModule({}));
it('RSA256 token with key 512 should be created', () => {
const token = createToken(rsa256Header, payLoad, claims, private512TestKey, false);
expect(token).toEqual(rsa256Token512key);
});
it('RSA256 token should be created', () => {
const token = createRsaJwtToken(rsa256Header, payLoad, claims);
expect(token).toEqual(rsa256Token);
});
it('RSA256 token with small payload should be created', () => {
const token = createRsaJwtToken(rsa256Header, smallPayLoad, null);
expect(token).toEqual(rsaSmallPayload256Token);
});
it('RSA384 token should be created', () => {
const token = createRsaJwtToken(rsa384Header, payLoad, claims);
expect(token).toEqual(rsa384Token);
});
it('RSA512 token should be created', () => {
const token = createRsaJwtToken(rsa512Header, payLoad, claims);
expect(token).toEqual(rsa512Token);
});
it('HMAC256 token should be created', () => {
const token = createHmacJwtToken(hmac256Header, payLoad, claims);
expect(token).toEqual(hmac256Token);
});
it('HMAC384 token should be created', () => {
const token = createHmacJwtToken(hmac384Header, payLoad, claims);
expect(token).toEqual(hmac384Token);
});
it('HMAC512 token should be created', () => {
const token = createHmacJwtToken(hmac512Header, payLoad, claims);
expect(token).toEqual(hmac512Token);
});
it('RSA256 token with key 512 should be verified', () => {
const result = verifyToken(rsa256Token512key, public512TestKey);
expect(result).toBe(true);
});
it('RSA256 token should be verified', () => {
const result = verifyRsaJwtToken(rsa256Token);
expect(result).toBe(true);
});
it('RSA256 token with small payload should be verified', () => {
const result = verifyRsaJwtToken(rsaSmallPayload256Token);
expect(result).toBe(true);
});
it('RSA384 token should be verified', () => {
const result = verifyRsaJwtToken(rsa384Token);
expect(result).toBe(true);
});
it('RSA512 token should be verified', () => {
const result = verifyRsaJwtToken(rsa512Token);
expect(result).toBe(true);
});
it('HMAC256 token should be verified', () => {
const result = verifyHmacJwtToken(hmac256Token);
expect(result).toBe(true);
});
it('HMAC384 token should be verified', () => {
const result = verifyHmacJwtToken(hmac384Token);
expect(result).toBe(true);
});
it('HMAC512 token should be verified', () => {
const result = verifyHmacJwtToken(hmac512Token);
expect(result).toBe(true);
});
it('HMAC512 token should not be verified', () => {
const result = verifyHmacJwtToken(hmac512Token + 't');
expect(result).toBe(false);
});
});The Jwt-library shall be integrated into the mock-backend-interceptor.service.ts file. We will integrate the new functions or expand existing functions for the token handling:
- isLoggedIn: checks if a correctly signed token is available
- isTokenExpired: checks if the token is expired
- isInRole: checks if the token contains the correct role
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 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;
}The interface to the library including the public and private key information shall be available in
a separate file jwt-backend.data.ts in the folder src/app/shared/helper/mock.
import {IJwtHeader, IJwtStdPayload, createToken, verifyToken} from '../helper.jwt';
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);
}The updated source code integrates the new jwt-library to the mock backend.
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 {createRsaJwtToken, verifyRsaJwtToken} from './jwt-backend.data';
import {decodeToken, IJwtStdPayload, Jwt} from '../helper.jwt';
/**
* 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
*
*/
@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(500))
.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') && 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.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);
}
return response;
}
// --- route functions ---
function authenticate() {
const {username, password} = body;
let users = JSON.parse(localStorage.getItem('users')) || [];
const user = users.find(x => x.username === username && x.password === password);
if (!user) {
return error('Username or password is incorrect');
} else {
const token = createToken(user.username);
let headers: HttpHeaders = new HttpHeaders();
headers = addTokenToHeader(headers, token);
headers = addAcceptToHeader(headers);
headers = addContentTypeToHeader(headers);
return ok({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName
}, headers);
}
}
function register() {
const user = body;
let users = JSON.parse(localStorage.getItem('users')) || [];
// console.log(users);
if (users.find(x => x.username === user.username)) {
return error('Username "' + user.username + '" is already taken')
}
console.log(user);
user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
console.log(user);
users.push(user);
localStorage.setItem('users', JSON.stringify(users));
return ok(user);
}
function getUsers() {
if (!isLoggedIn()) return unauthorized();
if (!isTokenExpired()) return expired();
if (!isInRole('admin')) return notInRole();
let users = JSON.parse(localStorage.getItem('users')) || [];
if (!isLoggedIn()) return unauthorized();
return ok(users);
}
function getUser() {
let users = JSON.parse(localStorage.getItem('users')) || [];
users = users.filter(x => x.id === idFromUrl());
if (users.length > 0) {
return ok(users[0]);
} else {
return noContent('User with id ' + idFromUrl() + ' not found.')
}
}
function getUserByName() {
let users = JSON.parse(localStorage.getItem('users')) || [];
users = users.filter(x => x.username === nameFromUrl());
if (users.length > 0) {
return ok(users[0]);
} else {
return noContent('User with name ' + nameFromUrl() + ' 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.')
}
}
function deleteUser() {
if (!isLoggedIn()) return unauthorized();
let users = JSON.parse(localStorage.getItem('users')) || [];
users = users.filter(x => x.id !== idFromUrl());
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
// 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) {
return throwError({error: {message}});
}
function unauthorized() {
return throwError({status: 401, error: {message: 'Unauthorised'}});
}
function expired() {
return throwError({status: 401, error: {message: 'Unauthorised - Token expired'}});
}
function notInRole() {
return throwError({status: 403, error: {message: 'Forbidden - not correct role'}});
}
function notFound() {
return throwError({status: 404, error: {message: 'Not found'}});
}
function noContent(message) {
return throwError({status: 204, error: {message: message}});
}
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 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 createToken(username: string) {
const roles: Array<string> = username === 'admin' ? ['admin', 'user'] : ['user'];
const payLoad: IJwtStdPayload = {
iat: 0,
exp: 0,
iss: "",
aud: "",
sub: username,
};
const claims = {
roles: roles,
accessToken: 'secretaccesstoken',
};
const token = createRsaJwtToken(payLoad, claims);
// console.log (verifyRsaJwtToken(token));
return token;
}
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())));
}
function nowEpochSeconds() {
return Math.floor(new Date().getTime() / 1000);
}
}
}
/**
* 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 tests are implemented:
- should catch 401
- should catch 401 at get users
- should return an auction
- should register a user
- should register an admin
- should authenticate an admin
- should not authenticate an unknown user
- should not authenticate a user with wrong password
- should get all users with valid token and role admin
- should NOT get all users with an EXPIRED token and role admin with return 401
- should NOT get all users with an INVALID token and role admin with return 401
- should authenticate a user
- should NOT get all users with valid token and role user with return 403 forbidden
We have created a file mock-backend-interceptor.service.spec.ts during the last task.
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;
}
const regUser: User = {
id: 0,
firstName: 'default',
lastName: 'default',
username: 'default',
password: 'default'
};
const adminRegUser: User = {
id: 0,
firstName: 'admin',
lastName: 'admin',
username: 'admin',
password: 'admin'
};
const loginUser: User = {
username: 'default',
password: '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').subscribe(() => {
}, () => {
// 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(() => {
fail();
}, (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 register an admin', (done) => {
httpClient.post<User>('/users/register', adminRegUser, httpOptions)
.subscribe((data) => {
const user: User = data.body;
const status: number = data.status;
expect(status).toBe(200);
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
});
} 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('mockTestResponse', data);
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);
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) => {
console.log('mockTestResponse', data);
expect(data.status).toBe(200);
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);
fail();
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);
fail();
done();
},
(err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(401);
done();
}, () => {
// 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
});
} 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);
fail();
done();
},
(err) => {
console.log('mockTestResponse', err);
expect(err.status).toBe(403);
done();
}, () => {
// console.log('Test complete');
done();
});
});
});Create a simple jwt-token-service to call the functions for signing, verifying and parsing the token
The injectable service shall call the functions in the Jwt-library.
- Create a folder
servicein the foldersrc/app/shared. - Create a file
jwt-token.service.tsint the foldersrc/app/shared/service.
The file contains the following content:
import {Injectable} from '@angular/core';
import {createToken, decodeToken, verifyToken, IJwtHeader, IJwtStdPayload, Jwt, JwtParseError} from '../helper/helper.jwt';
export interface JWTClaims {
roles: Array<string>;
accessToken: string;
}
@Injectable({providedIn: 'root'})
// @ts-ignore
export class JwtTokenService {
createToken(header: IJwtHeader, payload: IJwtStdPayload, claims: JWTClaims, signingKey: string): string {
return createToken(header, payload, claims, signingKey);
}
decodeToken(token: string): Jwt | JwtParseError {
return decodeToken(token);
}
verifyToken(token: string, key: string): boolean {
return verifyToken(token, key);
}
}Use the jwt-decoder library if only a decoding without signing and verifiying is required. This step is NOT neccessary if the own Jwt-library is used.
npm install jwt-decode --save
npm install @types/jwt-decode --save-dev
Import this package into your TypeScript class through this syntax:
import * as jwt_decode from "jwt-decode";You can use this library method for decoding your access token like this
getDecodedAccessToken(token: string): any {
try{
return jwt_decode(token);
}
catch(Error){
return null;
}
}The token parameter defines your access token which you get from your API.
Example:
let tokenInfo = this.getDecodedAccessToken(token); // decode token
let expireDate = tokenInfo.exp; // get token expiration dateTime
console.log(tokenInfo); // show decoded token object in consoleThe index.ts file is helping to shorten the imports in other files.
Create an index.ts file in the folder src/app/shared/helper.
export * from './helper.service'
export * from './base64url'
export * from './helper.jwt'
export * from './mock/mock.module';
export * from './angular-date-http-interceptor.service';