Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 25s
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>
29 lines
3.6 KiB
Plaintext
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)))
|