├── src
├── models
│ ├── supported-tokens.model.ts
│ └── token-validation.model.ts
├── introspection
│ ├── token-types.ts
│ ├── introspection.error.ts
│ ├── oauth2-introspection-options.ts
│ ├── oauth2-introspection-handler.ts
│ └── oidc-constants.ts
├── discovery
│ ├── discovery.error.ts
│ ├── discovery-document-response.ts
│ └── client-discovery.ts
├── errors
│ └── invalid-token-error.ts
├── index.ts
├── authentication-options.ts
├── jwt-token
│ ├── jwt-bearer-options.ts
│ └── jwt-token-handler.ts
├── token.retriever.ts
├── app.ts
└── access-token-handler.ts
├── .eslintignore
├── test
├── jwks.json
├── introspection-response.json
├── token-retriever.tests.ts
├── well-known.json
└── access-token-handler.tests.ts
├── tsconfig.json
├── .eslintrc
├── package.json
├── .gitignore
└── README.MD
/src/models/supported-tokens.model.ts:
--------------------------------------------------------------------------------
1 | export enum SupportedTokens {
2 | Both,
3 | Jwt,
4 | Reference
5 | }
6 |
--------------------------------------------------------------------------------
/src/introspection/token-types.ts:
--------------------------------------------------------------------------------
1 | export default class TokenTypes {
2 | public static AccessToken = "AccessToken";
3 | public static IdentityToken = "id_token";
4 | public static RefreshToken = "refresh_token";
5 |
6 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # /node_modules/* in the project root is ignored by default
2 | # build artefacts
3 | dist/*
4 | coverage/*
5 | # data definition files
6 | **/*.d.ts
7 | # 3rd party libs
8 | /src/public/
9 | # custom definition files
10 | /src/types/
11 |
--------------------------------------------------------------------------------
/test/jwks.json:
--------------------------------------------------------------------------------
1 | {
2 | "keys": [{
3 | "kty": "EC",
4 | "use": "sig",
5 | "kid": "9WTTu6ogg42jdvYJZMFQaw",
6 | "alg": "ES256",
7 | "x": "6y2OU8HlFvhmS9RQT74Tm26JeH1GILhdKWh4j0edkSw",
8 | "y": "QH6Gm269mXfXjjEqpus1yefErSMOxns2Ii7das63pD0",
9 | "crv": "P-256"
10 | }]
11 | }
--------------------------------------------------------------------------------
/src/discovery/discovery.error.ts:
--------------------------------------------------------------------------------
1 | export default class DiscoveryError extends Error {
2 | constructor(
3 | public authority: string,
4 | public description: string,
5 | public errorCode: string = "invalid_discovery") {
6 | super(errorCode);
7 | Error.captureStackTrace(this, DiscoveryError);
8 | }
9 |
10 |
11 | }
--------------------------------------------------------------------------------
/src/errors/invalid-token-error.ts:
--------------------------------------------------------------------------------
1 | export default class InvalidTokenError extends Error {
2 | constructor(
3 | public authority: string,
4 | public description: string,
5 | public token: string,
6 | public errorCode: string = "invalid_token") {
7 | super(errorCode);
8 | Error.captureStackTrace(this, InvalidTokenError);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/introspection/introspection.error.ts:
--------------------------------------------------------------------------------
1 | export default class IntrospectionError extends Error {
2 | constructor(
3 | public authority: string,
4 | public description: string,
5 | public errorCode: string = "invalid_introspection") {
6 | super(errorCode);
7 | Error.captureStackTrace(this, IntrospectionError);
8 | }
9 |
10 |
11 | }
--------------------------------------------------------------------------------
/test/introspection-response.json:
--------------------------------------------------------------------------------
1 | {
2 | "iss": "https://localhost:5000",
3 | "nbf": 1586032444,
4 | "exp": 1586036044,
5 | "aud": "jp_api",
6 | "client_id": "IS4-Admin",
7 | "sub": "0904e75e-413d-4860-9329-b2587247065a",
8 | "auth_time": 1585723675,
9 | "idp": "local",
10 | "amr": "pwd",
11 | "is4-rights": "manager",
12 | "role": "Administrator",
13 | "email": "bhdebrito@gmail.com",
14 | "username": "bruno",
15 | "active": true,
16 | "scope": "jp_api.is4"
17 | }
--------------------------------------------------------------------------------
/src/models/token-validation.model.ts:
--------------------------------------------------------------------------------
1 | import { isArray } from 'util';
2 |
3 | export default class TokenValidation{
4 |
5 |
6 | public static hasClaims(claims: any, key: string, value: string) {
7 |
8 | var foundClaims = claims[key];
9 | if (foundClaims != null) {
10 | if (isArray(foundClaims)) {
11 | return foundClaims.find(f => f == value);
12 | }
13 |
14 | return foundClaims == value;
15 | }
16 |
17 | return false;
18 | }
19 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import AccessTokenHandler from './access-token-handler';
2 | import DiscoveryError from './discovery/discovery.error';
3 | import InvalidTokenError from './errors/invalid-token-error';
4 | import IntrospectionError from './introspection/introspection.error';
5 | import { SupportedTokens } from './models/supported-tokens.model';
6 |
7 | export = {
8 | AccessTokenHandler: AccessTokenHandler,
9 | InvalidTokenError: InvalidTokenError,
10 | DiscoveryError: DiscoveryError,
11 | IntrospectionError: IntrospectionError,
12 | SupportedTokens: SupportedTokens
13 | }
--------------------------------------------------------------------------------
/src/authentication-options.ts:
--------------------------------------------------------------------------------
1 | import TokenTypes from './introspection/token-types';
2 | import { SupportedTokens } from './models/supported-tokens.model';
3 |
4 | export default class AuthenticationOption {
5 | constructor(
6 | public authority: string,
7 | public apiName: string = "",
8 | public apiSecret: string = "",
9 | public supportedTokens: SupportedTokens = SupportedTokens.Both,
10 | public tokenTypeHint: string = TokenTypes.AccessToken,
11 | public enableCache: boolean = false,
12 | public cacheDuration: number = 600) {
13 | }
14 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "esModuleInterop": true,
5 | "allowSyntheticDefaultImports": true,
6 | "target": "es6",
7 | "noImplicitAny": true,
8 | "moduleResolution": "node",
9 | "sourceMap": true,
10 | "outDir": "lib",
11 | "declaration": true,
12 | "baseUrl": ".",
13 | "paths": {
14 | "*": [
15 | "node_modules/*",
16 | "src/types/*"
17 | ]
18 | }
19 | },
20 | "include": [
21 | "src/**/*",
22 | "app.ts"
23 | ],
24 | "exclude": [
25 | "node_module",
26 | "lib"
27 | ]
28 | }
--------------------------------------------------------------------------------
/src/introspection/oauth2-introspection-options.ts:
--------------------------------------------------------------------------------
1 | import TokenTypes from './token-types';
2 |
3 | export default class OAuth2IntrospectionOptions{
4 | constructor(
5 | public authority: string,
6 | public clientId: string,
7 | public clientSecret: string,
8 | public requireHttpsMetadata: boolean = true,
9 | public tokenTypeHint = TokenTypes.AccessToken,
10 | public enableCache: boolean = false,
11 | public cacheDuration: number = 600) {
12 |
13 | if (clientId === "")
14 | return;
15 |
16 | if (clientSecret === "")
17 | throw new Error('clientId must be configured if clientSecret is set.');
18 | }
19 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "plugin:@typescript-eslint/recommended"
5 | ],
6 | "parserOptions": {
7 | "ecmaVersion": 2018,
8 | "sourceType": "module"
9 | },
10 | "rules": {
11 | "semi": [
12 | "error",
13 | "always"
14 | ],
15 | "quotes": [
16 | "error",
17 | "double"
18 | ],
19 | "@typescript-eslint/explicit-function-return-type": "off",
20 | "@typescript-eslint/no-explicit-any": 1,
21 | "@typescript-eslint/no-inferrable-types": [
22 | "warn",
23 | {
24 | "ignoreParameters": true
25 | }
26 | ],
27 | "@typescript-eslint/no-unused-vars": "warn"
28 | }
29 | }
--------------------------------------------------------------------------------
/src/jwt-token/jwt-bearer-options.ts:
--------------------------------------------------------------------------------
1 | export default class JwtBearerOptions {
2 | constructor(
3 |
4 | ///
5 | /// Gets or sets the Authority to use when making OpenIdConnect calls.
6 | ///
7 | public authority: string,
8 | ///
9 | /// Gets or sets if HTTPS is required for the metadata address or authority.
10 | /// The default is true. This should be disabled only in development environments.
11 | ///
12 | public requireHttpsMetadata = true,
13 |
14 |
15 | ///
16 | /// Gets or sets the audience for any received OpenIdConnect token.
17 | ///
18 | ///
19 | /// The expected audience for any received OpenIdConnect token.
20 | ///
21 | public audience: string = "",
22 |
23 |
24 | public checkAudience: boolean = true) { }
25 | }
--------------------------------------------------------------------------------
/src/token.retriever.ts:
--------------------------------------------------------------------------------
1 | import { SupportedTokens } from './models/supported-tokens.model';
2 |
3 | export default class TokenRetriever {
4 |
5 | public static Retrieve(authorization: string, scheme: string = "bearer") {
6 |
7 | if (authorization == null || authorization.trim() == "")
8 | return null;
9 |
10 | if (authorization.toLowerCase().startsWith(scheme))
11 | return authorization.substring(`${scheme}`.length + 1).trim();
12 |
13 | return null;
14 | }
15 |
16 | public static FromAuthorizationHeader(request: any, scheme: string = "bearer") {
17 | const authorization = request.headers["authorization"].toString().toLower();
18 | return this.Retrieve(authorization);
19 | }
20 |
21 | public static FindType(token: string) {
22 | if (token.includes("."))
23 | return SupportedTokens.Jwt;
24 |
25 | return SupportedTokens.Reference;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import https from 'https';
2 |
3 | import accessTokenHandler from './access-token-handler';
4 | import AuthenticationOptions from './authentication-options';
5 |
6 | require('dotenv').config({ path: __dirname + '/.env' });
7 |
8 |
9 | https.globalAgent.options.rejectUnauthorized = process.env.rejectUnauthorized == "true";
10 |
11 | var at_validation = new accessTokenHandler({
12 | authority: 'https://sso.jpproject.net',
13 | apiName: "",
14 | apiSecret: "",
15 | });
16 |
17 | at_validation.Handle('bearer eyJhbGciOiJFUzI1NiIsImtpZCI6IjlXVFR1Nm9nZzQyamR2WUpaTUZRYXciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODU5NTA3MzMsImV4cCI6MTU4NTk1NDMzMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImpwX2FwaSIsImNsaWVudF9pZCI6IklTNC1BZG1pbiIsInN1YiI6IjA5MDRlNzVlLTQxM2QtNDg2MC05MzI5LWIyNTg3MjQ3MDY1YSIsImF1dGhfdGltZSI6MTU4NTcyMzY3NSwiaWRwIjoibG9jYWwiLCJpczQtcmlnaHRzIjoibWFuYWdlciIsInJvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZW1haWwiOiJiaGRlYnJpdG9AZ21haWwuY29tIiwidXNlcm5hbWUiOiJicnVubyIsInNjb3BlIjpbInJvbGUiLCJlbWFpbCIsInByb2ZpbGUiLCJvcGVuaWQiLCJqcF9hcGkuaXM0Il0sImFtciI6WyJwd2QiXX0.S8fc_c1V887By0gJoYk0Neoeh1OvbHJdNiAzNOMhWaSVxAymdahRTsCCf-43XHiDzP4cyT9K6CdGZbU_z7l7UQ').then(console.log).catch(console.warn);
--------------------------------------------------------------------------------
/test/token-retriever.tests.ts:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import https from 'https';
3 |
4 | import TokenRetriever from '../src/token.retriever';
5 |
6 | const jws = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjlXVFR1Nm9nZzQyamR2WUpaTUZRYXciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODU3MjMyNDYsImV4cCI6MTU4NTcyNjg0NiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImpwX2FwaSIsImNsaWVudF9pZCI6IklTNC1BZG1pbiIsInN1YiI6IjA5MDRlNzVlLTQxM2QtNDg2MC05MzI5LWIyNTg3MjQ3MDY1YSIsImF1dGhfdGltZSI6MTU4NTUyODczNCwiaWRwIjoibG9jYWwiLCJpczQtcmlnaHRzIjoibWFuYWdlciIsInJvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZW1haWwiOiJiaGRlYnJpdG9AZ21haWwuY29tIiwidXNlcm5hbWUiOiJicnVubyIsInNjb3BlIjpbInJvbGUiLCJlbWFpbCIsInByb2ZpbGUiLCJvcGVuaWQiLCJqcF9hcGkuaXM0Il0sImFtciI6WyJwd2QiXX0.uIXwj7hSOrTJIlUJxelBm9hM9vXSXEtLI3eMbc81VVIW5oSzTfVb39S-I67zCNfLCi4V-hD1_qBa7jdtI_mgmg";
7 | const assert = chai.assert;
8 | // it works
9 | https.globalAgent.options.rejectUnauthorized = process.env.rejectUnauthorized == "true";
10 |
11 | describe('TokenRetriever', () => {
12 | it('Should return null when bearer not found', () => {
13 | assert.isNull(TokenRetriever.Retrieve("aoeua euo ao", "bearer"))
14 | });
15 |
16 | it('Should return Bearer token', () => {
17 | const foundToken =TokenRetriever.Retrieve(`Bearer ${jws}`);
18 | expect(foundToken).not.to.be.null;
19 | });
20 |
21 | it('Should token same JWS', () => {
22 | const foundToken =TokenRetriever.Retrieve(`Bearer ${jws}`);
23 | assert.equal(jws, foundToken);
24 | });
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/src/discovery/discovery-document-response.ts:
--------------------------------------------------------------------------------
1 | import { JWKS } from 'jose';
2 | import NodeCache from 'node-cache';
3 |
4 | const myCache = new NodeCache({useClones: false});
5 | export default class DiscoveryDocumentResponse {
6 |
7 | issuer: string;
8 | authorization_endpoint: string;
9 | token_endpoint: string;
10 | userinfo_endpoint: string;
11 | jwks_uri: string;
12 | scopes_supported: string[];
13 | response_types_supported: string[];
14 | grant_types_supported: string[];
15 | subject_types_supported: string[];
16 | id_token_signing_alg_values_supported: string[];
17 | id_token_encryption_alg_values_supported: string[];
18 | id_token_encryption_enc_values_supported: string[];
19 | token_endpoint_auth_methods_supported: string[];
20 | token_endpoint_auth_signing_alg_values_supported: string[];
21 | claims_parameter_supported: boolean;
22 | request_parameter_supported: boolean;
23 | request_uri_parameter_supported: boolean;
24 | device_authorization_endpoint: string;
25 | introspection_endpoint: string;
26 | revocation_uEndpoint: string;
27 | KeySet: JWKS.KeyStore;
28 |
29 | public loaded: boolean;
30 |
31 | public static getInstance() {
32 | var instance = myCache.get("DiscoveryDocument");
33 | if (instance == undefined) {
34 | instance = new DiscoveryDocumentResponse();
35 | instance.loaded = false;
36 |
37 | myCache.set("DiscoveryDocument", instance, 600);
38 | }
39 | return instance;
40 | }
41 |
42 | public static saveInstance(instance: DiscoveryDocumentResponse) {
43 | myCache.set("DiscoveryDocument", instance, 600);
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/src/jwt-token/jwt-token-handler.ts:
--------------------------------------------------------------------------------
1 | import { JWT } from 'jose';
2 | import url_parser from 'url';
3 | import { isArray } from 'util';
4 |
5 | import ClientDiscovery from '../discovery/client-discovery';
6 | import DiscoveryDocumentResponse from '../discovery/discovery-document-response';
7 | import InvalidTokenError from '../errors/invalid-token-error';
8 | import TokenValidation from '../models/token-validation.model';
9 | import JwtBearerOptions from './jwt-bearer-options';
10 |
11 | export default class JwtTokenHandler {
12 |
13 |
14 | constructor(public options: JwtBearerOptions) {
15 | if (!this.isSecureScheme(this.options.authority)) {
16 | throw new InvalidTokenError(options.authority, "HTTPS required", null);
17 | }
18 | }
19 |
20 | /**
21 | * Validate JWT (In fact JWS)
22 | */
23 | public async Handle(token: string) {
24 |
25 | if (!this.isSecureScheme(token))
26 |
27 | await ClientDiscovery.loadJsonWebKeySet(this.options.authority);
28 |
29 | var discoveryDocument = DiscoveryDocumentResponse.getInstance();
30 | var claims = JWT.verify(token, discoveryDocument.KeySet);
31 |
32 | if (!this.options.checkAudience)
33 | return claims;
34 |
35 | if (TokenValidation.hasClaims(claims, "aud", this.options.audience))
36 | return claims;
37 |
38 | throw new InvalidTokenError(this.options.authority, `Failed to validate the token. Audience validation failed: ${this.options.audience}`, token);
39 | }
40 |
41 |
42 | ///
43 | /// Determines whether uses a secure scheme according to the policy.
44 | ///
45 | private isSecureScheme(url: string) {
46 | if (this.options.requireHttpsMetadata) {
47 | var parsedUrl = url_parser.parse(url);
48 | return parsedUrl.protocol === "https:";
49 | }
50 |
51 | return true;
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-accesstoken-validation",
3 | "author": "Bruno Brito",
4 | "repository": {
5 | "url": "git+https://github.com/brunohbrito/node-access-token-validation.git",
6 | "type": "git"
7 | },
8 | "bugs": {
9 | "url": "https://github.com/brunohbrito/node-access-token-validation/issues"
10 | },
11 | "homepage": "https://github.com/brunohbrito/node-access-token-validation#readme",
12 | "license": "MIT",
13 | "version": "1.0.5",
14 | "description": "Component to validate Bearer Token. It validate JWT tokens and reference tokens (Introspection) as well. It support almost all algorithms. RSA, ECDsa, HMAC. Trough jose.",
15 | "main": "lib/index.js",
16 | "types": "lib/index.d.ts",
17 | "scripts": {
18 | "test": "mocha -r ts-node/register test/**/*.tests.ts",
19 | "start": "tsc && npm run serve",
20 | "serve": "node lib/app.js",
21 | "build": "tsc",
22 | "prepublish": "tsc"
23 | },
24 | "directories": {
25 | "test": "mocha"
26 | },
27 | "keywords": [
28 | "oauth2",
29 | "oauth",
30 | "access_token",
31 | "jwks",
32 | "jwt"
33 | ],
34 | "devDependencies": {
35 | "@types/chai": "^4.2.11",
36 | "@types/debug": "^4.1.5",
37 | "@types/mocha": "^7.0.2",
38 | "@types/node": "^13.9.8",
39 | "@types/valid-url": "^1.0.3",
40 | "chai": "^4.2.0",
41 | "mocha": "^7.1.1",
42 | "nock": "^12.0.3",
43 | "ts-node": "^8.8.1",
44 | "tslint": "^6.1.0",
45 | "typescript": "^3.8.3"
46 | },
47 | "dependencies": {
48 | "@types/nock": "^11.1.0",
49 | "@types/sinon": "^9.0.0",
50 | "axios": "^0.19.2",
51 | "dotenv": "^8.2.0",
52 | "jose": "^1.25.0",
53 | "node-cache": "^5.1.0",
54 | "sinon": "^9.0.1",
55 | "valid-url": "^1.0.9"
56 | }
57 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/node
2 | # Edit at https://www.gitignore.io/?templates=node
3 |
4 | ### Node ###
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 | lib/
48 |
49 | # TypeScript v1 declaration files
50 | typings/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional REPL history
62 | .node_repl_history
63 |
64 | # Output of 'npm pack'
65 | *.tgz
66 |
67 | # Yarn Integrity file
68 | .yarn-integrity
69 |
70 | # dotenv environment variables file
71 | .env
72 | .env.test
73 |
74 | # parcel-bundler cache (https://parceljs.org/)
75 | .cache
76 |
77 | # next.js build output
78 | .next
79 |
80 | # nuxt.js build output
81 | .nuxt
82 |
83 | # rollup.js default build output
84 | dist/
85 |
86 | # Uncomment the public line if your project uses Gatsby
87 | # https://nextjs.org/blog/next-9-1#public-directory-support
88 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav
89 | # public
90 |
91 | # Storybook build outputs
92 | .out
93 | .storybook-out
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | #vscode
108 | .vscode/
109 |
110 | # Temporary folders
111 | tmp/
112 | temp/
113 |
114 | # End of https://www.gitignore.io/api/node
--------------------------------------------------------------------------------
/src/discovery/client-discovery.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import jose, { JWKS } from 'jose';
3 | import validUrl from 'valid-url';
4 |
5 | import OidcConstants from '../introspection/oidc-constants';
6 | import DiscoveryDocumentResponse from './discovery-document-response';
7 | import DiscoveryError from './discovery.error';
8 |
9 |
10 | export default class ClientDiscovery {
11 |
12 | /**
13 | * Load discovery document. .well-known
14 | */
15 | public static async loadDiscoveryDocument(authority: string) {
16 | if (!validUrl.isUri(authority))
17 | throw new DiscoveryError(authority, "Invalid uri");
18 |
19 | var discoveryDocument = DiscoveryDocumentResponse.getInstance();
20 | if (discoveryDocument != undefined && discoveryDocument.loaded)
21 | return;
22 |
23 | return axios.get(this.getDiscoveryUrl(authority)).then(response => {
24 | let instance = response.data;
25 | let keystore = new JWKS.KeyStore();
26 | instance = Object.assign({ loaded: true, KeySet: keystore }, instance);
27 | DiscoveryDocumentResponse.saveInstance(instance);
28 | });
29 | }
30 |
31 | /**
32 | * Load public keys from jwks_uri
33 | */
34 | public static async loadJsonWebKeySet(authority: string) {
35 |
36 | var discoveryDocument = DiscoveryDocumentResponse.getInstance();
37 | if (discoveryDocument == undefined || !discoveryDocument.loaded)
38 | await this.loadDiscoveryDocument(authority);
39 |
40 | // Reload from cache
41 | discoveryDocument = DiscoveryDocumentResponse.getInstance();
42 |
43 | return axios.get<{ keys: [] }>(discoveryDocument.jwks_uri).then(response => {
44 |
45 | const keys = response.data.keys.map(key => jose.JWK.asKey(key));
46 | keys.forEach(k => discoveryDocument.KeySet.add(k));
47 |
48 | DiscoveryDocumentResponse.saveInstance(discoveryDocument);
49 | });
50 | }
51 |
52 | private static getDiscoveryUrl(authority: string) {
53 | let url = this.removeTrailingSlash(authority);
54 |
55 | if (url.endsWith(OidcConstants.DiscoveryEndpoint)) {
56 | return authority;
57 | }
58 |
59 | return `${url}/${OidcConstants.DiscoveryEndpoint}`;
60 | }
61 |
62 | private static removeTrailingSlash(term: string) {
63 |
64 | if (term != null && term.endsWith("/"))
65 | term = term.substring(0, term.length - 1);
66 |
67 | return term;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/introspection/oauth2-introspection-handler.ts:
--------------------------------------------------------------------------------
1 | import Axios, { AxiosRequestConfig } from 'axios';
2 | import url_parser from 'url';
3 |
4 | import ClientDiscovery from '../discovery/client-discovery';
5 | import DiscoveryDocumentResponse from '../discovery/discovery-document-response';
6 | import IntrospectionError from './introspection.error';
7 | import OAuth2IntrospectionOptions from './oauth2-introspection-options';
8 |
9 |
10 |
11 | export default class OAuth2IntrospectionHandler {
12 |
13 | constructor(public options: OAuth2IntrospectionOptions) {
14 |
15 | if (!this.isSecureScheme(this.options.authority)) {
16 | throw new IntrospectionError(options.authority, "HTTPS required");
17 | }
18 | if (options.clientId == "" || options.clientSecret == "")
19 | throw new IntrospectionError(options.authority, "Invalid clientId or clientSecret for introspection.");
20 | }
21 |
22 | public async Introspect(token: string) {
23 |
24 | await ClientDiscovery.loadDiscoveryDocument(this.options.authority);
25 |
26 | const authorizationHeader = `Basic ${Buffer.from(`${this.options.clientId}:${this.options.clientSecret}`).toString('base64')}`;
27 | const fetchOption: AxiosRequestConfig = {
28 | method: 'POST',
29 | headers: {
30 | Authorization: authorizationHeader,
31 | 'Content-Type': 'application/x-www-form-urlencoded',
32 | 'User-Agent': 'token-introspection',
33 | }
34 | };
35 | var params = new URLSearchParams();
36 | params.append('token', token);
37 | params.append('token_type_hint', this.options.tokenTypeHint);
38 |
39 | var discoveryDocument = DiscoveryDocumentResponse.getInstance();
40 |
41 | let res;
42 | try {
43 | const request = Axios.create(fetchOption);
44 | res = await request.post(discoveryDocument.introspection_endpoint, params);
45 | }
46 | catch (err) {
47 | throw new IntrospectionError(this.options.authority, 'Remote introspection request failed');
48 | }
49 | if (res.status === 200 && res.data.active) {
50 | return res.data;
51 | }
52 |
53 | throw new IntrospectionError(this.options.authority, 'Token not active');
54 | }
55 |
56 | ///
57 | /// Determines whether uses a secure scheme according to the policy.
58 | ///
59 | private isSecureScheme(url: string) {
60 | if (this.options.requireHttpsMetadata) {
61 | var parsedUrl = url_parser.parse(url);
62 | return parsedUrl.protocol === "https:";
63 | }
64 |
65 | return true;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/well-known.json:
--------------------------------------------------------------------------------
1 | {
2 | "issuer": "https://localhost:5000",
3 | "jwks_uri": "https://localhost:5000/.well-known/openid-configuration/jwks",
4 | "authorization_endpoint": "https://localhost:5000/connect/authorize",
5 | "token_endpoint": "https://localhost:5000/connect/token",
6 | "userinfo_endpoint": "https://localhost:5000/connect/userinfo",
7 | "end_session_endpoint": "https://localhost:5000/connect/endsession",
8 | "check_session_iframe": "https://localhost:5000/connect/checksession",
9 | "revocation_endpoint": "https://localhost:5000/connect/revocation",
10 | "introspection_endpoint": "https://localhost:5000/connect/introspect",
11 | "device_authorization_endpoint": "https://localhost:5000/connect/deviceauthorization",
12 | "frontchannel_logout_supported": true,
13 | "frontchannel_logout_session_supported": true,
14 | "backchannel_logout_supported": true,
15 | "backchannel_logout_session_supported": true,
16 | "scopes_supported": [
17 | "is4-rights",
18 | "role",
19 | "username",
20 | "email",
21 | "profile",
22 | "openid",
23 | "jp_api.is4",
24 | "jp_api.user",
25 | "teste-api",
26 | "offline_access"
27 | ],
28 | "claims_supported": [
29 | "is4-rights",
30 | "role",
31 | "username",
32 | "email",
33 | "email_verified",
34 | "family_name",
35 | "given_name",
36 | "middle_name",
37 | "nickname",
38 | "preferred_username",
39 | "profile",
40 | "picture",
41 | "website",
42 | "gender",
43 | "birthdate",
44 | "zoneinfo",
45 | "locale",
46 | "updated_at",
47 | "name",
48 | "sub",
49 | "openid"
50 | ],
51 | "grant_types_supported": [
52 | "authorization_code",
53 | "client_credentials",
54 | "refresh_token",
55 | "implicit",
56 | "password",
57 | "urn:ietf:params:oauth:grant-type:device_code"
58 | ],
59 | "response_types_supported": [
60 | "code",
61 | "token",
62 | "id_token",
63 | "id_token token",
64 | "code id_token",
65 | "code token",
66 | "code id_token token"
67 | ],
68 | "response_modes_supported": [
69 | "form_post",
70 | "query",
71 | "fragment"
72 | ],
73 | "token_endpoint_auth_methods_supported": [
74 | "client_secret_basic",
75 | "client_secret_post"
76 | ],
77 | "id_token_signing_alg_values_supported": [
78 | "ES256"
79 | ],
80 | "subject_types_supported": [
81 | "public"
82 | ],
83 | "code_challenge_methods_supported": [
84 | "plain",
85 | "S256"
86 | ],
87 | "request_parameter_supported": true
88 | }
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # Node JS - OAuth 2.0 - Access Token Validation
2 |
3 | [](https://badge.fury.io/js/node-accesstoken-validation)
4 |
5 |
6 | Authentication handler for node that allows accepting both JWTs and reference tokens (Introspection).
7 |
8 |
9 | ## Install
10 |
11 | ```
12 | $ npm install --save node-accesstoken-validation
13 | ```
14 |
15 | ## JWT Usage
16 |
17 | Simply specify authority and API name (aka audience):
18 |
19 | ```typescript
20 |
21 | import { AccessTokenHandler } from 'node-accesstoken-validation';
22 |
23 |
24 | var at_validation = new AccessTokenHandler({
25 | authority: 'https://localhost:5000',
26 | apiName: "
27 | });
28 |
29 | at_validation.Handle('bearer eyJhbGciOiJFUzI1NiIsImtpZCI6IjlXVFR1Nm9nZzQyamR2WUpaTUZRYXciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODU5NDI5MjIsImV4cCI6MTU4NTk0NjUyMiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImpwX2FwaSIsImNsaWVudF9pZCI6IklTNC1BZG1pbiIsInN1YiI6IjA5MDRlNzVlLTQxM2QtNDg2MC05MzI5LWIyNTg3MjQ3MDY1YSIsImF1dGhfdGltZSI6MTU4NTcyMzY3NSwiaWRwIjoibG9jYWwiLCJpczQtcmlnaHRzIjoibWFuYWdlciIsInJvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZW1haWwiOiJiaGRlYnJpdG9AZ21haWwuY29tIiwidXNlcm5hbWUiOiJicnVubyIsInNjb3BlIjpbInJvbGUiLCJlbWFpbCIsInByb2ZpbGUiLCJvcGVuaWQiLCJqcF9hcGkuaXM0Il0sImFtciI6WyJwd2QiXX0.IXXE3P1lU5a_G4uMdVuilpej4E6inlV7ObOprszbyZbjnoS2gwOyegB3WSAjwsbTmzGM-T9_SgLhVP-lqJ94mg').then(console.log).catch(console.warn);
30 |
31 | ```
32 |
33 | ## Enable reference tokens
34 | Additionally specify the API secret for the introspection endpoint:
35 |
36 | ```typescript
37 |
38 | import { AccessTokenHandler } from 'node-accesstoken-validation';
39 |
40 | var at_validation = new AccessTokenHandler({
41 | authority: 'https://localhost:5000',
42 | apiName: "",
43 | apiSecret: ""
44 | });
45 |
46 | at_validation.Handle('bearer z5RuNoKkZQ1cAwstP7ZhHAV8NcmljulxPHzOvNuIRLQ').then(console.log).catch(console.warn);
47 | ```
48 |
49 | ## Scope validation
50 | In addition to API name checking, you can do more fine-grained scope checks.
51 |
52 | ```typescript
53 | at_validation.Handle('bearer ').then(claims => {
54 | if(claims["custom_claims"] != "custom_value")
55 | throw new Error();
56 |
57 | //...
58 | }).catch(console.warn):
59 | ```
60 |
61 | ## Cache response
62 |
63 | By default response stay cached by 5 minutes (300 sec). You can change or disable.
64 |
65 |
66 | ```typescript
67 |
68 | var at_validation = new AccessTokenHandler({
69 | authority: 'https://localhost:5000',
70 | apiName: "",
71 | apiSecret: "",
72 | enableCache: true,
73 | cacheDuration: 300
74 | });
75 |
76 | at_validation.Handle('bearer ').then(console.log).catch(console.warn);
77 | ```
78 |
79 | ## Restrict token type
80 |
81 | You can explicit disable JWT or Reference tokens.
82 |
83 |
84 | ```typescript
85 |
86 | var at_validation = new AccessTokenHandler({
87 | authority: 'https://localhost:5000',
88 | supportedTokens = SupportedTokens.Jwt
89 | });
90 |
91 | at_validation.Handle('bearer ').then(console.log).catch(console.warn);
92 | ```
93 |
94 | ## Audience
95 |
96 | By default it checks audience. If you are running through API Gateway and validating many requests disable it.
97 |
98 | ```typescript
99 |
100 | var at_validation = new AccessTokenHandler({
101 | authority: 'https://localhost:5000',
102 | apiName: ''
103 | checkAudience = false;
104 | });
105 |
106 | at_validation.Handle('bearer ').then(console.log).catch(console.warn);
107 | ```
108 |
109 | # License
110 |
111 | Node Access Token Validation is Open Source software and is released under the MIT license. This license allow the use of Node Access Token Validation in free and commercial applications and libraries without restrictions.
112 |
--------------------------------------------------------------------------------
/src/introspection/oidc-constants.ts:
--------------------------------------------------------------------------------
1 | export default class OidcConstants {
2 |
3 | public static Issuer = "issuer";
4 | // endpoints
5 | public static AuthorizationEndpoint = "authorization_endpoint";
6 | public static DeviceAuthorizationEndpoint = "device_authorization_endpoint";
7 | public static TokenEndpoint = "token_endpoint";
8 | public static UserInfoEndpoint = "userinfo_endpoint";
9 | public static IntrospectionEndpoint = "introspection_endpoint";
10 | public static RevocationEndpoint = "revocation_endpoint";
11 | public static DiscoveryEndpoint = ".well-known/openid-configuration";
12 | public static JwksUri = "jwks_uri";
13 | public static EndSessionEndpoint = "end_session_endpoint";
14 | public static CheckSessionIframe = "check_session_iframe";
15 | public static RegistrationEndpoint = "registration_endpoint";
16 | public static MtlsEndpointAliases = "mtls_endpoint_aliases";
17 | // common capabilities
18 | public static FrontChannelLogoutSupported = "frontchannel_logout_supported";
19 | public static FrontChannelLogoutSessionSupported = "frontchannel_logout_session_supported";
20 | public static BackChannelLogoutSupported = "backchannel_logout_supported";
21 | public static BackChannelLogoutSessionSupported = "backchannel_logout_session_supported";
22 | public static GrantTypesSupported = "grant_types_supported";
23 | public static CodeChallengeMethodsSupported = "code_challenge_methods_supported";
24 | public static ScopesSupported = "scopes_supported";
25 | public static SubjectTypesSupported = "subject_types_supported";
26 | public static ResponseModesSupported = "response_modes_supported";
27 | public static ResponseTypesSupported = "response_types_supported";
28 | public static ClaimsSupported = "claims_supported";
29 | public static TokenEndpointAuthenticationMethodsSupported = "token_endpoint_auth_methods_supported";
30 | // more capabilities
31 | public static ClaimsLocalesSupported = "claims_locales_supported";
32 | public static ClaimsParameterSupported = "claims_parameter_supported";
33 | public static ClaimTypesSupported = "claim_types_supported";
34 | public static DisplayValuesSupported = "display_values_supported";
35 | public static AcrValuesSupported = "acr_values_supported";
36 | public static IdTokenEncryptionAlgorithmsSupported = "id_token_encryption_alg_values_supported";
37 | public static IdTokenEncryptionEncValuesSupported = "id_token_encryption_enc_values_supported";
38 | public static IdTokenSigningAlgorithmsSupported = "id_token_signing_alg_values_supported";
39 | public static OpPolicyUri = "op_policy_uri";
40 | public static OpTosUri = "op_tos_uri";
41 | public static RequestObjectEncryptionAlgorithmsSupported = "request_object_encryption_alg_values_supported";
42 | public static RequestObjectEncryptionEncValuesSupported = "request_object_encryption_enc_values_supported";
43 | public static RequestObjectSigningAlgorithmsSupported = "request_object_signing_alg_values_supported";
44 | public static RequestParameterSupported = "request_parameter_supported";
45 | public static RequestUriParameterSupported = "request_uri_parameter_supported";
46 | public static RequireRequestUriRegistration = "require_request_uri_registration";
47 | public static ServiceDocumentation = "service_documentation";
48 | public static TokenEndpointAuthSigningAlgorithmsSupported = "token_endpoint_auth_signing_alg_values_supported";
49 | public static UILocalesSupported = "ui_locales_supported";
50 | public static UserInfoEncryptionAlgorithmsSupported = "userinfo_encryption_alg_values_supported";
51 | public static UserInfoEncryptionEncValuesSupported = "userinfo_encryption_enc_values_supported";
52 | public static UserInfoSigningAlgorithmsSupported = "userinfo_signing_alg_values_supported";
53 | public static TlsClientCertificateBoundAccessTokens = "tls_client_certificate_bound_access_tokens";
54 |
55 | }
--------------------------------------------------------------------------------
/src/access-token-handler.ts:
--------------------------------------------------------------------------------
1 | import NodeCache from 'node-cache';
2 |
3 | import AuthenticationOptions from './authentication-options';
4 | import IAuthenticationOptions from './authentication-options';
5 | import InvalidTokenError from './errors/invalid-token-error';
6 | import OAuth2IntrospectionHandler from './introspection/oauth2-introspection-handler';
7 | import OAuth2IntrospectionOptions from './introspection/oauth2-introspection-options';
8 | import TokenTypes from './introspection/token-types';
9 | import JwtBearerOptions from './jwt-token/jwt-bearer-options';
10 | import JwtTokenHandler from './jwt-token/jwt-token-handler';
11 | import { SupportedTokens } from './models/supported-tokens.model';
12 | import TokenRetriever from './token.retriever';
13 |
14 | const myCache = new NodeCache({ useClones: false });
15 |
16 | export default class AccessTokenHandler {
17 |
18 | private tokenIntrospectionOptions: OAuth2IntrospectionOptions;
19 | private introspectionHandler: OAuth2IntrospectionHandler;
20 | private jwtTokenHandler: JwtTokenHandler;
21 | private options: AuthenticationOptions;
22 | constructor(
23 | {
24 | authority,
25 | apiName = "",
26 | apiSecret = "",
27 | requireHttpsMetadata = true,
28 | supportedTokens = SupportedTokens.Both,
29 | tokenTypeHint = TokenTypes.AccessToken,
30 | enableCache = true,
31 | cacheDuration = 300,
32 | checkAudience = true
33 | }: any) {
34 |
35 | this.options = new AuthenticationOptions(authority, apiName, apiSecret, supportedTokens, tokenTypeHint, enableCache, cacheDuration);
36 | this.jwtTokenHandler = new JwtTokenHandler(new JwtBearerOptions(authority, requireHttpsMetadata, apiName, checkAudience));
37 |
38 | if (apiSecret === "") {
39 | this.options.supportedTokens = SupportedTokens.Jwt;
40 | return;
41 | }
42 | if (apiName === "")
43 | throw new Error('apiName must be configured if ApiSecret is set.');
44 |
45 | this.tokenIntrospectionOptions = new OAuth2IntrospectionOptions(authority, apiName, apiSecret, requireHttpsMetadata, tokenTypeHint, enableCache, cacheDuration);
46 | this.introspectionHandler = new OAuth2IntrospectionHandler(this.tokenIntrospectionOptions);
47 | }
48 |
49 | /**
50 | * Method to handle bearer token. If success, return the payload.
51 | */
52 | public async Handle(authorization: string, scheme: string = "bearer") {
53 | const token = TokenRetriever.Retrieve(authorization, scheme);
54 |
55 | if (token == null) {
56 | throw new InvalidTokenError(this.options.authority, "Token not found", token);
57 | }
58 |
59 | const tokenType = TokenRetriever.FindType(token);
60 | if (!this.supportThisToken(tokenType))
61 | throw new InvalidTokenError(this.options.authority, "Not supported token type", token);
62 |
63 | if (this.options.enableCache) {
64 | var responseCached = myCache.get(token);
65 | if (responseCached != undefined)
66 | return responseCached;
67 | }
68 |
69 |
70 | if (tokenType == SupportedTokens.Reference) {
71 | return this.checkReferenceToken(token);
72 | }
73 |
74 | if (tokenType == SupportedTokens.Jwt) {
75 | return this.checkJwtToken(token);
76 | }
77 | }
78 |
79 | public async checkReferenceToken(token: string) {
80 | var response = await this.introspectionHandler.Introspect(token);
81 | this.saveCache(token, response);
82 |
83 | return response;
84 | }
85 |
86 | public checkJwtToken(token: string) {
87 | var response = this.jwtTokenHandler.Handle(token);
88 | this.saveCache(token, response);
89 |
90 | return response;
91 | }
92 |
93 | private saveCache(token: string, response: any) {
94 | if (this.options.enableCache)
95 | myCache.set(token, response, this.options.cacheDuration);
96 | }
97 |
98 | private supportThisToken(tokenType: SupportedTokens) {
99 | if (this.options.supportedTokens == SupportedTokens.Both)
100 | return true;
101 |
102 | return tokenType == this.options.supportedTokens;
103 | }
104 | }
--------------------------------------------------------------------------------
/test/access-token-handler.tests.ts:
--------------------------------------------------------------------------------
1 | import { equal } from 'assert';
2 | import chai, { expect } from 'chai';
3 | import fs from 'fs';
4 | import { JWK, JWKS, JWT } from 'jose';
5 | import { describe } from 'mocha';
6 | import nock from 'nock';
7 | import path from 'path';
8 | import sinon from 'sinon';
9 |
10 | import AccessTokenHandler from '../src/access-token-handler';
11 | import { SupportedTokens } from '../src/models/supported-tokens.model';
12 |
13 | const jws = "bearer eyJhbGciOiJFUzI1NiIsImtpZCI6IjlXVFR1Nm9nZzQyamR2WUpaTUZRYXciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODU5NTA3MzMsImV4cCI6MTU4NTk1NDMzMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImpwX2FwaSIsImNsaWVudF9pZCI6IklTNC1BZG1pbiIsInN1YiI6IjA5MDRlNzVlLTQxM2QtNDg2MC05MzI5LWIyNTg3MjQ3MDY1YSIsImF1dGhfdGltZSI6MTU4NTcyMzY3NSwiaWRwIjoibG9jYWwiLCJpczQtcmlnaHRzIjoibWFuYWdlciIsInJvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZW1haWwiOiJiaGRlYnJpdG9AZ21haWwuY29tIiwidXNlcm5hbWUiOiJicnVubyIsInNjb3BlIjpbInJvbGUiLCJlbWFpbCIsInByb2ZpbGUiLCJvcGVuaWQiLCJqcF9hcGkuaXM0Il0sImFtciI6WyJwd2QiXX0.S8fc_c1V887By0gJoYk0Neoeh1OvbHJdNiAzNOMhWaSVxAymdahRTsCCf-43XHiDzP4cyT9K6CdGZbU_z7l7UQ";
14 | const assert = chai.assert;
15 | const refToken = 'bearer zSh5whhIi4vTLkG_A-am3RzZeZhJAhPHto-mFCAigp0';
16 |
17 | afterEach(() => {
18 | sinon.restore();
19 | });
20 |
21 | const authority = 'https://localhost:5000';
22 | const jwks_uri = '/.well-known/openid-configuration/jwks';
23 | const introspect = '/connect/introspect';
24 | const discoveryDocument = '/.well-known/openid-configuration';
25 |
26 | beforeEach(function () {
27 | nock.cleanAll();
28 | nock(authority)
29 | .get(jwks_uri)
30 | .replyWithFile(200, path.join(__dirname, 'jwks.json'));
31 |
32 | nock(authority)
33 | .get(discoveryDocument)
34 | .replyWithFile(200, path.join(__dirname, 'well-known.json'));
35 |
36 | nock(authority)
37 | .post(introspect)
38 | .replyWithFile(200, path.join(__dirname, 'introspection-response.json'));
39 | });
40 |
41 | /// I use this tests for development only. I'm quite new at node. In future I'll provide mock
42 | describe('Reference Token', async () => {
43 |
44 | it('Should accept reference token', async () => {
45 |
46 | let atHandler = new AccessTokenHandler({
47 | authority: authority,
48 | apiName: "jp_api",
49 | apiSecret: "teste",
50 | requireHttpsMetadata: true
51 | });
52 |
53 | let token = await atHandler.Handle(refToken);
54 |
55 | expect(token.active).be.true;
56 | });
57 |
58 | it('Should not accept reference token', async () => {
59 | let atHandler = new AccessTokenHandler({
60 | authority: authority,
61 | apiName: "jp_api",
62 | apiSecret: "teste",
63 | requireHttpsMetadata: true,
64 | SupportedTokens: SupportedTokens.Jwt
65 | });
66 |
67 | try {
68 | await atHandler.Handle(refToken);
69 | } catch (error) {
70 | assert.equal(error.description, "Not supported token type");
71 | }
72 | });
73 |
74 | });
75 |
76 | describe('JWT Bearer Token', () => {
77 |
78 | it('Should validate Bearer Token', async () => {
79 |
80 | var test: any = JWT.decode(jws.replace('bearer ', ''));
81 | sinon.stub(JWT, 'verify').returns(test);
82 |
83 | let atHandler = new AccessTokenHandler({
84 | authority: authority,
85 | apiName: "jp_api",
86 | apiSecret: "teste",
87 | requireHttpsMetadata: true
88 | });
89 | let token = await atHandler.Handle(jws);
90 |
91 | expect(token).be.not.null;
92 | sinon.verify();
93 | sinon.restore();
94 | });
95 |
96 | it('Should not accept JWT Token', async () => {
97 |
98 | let atHandler = new AccessTokenHandler({
99 | authority: authority,
100 | apiName: "jp_api",
101 | supportedTokens: SupportedTokens.Reference
102 | });
103 | let token = await atHandler.Handle(jws);
104 |
105 | expect(token).be.not.null;
106 | sinon.verify();
107 | sinon.restore();
108 | });
109 |
110 | it('Should not accept http when require https', async () => {
111 |
112 | var test: any = JWT.decode(jws.replace('bearer ', ''));
113 | sinon.stub(JWT, 'verify').returns(test);
114 |
115 | try {
116 | let atHandler = new AccessTokenHandler({
117 | authority: authority.replace('https', 'http'),
118 | apiName: "jp_api",
119 | requireHttpsMetadata: true
120 | });
121 | } catch (error) {
122 | equal(error.description, 'HTTPS required');
123 | }
124 |
125 | sinon.verify();
126 | sinon.restore();
127 | });
128 |
129 | it('Should get token from Cache', async () => {
130 | const newJws = 'bearer eyJhbGciOiJFUzI1NiIsImtpZCI6IjlXVFR1Nm9nZzQyamR2WUpaTUZRYXciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODYwNDE3MzAsImV4cCI6MTU4NjA0NTMzMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImpwX2FwaSIsImNsaWVudF9pZCI6IklTNC1BZG1pbiIsInN1YiI6IjA5MDRlNzVlLTQxM2QtNDg2MC05MzI5LWIyNTg3MjQ3MDY1YSIsImF1dGhfdGltZSI6MTU4NTcyMzY3NSwiaWRwIjoibG9jYWwiLCJpczQtcmlnaHRzIjoibWFuYWdlciIsInJvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZW1haWwiOiJiaGRlYnJpdG9AZ21haWwuY29tIiwidXNlcm5hbWUiOiJicnVubyIsInNjb3BlIjpbInJvbGUiLCJlbWFpbCIsInByb2ZpbGUiLCJvcGVuaWQiLCJqcF9hcGkuaXM0Il0sImFtciI6WyJwd2QiXX0.DVRwJ2Q81pfkVg2QBHv9S4A1C_gpTnroSA7FJVX70h_ARZkhpLviFS6lrXDEmUZz_0GIk_iibZkOXGFbO7MX7w';
131 | var test: any = JWT.decode(newJws.replace('bearer ', ''));
132 | sinon.stub(JWT, 'verify').returns(test);
133 |
134 |
135 | let atHandler = new AccessTokenHandler({
136 | authority: authority,
137 | apiName: "jp_api",
138 | requireHttpsMetadata: true
139 | });
140 |
141 | let jwtHandle = sinon.spy(atHandler, 'checkJwtToken');
142 | await atHandler.Handle(newJws);
143 | await atHandler.Handle(newJws);
144 |
145 | assert(jwtHandle.calledOnce);
146 | sinon.verify();
147 | sinon.restore();
148 | });
149 |
150 |
151 | it('Should accept audience as array', async () => {
152 | const newJws = 'bearer eyJhbGciOiJFUzI1NiIsImtpZCI6Imx2b2wtSi11S1p4X2hUTk5PTEctNnciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODYxOTI4MTcsImV4cCI6MTU4NjE5NjQxNywiaXNzIjoiaHR0cHM6Ly9zc28uaG1sLmRpZ2l0YWwtc2VndXJvc3VuaW1lZC5jb20iLCJhdWQiOlsiYXBpLXNhdWRlIiwiYXBpLXZpZGEiLCJhcGktb2RvbnRvIiwiYXBpLXByZXYiLCJhcGktcmUiLCJhcGktcG9ydGFscGoiXSwiY2xpZW50X2lkIjoiY2xpZW50LXRlc3RlIiwic3ViIjoiMTdkNWRlZmQtMGFmOC00NDBjLThmYzQtZjc1YjI4MGJkYThjIiwiYXV0aF90aW1lIjoxNTg1MTU3NDU0LCJpZHAiOiJsb2NhbCIsInNhdWRlIjpbInBqLXNhdWRlLXBhZHJhbyIsInBqLXNhdWRlLW1vdmkiXSwicG9ydGFscGoiOiJ0ZXN0ZSIsIm5hbWUiOiJkYW5pZWwuYW1hcmFsIiwic29jaWFsX251bWJlciI6IjQwMDE4NDg0MDAwMTEwIiwic2NvcGUiOlsicHJvZmlsZSIsIm9wZW5pZCIsInNhdWRlLXBhZHJhbyIsInNhdWRlLW1vdmkiLCJzYXVkZS1zZWJyYWUiLCJzYXVkZS12aXZvIiwidmlkYS1wYWRyYW8iLCJvZG9udG8tcGFkcmFvIiwicHJldi1wYWRyYW8iLCJyZS1wYWRyYW8iLCJwb3J0YWxwai1wYWRyYW8iXSwiYW1yIjpbInB3ZCJdfQ.dZIDQ33-8OQFEapnU46KGjV1OnIbdt6dogkmBf3EW0PXi1TTD6E1WsL7Ox7ExqboiVANjBwV5VCq9HE1vLxCvg';
153 | var test: any = JWT.decode(newJws.replace('bearer ', ''));
154 | sinon.stub(JWT, 'verify').returns(test);
155 |
156 |
157 | let atHandler = new AccessTokenHandler({
158 | authority: authority,
159 | apiName: "api-saude",
160 | requireHttpsMetadata: true
161 | });
162 |
163 | let jwtHandle = sinon.spy(atHandler, 'checkJwtToken');
164 | await atHandler.Handle(newJws);
165 | await atHandler.Handle(newJws);
166 |
167 | assert(jwtHandle.calledOnce);
168 | sinon.verify();
169 | sinon.restore();
170 | });
171 | });
172 |
--------------------------------------------------------------------------------