diff --git a/src/authentication/connector.ts b/src/authentication/connector.ts index f7ec35cf..48ec1167 100644 --- a/src/authentication/connector.ts +++ b/src/authentication/connector.ts @@ -389,39 +389,59 @@ export const logInWithPopUp = async (reset = false) => { return jso_getToken(authConfig.provider); }; +// If you read the Authentication in Console, you already have an idea of what it's going on here const logInWithWebMessageAndPKCE = async (reset: boolean) => { + // The first thing we do is to get the config const auth = getConfig(); + // Then we return a pending promise that will handle the authentication // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { + // Some guards. If we don't have the client_id specified there is no use trying to run authentication if (!auth.client_id) { reject(new Error("Client_id in AUTH_CONFIG is mandatory")); } - // ensure that there is an access token, redirecting to auth if needed + // If the redirect_uri isn't specified, we will take the current origin (we specify it on console, + // but if we didn't, this would be set to console.platform.sh) if (!auth.redirect_uri) { - // Some targets just need some dynamism auth.redirect_uri = window.location.origin; } + // Then we "configure" the config. We are not going to review the implementation for simplicity sake, but we: + // 1. Check that the provided configure is not in popUp mode, and that it has the response_type set to token (our case) + // 2. Check if the hash contains an access token. And if it do, extract the state, compare with config, and store the access token for later use. jso_configure({ [auth.provider]: auth }); + + // Then we access the token here const storedToken = jso_getToken(auth.provider); + // If we found a token and we don't want to refres, we will resolve with this token if (storedToken && !reset) { resolve(storedToken); } + // If not, we will delete all tokens and remove any existing iframes jso_wipe(); removeIFrame(); - // If we come from a redirect + // Remember that after authenticating in the auth ui, we are redirected to console with code and + // state in the query params. Here we check the query params an get the code and the state. const oauthResp = jso_checkforcode(); + // If we found them, it means that we just got redirected from the auth ui. We are going to request + // our first token! if (oauthResp) { + // We now get the code verifier from localStorage (remember that console always has it on the localStorage) const codeVerifier = jso_getCodeVerifier(auth.provider); if (codeVerifier && oauthResp.code && oauthResp.state) { + // Now we resolve resolve( + // In this function, we get the token by making a request to get it, sending the code and the code_verifier. We also send + // the string "platform-cli:" encoded on base64 in the Authorization header. This is so that Auth Server + // knows that we are a trusted client. If we get the token, we will set it's expiration, save it, and + // then remove the state and the code_verifier from the localStorage. await authorizationCodeCallback( auth, codeVerifier, @@ -432,47 +452,76 @@ const logInWithWebMessageAndPKCE = async (reset: boolean) => { } } + // If we are here it means 1. We don't have the code on the query params or 2. We don't have the code_verifier in the local storage + // either ways, it means it's not our first token request, so we need to prepare the iframe silent token refresh. In order to do this + // cross domain iframe trick, we need the Storage Access API. So if we don't have access, we will request it. This will create an + // iframe, and set it's src to /request-storage-access.html. It will give us an HTML as a response, that contains an + // script granting us access to make this operation. if (document.hasStorageAccess !== null) { await checkForStorageAccess(auth); } + // Then, we will wrab the request from localStorage const req = jso_getAuthRequest(auth.provider, auth.scope); + // We will generate a new code_challenge and a new code_verifier const pkce = await generatePKCE(); + // And we will set the request code_challenge to the one we just generated req.code_challenge = pkce.codeChallenge; req.code_challenge_method = "S256"; + // Here we define a fallback so that if the silent token refresh fails we have a way to reauthenticate the user. It's worst beacuse + // it stops our users and make them reauthenticate. const timeout = setTimeout(() => { + // If we have the popupMode enabled (meaning we will interrupt our users and prompt them with a popUp to reauthenticate), + // we will resolve with it. if (auth.popupMode) { resolve(logInWithPopUp()); return; } + // If we don't, then we will redirect them to auth ui so that they authenticate resolve(logInWithRedirect()); }, 5000); + // This function is later used as the handler of an event listener that listen to messages. This is the function that will + // run if we receive a message from an iframe const receiveMessage = async (event: MessageEvent) => { + // If the message is not from an auth iframe, we don't care about it if (event.origin !== auth.authentication_url) { return false; } + // We grab the data from the message const { data } = event; + // If there is an error, or que don't have a payload, or the message isn't the one we expect, we do the same as with the fallback + // "if the silent token refresh fails we have a way to reauthenticate the user. It's worst beacuse + // it stops our users and make them reauthenticate." if (data.error || !data.payload || data.state !== req.state) { + // If we have the popupMode enabled (meaning we will interrupt our users and prompt them with a popUp to reauthenticate), + // we will resolve with it. if (auth.popupMode) { return logInWithPopUp(); } + // If we don't, then we will redirect them to auth ui so that they authenticate return logInWithRedirect(); } + // If we are here, it means the message is what we expected, so we remove the request form the localStorage localStorage.removeItem(`state-${req.providerID}-${req.state}`); + // And also the event listener, because we already have what we wanted window.removeEventListener("message", receiveMessage, false); + // We also clear the timeout of 5 seconds that runs the fallback, because if we are here it means the silent refresh + // is going well clearTimeout(timeout); + // We get the code from the payload of the data of the message const code = data.payload; + // We resolve with the code and the code_verifier we generated before resolve( await authorizationCodeCallback( auth, @@ -484,11 +533,14 @@ const logInWithWebMessageAndPKCE = async (reset: boolean) => { return; }; + // Here we add the event listener that listen to messages of iframes window.addEventListener("message", receiveMessage, false); + // And here we create the auth iframe! const authUrl = encodeURL(auth.authorization, req); createIFrame(authUrl); } catch (err) { + // If anything happen, we catch it and run the fallback console.log("Error Silent refresh"); console.log(err); if (auth.popupMode) { @@ -496,30 +548,40 @@ const logInWithWebMessageAndPKCE = async (reset: boolean) => { } console.log("Error In web message mode, trying redirect..."); void logInWithRedirect(); + + // That is it! Authentication in Console } }); }; +// This function takes a token, a reset value and a config (already described in previous steps) export default async ( token?: string, reset = false, config?: Partial ) => { + // If we are running the client through NodeJS, we use the token to authenticate if (isNode && token) { return logInWithToken(token).catch(e => new Error(e)); } + // If we have extra_params, we use the only login function that works with them if (config?.extra_params && Object.entries(config.extra_params).length) { return logInWithRedirect(reset, config.extra_params); } + // Console usecase! if (config?.response_mode === "web_message" && config.prompt === "none") { return logInWithWebMessageAndPKCE(reset); } + // If we run with the popUp mode enabled if (config?.popupMode) { return logInWithPopUp(reset); } + // If not, we fallback to logIn with redirection return logInWithRedirect(reset, config?.extra_params); }; + +// Now let's review the console usecase! Go to line 392 diff --git a/src/authentication/index.ts b/src/authentication/index.ts index 429bfab0..827097a6 100644 --- a/src/authentication/index.ts +++ b/src/authentication/index.ts @@ -17,6 +17,7 @@ export type JWTToken = { scope: string; }; +// This is the function. It receives a config, and runs the authentication. export default async ( { api_token, @@ -29,35 +30,60 @@ export default async ( }: ClientConfiguration, reset = false ): Promise => { + // As you can see, we declare a let variable on line 10 called authenticationInProgress. + // We will use it to know if there is an ongoing authentication, and if so, we will return + // the promise that handles it. if (authenticationInProgress) { return getAuthenticationPromise(); } + // If we don't have an outgoing authentication, we will start one. authenticationInProgress = true; + // If we received an access_token in the config, we can resolve the promise with it. + // Note that it will we are setting this token with expires: -1. We use this so that + // when we call the authenticatedRequests function, it doesn't fail even if the token + // is expired const promise = access_token ? Promise.resolve({ access_token, expires: -1 }) - : connector(api_token, reset, { + : // If not, we will try to get an access token. API tokens are used if you are running + // the client in NodeJS, which is not our case. Reset is set to false by default. We + // set it to true if the function is called through reAuthenticate(), defined in line + // 50 in /src/index.ts + connector(api_token, reset, { provider, + // This defines if we should open a popup to handle authentication, we don't use it on Console popupMode, + // This is set to web_message on Console, triggering the iframe creation response_mode, + // This is set to none, so that we don't promp the user to authenticate in Console, + // doing a silent token refresh with the iframes prompt, + // This is in case you want to use extra parameters, we don't use it on Console extra_params }); + // Now that we have defined the promise, if we have any, we will: if (promise) { + // Set the authenticationPromise to the promise, this will allow us to get the + // promise anywhere in the application setAuthenticationPromise(promise); + // Make sure to set the authenticationInProgress to false once the promise is resolved void promise.then(() => { authenticationInProgress = false; }); + // Return the promise return promise; } + // If we don't have a promise, something went wrong :( return Promise.reject(new Error()); }; +// Now lets go to read the connector function, go to line 557 in /src/authentication/connector.ts + export const authenticatedRequest = api; export const wipeToken = jso_wipe; diff --git a/src/index.ts b/src/index.ts index 152c0f1b..23e3bdb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,13 @@ export default class Client { getAccountInfoPromise: Promise | undefined; constructor(authenticationConfig: ClientConfiguration) { + // When the client is initialiced, we set the config with this function + // this will allow us to acces the config from anywhere in the client. setConfig(authenticationConfig); + // Then we call the connector function passing it the config, and we + // set the authenticationPromise to the return of it. Go to line 20 + // of /src/authentication/index.ts this.authenticationPromise = connector(authenticationConfig); }