// Copyright Inrupt Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the // Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // /** * @hidden * @packageDocumentation */ import type { IClient, IClientRegistrar, IIssuerConfigFetcher, IStorageUtility, KeyPair, ITokenRefresher, TokenEndpointResponse, } from "@inrupt/solid-client-authn-core"; import { loadOidcContextFromStorage, getWebidFromTokenPayload, PREFERRED_SIGNING_ALG, EVENTS, } from "@inrupt/solid-client-authn-core"; import type { IssuerMetadata, TokenSet } from "openid-client"; import { Issuer } from "openid-client"; import type { KeyObject } from "crypto"; import type { EventEmitter } from "events"; import { configToIssuerMetadata } from "../IssuerConfigFetcher"; import { negotiateClientSigningAlg } from "../ClientRegistrar"; // Camelcase identifiers are required in the OIDC specification. /* eslint-disable camelcase*/ const tokenSetToTokenEndpointResponse = async ( tokenSet: TokenSet, issuerMetadata: IssuerMetadata, clientInfo: IClient, ): Promise => { if (tokenSet.access_token === undefined || tokenSet.id_token === undefined) { // The error message is left minimal on purpose not to leak the tokens. throw new Error( `The Identity Provider [${issuerMetadata.issuer}] did not return the expected tokens on refresh: missing at least one of 'access_token', 'id_token'.`, ); } if (tokenSet.token_type !== "Bearer" && tokenSet.token_type !== "DPoP") { throw new Error( `The Identity Provider [${issuerMetadata.issuer}] returned an unknown token type: [${tokenSet.token_type}].`, ); } if (typeof issuerMetadata.jwks_uri != "string") { throw new Error( `Cannot verify ID Token: Issuer Metadata is missing a JWKS URI (${JSON.stringify(issuerMetadata)})`, ); } const { webId } = await getWebidFromTokenPayload( tokenSet.id_token, issuerMetadata.jwks_uri, issuerMetadata.issuer, clientInfo.clientId, ); return { accessToken: tokenSet.access_token, tokenType: tokenSet.token_type, idToken: tokenSet.id_token, refreshToken: tokenSet.refresh_token, webId, expiresAt: tokenSet.expires_at, }; }; /** * @hidden */ export default class TokenRefresher implements ITokenRefresher { constructor( private storageUtility: IStorageUtility, private issuerConfigFetcher: IIssuerConfigFetcher, private clientRegistrar: IClientRegistrar, ) { this.storageUtility = storageUtility; this.issuerConfigFetcher = issuerConfigFetcher; this.clientRegistrar = clientRegistrar; } async refresh( sessionId: string, refreshToken?: string, dpopKey?: KeyPair, eventEmitter?: EventEmitter, ): Promise { const oidcContext = await loadOidcContextFromStorage( sessionId, this.storageUtility, this.issuerConfigFetcher, ); const issuer = new Issuer(configToIssuerMetadata(oidcContext.issuerConfig)); // This should also retrieve the client from storage const clientInfo: IClient = await this.clientRegistrar.getClient( { sessionId }, oidcContext.issuerConfig, ); if (clientInfo.idTokenSignedResponseAlg === undefined) { clientInfo.idTokenSignedResponseAlg = negotiateClientSigningAlg( oidcContext.issuerConfig, PREFERRED_SIGNING_ALG, ); } const client = new issuer.Client({ client_id: clientInfo.clientId, client_secret: clientInfo.clientSecret, token_endpoint_auth_method: typeof clientInfo.clientSecret === "undefined" ? "none" : "client_secret_basic", id_token_signed_response_alg: clientInfo.idTokenSignedResponseAlg, }); if (refreshToken === undefined) { throw new Error( `Session [${sessionId}] has no refresh token to allow it to refresh its access token.`, ); } if (oidcContext.dpop && dpopKey === undefined) { throw new Error( `For session [${sessionId}], the key bound to the DPoP access token must be provided to refresh said access token.`, ); } const tokenSet = await tokenSetToTokenEndpointResponse( await client.refresh(refreshToken, { // openid-client does not support yet jose@3.x, and expects // type definitions that are no longer present. However, the JWK // type that we pass here is compatible with the API, hence the type // assertion. DPoP: dpopKey ? (dpopKey.privateKey as KeyObject) : undefined, }), issuer.metadata, clientInfo, ); if (tokenSet.refreshToken !== undefined) { eventEmitter?.emit(EVENTS.NEW_REFRESH_TOKEN, tokenSet.refreshToken); await this.storageUtility.setForUser(sessionId, { refreshToken: tokenSet.refreshToken, }); } eventEmitter?.emit(EVENTS.NEW_TOKENS, { accessToken: tokenSet.accessToken, idToken: tokenSet.idToken, refreshToken: tokenSet.refreshToken, webId: tokenSet.webId, expiresAt: tokenSet.expiresAt, dpopKey, clientId: clientInfo.clientId, issuer: issuer.metadata.issuer, }); return tokenSet; } }