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

malach-it / boruta_auth / 1fd28ded8c2359e2a760df205edae8662f3b7487-PR-13

28 Jan 2024 10:25AM UTC coverage: 92.059% (-4.4%) from 96.441%
1fd28ded8c2359e2a760df205edae8662f3b7487-PR-13

Pull #13

github

pknoth
ebsi in time issuance flow compliance
Pull Request #13: OpenID for Verifiable Credential Issuance - draft 11 implementation

204 of 261 new or added lines in 30 files covered. (78.16%)

20 existing lines in 10 files now uncovered.

997 of 1083 relevant lines covered (92.06%)

48.73 hits per line

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

98.0
/lib/boruta/adapters/ecto/schemas/client.ex
1
defmodule Boruta.Ecto.Client do
2
  @moduledoc """
3
  Ecto Adapter Client Schema
4
  """
5

6
  use Ecto.Schema
7

8
  import Ecto.Changeset
9

10
  import Boruta.Config,
11
    only: [
12
      token_generator: 0,
13
      repo: 0,
14
      access_token_max_ttl: 0,
15
      authorization_code_max_ttl: 0,
16
      id_token_max_ttl: 0,
17
      refresh_token_max_ttl: 0
18
    ]
19

20
  alias Boruta.Ecto.Scope
21
  alias Boruta.Oauth
22
  alias Boruta.Oauth.Client
23

24
  @type t :: %__MODULE__{
25
          secret: String.t(),
26
          authorize_scope: boolean(),
27
          redirect_uris: list(String.t()),
28
          supported_grant_types: list(String.t()),
29
          pkce: boolean(),
30
          public_refresh_token: boolean(),
31
          public_revoke: boolean(),
32
          access_token_ttl: integer(),
33
          authorization_code_ttl: integer(),
34
          refresh_token_ttl: integer(),
35
          authorized_scopes: Ecto.Association.NotLoaded.t() | list(Scope.t()),
36
          id_token_ttl: integer(),
37
          id_token_signature_alg: String.t(),
38
          token_endpoint_auth_methods: list(String.t()),
39
          token_endpoint_jwt_auth_alg: String.t(),
40
          userinfo_signed_response_alg: String.t() | nil,
41
          jwt_public_key: String.t(),
42
          public_key: String.t(),
43
          private_key: String.t()
44
        }
45

46
  @token_endpoint_auth_methods [
47
    "client_secret_basic",
48
    "client_secret_post",
49
    "client_secret_jwt",
50
    "private_key_jwt"
51
  ]
52

53
  @token_endpoint_jwt_auth_algs [
54
    :RS256,
55
    :RS384,
56
    :RS512,
57
    :HS256,
58
    :HS384,
59
    :HS512
60
  ]
61

62
  @primary_key {:id, Ecto.UUID, autogenerate: true}
63
  @foreign_key_type :binary_id
64
  @timestamps_opts type: :utc_datetime
65
  schema "oauth_clients" do
4,064✔
66
    field(:public_client_id, :string)
67
    field(:name, :string)
68
    field(:secret, :string)
69
    field(:confidential, :boolean, default: false)
70
    field(:authorize_scope, :boolean, default: false)
71
    field(:redirect_uris, {:array, :string}, default: [])
72

73
    field(:supported_grant_types, {:array, :string}, default: Oauth.Client.grant_types())
74

75
    field(:pkce, :boolean, default: false)
76
    field(:public_refresh_token, :boolean, default: false)
77
    field(:public_revoke, :boolean, default: false)
78

79
    field(:access_token_ttl, :integer)
80
    field(:authorization_code_ttl, :integer)
81
    field(:id_token_ttl, :integer)
82
    field(:refresh_token_ttl, :integer)
83

84
    field(:id_token_signature_alg, :string, default: "RS512")
85
    field(:id_token_kid, :string)
86

87
    field(:public_key, :string)
88
    field(:private_key, :string)
89

90
    field(:token_endpoint_auth_methods, {:array, :string}, default: ["client_secret_basic", "client_secret_post"])
91
    field(:token_endpoint_jwt_auth_alg, :string, default: "HS256")
92
    field(:jwt_public_key, :string)
93
    field(:jwk, :map, virtual: true)
94
    field(:jwks_uri, :string)
95

96
    field(:userinfo_signed_response_alg, :string)
97

98
    field(:logo_uri, :string)
99
    field(:metadata, :map, default: %{})
100

101
    many_to_many :authorized_scopes, Scope,
102
      join_through: "oauth_clients_scopes",
103
      on_replace: :delete
104

105
    timestamps()
106
  end
107

108
  def create_changeset(client, attrs) do
109
    client
110
    |> repo().preload(:authorized_scopes)
111
    |> cast(attrs, [
112
      :id,
113
      :name,
114
      :secret,
115
      :confidential,
116
      :access_token_ttl,
117
      :authorization_code_ttl,
118
      :refresh_token_ttl,
119
      :id_token_ttl,
120
      :redirect_uris,
121
      :authorize_scope,
122
      :supported_grant_types,
123
      :token_endpoint_auth_methods,
124
      :token_endpoint_jwt_auth_alg,
125
      :jwk,
126
      :jwks_uri,
127
      :jwt_public_key,
128
      :pkce,
129
      :public_refresh_token,
130
      :public_revoke,
131
      :id_token_signature_alg,
132
      :id_token_kid,
133
      :userinfo_signed_response_alg,
134
      :logo_uri,
135
      :metadata
136
    ])
137
    |> validate_required([:redirect_uris])
138
    |> unique_constraint(:id, name: :clients_pkey)
139
    |> change_access_token_ttl()
140
    |> change_authorization_code_ttl()
141
    |> change_id_token_ttl()
142
    |> change_refresh_token_ttl()
143
    |> validate_redirect_uris()
144
    |> validate_supported_grant_types()
145
    |> validate_id_token_signature_alg()
146
    |> validate_subset(:token_endpoint_auth_methods, @token_endpoint_auth_methods)
147
    |> validate_inclusion(
148
      :token_endpoint_jwt_auth_alg,
149
      Enum.map(@token_endpoint_jwt_auth_algs, &Atom.to_string/1)
150
    )
151
    |> validate_inclusion(
152
      :userinfo_signed_response_alg,
153
      Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
154
    )
155
    |> put_assoc(:authorized_scopes, parse_authorized_scopes(attrs))
156
    |> translate_jwk()
157
    |> generate_key_pair()
158
    |> put_secret()
159
    |> validate_required(:secret)
53✔
160
  end
161

162
  def update_changeset(client, attrs) do
163
    client
164
    |> repo().preload(:authorized_scopes)
165
    |> cast(attrs, [
166
      :name,
167
      :secret,
168
      :confidential,
169
      :access_token_ttl,
170
      :authorization_code_ttl,
171
      :refresh_token_ttl,
172
      :id_token_ttl,
173
      :redirect_uris,
174
      :authorize_scope,
175
      :supported_grant_types,
176
      :token_endpoint_auth_methods,
177
      :token_endpoint_jwt_auth_alg,
178
      :jwk,
179
      :jwks_uri,
180
      :jwt_public_key,
181
      :pkce,
182
      :public_refresh_token,
183
      :public_revoke,
184
      :id_token_signature_alg,
185
      :id_token_kid,
186
      :userinfo_signed_response_alg,
187
      :logo_uri,
188
      :metadata
189
    ])
190
    |> validate_required([
191
      :authorization_code_ttl,
192
      :access_token_ttl,
193
      :refresh_token_ttl,
194
      :id_token_ttl
195
    ])
196
    |> validate_inclusion(:access_token_ttl, 1..access_token_max_ttl())
197
    |> validate_inclusion(:authorization_code_ttl, 1..authorization_code_max_ttl())
198
    |> validate_inclusion(:refresh_token_ttl, 1..refresh_token_max_ttl())
199
    |> validate_inclusion(:refresh_token_ttl, 1..id_token_max_ttl())
200
    |> validate_subset(:token_endpoint_auth_methods, @token_endpoint_auth_methods)
201
    |> validate_inclusion(
202
      :token_endpoint_jwt_auth_alg,
203
      Enum.map(@token_endpoint_jwt_auth_algs, &Atom.to_string/1)
204
    )
205
    |> validate_inclusion(
206
      :userinfo_signed_response_alg,
207
      Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
208
    )
209
    |> validate_redirect_uris()
210
    |> validate_supported_grant_types()
211
    |> validate_id_token_signature_alg()
212
    |> put_assoc(:authorized_scopes, parse_authorized_scopes(attrs))
213
    |> translate_jwk()
14✔
214
  end
215

216
  def secret_changeset(client, secret \\ nil) do
217
    client
218
    |> cast(%{secret: secret}, [:secret])
219
    |> put_secret()
220
    |> validate_required(:secret)
2✔
221
  end
222

223
  def key_pair_changeset(client, attrs \\ %{}) do
224
    client
225
    |> cast(attrs, [:public_key, :private_key])
226
    |> generate_key_pair()
2✔
227
  end
228

229
  defp change_access_token_ttl(changeset) do
230
    case fetch_change(changeset, :access_token_ttl) do
53✔
231
      {:ok, _access_token_ttl} ->
232
        validate_inclusion(changeset, :access_token_ttl, 1..access_token_max_ttl())
2✔
233

234
      :error ->
235
        put_change(changeset, :access_token_ttl, access_token_max_ttl())
51✔
236
    end
237
  end
238

239
  defp change_authorization_code_ttl(changeset) do
240
    case fetch_change(changeset, :authorization_code_ttl) do
53✔
241
      {:ok, _authorization_code_ttl} ->
242
        validate_inclusion(changeset, :authorization_code_ttl, 1..authorization_code_max_ttl())
2✔
243

244
      :error ->
245
        put_change(changeset, :authorization_code_ttl, authorization_code_max_ttl())
51✔
246
    end
247
  end
248

249
  defp change_refresh_token_ttl(changeset) do
250
    case fetch_change(changeset, :refresh_token_ttl) do
53✔
251
      {:ok, _access_token_ttl} ->
252
        validate_inclusion(changeset, :refresh_token_ttl, 1..refresh_token_max_ttl())
1✔
253

254
      :error ->
255
        put_change(changeset, :refresh_token_ttl, refresh_token_max_ttl())
52✔
256
    end
257
  end
258

259
  defp change_id_token_ttl(changeset) do
260
    case fetch_change(changeset, :id_token_ttl) do
53✔
261
      {:ok, _id_token_ttl} ->
262
        validate_inclusion(changeset, :id_token_ttl, 1..id_token_max_ttl())
1✔
263

264
      :error ->
265
        put_change(changeset, :id_token_ttl, id_token_max_ttl())
52✔
266
    end
267
  end
268

269
  defp validate_redirect_uris(changeset) do
270
    validate_change(changeset, :redirect_uris, fn field, values ->
67✔
271
      Enum.map(values, &validate_uri/1)
272
      |> Enum.reject(&is_nil/1)
6✔
273
      |> Enum.map(fn error -> {field, error} end)
6✔
274
    end)
275
  end
276

277
  defp validate_supported_grant_types(changeset) do
278
    server_grant_types = Oauth.Client.grant_types()
67✔
279

280
    validate_change(changeset, :supported_grant_types, fn :supported_grant_types,
67✔
281
                                                          current_grant_types ->
282
      case Enum.empty?(current_grant_types -- server_grant_types) do
2✔
283
        true -> []
1✔
284
        false -> [supported_grant_types: "must be part of #{Enum.join(server_grant_types, ", ")}"]
1✔
285
      end
286
    end)
287
  end
288

UNCOV
289
  defp validate_uri(nil), do: "empty values are not allowed"
×
290

291
  defp validate_uri("" <> uri) do
292
    case URI.parse(uri) do
6✔
293
      %URI{scheme: scheme, host: host, fragment: fragment}
294
      when not is_nil(scheme) and not is_nil(host) and is_nil(fragment) ->
3✔
295
        nil
296

297
      _ ->
298
        "`#{uri}` is invalid"
3✔
299
    end
300
  end
301

302
  defp validate_id_token_signature_alg(changeset) do
303
    signature_algorithms = Enum.map(Client.Crypto.signature_algorithms(), &Atom.to_string/1)
67✔
304
    validate_inclusion(changeset, :id_token_signature_alg, signature_algorithms)
67✔
305
  end
306

307
  defp parse_authorized_scopes(attrs) do
308
    Enum.map(
309
      attrs["authorized_scopes"] || attrs[:authorized_scopes] || [],
67✔
310
      fn scope_attrs ->
311
        case apply_action(Scope.assoc_changeset(%Scope{}, scope_attrs), :replace) do
5✔
312
          {:ok, %Scope{id: id}} when is_binary(id) ->
313
            repo().get_by(Scope, id: id)
2✔
314

315
          {:ok, %Scope{name: name}} when is_binary(name) ->
316
            repo().get_by(Scope, name: name) || %Scope{name: name}
2✔
317

318
          _ ->
1✔
319
            nil
320
        end
321
      end
322
    )
323
    |> Enum.reject(&is_nil/1)
67✔
324
  end
325

326
  defp translate_jwk(%Ecto.Changeset{changes: %{jwk: jwk}} = changeset) do
327
    {_key_type, pem} = JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem()
5✔
328

329
    put_change(changeset, :jwt_public_key, pem)
5✔
330
  end
331

332
  defp translate_jwk(changeset), do: changeset
62✔
333

334
  defp generate_key_pair(%Ecto.Changeset{changes: %{private_key: _private_key}} = changeset) do
335
    changeset
1✔
336
  end
337

338
  defp generate_key_pair(changeset) do
339
    private_key = JOSE.JWK.generate_key({:rsa, 1024, 65_537})
54✔
340
    public_key = JOSE.JWK.to_public(private_key)
54✔
341

342
    {_type, public_pem} = JOSE.JWK.to_pem(public_key)
54✔
343
    {_type, private_pem} = JOSE.JWK.to_pem(private_key)
54✔
344

345
    changeset
346
    |> put_change(:public_key, public_pem)
347
    |> put_change(:private_key, private_pem)
54✔
348
  end
349

350
  defp put_secret(%Ecto.Changeset{data: data, changes: changes} = changeset) do
351
    case fetch_change(changeset, :secret) do
55✔
352
      {:ok, nil} ->
353
        put_change(changeset, :secret, token_generator().secret(struct(data, changes)))
1✔
354

355
      {:ok, _secret} ->
356
        changeset
2✔
357

358
      :error ->
359
        put_change(changeset, :secret, token_generator().secret(struct(data, changes)))
52✔
360
    end
361
  end
362
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