Skip to content

Commit 2f9a43a

Browse files
authored
docs: example of using a state token (#58)
* docs: example of using a state token Closes #11 * provide extended example * fix uri * remove elixirls
1 parent b148bf3 commit 2f9a43a

File tree

2 files changed

+47
-8
lines changed

2 files changed

+47
-8
lines changed

README.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ google_config = %{
3232
scope: "openid email profile"
3333
}
3434

35-
authorization_uri(google_config)
35+
OpenIDConnect.authorization_uri(google_config)
3636
```
3737

3838
Most major OAuth2 providers have added support for OpenIDConnect. [See a short list of most major adopters of OpenIDConnect](https://en.wikipedia.org/wiki/List_of_OAuth_providers).
@@ -93,22 +93,31 @@ Setting your app's session state is outside the scope of this library.
9393
```elixir
9494
# router.ex
9595
get("/session", SessionController, :create)
96-
get("/session/authorization-uri", SessionController, :authorization_uri)
96+
get("/session/:provider/authorization-uri", SessionController, :authorization_uri)
9797

9898
# session_controller.ex
99-
# you could also take the `provider` as a query param to pass into the function
100-
def authorization_uri(conn, _params) do
99+
def authorization_uri(conn, %{"provider" => "google"}) do
101100
google_config = Application.fetch_env!(:my_app, :google_oidc_config)
102-
{:ok, uri} = OpenIDConnect.authorization_uri(google_config)
101+
102+
state_token = Base.encode64(:crypto.strong_rand_bytes(32), padding: false)
103+
redirect_uri = url(Endpoint, ~p"/session/google/authorization-uri")
104+
{:ok, uri} = OpenIDConnect.authorization_uri(google_config, redirect_uri, %{state: state})
103105

104-
json(conn, %{uri: uri})
106+
conn
107+
# Store the state token for verifying we get the same one back
108+
|> put_session(:state_token, state_token)
109+
|> redirect(external: uri)
105110
end
106111

107112
# The `Authentication` module here is an imaginary interface for setting session state
108113
def create(conn, params) do
109114
google_config = Application.fetch_env!(:my_app, :google_oidc_config)
110115

111-
with {:ok, tokens} <- OpenIDConnect.fetch_tokens(google_config, params["code"]),
116+
# State token should be passed back by the provider
117+
state_token = params["state"]
118+
119+
with true <- valid_state_token?(conn, state_token),
120+
{:ok, tokens} <- OpenIDConnect.fetch_tokens(google_config, params["code"]),
112121
{:ok, claims} <- OpenIDConnect.verify(google_config, tokens["id_token"]),
113122
{:ok, user} <- Authentication.call(google_config, Repo, claims) do
114123

@@ -119,6 +128,14 @@ def create(conn, params) do
119128
_ -> send_resp(conn, 401, "")
120129
end
121130
end
131+
132+
defp valid_state_token?(conn, token) do
133+
# Pull out the state token from session
134+
case get_session(conn, :state_token) do
135+
nil -> false
136+
state_token -> Plug.Crypto.secure_compare(state_token, token)
137+
end
138+
end
122139
```
123140

124141
## Authors

lib/openid_connect.ex

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,32 @@ defmodule OpenIDConnect do
6868
The `params` option can be used to add additional query params to the URI
6969
7070
Example:
71+
7172
OpenIDConnect.authorization_uri(:google, %{"hd" => "dockyard.com"})
7273
74+
> ### Warning {: .warning}
7375
> It is *highly suggested* that you add the `state` param for security reasons. Your
74-
> OpenID Connect provider should have more information on this topic.
76+
> OpenID Connect provider should have more information on this topic.
77+
> See [Google's Documentation](https://developers.google.com/identity/openid-connect/openid-connect#createxsrftoken) as an exampl
78+
79+
### Creating a state token
80+
81+
A state token should be a hashed cookie or a randomly generated value of reasonable length
82+
83+
state_token = Base.encode64(:crypto.strong_rand_bytes(32), padding: false)
84+
85+
OpenIDConnect.authorization_uri(:google, %{"hd" => "dockyard.com", "state" => state_token})
86+
87+
# Store token somewhere, such as your session state
88+
conn = Plug.Conn.put_session(conn, :state_token, state_token)
89+
90+
### Validating a state token
91+
92+
Once your application's callback has been fired, validate the token received vs the one
93+
you have stored.
94+
95+
Plug.Crypto.secure_compare(Plug.Conn.get_session(conn, :state_token), params["state"])
96+
7597
"""
7698
@spec authorization_uri(
7799
config(),

0 commit comments

Comments
 (0)