Signing in

Every sign-in method, two-factor, OAuth, bot checks, organizations, the user's own account and signing out, with @authoric/js.

Every way in answers the same way: a session, or a second factor still owed. The result is typed so you handle both.

const result = await authoric.auth.signInWithPassword({ email, password });

if (result.mfa) {
  // result.mfa.factors says which factors the user has; result.mfa.activeKind which one was asked
  const done = await authoric.auth.mfa.verify({
    challengeId: result.mfa.challengeId,
    code,
    rememberDevice: true, // skip the second factor on this device next time
  });
  // done.session, or done.mfa again for a wrong code
} else {
  console.log(result.session.user.email, result.session.activeOrgUid);
}

Which methods your app offers, for rendering the sign-in page before anyone signs in:

const methods = await authoric.auth.methods();
// { password, magicLink, emailOtp, oauth, signup, botCheck }

The methods

MethodCall
Passwordauth.signUp({ email, password, fname?, lname?, verifyRedirectTo? }), auth.signInWithPassword({ email, password })
Password resetauth.requestPasswordReset({ email, redirectTo? }), then auth.confirmPasswordReset({ token, newPassword })
Magic linkauth.magicLink.request({ email, redirectTo? }), then on the link's page auth.magicLink.verify({ token })
Email codeconst { requestId } = await auth.otp.request({ email }), then auth.otp.verify({ requestId, code })
OAuthauth.oauth.redirect(provider, { redirectTo }), then on that page auth.oauth.handleCallback()

Requests that send an email always answer the same way, whether or not the address has an account.

redirectTo must be on your app's allowed domains, or localhost.

Signing up

A password sign-up is sent a "confirm your email" link. verifyRedirectTo is the page of yours it opens, and that page confirms the address with the token the link carries:

const result = await authoric.auth.signUp({
  email,
  password,
  verifyRedirectTo: "https://app.example.com/confirm-email",
});

if (result.verificationRequired) {
  showCheckYourEmail(result.email); // no session yet: the app requires a confirmed address
} else {
  console.log(result.session.user.email); // signed in
}

// On https://app.example.com/confirm-email:
const token = new URL(location.href).searchParams.get("token");
await authoric.auth.verifyEmail.confirm({ token }); // works signed out

signUp answers { session }, or { verificationRequired: true, email } when your app requires a confirmed address before anyone signs in; then a password sign-in throws EMAIL_NOT_VERIFIED until the link is followed. Otherwise the user is signed in at once, and auth.verifyEmail.request() sends the link again. Until the address is confirmed, the user cannot send invitations or create organizations (EMAIL_NOT_VERIFIED). auth.getUser() says whether it is (user.emailVerified).

OAuth

// On your sign-in page:
await authoric.auth.oauth.redirect("google", { redirectTo: "https://app.example.com/auth/callback" });

// On https://app.example.com/auth/callback:
const { result, redirectTo } = await authoric.auth.oauth.handleCallback();

handleCallback reads the result from the address, exchanges its single-use code for a session, and removes the code from the address bar and history. A failed sign-in throws an AuthoricError whose code says why (for example OAUTH_EMAIL_UNVERIFIED).

Bot checks

When your app's bot check is on, auth.methods() returns its Turnstile site key. Render the widget with it, and pass its token to the guarded calls (sign-up, sign-in, password reset, magic link and email code requests):

await authoric.auth.signUp({ email, password, botCheckToken: turnstileToken });

Two-factor

For the signed-in user:

await authoric.auth.mfa.list(); // their factors

// An authenticator app: show `keyUri` as a QR code (and `secret` for typing in), then confirm a code.
const { secret, keyUri } = await authoric.auth.mfa.enroll({ kind: "totp" });
await authoric.auth.mfa.confirm({ kind: "totp", code });

// A phone: a code is texted to it, then confirmed with the enrollment it belongs to.
const { enrollmentId } = await authoric.auth.mfa.enroll({ kind: "sms", phone: "+15555550123" });
await authoric.auth.mfa.confirm({ kind: "sms", code, enrollmentId });

await authoric.auth.mfa.regenerateRecoveryCodes(); // show them once
await authoric.auth.mfa.forgetDevices(); // remembered devices ask again

When an organization requires two-factor and the user has none, the session says mfaEnrollmentRequired: true and reaches only enrollment. After they add a factor, call auth.refresh() for a full session.

Organizations

A session acts in one organization at a time, session.activeOrgUid. Every org-scoped read and write is in that organization.

const session = await authoric.auth.switchOrg(orgUid);

Members, invitations and roles: Teams and roles.

The user's own account

const me = await authoric.auth.getUser(); // asks the API: { user, activeOrgUid, org }

await authoric.auth.updateProfile({ fname: "Dana", lname: "Reyes" });
await authoric.auth.changePassword({ currentPassword, newPassword }); // other devices are signed out

changePassword signs out every other device and emails the user that the password changed. An account with no password (one that signs in with Google, GitHub or a sign-in link) gets PASSWORD_NOT_SET: send it the password reset email instead.

Proving it is still you

Changing the email address and deleting the account each need a fresh proof first. reverify takes exactly one of a password, a two-factor code, or a code emailed to the user, and answers a receipt that works once, for ten minutes, from this session only:

const { reverifyToken } = await authoric.auth.reverify({ password });
// or: await authoric.auth.reverify({ mfaCode, mfaKind: "totp" })
// or: const { requestId } = await authoric.auth.reverify.emailCode();
//     await authoric.auth.reverify({ requestId, code }) // the 6-digit code from the email

A failed proof throws REVERIFY_REQUIRED, and its details.methods lists the proofs this account can give (password, totp, sms, email), so you can show the right prompt.

Changing the email address

await authoric.auth.changeEmail({
  newEmail: "dana.reyes@example.com",
  reverifyToken,
  redirectTo: "https://app.example.com/confirm-new-email",
}); // { sent: true }

// On https://app.example.com/confirm-new-email (signed in or not):
const token = new URL(location.href).searchParams.get("token");
const { email } = await authoric.auth.changeEmail.confirm({ token });

A link goes to the new address and a notice to the current one. The answer is the same whether or not the new address already has an account. Once the link is followed, the account moves, and every session except the one that asked ends.

Deleting the account

const { orgsDeleted } = await authoric.auth.deleteAccount({ reverifyToken }); // signs out here too

The account's personal details are erased and every way in is removed. An organization the user was the only member of is deleted; its rows are kept for you. The address can sign up again. It throws ACCOUNT_LAST_OWNER (with details.orgs) while the user is the last owner of an organization with other members, ACCOUNT_HAS_SUBSCRIPTION while a subscription is still billing, and ACCOUNT_DELETE_DISABLED when your app does not let users delete their own account.

The REST routes, the app settings that shape them, and every refusal: The user's own account.

Sessions and signing out

await authoric.auth.sessions.list(); // the user's signed-in devices
await authoric.auth.sessions.revoke(sessionUid); // sign one out
await authoric.auth.sessions.revokeOthers(); // sign out everywhere else
await authoric.auth.signOut(); // this device, in every tab

signOut ends the session on the server and clears it here, in every tab, even when the server cannot be reached.

auth.getAccessToken() returns a current access token for calling your own backend, refreshing it first if it is about to expire. Your backend verifies it by calling Authoric with it.

On a server

For a server that renders pages, createServerClient keeps the session in a cookie on your own domain: see Server-side sign-in.

A native app can keep the refresh token in its own secure store, through a storage adapter, or hand one back to the client itself:

const authoric = createClient({ url, appUid, storage: "memory" });
await authoric.auth.setSession({ refreshToken, user }); // the next call mints an access token

On this page