Skip to content

Auth & token refresh

Two common needs: attach an auth token to every request, and transparently refresh it when it expires. Both are plugins.

Attaching a token

The simplest case, a static token, is just a header on the client:

ts
const 
api
=
corgi
.
create
({
baseURL
: 'https://api.example.com',
headers
: {
authorization
: `Bearer ${
token
}` },
});

For a token that changes at runtime, use a plugin so it's read fresh on every call. Slot it just outside retry (ORDER.retry - 1) so it's applied once per logical call, not re-applied on every retry attempt:

ts
const 
withAuth
=
order
(
ORDER
.
retry
- 1, (
next
:
Fetcher
):
Fetcher
=> (
url
,
init
) => {
const
headers
= new
Headers
(
init
?.
headers
);
const
token
=
tokenStore
.
get
();
if (
token
)
headers
.
set
('authorization', `Bearer ${
token
}`);
return
next
(
url
, { ...
init
,
headers
});
}); const
api
=
corgi
.
create
({
baseURL
: 'https://api.example.com',
plugins
: [
withAuth
] });

Refresh on 401, exactly once

Extend the plugin: if a response comes back 401, refresh the token and replay the request once. Because a plugin sees the raw Response, this is straightforward:

ts
const 
withAuthRefresh
=
order
(
ORDER
.
retry
- 1, (
next
:
Fetcher
):
Fetcher
=> async (
url
,
init
) => {
const
call
= (
token
: string | undefined) => {
const
headers
= new
Headers
(
init
?.
headers
);
if (
token
)
headers
.
set
('authorization', `Bearer ${
token
}`);
return
next
(
url
, { ...
init
,
headers
});
}; let
res
= await
call
(
tokenStore
.
get
());
if (
res
.
status
=== 401) {
res
.
body
?.
cancel
().
catch
(() => {}); // release the 401 body's connection
const
fresh
= await
refreshToken
();
tokenStore
.
set
(
fresh
);
res
= await
call
(
fresh
); // replay once with the new token
} return
res
;
});

Single-flight the refresh

If many requests get a 401 at once, you don't want a refresh storm. Cache the in-flight refresh promise so concurrent callers share one refresh:

ts
let 
refreshing
:
Promise
<string> | undefined;
function
refreshToken
():
Promise
<string> {
refreshing
??=
doRefresh
().
finally
(() => (
refreshing
=
undefined
));
return
refreshing
;
}

Scoped sub-clients with extend

A common shape: one authenticated base client, several feature clients that inherit its auth and add their own defaults. extend combines headers and plugins rather than replacing them:

ts
const 
base
=
corgi
.
create
({
baseURL
: 'https://api.example.com',
plugins
: [
withAuth
] });
const
billing
=
base
.
extend
({
headers
: { 'x-scope': 'billing' } }); // keeps auth
const
admin
=
base
.
extend
({
headers
: { 'x-scope': 'admin' } }); // keeps auth

Released under the MIT License.