index.js

  1. const Utils = require('./utils');
  2. const RequestHandler = require('./RequestHandler');
  3. const PopupController = require('./PopupController');
  4. const IFrameController = require('./IFrameController');
  5. const OpenIdConfigurationResource = require('./OpenIDConfigurationResource')
  6. const TokenValidator = require('./TokenValidator');
  7. const constants = require('./constants');
  8. const AppIDError = require('./errors/AppIDError');
  9. const jsrsasign = require('jsrsasign');
  10. /**
  11. * This class provides functions to support authentication.
  12. */
  13. class AppID {
  14. /**
  15. * This creates an instance of AppID. Once created, call init() before attempting to sign in.
  16. * @example
  17. * const appID = new AppID();
  18. */
  19. constructor(
  20. {
  21. popup = new PopupController(),
  22. iframe = new IFrameController(),
  23. openIdConfigResource = new OpenIdConfigurationResource(),
  24. utils,
  25. requestHandler = new RequestHandler(),
  26. tokenValidator = new TokenValidator(),
  27. w = window,
  28. url = URL
  29. } = {}) {
  30. this.popup = popup;
  31. this.iframe = iframe;
  32. this.openIdConfigResource = openIdConfigResource;
  33. this.URL = url;
  34. this.utils = utils;
  35. this.tokenValidator = tokenValidator;
  36. if (!utils) {
  37. this.utils = new Utils({
  38. openIdConfigResource: this.openIdConfigResource,
  39. url: this.URL,
  40. popup: this.popup,
  41. jsrsasign
  42. });
  43. }
  44. this.request = requestHandler.request;
  45. this.window = w;
  46. this.initialized = false;
  47. }
  48. /**
  49. * Initialize AppID. Call this function before attempting to sign in. You must wait for the promise to resolve.
  50. * @param {Object} options
  51. * @param {string} options.clientId - The clientId from the singlepageapp application credentials.
  52. * @param {string} options.discoveryEndpoint - The discoveryEndpoint from the singlepageapp application credentials.
  53. * @param {Object} [options.popup] - The popup configuration.
  54. * @param {Number} options.popup.height - The popup height.
  55. * @param {Number} options.popup.width - The popup width.
  56. * @returns {Promise<void>}
  57. * @throws {AppIDError} For missing required params.
  58. * @throws {RequestError} Any errors during a HTTP request.
  59. * @example
  60. * await appID.init({
  61. * clientId: '<SPA_CLIENT_ID>',
  62. * discoveryEndpoint: '<WELL_KNOWN_ENDPOINT>'
  63. * });
  64. *
  65. */
  66. async init({clientId, discoveryEndpoint, popup = {height: window.screen.height * .80, width: 400}}) {
  67. if (!clientId) {
  68. throw new AppIDError(constants.MISSING_CLIENT_ID);
  69. }
  70. try {
  71. new this.URL(discoveryEndpoint)
  72. } catch (e) {
  73. throw new AppIDError(constants.INVALID_DISCOVERY_ENDPOINT);
  74. }
  75. await this.openIdConfigResource.init({discoveryEndpoint, requestHandler: this.request});
  76. this.popup.init(popup);
  77. this.clientId = clientId;
  78. this.initialized = true;
  79. }
  80. /**
  81. * @typedef {Object} Tokens
  82. * @property {string} accessToken A JWT.
  83. * @property {Object} accessTokenPayload The decoded JWT.
  84. * @property {string} idToken A JWT.
  85. * @property {Object} idTokenPayload The decoded JWT.
  86. */
  87. /**
  88. * This will open a sign in widget in a popup which will prompt the user to enter their credentials.
  89. * After a successful sign in, the popup will close and tokens are returned.
  90. * @returns {Promise<Tokens>} The tokens of the authenticated user.
  91. * @throws {PopupError} "Popup closed" - The user closed the popup before authentication was completed.
  92. * @throws {TokenError} Any token validation error.
  93. * @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: ''}
  94. * @throws {RequestError} Any errors during a HTTP request.
  95. * @example
  96. * const {accessToken, accessTokenPayload, idToken, idTokenPayload} = await appID.signin();
  97. */
  98. async signin() {
  99. this._validateInitalize();
  100. const endpoint = this.openIdConfigResource.getAuthorizationEndpoint();
  101. let origin = this.window.location.origin;
  102. if (!origin) {
  103. origin = this.window.location.protocol + "//" + this.window.location.hostname + (this.window.location.port ? ':' + this.window.location.port : '');
  104. }
  105. return this.utils.performOAuthFlowAndGetTokens({
  106. origin,
  107. endpoint,
  108. clientId: this.clientId
  109. });
  110. }
  111. /**
  112. * Silent sign in allows you to automatically obtain new tokens for a user without the user having to re-authenticate using a popup.
  113. * This will attempt to authenticate the user in a hidden iframe.
  114. * You will need to [enable Cloud Directory SSO]{@link https://cloud.ibm.com/docs/services/appid?topic=appid-single-page#spa-silent-login}.
  115. * Sign in will be successful only if the user has previously signed in using Cloud Directory and their session is not expired.
  116. * @returns {Promise<Tokens>} The tokens of the authenticated user.
  117. * @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'}
  118. * @throws {IFrameError} "Silent sign-in timed out" - The iframe will close after 5 seconds if authentication could not be completed.
  119. * @throws {TokenError} Any token validation error.
  120. * @throws {RequestError} Any errors during a HTTP request.
  121. * @example
  122. * const {accessToken, accessTokenPayload, idToken, idTokenPayload} = await appID.silentSignin();
  123. */
  124. async silentSignin() {
  125. this._validateInitalize();
  126. const endpoint = this.openIdConfigResource.getAuthorizationEndpoint();
  127. const {codeVerifier, nonce, state, url} = this.utils.getAuthParamsAndUrl({
  128. clientId: this.clientId,
  129. origin: this.window.origin,
  130. prompt: constants.PROMPT,
  131. endpoint
  132. });
  133. this.iframe.open(url);
  134. let message;
  135. try {
  136. message = await this.iframe.waitForMessage({messageType: 'authorization_response'});
  137. } finally {
  138. this.iframe.remove();
  139. }
  140. this.utils.verifyMessage({message, state});
  141. let authCode = message.data.code;
  142. return await this.utils.retrieveTokens({
  143. clientId: this.clientId,
  144. authCode,
  145. codeVerifier,
  146. nonce,
  147. openId: this.openIdConfigResource,
  148. windowOrigin: this.window.origin
  149. });
  150. }
  151. /**
  152. * 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.
  153. * @param {string} accessToken The App ID access token of the user.
  154. * @returns {Promise} The user information for the authenticated user. Example: {sub: '', email: ''}
  155. * @throws {AppIDError} "Access token must be a string" Invalid access token.
  156. * @throws {RequestError} Any errors during a HTTP request.
  157. */
  158. async getUserInfo(accessToken) {
  159. this._validateInitalize();
  160. if (typeof accessToken !== 'string') {
  161. throw new AppIDError(constants.INVALID_ACCESS_TOKEN);
  162. }
  163. return await this.request(this.openIdConfigResource.getUserInfoEndpoint(), {
  164. headers: {
  165. 'Authorization': 'Bearer ' + accessToken
  166. }
  167. });
  168. }
  169. /**
  170. * This method will open a popup to the change password widget for Cloud Directory users.
  171. * You must enable users to manage their account from your app in Cloud Directory settings.
  172. * @param {string} idToken A JWT.
  173. * @returns {Promise<Tokens>} The tokens of the authenticated user.
  174. * @throws {AppIDError} "Expect id token payload object to have identities field"
  175. * @throws {AppIDError} "Must be a Cloud Directory user"
  176. * @throws {AppIDError} "Missing id token string"
  177. * @example
  178. * let tokens = await appID.changePassword(idToken);
  179. */
  180. async changePassword(idToken) {
  181. this._validateInitalize();
  182. if (!idToken || typeof idToken !== 'string') {
  183. throw new AppIDError(constants.MISSING_ID_TOKEN);
  184. }
  185. let userId;
  186. const publicKeys = await this.openIdConfigResource.getPublicKeys();
  187. let decodedToken = this.tokenValidator.decodeAndValidate({
  188. token: idToken,
  189. publicKeys,
  190. issuer: this.openIdConfigResource.getIssuer(),
  191. clientId: this.clientId
  192. });
  193. if (decodedToken.identities && decodedToken.identities[0] && decodedToken.identities[0].id) {
  194. if (decodedToken.identities[0].provider !== 'cloud_directory') {
  195. throw new AppIDError(constants.NOT_CD_USER);
  196. }
  197. userId = decodedToken.identities[0].id;
  198. } else {
  199. throw new AppIDError(constants.INVALID_ID_TOKEN);
  200. }
  201. const endpoint = this.openIdConfigResource.getIssuer() + constants.CHANGE_PASSWORD;
  202. return await this.utils.performOAuthFlowAndGetTokens({
  203. userId,
  204. origin: this.window.origin,
  205. clientId: this.clientId,
  206. endpoint
  207. });
  208. }
  209. /**
  210. * This method will open a popup to the change details widget for Cloud Directory users.
  211. * You must enable users to manage their account from your app in Cloud Directory settings.
  212. * @param {Object} tokens App ID tokens
  213. * @returns {Promise<Tokens>}
  214. * @throws {AppIDError} "Missing id token string"
  215. * @throws {AppIDError} "Missing access token string"
  216. * @throws {AppIDError} "Missing tokens object"
  217. * @example
  218. * let tokens = {accessToken, idToken}
  219. * let newTokens = await appID.changeDetails(tokens);
  220. */
  221. async changeDetails({accessToken, idToken}) {
  222. this._validateInitalize();
  223. if (!accessToken && typeof accessToken !== 'string') {
  224. throw new AppIDError(constants.MISSING_ACCESS_TOKEN);
  225. }
  226. if (!idToken && typeof idToken !== 'string') {
  227. throw new AppIDError(constants.MISSING_ID_TOKEN);
  228. }
  229. const generateCodeUrl = this.openIdConfigResource.getIssuer() + constants.GENERATE_CODE;
  230. const changeDetailsCode = await this.request(generateCodeUrl, {
  231. headers: {
  232. 'Authorization': 'Bearer ' + accessToken + ' ' + idToken
  233. }
  234. });
  235. const endpoint = this.openIdConfigResource.getIssuer() + constants.CHANGE_DETAILS;
  236. return this.utils.performOAuthFlowAndGetTokens({
  237. origin: this.window.origin,
  238. clientId: this.clientId,
  239. endpoint,
  240. changeDetailsCode
  241. });
  242. }
  243. /**
  244. *
  245. * @private
  246. */
  247. _validateInitalize() {
  248. if (!this.initialized) {
  249. throw new AppIDError(constants.FAIL_TO_INITIALIZE);
  250. }
  251. }
  252. }
  253. module.exports = AppID;