Skip to content

Commit ee1ddad

Browse files
committed
feat: add new token manager for ICP4D
Introduce a constructor parameter, `authentication_type`, for specifying the authentication pattern. Required for using ICP4D
1 parent 257dc92 commit ee1ddad

9 files changed

+841
-1029
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ doc/
99
.env
1010
.eslintcache
1111
lib/*.js
12+
auth/*.js
1213
iam-token-manager/*.js
1314
index.js
1415
.nyc_output

auth/icp-token-manager.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Copyright 2019 IBM Corp. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import extend = require('extend');
18+
import { sendRequest } from '../lib/requestwrapper';
19+
import { JwtTokenManager } from './jwt-token-manager';
20+
21+
export type Options = {
22+
url: string;
23+
accessToken?: string;
24+
username?: string;
25+
password?: string;
26+
}
27+
28+
// this interface is a representation of the response
29+
// object from the ICP service
30+
export interface IcpTokenData {
31+
username: string;
32+
role: string;
33+
permissions: string[];
34+
sub: string;
35+
iss: string;
36+
aud: string;
37+
uid: string;
38+
_messageCode_: string;
39+
message: string;
40+
accessToken: string;
41+
}
42+
43+
export class IcpTokenManagerV1 extends JwtTokenManager {
44+
private username: string;
45+
private password: string;
46+
47+
/**
48+
* ICP Token Manager Service
49+
*
50+
* Retreives, stores, and refreshes ICP tokens.
51+
*
52+
* @param {Object} options
53+
* @param {String} options.icpApikey
54+
* @param {String} options.icpAccessToken
55+
* @param {String} options.url - url of the icp api to retrieve tokens from
56+
* @constructor
57+
*/
58+
constructor(options: Options) {
59+
super(options);
60+
61+
this.tokenName = 'accessToken';
62+
63+
if (this.url) {
64+
this.url = this.url + '/v1/preauth/validateAuth';
65+
} else {
66+
// this is required
67+
console.error('`url` is a required parameter for the ICP token manager.');
68+
}
69+
if (options.username) {
70+
this.username = options.username;
71+
}
72+
if (options.password) {
73+
this.password = options.password;
74+
}
75+
// username and password are required too, unless there's access token
76+
}
77+
78+
/**
79+
* Request an ICP token using a basic auth header.
80+
*
81+
* @param {Function} cb - The callback that handles the response.
82+
* @returns {void}
83+
*/
84+
protected requestToken(cb: Function): void {
85+
const parameters = {
86+
options: {
87+
url: this.url,
88+
method: 'GET',
89+
headers: {
90+
Authorization:
91+
this.computeBasicAuthHeader(this.username, this.password),
92+
},
93+
}
94+
};
95+
sendRequest(parameters, cb);
96+
}
97+
}

auth/jwt-token-manager.ts

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Copyright 2015 IBM Corp. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import extend = require('extend');
18+
import jwt = require('jsonwebtoken');
19+
import { sendRequest } from '../lib/requestwrapper';
20+
21+
function getCurrentTime(): number {
22+
return Math.floor(Date.now() / 1000);
23+
}
24+
25+
export type Options = {
26+
accessToken?: string;
27+
url?: string;
28+
}
29+
30+
export class JwtTokenManager {
31+
protected url: string;
32+
protected tokenName: string;
33+
protected userAccessToken: string;
34+
private tokenInfo: any;
35+
private timeToLive: number;
36+
private expireTime: number;
37+
38+
/**
39+
* Token Manager Service
40+
*
41+
* Retreives, stores, and refreshes JSON web tokens.
42+
*
43+
* @param {Object} options
44+
* @param {String} options.url - url of the api to retrieve tokens from
45+
* @param {String} options.accessToken
46+
* @constructor
47+
*/
48+
constructor(options: Options) {
49+
this.tokenInfo = {};
50+
51+
this.tokenName = 'access_token';
52+
53+
if (options.url) {
54+
this.url = options.url;
55+
}
56+
if (options.accessToken) {
57+
this.userAccessToken = options.accessToken;
58+
}
59+
}
60+
61+
/**
62+
* This function sends an access token back through a callback. The source of the token
63+
* is determined by the following logic:
64+
* 1. If user provides their own managed access token, assume it is valid and send it
65+
* 2. a) If this class is managing tokens and does not yet have one, make a request for one
66+
* b) If this class is managing tokens and the token has expired, request a new one
67+
* 3. If this class is managing tokens and has a valid token stored, send it
68+
*
69+
* @param {Function} cb - callback function that the token will be passed to
70+
*/
71+
public getToken(cb: Function) {
72+
if (this.userAccessToken) {
73+
// 1. use user-managed token
74+
return cb(null, this.userAccessToken);
75+
} else if (!this.tokenInfo[this.tokenName] || this.isTokenExpired()) {
76+
// 2. request a new token
77+
this.requestToken((err, tokenResponse) => {
78+
this.saveTokenInfo(tokenResponse);
79+
return cb(err, this.tokenInfo[this.tokenName]);
80+
});
81+
} else {
82+
// 3. use valid, sdk-managed token
83+
return cb(null, this.tokenInfo[this.tokenName]);
84+
}
85+
}
86+
87+
/**
88+
* Set a self-managed access token.
89+
* The access token should be valid and not yet expired.
90+
*
91+
* By using this method, you accept responsibility for managing the
92+
* access token yourself. You must set a new access token before this
93+
* one expires. Failing to do so will result in authentication errors
94+
* after this token expires.
95+
*
96+
* @param {string} accessToken - A valid, non-expired access token
97+
* @returns {void}
98+
*/
99+
public setAccessToken(accessToken: string): void {
100+
this.userAccessToken = accessToken;
101+
}
102+
103+
/**
104+
* Request a JWT using an API key.
105+
*
106+
* @param {Function} cb - The callback that handles the response.
107+
* @returns {void}
108+
*/
109+
protected requestToken(cb: Function): void {
110+
cb(null, 'token');
111+
}
112+
113+
/**
114+
* Compute and return a Basic Authorization header from a username and password.
115+
*
116+
* @param {string} username - The username or client id
117+
* @param {string} password - The password or client secret
118+
* @returns {string}
119+
*/
120+
protected computeBasicAuthHeader(username, password): string {
121+
const encodedCreds = Buffer.from(`${username}:${password}`).toString('base64');
122+
return `Basic ${encodedCreds}`;
123+
}
124+
125+
/**
126+
* Check if currently stored token is expired.
127+
*
128+
* Using a buffer to prevent the edge case of the
129+
* token expiring before the request could be made.
130+
*
131+
* The buffer will be a fraction of the total TTL. Using 80%.
132+
*
133+
* @private
134+
* @returns {boolean}
135+
*/
136+
private isTokenExpired(): boolean {
137+
const { timeToLive, expireTime } = this;
138+
139+
if (!timeToLive || !expireTime) {
140+
return true;
141+
}
142+
143+
const fractionOfTtl = 0.8;
144+
const currentTime = getCurrentTime();
145+
const refreshTime = expireTime - (timeToLive * (1.0 - fractionOfTtl));
146+
return refreshTime < currentTime;
147+
}
148+
149+
/**
150+
* Decode the access token and save the response from the JWT service to the object's state.
151+
*
152+
* @param tokenResponse - Response object from JWT service request
153+
* @private
154+
* @returns {void}
155+
*/
156+
private saveTokenInfo(tokenResponse): void {
157+
const accessToken = tokenResponse[this.tokenName];
158+
159+
// the time of expiration is found by decoding the JWT access token
160+
const decodedResponse = jwt.decode(accessToken);
161+
const { exp, iat } = decodedResponse;
162+
163+
// exp is the time of expire and iat is the time of token retrieval
164+
this.timeToLive = exp - iat;
165+
this.expireTime = exp;
166+
167+
this.tokenInfo = extend({}, tokenResponse);
168+
}
169+
}

0 commit comments

Comments
 (0)