Authentication
Three gates and a per-slice signing key — no auth service to run.
Drift gives you authentication without an auth provider. There's no JWKS endpoint to host, no session
store to scale, no OAuth callback to wire up. A function declares how it's protected with one keyword
in its @atomic directive, and the SDK's drift.JWT primitive mints and verifies
tokens with a signing key the platform manages for you and never exposes to your code.
The three gates
The auth keyword gates a function at the platform edge, before your code runs:
| Mode | Behaviour |
|---|---|
auth=none | Public — anyone can call it. The default. |
auth=apikey | Requires a valid X-API-Key header. Missing → 401, wrong → 403. Manage keys with drift atomic auth set/list/revoke. |
auth=jwt | Requires a valid Bearer token in the Authorization header, verified against the slice's own JWT signing key. Missing or invalid (bad signature, expired, wrong issuer) → 401. |
apikey is for machine-to-machine calls; jwt is for user sessions, where you
want expiry, identity, and roles. For full control — optional auth, custom error bodies, or
reading claims — leave the gate at auth=none and verify the token yourself inside the
handler (shown below).
JWT: issue & verify
Tokens are HS256, signed with a 32-byte key unique to your slice. You never see, set, or
rotate the key — the platform holds it and uses it on both sides. The issuer (iss) is
stamped as your slice's identity automatically and checked on every verify, so a token minted by one
slice can't be replayed against another.
| Call | Does |
|---|---|
drift.JWT.Issue(claims) | Mints a signed token. You set sub, exp (required), optional nbf/aud/jti, and a free-form custom map (roles, display name, anything JSON). |
drift.JWT.Verify(token, opts) | Verifies the signature and rejects an expired (exp), not-yet-valid (nbf), wrong-issuer, wrong-audience, or non-HS256 token. Returns the decoded claims, including your custom map. |
exp. Verification rejects a token with no expiry — an un-expiring session
token is a liability, so Drift won't issue one silently.
A login → protected-route flow
The login function is public (auth=none): it checks credentials and mints a token. The
protected function declares auth=jwt, so the edge verifies the token before your code runs;
inside, you call Verify again only to read the claims.
Go
// @atomic http=post:login auth=none
func PostLogin(body map[string]any, req drift.Request) (int, string, any, map[string]string) {
user, ok := checkPassword(body["email"], body["password"]) // your check, against Backbone
if !ok {
return 401, "Unauthorized", map[string]any{"error": "bad credentials"}, nil
}
token, _ := drift.JWT.Issue(drift.JWTClaims{
Sub: user.ID,
Exp: time.Now().Add(24 * time.Hour).Unix(),
Custom: map[string]any{"role": user.Role},
})
return 200, "OK", map[string]any{"token": token}, nil
}
// @atomic http=get:me auth=jwt // edge verifies the Bearer token first
func GetMe(req drift.Request) (int, string, any, map[string]string) {
token := strings.TrimPrefix(req.Headers["Authorization"], "Bearer ")
claims, _ := drift.JWT.Verify(token, drift.JWTVerifyOptions{}) // read claims
return 200, "OK", claims.Custom, nil
}
Python
# @atomic http=post:login auth=none
def post_login(body, req):
user = check_password(body["email"], body["password"])
if not user:
return 401, "Unauthorized", {"error": "bad credentials"}
token = drift.jwt.issue({
"sub": user["id"],
"exp": int(time.time()) + 86400, # 24h
"custom": {"role": user["role"]},
})
return 200, "OK", {"token": token}
# @atomic http=get:me auth=jwt
def get_me(req):
token = req["headers"]["Authorization"].removeprefix("Bearer ")
claims = drift.jwt.verify(token)
return 200, "OK", claims["custom"]
The token rides in the standard Authorization: Bearer <token> header — a browser
frontend served from Canvas calls /api/me same-origin and adds
that header, no CORS in the way. Store the token however your client prefers.