Server-side sign-in
Keep a user's session in a cookie on your own domain, so pages your server renders know who is asking, with @authoric/js.
A page your server renders needs to know who is asking on the first request, before any script
runs. createServerClient keeps the user's session in a cookie on your domain, which the
browser sends with every request to your server. The Authoric API itself never reads a cookie: your
server reads it and calls Authoric with the session's token.
import { createServerClient } from "@authoric/js";
// Inside a request handler: one client per request, from that request's cookies.
const authoric = createServerClient({
url: process.env.AUTHORIC_URL,
appUid: process.env.AUTHORIC_APP_UID,
cookies: {
get: (name) => readTheRequestCookie(name),
set: (name, value, options) => setAResponseCookie(name, value, options),
remove: (name, options) => removeAResponseCookie(name, options),
},
});
const me = await authoric.auth.getUser(); // asks the API; null when no one is signed inThe client has everything the browser client has: sign-in, rows, files, teams and billing, all as the signed-in user.
Two rules
Create one client per request. Build it inside the request handler, from that request's cookies. A client created once at module level and shared across requests would hand one user's session to the next.
Trust getUser(), never the cookie. A cookie is input the user can edit.
auth.getUser() asks the API (GET /{appUid}/auth/me), which checks the session is live and
answers who it belongs to. auth.getSession() is only what this device remembers. On a server,
call getUser() before you decide anything about who the user is.
{
"user": {
"uid": "0199a3c2-5b1e-7c4d-9f2a-3e8b6d1c0a47",
"email": "dana@example.com",
"fname": "Dana",
"lname": "Reyes",
"emailVerified": true
},
"activeOrgUid": "0199a3c2-5b1d-74e0-8c55-1f0e2a9b7d31",
"org": { "uid": "0199a3c2-5b1d-74e0-8c55-1f0e2a9b7d31", "name": "Dana Reyes" }
}getUser() answers null when the cookie holds no session. When the session has ended on the
server, it throws an AuthoricError: treat that as signed out.
The cookie
| Default | |
|---|---|
| Name | __Host-authoric-session-<appUid> (authoric-session-<appUid> when secure: false) |
Secure | On. Turn it off only for plain-http local development. |
SameSite | Lax: the cookie is not sent on cross-site form posts. |
Path | / |
Domain | None: the cookie belongs to the exact host that set it. |
HttpOnly | Off, so scripts on your pages can read the same session. |
Max-Age | 30 days, the session's idle window. |
The __Host- prefix makes the browser refuse a cookie of that name set by another subdomain or
over plain http, so a sibling site cannot plant a session. Change any default with cookie:
createServerClient({ url, appUid, cookies, cookie: { httpOnly: true } });
createServerClient({ url, appUid, cookies, cookie: { secure: false } }); // http://localhost onlyhttpOnly: true hides the cookie from your pages' scripts, which protects the session from
script injection. Then code in the browser cannot use that session, and every call that acts as the
user goes through your server.
The options the SDK hands your set and remove are { path, maxAge, httpOnly, secure, sameSite, domain? }, with maxAge in seconds. Most frameworks take seconds too; Express takes
milliseconds, so multiply by 1,000 there. remove passes maxAge: 0.
When the user chooses to remember a device for two-factor, a
second cookie, __Host-authoric-device-<appUid>, remembers it the same way.
Refreshing
The cookie carries the 15-minute access token as well as the refresh token, so an ordinary page load spends nothing. When the access token is within a minute of expiring, the next call refreshes it and writes the new cookie on that response.
A refresh spends the old refresh token. If the new cookie is never written, a later request sends
the spent one and the session ends. Some frameworks cannot set a cookie while a page is rendering,
so refresh in middleware, which can: call auth.getAccessToken() there, before the page
renders. The examples below do.
Framework examples
Each creates the client per request and wires the three cookie methods to the framework's own.
import express from "express";
import cookieParser from "cookie-parser";
import { createServerClient } from "@authoric/js";
const app = express();
app.use(cookieParser());
app.use((req, res, next) => {
req.authoric = createServerClient({
url: process.env.AUTHORIC_URL,
appUid: process.env.AUTHORIC_APP_UID,
cookies: {
get: (name) => req.cookies[name],
// Express takes milliseconds.
set: (name, value, options) => res.cookie(name, value, { ...options, maxAge: options.maxAge * 1000 }),
remove: (name, options) => res.cookie(name, "", { ...options, maxAge: 0 }),
},
});
next();
});
app.get("/dashboard", async (req, res) => {
const me = await req.authoric.auth.getUser();
if (!me) return res.redirect("/sign-in");
const { items } = await req.authoric.from("invoices").list({ limit: 25 });
res.render("dashboard", { me, items });
});serializeCookie(name, value, options) answers a Set-Cookie header value, and
readCookie(cookieHeader, name) reads one cookie from a Cookie header.
Signing in on the server
Every sign-in method works from a server client, and a successful sign-in writes the cookie:
const result = await authoric.auth.signInWithPassword({ email, password });
if (result.mfa) {
// Ask for the code, then auth.mfa.verify({ challengeId: result.mfa.challengeId, code }).
}For Google or GitHub, auth.oauth.start(provider, { redirectTo }) answers the provider's address to
send the browser to, and on your callback route auth.oauth.exchange(code) takes the code from the
callback's query string. auth.signOut() ends the session and removes the cookie.
Sign-in attempts are throttled per IP address. Requests from your server all come from its address and share one budget, so where you can, let the browser make the sign-in call itself. See Auth.
Your own routes
A route on your server that changes something with the user's session is open to cross-site
request forgery: another site can make the browser send a request to it. SameSite=Lax already
keeps the cookie off cross-site form posts. Add the second check on every such route, and never
change anything on a GET:
import { isSameOriginRequest } from "@authoric/js";
app.post("/invoices/:uid/approve", async (req, res) => {
if (!isSameOriginRequest(req, ["https://app.example.com"])) return res.sendStatus(403);
await req.authoric.action("invoices", "/approve").call({ invoice_uid: req.params.uid });
res.redirect("/dashboard");
});isSameOriginRequest({ method, headers }, allowedOrigins) lets reads (GET, HEAD, OPTIONS)
pass. A write passes only when its Origin header, or its Referer when there is no Origin, is
one of allowedOrigins. It takes an Express or Node request, or a Fetch Request.
Backends in other languages
A server that is not JavaScript does the same thing by hand: keep the user's tokens in a cookie on
your domain, send the access token to Authoric, and ask GET /{appUid}/auth/me who it belongs to.
curl "$AUTHORIC_URL/$APP_UID/auth/me" \
-H "Authorization: Bearer $ACCESS_TOKEN"Sessions & token refresh covers refreshing the pair safely.