Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
oauth.sx — RFC 6749 §4.1 as a state machine on one authz-server process:
authorize → {consent_required} → consent(allow|deny) → {code} → exchange
→ {ok, Token}. Exchange enforces single-use codes (§10.5, replay →
invalid_grant), client_id + redirect_uri binding (§4.1.3), and PKCE
(RFC 7636 plain) verifier match. Issued tokens are grant-backed via
token.sx so revocation stays real. 53/53.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 lines
4.7 KiB
Plaintext
30 lines
4.7 KiB
Plaintext
;; identity/oauth.sx — the OAuth2 authorization-code flow as a message
|
|
;; protocol (RFC 6749 §4.1), with PKCE (RFC 7636, `plain` method).
|
|
;;
|
|
;; The flow is a state machine threaded through one authorization-server
|
|
;; process, never a single function:
|
|
;;
|
|
;; authorize -> {consent_required, ReqId} (§4.1.1 request stored)
|
|
;; consent -> {code, Code} | {error, access_denied} (§4.1.2 grant/deny)
|
|
;; exchange -> {ok, Token} | {error, invalid_grant} (§4.1.3 / §5.2)
|
|
;;
|
|
;; Security invariants enforced at exchange (§4.1.3, §10.5):
|
|
;; - the code is single-use: it is removed on the FIRST exchange attempt,
|
|
;; so replay yields invalid_grant;
|
|
;; - the code is bound to its client_id and redirect_uri; a mismatch is
|
|
;; invalid_grant;
|
|
;; - PKCE: the presented verifier must match the stored challenge
|
|
;; (plain method: challenge == verifier), else invalid_grant.
|
|
;;
|
|
;; On success a token is issued into a grant-backed table (token.sx), so
|
|
;; revocation stays real. The server proves identity; it does not decide
|
|
;; permission — that is acl's job, keyed off the issued grant.
|
|
|
|
(define
|
|
identity-oauth-source
|
|
"-module(identity_oauth).\n\n start() ->\n spawn(fun () ->\n TokReg = identity_tokens:start(),\n loop(TokReg, [], [])\n end).\n\n authorize(O, ClientId, RedirectUri, Scope, Subject, Challenge) ->\n O ! {authorize, ClientId, RedirectUri, Scope, Subject, Challenge, self()},\n receive {oauth_reply, R} -> R end.\n\n consent(O, ReqId, Decision) ->\n O ! {consent, ReqId, Decision, self()},\n receive {oauth_reply, R} -> R end.\n\n exchange(O, Code, ClientId, RedirectUri, Verifier) ->\n O ! {exchange, Code, ClientId, RedirectUri, Verifier, self()},\n receive {oauth_reply, R} -> R end.\n\n introspect(O, Token) ->\n O ! {introspect, Token, self()},\n receive {oauth_reply, R} -> R end.\n\n revoke(O, Token) ->\n O ! {revoke, Token, self()},\n receive {oauth_reply, R} -> R end.\n\n loop(TokReg, Pending, Codes) ->\n receive\n {authorize, ClientId, RedirectUri, Scope, Subject, Challenge, From} ->\n ReqId = make_ref(),\n Rec = {ClientId, RedirectUri, Scope, Subject, Challenge},\n From ! {oauth_reply, {consent_required, ReqId}},\n loop(TokReg, [{ReqId, Rec} | Pending], Codes);\n {consent, ReqId, Decision, From} ->\n case find(ReqId, Pending) of\n none ->\n From ! {oauth_reply, {error, unknown_request}},\n loop(TokReg, Pending, Codes);\n {ok, Rec} ->\n Pending2 = remove(ReqId, Pending),\n case Decision of\n allow ->\n Code = make_ref(),\n From ! {oauth_reply, {code, Code}},\n loop(TokReg, Pending2, [{Code, Rec} | Codes]);\n deny ->\n From ! {oauth_reply, {error, access_denied}},\n loop(TokReg, Pending2, Codes)\n end\n end;\n {exchange, Code, ClientId, RedirectUri, Verifier, From} ->\n case find(Code, Codes) of\n none ->\n From ! {oauth_reply, {error, invalid_grant}},\n loop(TokReg, Pending, Codes);\n {ok, Rec} ->\n Codes2 = remove(Code, Codes),\n From ! {oauth_reply, redeem(TokReg, Rec, ClientId, RedirectUri, Verifier)},\n loop(TokReg, Pending, Codes2)\n end;\n {introspect, Token, From} ->\n From ! {oauth_reply, identity_tokens:introspect(TokReg, Token)},\n loop(TokReg, Pending, Codes);\n {revoke, Token, From} ->\n identity_tokens:revoke(TokReg, Token),\n From ! {oauth_reply, ok},\n loop(TokReg, Pending, Codes)\n end.\n\n redeem(TokReg, {CCid, CRedir, Scope, Subject, Challenge}, ClientId, RedirectUri, Verifier) ->\n case CCid =:= ClientId of\n false -> {error, invalid_grant};\n true ->\n case CRedir =:= RedirectUri of\n false -> {error, invalid_grant};\n true ->\n case Challenge =:= Verifier of\n false -> {error, invalid_grant};\n true ->\n {ok, Token} = identity_tokens:issue(TokReg, Subject, ClientId, Scope),\n {ok, Token}\n end\n end\n end.\n\n find(_, []) -> none;\n find(Key, [{K, Rec} | Rest]) ->\n case K =:= Key of\n true -> {ok, Rec};\n false -> find(Key, Rest)\n end.\n\n remove(_, []) -> [];\n remove(Key, [{K, Rec} | Rest]) ->\n case K =:= Key of\n true -> remove(Key, Rest);\n false -> [{K, Rec} | remove(Key, Rest)]\n end.")
|
|
|
|
(define
|
|
identity-load-oauth!
|
|
(fn () (erlang-load-module identity-oauth-source)))
|