Skip to content

Commit ac0ef88

Browse files
authored
Merge pull request #56 from firezone/andrew/more-grant-types
Support grant types other than "authorization_code"
2 parents be0d85a + 13320ed commit ac0ef88

File tree

2 files changed

+110
-47
lines changed

2 files changed

+110
-47
lines changed

lib/openid_connect.ex

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ defmodule OpenIDConnect do
5050
required(:discovery_document_uri) => discovery_document_uri(),
5151
required(:client_id) => client_id(),
5252
required(:client_secret) => client_secret(),
53-
required(:redirect_uri) => redirect_uri(),
5453
required(:response_type) => response_type(),
5554
required(:scope) => scope(),
5655
optional(:leeway) => non_neg_integer()
@@ -74,9 +73,12 @@ defmodule OpenIDConnect do
7473
> It is *highly suggested* that you add the `state` param for security reasons. Your
7574
> OpenID Connect provider should have more information on this topic.
7675
"""
77-
@spec authorization_uri(config(), params :: %{optional(atom) => term()}) ::
78-
{:ok, uri :: String.t()} | {:error, term()}
79-
def authorization_uri(config, params \\ %{}) do
76+
@spec authorization_uri(
77+
config(),
78+
redirect_uri :: redirect_uri(),
79+
params :: %{optional(atom) => term()}
80+
) :: {:ok, uri :: String.t()} | {:error, term()}
81+
def authorization_uri(config, redirect_uri, params \\ %{}) do
8082
discovery_document_uri = config.discovery_document_uri
8183

8284
with {:ok, document} <- Document.fetch_document(discovery_document_uri),
@@ -86,7 +88,7 @@ defmodule OpenIDConnect do
8688
Map.merge(
8789
%{
8890
client_id: config.client_id,
89-
redirect_uri: config.redirect_uri,
91+
redirect_uri: redirect_uri,
9092
response_type: response_type,
9193
scope: scope
9294
},
@@ -165,27 +167,25 @@ defmodule OpenIDConnect do
165167
end
166168

167169
@doc """
168-
Fetches the authentication tokens from the provider
170+
Fetches the authentication tokens from the provider using the token endpoint retrieved from a discovery document.
171+
172+
The `params` option depends on the `grant_type`:
173+
174+
* for "authorization_code" grant type, `params` should at least include the `redirect_uri` and `code` params;
175+
* for "refresh_token" grant type, `params` should at least include the `refresh_token` param;
176+
* for other grant types and more details see the
177+
[OpenID Connect spec](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint).
169178
170-
The `params` option should at least include the key/value pairs of the `response_type` that
171-
was requested during authorization. `params` may also include any one-off overrides for token
172-
fetching.
179+
`params` may also include any one-off overrides for token fetching.
173180
"""
174181
@spec fetch_tokens(config(), params :: %{optional(atom) => term()}) ::
175182
{:ok, response :: map()} | {:error, term()}
176183
def fetch_tokens(config, params) do
177184
discovery_document_uri = config.discovery_document_uri
178185

179186
form_body =
180-
Map.merge(
181-
%{
182-
client_id: config.client_id,
183-
client_secret: config.client_secret,
184-
redirect_uri: config.redirect_uri,
185-
grant_type: "authorization_code"
186-
},
187-
params
188-
)
187+
%{client_id: config.client_id, client_secret: config.client_secret}
188+
|> Map.merge(params)
189189
|> URI.encode_query(:www_form)
190190

191191
headers = [{"Content-Type", "application/x-www-form-urlencoded"}]

test/openid_connect_test.exs

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ defmodule OpenIDConnectTest do
33
import OpenIDConnect.Fixtures
44
import OpenIDConnect
55

6+
@redirect_uri "https://localhost/redirect_uri"
7+
68
@config %{
79
discovery_document_uri: nil,
810
client_id: "CLIENT_ID",
911
client_secret: "CLIENT_SECRET",
10-
redirect_uri: "https://localhost/redirect_uri",
1112
response_type: "code id_token token",
1213
scope: "openid email profile"
1314
}
1415

15-
describe "authorization_uri/2" do
16+
describe "authorization_uri/3" do
1617
test "generates authorization url with scope and response_type as binaries" do
1718
{_bypass, uri} = start_fixture("google")
1819
config = %{@config | discovery_document_uri: uri}
1920

20-
assert authorization_uri(config) ==
21+
assert authorization_uri(config, @redirect_uri) ==
2122
{:ok,
2223
"https://accounts.google.com/o/oauth2/v2/auth?" <>
2324
"client_id=CLIENT_ID" <>
24-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <>
25+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}" <>
2526
"&response_type=code+id_token+token" <>
2627
"&scope=openid+email+profile"}
2728
end
@@ -30,11 +31,11 @@ defmodule OpenIDConnectTest do
3031
{_bypass, uri} = start_fixture("google")
3132
config = %{@config | discovery_document_uri: uri, scope: ["openid", "email", "profile"]}
3233

33-
assert authorization_uri(config) ==
34+
assert authorization_uri(config, @redirect_uri) ==
3435
{:ok,
3536
"https://accounts.google.com/o/oauth2/v2/auth?" <>
3637
"client_id=CLIENT_ID" <>
37-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <>
38+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}" <>
3839
"&response_type=code+id_token+token" <>
3940
"&scope=openid+email+profile"}
4041
end
@@ -48,11 +49,11 @@ defmodule OpenIDConnectTest do
4849
response_type: ["code", "id_token", "token"]
4950
}
5051

51-
assert authorization_uri(config) ==
52+
assert authorization_uri(config, @redirect_uri) ==
5253
{:ok,
5354
"https://accounts.google.com/o/oauth2/v2/auth?" <>
5455
"client_id=CLIENT_ID" <>
55-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <>
56+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}" <>
5657
"&response_type=code+id_token+token" <>
5758
"&scope=openid+email+profile"}
5859
end
@@ -61,37 +62,37 @@ defmodule OpenIDConnectTest do
6162
{_bypass, uri} = start_fixture("google")
6263

6364
config = %{@config | discovery_document_uri: uri, scope: nil}
64-
assert authorization_uri(config) == {:error, :invalid_scope}
65+
assert authorization_uri(config, @redirect_uri) == {:error, :invalid_scope}
6566

6667
config = %{@config | discovery_document_uri: uri, scope: ""}
67-
assert authorization_uri(config) == {:error, :invalid_scope}
68+
assert authorization_uri(config, @redirect_uri) == {:error, :invalid_scope}
6869

6970
config = %{@config | discovery_document_uri: uri, scope: []}
70-
assert authorization_uri(config) == {:error, :invalid_scope}
71+
assert authorization_uri(config, @redirect_uri) == {:error, :invalid_scope}
7172
end
7273

7374
test "returns error on empty response_type" do
7475
{_bypass, uri} = start_fixture("google")
7576

7677
config = %{@config | discovery_document_uri: uri, response_type: nil}
77-
assert authorization_uri(config) == {:error, :invalid_response_type}
78+
assert authorization_uri(config, @redirect_uri) == {:error, :invalid_response_type}
7879

7980
config = %{@config | discovery_document_uri: uri, response_type: ""}
80-
assert authorization_uri(config) == {:error, :invalid_response_type}
81+
assert authorization_uri(config, @redirect_uri) == {:error, :invalid_response_type}
8182

8283
config = %{@config | discovery_document_uri: uri, response_type: []}
83-
assert authorization_uri(config) == {:error, :invalid_response_type}
84+
assert authorization_uri(config, @redirect_uri) == {:error, :invalid_response_type}
8485
end
8586

8687
test "adds optional params" do
8788
{_bypass, uri} = start_fixture("google")
8889
config = %{@config | discovery_document_uri: uri}
8990

90-
assert authorization_uri(config, %{"state" => "foo"}) ==
91+
assert authorization_uri(config, @redirect_uri, %{"state" => "foo"}) ==
9192
{:ok,
9293
"https://accounts.google.com/o/oauth2/v2/auth?" <>
9394
"client_id=CLIENT_ID" <>
94-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <>
95+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}" <>
9596
"&response_type=code+id_token+token" <>
9697
"&scope=openid+email+profile" <>
9798
"&state=foo"}
@@ -101,11 +102,11 @@ defmodule OpenIDConnectTest do
101102
{_bypass, uri} = start_fixture("google")
102103
config = %{@config | discovery_document_uri: uri}
103104

104-
assert authorization_uri(config, %{client_id: "foo"}) ==
105+
assert authorization_uri(config, @redirect_uri, %{client_id: "foo"}) ==
105106
{:ok,
106107
"https://accounts.google.com/o/oauth2/v2/auth?" <>
107108
"client_id=foo" <>
108-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri" <>
109+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}" <>
109110
"&response_type=code+id_token+token" <>
110111
"&scope=openid+email+profile"}
111112
end
@@ -117,7 +118,7 @@ defmodule OpenIDConnectTest do
117118

118119
config = %{@config | discovery_document_uri: uri}
119120

120-
assert authorization_uri(config, %{client_id: "foo"}) ==
121+
assert authorization_uri(config, @redirect_uri, %{client_id: "foo"}) ==
121122
{:error, %Mint.TransportError{reason: :econnrefused}}
122123
end
123124
end
@@ -187,8 +188,14 @@ defmodule OpenIDConnectTest do
187188
{_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint})
188189
config = %{@config | discovery_document_uri: uri}
189190

190-
assert fetch_tokens(config, %{code: "1234", id_token: "abcd"}) ==
191-
{:ok, token_response_attrs}
191+
params = %{
192+
grant_type: "authorization_code",
193+
redirect_uri: @redirect_uri,
194+
code: "1234",
195+
id_token: "abcd"
196+
}
197+
198+
assert fetch_tokens(config, params) == {:ok, token_response_attrs}
192199

193200
assert_receive {:req, body}
194201

@@ -198,7 +205,7 @@ defmodule OpenIDConnectTest do
198205
"&code=1234" <>
199206
"&grant_type=authorization_code" <>
200207
"&id_token=abcd" <>
201-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri"
208+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}"
202209
end
203210

204211
test "allows to override the default params" do
@@ -221,15 +228,50 @@ defmodule OpenIDConnectTest do
221228
{_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint})
222229
config = %{@config | discovery_document_uri: uri}
223230

224-
fetch_tokens(config, %{client_id: "foo"})
231+
fetch_tokens(config, %{
232+
client_id: "foo",
233+
grant_type: "authorization_code",
234+
redirect_uri: @redirect_uri
235+
})
225236

226237
assert_receive {:req, body}
227238

228239
assert body ==
229240
"client_id=foo" <>
230241
"&client_secret=CLIENT_SECRET" <>
231242
"&grant_type=authorization_code" <>
232-
"&redirect_uri=https%3A%2F%2Flocalhost%2Fredirect_uri"
243+
"&redirect_uri=#{URI.encode_www_form(@redirect_uri)}"
244+
end
245+
246+
test "allows to use refresh_token grant type" do
247+
bypass = Bypass.open()
248+
test_pid = self()
249+
250+
token_response_attrs = %{
251+
"access_token" => "ACCESS_TOKEN",
252+
"id_token" => "ID_TOKEN",
253+
"refresh_token" => "REFRESH_TOKEN"
254+
}
255+
256+
Bypass.expect_once(bypass, "POST", "/token", fn conn ->
257+
{:ok, body, conn} = Plug.Conn.read_body(conn)
258+
send(test_pid, {:req, body})
259+
Plug.Conn.resp(conn, 200, Jason.encode!(token_response_attrs))
260+
end)
261+
262+
token_endpoint = "http://localhost:#{bypass.port}/token"
263+
{_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint})
264+
config = %{@config | discovery_document_uri: uri}
265+
266+
fetch_tokens(config, %{grant_type: "refresh_token", refresh_token: "foo"})
267+
268+
assert_receive {:req, body}
269+
270+
assert body ==
271+
"client_id=CLIENT_ID" <>
272+
"&client_secret=CLIENT_SECRET" <>
273+
"&grant_type=refresh_token" <>
274+
"&refresh_token=foo"
233275
end
234276

235277
test "returns error when token endpoint is not available" do
@@ -238,8 +280,9 @@ defmodule OpenIDConnectTest do
238280
token_endpoint = "http://localhost:#{bypass.port}/token"
239281
{_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint})
240282
config = %{@config | discovery_document_uri: uri}
283+
params = %{grant_type: "authorization_code", redirect_uri: @redirect_uri}
241284

242-
assert fetch_tokens(config, %{client_id: "foo"}) ==
285+
assert fetch_tokens(config, params) ==
243286
{:error, %Mint.TransportError{reason: :econnrefused}}
244287
end
245288

@@ -254,14 +297,21 @@ defmodule OpenIDConnectTest do
254297
{_bypass, uri} = start_fixture("google", %{token_endpoint: token_endpoint})
255298
config = %{@config | discovery_document_uri: uri}
256299

257-
assert fetch_tokens(config, %{client_id: "foo"}) ==
300+
assert fetch_tokens(config, %{}) ==
258301
{:error, {401, "{\"error\":\"unauthorized\"}"}}
259302
end
260303

261304
test "returns error when real provider token endpoint is responded with invalid code" do
262305
{_bypass, uri} = start_fixture("google")
263306
config = %{@config | discovery_document_uri: uri}
264-
assert {:error, {401, resp}} = fetch_tokens(config, %{code: "foo"})
307+
308+
assert {:error, {401, resp}} =
309+
fetch_tokens(config, %{
310+
grant_type: "authorization_code",
311+
redirect_uri: @redirect_uri,
312+
code: "foo"
313+
})
314+
265315
resp_json = Jason.decode!(resp)
266316

267317
assert resp_json == %{
@@ -272,7 +322,14 @@ defmodule OpenIDConnectTest do
272322
for provider <- ["auth0", "okta", "onelogin"] do
273323
{_bypass, uri} = start_fixture(provider)
274324
config = %{@config | discovery_document_uri: uri}
275-
assert {:error, {status, _resp}} = fetch_tokens(config, %{code: "foo"})
325+
326+
assert {:error, {status, _resp}} =
327+
fetch_tokens(config, %{
328+
grant_type: "authorization_code",
329+
redirect_uri: @redirect_uri,
330+
code: "foo"
331+
})
332+
276333
assert status in 400..499
277334
end
278335
end
@@ -284,7 +341,13 @@ defmodule OpenIDConnectTest do
284341

285342
config = %{@config | discovery_document_uri: uri}
286343

287-
assert fetch_tokens(config, %{code: "foo"}) ==
344+
params = %{
345+
grant_type: "authorization_code",
346+
redirect_uri: @redirect_uri,
347+
code: "foo"
348+
}
349+
350+
assert fetch_tokens(config, params) ==
288351
{:error, %Mint.TransportError{reason: :econnrefused}}
289352
end
290353
end

0 commit comments

Comments
 (0)