Skip to content

Authentication

Crate’s auth system has three layers:

  1. Core (crate.auth.core) — protocol-agnostic token extraction and validation
  2. Policies — protocol-specific response handling (HTTP 401 vs JSON-RPC error)
  3. Middleware — per-operation auth rules via UDAs (@getItem, @create, etc.)

The core module extracts tokens from two sources:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

extractBearerToken strips the “Bearer ” prefix and returns the token string.

Cookie: auth-token=abc123

Read only in permissive mode, for callers that cannot set a header — a server-side render, or an <img> tag pointing at a protected resource. Scoping it to permissive auth means a cookie alone never authorizes a mutation. An invalid cookie leaves the request anonymous instead of failing it. The legacy ember_simple_auth-session cookie no longer authenticates anything.

Both produce the same AuthenticationResult:

struct AuthenticationResult {
AuthResult status; // success, unauthorized, invalidToken
TokenSource source; // bearerHeader, cookie, none
User user; // The authenticated user (if successful)
string token;
string errorMessage;
}

The Authenticator class delegates response handling to a policy struct:

class Authenticator {
AuthenticationResult authenticate(Policy)(
ref Policy policy,
HTTPServerRequest req,
HTTPServerResponse res,
AuthMode mode);
}

A policy struct defines what happens on success, failure, or invalid tokens:

struct HttpAuthPolicy {
void onSuccess(AuthenticationResult result,
HTTPServerRequest req, HTTPServerResponse res) {
// Set request context with user info
req.username = result.user.id;
req.context["email"] = result.user.email;
req.context["user"] = result.user.toJson;
}
void onUnauthorized(AuthenticationResult result,
HTTPServerRequest req, HTTPServerResponse res) {
respondUnauthorized(res); // HTTP 401
}
void onInvalidToken(AuthenticationResult result,
HTTPServerRequest req, HTTPServerResponse res) {
respondUnauthorized(res, "Invalid token.", 400); // HTTP 400
}
}

This design means the same authentication logic works for HTTP REST (returns 401 status codes) and MCP JSON-RPC (returns error objects) — only the policy changes.

enum AuthMode {
mandatory, // Request MUST be authenticated or it's rejected
permissive // Authentication is optional — unauthenticated requests pass through
}

With mandatory, unauthenticated requests get a 401 immediately. With permissive, the request continues but req.context["email"] won’t be set.

The AuthMiddleware base class accepts an optional ClientProvider (from crate.auth.protocols.oauth2.clientprovider) in its constructor:

class AuthMiddleware {
this(UserCollection userCollection,
const OAuth2Configuration configuration,
ClientProvider clientProvider = null) {
this.oAuth2 = new OAuth2(userCollection, new AuthorizationCodeStore(),
configuration, null, clientProvider);
this.emberAuth = new EmberSimpleAuth(userCollection);
this.authenticator = new Authenticator(userCollection);
}
}

When provided, the ClientProvider is forwarded to the OAuth2 class. This enables custom client credential validation strategies (e.g. machine-to-machine authentication, API keys) without modifying the middleware layer.

All built-in middleware subclasses (PublicDataMiddleware, PrivateDataMiddleware, ContributionMiddleware, etc.) propagate this parameter through their own constructors.

Crate ships five auth middleware classes. Each applies different auth rules to different CRUD operations using UDAs:

Everything requires authentication.

class PrivateDataMiddleware : AuthMiddleware {
@any
void mandatory(HTTPServerRequest req, HTTPServerResponse res) {
mandatoryAuth(req, res);
}
}

Read operations are public, write operations require auth.

class PublicDataMiddleware : AuthMiddleware {
@getItem @getList
void permissive(HTTPServerRequest req, HTTPServerResponse res) {
permissiveAuth(req, res);
}
@create @replace @patch @delete_
void mandatory(HTTPServerRequest req, HTTPServerResponse res) {
mandatoryAuth(req, res);
}
}

Read and create are public, edit and delete require auth.

class PublicContributionMiddleware : AuthMiddleware {
@getItem @getList @create
void permissive(HTTPServerRequest req, HTTPServerResponse res) {
permissiveAuth(req, res);
}
@replace @patch @delete_
void mandatory(HTTPServerRequest req, HTTPServerResponse res) {
mandatoryAuth(req, res);
}
}

Only create is public, everything else requires auth.

class ContributionMiddleware : AuthMiddleware {
@create
void permissive(HTTPServerRequest req, HTTPServerResponse res) {
permissiveAuth(req, res);
}
@getItem @getList @replace @patch @delete_
void mandatory(HTTPServerRequest req, HTTPServerResponse res) {
mandatoryAuth(req, res);
}
}

Like PublicContributionMiddleware, but GET operations use identifiableAuth — it identifies the user if a token is present without failing if it’s missing or invalid.

class IdentifiableContributionMiddleware : AuthMiddleware {
@getItem @getList
void identify(HTTPServerRequest req, HTTPServerResponse res) {
identifiableAuth(req, res);
}
@create
void permissive(HTTPServerRequest req, HTTPServerResponse res) {
permissiveAuth(req, res);
}
@replace @patch @delete_
void mandatory(HTTPServerRequest req, HTTPServerResponse res) {
mandatoryAuth(req, res);
}
}

After successful authentication, the request context contains:

KeyValue
req.usernameUser ID
req.context["email"]User email address
req.context["user"]Full user object as JSON

Downstream middleware can check if ("email" in req.context) to determine whether the request is authenticated.

CrateRouter.enable() registers OAuth2 globally:

auto crateRouter = router.crateSetup!RestApi;
crateRouter.enable(auth); // Adds auth.tokenHandlers to URLRouter

This runs before the CrateRouter’s own handler, so tokens are validated on every request. Individual models then use the middleware classes above to control which operations require authentication.

Crate provides UserCrateCollection, a UserCollection implementation backed by a Crate!User:

class UserCrateCollection : UserCollection {
Token createToken(string email, SysTime expire, string[] scopes = [],
string type = "Bearer", string label = "");
void revoke(string token);
string graceRotate(string token, string successor, SysTime expire);
User opIndex(string email); // Get by email
User byToken(string token); // Get by token
User byId(string id); // Get by ID
bool contains(string email);
void empower(string email, string access); // Grant scope
void disempower(string email, string access); // Revoke scope
}

Features a built-in LRU cache (2048 users, 10-second TTL) to avoid hitting the database on every token validation.

graceRotate performs the compare-and-set behind refresh-token rotation and returns the winning successor, so two concurrent refreshes converge on the same token instead of logging the user out. It delegates to a TokenRotationStoreCrateTokenRotationStore for a single process, MongoTokenRotationStore for a shared database. See the OAuth2 reference.