Skip to content

Commit

Permalink
WIP: Elixir Bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
maennchen committed Sep 8, 2023
1 parent 49cec57 commit 4458270
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 15 deletions.
8 changes: 7 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[
inputs: ["conformance/**/*.{ex,exs}"]
inputs: [
"lib/**/*.ex",
"test/**/*.exs",
"conformance/**/*.{ex,exs}",
".formatter.exs",
"mix.exs"
]
]
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
erlang 26.0.2
rebar 3.22.1
elixir 1.15.5-otp-26
3 changes: 3 additions & 0 deletions lib/oidcc.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Oidcc do
@moduledoc "foo"
end
97 changes: 97 additions & 0 deletions lib/oidcc/provider_configuration.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
defmodule Oidcc.ProviderConfiguration do
@moduledoc """
Tooling to load and parse Openid Configuration
"""

import Record, only: [defrecordp: 3, extract: 2]

defrecordp :configuration,
:oidcc_provider_configuration,
extract(:oidcc_provider_configuration,
from: "include/oidcc_provider_configuration.hrl"
)

defstruct extract(:oidcc_provider_configuration,
from: "include/oidcc_provider_configuration.hrl"
)

@typedoc """
Configuration Struct
For details on the fields see:
* https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
* https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1
"""
@type t() :: %__MODULE__{
issuer: :uri_string.uri_string(),
authorization_endpoint: :uri_string.uri_string(),
token_endpoint: :uri_string.uri_string() | :undefined,
userinfo_endpoint: :uri_string.uri_string() | :undefined,
jwks_uri: :uri_string.uri_string() | :undefined,
registration_endpoint: :uri_string.uri_string() | :undefined,
scopes_supported: [String.t()] | :undefined,
response_types_supported: [String.t()],
response_modes_supported: [String.t()],
grant_types_supported: [String.t()],
acr_values_supported: [String.t()] | :undefined,
subject_types_supported: [:pairwise | :public],
id_token_signing_alg_values_supported: [String.t()],
id_token_encryption_alg_values_supported: [String.t()] | :undefined,
id_token_encryption_enc_values_supported: [String.t()] | :undefined,
userinfo_signing_alg_values_supported: [String.t()] | :undefined,
userinfo_encryption_alg_values_supported: [String.t()] | :undefined,
userinfo_encryption_enc_values_supported: [String.t()] | :undefined,
request_object_signing_alg_values_supported: [String.t()] | :undefined,
request_object_encryption_alg_values_supported: [String.t()] | :undefined,
request_object_encryption_enc_values_supported: [String.t()] | :undefined,
token_endpoint_auth_methods_supported: [String.t()],
token_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined,
display_values_supported: [String.t()] | :undefined,
claim_types_supported: [:normal | :aggregated | :distributed],
claims_supported: [String.t()] | :undefined,
service_documentation: :uri_string.uri_string() | :undefined,
claims_locales_supported: [String.t()] | :undefined,
ui_locales_supported: [String.t()] | :undefined,
claims_parameter_supported: boolean(),
request_parameter_supported: boolean(),
request_uri_parameter_supported: boolean(),
require_request_uri_registration: boolean(),
op_policy_uri: :uri_string.uri_string() | :undefined,
op_tos_uri: :uri_string.uri_string() | :undefined,
revocation_endpoint: :uri_string.uri_string() | :undefined,
revocation_endpoint_auth_methods_supported: [String.t()],
revocation_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined,
introspection_endpoint: :uri_string.uri_string() | :undefined,
introspection_endpoint_auth_methods_supported: [String.t()],
introspection_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined,
code_challenge_methods_supported: [String.t()] | :undefined,
extra_fields: %{String.t() => term()}
}

@doc false
@spec record_to_struct(record :: :oidcc_provider_configuration.configuration()) :: t()
def record_to_struct(record), do: struct!(__MODULE__, configuration(record))

@doc """
Load OpenID Configuration
## Examples
iex> {:ok, {
...> %ProviderConfiguration{issuer: "https://accounts.google.com"},
...> _expiry
...> }} = Oidcc.ProviderConfiguration.load_configuration("https://accounts.google.com")
"""
@spec load_configuration(
issuer :: :uri_string.uri_string(),
opts :: :oidcc_provider_configuration.opts()
) ::
{:ok, {configuration :: t(), expiry :: pos_integer()}}
| {:error, :oidcc_provider_configuration.error()}
def load_configuration(issuer, opts \\ %{}) do
with {:ok, {configuration, expiry}} <-
:oidcc_provider_configuration.load_configuration(issuer, opts) do
{:ok, {record_to_struct(configuration), expiry}}
end
end
end
139 changes: 139 additions & 0 deletions lib/oidcc/provider_configuration/worker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
defmodule Oidcc.ProviderConfiguration.Worker do
@moduledoc """
OIDC Config Provider Worker
Loads and continuously refreshes the OIDC configuration and JWKs
## Usage in Supervisor
```elixir
Supervisor.init([
{Oidcc.ProviderConfiguration.Worker, %{issuer: "https://accounts.google.com/"}}
], strategy: :one_for_one)
```
"""

alias Oidcc.ProviderConfiguration

@typedoc """
See `:oidcc_provider_configuration_worker.opts/0`
"""
@type opts() :: %{
optional(:name) => GenServer.name(),
required(:issuer) => :uri_string.uri_string(),
optional(:provider_configuration_opts) => :oidcc_provider_configuration.opts()
}

@doc """
Start Configuration Worker
## Examples
iex> {:ok, _pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com/",
...> name: MyApp.GoogleConfigProvider
...> })
"""
@spec start_link(opts :: :oidcc_provider_configuration_worker.opts()) :: GenServer.on_start()
def start_link(opts)

def start_link(%{name: name} = opts) when is_atom(name),
do: start_link(%{opts | name: {:local, name}})

def start_link(opts), do: :oidcc_provider_configuration_worker.start_link(opts)

@spec child_spec(opts :: :oidcc_provider_configuration_worker.opts()) :: Supervisor.child_spec()
def child_spec(opts),
do:
Supervisor.child_spec(
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]}
},
[]
)

@doc """
Get Configuration
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com/"
...> })
...> %Oidcc.ProviderConfiguration{issuer: "https://accounts.google.com"} =
...> Oidcc.ProviderConfiguration.Worker.get_provider_configuration(pid)
"""
@spec get_provider_configuration(name :: GenServer.name()) :: ProviderConfiguration.t()
def get_provider_configuration(name),
do:
name
|> :oidcc_provider_configuration_worker.get_provider_configuration()
|> ProviderConfiguration.record_to_struct()

@doc """
Get Parsed Jwks
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com/"
...> })
...> %JOSE.JWK{} =
...> Oidcc.ProviderConfiguration.Worker.get_jwks(pid)
"""
@spec get_jwks(name :: GenServer.name()) :: JOSE.JWK.t()
def get_jwks(name),
do:
name
|> :oidcc_provider_configuration_worker.get_jwks()
|> JOSE.JWK.from_record()

@doc """
Refresh Configuration
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com/"
...> })
...> :ok = Oidcc.ProviderConfiguration.Worker.refresh_configuration(pid)
"""
@spec refresh_configuration(name :: GenServer.name()) :: :ok
def refresh_configuration(name),
do: :oidcc_provider_configuration_worker.refresh_configuration(name)

@doc """
Refresh JWKs
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com/"
...> })
...> :ok = Oidcc.ProviderConfiguration.Worker.refresh_jwks(pid)
"""
@spec refresh_jwks(name :: GenServer.name()) :: :ok
def refresh_jwks(name),
do: :oidcc_provider_configuration_worker.refresh_jwks(name)

@doc """
Refresh JWKs if the provided `Kid` is not matching any currently loaded keys
## Examples
iex> {:ok, pid} =
...> Oidcc.ProviderConfiguration.Worker.start_link(%{
...> issuer: "https://accounts.google.com/"
...> })
...> :ok = Oidcc.ProviderConfiguration.Worker.refresh_jwks_for_unknown_kid(pid, "kid")
"""
@spec refresh_jwks_for_unknown_kid(name :: GenServer.name(), kid :: String.t()) :: :ok
def refresh_jwks_for_unknown_kid(name, kid),
do: :oidcc_provider_configuration_worker.refresh_jwks_for_unknown_kid(name, kid)
end
64 changes: 64 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule Oidcc.Mixfile do
use Mix.Project

{:ok, [{:application, :oidcc, props}]} = :file.consult(~c"src/oidcc.app.src")
@props Keyword.take(props, [:applications, :description, :env, :mod, :licenses, :vsn])

def project() do
[
app: :oidcc,
version: to_string(@props[:vsn]),
elixir: "~> 1.15",
erlc_options: erlc_options(Mix.env()),
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
deps: deps(),
name: "Oidcc",
source_url: "https://github.com/Erlang-Openid/oidcc",
docs: &docs/0,
description: to_string(@props[:description]),
package: package()
]
end

def application() do
[extra_applications: [:inets, :ssl]]
end

defp deps() do
[
{:telemetry, "~> 1.2"},
{:jose, "~> 1.11"},
{:jsx, "~> 3.1"},
{:ex_doc, "~> 0.29.4", only: :dev, runtime: false}
]
end

defp erlc_options(:prod), do: []

defp erlc_options(_enc),
do: [:debug_info, :warn_unused_import, :warn_export_vars, :warnings_as_errors, :verbose]

defp package() do
[
maintainers: ["Jonatan Männchen"],
build_tools: ["rebar3", "mix"],
files: [
"include",
"lib",
"LICENSE*",
"mix.exs",
"README*",
"rebar.config",
"src"
],
licenses: @props[:licenses],
links: %{"Github" => "https://github.com/Erlang-Openid/oidcc"}
]
end

defp docs do
{ref, 0} = System.cmd("git", ["rev-parse", "--verify", "--quiet", "HEAD"])
[source_ref: ref, main: "Oidcc", extras: ["README.md"]]
end
end
11 changes: 11 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
"jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}
19 changes: 5 additions & 14 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,18 @@

{project_plugins, [
coveralls,
rebar3_ex_doc,
erlfmt,
rebar3_hank,
rebar3_hex,
rebar3_lint,
rebar3_sbom
rebar3_lint
]}.

{validate_app_modules, true}.

{dialyzer, [{warnings, [no_return, unmatched_returns, error_handling, no_underspecs]}]}.

{ex_doc, [
{extras, ["README.md", "LICENSE"]},
{main, "README.md"},
{source_url, "https://github.com/Erlang-Openid/oidcc"}
{hex, [
{doc, #{provider => ex_doc}}
]}.

{hex, [{doc, #{provider => ex_doc}}]}.
{edoc_opts, [{preprocess, true}]}.

{hank, [{ignore, [{"test/**/*_SUITE.erl", [unnecessary_function_arguments]}, "include/**/*.hrl"]}]}.

Expand All @@ -40,6 +33,4 @@

{cover_opts, [verbose]}.

{shell,
% {config, "config/sys.config"},
[{apps, [oidcc]}]}.
{shell, [{apps, [oidcc]}]}.
Loading

0 comments on commit 4458270

Please sign in to comment.