Token holder
A token holder in Alette Signal is an access control helper, storing a value acting as a JWT or OAuth token and a value acting as a refresh token, while managing their lifecycle.
Token provider
A token provider is a synchronous or asynchronous function, returning a value acting as a JWT or OAuth token and a value acting as a refresh token to the token holder:
async ({
id,
prevToken,
refreshToken,
context,
getCredentials,
getCredentialsOrThrow,
}) => {
const { accessToken, refreshToken } = await getToken.execute();
return {
token: accessToken,
refreshToken
};
}
TIP
Token providers can omit refresh tokens:
async () => {
const { accessToken, refreshToken } = await getToken.execute();
return accessToken;
}
Token credentials
Token credentials is an arbitrary data stored inside a token holder, and is used by token providers to obtain or refresh tokens.
async ({
/* ... */
getCredentials,
getCredentialsOrThrow,
}) => {
const { email, password } = await getCredentialsOrThrow();
const { accessToken, refreshToken } = await getToken.execute({
args: { email, password }
});
return {
token: accessToken,
refreshToken
};
}
Configuring token holders
To configure a token holder, call the token()
function obtained from the Alette Signal core plugin, and pass a token provider to the .from()
token holder builder method:
// ./src/api/base.ts
const core = coreApiPlugin();
export const { token } = core.use();
// ./src/api/auth.ts
// ...
const getToken = mutation(/* ... */)
export const jwtToken = token()
.from(async () => {
const token = await getToken.execute();
return token;
})
.build();
Return a record from the token provider to save both the token and the refresh token inside the token holder:
export const jwtToken = token()
.from(async () => {
const { accessToken, refreshToken } = await getToken.execute();
return {
token: accessToken,
refreshToken
};
})
.build();
DANGER
- If a non-string value is returned to act as a token, the
TokenTypeValidationError
fatal error will be thrown. - If a non-string value is returned to act as a refresh token, the
RefreshTokenTypeValidationError
fatal error will be thrown.
TIP
The initial token provider can be overridden:
export const thirdPartyToken = token()
.from(() => {
console.error('Third party token provider was not implemented.');
return '';
})
.build();
// Overrides the initial token provider
thirdPartyToken.from(() => getToken.execute())
TIP
Alette Signal allows for multiple token holders in the same application:
export const jwtToken = token()
.from(() => getToken.execute())
.build();
export const aiKey = token()
.from(() => process['env']['AI_KEY'])
.build();
Configuring credential storage
To configure the credential storage of a token holder, pass a runtime schema implementing the Standard Schema interface to the .credentials()
token holder builder method:
const jwtToken = token()
.credentials(z.object({
email: z.string(),
name: z.string()
}))
.from(() => getToken.execute())
.build();
To access token credentials, destructure the first token provider argument:
const jwtToken = token()
.credentials(z.object({
email: z.string(),
password: z.string()
}))
.from(async ({
getCredentials,
getCredentialsOrThrow,
}) => {
const { email, password } = await getCredentialsOrThrow();
return getToken.execute({ args: { email, password } })
})
.build();
Setting token credentials
To set token credentials, call the .using()
token holder method:
jwtToken.using({
email: 'alette-signal@mail.com',
password: '12345',
})
To set token credentials while accessing previous credentials, pass a function to the .using()
token holder method:
jwtToken.using(({ previous, context }) => {
const previousPassword = previous?.password;
return {
email: 'alette-signal@mail.com',
password: previousPassword || '12345',
};
})
TIP
The .using()
token holder method can be used with UI forms:
// React component
const Form = () => {
// Pseudo code
const { subscribe, /* ... */ } = useForm();
useEffect(
() => subscribe(({ values: { email, password } }) => {
jwtToken.using({ email, password });
}),
[]
);
return <form>{ /* ... */ }</form>
}
Obtaining tokens
To obtain a token from the server, invoke the token provider by calling the .get()
method on the token holder:
const myJwtToken = await jwtToken.get();
TIP
- Only a single
.get()
call is made, while other calls to the same.get()
method are queued. - If the token provider resolves successfully, the token value is propagated to the queued
.get()
callers without invoking the token provider again:
const [
token1,
token2,
token3,
token4,
token5,
] = await Promise.all([
// Only one server request is made,
// and every pending `get()` invocation
// receive the same token without
// calling the server again.
jwtToken.get(),
jwtToken.get(),
jwtToken.get(),
jwtToken.get(),
jwtToken.get(),
])
Invalidating tokens
To refresh a token, call the .refresh()
method on the token holder:
jwtToken.invalidate();
Next time the .get()
method is called, the token is re-obtained:
const newToken = await jwtToken.get();
WARNING
The .invalidate()
token holder method does not call the token provider automatically.
Refreshing tokens
To refresh a token in the background, call the .refresh()
method on the token holder:
jwtToken.refresh();
To refresh and get the token simultaneously, call the .refreshAndGet()
method on the token holder:
const newToken = await jwtToken.refreshAndGet();
Subscribing to token changes
To subscribe to token changes, call the .onStatus()
token holder method:
const unsubscribe = jwtToken.onStatus({
loading: async ({ context }) => {
// ...
},
valid: async ({ context }) => {
// ...
},
invalid: async ({ context }) => {
// ...
},
});
TIP
Token holders allow subscribing to a subset of the token status events:
jwtToken.onStatus({
valid: () => {
// ...
},
});
TIP
Token status subscriptions can be used to synchronize UI and token updates:
// React component
const AuthScreen = () => {
const [isTokenValid, setIsValid] = useState(false);
useEffect(() => {
return jwtToken.onStatus({
loading: async ({ context }) => {},
valid: async ({ context }) => {},
invalid: async ({ context }) => {},
})
}, []);
if (isTokenValid) {
return <div>{ /*...*/ }</div>;
}
return <div>Unauthorized</div>;
}
Periodic token refresh
To set up periodic token refresh in the background, pass an interval value to the .refreshEvery()
token holder builder method:
const jwtToken = token()
/* ... */
.refreshEvery("20 seconds")
// or
.refreshEvery(5000)
// or
.refreshEvery("1 hour")
.build();
INFO
Periodic token refresh uses the token obtaining algorithm under the hood.
Converting tokens to headers
To convert a token to HTTP headers, use the .toHeaders()
token holder method:
const tokenValue = 'hey';
const jwtToken = token()
.from(() => tokenValue)
.build();
/**
* authHeaders - { Authorization: `Bearer ${tokenValue}` },
* */
const authHeaders = await jwtToken.toHeaders()
To change how a token is converted to HTTP headers, pass a function to the .whenConvertedToHeaders()
token holder builder method:
const tokenValue = 'hey';
const jwtToken = token()
.from(() => tokenValue)
.whenConvertedToHeaders(({ token, context }) => ({
'X-XSRF-TOKEN': token
}))
.build();
/**
* authHeaders -
* {
* 'X-XSRF-TOKEN': 'hey'
* },
* */
const authHeaders = await jwtToken.toHeaders()
INFO
Alette Signal automatically obtains tokens being converted into headers.
Token obtaining algorithm
Alette Signal token obtaining algorithm has 6 steps:
- The
.get()
method of the token holder is called. - If a valid token is stored inside the token holder, the token provider is not called, and the valid token is returned.
- If an invalid token is stored inside the token holder, the token provider is invoked.
- If a token is absent from the token holder, the token provider is invoked.
- If the token provider throws an error, the token stored inside the token holder is marked as invalid.
- If the token provider succeeds, the token is marked as valid and replaces the old token stored inside the token holder.