From ab989a8e5ff9c67243c5fb529676f66f5a3f5fc5 Mon Sep 17 00:00:00 2001 From: Tomas Micko Date: Fri, 21 Jun 2024 10:01:18 +0200 Subject: [PATCH 1/3] Add accessTokenExpiresIn field --- CHANGELOG.md | 6 +++++- README.md | 23 ++++++++++++++++++++--- src/oauth-agent-client.ts | 21 ++++++++++++++------- src/types.ts | 13 ++++++++++++- tests/index.test.ts | 8 +++++--- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c10fd1d..6c8dd1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,8 @@ ## 1.0.0 -- Initial release of the Token Handler Assistant library \ No newline at end of file +- Initial release of the Token Handler Assistant library + +## 1.1.0 + +- Add `accessTokenExpiresIn` in responses to `session()`, `refresh()` and `endLogin()` functions. \ No newline at end of file diff --git a/README.md b/README.md index 67d3041..373b067 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The `Configuration` object contains the following options: const url = new URL(location.href) const response = await client.endLogin({ searchParams: url.searchParams }) if (response.isLoggedIn) { - // use id token claims to get username, e.g. response.idTokenClaims?.sub + // use id token claims to get username, e.g. response.idTokenClaims?.sub } ``` Note: The `endLogin` function should only be called with authorization response parameters (when the authorization @@ -64,7 +64,7 @@ on every load of the SPA. This function makes a decision based the query string const sessionResponse = await client.session() // use session data if (session.isLoggedIn === true) { - session.idTokenClaims?.sub + session.idTokenClaims?.sub } ``` 6. Logging out @@ -74,4 +74,21 @@ on every load of the SPA. This function makes a decision based the query string // redirect user to the single logout url location.href = logoutResponse.logoutUrl; } - ``` \ No newline at end of file + ``` + +7. Implementing preemptive refresh. `session()`, `refresh()`, `endLogin()` and `onPageLoad()` functions return `accessTokenExpiresIn` + if the Authorization Server includes `expires_in` in token responses. This field contains number of seconds until an + access token that is in the proxy cookie expires. This value can be used to preemptively refresh the access token. + After calling `onPageLoad()` and `refresh()`: + ```typescript + // const response = await client.onPageLoad(location.href) + // const response = await client.refresh() + if (response.accessTokenExpiresIn != null) { + const delay = Math.max(response.accessTokenExpiresIn - 2, 1) + setTimeout( + () => { client.refresh(); }, + delay * 1000 + ); + } + ``` + Note: This is just a simplified example. The timeout has to be cleared properly (before every refresh, or before logout). \ No newline at end of file diff --git a/src/oauth-agent-client.ts b/src/oauth-agent-client.ts index 69ad84b..906ed6b 100644 --- a/src/oauth-agent-client.ts +++ b/src/oauth-agent-client.ts @@ -16,6 +16,7 @@ import { EndLoginRequest, LogoutResponse, OAuthAgentRemoteError, + RefreshResponse, SessionResponse, StartLoginRequest, StartLoginResponse @@ -48,10 +49,16 @@ export class OAuthAgentClient { /** * Refreshes the access token. Calls the `/refresh` endpoint. * + * @return the refresh token response possibly containing the new access token's expiration time + * * @throws OAuthAgentRemoteError when OAuth Agent responded with an error */ - async refresh(): Promise { - return await this.fetch("POST", "refresh") + async refresh(): Promise { + const refreshResponse = await this.fetch("POST", "refresh") + + return { + accessTokenExpiresIn: refreshResponse.access_token_expires_in + } } /** @@ -66,7 +73,8 @@ export class OAuthAgentClient { const sessionResponse = await this.fetch("GET", "session"); return { isLoggedIn: sessionResponse.is_logged_in as boolean, - idTokenClaims: sessionResponse.id_token_claims + idTokenClaims: sessionResponse.id_token_claims, + accessTokenExpiresIn: sessionResponse.access_token_expires_in } } @@ -83,7 +91,6 @@ export class OAuthAgentClient { * @throws OAuthAgentRemoteError when OAuth Agent responded with an error */ async startLogin(request?: StartLoginRequest): Promise { - // const body = this.toUrlEncodedString(request?.extraAuthorizationParameters) const urlSearchParams = this.toUrlSearchParams(request?.extraAuthorizationParameters) const startLoginResponse = await this.fetch("POST", "login/start", urlSearchParams) return { @@ -103,10 +110,10 @@ export class OAuthAgentClient { */ async endLogin(request: EndLoginRequest): Promise { const endLoginResponse = await this.fetch("POST", "login/end", request.searchParams) - const isLoggedIn = endLoginResponse.is_logged_in as boolean return { - isLoggedIn: isLoggedIn, - idTokenClaims: endLoginResponse.id_token_claims + isLoggedIn: endLoginResponse.is_logged_in as boolean, + idTokenClaims: endLoginResponse.id_token_claims, + accessTokenExpiresIn: endLoginResponse.access_token_expires_in } } diff --git a/src/types.ts b/src/types.ts index 23718d5..eb2327d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,11 +45,23 @@ export interface EndLoginRequest { * - `isLoggedIn` - a boolean flag indicationg whether a user is logged in * - `idTokenClaims` - an object containing ID token claims. This will be `null` if the user is * logged out; or the user is logged in but no ID token was issued. + * - `accessTokenExpiresIn` - expiration time of access token in seconds (`null` if no `expires_in` parameter + * was returned from the Authorization Server's token endpoint) */ export interface SessionResponse { readonly isLoggedIn: boolean; readonly idTokenClaims?: any; + readonly accessTokenExpiresIn?: number; +} + +/** + * Returned from the {@link OAuthAgentClient#refresh} function. Contains: + * - `accessTokenExpiresIn` - expiration time of access token in seconds (`null` if no `expires_in` parameter + * was returned from the Authorization Server's token endpoint) + */ +export interface RefreshResponse { + readonly accessTokenExpiresIn?: number; } /** @@ -59,7 +71,6 @@ export interface SessionResponse { */ export interface LogoutResponse { readonly logoutUrl?: string; - } /** diff --git a/tests/index.test.ts b/tests/index.test.ts index 83e1f12..ca2b59e 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -13,7 +13,7 @@ */ import fetchMock from "jest-fetch-mock" -import {Configuration, OAuthAgentClient, StartLoginRequest} from '../src'; +import {Configuration, OAuthAgentClient} from '../src'; const serverUrl = 'https://example.com' const authzUrl = serverUrl + '/authz' @@ -32,7 +32,8 @@ beforeEach(() => { is_logged_in: true, id_token_claims: { sub: 'login-end' // we are using 'sub' claims to distinguish between call to /login/end and /session (otherwise they return the same JSON structure) - } + }, + access_token_expires_in: 300 }) return Promise.resolve(body) } else if (req.url.endsWith("/session")) { @@ -40,7 +41,8 @@ beforeEach(() => { is_logged_in: true, id_token_claims: { sub: 'session' - } + }, + access_token_expires_in: 300 }) return Promise.resolve(body) } From 21fd402b63a4e877dbafa5ecc347330d9901a18d Mon Sep 17 00:00:00 2001 From: Tomas Micko Date: Mon, 24 Jun 2024 13:27:42 +0200 Subject: [PATCH 2/3] IS--8894 Update unit tests --- tests/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/index.test.ts b/tests/index.test.ts index ca2b59e..b6afc82 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -57,6 +57,8 @@ describe('test onPageLoad() function', () => { const queryString = '?state=foo&code=bar' const response = await client.onPageLoad(redirectUri + queryString); expect(response.idTokenClaims?.sub).toBe('login-end'); + expect(response.isLoggedIn).toBe(true); + expect(response.accessTokenExpiresIn).toBe(300); }); test('when url contains state and error, /login/end should be called', async () => { @@ -75,6 +77,8 @@ describe('test onPageLoad() function', () => { const queryString = '?response=eyjwt&state=foo' const response = await client.onPageLoad(redirectUri + queryString); expect(response.idTokenClaims?.sub).toBe('session'); + expect(response.isLoggedIn).toBe(true); + expect(response.accessTokenExpiresIn).toBe(300); }); test('when url contains only state, /session should be called', async () => { From 389e5329f928585b038a66918629442a80e75c48 Mon Sep 17 00:00:00 2001 From: Tomas Micko Date: Mon, 24 Jun 2024 13:31:33 +0200 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8dd1e..efd9e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Token Handler Assistant Changelog -## 1.0.0 +## [1.0.0] - 2024-06-13 - Initial release of the Token Handler Assistant library -## 1.1.0 +## [Unreleased] - Add `accessTokenExpiresIn` in responses to `session()`, `refresh()` and `endLogin()` functions. \ No newline at end of file