Authentication
Sign-up and email verification
Hosted and server-side sign-up, and the emailed verification code.
Hosted sign-up#
Link to /api/auth/signup (or use SignUpButton). The hosted page takes an email and password and emails a six-digit code. Once the code is confirmed, the page returns to your redirect URI. When sign-up is turned off for the environment, the hosted page says so and the API refuses it (403).
Server-side sign-up#
handleAuth also accepts POST /api/auth/sign-up with { email, password, firstName?, lastName? }, and AuthClient.signUp does the same from any server. Both create the user and send the code, and return an authorization_session_id. The code is then confirmed on the hosted verification page.
// Server-side calls with AuthClient, for flows you drive yourself.
import { AuthClient } from '@3een/auth';
import type { AuthError } from '@3een/auth';
const apiUrl = process.env.AUTH_API_URL!;
const apiKey = process.env.AUTH_API_KEY!; // secret: server only
const client = new AuthClient({
clientId: process.env.AUTH_CLIENT_ID!,
apiKey,
apiUrl,
redirectUri: 'https://app.example.com/dashboard',
});
/** The environment your API key belongs to (AuthClient methods need its id). */
export async function environmentId(): Promise<string> {
const res = await fetch(`${apiUrl}/app/auth/validate-key`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error('The API key was rejected');
const body = (await res.json()) as { environmentId: string };
return body.environmentId;
}
/** AuthClient throws an Error carrying the API's status and code. */
function describe(error: unknown): string {
const e = error as Partial<AuthError> & Error;
if (e.statusCode === 401) return 'Invalid email or password';
if (e.statusCode === 429) return 'Too many attempts. Try again in a minute.';
return e.message || 'Something went wrong';
}
export async function signUp(email: string, password: string) {
try {
// Sends a verification code by email; the user finishes on the hosted
// verification page with the returned authorization_session_id.
const { authorization_session_id } = await client.signUp({ email, password, environmentId: await environmentId() });
return { ok: true as const, authorizationSessionId: authorization_session_id };
} catch (error) {
return { ok: false as const, message: describe(error) };
}
}
export async function signIn(email: string, password: string) {
try {
const { accessToken, refreshToken, expiresIn, user } = await client.signIn({
email,
password,
environmentId: await environmentId(),
});
// Store the tokens in httpOnly cookies; never hand them to browser JavaScript.
return { ok: true as const, accessToken, refreshToken, expiresIn, user };
} catch (error) {
return { ok: false as const, message: describe(error) };
}
}
export async function refresh(refreshToken: string) {
const { accessToken, refreshToken: next, expiresIn } = await client.refreshToken(refreshToken);
return { accessToken, refreshToken: next ?? refreshToken, expiresIn };
}
export async function forgotPassword(email: string) {
// Always the same answer, whether or not the address has an account.
// The email links to your hosted reset-password page.
await client.forgotPassword(email, await environmentId());
}
export async function signOut(accessToken: string) {
await client.signOut(accessToken); // revokes the session server-side
}Email verification#
- The code is six digits and expires after 10 minutes.
- At most three codes can be sent to an address in 10 minutes; a new code resets the countdown.
- A code is accepted only for the sign-up it was issued for: its authorization session, its environment and its kind of account.
Password policy#
8 to 128 characters, not the account's own email, and not found in known breaches. The breach check sends only the first five characters of the password's SHA-1 hash (k-anonymity), and is skipped, not failed, if the breach service is unreachable. There are no composition rules.