// ===================================================================== // auth.sx — bearer-token authentication for distd's write surface // (subplan 04, Slice 2). // // `authenticate` takes the raw header block plus the (scope, app, channel) // the operation demands, and either returns the live Token or refuses: // // 401 Unauthorized — no `Authorization` header, a non-Bearer scheme, or // a secret whose hash matches no stored token. The response never // distinguishes "no such token" from "wrong secret": both are // `auth.unknown_token`. // 403 Forbidden — the token exists but `check_token` refuses it; the // code names the refusal (auth.revoked / auth.expired / // auth.missing_scope / auth.app_forbidden / auth.channel_forbidden). // 503 Unavailable — the store database exists but could not be loaded. // // The presented secret is re-hashed (`digest_of_bytes`, the store's // SHA-256) and matched against hashes at rest — the secret itself is never // stored or logged. Successful auth stamps `last_used_at` and persists; // the stamp is best-effort (a failed save must not fail an authorized // request — the operation's own save is the authoritative write). // ===================================================================== #import "modules/std.sx"; #import "modules/std/fs.sx"; #import "../domain/platform.sx"; #import "../domain/app.sx"; #import "../domain/release.sx"; #import "../domain/artifact.sx"; #import "../domain/channel.sx"; #import "../domain/token.sx"; #import "../domain/audit.sx"; #import "../repo/repo.sx"; #import "../store/store.sx"; db :: #import "../repo/db.sx"; jout :: #import "../json_out.sx"; pl :: #import "../publish/publish.sx"; tops :: #import "../token/ops.sx"; http :: #import "http.sx"; AuthErr :: error { Unauthorized, // -> 401 Forbidden, // -> 403 Unavailable, // -> 503 } // The secret inside `Authorization: Bearer `, or null when the // value does not carry the Bearer scheme (case-insensitive, per RFC 7235) // or carries an empty credential. bearer_secret :: (header: string) -> ?string { SCHEME :: "bearer "; if header.len <= 7 { return null; } i := 0; while i < 7 { c := header[i]; if c >= 65 and c <= 90 { c += 32; } // ASCII lower if c != SCHEME[i] { return null; } i += 1; } return string.{ ptr = @header[7], len = header.len - 7 }; } check_code :: (e: TokenCheckErr) -> string { if e == error.Revoked { return "auth.revoked"; } if e == error.Expired { return "auth.expired"; } if e == error.ScopeMissing { return "auth.missing_scope"; } if e == error.AppMismatch { return "auth.app_forbidden"; } return "auth.channel_forbidden"; } check_message :: (e: TokenCheckErr, scope: string, app_slug: string, channel: string) -> string { if e == error.Revoked { return "token has been revoked"; } if e == error.Expired { return "token has expired"; } if e == error.ScopeMissing { return concat("token lacks the required scope: ", scope); } if e == error.AppMismatch { return concat("token is not scoped to this app: ", app_slug); } return concat("token is not scoped to this channel: ", channel); } // Authenticate the request these headers came with, demanding `scope` // against (`app_slug`, `channel`) — "" where the operation has no app or // channel to constrain. authenticate :: (store_dir: string, headers: string, scope: string, app_slug: string, channel: string, fail_out: *jout.CliFailure) -> (Token, !AuthErr) { hq := http.header_value(headers, "authorization"); if hq == null { fail_out.code = "auth.missing"; fail_out.message = "this route requires Authorization: Bearer "; raise error.Unauthorized; } sq := bearer_secret(hq!); if sq == null { fail_out.code = "auth.malformed"; fail_out.message = "Authorization header is not a Bearer credential"; raise error.Unauthorized; } presented_hash := digest_of_bytes(sq!); if !db.store_exists(store_dir) { fail_out.code = "auth.unknown_token"; fail_out.message = "unknown token"; raise error.Unauthorized; } repo, le := db.load(store_dir); if le { fail_out.code = "store.load"; fail_out.message = concat("the store database could not be loaded: ", store_dir); raise error.Unavailable; } tq := repo.find_token_by_hash(presented_hash); if tq == null { fail_out.code = "auth.unknown_token"; fail_out.message = "unknown token"; raise error.Unauthorized; } t := tq!; now := pl.now_secs(); cerr := false; ccode := ""; cmsg := ""; check_token(t, scope, app_slug, channel, now) catch (e) { cerr = true; ccode = check_code(e); cmsg = check_message(e, scope, app_slug, channel); }; if cerr { fail_out.code = ccode; fail_out.message = cmsg; raise error.Forbidden; } tops.mark_token_used(@repo, t.id, now); db.save(@repo, store_dir) catch {}; // best-effort stamp (see header) return t; }