Packages
boruta
3.0.0-beta.4
3.0.0-beta.4
3.0.0-beta.3
3.0.0-beta.2
3.0.0-beta.1
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.2
2.2.1
2.2.0
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.1
2.0.0
2.0.0-rc.1
2.0.0-rc.0
1.2.1
1.2.0
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-rc.5
0.1.0-rc.4
0.1.0-rc.3
0.1.0-rc.2
0.1.0-rc.1
Core of an OAuth/OpenID Connect provider enabling authorization in your applications.
Current section
Files
Jump to
Current section
Files
lib/boruta/adapters/ecto/schemas/client.ex
defmodule Boruta.Ecto.Client do
@moduledoc """
Ecto Adapter Client Schema
"""
use Ecto.Schema
import Ecto.Changeset
import Boruta.Config,
only: [
token_generator: 0,
repo: 0,
access_token_max_ttl: 0,
agent_token_max_ttl: 0,
authorization_code_max_ttl: 0,
authorization_request_max_ttl: 0,
id_token_max_ttl: 0,
refresh_token_max_ttl: 0
]
alias Boruta.Did
alias Boruta.Ecto.Scope
alias Boruta.Oauth
alias Boruta.Oauth.Client
alias Boruta.Universal
alias ExJsonSchema.Validator.Error.BorutaFormatter
@type t :: %__MODULE__{
secret: String.t(),
authorize_scope: boolean(),
redirect_uris: list(String.t()),
supported_grant_types: list(String.t()),
enforce_dpop: boolean(),
enforce_tx_code: boolean(),
pkce: boolean(),
public_refresh_token: boolean(),
public_revoke: boolean(),
access_token_ttl: integer(),
agent_token_ttl: integer(),
authorization_code_ttl: integer(),
refresh_token_ttl: integer(),
authorized_scopes: Ecto.Association.NotLoaded.t() | list(Scope.t()),
id_token_ttl: integer(),
id_token_signature_alg: String.t(),
token_endpoint_auth_methods: list(String.t()),
token_endpoint_jwt_auth_alg: String.t(),
userinfo_signed_response_alg: String.t() | nil,
jwt_public_key: String.t(),
public_key: String.t(),
private_key: String.t(),
response_mode: String.t(),
signatures_adapter: String.t()
}
@token_endpoint_auth_methods [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
]
@token_endpoint_jwt_auth_algs [
:RS256,
:RS384,
:RS512,
:HS256,
:HS384,
:HS512
]
@response_modes ["post", "direct_post"]
@key_pair_type_schema %{
"type" => "object",
"properties" => %{
"type" => %{"type" => "string", "pattern" => "^ec|rsa|universal$"},
"modulus_size" => %{"type" => "string"},
"exponent_size" => %{"type" => "string"},
"curve" => %{"type" => "string", "pattern" => "^P-256|P-384|P-512$"}
},
"required" => ["type"]
}
@key_pair_type_jwt_algs %{
"ec" => [
"ES256",
"ES384",
"ES512",
"HS256",
"HS384",
"HS512"
],
"rsa" => [
"RS256",
"RS384",
"RS512",
"HS256",
"HS384",
"HS512"
],
"universal" => [
"EdDSA"
]
}
@primary_key {:id, Ecto.UUID, autogenerate: true}
@foreign_key_type :binary_id
@timestamps_opts type: :utc_datetime
schema "oauth_clients" do
field(:public_client_id, :string)
field(:name, :string)
field(:secret, :string)
field(:confidential, :boolean, default: false)
field(:authorize_scope, :boolean, default: false)
field(:enforce_tx_code, :boolean, default: false)
field(:enforce_dpop, :boolean, default: false)
field(:redirect_uris, {:array, :string}, default: [])
field(:supported_grant_types, {:array, :string}, default: Oauth.Client.grant_types())
field(:check_public_client_id, :boolean, default: false)
field(:pkce, :boolean, default: false)
field(:public_refresh_token, :boolean, default: false)
field(:public_revoke, :boolean, default: false)
field(:access_token_ttl, :integer)
field(:agent_token_ttl, :integer)
field(:authorization_code_ttl, :integer)
field(:authorization_request_ttl, :integer)
field(:id_token_ttl, :integer)
field(:refresh_token_ttl, :integer)
field(:id_token_signature_alg, :string, default: "RS512")
field(:id_token_kid, :string)
field(:signatures_adapter, :string, default: "Elixir.Boruta.Internal.Signatures")
field(:key_pair_type, :map,
default: %{
"type" => "rsa",
"modulus_size" => "1024",
"exponent_size" => "65537"
}
)
field(:public_key, :string)
field(:private_key, :string)
field(:did, :string)
field(:token_endpoint_auth_methods, {:array, :string},
default: ["client_secret_basic", "client_secret_post"]
)
field(:token_endpoint_jwt_auth_alg, :string, default: "HS256")
field(:jwt_public_key, :string)
field(:jwk, :map, virtual: true)
field(:jwks_uri, :string)
field(:userinfo_signed_response_alg, :string)
field(:logo_uri, :string)
field(:metadata, :map, default: %{})
field(:response_mode, :string, default: "direct_post")
many_to_many :authorized_scopes, Scope,
join_through: "oauth_clients_scopes",
on_replace: :delete
timestamps()
end
def create_changeset(client, attrs) do
client
|> repo().preload(:authorized_scopes)
|> cast(attrs, [
:id,
:public_client_id,
:name,
:secret,
:confidential,
:access_token_ttl,
:agent_token_ttl,
:authorization_code_ttl,
:authorization_request_ttl,
:refresh_token_ttl,
:id_token_ttl,
:redirect_uris,
:authorize_scope,
:enforce_dpop,
:enforce_tx_code,
:supported_grant_types,
:token_endpoint_auth_methods,
:token_endpoint_jwt_auth_alg,
:jwk,
:jwks_uri,
:jwt_public_key,
:check_public_client_id,
:pkce,
:public_refresh_token,
:public_revoke,
:id_token_signature_alg,
:id_token_kid,
:userinfo_signed_response_alg,
:logo_uri,
:metadata,
:response_mode,
:signatures_adapter,
:key_pair_type,
])
|> validate_required([:redirect_uris, :key_pair_type])
|> unique_constraint(:id, name: :clients_pkey)
|> change_access_token_ttl()
|> change_agent_token_ttl()
|> change_authorization_code_ttl()
|> change_authorization_request_ttl()
|> change_id_token_ttl()
|> change_refresh_token_ttl()
|> validate_redirect_uris()
|> validate_supported_grant_types()
|> validate_id_token_signature_alg()
|> validate_inclusion(:response_mode, @response_modes)
|> validate_subset(:token_endpoint_auth_methods, @token_endpoint_auth_methods)
|> validate_inclusion(
:token_endpoint_jwt_auth_alg,
Enum.map(@token_endpoint_jwt_auth_algs, &Atom.to_string/1)
)
|> validate_inclusion(
:userinfo_signed_response_alg,
Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
)
|> put_assoc(:authorized_scopes, parse_authorized_scopes(attrs))
|> translate_jwk()
|> validate_signatures_adapter()
|> validate_key_pair_type()
|> generate_key_pair()
|> put_secret()
|> validate_required(:secret)
end
def update_changeset(client, attrs) do
client
|> repo().preload(:authorized_scopes)
|> cast(attrs, [
:public_client_id,
:name,
:secret,
:confidential,
:access_token_ttl,
:agent_token_ttl,
:authorization_code_ttl,
:authorization_request_ttl,
:refresh_token_ttl,
:id_token_ttl,
:redirect_uris,
:authorize_scope,
:enforce_dpop,
:enforce_tx_code,
:supported_grant_types,
:token_endpoint_auth_methods,
:token_endpoint_jwt_auth_alg,
:jwk,
:jwks_uri,
:jwt_public_key,
:check_public_client_id,
:pkce,
:public_refresh_token,
:public_revoke,
:id_token_signature_alg,
:id_token_kid,
:userinfo_signed_response_alg,
:logo_uri,
:metadata,
:response_mode,
:signatures_adapter,
:key_pair_type
])
|> validate_required([
:authorization_code_ttl,
:access_token_ttl,
:agent_token_ttl,
:refresh_token_ttl,
:id_token_ttl,
:key_pair_type
])
|> validate_inclusion(:access_token_ttl, 1..access_token_max_ttl())
|> validate_inclusion(:agent_token_ttl, 1..agent_token_max_ttl())
|> validate_inclusion(:authorization_code_ttl, 1..authorization_code_max_ttl())
|> validate_inclusion(:access_token_ttl, 1..authorization_request_max_ttl())
|> validate_inclusion(:refresh_token_ttl, 1..refresh_token_max_ttl())
|> validate_inclusion(:refresh_token_ttl, 1..id_token_max_ttl())
|> validate_inclusion(:response_mode, @response_modes)
|> validate_subset(:token_endpoint_auth_methods, @token_endpoint_auth_methods)
|> validate_inclusion(
:token_endpoint_jwt_auth_alg,
Enum.map(@token_endpoint_jwt_auth_algs, &Atom.to_string/1)
)
|> validate_inclusion(
:userinfo_signed_response_alg,
Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
)
|> validate_redirect_uris()
|> validate_supported_grant_types()
|> validate_id_token_signature_alg()
|> put_assoc(:authorized_scopes, parse_authorized_scopes(attrs))
|> validate_signatures_adapter()
|> validate_key_pair_type()
|> translate_jwk()
end
def secret_changeset(client, secret \\ nil) do
client
|> cast(%{secret: secret}, [:secret])
|> put_secret()
|> validate_required(:secret)
end
def did_changeset(client) do
change(client)
|> put_did()
end
def key_pair_changeset(client, attrs \\ %{}) do
client
|> cast(attrs, [:public_key, :private_key])
|> generate_key_pair()
end
defp change_access_token_ttl(changeset) do
case fetch_change(changeset, :access_token_ttl) do
{:ok, _access_token_ttl} ->
validate_inclusion(changeset, :access_token_ttl, 1..access_token_max_ttl())
:error ->
put_change(changeset, :access_token_ttl, access_token_max_ttl())
end
end
defp change_agent_token_ttl(changeset) do
case fetch_change(changeset, :agent_token_ttl) do
{:ok, _agent_token_ttl} ->
validate_inclusion(changeset, :agent_token_ttl, 1..agent_token_max_ttl())
:error ->
put_change(changeset, :agent_token_ttl, agent_token_max_ttl())
end
end
defp change_authorization_code_ttl(changeset) do
case fetch_change(changeset, :authorization_code_ttl) do
{:ok, _authorization_code_ttl} ->
validate_inclusion(changeset, :authorization_code_ttl, 1..authorization_code_max_ttl())
:error ->
put_change(changeset, :authorization_code_ttl, authorization_code_max_ttl())
end
end
defp change_authorization_request_ttl(changeset) do
case fetch_change(changeset, :authorization_request_ttl) do
{:ok, _authorization_request_ttl} ->
validate_inclusion(
changeset,
:authorization_request_ttl,
1..authorization_request_max_ttl()
)
:error ->
put_change(changeset, :authorization_request_ttl, authorization_request_max_ttl())
end
end
defp change_refresh_token_ttl(changeset) do
case fetch_change(changeset, :refresh_token_ttl) do
{:ok, _access_token_ttl} ->
validate_inclusion(changeset, :refresh_token_ttl, 1..refresh_token_max_ttl())
:error ->
put_change(changeset, :refresh_token_ttl, refresh_token_max_ttl())
end
end
defp change_id_token_ttl(changeset) do
case fetch_change(changeset, :id_token_ttl) do
{:ok, _id_token_ttl} ->
validate_inclusion(changeset, :id_token_ttl, 1..id_token_max_ttl())
:error ->
put_change(changeset, :id_token_ttl, id_token_max_ttl())
end
end
defp validate_signatures_adapter(changeset) do
key_pair_type = get_field(changeset, :key_pair_type)
case key_pair_type do
%{"type" => "universal"} ->
validate_inclusion(changeset, :signatures_adapter, [Atom.to_string(Boruta.Universal.Signatures)])
%{"type" => type} when type in ["ec", "rsa"] ->
validate_inclusion(changeset, :signatures_adapter, [Atom.to_string(Boruta.Internal.Signatures)])
_ ->
add_error(changeset, :signatures_adapter, "unknown key pair type")
end
end
defp validate_key_pair_type(changeset) do
key_pair_type = get_field(changeset, :key_pair_type)
case ExJsonSchema.Validator.validate(
@key_pair_type_schema,
key_pair_type,
error_formatter: BorutaFormatter
) do
:ok ->
changeset
|> validate_inclusion(
:id_token_signature_alg,
@key_pair_type_jwt_algs[key_pair_type["type"]]
)
|> validate_inclusion(
:userinfo_signed_response_alg,
@key_pair_type_jwt_algs[key_pair_type["type"]]
)
{:error, errors} ->
add_error(changeset, :key_pair_type, "validation failed: #{Enum.join(errors, " ")}")
end
end
defp validate_redirect_uris(changeset) do
validate_change(changeset, :redirect_uris, fn field, values ->
Enum.map(values, &validate_uri/1)
|> Enum.reject(&is_nil/1)
|> Enum.map(fn error -> {field, error} end)
end)
end
defp validate_supported_grant_types(changeset) do
server_grant_types = Oauth.Client.grant_types()
validate_change(changeset, :supported_grant_types, fn :supported_grant_types,
current_grant_types ->
case Enum.empty?(current_grant_types -- server_grant_types) do
true -> []
false -> [supported_grant_types: "must be part of #{Enum.join(server_grant_types, ", ")}"]
end
end)
end
defp validate_uri(nil), do: "empty values are not allowed"
defp validate_uri("" <> uri) do
case URI.parse(uri) do
%URI{scheme: scheme, host: host, fragment: fragment}
when not is_nil(scheme) and not is_nil(host) and is_nil(fragment) ->
nil
_ ->
"`#{uri}` is invalid"
end
end
defp validate_id_token_signature_alg(changeset) do
signature_algorithms = Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
validate_inclusion(changeset, :id_token_signature_alg, signature_algorithms)
end
defp parse_authorized_scopes(attrs) do
Enum.map(
attrs["authorized_scopes"] || attrs[:authorized_scopes] || [],
fn scope_attrs ->
case apply_action(Scope.assoc_changeset(%Scope{}, scope_attrs), :replace) do
{:ok, %Scope{id: id}} when is_binary(id) ->
repo().get_by(Scope, id: id)
{:ok, %Scope{name: name}} when is_binary(name) ->
repo().get_by(Scope, name: name) || %Scope{name: name}
_ ->
nil
end
end
)
|> Enum.reject(&is_nil/1)
end
defp translate_jwk(%Ecto.Changeset{changes: %{jwk: jwk}} = changeset) do
{_key_type, pem} = JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem()
put_change(changeset, :jwt_public_key, pem)
end
defp translate_jwk(changeset), do: changeset
defp generate_key_pair(%Ecto.Changeset{changes: %{private_key: _private_key}} = changeset) do
changeset
end
defp generate_key_pair(changeset) do
private_key =
case get_field(changeset, :key_pair_type) do
%{"type" => "rsa", "modulus_size" => modulus_size, "exponent_size" => exponent_size} ->
JOSE.JWK.generate_key(
{:rsa, String.to_integer(modulus_size), String.to_integer(exponent_size)}
)
%{"type" => "ec", "curve" => curve} ->
JOSE.JWK.generate_key({:ec, curve})
%{"type" => "universal"} ->
"universal"
_ ->
nil
end
case private_key do
nil ->
add_error(changeset, :private_key, "private_key_type is invalid")
"universal" ->
with {:ok, did, jwk} <- Did.create("key"),
{:ok, key_id} <- Universal.Signatures.SigningKey.get_key_by_did(did) do
"did:key:" <> key = did
public_key = JOSE.JWK.from_map(jwk)
{_type, public_pem} = JOSE.JWK.to_pem(public_key)
changeset
|> put_change(:private_key, key_id["id"])
|> put_change(:public_key, public_pem)
|> put_change(:did, "#{did}##{key}")
|> put_change(:signatures_adapter, Boruta.Universal.Signatures |> Atom.to_string())
|> put_change(:id_token_signature_alg, "EdDSA")
|> put_change(:userinfo_signed_response_alg, "EdDSA")
else
{:error, error} ->
add_error(changeset, :private_key, error)
end
private_key ->
public_key = JOSE.JWK.to_public(private_key)
{_type, public_pem} = JOSE.JWK.to_pem(public_key)
{_type, private_pem} = JOSE.JWK.to_pem(private_key)
changeset
|> put_change(:public_key, public_pem)
|> put_change(:private_key, private_pem)
|> put_change(:signatures_adapter, Boruta.Internal.Signatures |> Atom.to_string())
end
end
defp put_secret(%Ecto.Changeset{data: data, changes: changes} = changeset) do
case fetch_change(changeset, :secret) do
{:ok, nil} ->
put_change(changeset, :secret, token_generator().secret(struct(data, changes)))
{:ok, _secret} ->
changeset
:error ->
put_change(changeset, :secret, token_generator().secret(struct(data, changes)))
end
end
defp put_did(%Ecto.Changeset{} = changeset) do
case get_field(changeset, :public_key) do
nil ->
changeset
pem ->
{_, jwk} = JOSE.JWK.from_pem(pem) |> JOSE.JWK.to_map()
case Did.create("key", jwk) do
{:ok, did, _jwk} ->
"did:key:" <> key = did
put_change(changeset, :did, "#{did}##{key}")
{:error, error} ->
add_error(changeset, :did, error)
end
end
end
end