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

malach-it / boruta_auth / 350fa49e102b52eed8d621f54a7efffe0698a4f4-PR-13

27 Jan 2024 08:12AM UTC coverage: 93.627% (-2.8%) from 96.441%
350fa49e102b52eed8d621f54a7efffe0698a4f4-PR-13

Pull #13

github

pknoth
siopv2 authorize requests
Pull Request #13: OpenID for Verifiable Credential Issuance - draft 11 implementation

206 of 245 new or added lines in 29 files covered. (84.08%)

25 existing lines in 10 files now uncovered.

999 of 1067 relevant lines covered (93.63%)

48.55 hits per line

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

86.57
/lib/boruta/oauth/request/base.ex
1
defmodule Boruta.Oauth.Request.Base do
2
  @moduledoc false
3

4
  alias Boruta.BasicAuth
5
  alias Boruta.Oauth.AuthorizationCodeRequest
6
  alias Boruta.Oauth.ClientCredentialsRequest
7
  alias Boruta.Oauth.CodeRequest
8
  alias Boruta.Oauth.HybridRequest
9
  alias Boruta.Oauth.IntrospectRequest
10
  alias Boruta.Oauth.PasswordRequest
11
  alias Boruta.Oauth.PreauthorizationCodeRequest
12
  alias Boruta.Oauth.PreauthorizedCodeRequest
13
  alias Boruta.Oauth.RefreshTokenRequest
14
  alias Boruta.Oauth.RevokeRequest
15
  alias Boruta.Oauth.SiopV2Request
16
  alias Boruta.Oauth.TokenRequest
17

18
  @spec authorization_header(req_headers :: list()) ::
19
          {:ok, header :: String.t()}
20
          | {:error, :no_authorization_header}
21
  def authorization_header(req_headers) do
22
    case List.keyfind(req_headers, "authorization", 0) do
109✔
23
      nil -> {:error, :no_authorization_header}
67✔
24
      {"authorization", header} -> {:ok, header}
42✔
25
    end
26
  end
27

28
  def build_request(%{"grant_type" => "client_credentials"} = params) do
16✔
29
    {:ok,
30
     %ClientCredentialsRequest{
31
       client_id: params["client_id"],
32
       client_authentication: client_authentication_from_params(params),
33
       scope: params["scope"]
34
     }}
35
  end
36

37
  def build_request(%{"grant_type" => "password"} = params) do
10✔
38
    {:ok,
39
     %PasswordRequest{
40
       client_id: params["client_id"],
41
       client_authentication: client_authentication_from_params(params),
42
       username: params["username"],
43
       password: params["password"],
44
       scope: params["scope"]
45
     }}
46
  end
47

48
  def build_request(%{"grant_type" => "authorization_code"} = params) do
23✔
49
    {:ok,
50
     %AuthorizationCodeRequest{
51
       client_id: params["client_id"],
52
       client_authentication: client_authentication_from_params(params),
53
       code: params["code"],
54
       redirect_uri: params["redirect_uri"],
55
       code_verifier: params["code_verifier"]
56
     }}
57
  end
58

59
  def build_request(
4✔
60
        %{"grant_type" => "urn:ietf:params:oauth:grant-type:pre-authorized_code"} = params
61
      ) do
62
    {:ok,
63
     %PreauthorizationCodeRequest{
64
       preauthorized_code: params["pre-authorized_code"],
65
       code_verifier: params["code_verifier"]
66
     }}
67
  end
68

69
  def build_request(
9✔
70
        %{"response_type" => "urn:ietf:params:oauth:response-type:pre-authorized_code"} = params
71
      ) do
72
    {:ok,
73
     %PreauthorizedCodeRequest{
74
       client_id: params["client_id"],
75
       redirect_uri: params["redirect_uri"],
76
       resource_owner: params["resource_owner"],
77
       state: params["state"],
78
       prompt: params["prompt"],
79
       scope: params["scope"]
80
     }}
81
  end
82

83
  def build_request(%{"grant_type" => "refresh_token"} = params) do
21✔
84
    {:ok,
85
     %RefreshTokenRequest{
86
       client_id: params["client_id"],
87
       client_authentication: client_authentication_from_params(params),
88
       refresh_token: params["refresh_token"],
89
       scope: params["scope"]
90
     }}
91
  end
92

93
  def build_request(%{"response_type" => "code", "client_metadata" => client_metadata} = params) do
94
    request = %SiopV2Request{
3✔
95
      client_id: params["client_id"],
96
      redirect_uri: params["redirect_uri"],
97
      state: params["state"],
98
      nonce: params["nonce"],
99
      prompt: params["prompt"],
100
      code_challenge: params["code_challenge"],
101
      code_challenge_method: params["code_challenge_method"],
102
      scope: params["scope"],
103
      client_metadata: client_metadata
104
    }
105

106
    request =
3✔
107
      case params["authorization_details"] do
108
        nil -> request
3✔
NEW
109
        authorization_details -> %{request | authorization_details: authorization_details}
×
110
      end
111

112
    {:ok, request}
113
  end
114

115
  def build_request(%{"response_type" => "code"} = params) do
116
    request = %CodeRequest{
33✔
117
      client_id: params["client_id"],
118
      redirect_uri: params["redirect_uri"],
119
      resource_owner: params["resource_owner"],
120
      state: params["state"],
121
      nonce: params["nonce"],
122
      prompt: params["prompt"],
123
      code_challenge: params["code_challenge"],
124
      code_challenge_method: params["code_challenge_method"],
125
      scope: params["scope"]
126
    }
127

128
    request =
33✔
129
      case params["authorization_details"] do
130
        nil -> request
30✔
131
        authorization_details -> %{request | authorization_details: authorization_details}
3✔
132
      end
133

134
    {:ok, request}
135
  end
136

137
  def build_request(%{"response_type" => "introspect"} = params) do
10✔
138
    {:ok,
139
     %IntrospectRequest{
140
       client_id: params["client_id"],
141
       client_authentication: client_authentication_from_params(params),
142
       token: params["token"]
143
     }}
144
  end
145

146
  def build_request(%{"response_type" => response_type} = params) do
147
    response_types = String.split(response_type, " ")
85✔
148

149
    case Enum.member?(response_types, "code") do
85✔
150
      true ->
151
        request = %HybridRequest{
41✔
152
          client_id: params["client_id"],
153
          code_challenge: params["code_challenge"],
154
          code_challenge_method: params["code_challenge_method"],
155
          nonce: params["nonce"],
156
          prompt: params["prompt"],
157
          redirect_uri: params["redirect_uri"],
158
          resource_owner: params["resource_owner"],
159
          response_mode: params["response_mode"],
160
          response_types: response_types,
161
          scope: params["scope"],
162
          state: params["state"]
163
        }
164

165
        request =
41✔
166
          case params["authorization_details"] do
167
            nil -> request
41✔
NEW
UNCOV
168
            authorization_details -> %{request | authorization_details: authorization_details}
×
169
          end
170

171
        {:ok, request}
172

173
      false ->
44✔
174
        {:ok,
175
         %TokenRequest{
176
           client_id: params["client_id"],
177
           nonce: params["nonce"],
178
           prompt: params["prompt"],
179
           redirect_uri: params["redirect_uri"],
180
           resource_owner: params["resource_owner"],
181
           response_types: response_types,
182
           scope: params["scope"],
183
           state: params["state"]
184
         }}
185
    end
186
  end
187

188
  # revoke request
189
  def build_request(%{"token" => _} = params) do
12✔
190
    {:ok,
191
     %RevokeRequest{
192
       client_id: params["client_id"],
193
       client_authentication: client_authentication_from_params(params),
194
       token: params["token"],
195
       token_type_hint: params["token_type_hint"]
196
     }}
197
  end
198

199
  def fetch_unsigned_request(%{query_params: %{"request" => request}}) do
200
    case Joken.peek_claims(request) do
2✔
201
      {:ok, params} ->
1✔
202
        {:ok, params}
203

204
      _ ->
1✔
205
        {:error, "Unsigned request jwt param is malformed."}
206
    end
207
  end
208

209
  def fetch_unsigned_request(%{query_params: %{"request_uri" => request_uri}}) do
210
    with %URI{scheme: "" <> _scheme} <- URI.parse(request_uri),
3✔
211
         {:ok, %Finch.Response{body: request, status: 200}} <-
3✔
212
           Finch.build(:get, request_uri) |> Finch.request(OpenIDHttpClient),
213
         {:ok, params} <- Joken.peek_claims(request) do
2✔
214
      {:ok, params}
215
    else
216
      _ ->
217
        {:error, "Could not fetch unsigned request parameter from given URI."}
218
    end
219
  end
220

221
  def fetch_unsigned_request(%{body_params: %{"request" => request}}) do
222
    case Joken.peek_claims(request) do
2✔
223
      {:ok, params} ->
1✔
224
        {:ok, params}
225

226
      _ ->
1✔
227
        {:error, "Unsigned request jwt param is malformed."}
228
    end
229
  end
230

231
  def fetch_unsigned_request(%{body_params: %{"request_uri" => request_uri}}) do
232
    with %URI{scheme: "" <> _scheme} <- URI.parse(request_uri),
5✔
233
         {:ok, %Finch.Response{body: request, status: 200}} <-
3✔
234
           Finch.build(:get, request_uri) |> Finch.request(OpenIDHttpClient),
235
         {:ok, params} <- Joken.peek_claims(request) do
2✔
236
      {:ok, params}
237
    else
238
      _ ->
239
        {:error, "Could not fetch unsigned request parameter from given URI."}
240
    end
241
  end
242

243
  def fetch_unsigned_request(_request) do
235✔
244
    {:ok, %{}}
245
  end
246

247
  def fetch_client_authentication(%{
248
        query_params: %{
249
          "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
250
          "client_assertion" => client_assertion
251
        }
252
      }) do
UNCOV
253
    case Joken.peek_claims(client_assertion) do
×
254
      {:ok, claims} ->
UNCOV
255
        with :ok <- check_issuer(claims),
×
UNCOV
256
             :ok <- check_audience(claims),
×
UNCOV
257
             :ok <- check_expiration(claims) do
×
UNCOV
258
          client_authentication_params = %{
×
259
            "client_id" => claims["sub"],
260
            "client_authentication" => %{"type" => "jwt", "value" => client_assertion}
261
          }
262

263
          {:ok, client_authentication_params}
264
        end
265

UNCOV
266
      {:error, _error} ->
×
267
        {:error, "Could not decode client assertion JWT."}
268
    end
269
  end
270

271
  def fetch_client_authentication(%{
272
        body_params: %{
273
          "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
274
          "client_assertion" => client_assertion
275
        }
276
      }) do
277
    case Joken.peek_claims(client_assertion) do
21✔
278
      {:ok, claims} ->
279
        with :ok <- check_issuer(claims),
18✔
280
             :ok <- check_audience(claims),
15✔
281
             :ok <- check_expiration(claims) do
9✔
282
          client_authentication_params = %{
6✔
283
            "client_id" => claims["sub"],
284
            "client_authentication" => %{"type" => "jwt", "value" => client_assertion}
285
          }
286

287
          {:ok, client_authentication_params}
288
        end
289

290
      {:error, _error} ->
3✔
291
        {:error, "Could not decode client assertion JWT."}
292
    end
293
  end
294

295
  def fetch_client_authentication(%{
296
        req_headers: req_headers,
297
        body_params: %{} = body_params
298
      }) do
299
    with {:ok, authorization_header} <- authorization_header(req_headers),
109✔
300
         {:ok, [client_id, client_secret]} <- BasicAuth.decode(authorization_header) do
42✔
301
      client_authentication_params = %{
37✔
302
        "client_id" => client_id,
303
        "client_authentication" => %{"type" => "basic", "value" => client_secret}
304
      }
305

306
      {:ok, client_authentication_params}
307
    else
308
      {:error, :no_authorization_header} ->
309
        try do
67✔
310
          {:ok,
311
           %{
312
             "client_authentication" => %{
313
               "type" => "post",
314
               "value" => body_params["client_secret"]
315
             }
316
           }}
317
        rescue
UNCOV
318
          _e in ArgumentError ->
×
319
            {:error, "No client authentication method found in request."}
320
        end
321

322
      {:error, reason} ->
323
        {:error, reason}
324
    end
325
  end
326

327
  defp check_issuer(%{"iss" => _iss}), do: :ok
15✔
328

329
  defp check_issuer(_claims),
3✔
330
    do: {:error, "Client assertion iss claim not found in client assertion JWT."}
331

332
  defp check_audience(%{"aud" => aud}) do
333
    server_issuer = Boruta.Config.issuer()
12✔
334

335
    case aud =~ ~r/^#{server_issuer}/ do
12✔
336
      true ->
9✔
337
        :ok
338

339
      false ->
3✔
340
        {:error,
341
         "Client assertion aud claim does not match with authorization server (#{server_issuer})."}
3✔
342
    end
343
  end
344

345
  defp check_audience(_claims),
3✔
346
    do: {:error, "Client assertion aud claim not found in client assertion JWT."}
347

348
  defp check_expiration(%{"exp" => _exp}), do: :ok
6✔
349

350
  defp check_expiration(_claims),
3✔
351
    do: {:error, "Client assertion exp claim not found in client assertion JWT."}
352

353
  defp client_authentication_from_params(%{"client_authentication" => client_authentication}) do
354
    %{type: client_authentication["type"], value: client_authentication["value"]}
92✔
355
  end
356
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