Authentication
Crate ships its own OAuth2 authorization server, user collection, and auth middleware under the crate.auth package. Nothing external is needed — add a UserCollection, hand an OAuth2 instance to the router, and pick a middleware per model.
Wire OAuth2 into the Router
Section titled “Wire OAuth2 into the Router”import crate.auth.identity.usermodel;import crate.auth.protocols.oauth2.auth;import crate.auth.protocols.oauth2.codestore;import crate.auth.usercollection;
auto userCrate = new MemoryCrate!UserModel;auto userCollection = new UserCrateCollection([], userCrate);
auto oauth = new OAuth2(userCollection, new AuthorizationCodeStore());
auto crateRouter = router.crateSetup!RestApi;crateRouter.enable(oauth);
crateRouter.add(productCrate);enable() registers oauth.tokenHandlers as an any("*") route on the vibe.d URLRouter, ahead of Crate’s own handler, so the OAuth2 endpoints answer before any model route is reached.
The AuthorizationCodeStore is required — OAuth2 throws if it is null. It keeps authorization codes in memory with a 10-minute TTL by default:
new AuthorizationCodeStore(2.minutes);Endpoint Paths
Section titled “Endpoint Paths”Every route is configurable through OAuth2Configuration:
| Field | Default | Purpose |
|---|---|---|
tokenPath | /auth/token | Issue a token (password, authorization_code, refresh_token grants) |
authorizePath | /auth/authorize | Start an authorization-code flow; redirects to loginPath |
authorizeCompletePath | /auth/authorize/complete | Complete authorization and emit the code |
authenticatePath | /auth/authenticate | Authenticate a user during the authorize flow |
revokePath | /auth/revoke | Revoke a token (logout) |
registrationPath | /auth/register | Dynamic client registration (RFC 7591) |
clientLookupPrefix | /auth/clients/ | GET <prefix>/<clientId> for public client metadata |
loginPath | /sign-in | Your login page, where authorizePath sends the browser |
OAuth2Configuration config;config.tokenPath = "/api/token";config.loginPath = "/login";
auto oauth = new OAuth2(userCollection, new AuthorizationCodeStore(), config);Refresh Tokens in an HttpOnly Cookie
Section titled “Refresh Tokens in an HttpOnly Cookie”By default the refresh token comes back only in the JSON body, which means JavaScript has to store it. Set refreshCookie.enabled to have the token endpoint emit it as a cookie instead:
OAuth2Configuration config;config.refreshCookie.enabled = true;config.refreshCookie.name = "refresh_token";config.refreshCookie.companionName = "logged_in";config.rotateRefreshToken = true;| Field | Default | Meaning |
|---|---|---|
enabled | false | Emit the refresh token as a cookie |
name | refresh_token | Cookie name |
maxAge | 2419200 (4 weeks) | Cookie lifetime in seconds; 0 makes it a session cookie |
secure | true | Secure attribute |
sameSite | lax | SameSite attribute |
httpOnly | true | Keeps the token unreadable from JavaScript |
path | / | Cookie path, so server-side rendering also receives it |
companionName | "" | Name of a JS-readable presence cookie; empty disables it |
The companion cookie carries the constant "1", never the token. It is set and cleared in lockstep with the refresh cookie, so a browser boot check reads the same “am I logged in” answer that server-side rendering does.
Cookies and a wildcard CORS origin are mutually exclusive in browsers, so a cookie flow also needs a credentialed-origin predicate — see Cookie Flows and CORS below.
Rotation
Section titled “Rotation”With rotateRefreshToken = true, every refresh_token grant issues a new refresh token and retires the one presented. The retired token stays valid as an alias of its successor for a 40-second grace window, which covers the case where a client consumed the token but the rotated cookie never made it back to the browser.
Rotation is a compare-and-set on the stored token, so two concurrent refreshes resolve to the same successor rather than logging the user out. On MongoDB this is a single-document findAndModify — no transaction required.
Choosing Middleware Per Model
Section titled “Choosing Middleware Per Model”Each model gets the auth rules it needs. All five middleware classes take the same constructor arguments:
import crate.auth.middleware;
auto oauthConfig = OAuth2Configuration();
auto publicData = new PublicDataMiddleware(userCollection, oauthConfig);auto privateData = new PrivateDataMiddleware(userCollection, oauthConfig);auto contribution = new ContributionMiddleware(userCollection, oauthConfig);
crateRouter.prepare(articleCrate).and(publicData);crateRouter.prepare(settingsCrate).and(privateData);crateRouter.prepare(issueCrate).and(contribution);| Middleware | Read | Create | Update / Delete |
|---|---|---|---|
PublicDataMiddleware | public | authenticated | authenticated |
PrivateDataMiddleware | authenticated | authenticated | authenticated |
PublicContributionMiddleware | public | public | authenticated |
ContributionMiddleware | authenticated | public | authenticated |
IdentifiableContributionMiddleware | identified if a token is present | public | authenticated |
The internals page shows how each class maps operations to auth modes.
Where the Token Comes From
Section titled “Where the Token Comes From”A request is authenticated from one of two places:
| Source | Header or cookie | Applies to |
|---|---|---|
TokenSource.bearerHeader | Authorization: Bearer <token> | Every request |
TokenSource.cookie | auth-token cookie | Permissive requests only |
The auth-token cookie exists for contexts that cannot send a header — a server-side render, an <img> tag pointing at a protected resource. It is deliberately scoped to permissive auth, so a cookie alone never authorizes a mutation. The legacy ember_simple_auth-session cookie no longer authenticates anything.
Cookie Flows and CORS
Section titled “Cookie Flows and CORS”Browsers reject Set-Cookie on a response that also carries Access-Control-Allow-Origin: *. Set isCredentialedOrigin on the router to name the origins allowed to make credentialed cross-origin requests:
import crate.http.cors : hostFromOrigin;
crateRouter.isCredentialedOrigin = (string origin) { return hostFromOrigin(origin) == "app.example.com";};For an allowed origin the router echoes it back with Access-Control-Allow-Credentials: true and Vary: Origin. For everything else it keeps the wildcard. Leave the predicate null and no cookie-based flow will work in a browser.
Creating Users and Tokens
Section titled “Creating Users and Tokens”import std.datetime;
UserModel admin;admin.email = "admin@example.com";admin.username = "admin";admin.isActive = true;
userCollection.createUser(admin, "s3cret");
auto token = userCollection.createToken( "admin@example.com", Clock.currTime + 24.hours, ["readData"], "Bearer", "CI deploy key");The last argument is a human-friendly label, meant for personal API tokens; issued tokens leave it empty. userCollection.revoke(token.name) removes a token.
Human Verification
Section titled “Human Verification”Registration and password-reset endpoints usually need a challenge before they accept a request. Three implementations of IChallenge ship with Crate — see Challenges for the interface and ALTCHA for the self-hosted proof-of-work option.
Custom Client Validation
Section titled “Custom Client Validation”Every auth middleware accepts an optional ClientProvider for validating OAuth2 client credentials beyond the default user-token flow:
import crate.auth.protocols.oauth2.clientprovider;
auto authMiddleware = new PublicDataMiddleware( userCollection, oauthConfig, new MyClientProvider());It is forwarded to the OAuth2 instance the middleware builds.
Model References and crateGetters
Section titled “Model References and crateGetters”When a model references another model, Crate looks the referenced item up during POST and PATCH. Register a getter for each referenced model:
crateGetters["Team"] = &teamCrate.getItem;crateGetters["Picture"] = &pictureCrate.getItem;
crateRouter.add(campaignCrate);A missing getter fails the request with a 500 and the message no getter for Team model.
Next Steps
Section titled “Next Steps”- OAuth2 reference — grants, protected-resource metadata, and the full configuration surface
- Authentication internals — how token extraction and auth policies fit together
- Multi-protocol — serving REST, MCP, and GraphQL from the same models