Skip to content

Commit e5e5fe4

Browse files
committed
chore(appdev-common): client creds force new token
1 parent 10f7671 commit e5e5fe4

3 files changed

Lines changed: 52 additions & 18 deletions

File tree

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/appdev-common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dvsa/appdev-api-common",
3-
"version": "1.4.0",
3+
"version": "2.0.0",
44
"keywords": ["dvsa", "nodejs", "typescript"],
55
"author": "DVSA",
66
"repository": {

packages/appdev-common/src/auth/client-credentials.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { stringify } from "node:querystring";
2-
import { decodeJwt } from "jose";
2+
import { type JWTPayload, decodeJwt } from "jose";
33

44
export interface ClientCredentialsResponse {
55
token_type: string;
@@ -8,6 +8,27 @@ export interface ClientCredentialsResponse {
88
access_token: string;
99
}
1010

11+
interface Options {
12+
/**
13+
*
14+
* Whether to log debug messages & tokens during the token retrieval process.
15+
* Useful for troubleshooting and understanding the flow of operations.
16+
*
17+
* Use with Caution, this could leak secrets!
18+
*/
19+
debugMode?: boolean;
20+
/**
21+
* Whether to skip any token caching and force fresh retrievals
22+
*
23+
*/
24+
forceFreshAuth?: boolean;
25+
/**
26+
* Treat token as expired if it has less than this many seconds remaining.
27+
* Helps avoid clock skew / in-flight expiry. (Default 30 secs)
28+
*/
29+
expirySkewSeconds?: number;
30+
}
31+
1132
export class ClientCredentials {
1233
private static accessToken: string;
1334
private static readonly grantType = "client_credentials";
@@ -18,15 +39,20 @@ export class ClientCredentials {
1839
* @param clientId - The client id
1940
* @param clientSecret - The client secret
2041
* @param scope - The scope of the access token
21-
* @param debugMode - Whether to log debug messages
42+
* @param resource
43+
* @param options
2244
*/
2345
constructor(
2446
private readonly tokenUrl: string,
2547
private readonly clientId: string,
2648
private readonly clientSecret: string,
2749
private readonly scope: string | undefined,
2850
private readonly resource: string | undefined,
29-
private readonly debugMode: boolean = false,
51+
private readonly options: Options = {
52+
debugMode: false,
53+
forceFreshAuth: false,
54+
expirySkewSeconds: 30,
55+
},
3056
) {}
3157

3258
/**
@@ -35,14 +61,20 @@ export class ClientCredentials {
3561
* @returns {Promise<string>} - The access token
3662
*/
3763
public async getAccessToken(): Promise<string> {
38-
if (!ClientCredentials.accessToken || this.isAccessTokenExpired()) {
64+
if (
65+
this.options?.forceFreshAuth ||
66+
!ClientCredentials.accessToken ||
67+
ClientCredentials.isAccessTokenExpired(
68+
this.options.expirySkewSeconds ?? 30,
69+
)
70+
) {
3971
const { access_token } = await this.fetchClientCredentials();
4072

41-
if (this.debugMode)
73+
if (this.options?.debugMode)
4274
console.log("[DEBUG] New access token fetched:", access_token);
4375

4476
ClientCredentials.accessToken = access_token;
45-
} else if (this.debugMode) {
77+
} else if (this.options?.debugMode) {
4678
console.log(
4779
"[DEBUG] Using existing access token:",
4880
ClientCredentials.accessToken,
@@ -94,20 +126,22 @@ export class ClientCredentials {
94126
* @returns {boolean} - Whether the access token is expired
95127
* @private
96128
*/
97-
private isAccessTokenExpired(): boolean {
98-
try {
99-
const decodedAccessToken = decodeJwt(ClientCredentials.accessToken);
129+
private static isAccessTokenExpired(skewSeconds: number): boolean {
130+
let decodedAccessToken: JWTPayload;
100131

101-
const currentTime = new Date().getTime() / 1000;
102-
103-
// Check if exp exists before comparing as it can be undefined
104-
if (!decodedAccessToken?.exp) {
105-
return true;
106-
}
107-
return currentTime > decodedAccessToken.exp;
132+
try {
133+
decodedAccessToken = decodeJwt(ClientCredentials.accessToken);
108134
} catch (err) {
109135
console.error("Error decoding access token:", err);
110136
return true;
111137
}
138+
139+
const currentTime = Math.floor(Date.now() / 1000);
140+
141+
const exp = decodedAccessToken?.exp;
142+
if (!exp) return true;
143+
144+
// treat as expired if we're within the skew window
145+
return currentTime >= exp - skewSeconds;
112146
}
113147
}

0 commit comments

Comments
 (0)