Skip to content

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.

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);

Every route is configurable through OAuth2Configuration:

FieldDefaultPurpose
tokenPath/auth/tokenIssue a token (password, authorization_code, refresh_token grants)
authorizePath/auth/authorizeStart an authorization-code flow; redirects to loginPath
authorizeCompletePath/auth/authorize/completeComplete authorization and emit the code
authenticatePath/auth/authenticateAuthenticate a user during the authorize flow
revokePath/auth/revokeRevoke a token (logout)
registrationPath/auth/registerDynamic client registration (RFC 7591)
clientLookupPrefix/auth/clients/GET <prefix>/<clientId> for public client metadata
loginPath/sign-inYour login page, where authorizePath sends the browser
OAuth2Configuration config;
config.tokenPath = "/api/token";
config.loginPath = "/login";
auto oauth = new OAuth2(userCollection, new AuthorizationCodeStore(), config);

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;
FieldDefaultMeaning
enabledfalseEmit the refresh token as a cookie
namerefresh_tokenCookie name
maxAge2419200 (4 weeks)Cookie lifetime in seconds; 0 makes it a session cookie
securetrueSecure attribute
sameSitelaxSameSite attribute
httpOnlytrueKeeps 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.

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.

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);
MiddlewareReadCreateUpdate / Delete
PublicDataMiddlewarepublicauthenticatedauthenticated
PrivateDataMiddlewareauthenticatedauthenticatedauthenticated
PublicContributionMiddlewarepublicpublicauthenticated
ContributionMiddlewareauthenticatedpublicauthenticated
IdentifiableContributionMiddlewareidentified if a token is presentpublicauthenticated

The internals page shows how each class maps operations to auth modes.

A request is authenticated from one of two places:

SourceHeader or cookieApplies to
TokenSource.bearerHeaderAuthorization: Bearer <token>Every request
TokenSource.cookieauth-token cookiePermissive 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.

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.

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.

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.

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.

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.