Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
dc32aad
siopv2 request response encryption
patatoid Feb 9, 2026
04a995a
store code siopv2 client encryption params
patatoid Feb 10, 2026
52f41c5
peek client encryption from id token in direct post response
patatoid Feb 10, 2026
2d87cb1
presentation request response encryption
patatoid Feb 10, 2026
512f6de
direct post encryption alg from client configuration
patatoid Feb 10, 2026
ed98398
add client encryption to encrypted direct post requests
patatoid Feb 11, 2026
0918bf9
add presentation encryption alg
patatoid Feb 11, 2026
2da12ce
whitelist client encryption in ecto adapter
patatoid Feb 11, 2026
60534f4
hide client encryption in a code parameter
patatoid Feb 11, 2026
02305ff
do not revoke direct post code response
patatoid Feb 11, 2026
14262ff
update client encrypion at direct post success
patatoid Feb 11, 2026
e12bc82
fix vp token signature validation
patatoid Feb 11, 2026
ea8053f
fix direct post vp token presentation
patatoid Feb 15, 2026
b657ba5
decrypt oauth token requests / responses
patatoid Feb 28, 2026
78c9cb5
decrypt credential encrypted requests
patatoid Feb 28, 2026
5523ba1
store presentation previous code
patatoid Jun 7, 2025
1cd3c73
do not check public client id in siopv2
patatoid Jun 7, 2025
30ae60b
code chains validation in direct post requests
patatoid Jun 8, 2025
b995b9e
avoid code chains replay attacks
patatoid Jun 8, 2025
08118a9
WIP check public client id in chain
patatoid Jun 8, 2025
dd05262
issuance and presentation siopv2 hybrid grants
patatoid Jun 11, 2025
43a554d
issuance code chains management
patatoid Jun 14, 2025
8fdea6a
validate code in presentation chains
patatoid Jun 14, 2025
2cb5195
oid4vci public client id check error wording
patatoid Jun 14, 2025
d644d15
validate preauthorized code authorization previous code
patatoid Jun 17, 2025
7c78fe8
return preauthorized code errors as query
patatoid Jun 17, 2025
e20da1d
[ssi] refactor check public client id
patatoid Jun 21, 2025
ef0029f
WIP provider policies registration
patatoid Jun 22, 2025
27c6c01
WIP code bound metadata policy
patatoid Jun 22, 2025
052fbd8
Revert "WIP code bound metadata policy"
patatoid Jun 22, 2025
f9dc180
Revert "Revert "WIP code bound metadata policy""
patatoid Jun 22, 2025
788262e
continue in case of siop metadata policy error
patatoid Jun 22, 2025
979bb31
refactoring + apply metadata policies at issuance
patatoid Jun 23, 2025
0e9e967
anonymous user scope authorization
patatoid Jun 24, 2025
bde3d45
add code to credential offer responses
patatoid Jun 24, 2025
438ed60
add client_id to credential offer responses
patatoid Aug 23, 2025
bd77034
credential offer responses redirect to deeplink
patatoid Aug 23, 2025
119c234
revoke code chain at credential issuance
patatoid Aug 23, 2025
ed77582
fix preauthorized code grant tests
patatoid Mar 30, 2026
cbcb563
Merge branch 'master' into provider-policies-registration
patatoid Apr 3, 2026
4badfb2
fix preauthorized code persistence
patatoid Apr 3, 2026
295077b
vp_token preauthorized code hybrid flow implementation
patatoid Apr 4, 2026
05c0c68
enable presentation code chains
patatoid Apr 9, 2026
006d474
implement superset_of constraint in metadata policies
patatoid Apr 9, 2026
597eca9
fix metadata policies validation
patatoid Apr 9, 2026
cf7e6e1
fix verifiable presentation response type parsing
patatoid Apr 12, 2026
6b00202
get metadata policy from signed id token
patatoid Apr 17, 2026
2a21444
add algorithm in exposed jwks keys
patatoid Apr 17, 2026
8cb2f15
fix code chain meadata policy check
patatoid Apr 17, 2026
da91089
validate oauth clients rsa modulus size
patatoid Apr 25, 2026
ad78b61
fix invalid client signature algorithm test
patatoid Apr 25, 2026
04dfb82
add agent token to presentation codes
patatoid Apr 25, 2026
4638f43
authorize responses return token structs instead of values
patatoid Apr 26, 2026
d5f6548
handle siopv2 code response type
patatoid Apr 26, 2026
137e920
fix authorize response redirect to url
patatoid Apr 26, 2026
44689a5
add agent subject from token to resource owners
patatoid Apr 26, 2026
19ee3e4
apply scope restriction to verifiable credential issuance
patatoid Apr 28, 2026
507e2e9
authorize verifiable presentation identifier as scope
patatoid Apr 28, 2026
78c7776
fix preauthorized code without previous code persistence
patatoid Apr 28, 2026
6ca828a
filter scopes for preauthorized code authorization requests
patatoid Apr 28, 2026
07b48aa
filter verifiable presetation scopes
patatoid Apr 28, 2026
8166f33
add requested scope to tokens
patatoid Apr 28, 2026
261facd
fix authorization redirect to url tests
patatoid Apr 28, 2026
ab65a37
request all verifiable credential configured scopes
patatoid Apr 28, 2026
68d15d9
remove authorized scopes from requested scopes
patatoid Apr 28, 2026
83b1070
add code chain scopes in preauthorized responses
patatoid Apr 29, 2026
9640578
update cache on codes updates
patatoid Apr 29, 2026
cb3c131
fix scope authorization against nil values
patatoid Apr 29, 2026
941f756
validate presentation descriptor map count
patatoid Apr 30, 2026
b2a270a
Merge branch 'master' into provider-policies-registration
patatoid May 3, 2026
bb9fffd
reduce resource owner lookups in code grants
patatoid May 4, 2026
2566455
fix preauthorized code scope filtering tests
patatoid May 4, 2026
a6e7884
preserve filtered scope order
patatoid May 4, 2026
38f4d61
preserve siopv2 encryption parameters in presentation requests
patatoid May 4, 2026
401285a
validate authorization requests response types from whitelist
patatoid May 4, 2026
5334970
fix implicit and preauthorize tests + skip replay presentation test
patatoid May 4, 2026
7d1d364
fix jwks tests
patatoid May 4, 2026
f7e00ed
skip invalid public client presentation tests
patatoid May 4, 2026
ff001d3
Merge branch 'master' into provider-policies-registration
patatoid May 13, 2026
b39ebbb
fix direct post handler types
patatoid May 16, 2026
3cfdf61
prefer jwk to kid in credentials presentations validation
patatoid May 16, 2026
e01010b
fix linter warnings
patatoid May 16, 2026
61aced1
fix dialyzer warnings
patatoid May 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
# TODO: enable by default in Credo 1.1
{Credo.Check.Readability.UnnecessaryAliasExpansion, false},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, false},

#
## Refactoring Opportunities
Expand Down Expand Up @@ -129,6 +130,7 @@
{Credo.Check.Warning.RaiseInsideRescue, []},
# TODO enable spec with struct check
{Credo.Check.Warning.SpecWithStruct, false},
{Credo.Check.Warning.StructFieldAmount, [max_fields: 40]},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `AuthorizeResponse` result expose an `Oauth.Token` struct instead of value

### Added

- OAuth Resource Indicators support for authorization, token, refresh, and introspection flows, with RFC 8707 validation and optional client-authorized resources
Expand Down
3 changes: 3 additions & 0 deletions lib/boruta/adapters/codes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ defmodule Boruta.CodesAdapter do

def get_by(params), do: codes().get_by(params)
def create(params), do: codes().create(params)
def update_client_encryption(code, params), do: codes().update_client_encryption(code, params)
def revoke(code), do: codes().revoke(code)
def revoke_previous_token(code), do: codes().revoke_previous_token(code)
def update_sub(code, sub, metadata_policy), do: codes().update_sub(code, sub, metadata_policy)
def code_chain(code), do: codes().code_chain(code)
end
2 changes: 2 additions & 0 deletions lib/boruta/adapters/ecto/access_tokens.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ defmodule Boruta.Ecto.AccessTokens do
redirect_uri = params[:redirect_uri]
previous_token = params[:previous_token]
previous_code = params[:previous_code]
requested_scope = params[:requested_scope]
resource_owner = params[:resource_owner]
agent_token = params[:agent_token]

Expand All @@ -70,6 +71,7 @@ defmodule Boruta.Ecto.AccessTokens do
redirect_uri: redirect_uri,
state: state,
scope: scope,
requested_scope: requested_scope || "",
resource: resource,
access_token_ttl: access_token_ttl,
previous_token: previous_token,
Expand Down
2 changes: 2 additions & 0 deletions lib/boruta/adapters/ecto/agent_tokens.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ defmodule Boruta.Ecto.AgentTokens do
redirect_uri = params[:redirect_uri]
previous_token = params[:previous_token]
previous_code = params[:previous_code]
requested_scope = params[:requested_scope]
resource_owner = params[:resource_owner]
bind_data = params[:bind_data]
bind_configuration = params[:bind_configuration]
Expand All @@ -71,6 +72,7 @@ defmodule Boruta.Ecto.AgentTokens do
redirect_uri: redirect_uri,
state: state,
scope: scope,
requested_scope: requested_scope || "",
resource: resource,
access_token_ttl: agent_token_ttl,
previous_token: previous_token,
Expand Down
6 changes: 4 additions & 2 deletions lib/boruta/adapters/ecto/clients.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ defmodule Boruta.Ecto.Clients do
end
end

defp rsa_key(%Client{public_key: public_key, private_key: private_key}) do
defp rsa_key(%Client{public_key: public_key, private_key: private_key, id_token_signature_alg: id_token_signature_alg}) do
{_type, jwk} = public_key |> :jose_jwk.from_pem() |> :jose_jwk.to_map()

Map.put(jwk, "kid", Oauth.Client.Crypto.kid_from_private_key(private_key))
jwk
|> Map.put("kid", Oauth.Client.Crypto.kid_from_private_key(private_key))
|> Map.put("alg", id_token_signature_alg)
end
end
101 changes: 95 additions & 6 deletions lib/boruta/adapters/ecto/codes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ defmodule Boruta.Ecto.Codes do

def get_by(id: id) do
with {:ok, id} <- Ecto.UUID.cast(id),
{:ok, token} <- TokenStore.get(id: id) do
token
{:ok, token} <- TokenStore.get(id: id) do
token
else
:error -> nil
:error ->
nil

{:error, "Not cached."} ->
with %Token{} = token <-
repo().one(
Expand All @@ -64,7 +66,8 @@ defmodule Boruta.Ecto.Codes do
token

{:error, "Not cached."} ->
with %Token{} = token <-
with "" <> value <- value,
%Token{} = token <-
repo().one(
from t in Token,
where: t.type in ["code", "preauthorized_code"] and t.value == ^value
Expand All @@ -74,6 +77,9 @@ defmodule Boruta.Ecto.Codes do
|> to_oauth_schema()
|> TokenStore.put() do
token
else
{:error, error} -> {:error, error}
nil -> {:error, "Code not found."}
end
end
end
Expand All @@ -100,19 +106,24 @@ defmodule Boruta.Ecto.Codes do
apply(Token, changeset_method(client), [
%Token{resource_owner: params[:resource_owner]},
%{
response_type: params[:response_type],
client_id: client_id,
sub: sub,
redirect_uri: redirect_uri,
state: state,
nonce: params[:nonce],
scope: scope,
requested_scope: params[:requested_scope] || "",
resource: params[:resource],
authorization_code_ttl: authorization_code_ttl,
code_challenge: code_challenge,
code_challenge_method: code_challenge_method,
authorization_details: authorization_details,
presentation_definition: params[:presentation_definition],
public_client_id: params[:public_client_id]
public_client_id: params[:public_client_id],
client_encryption_key: params[:client_encryption_key],
client_encryption_alg: params[:client_encryption_alg],
previous_code: params[:previous_code]
}
])

Expand All @@ -131,13 +142,62 @@ defmodule Boruta.Ecto.Codes do
defp changeset_method(%Oauth.Client{pkce: true}), do: :pkce_code_changeset

@impl Boruta.Oauth.Codes
def update_sub(%Oauth.Token{id: id}, sub, metadata_policy) do
with %Token{} = code <-
repo().one(
from t in Token,
where: t.type in ["code", "preauthorized_code"] and t.id == ^id
),
{:ok, code} <- Token.sub_changeset(code, sub, metadata_policy) |> repo().update(),
{:ok, token} <- TokenStore.put(to_oauth_schema(code)) do
{:ok, token}
else
_ ->
{:error, "Preauthorized code not found."}
end
end

@impl Boruta.Oauth.Codes
def update_client_encryption(%Oauth.Token{value: value}, params) do
with %Token{} = token <- repo().get_by(Token, value: value),
{:ok, token} <- Token.client_encryption_changeset(token, params) |> repo().update(),
token <- to_oauth_schema(token) do
TokenStore.put(token)
end
end

@impl Boruta.Oauth.Codes
def revoke(codes) when is_list(codes) do
code_count = Enum.count(codes)
code_ids = Enum.map(codes, fn code -> code.id end)
now = DateTime.utc_now()

with {^code_count, _} <-
from(t in Token, where: t.id in ^code_ids)
|> repo().update_all(set: [revoked_at: now]),
:ok <-
Enum.reduce(codes, :ok, fn code, acc ->
case TokenStore.invalidate(code) do
{:ok, _token} ->
acc

error ->
error
end
end) do
{:ok, Enum.map(codes, fn code -> %{code | revoked_at: now} end)}
else
_ -> {:error, "Could not revoke code chain."}
end
end

def revoke(%Oauth.Token{value: value} = code) do
with %Token{} = token <- repo().get_by(Token, value: value),
{:ok, token} <-
Token.revoke_changeset(token)
|> repo().update(),
{:ok, _token} <- TokenStore.invalidate(code) do
{:ok, token}
{:ok, %{code | revoked_at: token.revoked_at}}
else
nil ->
{:error, "Code not found."}
Expand All @@ -156,4 +216,33 @@ defmodule Boruta.Ecto.Codes do
{:ok, code}
end
end

@impl Boruta.Oauth.Codes
def code_chain(token, acc \\ [])

def code_chain(%Oauth.Token{previous_code: nil} = code, acc) do
Enum.reject([code | acc], &is_nil/1) |> Enum.reverse()
end

def code_chain(%Oauth.Token{type: "preauthorized_code", previous_code: value} = code, acc) do
case code_chain(get_by(value: value)) do
chain when is_list(chain) ->
[code | acc ++ chain]

_ ->
acc
end
end

def code_chain(%Oauth.Token{type: "code", previous_code: value} = code, acc) do
case code_chain(get_by(value: value)) do
chain when is_list(chain) ->
[code | acc ++ chain]

_ ->
acc
end
end

def code_chain(nil, _acc), do: {:error, "Previous code not found."}
end
25 changes: 13 additions & 12 deletions lib/boruta/adapters/ecto/oauth_mapper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ end
defimpl Boruta.Ecto.OauthMapper, for: Boruta.Ecto.Token do
import Boruta.Config, only: [repo: 0, resource_owners: 0, clients: 0]

alias Boruta.Oauth
alias Boruta.Oauth.ResourceOwner
alias Boruta.Ecto
alias Boruta.Ecto.AgentTokens
alias Boruta.Ecto.OauthMapper
alias Boruta.Oauth
alias Boruta.Oauth.ResourceOwner

def to_oauth_schema(%Ecto.Token{} = token) do
client =
Expand All @@ -33,15 +33,16 @@ defimpl Boruta.Ecto.OauthMapper, for: Boruta.Ecto.Token do
_ -> nil
end

resource_owner = with "" <> agent_token <- token.agent_token,
%Oauth.Token{} = token <- AgentTokens.get_by(value: agent_token),
{:ok, claims} <- AgentTokens.claims_from_agent_token(token) do
resource_owner = resource_owner || %ResourceOwner{sub: nil}
%{resource_owner | extra_claims: Map.merge(resource_owner.extra_claims, claims)}
else
_ ->
resource_owner
end
resource_owner =
with "" <> agent_token <- token.agent_token,
%Oauth.Token{} = token <- AgentTokens.get_by(value: agent_token),
{:ok, claims} <- AgentTokens.claims_from_agent_token(token) do
resource_owner = resource_owner || %ResourceOwner{sub: ResourceOwner.agent_sub()}
%{resource_owner | extra_claims: Map.merge(resource_owner.extra_claims, claims)}
else
_ ->
resource_owner
end

struct(
Oauth.Token,
Expand All @@ -59,9 +60,9 @@ end
defimpl Boruta.Ecto.OauthMapper, for: Boruta.Ecto.Client do
import Boruta.Config, only: [repo: 0]

alias Boruta.Oauth
alias Boruta.Ecto
alias Boruta.Ecto.OauthMapper
alias Boruta.Oauth

def to_oauth_schema(%Ecto.Client{} = client) do
client = repo().preload(client, :authorized_scopes)
Expand Down
23 changes: 16 additions & 7 deletions lib/boruta/adapters/ecto/preauthorized_codes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,25 @@ defmodule Boruta.Ecto.PreauthorizedCodes do
apply(Token, changeset_method(client), [
%Token{resource_owner: resource_owner},
%{
client_id: client_id,
sub: sub,
state: state,
nonce: params[:nonce],
agent_token: params[:agent_token],
scope: scope,
resource: params[:resource],
redirect_uri: redirect_uri,
authorization_code_ttl: authorization_code_ttl,
authorization_details: resource_owner.authorization_details
authorization_details: resource_owner.authorization_details,
client_id: client_id,
client_encryption_key: params[:client_encryption_key],
client_encryption_alg: params[:client_encryption_alg],
code_challenge: params[:code_challenge],
code_challenge_method: params[:code_challenge_method],
nonce: params[:nonce],
presentation_definition: params[:presentation_definition],
previous_code: params[:previous_code],
public_client_id: params[:public_client_id],
redirect_uri: redirect_uri,
response_type: params[:response_type],
scope: scope,
requested_scope: params[:requested_scope] || "",
state: state,
sub: sub
}
])

Expand Down
26 changes: 17 additions & 9 deletions lib/boruta/adapters/ecto/schemas/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ defmodule Boruta.Ecto.Client do
public_key: String.t(),
private_key: String.t(),
response_mode: String.t(),
signatures_adapter: String.t()
signatures_adapter: String.t(),
key_pair_type: map()
}

@token_endpoint_auth_methods [
Expand Down Expand Up @@ -405,9 +406,15 @@ defmodule Boruta.Ecto.Client do

case key_pair_type do
%{"type" => "universal"} ->
validate_inclusion(changeset, :signatures_adapter, [Atom.to_string(Boruta.Universal.Signatures)])
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)])
validate_inclusion(changeset, :signatures_adapter, [
Atom.to_string(Boruta.Internal.Signatures)
])

_ ->
add_error(changeset, :signatures_adapter, "unknown key pair type")
end
Expand Down Expand Up @@ -536,9 +543,12 @@ defmodule Boruta.Ecto.Client do
Regex.match?(@resource_indicator_uri_characters, uri)
end

defp valid_resource_authority?(%URI{authority: nil}), do: true
defp valid_resource_authority?(%URI{host: host}) when is_binary(host) and host != "", do: true
defp valid_resource_authority?(_uri), do: false
defp valid_resource_authority?(%URI{} = uri) do
is_nil(Map.get(uri, :authority)) or valid_resource_host?(uri)
end

defp valid_resource_host?(%URI{host: host}) when is_binary(host) and host != "", do: true
defp valid_resource_host?(_uri), do: false

defp validate_id_token_signature_alg(changeset) do
signature_algorithms = Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
Expand Down Expand Up @@ -582,9 +592,7 @@ defmodule Boruta.Ecto.Client do
%{"type" => "rsa", "modulus_size" => modulus_size, "exponent_size" => exponent_size} ->
case parse_rsa_modulus_size(modulus_size) do
{:ok, modulus_size} when modulus_size >= @minimum_rsa_modulus_size ->
JOSE.JWK.generate_key(
{:rsa, modulus_size, String.to_integer(exponent_size)}
)
JOSE.JWK.generate_key({:rsa, modulus_size, String.to_integer(exponent_size)})

_ ->
nil
Expand Down
Loading
Loading