Appearance
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:
ts
async ({
id,
isInvalid,
prevToken,
refreshToken,
context,
getCredentials,
getCredentialsOrThrow,
}) => {
const { accessToken, refreshToken } = await getToken();
return {
token: accessToken,
refreshToken
};
}TIP
Token providers can omit refresh tokens:
ts
async () => {
const { accessToken, refreshToken } = await getToken();
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.
ts
async ({
/* ... */
getCredentials,
getCredentialsOrThrow,
}) => {
const { email, password } = await getCredentialsOrThrow();
const { accessToken, refreshToken } = await getToken({
args: { email, password }
});
return {
token: accessToken,
refreshToken
};
}Configuring token holders
To configure a token holder, call the token() function obtained from the Alette Signal access control plugin, and pass a token provider to the .from() token holder builder method:
api/baseAuth.ts
ts
import { coreAuthPlugin } from "@alette/signal";
export const auth = coreAuthPlugin();
export const { token, cookie } = auth.use();api/auth.ts
ts
import { coreAuthPlugin, path, as } from "@alette/signal";
import { mutation } from './base.ts'
// as() can be replaced with Zod.
const Credentials = as<{ email: string, password: string; }>();
const TokenOutput = as<{ accessToken: string, refreshToken: string; }>();
const getTokens = baseMutation.with(
input(Credentials),
output(TokenOutput),
path('/token'),
body(({ args }) => args)
);
export const jwt = token()
.from(async () => {
const token = await getToken();
return token;
})
.build();Return a record from the token provider to save both the token and the refresh token inside the token holder:
ts
export const jwt = token()
.from(async () => {
const { accessToken, refreshToken } = await getToken();
return {
token: accessToken,
refreshToken
};
})
.build();DANGER
- If a non-string value is returned to act as a token, the
TokenTypeValidationErrorfatal error will be thrown. - If a non-string value is returned to act as a refresh token, the
RefreshTokenTypeValidationErrorfatal error will be thrown.
TIP
The initial token provider can be overridden:
ts
export const thirdPartyToken = token()
.from(() => {
console.error('Third party token provider was not implemented.');
return '';
})
.build();
// Overrides the initial token provider
thirdPartyToken.from(() => getToken())TIP
Alette Signal allows for multiple token holders in the same application:
ts
export const jwt = token()
.from(() => getToken())
.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:
ts
const jwt = token()
.credentials(z.object({
email: z.string(),
name: z.string()
}))
.from(() => getToken())
.build();To access token credentials, destructure the first token provider argument:
ts
const jwt = token()
.credentials(z.object({
email: z.string(),
password: z.string()
}))
.from(async ({
getCredentials,
getCredentialsOrThrow,
}) => {
const { email, password } = await getCredentialsOrThrow();
return getToken({ args: { email, password } })
})
.build();Setting token credentials
To set token credentials, call the .using() token holder method:
ts
jwt.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:
ts
jwt.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:
tsx
// React component
const Form = () => {
// Pseudo code
const { subscribe, /* ... */ } = useForm();
useEffect(
() => subscribe(({ values: { email, password } }) => {
jwt.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:
ts
const myJwtToken = await jwt.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:
ts
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.
jwt.get(),
jwt.get(),
jwt.get(),
jwt.get(),
jwt.get(),
])Invalidating tokens
To refresh a token, call the .refresh() method on the token holder:
ts
jwt.invalidate();Next time the .get() method is called, the token is re-obtained:
ts
const newToken = await jwt.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:
ts
jwt.refresh();To refresh and get the token simultaneously, call the .refreshAndGet() method on the token holder:
ts
const newToken = await jwt.refreshAndGet();Subscribing to token changes
To subscribe to token changes, call the .onStatus() token holder method:
ts
const unsubscribe = jwt.onStatus({
loading: async ({ context }) => {
// ...
},
valid: async ({ context }) => {
// ...
},
invalid: async ({ context }) => {
// ...
},
});TIP
Token holders allow subscribing to a subset of the token status events:
ts
jwt.onStatus({
valid: () => {
// ...
},
});TIP
Token status subscriptions can be used to synchronize UI and token updates:
tsx
// React component
const AuthScreen = () => {
const [isTokenValid, setIsValid] = useState(false);
useEffect(() => {
return jwt.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:
ts
const jwt = 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:
ts
const tokenValue = 'hey';
const jwt = token()
.from(() => tokenValue)
.build();
/**
* authHeaders - { Authorization: `Bearer ${tokenValue}` },
* */
const authHeaders = await jwt.toHeaders()To change how a token is converted to HTTP headers, pass a function to the .whenConvertedToHeaders() token holder builder method:
ts
const tokenValue = 'hey';
const jwt = token()
.from(() => tokenValue)
.whenConvertedToHeaders(({ token, context }) => ({
'X-XSRF-TOKEN': token
}))
.build();
/**
* authHeaders -
* {
* 'X-XSRF-TOKEN': 'hey'
* },
* */
const authHeaders = await jwt.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.