• 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

95.15
/lib/boruta/oauth/authorization.ex
1
defprotocol Boruta.Oauth.Authorization do
2
  @moduledoc """
3
  OAuth requests authorization
4
  """
5

6
  @doc """
7
  Checks if request is valid for token creation for given request, depending of implementation.
8
  """
9
  @spec preauthorize(request :: any()) ::
10
          {:ok, Boruta.Oauth.AuthorizationSuccess.t()} | {:error, Boruta.Oauth.Error.t()}
11
  def preauthorize(request)
46✔
12

13
  @doc """
14
  Creates and returns tokens for given request, depending of implementation.
15
  """
16
  @spec token(request :: any()) ::
17
          {:ok,
18
           Boruta.Oauth.Token.t()
19
           | %{
20
               (type :: :code | :token | :id_token) =>
21
                 token :: Boruta.Oauth.Token.t() | String.t()
22
             }}
23
          | {:error, reason :: term()}
24
          | {:error, Boruta.Oauth.Error.t()}
25
  def token(request)
212✔
26
end
27

28
defmodule Boruta.Oauth.AuthorizationSuccess do
29
  @moduledoc """
30
  Struct encapsulating an authorization success data
31
  """
32

33
  @enforce_keys [:client, :scope]
34
  defstruct response_types: [],
35
            client: nil,
36
            redirect_uri: nil,
37
            resource_owner: nil,
38
            sub: nil,
39
            scope: nil,
40
            state: nil,
41
            nonce: nil,
42
            access_token: nil,
43
            code: nil,
44
            code_challenge: nil,
45
            code_challenge_method: nil,
46
            authorization_details: nil,
47
            presentation_definition: nil,
48
            issuer: nil,
49
            response_mode: nil,
50
            agent_token: nil,
51
            bind_data: nil,
52
            bind_configuration: nil
53

54
  @type t :: %__MODULE__{
55
          response_types: list(String.t()),
56
          client: Boruta.Oauth.Client.t(),
57
          access_token: Boruta.Oauth.Token.t() | nil,
58
          code: Boruta.Oauth.Token.t() | nil,
59
          redirect_uri: String.t() | nil,
60
          sub: String.t() | nil,
61
          resource_owner: Boruta.Oauth.ResourceOwner.t() | nil,
62
          scope: String.t(),
63
          state: String.t() | nil,
64
          nonce: String.t() | nil,
65
          code_challenge: String.t() | nil,
66
          code_challenge_method: String.t() | nil,
67
          authorization_details: list(map()) | nil,
68
          presentation_definition: map() | nil,
69
          issuer: String.t() | nil,
70
          response_mode: String.t() | nil,
71
          agent_token: String.t() | nil,
72
          bind_data: map(),
73
          bind_configuration: map()
74
        }
75
end
76

77
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.ClientCredentialsRequest do
78
  alias Boruta.AccessTokensAdapter
79
  alias Boruta.Dpop
80
  alias Boruta.Oauth.Authorization
81
  alias Boruta.Oauth.AuthorizationSuccess
82
  alias Boruta.Oauth.ClientCredentialsRequest
83
  alias Boruta.Oauth.Token
84

85
  def preauthorize(%ClientCredentialsRequest{
86
        client_id: client_id,
87
        client_authentication: client_source,
88
        scope: scope,
89
        grant_type: grant_type,
90
        dpop: dpop
91
      }) do
92
    with {:ok, client} <-
18✔
93
           Authorization.Client.authorize(
94
             id: client_id,
95
             source: client_source,
96
             grant_type: grant_type
97
           ),
98
         :ok <- Dpop.validate(dpop, client),
15✔
99
         {:ok, scope} <- Authorization.Scope.authorize(scope: scope, against: %{client: client}) do
7✔
100
      {:ok, %AuthorizationSuccess{client: client, scope: scope}}
101
    end
102
  end
103

104
  def token(request) do
105
    with {:ok, %AuthorizationSuccess{client: client, scope: scope}} <- preauthorize(request) do
18✔
106
      with {:ok, access_token} <-
5✔
107
             AccessTokensAdapter.create(
108
               %{
109
                 client: client,
110
                 scope: scope
111
               },
112
               refresh_token: true
113
             ) do
114
        {:ok, %{token: access_token}}
115
      end
116
    end
117
  end
118
end
119

120
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AgentCredentialsRequest do
121
  alias Boruta.AgentTokensAdapter
122
  alias Boruta.Dpop
123
  alias Boruta.Oauth.Authorization
124
  alias Boruta.Oauth.AuthorizationSuccess
125
  alias Boruta.Oauth.AgentCredentialsRequest
126
  alias Boruta.Oauth.Token
127

128
  def preauthorize(%AgentCredentialsRequest{
129
        client_id: client_id,
130
        client_authentication: client_source,
131
        scope: scope,
132
        grant_type: grant_type,
133
        dpop: dpop,
134
        bind_data: bind_data,
135
        bind_configuration: bind_configuration
136
      }) do
137
    with {:ok, client} <-
20✔
138
           Authorization.Client.authorize(
139
             id: client_id,
140
             source: client_source,
141
             grant_type: grant_type
142
           ),
143
         :ok <- Dpop.validate(dpop, client),
17✔
144
         {:ok, scope} <- Authorization.Scope.authorize(scope: scope, against: %{client: client}),
9✔
145
         {:ok, bind_data, bind_configuration} <-
7✔
146
           Authorization.Data.authorize(bind_data, bind_configuration) do
147
      {:ok,
148
       %AuthorizationSuccess{
149
         client: client,
150
         scope: scope,
151
         bind_data: bind_data,
152
         bind_configuration: bind_configuration
153
       }}
154
    end
155
  end
156

157
  def token(request) do
158
    with {:ok,
15✔
159
          %AuthorizationSuccess{
160
            client: client,
161
            scope: scope,
162
            bind_data: bind_data,
163
            bind_configuration: bind_configuration
164
          }} <- preauthorize(request) do
20✔
165
      with {:ok, agent_token} <-
5✔
166
             AgentTokensAdapter.create(
167
               %{
168
                 client: client,
169
                 scope: scope,
170
                 bind_data: bind_data,
171
                 bind_configuration: bind_configuration
172
               },
173
               refresh_token: true
174
             ) do
175
        {:ok, %{agent_token: agent_token}}
176
      end
177
    end
178
  end
179
end
180

181
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PasswordRequest do
182
  alias Boruta.AccessTokensAdapter
183
  alias Boruta.Oauth.Authorization
184
  alias Boruta.Oauth.AuthorizationSuccess
185
  alias Boruta.Oauth.PasswordRequest
186
  alias Boruta.Oauth.ResourceOwner
187
  alias Boruta.Oauth.Token
188

189
  def preauthorize(%PasswordRequest{
190
        client_id: client_id,
191
        client_authentication: client_source,
192
        username: username,
193
        password: password,
194
        scope: scope,
195
        grant_type: grant_type
196
      }) do
197
    with {:ok, client} <-
10✔
198
           Authorization.Client.authorize(
199
             id: client_id,
200
             source: client_source,
201
             grant_type: grant_type
202
           ),
203
         {:ok, %ResourceOwner{sub: sub} = resource_owner} <-
7✔
204
           Authorization.ResourceOwner.authorize(username: username, password: password),
205
         {:ok, scope} <-
5✔
206
           Authorization.Scope.authorize(
207
             scope: scope,
208
             against: %{client: client, resource_owner: resource_owner}
209
           ) do
210
      {:ok, %AuthorizationSuccess{client: client, sub: sub, scope: scope}}
211
    end
212
  end
213

214
  @dialyzer {:no_match, token: 1}
215
  def token(request) do
216
    with {:ok, %AuthorizationSuccess{client: client, sub: sub, scope: scope}} <-
10✔
217
           preauthorize(request) do
218
      with {:ok, access_token} <-
4✔
219
             AccessTokensAdapter.create(
220
               %{
221
                 client: client,
222
                 sub: sub,
223
                 scope: scope
224
               },
225
               refresh_token: true
226
             ) do
227
        {:ok, %{token: access_token}}
228
      end
229
    end
230
  end
231
end
232

233
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AuthorizationCodeRequest do
234
  alias Boruta.AccessTokensAdapter
235
  alias Boruta.CodesAdapter
236
  alias Boruta.Dpop
237
  alias Boruta.Oauth.Authorization
238
  alias Boruta.Oauth.AuthorizationCodeRequest
239
  alias Boruta.Oauth.AuthorizationSuccess
240
  alias Boruta.Oauth.Client
241
  alias Boruta.Oauth.IdToken
242
  alias Boruta.Oauth.ResourceOwner
243
  alias Boruta.Oauth.Scope
244
  alias Boruta.Oauth.Token
245

246
  def preauthorize(%AuthorizationCodeRequest{
247
        client_id: client_id,
248
        client_authentication: client_source,
249
        code: code,
250
        redirect_uri: redirect_uri,
251
        grant_type: grant_type,
252
        code_verifier: code_verifier,
253
        dpop: dpop
254
      }) do
255
    # TODO check client did against request from code phase in case of siopv2 requests
256
    with {:ok, client} <-
25✔
257
           Authorization.Client.authorize(
258
             id: client_id,
259
             source: client_source,
260
             redirect_uri: redirect_uri,
261
             grant_type: grant_type,
262
             code_verifier: code_verifier
263
           ),
264
         :ok <- Dpop.validate(dpop, client),
21✔
265
         {:ok, code} <-
21✔
266
           Authorization.Code.authorize(%{
267
             value: code,
268
             redirect_uri: redirect_uri,
269
             client: client,
270
             code_verifier: code_verifier
271
           }),
272
         {:ok, %ResourceOwner{sub: sub}} <-
12✔
273
           Authorization.ResourceOwner.authorize(resource_owner: code.resource_owner) do
12✔
274
      {:ok,
275
       %AuthorizationSuccess{
276
         client: client,
277
         code: code,
278
         redirect_uri: redirect_uri,
279
         sub: sub,
280
         scope: code.scope,
12✔
281
         nonce: code.nonce,
12✔
282
         authorization_details: code.authorization_details
12✔
283
       }}
284
    end
285
  end
286

287
  def token(request) do
288
    with {:ok,
13✔
289
          %AuthorizationSuccess{
290
            client: client,
291
            code: code,
292
            redirect_uri: redirect_uri,
293
            sub: sub,
294
            scope: scope,
295
            nonce: nonce,
296
            authorization_details: authorization_details
297
          }} <-
25✔
298
           preauthorize(request),
299
         {:ok, access_token} <-
12✔
300
           AccessTokensAdapter.create(
301
             %{
302
               client: client,
303
               redirect_uri: redirect_uri,
304
               previous_code: code.value,
12✔
305
               sub: sub,
306
               scope: scope,
307
               authorization_details: authorization_details
308
             },
309
             refresh_token: true
310
           ),
311
         {:ok, _code} <- CodesAdapter.revoke(code) do
12✔
312
      # TODO check if from an hybrid request
313
      case {Client.public?(client), String.match?(scope, ~r/#{Scope.openid().name}/)} do
12✔
314
        {true, _} ->
1✔
315
          {:ok, %{token: access_token}}
316

317
        {_, true} ->
318
          id_token = IdToken.generate(%{token: access_token}, nonce)
1✔
319

320
          {:ok, %{token: access_token, id_token: id_token}}
321

322
        {_, false} ->
10✔
323
          {:ok, %{token: access_token}}
324
      end
325
    end
326
  end
327
end
328

329
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AgentCodeRequest do
330
  alias Boruta.AgentTokensAdapter
331
  alias Boruta.CodesAdapter
332
  alias Boruta.Dpop
333
  alias Boruta.Oauth.Authorization
334
  alias Boruta.Oauth.AgentCodeRequest
335
  alias Boruta.Oauth.AuthorizationSuccess
336
  alias Boruta.Oauth.Client
337
  alias Boruta.Oauth.IdToken
338
  alias Boruta.Oauth.ResourceOwner
339
  alias Boruta.Oauth.Scope
340
  alias Boruta.Oauth.Token
341

342
  def preauthorize(%AgentCodeRequest{
343
        client_id: client_id,
344
        client_authentication: client_source,
345
        code: code,
346
        redirect_uri: redirect_uri,
347
        grant_type: grant_type,
348
        code_verifier: code_verifier,
349
        dpop: dpop,
350
        bind_data: bind_data,
351
        bind_configuration: bind_configuration
352
      }) do
353
    # TODO check client did against request from code phase in case of siopv2 requests
354
    with {:ok, client} <-
25✔
355
           Authorization.Client.authorize(
356
             id: client_id,
357
             source: client_source,
358
             redirect_uri: redirect_uri,
359
             grant_type: grant_type,
360
             code_verifier: code_verifier
361
           ),
362
         :ok <- Dpop.validate(dpop, client),
21✔
363
         {:ok, code} <-
21✔
364
           Authorization.Code.authorize(%{
365
             value: code,
366
             redirect_uri: redirect_uri,
367
             client: client,
368
             code_verifier: code_verifier
369
           }),
370
         {:ok, %ResourceOwner{sub: sub} = resource_owner} <-
12✔
371
           Authorization.ResourceOwner.authorize(resource_owner: code.resource_owner),
12✔
372
         {:ok, bind_data, bind_configuration} <-
12✔
373
           Authorization.Data.authorize(bind_data, bind_configuration, resource_owner) do
374
      {:ok,
375
       %AuthorizationSuccess{
376
         client: client,
377
         code: code,
378
         redirect_uri: redirect_uri,
379
         sub: sub,
380
         scope: code.scope,
12✔
381
         nonce: code.nonce,
12✔
382
         authorization_details: code.authorization_details,
12✔
383
         bind_data: bind_data,
384
         bind_configuration: bind_configuration,
385
       }}
386
    end
387
  end
388

389
  def token(request) do
390
    with {:ok,
13✔
391
          %AuthorizationSuccess{
392
            client: client,
393
            code: code,
394
            redirect_uri: redirect_uri,
395
            sub: sub,
396
            scope: scope,
397
            nonce: nonce,
398
            authorization_details: authorization_details,
399
            bind_data: bind_data,
400
            bind_configuration: bind_configuration,
401
          }} <-
25✔
402
           preauthorize(request),
403
         {:ok, agent_token} <-
12✔
404
           AgentTokensAdapter.create(
405
             %{
406
               client: client,
407
               redirect_uri: redirect_uri,
408
               previous_code: code.value,
12✔
409
               sub: sub,
410
               scope: scope,
411
               authorization_details: authorization_details,
412
               bind_data: bind_data,
413
               bind_configuration: bind_configuration
414
             },
415
             refresh_token: true
416
           ),
417
         {:ok, _code} <- CodesAdapter.revoke(code) do
12✔
418
      # TODO check if from an hybrid request
419
      case {Client.public?(client), String.match?(scope, ~r/#{Scope.openid().name}/)} do
12✔
NEW
420
        {true, _} ->
×
421
          {:ok, %{agent_token: agent_token}}
422

423
        {_, true} ->
424
          id_token = IdToken.generate(%{token: agent_token}, nonce)
1✔
425

426
          {:ok, %{agent_token: agent_token, id_token: id_token}}
427

428
        {_, false} ->
11✔
429
          {:ok, %{agent_token: agent_token}}
430
      end
431
    end
432
  end
433
end
434

435
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PreauthorizationCodeRequest do
436
  alias Boruta.Oauth.Client
437
  alias Boruta.AccessTokensAdapter
438
  alias Boruta.CodesAdapter
439
  alias Boruta.Oauth.Authorization
440
  alias Boruta.Oauth.PreauthorizationCodeRequest
441
  alias Boruta.Oauth.AuthorizationSuccess
442
  alias Boruta.Oauth.Error
443
  alias Boruta.Oauth.IdToken
444
  alias Boruta.Oauth.ResourceOwner
445
  alias Boruta.Oauth.Scope
446
  alias Boruta.Oauth.Token
447

448
  def preauthorize(%PreauthorizationCodeRequest{
449
        preauthorized_code: preauthorized_code,
450
        tx_code: tx_code
451
      }) do
452
    with {:ok, code} <-
7✔
453
           Authorization.Code.authorize(%{
454
             value: preauthorized_code
455
           }),
456
         :ok <- maybe_check_tx_code(tx_code, code),
4✔
457
         {:ok, %ResourceOwner{sub: sub}} <-
3✔
458
           (case code.agent_token do
3✔
459
             nil ->
460
               Authorization.ResourceOwner.authorize(resource_owner: code.resource_owner)
2✔
461
             _ ->
1✔
462
               {:ok, code.resource_owner}
1✔
463
           end) do
464
      {:ok,
465
       %AuthorizationSuccess{
466
         client: code.client,
3✔
467
         code: code,
468
         sub: sub,
469
         scope: code.scope,
3✔
470
         nonce: code.nonce,
3✔
471
         authorization_details: code.authorization_details,
3✔
472
         agent_token: code.agent_token
3✔
473
       }}
474
    end
475
  end
476

477
  def token(request) do
478
    with {:ok,
4✔
479
          %AuthorizationSuccess{
480
            client: client,
481
            code: code,
482
            sub: sub,
483
            scope: scope,
484
            nonce: nonce,
485
            authorization_details: authorization_details,
486
            agent_token: agent_token
487
          }} <-
7✔
488
           preauthorize(request),
489
         {:ok, access_token} <-
3✔
490
           AccessTokensAdapter.create(
491
             %{
492
               client: client,
493
               previous_code: code.value,
3✔
494
               sub: sub,
495
               scope: scope,
496
               authorization_details: authorization_details,
497
               agent_token: agent_token
498
             },
499
             refresh_token: true
500
           ),
501
         {:ok, _code} <- CodesAdapter.revoke(code) do
3✔
502
      case String.match?(scope, ~r/#{Scope.openid().name}/) do
3✔
503
        true ->
504
          id_token = IdToken.generate(%{token: access_token}, nonce)
×
505

506
          {:ok, %{preauthorized_token: access_token, id_token: id_token}}
507

508
        false ->
3✔
509
          {:ok, %{preauthorized_token: access_token}}
510
      end
511
    end
512
  end
513

514
  defp maybe_check_tx_code(tx_code, %Token{
515
         client: %Client{enforce_tx_code: true},
516
         tx_code: against_tx_code
517
       }) do
518
    case tx_code == against_tx_code do
2✔
519
      true ->
1✔
520
        :ok
521

522
      false ->
1✔
523
        {:error,
524
         %Error{
525
           status: :bad_request,
526
           error: :invalid_request,
527
           error_description: "Given transaction code is invalid."
528
         }}
529
    end
530
  end
531

532
  defp maybe_check_tx_code(_tx_code, _preauthorized_code), do: :ok
2✔
533
end
534

535
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.TokenRequest do
536
  alias Boruta.AccessTokensAdapter
537
  alias Boruta.Oauth.Authorization
538
  alias Boruta.Oauth.AuthorizationSuccess
539
  alias Boruta.Oauth.IdToken
540
  alias Boruta.Oauth.ResourceOwner
541
  alias Boruta.Oauth.Scope
542
  alias Boruta.Oauth.Token
543
  alias Boruta.Oauth.TokenRequest
544

545
  def preauthorize(
546
        %TokenRequest{
547
          response_types: response_types,
548
          client_id: client_id,
549
          redirect_uri: redirect_uri,
550
          resource_owner: resource_owner,
551
          state: state,
552
          nonce: nonce,
553
          scope: scope,
554
          grant_type: grant_type
555
        } = request
556
      ) do
557
    with {:ok, client} <-
27✔
558
           Authorization.Client.authorize(
559
             id: client_id,
560
             source: nil,
561
             redirect_uri: redirect_uri,
562
             grant_type: grant_type
563
           ),
564
         {:ok, %ResourceOwner{sub: sub}} <-
20✔
565
           Authorization.ResourceOwner.authorize(resource_owner: resource_owner),
566
         {:ok, scope} <-
17✔
567
           Authorization.Scope.authorize(
568
             scope: scope,
569
             against: %{client: client, resource_owner: resource_owner}
570
           ),
571
         :ok <- Authorization.Nonce.authorize(request) do
15✔
572
      {:ok,
573
       %AuthorizationSuccess{
574
         response_types: response_types,
575
         resource_owner: resource_owner,
576
         client: client,
577
         redirect_uri: redirect_uri,
578
         sub: sub,
579
         scope: scope,
580
         state: state,
581
         nonce: nonce
582
       }}
583
    end
584
  end
585

586
  def token(request) do
587
    with {:ok,
9✔
588
          %AuthorizationSuccess{
589
            response_types: response_types,
590
            resource_owner: resource_owner,
591
            client: client,
592
            redirect_uri: redirect_uri,
593
            sub: sub,
594
            scope: scope,
595
            state: state,
596
            nonce: nonce
597
          }} <- preauthorize(request) do
19✔
598
      response_types
599
      |> Enum.sort_by(fn response_type -> response_type == "id_token" end)
12✔
600
      |> Enum.reduce({:ok, %{}}, fn
10✔
601
        "id_token", {:ok, tokens} when tokens == %{} ->
602
          case String.match?(scope, ~r/#{Scope.openid().name}/) do
2✔
603
            true ->
604
              base_token = %Token{
1✔
605
                type: "base_token",
606
                client: client,
607
                resource_owner: resource_owner,
608
                redirect_uri: redirect_uri,
609
                sub: sub,
610
                scope: scope,
611
                state: state,
612
                inserted_at: DateTime.utc_now()
613
              }
614

615
              id_token = IdToken.generate(%{base_token: base_token}, nonce)
1✔
616
              {:ok, %{id_token: id_token}}
617

618
            false ->
1✔
619
              {:ok, %{}}
620
          end
621

622
        "id_token", {:ok, tokens} ->
623
          case String.match?(scope, ~r/#{Scope.openid().name}/) do
2✔
624
            true ->
625
              id_token = IdToken.generate(tokens, nonce)
1✔
626
              {:ok, Map.put(tokens, :id_token, id_token)}
627

628
            false ->
1✔
629
              {:ok, tokens}
630
          end
631

632
        "token", {:ok, tokens} ->
633
          with {:ok, access_token} <-
8✔
634
                 AccessTokensAdapter.create(
635
                   %{
636
                     client: client,
637
                     redirect_uri: redirect_uri,
638
                     sub: sub,
639
                     scope: scope,
640
                     state: state,
641
                     resource_owner: resource_owner
642
                   },
643
                   refresh_token: false
644
                 ) do
645
            {:ok, Map.put(tokens, :token, access_token)}
646
          end
647
      end)
648
    end
649
  end
650
end
651

652
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PreauthorizedCodeRequest do
653
  alias Boruta.PreauthorizedCodesAdapter
654
  alias Boruta.Oauth.Authorization
655
  alias Boruta.Oauth.AuthorizationSuccess
656
  alias Boruta.Oauth.Client
657
  alias Boruta.Oauth.CodeRequest
658
  alias Boruta.Oauth.Error
659
  alias Boruta.Oauth.PreauthorizedCodeRequest
660
  alias Boruta.Oauth.ResourceOwner
661
  alias Boruta.Oauth.Token
662

663
  def preauthorize(%PreauthorizedCodeRequest{
664
        agent_token: agent_token,
665
        client_id: client_id,
666
        redirect_uri: redirect_uri,
667
        resource_owner: resource_owner,
668
        state: state,
669
        scope: scope,
670
        grant_type: grant_type
671
      }) do
672
    with {:ok, client} <-
9✔
673
           Authorization.Client.authorize(
674
             id: client_id,
675
             source: nil,
676
             redirect_uri: redirect_uri,
677
             grant_type: grant_type
678
           ),
679
         {:ok, %ResourceOwner{sub: sub} = resource_owner} <-
7✔
680
           (case agent_token do
681
              nil ->
682
                Authorization.ResourceOwner.authorize(resource_owner: resource_owner)
5✔
683

684
              agent_token ->
685
                Authorization.AgentToken.authorize(
2✔
686
                  agent_token: agent_token,
687
                  resource_owner: resource_owner
688
                )
689
            end),
690
         {:ok, scope} <-
5✔
691
           Authorization.Scope.authorize(
692
             scope: scope,
693
             against: %{client: client, resource_owner: resource_owner}
694
           ) do
695
      {:ok,
696
       %AuthorizationSuccess{
697
         client: client,
698
         redirect_uri: redirect_uri,
699
         sub: sub,
700
         scope: scope,
701
         state: state,
702
         resource_owner: resource_owner,
703
         agent_token: agent_token
704
       }}
705
    else
706
      {:error, :invalid_code_challenge} ->
707
        {:error,
708
         %Error{
709
           status: :bad_request,
710
           error: :invalid_request,
711
           error_description: "Code challenge is invalid."
712
         }}
713

714
      error ->
715
        error
5✔
716
    end
717
  end
718

719
  def token(request) do
720
    with {:ok,
5✔
721
          %AuthorizationSuccess{
722
            client: client,
723
            resource_owner: resource_owner,
724
            redirect_uri: redirect_uri,
725
            sub: sub,
726
            scope: scope,
727
            state: state,
728
            nonce: nonce,
729
            agent_token: agent_token
730
          }} <-
9✔
731
           preauthorize(request) do
732
      # TODO create a preauthorized code
733
      with {:ok, preauthorized_code} <-
4✔
734
             PreauthorizedCodesAdapter.create(%{
735
               client: client,
736
               resource_owner: resource_owner,
737
               redirect_uri: redirect_uri,
738
               sub: sub,
739
               scope: scope,
740
               state: state,
741
               nonce: nonce,
742
               agent_token: agent_token
743
             }) do
744
        {:ok, %{preauthorized_code: preauthorized_code}}
745
      end
746
    end
747
  end
748
end
749

750
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.CodeRequest do
751
  alias Boruta.CodesAdapter
752
  alias Boruta.Oauth.Authorization
753
  alias Boruta.Oauth.AuthorizationSuccess
754
  alias Boruta.Oauth.Client
755
  alias Boruta.Oauth.CodeRequest
756
  alias Boruta.Oauth.Error
757
  alias Boruta.Oauth.ResourceOwner
758
  alias Boruta.Oauth.Token
759
  alias Boruta.Openid.VerifiableCredentials
760

761
  def preauthorize(
762
        %CodeRequest{
763
          client_id: client_id,
764
          redirect_uri: redirect_uri,
765
          resource_owner: resource_owner,
766
          state: state,
767
          nonce: nonce,
768
          scope: scope,
769
          code_challenge: code_challenge,
770
          code_challenge_method: code_challenge_method,
771
          authorization_details: authorization_details
772
        } = request
773
      ) do
774
    with {:ok, client} <-
54✔
775
           Authorization.Client.authorize(
776
             id: client_id,
777
             source: nil,
778
             redirect_uri: redirect_uri,
779
             # in order to differentiate code from authorization_code requests
780
             grant_type: "code"
781
           ),
782
         {:ok, %ResourceOwner{sub: sub} = resource_owner} <-
47✔
783
           Authorization.ResourceOwner.authorize(resource_owner: resource_owner),
784
         {:ok, scope} <-
43✔
785
           Authorization.Scope.authorize(
786
             scope: scope,
787
             against: %{client: client, resource_owner: resource_owner}
788
           ),
789
         :ok <- Authorization.Nonce.authorize(request),
39✔
790
         :ok <- VerifiableCredentials.validate_authorization_details(authorization_details),
36✔
791
         :ok <- check_code_challenge(client, code_challenge, code_challenge_method) do
35✔
792
      {:ok,
793
       %AuthorizationSuccess{
794
         client: client,
795
         redirect_uri: redirect_uri,
796
         sub: sub,
797
         scope: scope,
798
         state: state,
799
         nonce: nonce,
800
         code_challenge: code_challenge,
801
         code_challenge_method: code_challenge_method,
802
         resource_owner: resource_owner,
803
         authorization_details: Jason.decode!(authorization_details)
804
       }}
805
    else
806
      {:error, :invalid_code_challenge} ->
807
        {:error,
808
         %Error{
809
           status: :bad_request,
810
           error: :invalid_request,
811
           error_description: "Code challenge is invalid."
812
         }}
813

814
      error ->
815
        error
19✔
816
    end
817
  end
818

819
  def token(request) do
820
    with {:ok,
10✔
821
          %AuthorizationSuccess{
822
            client: client,
823
            resource_owner: resource_owner,
824
            redirect_uri: redirect_uri,
825
            sub: sub,
826
            scope: scope,
827
            state: state,
828
            nonce: nonce,
829
            code_challenge: code_challenge,
830
            code_challenge_method: code_challenge_method,
831
            authorization_details: authorization_details
832
          }} <-
25✔
833
           preauthorize(request) do
834
      with {:ok, code} <-
15✔
835
             CodesAdapter.create(%{
836
               client: client,
837
               resource_owner: resource_owner,
838
               redirect_uri: redirect_uri,
839
               sub: sub,
840
               scope: scope,
841
               state: state,
842
               nonce: nonce,
843
               code_challenge: code_challenge,
844
               code_challenge_method: code_challenge_method,
845
               authorization_details: authorization_details
846
             }) do
847
        {:ok, %{code: code}}
848
      end
849
    end
850
  end
851

852
  @spec check_code_challenge(
853
          client :: Client.t(),
854
          code_challenge :: String.t(),
855
          code_challenge_method :: String.t()
856
        ) :: :ok | {:error, :invalid_code_challenge}
857
  defp check_code_challenge(%Client{pkce: false}, _code_challenge, _code_challenge_method),
28✔
858
    do: :ok
859

860
  defp check_code_challenge(%Client{pkce: true}, "", _code_challenge_method),
×
861
    do: {:error, :invalid_code_challenge}
862

863
  defp check_code_challenge(%Client{pkce: true}, nil, _code_challenge_method),
2✔
864
    do: {:error, :invalid_code_challenge}
865

866
  defp check_code_challenge(%Client{pkce: true}, _code_challenge, _code_challenge_method), do: :ok
5✔
867
end
868

869
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.AuthorizationRequest do
870
  alias Boruta.CodesAdapter
871
  alias Boruta.Oauth.Authorization
872
  alias Boruta.Oauth.AuthorizationRequest
873
  alias Boruta.Oauth.AuthorizationSuccess
874
  alias Boruta.Oauth.Client
875
  alias Boruta.Oauth.CodeRequest
876
  alias Boruta.Oauth.Error
877
  alias Boruta.Oauth.Token
878
  alias Boruta.Openid.VerifiableCredentials
879

880
  def preauthorize(%AuthorizationRequest{
881
        client_id: client_id,
882
        client_authentication: client_authentication,
883
        redirect_uri: redirect_uri,
884
        state: state,
885
        scope: scope,
886
        code_challenge: code_challenge,
887
        code_challenge_method: code_challenge_method
888
      }) do
889
    with {:ok, client} <-
9✔
890
           Authorization.Client.authorize(
891
             id: client_id,
892
             source: client_authentication,
893
             redirect_uri: redirect_uri,
894
             # in order to differentiate code from authorization_code requests
895
             grant_type: "code"
896
           ),
897
         {:ok, scope} <-
6✔
898
           Authorization.Scope.authorize(
899
             scope: scope,
900
             against: %{client: client}
901
           ),
902
         :ok <- check_code_challenge(client, code_challenge, code_challenge_method) do
4✔
903
      {:ok,
904
       %AuthorizationSuccess{
905
         client: client,
906
         redirect_uri: redirect_uri,
907
         scope: scope,
908
         state: state,
909
         code_challenge: code_challenge,
910
         code_challenge_method: code_challenge_method
911
       }}
912
    else
913
      {:error, :invalid_code_challenge} ->
914
        {:error,
915
         %Error{
916
           status: :bad_request,
917
           error: :invalid_request,
918
           error_description: "Code challenge is invalid."
919
         }}
920

921
      error ->
922
        error
5✔
923
    end
924
  end
925

926
  def token(_params), do: raise("Not implemented")
×
927

928
  @spec check_code_challenge(
929
          client :: Client.t(),
930
          code_challenge :: String.t(),
931
          code_challenge_method :: String.t()
932
        ) :: :ok | {:error, :invalid_code_challenge}
933
  defp check_code_challenge(%Client{pkce: false}, _code_challenge, _code_challenge_method),
3✔
934
    do: :ok
935

936
  defp check_code_challenge(%Client{pkce: true}, "", _code_challenge_method),
×
937
    do: {:error, :invalid_code_challenge}
938

939
  defp check_code_challenge(%Client{pkce: true}, nil, _code_challenge_method),
1✔
940
    do: {:error, :invalid_code_challenge}
941

942
  defp check_code_challenge(%Client{pkce: true}, _code_challenge, _code_challenge_method), do: :ok
×
943
end
944

945
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.PresentationRequest do
946
  alias Boruta.CodesAdapter
947
  alias Boruta.Oauth.Authorization
948
  alias Boruta.Oauth.AuthorizationSuccess
949
  alias Boruta.Oauth.CodeRequest
950
  alias Boruta.Oauth.Error
951
  alias Boruta.Oauth.PresentationRequest
952
  alias Boruta.Oauth.Token
953
  alias Boruta.Openid.VerifiableCredentials
954
  alias Boruta.Openid.VerifiablePresentations
955

956
  def preauthorize(
957
        %PresentationRequest{
958
          client_id: client_id,
959
          resource_owner: resource_owner,
960
          redirect_uri: redirect_uri,
961
          state: state,
962
          nonce: nonce,
963
          scope: scope,
964
          code_challenge: code_challenge,
965
          code_challenge_method: code_challenge_method,
966
          authorization_details: authorization_details,
967
          client_metadata: client_metadata
968
        } = request
969
      ) do
970
    with [response_type] = response_types <-
4✔
971
           VerifiablePresentations.response_types(
972
             scope,
973
             resource_owner.presentation_configuration
4✔
974
           ),
975
         {:ok, client} <-
4✔
976
           Authorization.Client.authorize(
977
             id: client_id,
978
             source: nil,
979
             redirect_uri: redirect_uri,
980
             grant_type: response_type
981
           ),
982
         {:ok, scope} <-
4✔
983
           Authorization.Scope.authorize(
984
             scope: scope,
985
             against: %{client: client}
986
           ),
987
         :ok <- Authorization.Nonce.authorize(request),
4✔
988
         :ok <- VerifiableCredentials.validate_authorization_details(authorization_details),
4✔
989
         :ok <- VerifiablePresentations.check_client_metadata(client_metadata),
4✔
990
         presentation_definition <-
4✔
991
           VerifiablePresentations.presentation_definition(
992
             resource_owner.presentation_configuration,
4✔
993
             scope
994
           ) do
995
      {:ok,
996
       %AuthorizationSuccess{
997
         response_types: response_types,
998
         presentation_definition: presentation_definition,
999
         redirect_uri: redirect_uri,
1000
         client: client,
1001
         sub: resource_owner.sub,
4✔
1002
         scope: scope,
1003
         state: state,
1004
         nonce: nonce,
1005
         code_challenge: code_challenge,
1006
         code_challenge_method: code_challenge_method,
1007
         authorization_details: Jason.decode!(authorization_details),
1008
         response_mode: client.response_mode
4✔
1009
       }}
1010
    else
1011
      {:error, :invalid_code_challenge} ->
1012
        {:error,
1013
         %Error{
1014
           status: :bad_request,
1015
           error: :invalid_request,
1016
           error_description: "Code challenge is invalid."
1017
         }}
1018

1019
      error ->
1020
        error
×
1021
    end
1022
  end
1023

1024
  def token(request) do
1025
    with {:ok,
×
1026
          %AuthorizationSuccess{
1027
            response_types: response_types,
1028
            presentation_definition: presentation_definition,
1029
            redirect_uri: redirect_uri,
1030
            client: client,
1031
            sub: sub,
1032
            scope: scope,
1033
            state: state,
1034
            nonce: nonce,
1035
            code_challenge: code_challenge,
1036
            code_challenge_method: code_challenge_method,
1037
            authorization_details: authorization_details,
1038
            response_mode: response_mode
1039
          }} <-
4✔
1040
           preauthorize(request) do
1041
      with {:ok, code} <-
4✔
1042
             CodesAdapter.create(%{
1043
               client: client,
1044
               redirect_uri: redirect_uri,
1045
               sub: sub,
1046
               scope: scope,
1047
               state: state,
1048
               nonce: nonce,
1049
               code_challenge: code_challenge,
1050
               code_challenge_method: code_challenge_method,
1051
               authorization_details: authorization_details,
1052
               presentation_definition: presentation_definition
1053
             }) do
1054
        case response_types do
4✔
1055
          ["id_token"] ->
2✔
1056
            {:ok, %{siopv2_code: code, response_mode: response_mode}}
1057

1058
          ["vp_token"] ->
2✔
1059
            {:ok, %{vp_code: code, response_mode: response_mode}}
1060
        end
1061
      end
1062
    end
1063
  end
1064
end
1065

1066
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.HybridRequest do
1067
  alias Boruta.AccessTokensAdapter
1068
  alias Boruta.CodesAdapter
1069
  alias Boruta.Oauth.Authorization
1070
  alias Boruta.Oauth.AuthorizationSuccess
1071
  alias Boruta.Oauth.CodeRequest
1072
  alias Boruta.Oauth.Error
1073
  alias Boruta.Oauth.HybridRequest
1074
  alias Boruta.Oauth.IdToken
1075
  alias Boruta.Oauth.Scope
1076
  alias Boruta.Oauth.Token
1077

1078
  def preauthorize(%HybridRequest{response_types: response_types} = request) do
1079
    with {:ok, authorization} <-
29✔
1080
           Authorization.preauthorize(struct(CodeRequest, Map.from_struct(request))) do
1081
      {:ok, %{authorization | response_types: response_types}}
1082
    end
1083
  end
1084

1085
  def token(request) do
1086
    with {:ok,
11✔
1087
          %AuthorizationSuccess{
1088
            response_types: response_types,
1089
            client: client,
1090
            resource_owner: resource_owner,
1091
            redirect_uri: redirect_uri,
1092
            sub: sub,
1093
            scope: scope,
1094
            state: state,
1095
            nonce: nonce,
1096
            code_challenge: code_challenge,
1097
            code_challenge_method: code_challenge_method,
1098
            authorization_details: authorization_details
1099
          }} <-
29✔
1100
           preauthorize(request) do
1101
      response_types
1102
      |> Enum.sort_by(fn response_type -> response_type == "id_token" end)
39✔
1103
      |> Enum.reduce({:ok, %{}}, fn
18✔
1104
        "code", {:ok, tokens} when tokens == %{} ->
1105
          with {:ok, code} <-
18✔
1106
                 CodesAdapter.create(%{
1107
                   client: client,
1108
                   resource_owner: resource_owner,
1109
                   redirect_uri: redirect_uri,
1110
                   sub: sub,
1111
                   scope: scope,
1112
                   state: state,
1113
                   nonce: nonce,
1114
                   code_challenge: code_challenge,
1115
                   code_challenge_method: code_challenge_method,
1116
                   authorization_details: authorization_details
1117
                 }) do
1118
            {:ok, Map.put(tokens, :code, code)}
1119
          end
1120

1121
        "id_token", {:ok, tokens} ->
1122
          case String.match?(scope, ~r/#{Scope.openid().name}/) do
7✔
1123
            true ->
1124
              id_token = IdToken.generate(tokens, nonce)
5✔
1125

1126
              {:ok, Map.put(tokens, :id_token, id_token)}
1127

1128
            false ->
2✔
1129
              {:ok, tokens}
1130
          end
1131

1132
        "token", {:ok, tokens} ->
1133
          with {:ok, access_token} <-
13✔
1134
                 AccessTokensAdapter.create(
1135
                   %{
1136
                     client: client,
1137
                     resource_owner: resource_owner,
1138
                     redirect_uri: redirect_uri,
1139
                     sub: sub,
1140
                     scope: scope,
1141
                     state: state
1142
                   },
1143
                   refresh_token: false
1144
                 ) do
1145
            {:ok, Map.put(tokens, :token, access_token)}
1146
          end
1147

1148
        _, {:error, error} ->
1✔
1149
          {:error,
1150
           %Error{
1151
             status: :internal_server_error,
1152
             error: :unknown_error,
1153
             error_description: "An error occurred during token creation: #{inspect(error)}."
1154
           }}
1155
      end)
1156
    end
1157
  end
1158
end
1159

1160
defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.RefreshTokenRequest do
1161
  alias Boruta.AccessTokensAdapter
1162
  alias Boruta.Oauth.Authorization
1163
  alias Boruta.Oauth.AuthorizationSuccess
1164
  alias Boruta.Oauth.Error
1165
  alias Boruta.Oauth.RefreshTokenRequest
1166
  alias Boruta.Oauth.Token
1167

1168
  def preauthorize(%RefreshTokenRequest{
1169
        client_id: client_id,
1170
        client_authentication: client_source,
1171
        refresh_token: refresh_token,
1172
        scope: scope,
1173
        grant_type: grant_type
1174
      }) do
1175
    with {:ok, client} <-
21✔
1176
           Authorization.Client.authorize(
1177
             id: client_id,
1178
             source: client_source,
1179
             grant_type: grant_type
1180
           ),
1181
         {:ok,
1182
          %Token{
1183
            client: ^client,
1184
            sub: sub,
1185
            scope: token_scope
1186
          } = token} <- Authorization.AccessToken.authorize(refresh_token: refresh_token),
14✔
1187
         {:ok, scope} <-
9✔
1188
           Authorization.Scope.authorize(scope: scope || token_scope, against: %{token: token}) do
9✔
1189
      {:ok, %AuthorizationSuccess{client: client, sub: sub, scope: scope, access_token: token}}
1190
    else
1191
      {:ok, _token} ->
1192
        {:error,
1193
         %Error{
1194
           status: :bad_request,
1195
           error: :invalid_grant,
1196
           error_description: "Given refresh token is invalid, revoked, or expired."
1197
         }}
1198

1199
      error ->
1200
        error
12✔
1201
    end
1202
  end
1203

1204
  def token(request) do
1205
    with {:ok,
13✔
1206
          %AuthorizationSuccess{
1207
            client: client,
1208
            sub: sub,
1209
            scope: scope,
1210
            access_token: previous_token
1211
          }} <-
21✔
1212
           preauthorize(request) do
1213
      with {:ok, access_token} <-
8✔
1214
             AccessTokensAdapter.create(
1215
               %{
1216
                 previous_token: previous_token.value,
8✔
1217
                 client: client,
1218
                 sub: sub,
1219
                 scope: scope
1220
               },
1221
               refresh_token: true
1222
             ),
1223
           {:ok, _token} <- AccessTokensAdapter.revoke_refresh_token(previous_token) do
8✔
1224
        {:ok, %{token: access_token}}
1225
      end
1226
    end
1227
  end
1228
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