;; 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)))