defmodule Oidcc do
@moduledoc """
OpenID Connect High Level Interface
## Setup
{:ok, _pid} =
Oidcc.ProviderConfiguration.Worker.start_link(%{
issuer: "https://accounts.google.com",
name: MyApp.GoogleConfigProvider
})
or via a supervisor
Supervisor.init([
{Oidcc.ProviderConfiguration.Worker, %{issuer: "https://accounts.google.com"}}
], strategy: :one_for_one)
## Global Configuration
* `max_clock_skew` (default `0`) - Maximum allowed clock skew for JWT
`exp` / `nbf` validation
"""
@moduledoc since: "3.0.0"
@doc """
Create Auth Redirect URL
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com"
...> })
...>
...> {:ok, _redirect_uri} =
...> Oidcc.create_redirect_url(
...> pid,
...> "client_id",
...> "client_secret",
...> %{redirect_uri: "https://my.server/return"}
...> )
"""
@doc since: "3.0.0"
@spec create_redirect_url(
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_authorization.opts() | :oidcc_client_context.opts()
) ::
{:ok, :uri_string.uri_string()}
| {:error, :oidcc_client_context.error() | :oidcc_client_context.error()}
def create_redirect_url(provider_configuration_name, client_id, client_secret, opts),
do: :oidcc.create_redirect_url(provider_configuration_name, client_id, client_secret, opts)
@doc """
retrieve the token using the authcode received before and directly validate
the result.
the authcode was sent to the local endpoint by the OpenId Connect provider,
using redirects
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://api.login.yahoo.com"
...> })
...>
...> # Get auth_code fromm redirect
...> auth_code = "auth_code"
...>
...> Oidcc.retrieve_token(
...> auth_code,
...> pid,
...> "client_id",
...> "client_secret",
...> %{redirect_uri: "https://my.server/return"}
...> )
...> # => {:ok, %Oidcc.Token{}}
"""
@doc since: "3.0.0"
@spec retrieve_token(
auth_code :: String.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_token.retrieve_opts() | :oidcc_client_context.opts()
) ::
{:ok, Oidcc.Token.t()} | {:error, :oidcc_client_context.error() | :oidcc_token.error()}
def retrieve_token(auth_code, provider_configuration_name, client_id, client_secret, opts),
do:
auth_code
|> :oidcc.retrieve_token(
provider_configuration_name,
client_id,
client_secret,
opts
)
|> Oidcc.Token.normalize_token_response()
@doc """
Refresh Token
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://api.login.yahoo.com"
...> })
...>
...> # Get refresh_token fromm redirect
...> refresh_token = "refresh_token"
...>
...> Oidcc.refresh_token(
...> refresh_token,
...> pid,
...> "client_id",
...> "client_secret",
...> %{expected_subject: "sub_from_initial_id_token"}
...> )
...> # => {:ok, %Oidcc.Token{}}
"""
@doc since: "3.0.0"
@spec refresh_token(
refresh_token :: String.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_token.refresh_opts() | :oidcc_client_context.opts()
) :: {:ok, Oidcc.Token.t()} | {:error, :oidcc_token.error()}
@spec refresh_token(
token :: Oidcc.Token.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_token.refresh_opts_no_sub() | :oidcc_client_context.opts()
) ::
{:ok, Oidcc.Token.t()} | {:error, :oidcc_client_context.error() | :oidcc_token.error()}
def refresh_token(token, provider_configuration_name, client_id, client_secret, opts \\ %{}) do
token =
case token do
%Oidcc.Token{} = token -> Oidcc.Token.struct_to_record(token)
token when is_binary(token) -> token
end
token
|> :oidcc.refresh_token(
provider_configuration_name,
client_id,
client_secret,
opts
)
|> Oidcc.Token.normalize_token_response()
end
@doc """
Introspect the given access token
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://api.login.yahoo.com"
...> })
...>
...> Oidcc.introspect_token(
...> "access_token",
...> pid,
...> "client_id",
...> "client_secret"
...> )
...> # => {:ok, %Oidcc.TokenIntrospection{}}
"""
@doc since: "3.0.0"
@spec introspect_token(
access_token :: String.t() | Oidcc.Token.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_token_introspection.opts() | :oidcc_client_context.opts()
) ::
{:ok, Oidcc.TokenIntrospection.t()}
| {:error, :oidcc_client_context.error() | :oidcc_token_introspection.error()}
def introspect_token(
token,
provider_configuration_name,
client_id,
client_secret,
opts \\ %{}
) do
token =
case token do
%Oidcc.Token{} = token -> Oidcc.Token.struct_to_record(token)
token when is_binary(token) -> token
end
with {:ok, introspection} <-
:oidcc.introspect_token(
token,
provider_configuration_name,
client_id,
client_secret,
opts
) do
{:ok, Oidcc.TokenIntrospection.record_to_struct(introspection)}
end
end
@doc """
Load userinfo for the given token
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://api.login.yahoo.com"
...> })
...>
...> # Get access_token from Oidcc.Token.retrieve/3
...> access_token = "access_token"
...>
...> Oidcc.retrieve_userinfo(
...> access_token,
...> pid,
...> "client_id",
...> "client_secret",
...> %{expected_subject: "sub"}
...> )
...> # => {:ok, %{"sub" => "sub"}}
"""
@doc since: "3.0.0"
@spec retrieve_userinfo(
token :: Oidcc.Token.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_userinfo.retrieve_opts_no_sub() | :oidcc_client_context.opts()
) :: {:ok, :oidcc_jwt_util.claims()} | {:error, :oidcc_userinfo.error()}
@spec retrieve_userinfo(
access_token :: String.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_userinfo.retrieve_opts() | :oidcc_client_context.opts()
) ::
{:ok, :oidcc_jwt_util.claims()}
| {:error, :oidcc_client_context.error() | :oidcc_userinfo.error()}
def retrieve_userinfo(token, provider_configuration_name, client_id, client_secret, opts \\ %{}) do
token =
case token do
%Oidcc.Token{} = token -> Oidcc.Token.struct_to_record(token)
token when is_binary(token) -> token
end
:oidcc.retrieve_userinfo(token, provider_configuration_name, client_id, client_secret, opts)
end
@doc """
Retrieve JSON Web Token (JWT) Profile Token
https://datatracker.ietf.org/doc/html/rfc7523#section-4
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://erlef-test-w4a8z2.zitadel.cloud"
...> })
...>
...> %{"key" => key, "keyId" => kid, "userId" => subject} = "JWT_PROFILE"
...> |> System.fetch_env!()
...> |> JOSE.decode()
...>
...> jwk = JOSE.JWK.from_pem(key)
...>
...> {:ok, %Oidcc.Token{}} =
...> Oidcc.jwt_profile_token(
...> subject,
...> pid,
...> "client_id",
...> "client_secret",
...> jwk,
...> %{scope: ["urn:zitadel:iam:org:project:id:zitadel:aud"], kid: kid}
...> )
"""
@doc since: "3.0.0"
@spec jwt_profile_token(
subject :: String.t(),
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
jwk :: JOSE.JWK.t(),
opts :: :oidcc_token.jwt_profile_opts() | :oidcc_client_context.opts()
) ::
{:ok, Oidcc.Token.t()} | {:error, :oidcc_client_context.error() | :oidcc_token.error()}
def jwt_profile_token(subject, provider_configuration_name, client_id, client_secret, jwk, opts) do
jwk = JOSE.JWK.to_record(jwk)
subject
|> :oidcc.jwt_profile_token(
provider_configuration_name,
client_id,
client_secret,
jwk,
opts
)
|> Oidcc.Token.normalize_token_response()
end
@doc """
Retrieve Client Credential Token
See https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.4
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://erlef-test-w4a8z2.zitadel.cloud"
...> })
...>
...> {:ok, %Oidcc.Token{}} =
...> Oidcc.client_credentials_token(
...> pid,
...> System.fetch_env!("CLIENT_CREDENTIALS_CLIENT_ID"),
...> System.fetch_env!("CLIENT_CREDENTIALS_CLIENT_SECRET"),
...> %{scope: ["scope"]}
...> )
"""
@doc since: "3.0.0"
@spec client_credentials_token(
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
opts :: :oidcc_token.client_credentials_opts() | :oidcc_client_context.opts()
) ::
{:ok, Oidcc.Token.t()} | {:error, :oidcc_client_context.error() | :oidcc_token.error()}
def client_credentials_token(provider_configuration_name, client_id, client_secret, opts),
do:
provider_configuration_name
|> :oidcc.client_credentials_token(
client_id,
client_secret,
opts
)
|> Oidcc.Token.normalize_token_response()
@doc """
Create Initiate URI for Relaying Party initated Logout
See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout]
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://erlef-test-w4a8z2.zitadel.cloud"
...> })
...>
...> # Get access_token from Oidcc.Token.retrieve/3
...> token = "token"
...>
...> {:ok, _redirect_uri} = Oidcc.initiate_logout_url(
...> token,
...> pid,
...> "client_id"
...> )
"""
@doc since: "3.0.0"
@spec initiate_logout_url(
token :: id_token | Oidcc.Token.t() | :undefined,
provider_configuration_name :: GenServer.name(),
client_id :: String.t(),
opts :: :oidcc_logout.initiate_url_opts() | :oidcc_client_context.opts()
) ::
{:ok, :uri_string.uri_string()}
| {:error, :oidcc_client_context.error() | :oidcc_logout.error()}
when id_token: String.t()
def initiate_logout_url(
token,
provider_configuration_name,
client_id,
opts \\ %{}
) do
token =
case token do
%Oidcc.Token{} = token -> Oidcc.Token.struct_to_record(token)
token when is_binary(token) -> token
:undefined -> :undefined
end
:oidcc.initiate_logout_url(
token,
provider_configuration_name,
client_id,
opts
)
end
end