Skip to main content

src/auth/livery_auth_oidc.erl

-module(livery_auth_oidc).
-moduledoc """
OIDC provider discovery.

`discover/1,2` fetches an issuer's
`/.well-known/openid-configuration` document and returns it as a
decoded map (notably `jwks_uri`, `issuer`, `authorization_endpoint`,
`token_endpoint`). Feed the `jwks_uri` to `livery_auth_jwks:keys/1`
to get verification keys.

The HTTP fetch is pluggable via `fetch => fun((Url) -> {ok, Body}
| {error, _})`; the default uses `livery_auth_jwks:default_fetch/1`
(`hackney`).

```erlang
{ok, Cfg}  = livery_auth_oidc:discover(<<"https://issuer.example">>),
JwksUri    = maps:get(<<"jwks_uri">>, Cfg),
{ok, Keys} = livery_auth_jwks:keys(JwksUri).
```
""".

-export([discover/1, discover/2, well_known_url/1]).

-export_type([opts/0, config/0]).

-type opts() :: #{
    fetch => fun((binary()) -> {ok, binary()} | {error, term()})
}.
-type config() :: #{binary() => term()}.

-doc "Fetch and parse the OIDC discovery document for an issuer.".
-spec discover(binary()) -> {ok, config()} | {error, term()}.
discover(Issuer) -> discover(Issuer, #{}).

-spec discover(binary(), opts()) -> {ok, config()} | {error, term()}.
discover(Issuer, Opts) ->
    Fetch = maps:get(fetch, Opts, fun livery_auth_jwks:default_fetch/1),
    Url = well_known_url(Issuer),
    case Fetch(Url) of
        {ok, Body} ->
            try json:decode(Body) of
                #{<<"issuer">> := _} = Config -> {ok, Config};
                #{} = Config -> {ok, Config};
                _ -> {error, invalid_discovery}
            catch
                _:_ -> {error, invalid_json}
            end;
        {error, _} = E ->
            E
    end.

-doc "Build the discovery URL for an issuer (handles a trailing slash).".
-spec well_known_url(binary()) -> binary().
well_known_url(Issuer) ->
    Trimmed = string:trim(Issuer, trailing, "/"),
    Base = unicode:characters_to_binary(Trimmed),
    <<Base/binary, "/.well-known/openid-configuration">>.