Skip to content

Add accessTokenExpiresIn field #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Token Handler Assistant Changelog

## 1.0.0
## [1.0.0] - 2024-06-13

- Initial release of the Token Handler Assistant library
- Initial release of the Token Handler Assistant library

## [Unreleased]

- Add `accessTokenExpiresIn` in responses to `session()`, `refresh()` and `endLogin()` functions.
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
}
```
```

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).
21 changes: 14 additions & 7 deletions src/oauth-agent-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
EndLoginRequest,
LogoutResponse,
OAuthAgentRemoteError,
RefreshResponse,
SessionResponse,
StartLoginRequest,
StartLoginResponse
Expand Down Expand Up @@ -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<void> {
return await this.fetch("POST", "refresh")
async refresh(): Promise<RefreshResponse> {
const refreshResponse = await this.fetch("POST", "refresh")

return {
accessTokenExpiresIn: refreshResponse.access_token_expires_in
}
}

/**
Expand All @@ -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
}
}

Expand All @@ -83,7 +91,6 @@ export class OAuthAgentClient {
* @throws OAuthAgentRemoteError when OAuth Agent responded with an error
*/
async startLogin(request?: StartLoginRequest): Promise<StartLoginResponse> {
// const body = this.toUrlEncodedString(request?.extraAuthorizationParameters)
const urlSearchParams = this.toUrlSearchParams(request?.extraAuthorizationParameters)
const startLoginResponse = await this.fetch("POST", "login/start", urlSearchParams)
return {
Expand All @@ -103,10 +110,10 @@ export class OAuthAgentClient {
*/
async endLogin(request: EndLoginRequest): Promise<SessionResponse> {
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
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -59,7 +71,6 @@ export interface SessionResponse {
*/
export interface LogoutResponse {
readonly logoutUrl?: string;

}

/**
Expand Down
12 changes: 9 additions & 3 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -32,15 +32,17 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be asserted somewhere in the tests, to cover correct mapping?

})
return Promise.resolve(body)
} else if (req.url.endsWith("/session")) {
const body = JSON.stringify({
is_logged_in: true,
id_token_claims: {
sub: 'session'
}
},
access_token_expires_in: 300
})
return Promise.resolve(body)
}
Expand All @@ -55,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 () => {
Expand All @@ -73,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 () => {
Expand Down