• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

malach-it / boruta_auth / 954161a5b397cc94069ec938dc16fcb785e37074-PR-29

18 Jan 2025 10:28PM UTC coverage: 85.651% (-4.3%) from 89.944%
954161a5b397cc94069ec938dc16fcb785e37074-PR-29

Pull #29

github

patatoid
refactor verifiable credentials status tokens
Pull Request #29: Agent credentials PoC

188 of 304 new or added lines in 20 files covered. (61.84%)

3 existing lines in 1 file now uncovered.

1552 of 1812 relevant lines covered (85.65%)

85.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.47
/lib/boruta/oauth/schemas/client.ex
1
defmodule Boruta.Oauth.Client do
2
  @moduledoc """
3
  OAuth client schema and utilities
4
  """
5

6
  defmodule Token do
7
    @moduledoc false
8

9
    use Joken.Config
10

11
    def token_config, do: %{}
14✔
12
  end
13

14
  @enforce_keys [:id]
15
  defstruct id: nil,
16
            public_client_id: nil,
17
            name: nil,
18
            secret: nil,
19
            confidential: nil,
20
            authorize_scope: nil,
21
            authorized_scopes: [],
22
            enforce_dpop: nil,
23
            enforce_tx_code: nil,
24
            redirect_uris: [],
25
            supported_grant_types: [],
26
            access_token_ttl: nil,
27
            agent_token_ttl: nil,
28
            id_token_ttl: nil,
29
            authorization_code_ttl: nil,
30
            authorization_request_ttl: nil,
31
            refresh_token_ttl: nil,
32
            pkce: nil,
33
            public_refresh_token: nil,
34
            public_revoke: nil,
35
            id_token_signature_alg: nil,
36
            id_token_kid: nil,
37
            userinfo_signed_response_alg: nil,
38
            token_endpoint_auth_methods: nil,
39
            token_endpoint_jwt_auth_alg: nil,
40
            jwt_public_key: nil,
41
            jwks_uri: nil,
42
            public_key: nil,
43
            private_key: nil,
44
            did: nil,
45
            logo_uri: nil,
46
            response_mode: nil,
47
            metadata: %{},
48
            signatures_adapter: nil
49

50
  @type t :: %__MODULE__{
51
          id: any(),
52
          public_client_id: String.t() | nil,
53
          secret: String.t(),
54
          confidential: boolean(),
55
          name: String.t(),
56
          authorize_scope: boolean(),
57
          authorized_scopes: list(Boruta.Oauth.Scope.t()),
58
          enforce_dpop: boolean(),
59
          enforce_tx_code: boolean(),
60
          redirect_uris: list(String.t()),
61
          supported_grant_types: list(String.t()),
62
          access_token_ttl: integer(),
63
          agent_token_ttl: integer(),
64
          id_token_ttl: integer(),
65
          authorization_code_ttl: integer(),
66
          authorization_request_ttl: integer(),
67
          refresh_token_ttl: integer(),
68
          pkce: boolean(),
69
          public_refresh_token: boolean(),
70
          public_revoke: boolean(),
71
          id_token_signature_alg: String.t(),
72
          id_token_kid: String.t() | nil,
73
          userinfo_signed_response_alg: String.t() | nil,
74
          token_endpoint_auth_methods: list(String.t()),
75
          token_endpoint_jwt_auth_alg: String.t(),
76
          jwt_public_key: String.t(),
77
          jwks_uri: String.t() | nil,
78
          public_key: String.t(),
79
          private_key: String.t(),
80
          did: String.t() | nil,
81
          logo_uri: String.t() | nil,
82
          response_mode: String.t(),
83
          metadata: map(),
84
          signatures_adapter: String.t()
85
        }
86

87
  @wallet_grant_types [
88
    "id_token",
89
    "vp_token",
90
    "authorization_code",
91
    "agent_credentials"
92
  ]
93

94
  @grant_types Enum.uniq(
95
                 [
96
                   "client_credentials",
97
                   "password",
98
                   "authorization_code",
99
                   "agent_code",
100
                   "preauthorized_code",
101
                   "refresh_token",
102
                   "implicit",
103
                   "revoke",
104
                   "introspect"
105
                 ] ++ @wallet_grant_types
106
               )
107

108
  @doc """
109
  Returns grant types supported by the server. `Boruta.Oauth.Client` `supported_grant_types` attribute may be a subset of them.
110
  """
111
  @spec grant_types() :: grant_types :: list(String.t())
112
  def grant_types, do: @grant_types
142✔
113

114
  @spec grant_type_supported?(client :: t(), grant_type :: String.t()) :: boolean()
115
  def grant_type_supported?(%__MODULE__{supported_grant_types: supported_grant_types}, "code") do
116
    Enum.member?(supported_grant_types, "authorization_code") ||
56✔
117
      Enum.member?(supported_grant_types, "agent_code")
3✔
118
  end
119

120
  def grant_type_supported?(
121
        %__MODULE__{supported_grant_types: supported_grant_types},
122
        "preauthorization_code"
123
      ) do
124
    Enum.member?(supported_grant_types, "preauthorized_code")
×
125
  end
126

127
  def grant_type_supported?(%__MODULE__{supported_grant_types: supported_grant_types}, grant_type) do
128
    Enum.member?(supported_grant_types, grant_type)
168✔
129
  end
130

131
  @doc """
132
  Returns wallet grant types supported by the server. `Boruta.Oauth.Client` `supported_grant_types` attribute may be a subset of them.
133
  """
134
  @spec wallet_grant_types() :: grant_types :: list(String.t())
135
  def wallet_grant_types, do: @wallet_grant_types
×
136

137
  @spec wallet_grant_type_supported?(client :: t(), grant_type :: String.t()) :: boolean()
138
  def wallet_grant_type_supported?(
139
        %__MODULE__{supported_grant_types: supported_grant_types},
140
        grant_type
141
      )
142
      when grant_type in @wallet_grant_types do
143
    Enum.member?(supported_grant_types, grant_type)
3✔
144
  end
145

146
  def wallet_grant_type_supported?(_client, _grant_type), do: false
1✔
147

148
  @spec check_secret(client :: t(), secret :: String.t()) :: :ok | {:error, String.t()}
149
  def check_secret(%__MODULE__{secret: secret}, secret), do: :ok
62✔
150
  def check_secret(_client, _secret), do: {:error, "Invalid client secret."}
9✔
151

152
  @spec check_redirect_uri(client :: t(), redirect_uri :: String.t()) ::
153
          :ok | {:error, String.t()}
154
  def check_redirect_uri(%__MODULE__{redirect_uris: client_redirect_uris}, redirect_uri) do
155
    case Enum.any?(client_redirect_uris, fn client_redirect_uri ->
141✔
156
           redirect_uri_regex =
141✔
157
             client_redirect_uri
158
             |> Regex.escape()
159
             |> String.replace("\\*", "([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9])")
160

161
           redirect_uri_regex =
141✔
162
             "^#{redirect_uri_regex}$"
141✔
163
             |> Regex.compile!()
164

165
           Regex.match?(redirect_uri_regex, redirect_uri)
141✔
166
         end) do
167
      true -> :ok
135✔
168
      false -> {:error, "Client redirect_uri do not match."}
6✔
169
    end
170
  end
171

172
  @spec should_check_secret?(client :: t(), grant_type :: String.t()) :: boolean()
173
  def should_check_secret?(_client, grant_type)
80✔
174
      when grant_type in ["implicit", "code", "preauthorized_code"],
175
      do: false
176

177
  def should_check_secret?(client, grant_type) when grant_type in ["refresh_token", "revoke"] do
178
    not apply(__MODULE__, :"public_#{grant_type}?", [client])
22✔
179
  end
180

181
  def should_check_secret?(%__MODULE__{public_client_id: "" <> _client_id}, _grant_type),
×
182
    do: false
183

184
  def should_check_secret?(%__MODULE__{confidential: true}, _grant_type), do: true
7✔
185

186
  def should_check_secret?(_client, grant_type)
57✔
187
      when grant_type in ["client_credentials", "agent_credentials", "introspect"],
188
      do: true
189

190
  def should_check_secret?(%__MODULE__{confidential: false}, _grant_type), do: false
46✔
191

192
  @spec public_refresh_token?(client :: t()) :: boolean()
193
  def public_refresh_token?(%__MODULE__{public_refresh_token: public_refresh_token}) do
194
    public_refresh_token
15✔
195
  end
196

197
  @spec public_revoke?(client :: t()) :: boolean()
198
  def public_revoke?(%__MODULE__{public_revoke: public_revoke}) do
199
    public_revoke
7✔
200
  end
201

202
  @spec public?(client :: t()) :: boolean()
203
  def public?(%__MODULE__{public_client_id: public_client_id}) when is_binary(public_client_id),
2✔
204
    do: true
205

206
  def public?(%__MODULE__{public_client_id: _public_client_id}), do: false
64✔
207

208
  @spec signatures_adapter(client :: t()) :: signatures_adapter :: atom()
209
  def signatures_adapter(%__MODULE__{signatures_adapter: signatures_adapter}) do
210
    String.to_atom(signatures_adapter)
58✔
211
  end
212

213
  defmodule Crypto do
214
    @moduledoc false
215

216
    alias Boruta.Oauth.Client
217

218
    @signature_algorithms [
219
      :ES256,
220
      :ES384,
221
      :ES512,
222
      :RS256,
223
      :RS384,
224
      :RS512,
225
      :HS256,
226
      :HS384,
227
      :HS512,
228
      :EdDSA
229
    ]
230

231
    @spec signature_algorithms() :: list(atom())
232
    def signature_algorithms, do: @signature_algorithms
222✔
233

234
    @spec hash_alg(Client.t()) :: hash_alg :: atom()
NEW
235
    def hash_alg(client), do: Client.signatures_adapter(client).hash_alg(client)
×
236

237
    @spec hash_binary_size(Client.t()) :: binary_size :: integer()
NEW
238
    def hash_binary_size(client), do: Client.signatures_adapter(client).hash_binary_size(client)
×
239

240
    @spec hash(string :: String.t(), client :: Client.t()) :: hash :: String.t()
241
    def hash(string, client), do: Client.signatures_adapter(client).hash(string, client)
25✔
242

243
    @spec id_token_sign(payload :: map(), client :: Client.t()) ::
244
            jwt :: String.t() | {:error, reason :: String.t()}
245
    def id_token_sign(payload, client),
246
      do: Client.signatures_adapter(client).id_token_sign(payload, client)
21✔
247

248
    @spec verify_id_token_signature(id_token :: String.t(), jwk :: JOSE.JWK.t()) ::
249
            :ok | {:error, reason :: String.t()}
250
    def verify_id_token_signature(id_token, jwk) do
251
      case Joken.peek_header(id_token) do
5✔
252
        {:ok, %{"alg" => "EdDSA"}} ->
NEW
253
          signer =
×
254
            Joken.Signer.create(
255
              "EdDSA",
256
              %{"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem()},
257
              %{"alg" => "EdDSA"}
258
            )
259

NEW
260
          case Token.verify(id_token, signer) do
×
NEW
261
            {:ok, claims} -> {:ok, claims}
×
NEW
262
            {:error, reason} -> {:error, inspect(reason)}
×
263
          end
264

265
        {:ok, %{"alg" => alg}} ->
266
          signer =
5✔
267
            Joken.Signer.create(alg, %{"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem()})
268

269
          case Token.verify(id_token, signer) do
5✔
270
            {:ok, claims} -> {:ok, claims}
5✔
271
            {:error, reason} -> {:error, inspect(reason)}
×
272
          end
273

274
        {:error, reason} ->
×
275
          {:error, inspect(reason)}
276
      end
277
    end
278

279
    @spec userinfo_sign(payload :: map(), client :: Client.t()) ::
280
            jwt :: String.t() | {:error, reason :: String.t()}
281
    def userinfo_sign(payload, client),
282
      do: Client.signatures_adapter(client).userinfo_sign(payload, client)
2✔
283

284
    @spec kid_from_private_key(private_pem :: String.t()) :: kid :: String.t()
285
    def kid_from_private_key(private_pem) do
286
      :crypto.hash(:md5, private_pem) |> Base.url_encode64() |> String.slice(0..16)
35✔
287
    end
288

289
    @spec userinfo_signature_type(Client.t()) :: userinfo_token_signature_type :: atom()
290
    def userinfo_signature_type(client),
NEW
291
      do: Client.signatures_adapter(client).userinfo_signature_type(client)
×
292
  end
293
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc