Skip to content

Commit 2fd666c

Browse files
authored
Merge pull request #167 from pow-auth/cast-user-claims-values
Cast user claim values
2 parents f694eff + d453a8f commit 2fd666c

File tree

13 files changed

+279
-40
lines changed

13 files changed

+279
-40
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@
44

55
**This release consists of breaking changes.**
66

7+
Userinfo is now cast to the correct type per https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.1. When upgrading you must ensure that you do not depend on a specific type in the returned userinfo for any of the strategies listed below.
8+
79
### Breaking changes
810

911
* `Assent.Strategy.Auth0.authorize_url/2` no longer accepts `:domain` config, use `:base_url` instead
12+
* `Assent.Strategy.Basecamp.callback/2` now encodes `sub` as a `binary()` instead of an `integer()`
13+
* `Assent.Strategy.Github.callback/2` now encodes `sub` as a `binary()` instead of an `integer()`
14+
* `Assent.Strategy.Google` now encodes `email_verified` as a `boolean()` instead of a `binary()`
1015
* `Assent.Strategy.Google` now return `hd` instead of `google_hd`
16+
* `Assent.Strategy.Strava.callback/2` now encodes `sub` as a `binary()` instead of an `integer()`
17+
* `Assent.Strategy.Telegram.callback/2` now encodes `sub` as a `binary()` instead of an `integer()`
18+
* `Assent.Strategy.Twitter.callback/2` now encodes `sub` as a `binary()` instead of an `integer()`
19+
* `Assent.Strategy.VK.callback/2` now encodes `sub` as a `binary()` instead of an `integer()`
1120
* `:site` configuration option removed, use `:base_url` instead
1221
* `Assent.Strategy.OAuth2.authorize_url/2` no longer allows `:state` in `:authorization_params`
1322
* `Assent.Strategy.decode_response/2`removed, use `Assent.HTTPAdapter.decode_response/2` instead
1423
* `Assent.Strategy.request/5` removed, use `Assent.Strategy.http_request/5` instead
24+
* `Assent.Strategy.prune/1` removed
1525
* `Assent.MissingParamError` no longer accepts `:expected_key`, use `:key` instead
1626
* `Assent.HTTPAdapter.Mint` removed
1727
* `Assent.Config` removed
@@ -21,6 +31,7 @@
2131
* `Assent.Strategy.Auth0` now uses OIDC instead of OAuth 2.0 base strategy
2232
* `Assent.Strategy.Gitlab` now uses OIDC instead of OAuth 2.0 base strategy
2333
* `Assent.Strategy.Google` now uses OIDC instead of OAuth 2.0 base strategy
34+
* `Assent.Strategy.normalize_userinfo/2` now casts the user claims per OpenID specification
2435

2536
## v0.2
2637

lib/assent.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,35 @@ defmodule Assent do
132132
end
133133
end
134134

135+
defmodule CastClaimsError do
136+
defexception [:claims, :invalid_types]
137+
138+
@type t :: %__MODULE__{
139+
claims: map(),
140+
invalid_types: map()
141+
}
142+
143+
def message(exception) do
144+
"""
145+
The following claims couldn't be cast:
146+
147+
#{exception.invalid_types |> to_lines() |> Enum.join("\n")}
148+
"""
149+
end
150+
151+
defp to_lines(claim_types, prepend \\ "") do
152+
claim_types
153+
|> Enum.sort_by(&elem(&1, 0))
154+
|> Enum.reduce([], fn
155+
{key, %{} = claim_types}, acc ->
156+
acc ++ to_lines(claim_types, prepend <> "#{inspect(key)} -> ")
157+
158+
{key, type}, acc ->
159+
acc ++ ["- #{prepend}#{inspect(key)} to #{inspect(type)}"]
160+
end)
161+
end
162+
end
163+
135164
@doc """
136165
Fetches the key value from the configuration.
137166

lib/assent/strategies/auth0.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,15 @@ defmodule Assent.Strategy.Auth0 do
2626
client_authentication_method: "client_secret_post"
2727
]
2828
end
29+
30+
@impl true
31+
def normalize(_config, user) do
32+
{:ok, updated_at, 0} = DateTime.from_iso8601(user["updated_at"])
33+
34+
{:ok,
35+
%{
36+
user
37+
| "updated_at" => DateTime.to_unix(updated_at)
38+
}}
39+
end
2940
end

lib/assent/strategy.ex

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ defmodule Assent.Strategy do
2626
end
2727
end
2828
"""
29+
alias Assent.CastClaimsError
30+
2931
@callback authorize_url(Keyword.t()) ::
3032
{:ok, %{:url => binary(), optional(atom()) => any()}} | {:error, term()}
3133
@callback callback(Keyword.t(), map()) ::
@@ -124,36 +126,126 @@ defmodule Assent.Strategy do
124126

125127
defp encode_value(value), do: URI.encode_www_form(Kernel.to_string(value))
126128

129+
@registered_claim_member_types %{
130+
"sub" => :binary,
131+
"name" => :binary,
132+
"given_name" => :binary,
133+
"family_name" => :binary,
134+
"middle_name" => :binary,
135+
"nickname" => :binary,
136+
"preferred_username" => :binary,
137+
"profile" => :binary,
138+
"picture" => :binary,
139+
"website" => :binary,
140+
"email" => :binary,
141+
"email_verified" => :boolean,
142+
"gender" => :binary,
143+
"birthdate" => :binary,
144+
"zoneinfo" => :binary,
145+
"locale" => :binary,
146+
"phone_number" => :binary,
147+
"phone_number_verified" => :boolean,
148+
"address" => %{
149+
"formatted" => :binary,
150+
"street_address" => :binary,
151+
"locality" => :binary,
152+
"region" => :binary,
153+
"postal_code" => :binary,
154+
"country" => :binary
155+
},
156+
"updated_at" => :integer
157+
}
158+
127159
@doc """
128160
Normalize API user request response into standard claims.
129161
162+
The function will cast values to adhere to the following types:
163+
164+
```
165+
#{inspect(@registered_claim_member_types, pretty: true)}
166+
```
167+
168+
Returns an `Assent.CastClaimsError` if any of the above types can't be casted.
169+
130170
Based on https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.1
131171
"""
132-
@spec normalize_userinfo(map(), map()) :: {:ok, map()}
172+
@spec normalize_userinfo(map(), map()) :: {:ok, map()} | {:error, term()}
133173
def normalize_userinfo(claims, extra \\ %{}) do
134-
standard_claims =
135-
Map.take(
136-
claims,
137-
~w(sub name given_name family_name middle_name nickname
138-
preferred_username profile picture website email email_verified
139-
gender birthdate zoneinfo locale phone_number phone_number_verified
140-
address updated_at)
141-
)
174+
case cast_claims(@registered_claim_member_types, claims) do
175+
{casted_claims, nil} ->
176+
{:ok, deep_merge_claims(casted_claims, extra)}
142177

143-
{:ok, prune(Map.merge(extra, standard_claims))}
178+
{_claims, invalid_claims} ->
179+
{:error,
180+
CastClaimsError.exception(claims: claims, invalid_types: Enum.into(invalid_claims, %{}))}
181+
end
144182
end
145183

146-
@doc """
147-
Recursively prunes map for nil values.
148-
"""
149-
@spec prune(map()) :: map()
150-
def prune(map) do
151-
map
152-
|> Enum.map(fn {k, v} -> if is_map(v), do: {k, prune(v)}, else: {k, v} end)
153-
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
154-
|> Enum.into(%{})
184+
defp cast_claims(claim_types, claims) do
185+
{casted_claims, invalid_claims} =
186+
Enum.reduce(claim_types, {[], []}, fn {key, type}, acc ->
187+
cast_claim(key, type, Map.get(claims, key), acc)
188+
end)
189+
190+
{
191+
(casted_claims != [] && Enum.into(casted_claims, %{})) || nil,
192+
(invalid_claims != [] && Enum.into(invalid_claims, %{})) || nil
193+
}
155194
end
156195

196+
defp cast_claim(_key, _type, nil, acc), do: acc
197+
198+
defp cast_claim(key, %{} = claim_types, %{} = claims, {casted_claims, invalid_claims}) do
199+
{casted_sub_claims, invalid_sub_claims} = cast_claims(claim_types, claims)
200+
201+
{
202+
(casted_sub_claims && [{key, casted_sub_claims} | casted_claims]) || casted_claims,
203+
(invalid_sub_claims && [{key, invalid_sub_claims} | invalid_claims]) || invalid_claims
204+
}
205+
end
206+
207+
defp cast_claim(key, %{}, _value, {casted_claims, invalid_claims}) do
208+
{casted_claims, [{key, :map} | invalid_claims]}
209+
end
210+
211+
defp cast_claim(key, type, value, {casted_claims, invalid_claims}) do
212+
case cast_value(value, type) do
213+
{:ok, value} -> {[{key, value} | casted_claims], invalid_claims}
214+
:error -> {casted_claims, [{key, type} | invalid_claims]}
215+
end
216+
end
217+
218+
defp cast_value(value, :binary) when is_binary(value), do: {:ok, value}
219+
defp cast_value(value, :binary) when is_integer(value), do: {:ok, to_string(value)}
220+
defp cast_value(value, :integer) when is_integer(value), do: {:ok, value}
221+
defp cast_value(value, :integer) when is_binary(value), do: cast_integer(value)
222+
defp cast_value(value, :boolean) when is_boolean(value), do: {:ok, value}
223+
defp cast_value("true", :boolean), do: {:ok, true}
224+
defp cast_value("false", :boolean), do: {:ok, false}
225+
defp cast_value(_value, _type), do: :error
226+
227+
defp cast_integer(value) do
228+
case Integer.parse(value) do
229+
{integer, ""} -> {:ok, integer}
230+
_ -> :error
231+
end
232+
end
233+
234+
defp deep_merge_claims(claims, extra) do
235+
Enum.reduce(extra, claims, fn
236+
{_key, nil}, claims -> claims
237+
{key, value}, claims -> deep_merge_claim(claims, key, value, Map.get(claims, key))
238+
end)
239+
end
240+
241+
defp deep_merge_claim(claims, key, sub_extra, nil), do: Map.put(claims, key, sub_extra)
242+
243+
defp deep_merge_claim(claims, key, %{} = sub_extra, %{} = sub_claims) do
244+
Map.put(claims, key, deep_merge_claims(sub_claims, sub_extra))
245+
end
246+
247+
defp deep_merge_claim(claims, _key, _sub_extra, _value), do: claims
248+
157249
@doc false
158250
def __normalize__({:ok, %{user: user} = results}, config, strategy) do
159251
config

test/assent/strategies/auth0_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ defmodule Assent.Strategy.Auth0Test do
2626
"name" => "John Doe",
2727
"nickname" => "john.doe",
2828
"picture" => "https://myawesomeavatar.com/avatar.png",
29-
"updated_at" => "2017-03-30T15:13:40.474Z"
29+
"updated_at" => 1_490_886_820
3030
}
3131

3232
test "authorize_url/2", %{config: config} do

test/assent/strategies/basecamp_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ defmodule Assent.Strategy.BasecampTest do
4242
"family_name" => "Fried",
4343
"given_name" => "Jason",
4444
"name" => "Jason Fried",
45-
"sub" => 9_999_999
45+
"sub" => "9999999"
4646
}
4747

4848
test "authorize_url/2", %{config: config} do

test/assent/strategies/github_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ defmodule Assent.Strategy.GithubTest do
6565
"picture" => "https://github.com/images/error/octocat_happy.gif",
6666
"preferred_username" => "octocat",
6767
"profile" => "https://github.com/octocat",
68-
"sub" => 1
68+
"sub" => "1"
6969
}
7070

7171
test "authorize_url/2", %{config: config} do

test/assent/strategies/google_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ defmodule Assent.Strategy.GoogleTest do
1919
}
2020
@user %{
2121
"email" => "[email protected]",
22-
"email_verified" => "true",
22+
"email_verified" => true,
2323
"hd" => "example.com",
2424
"sub" => "10769150350006150715113082367"
2525
}

test/assent/strategies/strava_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ defmodule Assent.Strategy.StravaTest do
5353
}
5454

5555
@user %{
56-
"sub" => 1_234_567_890_987_654_321,
56+
"sub" => "1234567890987654321",
5757
"given_name" => "Marianne",
5858
"family_name" => "Teutenberg",
5959
"preferred_username" => "marianne_t",

test/assent/strategies/telegram_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Assent.Strategy.TelegramTest do
1111
"username" => "duroff"
1212
}
1313
@user %{
14-
"sub" => 928_474_348,
14+
"sub" => "928474348",
1515
"family_name" => "Duroff",
1616
"given_name" => "Paul",
1717
"preferred_username" => "duroff",

test/assent/strategies/twitter_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ defmodule Assent.Strategy.TwitterTest do
124124
"picture" => "https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg",
125125
"preferred_username" => "TwitterDev",
126126
"profile" => "https://twitter.com/TwitterDev",
127-
"sub" => 2_244_994_945,
127+
"sub" => "2244994945",
128128
"website" => "https://t.co/FGl7VOULyL"
129129
}
130130

test/assent/strategies/vk_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ defmodule Assent.Strategy.VKTest do
1919
@user %{
2020
"given_name" => "Lindsay",
2121
"family_name" => "Stirling",
22-
"sub" => 210_700_286,
22+
"sub" => "210700286",
2323
"email" => "[email protected]"
2424
}
2525

0 commit comments

Comments
 (0)