Sign in and recover accounts
Requires: A secret key with
MEMBER_AUTH, called from your server.
Every route here needs a secret key with MEMBER_AUTH, so every call runs
on your server. A publishable key answers 403 SECRET_KEY_REQUIRED.
POST /api/v1/communities/:communityTag/auth/sign-in
POST /api/v1/communities/:communityTag/auth/sign-up
POST /api/v1/communities/:communityTag/auth/email-link
POST /api/v1/communities/:communityTag/auth/email-link/verify
GET /api/v1/communities/:communityTag/auth/me
POST /api/v1/communities/:communityTag/auth/password-reset
GET /api/v1/communities/:communityTag/auth/password-reset/validate
POST /api/v1/communities/:communityTag/auth/password-reset/confirm
POST /api/v1/communities/:communityTag/auth/verify-email
POST /api/v1/communities/:communityTag/me/verify-email/resend
POST /api/v1/communities/:communityTag/auth/oauth/exchangeAt a glance
curl -X POST "https://api.cobuntu.com/api/v1/communities/your-community/auth/sign-in" \
-H "X-API-Key: $COBUNTU_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"alex@example.com","password":"correct horse battery"}'
# { "token": "...", "user": { ... }, "newMember": false }The session
Every sign-in path ends in the same answer:
{ "token": "eyJhbGciOi...", "user": { "id": "usr_123", "name": "Alex Doe", "email": "alex@example.com", "avatar": null }, "newMember": false }token is a 30-day session bound to this one community. Store it in your own
cookie and never expose it to page scripts:
cookies().set("member_session", token, {
httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 30,
});
const member = cobuntu.as(token); // sends X-Cobuntu-Auth-TokenOn 401 INVALID_SESSION from any member call, clear the cookie and send the
visitor to sign in.
Sign in and sign up
const s = await cobuntu.auth.signIn({ email, password });
const s2 = await cobuntu.auth.signUp({ name, email, password }, { endUserIp: visitorIp });| Code | Show |
|---|---|
INVALID_CREDENTIALS | "Email or password is incorrect." |
SUSPENDED | The account is suspended. |
APPLICATION_PENDING | Their application is still under review. |
NOT_MEMBER | Not a member: offer the apply form. |
WEAK_PASSWORD, EMAIL_TAKEN | Sign-up form errors. |
APPLICATION_REQUIRED | The community reviews applications: use the apply flow. |
RATE_LIMITED | Too many attempts. |
Signing in to an open community joins it (newMember: true). Pass
endUserIp on sign-up: its limit is per visitor IP, and every call comes from
your server.
Sign-in links
await cobuntu.auth.emailLink({ email, redirectPath: "/account" }); // always { ok: true }The answer never reveals whether the account exists. The email links to your
site with a token (valid 15 minutes); exchange it on your server:
const s = await cobuntu.auth.verifyEmailLink(token); // LINK_EXPIRED, INVALID_LINKWho is signed in
const me = await cobuntu.as(session).auth.me();
// { user: {...}, member: true, segments: [{ id: "seg_pro", name: "Pro" }] }member is true for an accepted membership; segments are the active tiers.
Password recovery
// 1. Request: always { ok: true }
await cobuntu.auth.passwordReset(email);
// 2. The email links to your page with ?token=. Check it before showing the form.
await cobuntu.auth.validatePasswordReset(token); // { valid: true }, does not consume
// 3. Set the new password. Consumes the token and returns a fresh session.
const s = await cobuntu.auth.confirmPasswordReset({ token, password });Codes: LINK_EXPIRED, INVALID_LINK, WEAK_PASSWORD, RATE_LIMITED
(3 requests per 15 minutes per email).
Email verification
The verification email links to your site with a token:
await cobuntu.auth.verifyEmail(token); // { verified: true }; LINK_EXPIRED, INVALID_LINK, EMAIL_TAKENA signed-in member can ask for a new one:
await cobuntu.as(session).me.verifyEmailResend(); // { sent: true }; ALREADY_VERIFIED, RATE_LIMITEDContinue with Google
Two routes on your site and a PKCE pair. The verifier stays on your server.
import { createPkcePair } from "@cobuntu/embed";
// GET /api/auth/google/start
const { codeVerifier, codeChallenge } = await createPkcePair();
const state = (await createPkcePair()).codeVerifier;
cookies().set("google_oauth", `${state}.${codeVerifier}`, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600 });
return redirect(cobuntu.auth.googleStartUrl({
returnUrl: `${origin}/api/auth/google/callback`,
codeChallenge,
state,
}));
// GET /api/auth/google/callback?code=&state= (or ?error=)
const [savedState, codeVerifier] = cookies().get("google_oauth")!.value.split(".");
cookies().delete("google_oauth");
if (searchParams.get("state") !== savedState) return redirect("/login?error=state");
if (searchParams.get("error")) return redirect(`/login?error=${searchParams.get("error")}`);
const { token, user, isSignUp } = await cobuntu.auth.exchangeOAuthCode({
code: searchParams.get("code")!,
codeVerifier,
});returnUrlmust be on the community's site origin (localhostis allowed while developing).- The code lives 60 seconds and works once. Every invalid, expired, reused or
mismatched code, and a wrong verifier, answers the same
400 INVALID_CODE, so codes cannot be probed. Start the flow again. 403 SUSPENDEDfor a suspended account.isSignUpis true when this Google sign-in created the account; use it for a welcome step.- No membership is created and
newMemberis alwaysfalse. Who may sign in follows the community's access rules; refusals come back to your callback as?error=(for exampleNOT_MEMBER,APPLICATION_PENDING).