• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ibm-cloud-security / appid-clientsdk-js / 8018892406

23 Feb 2024 12:04PM UTC coverage: 93.066%. Remained the same
8018892406

push

github

web-flow
Merge pull request #99 from ibm-cloud-security/abod-akhras-patch-2

Update dry-publish.js to use Node v20

78 of 94 branches covered (82.98%)

Branch coverage included in aggregate %.

177 of 180 relevant lines covered (98.33%)

4.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.15
/src/index.js
1
const Utils = require('./utils');
1✔
2
const RequestHandler = require('./RequestHandler');
1✔
3
const PopupController = require('./PopupController');
1✔
4
const IFrameController = require('./IFrameController');
1✔
5
const OpenIdConfigurationResource = require('./OpenIDConfigurationResource')
1✔
6
const TokenValidator = require('./TokenValidator');
1✔
7
const constants = require('./constants');
1✔
8
const AppIDError = require('./errors/AppIDError');
1✔
9
const jsrsasign = require('jsrsasign');
1✔
10

11
/**
12
 * This class provides functions to support authentication.
13
 */
14
class AppID {
15
        /**
16
         * This creates an instance of AppID. Once created, call init() before attempting to sign in.
17
         * @example
18
         * const appID = new AppID();
19
         */
20
        constructor(
21
                {
×
22
                        popup = new PopupController(),
×
23
                        iframe = new IFrameController(),
×
24
                        openIdConfigResource = new OpenIdConfigurationResource(),
×
25
                        utils,
26
                        requestHandler = new RequestHandler(),
×
27
                        tokenValidator = new TokenValidator(),
12✔
28
                        w = window,
×
29
                        url = URL
×
30
                } = {}) {
31

32
                this.popup = popup;
16✔
33
                this.iframe = iframe;
16✔
34
                this.openIdConfigResource = openIdConfigResource;
16✔
35
                this.URL = url;
16✔
36
                this.utils = utils;
16✔
37
                this.tokenValidator = tokenValidator;
16✔
38
                if (!utils) {
16!
39
                        this.utils = new Utils({
×
40
                                openIdConfigResource: this.openIdConfigResource,
41
                                url: this.URL,
42
                                popup: this.popup,
43
                                jsrsasign
44
                        });
45
                }
46
                this.request = requestHandler.request;
16✔
47
                this.window = w;
16✔
48
                this.initialized = false;
16✔
49
        }
50

51
        /**
52
         * Initialize AppID. Call this function before attempting to sign in. You must wait for the promise to resolve.
53
         * @param {Object} options
54
         * @param {string} options.clientId - The clientId from the singlepageapp application credentials.
55
         * @param {string} options.discoveryEndpoint - The discoveryEndpoint from the singlepageapp application credentials.
56
         * @param {Object} [options.popup] - The popup configuration.
57
         * @param {Number} options.popup.height - The popup height.
58
         * @param {Number} options.popup.width - The popup width.
59
         * @returns {Promise<void>}
60
         * @throws {AppIDError} For missing required params.
61
         * @throws {RequestError} Any errors during a HTTP request.
62
         * @example
63
         * await appID.init({
64
         *         clientId: '<SPA_CLIENT_ID>',
65
         *         discoveryEndpoint: '<WELL_KNOWN_ENDPOINT>'
66
         * });
67
         *
68
         */
69
        async init({clientId, discoveryEndpoint, popup = {height: window.screen.height * .80, width: 400}}) {
×
70
                if (!clientId) {
18✔
71
                        throw new AppIDError(constants.MISSING_CLIENT_ID);
1✔
72
                }
73
                try {
17✔
74
                        new this.URL(discoveryEndpoint)
17✔
75
                } catch (e) {
76
                        throw new AppIDError(constants.INVALID_DISCOVERY_ENDPOINT);
1✔
77
                }
78

79
                await this.openIdConfigResource.init({discoveryEndpoint, requestHandler: this.request});
16✔
80
                this.popup.init(popup);
16✔
81
                this.clientId = clientId;
16✔
82
                this.initialized = true;
16✔
83
        }
84

85
        /**
86
         * @typedef {Object} Tokens
87
         * @property {string} accessToken A JWT.
88
         * @property {Object} accessTokenPayload The decoded JWT.
89
         * @property {string} idToken A JWT.
90
         * @property {Object} idTokenPayload The decoded JWT.
91
         */
92

93
        /**
94
         * This will open a sign in widget in a popup which will prompt the user to enter their credentials.
95
         * After a successful sign in, the popup will close and tokens are returned.
96
         * @returns {Promise<Tokens>} The tokens of the authenticated user.
97
         * @throws {PopupError} "Popup closed" - The user closed the popup before authentication was completed.
98
         * @throws {TokenError} Any token validation error.
99
         * @throws {OAuthError} Any errors from the server according to the [OAuth spec]{@link https://tools.ietf.org/html/rfc6749#section-4.1.2.1}. e.g. {error: 'server_error', description: ''}
100
         * @throws {RequestError} Any errors during a HTTP request.
101
         * @example
102
         * const {accessToken, accessTokenPayload, idToken, idTokenPayload} = await appID.signin();
103
         */
104
        async signin() {
105
                this._validateInitalize();
6✔
106
                const endpoint = this.openIdConfigResource.getAuthorizationEndpoint();
5✔
107
                let origin = this.window.location.origin;
5✔
108
                if (!origin) {
5✔
109
                        origin = this.window.location.protocol + "//" + this.window.location.hostname + (this.window.location.port ? ':' + this.window.location.port : '');
2✔
110
                }
111
                return this.utils.performOAuthFlowAndGetTokens({
5✔
112
                        origin,
113
                        endpoint,
114
                        clientId: this.clientId
115
                });
116
        }
117

118
        /**
119
         * Silent sign in allows you to automatically obtain new tokens for a user without the user having to re-authenticate using a popup.
120
         * This will attempt to authenticate the user in a hidden iframe.
121
         * You will need to [enable Cloud Directory SSO]{@link https://cloud.ibm.com/docs/services/appid?topic=appid-single-page#spa-silent-login}.
122
         * Sign in will be successful only if the user has previously signed in using Cloud Directory and their session is not expired.
123
         * @returns {Promise<Tokens>} The tokens of the authenticated user.
124
         * @throws {OAuthError} Any errors from the server according to the [OAuth spec]{@link https://tools.ietf.org/html/rfc6749#section-4.1.2.1}. e.g. {error: 'access_denied', description: 'User not signed in'}
125
         * @throws {IFrameError} "Silent sign-in timed out" - The iframe will close after 5 seconds if authentication could not be completed.
126
         * @throws {TokenError} Any token validation error.
127
         * @throws {RequestError} Any errors during a HTTP request.
128
         * @example
129
         * const {accessToken, accessTokenPayload, idToken, idTokenPayload} = await appID.silentSignin();
130
         */
131
        async silentSignin() {
132
                this._validateInitalize();
4✔
133
                const endpoint = this.openIdConfigResource.getAuthorizationEndpoint();
4✔
134
                const {codeVerifier, nonce, state, url} = this.utils.getAuthParamsAndUrl({
4✔
135
                        clientId: this.clientId,
136
                        origin: this.window.origin,
137
                        prompt: constants.PROMPT,
138
                        endpoint
139
                });
140

141
                this.iframe.open(url);
4✔
142

143
                let message;
144
                try {
4✔
145
                        message = await this.iframe.waitForMessage({messageType: 'authorization_response'});
4✔
146
                } finally {
147
                        this.iframe.remove();
4✔
148
                }
149
                this.utils.verifyMessage({message, state});
4✔
150
                let authCode = message.data.code;
4✔
151

152
                return await this.utils.retrieveTokens({
4✔
153
                        clientId: this.clientId,
154
                        authCode,
155
                        codeVerifier,
156
                        nonce,
157
                        openId: this.openIdConfigResource,
158
                        windowOrigin: this.window.origin
159
                });
160
        }
161

162
        /**
163
         * This method will make a GET request to the [user info endpoint]{@link https://us-south.appid.cloud.ibm.com/swagger-ui/#/Authorization%2520Server%2520-%2520Authorization%2520Server%2520V4/oauth-server.userInfo} using the access token of the authenticated user.
164
         * @param {string} accessToken The App ID access token of the user.
165
         * @returns {Promise} The user information for the authenticated user. Example: {sub: '', email: ''}
166
         * @throws {AppIDError} "Access token must be a string" Invalid access token.
167
         * @throws {RequestError} Any errors during a HTTP request.
168
         */
169
        async getUserInfo(accessToken) {
170
                this._validateInitalize();
2✔
171
                if (typeof accessToken !== 'string') {
2✔
172
                        throw new AppIDError(constants.INVALID_ACCESS_TOKEN);
1✔
173
                }
174

175
                return await this.request(this.openIdConfigResource.getUserInfoEndpoint(), {
1✔
176
                        headers: {
177
                                'Authorization': 'Bearer ' + accessToken
178
                        }
179
                });
180
        }
181

182
        /**
183
         * This method will open a popup to the change password widget for Cloud Directory users.
184
         * You must enable users to manage their account from your app in Cloud Directory settings.
185
         * @param {string} idToken A JWT.
186
         * @returns {Promise<Tokens>} The tokens of the authenticated user.
187
         * @throws {AppIDError} "Expect id token payload object to have identities field"
188
         * @throws {AppIDError} "Must be a Cloud Directory user"
189
         * @throws {AppIDError} "Missing id token string"
190
         * @example
191
         * let tokens = await appID.changePassword(idToken);
192
         */
193
        async changePassword(idToken) {
194
                this._validateInitalize();
4✔
195

196
                if (!idToken || typeof idToken !== 'string') {
4✔
197
                        throw new AppIDError(constants.MISSING_ID_TOKEN);
1✔
198
                }
199

200
                let userId;
201
                const publicKeys = await this.openIdConfigResource.getPublicKeys();
3✔
202
                let decodedToken = this.tokenValidator.decodeAndValidate({
3✔
203
                        token: idToken,
204
                        publicKeys,
205
                        issuer: this.openIdConfigResource.getIssuer(),
206
                        clientId: this.clientId
207
                });
208

209
                if (decodedToken.identities && decodedToken.identities[0] && decodedToken.identities[0].id) {
3✔
210
                        if (decodedToken.identities[0].provider !== 'cloud_directory') {
2✔
211
                                throw new AppIDError(constants.NOT_CD_USER);
1✔
212
                        }
213
                        userId = decodedToken.identities[0].id;
1✔
214
                } else {
215
                        throw new AppIDError(constants.INVALID_ID_TOKEN);
1✔
216
                }
217

218
                const endpoint = this.openIdConfigResource.getIssuer() + constants.CHANGE_PASSWORD;
1✔
219
                return await this.utils.performOAuthFlowAndGetTokens({
1✔
220
                        userId,
221
                        origin: this.window.origin,
222
                        clientId: this.clientId,
223
                        endpoint
224
                });
225
        }
226

227
        /**
228
         * This method will open a popup to the change details widget for Cloud Directory users.
229
         * You must enable users to manage their account from your app in Cloud Directory settings.
230
         * @param {Object} tokens App ID tokens
231
         * @returns {Promise<Tokens>}
232
         * @throws {AppIDError} "Missing id token string"
233
         * @throws {AppIDError} "Missing access token string"
234
         * @throws {AppIDError} "Missing tokens object"
235
         * @example
236
         * let tokens = {accessToken, idToken}
237
         * let newTokens = await appID.changeDetails(tokens);
238
         */
239
        async changeDetails({accessToken, idToken}) {
240
                this._validateInitalize();
5✔
241

242
                if (!accessToken && typeof accessToken !== 'string') {
5✔
243
                        throw new AppIDError(constants.MISSING_ACCESS_TOKEN);
1✔
244
                }
245

246
                if (!idToken && typeof idToken !== 'string') {
4✔
247
                        throw new AppIDError(constants.MISSING_ID_TOKEN);
2✔
248
                }
249

250
                const generateCodeUrl = this.openIdConfigResource.getIssuer() + constants.GENERATE_CODE;
2✔
251
                const changeDetailsCode = await this.request(generateCodeUrl, {
2✔
252
                        headers: {
253
                                'Authorization': 'Bearer ' + accessToken + ' ' + idToken
254
                        }
255
                });
256
                const endpoint = this.openIdConfigResource.getIssuer() + constants.CHANGE_DETAILS;
2✔
257

258
                return this.utils.performOAuthFlowAndGetTokens({
2✔
259
                        origin: this.window.origin,
260
                        clientId: this.clientId,
261
                        endpoint,
262
                        changeDetailsCode
263
                });
264
        }
265

266
        /**
267
         *
268
         * @private
269
         */
270
        _validateInitalize() {
271
                if (!this.initialized) {
21✔
272
                        throw new AppIDError(constants.FAIL_TO_INITIALIZE);
1✔
273
                }
274
        }
275
}
276

277
module.exports = AppID;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc