Files
rose-ash/lib/identity/clients.sx
giles 9860582b4a
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 25s
identity: OAuth client registry — public/confidential clients + redirect allow-list (11 tests)
clients.sx (RFC 6749 §2) — confidential clients must present the correct
secret at the token endpoint (wrong → invalid_client); public clients are
identified but not authenticated; redirect_uris are pre-registered and
checked by exact-match valid_redirect (§3.1.2.2 + Security BCP). Standalone
module for now; wiring confidential-client auth into oauth exchange is a
follow-up. New tests/clients.sx. 149/149.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 02:03:44 +00:00

29 lines
3.6 KiB
Plaintext

;; identity/clients.sx — the OAuth client registry (RFC 6749 §2).
;;
;; A client is registered with a type, a secret, and its allow-listed
;; redirect_uris:
;;
;; public — cannot keep a secret (SPAs, native apps, §2.1);
;; identified but not authenticated.
;; confidential — can authenticate; MUST present its secret at the token
;; endpoint (§3.2.1, §4.1.3). A wrong secret is
;; invalid_client — never a soft pass.
;;
;; Redirect URIs must be pre-registered (§3.1.2.2 + OAuth Security BCP):
;; valid_redirect/3 is the exact-match check the authorize/exchange steps
;; consult so an attacker cannot redirect the code to an unregistered URI.
;;
;; register(C, ClientId, Type, Secret, RedirectUris) -> ok | {error, exists}
;; lookup(C, ClientId) -> {ok, Type, RedirectUris} | {error, unknown_client}
;; authenticate(C, ClientId, Sec) -> {ok, public} | {ok, confidential}
;; | {error, invalid_client} | {error, unknown_client}
;; valid_redirect(C, ClientId, U) -> true | false
(define
identity-clients-source
"-module(identity_clients).\n\n start() ->\n spawn(fun () -> loop([]) end).\n\n register(C, ClientId, Type, Secret, RedirectUris) ->\n C ! {register, ClientId, Type, Secret, RedirectUris, self()},\n receive {client_reply, R} -> R end.\n\n lookup(C, ClientId) ->\n C ! {lookup, ClientId, self()},\n receive {client_reply, R} -> R end.\n\n authenticate(C, ClientId, Secret) ->\n C ! {authenticate, ClientId, Secret, self()},\n receive {client_reply, R} -> R end.\n\n valid_redirect(C, ClientId, Uri) ->\n C ! {valid_redirect, ClientId, Uri, self()},\n receive {client_reply, R} -> R end.\n\n loop(Clients) ->\n receive\n {register, ClientId, Type, Secret, RedirectUris, From} ->\n case find(ClientId, Clients) of\n {ok, _} ->\n From ! {client_reply, {error, exists}},\n loop(Clients);\n none ->\n From ! {client_reply, ok},\n loop([{ClientId, {Type, Secret, RedirectUris}} | Clients])\n end;\n {lookup, ClientId, From} ->\n case find(ClientId, Clients) of\n none -> From ! {client_reply, {error, unknown_client}};\n {ok, {Type, _, Uris}} -> From ! {client_reply, {ok, Type, Uris}}\n end,\n loop(Clients);\n {authenticate, ClientId, Secret, From} ->\n case find(ClientId, Clients) of\n none ->\n From ! {client_reply, {error, unknown_client}};\n {ok, {public, _, _}} ->\n From ! {client_reply, {ok, public}};\n {ok, {confidential, S, _}} ->\n case S =:= Secret of\n true -> From ! {client_reply, {ok, confidential}};\n false -> From ! {client_reply, {error, invalid_client}}\n end\n end,\n loop(Clients);\n {valid_redirect, ClientId, Uri, From} ->\n case find(ClientId, Clients) of\n none -> From ! {client_reply, false};\n {ok, {_, _, Uris}} -> From ! {client_reply, member(Uri, Uris)}\n end,\n loop(Clients);\n {stop, From} ->\n From ! {client_reply, ok}\n end.\n\n member(_, []) -> false;\n member(X, [Y | Rest]) ->\n case X =:= Y of\n true -> true;\n false -> member(X, Rest)\n end.\n\n find(_, []) -> none;\n find(Key, [{K, V} | Rest]) ->\n case K =:= Key of\n true -> {ok, V};\n false -> find(Key, Rest)\n end.")
(define
identity-load-clients!
(fn () (erlang-load-module identity-clients-source)))