Skip to content

Commit 1022738

Browse files
committed
fix bugs in token refresh, add tokenStore example
1 parent 83b851b commit 1022738

File tree

4 files changed

+118
-37
lines changed

4 files changed

+118
-37
lines changed

gusto_embedded/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,38 @@ For supported JavaScript runtimes, please consult [RUNTIMES.md](RUNTIMES.md).
9797
## SDK Example Usage
9898

9999
### Example
100+
Automatic token refresh using a persistent data store.
100101

101102
```typescript
102103
import { GustoEmbedded } from "@gusto/embedded-api";
103104
import { CompanyAuthenticatedClient } = "@gusto/embedded-api/CompanyAuthenticatedClient";
104105

106+
class PersistentTokenStore implements TokenStore {
107+
constructor() {}
108+
109+
async get() {
110+
const { token, expires, refreshToken } = retrieveToken();
111+
112+
return {
113+
token,
114+
expires,
115+
refreshToken,
116+
};
117+
}
118+
119+
async set({
120+
token,
121+
expires,
122+
refreshToken,
123+
}: {
124+
token: string;
125+
expires: number;
126+
refreshToken: string;
127+
}) {
128+
saveToken(token, refreshToken, expires);
129+
}
130+
}
131+
105132
const client = new GustoEmbedded();
106133
const clientId = process.env["GUSTOEMBEDDED_CLIENT_ID"]
107134
const clientSecret = process.env["GUSTOEMBEDDED_CLIENT_SECRET"];
@@ -132,6 +159,8 @@ async function run() {
132159

133160
const { accessToken, refreshToken, companyUuid, expiresIn } = response.object;
134161

162+
const tokenStore = PersistentTokenStore();
163+
135164
const companyAuthClient = CompanyAuthenticatedClient({
136165
clientId,
137166
clientSecret,
@@ -140,6 +169,7 @@ async function run() {
140169
expiresIn,
141170
options: {
142171
server: "demo",
172+
tokenStore,
143173
},
144174
});
145175

gusto_embedded/src/CompanyAuthenticatedClient.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { TokenRefreshOptions, withTokenRefresh } from "./companyAuth.js";
2-
import { GustoEmbeddedCore } from "./core.js";
1+
import { HTTPClient } from "./lib/http.js";
2+
import {
3+
InMemoryTokenStore,
4+
refreshAndSaveAuthToken,
5+
TokenRefreshOptions,
6+
withTokenRefresh,
7+
} from "./companyAuth.js";
8+
import { GustoEmbedded } from "./sdk/sdk.js";
39
import { SDKOptions, ServerDemo, ServerList } from "./lib/config.js";
410

511
type ClientArguments = {
@@ -20,16 +26,33 @@ export function CompanyAuthenticatedClient({
2026
options,
2127
}: ClientArguments) {
2228
const authUrl = constructAuthUrl(options);
29+
const tokenStore = new InMemoryTokenStore();
2330

24-
return new GustoEmbeddedCore({
31+
const httpClientWithTokenRefresh = new HTTPClient();
32+
33+
httpClientWithTokenRefresh.addHook("response", async (res) => {
34+
if (res.status === 401) {
35+
console.log("Unauthorized, attempting to refresh token");
36+
37+
await refreshAndSaveAuthToken(
38+
authUrl,
39+
{ clientId, clientSecret, refreshToken },
40+
tokenStore
41+
);
42+
}
43+
});
44+
45+
return new GustoEmbedded({
2546
...options,
47+
httpClient: httpClientWithTokenRefresh,
2648
companyAccessAuth: withTokenRefresh(
2749
clientId,
2850
clientSecret,
2951
accessToken,
3052
refreshToken,
3153
expiresIn,
3254
{
55+
tokenStore,
3356
...options,
3457
url: authUrl,
3558
}

gusto_embedded/src/companyAuth.ts

+56-34
Original file line numberDiff line numberDiff line change
@@ -49,43 +49,65 @@ export function withTokenRefresh(
4949
return session.token;
5050
}
5151

52-
try {
53-
const response = await fetch(url, {
54-
method: "POST",
55-
headers: {
56-
"content-type": "application/x-www-form-urlencoded",
57-
// Include the SDK's user agent in the request so requests can be
58-
// tracked using observability infrastructure.
59-
"user-agent": SDK_METADATA.userAgent,
60-
},
61-
body: new URLSearchParams({
62-
client_id: clientId,
63-
client_secret: clientSecret,
64-
grant_type: "refresh_token",
65-
refresh_token: refreshToken,
66-
}),
67-
});
68-
69-
if (!response.ok) {
70-
throw new Error("Unexpected status code: " + response.status);
71-
}
72-
73-
const json = await response.json();
74-
const data = tokenResponseSchema.parse(json);
75-
76-
await tokenStore.set({
77-
token: data.access_token,
78-
expires: Date.now() + data.expires_in * 1000 - tolerance,
79-
refreshToken: data.refresh_token,
80-
});
81-
82-
return data.access_token;
83-
} catch (error) {
84-
throw new Error("Failed to obtain OAuth token: " + error);
85-
}
52+
return await refreshAndSaveAuthToken(
53+
url,
54+
{
55+
clientId,
56+
clientSecret,
57+
refreshToken: session?.refreshToken ?? refreshToken,
58+
},
59+
tokenStore
60+
);
8661
};
8762
}
8863

64+
export async function refreshAndSaveAuthToken(
65+
authUrl: string,
66+
refreshCredentials: {
67+
clientId: string;
68+
clientSecret: string;
69+
refreshToken: string;
70+
},
71+
tokenStore: TokenStore
72+
): Promise<string> {
73+
const { clientId, clientSecret, refreshToken } = refreshCredentials;
74+
75+
try {
76+
const response = await fetch(authUrl, {
77+
method: "POST",
78+
headers: {
79+
"content-type": "application/x-www-form-urlencoded",
80+
// Include the SDK's user agent in the request so requests can be
81+
// tracked using observability infrastructure.
82+
"user-agent": SDK_METADATA.userAgent,
83+
},
84+
body: new URLSearchParams({
85+
client_id: clientId,
86+
client_secret: clientSecret,
87+
grant_type: "refresh_token",
88+
refresh_token: refreshToken,
89+
}),
90+
});
91+
92+
if (!response.ok) {
93+
throw new Error("Unexpected status code: " + response.status);
94+
}
95+
96+
const json = await response.json();
97+
const data = tokenResponseSchema.parse(json);
98+
99+
await tokenStore.set({
100+
token: data.access_token,
101+
expires: Date.now() + data.expires_in * 1000 - tolerance,
102+
refreshToken: data.refresh_token,
103+
});
104+
105+
return data.access_token;
106+
} catch (error) {
107+
throw new Error("Failed to obtain OAuth token: " + error);
108+
}
109+
}
110+
89111
/**
90112
* A TokenStore is used to save and retrieve OAuth tokens for use across SDK
91113
* method calls. This interface can be implemented to store tokens in memory,

gusto_embedded/src/hooks/clientcredentials.ts

+6
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ export class ClientCredentialsHook
164164
if (typeof source === "function") {
165165
security = await source();
166166
}
167+
168+
// The client was passed a raw access token, no need to fetch one.
169+
if (typeof security === "string") {
170+
return null;
171+
}
172+
167173
const out = parse(
168174
security,
169175
(val) =>

0 commit comments

Comments
 (0)