Skip to content

Commit 2d1abc4

Browse files
authored
Merge pull request #4 from curityio/feature/IS-8894-at-expiration
Add accessTokenExpiresIn field
2 parents d65a05c + 389e532 commit 2d1abc4

File tree

5 files changed

+61
-16
lines changed

5 files changed

+61
-16
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Token Handler Assistant Changelog
22

3-
## 1.0.0
3+
## [1.0.0] - 2024-06-13
44

5-
- Initial release of the Token Handler Assistant library
5+
- Initial release of the Token Handler Assistant library
6+
7+
## [Unreleased]
8+
9+
- Add `accessTokenExpiresIn` in responses to `session()`, `refresh()` and `endLogin()` functions.

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The `Configuration` object contains the following options:
3737
const url = new URL(location.href)
3838
const response = await client.endLogin({ searchParams: url.searchParams })
3939
if (response.isLoggedIn) {
40-
// use id token claims to get username, e.g. response.idTokenClaims?.sub
40+
// use id token claims to get username, e.g. response.idTokenClaims?.sub
4141
}
4242
```
4343
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
6464
const sessionResponse = await client.session()
6565
// use session data
6666
if (session.isLoggedIn === true) {
67-
session.idTokenClaims?.sub
67+
session.idTokenClaims?.sub
6868
}
6969
```
7070
6. Logging out
@@ -74,4 +74,21 @@ on every load of the SPA. This function makes a decision based the query string
7474
// redirect user to the single logout url
7575
location.href = logoutResponse.logoutUrl;
7676
}
77-
```
77+
```
78+
79+
7. Implementing preemptive refresh. `session()`, `refresh()`, `endLogin()` and `onPageLoad()` functions return `accessTokenExpiresIn`
80+
if the Authorization Server includes `expires_in` in token responses. This field contains number of seconds until an
81+
access token that is in the proxy cookie expires. This value can be used to preemptively refresh the access token.
82+
After calling `onPageLoad()` and `refresh()`:
83+
```typescript
84+
// const response = await client.onPageLoad(location.href)
85+
// const response = await client.refresh()
86+
if (response.accessTokenExpiresIn != null) {
87+
const delay = Math.max(response.accessTokenExpiresIn - 2, 1)
88+
setTimeout(
89+
() => { client.refresh(); },
90+
delay * 1000
91+
);
92+
}
93+
```
94+
Note: This is just a simplified example. The timeout has to be cleared properly (before every refresh, or before logout).

src/oauth-agent-client.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
EndLoginRequest,
1717
LogoutResponse,
1818
OAuthAgentRemoteError,
19+
RefreshResponse,
1920
SessionResponse,
2021
StartLoginRequest,
2122
StartLoginResponse
@@ -48,10 +49,16 @@ export class OAuthAgentClient {
4849
/**
4950
* Refreshes the access token. Calls the `/refresh` endpoint.
5051
*
52+
* @return the refresh token response possibly containing the new access token's expiration time
53+
*
5154
* @throws OAuthAgentRemoteError when OAuth Agent responded with an error
5255
*/
53-
async refresh(): Promise<void> {
54-
return await this.fetch("POST", "refresh")
56+
async refresh(): Promise<RefreshResponse> {
57+
const refreshResponse = await this.fetch("POST", "refresh")
58+
59+
return {
60+
accessTokenExpiresIn: refreshResponse.access_token_expires_in
61+
}
5562
}
5663

5764
/**
@@ -66,7 +73,8 @@ export class OAuthAgentClient {
6673
const sessionResponse = await this.fetch("GET", "session");
6774
return {
6875
isLoggedIn: sessionResponse.is_logged_in as boolean,
69-
idTokenClaims: sessionResponse.id_token_claims
76+
idTokenClaims: sessionResponse.id_token_claims,
77+
accessTokenExpiresIn: sessionResponse.access_token_expires_in
7078
}
7179
}
7280

@@ -83,7 +91,6 @@ export class OAuthAgentClient {
8391
* @throws OAuthAgentRemoteError when OAuth Agent responded with an error
8492
*/
8593
async startLogin(request?: StartLoginRequest): Promise<StartLoginResponse> {
86-
// const body = this.toUrlEncodedString(request?.extraAuthorizationParameters)
8794
const urlSearchParams = this.toUrlSearchParams(request?.extraAuthorizationParameters)
8895
const startLoginResponse = await this.fetch("POST", "login/start", urlSearchParams)
8996
return {
@@ -103,10 +110,10 @@ export class OAuthAgentClient {
103110
*/
104111
async endLogin(request: EndLoginRequest): Promise<SessionResponse> {
105112
const endLoginResponse = await this.fetch("POST", "login/end", request.searchParams)
106-
const isLoggedIn = endLoginResponse.is_logged_in as boolean
107113
return {
108-
isLoggedIn: isLoggedIn,
109-
idTokenClaims: endLoginResponse.id_token_claims
114+
isLoggedIn: endLoginResponse.is_logged_in as boolean,
115+
idTokenClaims: endLoginResponse.id_token_claims,
116+
accessTokenExpiresIn: endLoginResponse.access_token_expires_in
110117
}
111118
}
112119

src/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,23 @@ export interface EndLoginRequest {
4545
* - `isLoggedIn` - a boolean flag indicationg whether a user is logged in
4646
* - `idTokenClaims` - an object containing ID token claims. This will be `null` if the user is
4747
* logged out; or the user is logged in but no ID token was issued.
48+
* - `accessTokenExpiresIn` - expiration time of access token in seconds (`null` if no `expires_in` parameter
49+
* was returned from the Authorization Server's token endpoint)
4850
*/
4951
export interface SessionResponse {
5052
readonly isLoggedIn: boolean;
5153
readonly idTokenClaims?: any;
54+
readonly accessTokenExpiresIn?: number;
55+
}
56+
5257

58+
/**
59+
* Returned from the {@link OAuthAgentClient#refresh} function. Contains:
60+
* - `accessTokenExpiresIn` - expiration time of access token in seconds (`null` if no `expires_in` parameter
61+
* was returned from the Authorization Server's token endpoint)
62+
*/
63+
export interface RefreshResponse {
64+
readonly accessTokenExpiresIn?: number;
5365
}
5466

5567
/**
@@ -59,7 +71,6 @@ export interface SessionResponse {
5971
*/
6072
export interface LogoutResponse {
6173
readonly logoutUrl?: string;
62-
6374
}
6475

6576
/**

tests/index.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414

1515
import fetchMock from "jest-fetch-mock"
16-
import {Configuration, OAuthAgentClient, StartLoginRequest} from '../src';
16+
import {Configuration, OAuthAgentClient} from '../src';
1717

1818
const serverUrl = 'https://example.com'
1919
const authzUrl = serverUrl + '/authz'
@@ -32,15 +32,17 @@ beforeEach(() => {
3232
is_logged_in: true,
3333
id_token_claims: {
3434
sub: 'login-end' // we are using 'sub' claims to distinguish between call to /login/end and /session (otherwise they return the same JSON structure)
35-
}
35+
},
36+
access_token_expires_in: 300
3637
})
3738
return Promise.resolve(body)
3839
} else if (req.url.endsWith("/session")) {
3940
const body = JSON.stringify({
4041
is_logged_in: true,
4142
id_token_claims: {
4243
sub: 'session'
43-
}
44+
},
45+
access_token_expires_in: 300
4446
})
4547
return Promise.resolve(body)
4648
}
@@ -55,6 +57,8 @@ describe('test onPageLoad() function', () => {
5557
const queryString = '?state=foo&code=bar'
5658
const response = await client.onPageLoad(redirectUri + queryString);
5759
expect(response.idTokenClaims?.sub).toBe('login-end');
60+
expect(response.isLoggedIn).toBe(true);
61+
expect(response.accessTokenExpiresIn).toBe(300);
5862
});
5963

6064
test('when url contains state and error, /login/end should be called', async () => {
@@ -73,6 +77,8 @@ describe('test onPageLoad() function', () => {
7377
const queryString = '?response=eyjwt&state=foo'
7478
const response = await client.onPageLoad(redirectUri + queryString);
7579
expect(response.idTokenClaims?.sub).toBe('session');
80+
expect(response.isLoggedIn).toBe(true);
81+
expect(response.accessTokenExpiresIn).toBe(300);
7682
});
7783

7884
test('when url contains only state, /session should be called', async () => {

0 commit comments

Comments
 (0)