Skip to content

Commit

Permalink
signatures interface / adapter with dependency injection
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Nov 14, 2024
1 parent 9e1b160 commit b84f720
Show file tree
Hide file tree
Showing 19 changed files with 485 additions and 213 deletions.
216 changes: 216 additions & 0 deletions lib/boruta/adapters/internal/signatures.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
defmodule Boruta.Internal.Signatures do
@behaviour Boruta.Oauth.Signatures

defmodule Token do
@moduledoc false

use Joken.Config

def token_config, do: %{}
end

@moduledoc false

alias Boruta.Internal.Signatures.KeyPair
alias Boruta.Oauth.Client

@signature_algorithms [
ES256: [type: :asymmetric, hash_algorithm: :SHA256, binary_size: 16],
ES384: [type: :asymmetric, hash_algorithm: :SHA384, binary_size: 24],
ES512: [type: :asymmetric, hash_algorithm: :SHA512, binary_size: 32],
RS256: [type: :asymmetric, hash_algorithm: :SHA256, binary_size: 16],
RS384: [type: :asymmetric, hash_algorithm: :SHA384, binary_size: 24],
RS512: [type: :asymmetric, hash_algorithm: :SHA512, binary_size: 32],
HS256: [type: :symmetric, hash_algorithm: :SHA256, binary_size: 16],
HS384: [type: :symmetric, hash_algorithm: :SHA384, binary_size: 24],
HS512: [type: :symmetric, hash_algorithm: :SHA512, binary_size: 32]
]

@spec signature_algorithms() :: list(atom())
def signature_algorithms, do: Keyword.keys(@signature_algorithms)

@spec hash_alg(Client.t()) :: hash_alg :: atom()
def hash_alg(%Client{id_token_signature_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:hash_algorithm]

@spec hash_binary_size(Client.t()) :: binary_size :: integer()
def hash_binary_size(%Client{id_token_signature_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:binary_size]

@spec hash(string :: String.t(), client :: Client.t()) :: hash :: String.t()
def hash(string, client) do
hash_alg(client)
|> Atom.to_string()
|> String.downcase()
|> String.to_atom()
|> :crypto.hash(string)
|> binary_part(0, hash_binary_size(client))
|> Base.url_encode64(padding: false)
end

@spec id_token_sign(payload :: map(), client :: Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def id_token_sign(
payload,
%Client{
id_token_signature_alg: signature_alg
} = client
) do
with {:ok, signing_key} <- get_signing_key(client, :id_token) do
signer =
case id_token_signature_type(client) do
:symmetric ->
Joken.Signer.create(signature_alg, signing_key.secret)

:asymmetric ->
Joken.Signer.create(
signature_alg,
%{"pem" => signing_key.private_key},
%{
"kid" => signing_key.kid,
"trust_chain" => signing_key.trust_chain
}
)
end

case Token.encode_and_sign(payload, signer) do
{:ok, token, _payload} ->
token

{:error, error} ->
{:error, "Could not sign the given payload with client credentials: #{inspect(error)}"}
end
end
end

@spec verify_id_token_signature(id_token :: String.t(), jwk :: JOSE.JWK.t()) ::
:ok | {:error, reason :: String.t()}
def verify_id_token_signature(id_token, jwk) do
case Joken.peek_header(id_token) do
{:ok, %{"alg" => alg}} ->
signer = Joken.Signer.create(alg, %{"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem()})

case Token.verify(id_token, signer) do
{:ok, claims} -> {:ok, claims}
{:error, reason} -> {:error, inspect(reason)}
end

{:error, reason} ->
{:error, inspect(reason)}
end
end

@spec userinfo_sign(payload :: map(), client :: Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def userinfo_sign(
payload,
%Client{
userinfo_signed_response_alg: signature_alg
} = client
) do
with {:ok, signing_key} <- get_signing_key(client, :userinfo) do
signer =
case userinfo_signature_type(client) do
:symmetric ->
Joken.Signer.create(signature_alg, signing_key.secret)

:asymmetric ->
Joken.Signer.create(
signature_alg,
%{"pem" => signing_key.private_key},
%{
"kid" => signing_key.kid,
"trust_chain" => signing_key.trust_chain
}
)
end

case Token.encode_and_sign(payload, signer) do
{:ok, token, _payload} ->
token

{:error, error} ->
{:error, "Could not sign the given payload with client credentials: #{inspect(error)}"}
end
end
end

@spec verifiable_credential_sign(payload :: map(), client :: Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def verifiable_credential_sign(
payload,
%Client{
id_token_signature_alg: signature_alg
} = client
) do
with {:ok, signing_key} <- get_signing_key(client, :verifiable_credential) do
signer =
case id_token_signature_type(client) do
:symmetric ->
Joken.Signer.create(signature_alg, signing_key.secret)

:asymmetric ->
Joken.Signer.create(
signature_alg,
%{"pem" => signing_key.private_key},
%{
"typ" => "JWT",
"kid" => signing_key.kid,
"trust_chain" => signing_key.trust_chain
}
)
end

case Token.encode_and_sign(payload, signer) do
{:ok, token, _payload} ->
token

{:error, error} ->
{:error, "Could not sign the given payload with client credentials: #{inspect(error)}"}
end
end
end

@spec kid_from_private_key(private_pem :: String.t()) :: kid :: String.t()
def kid_from_private_key(private_pem) do
:crypto.hash(:md5, private_pem) |> Base.url_encode64() |> String.slice(0..16)
end

@spec userinfo_signature_type(Client.t()) :: userinfo_token_signature_type :: atom()
def userinfo_signature_type(%Client{userinfo_signed_response_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:type]

@spec id_token_signature_type(Client.t()) :: id_token_signature_type :: atom()
def id_token_signature_type(%Client{id_token_signature_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:type]

defp get_signing_key(client, :id_token) do
{:ok,
%KeyPair{
type: :internal,
private_key: client.private_key,
secret: client.secret,
kid: client.id_token_kid || kid_from_private_key(client.private_key)
}}
end

defp get_signing_key(client, :userinfo) do
{:ok,
%KeyPair{
type: :internal,
private_key: client.private_key,
secret: client.secret,
kid: client.id
}}
end

defp get_signing_key(client, :verifiable_credential) do
{:ok,
%KeyPair{
type: :internal,
private_key: client.private_key,
secret: client.secret,
kid: client.did || client.id_token_kid || kid_from_private_key(client.private_key)
}}
end
end
15 changes: 15 additions & 0 deletions lib/boruta/adapters/internal/signatures/key_pair.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Boruta.Internal.Signatures.KeyPair do
@moduledoc false

@enforce_keys [:type]
defstruct [:type, :kid, :public_key, :private_key, :secret, :trust_chain]

@type t :: %__MODULE__{
type: :external | :internal,
public_key: String.t() | nil,
private_key: String.t() | nil,
kid: String.t() | nil,
secret: String.t() | nil,
trust_chain: list(String.t()) | nil
}
end
42 changes: 42 additions & 0 deletions lib/boruta/adapters/signatures.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Boruta.SignaturesAdapter do
@moduledoc """
Encapsulate injected `Boruta.Oauth.Signatures` and `Boruta.Openid.Signatures` adapter in context configuration
"""

@behaviour Boruta.Oauth.Signatures
@behaviour Boruta.Openid.Signatures

import Boruta.Config, only: [signatures: 0]

@impl Boruta.Oauth.Signatures
def signature_algorithms, do: signatures().signature_algorithms()

@impl Boruta.Oauth.Signatures
def hash_alg(client), do: signatures().hash_alg(client)

@impl Boruta.Oauth.Signatures
def hash_binary_size(client), do: signatures().hash_binary_size(client)

@impl Boruta.Oauth.Signatures
def hash(string, client), do: signatures().hash(string, client)

@impl Boruta.Oauth.Signatures
def id_token_sign(payload, client), do: signatures().id_token_sign(payload, client)

@impl Boruta.Oauth.Signatures
def verify_id_token_signature(id_token, jwk),
do: signatures().verify_id_token_signature(id_token, jwk)

@impl Boruta.Oauth.Signatures
def userinfo_sign(payload, client), do: signatures().userinfo_sign(payload, client)

@impl Boruta.Openid.Signatures
def verifiable_credential_sign(payload, client),
do: signatures().verifiable_credential_sign(payload, client)

@impl Boruta.Oauth.Signatures
def kid_from_private_key(private_pem), do: signatures().kid_from_private_key(private_pem)

@impl Boruta.Oauth.Signatures
def userinfo_signature_type(client), do: signatures().userinfo_signature_type(client)
end
File renamed without changes.
12 changes: 10 additions & 2 deletions lib/boruta/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ defmodule Boruta.Config do
resource_owners: MyApp.ResourceOwners, # mandatory for user flows
scopes: Boruta.Ecto.Scopes,
requests: Boruta.Ecto.Requests,
credentials: Boruta.Ecto.Credentials
credentials: Boruta.Ecto.Credentials,
signatures: Boruta.Internal.Signatures
],
max_ttl: [
authorization_code: 60,
Expand Down Expand Up @@ -46,7 +47,8 @@ defmodule Boruta.Config do
resource_owners: nil,
scopes: Boruta.Ecto.Scopes,
requests: Boruta.Ecto.Requests,
credentials: Boruta.Ecto.Credentials
credentials: Boruta.Ecto.Credentials,
signatures: Boruta.Internal.Signatures
],
max_ttl: [
authorization_request: 300,
Expand Down Expand Up @@ -155,6 +157,12 @@ defmodule Boruta.Config do
Keyword.fetch!(oauth_config(), :contexts)[:credentials]
end

@spec signatures() :: module()
@doc false
def signatures do
Keyword.fetch!(oauth_config(), :contexts)[:signatures]
end

@spec resource_owners() :: module()
@doc false
def resource_owners do
Expand Down
8 changes: 4 additions & 4 deletions lib/boruta/oauth/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.CodeRequest do
alias Boruta.Oauth.Error
alias Boruta.Oauth.ResourceOwner
alias Boruta.Oauth.Token
alias Boruta.VerifiableCredentials
alias Boruta.Openid.VerifiableCredentials

def preauthorize(
%CodeRequest{
Expand Down Expand Up @@ -657,7 +657,7 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AuthorizationRequest do
alias Boruta.Oauth.CodeRequest
alias Boruta.Oauth.Error
alias Boruta.Oauth.Token
alias Boruta.VerifiableCredentials
alias Boruta.Openid.VerifiableCredentials

def preauthorize(%AuthorizationRequest{
client_id: client_id,
Expand Down Expand Up @@ -732,8 +732,8 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PresentationRequest do
alias Boruta.Oauth.Error
alias Boruta.Oauth.PresentationRequest
alias Boruta.Oauth.Token
alias Boruta.VerifiableCredentials
alias Boruta.VerifiablePresentations
alias Boruta.Openid.VerifiableCredentials
alias Boruta.Openid.VerifiablePresentations

def preauthorize(
%PresentationRequest{
Expand Down
19 changes: 19 additions & 0 deletions lib/boruta/oauth/contexts/signatures.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Boruta.Oauth.Signatures do
@moduledoc """
TODO Utilities to provide signature abilities to OAuth clients
"""

@callback signature_algorithms() :: list(atom())
@callback hash_alg(Boruta.Oauth.Client.t()) :: hash_alg :: atom()
@callback hash_binary_size(Boruta.Oauth.Client.t()) :: binary_size :: integer()
@callback hash(string :: String.t(), client :: Boruta.Oauth.Client.t()) :: hash :: String.t()
@callback id_token_sign(payload :: map(), client :: Boruta.Oauth.Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
@callback verify_id_token_signature(id_token :: String.t(), jwk :: JOSE.JWK.t()) ::
:ok | {:error, reason :: String.t()}
@callback userinfo_sign(payload :: map(), client :: Boruta.Oauth.Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
@callback kid_from_private_key(private_pem :: String.t()) :: kid :: String.t()
@callback userinfo_signature_type(Boruta.Oauth.Client.t()) ::
userinfo_token_signature_type :: atom()
end
2 changes: 1 addition & 1 deletion lib/boruta/oauth/request/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ defmodule Boruta.Oauth.Request.Base do
alias Boruta.Oauth.RefreshTokenRequest
alias Boruta.Oauth.RevokeRequest
alias Boruta.Oauth.TokenRequest
alias Boruta.Openid.VerifiableCredentials
alias Boruta.RequestsAdapter
alias Boruta.VerifiableCredentials

@spec authorization_header(req_headers :: list()) ::
{:ok, header :: String.t()}
Expand Down
Loading

0 comments on commit b84f720

Please sign in to comment.