···
1
-
if Code.ensure_loaded?(Plug) do
2
-
defmodule Atex.OAuth.Plug do
4
-
Plug router for handling AT Protocol's OAuth flow.
1
+
defmodule Atex.OAuth.Plug do
3
+
Plug router for handling AT Protocol's OAuth flow.
6
-
This module provides three endpoints:
5
+
This module provides three endpoints:
8
-
- `GET /login?handle=<handle>` - Initiates the OAuth authorization flow for
10
-
- `GET /callback` - Handles the OAuth callback after user authorization
11
-
- `GET /client-metadata.json` - Serves the OAuth client metadata
7
+
- `GET /login?handle=<handle>` - Initiates the OAuth authorization flow for
9
+
- `GET /callback` - Handles the OAuth callback after user authorization
10
+
- `GET /client-metadata.json` - Serves the OAuth client metadata
15
-
This module requires `Plug.Session` to be in your pipeline, as well as
16
-
`secret_key_base` to have been set on your connections. Ideally it should be
17
-
routed to via `Plug.Router.forward/2`, under a route like "/oauth".
14
+
This module requires `Plug.Session` to be in your pipeline, as well as
15
+
`secret_key_base` to have been set on your connections. Ideally it should be
16
+
routed to via `Plug.Router.forward/2`, under a route like "/oauth".
21
-
Example implementation showing how to set up the OAuth plug with proper
20
+
Example implementation showing how to set up the OAuth plug with proper
24
-
defmodule ExampleOAuthPlug do
23
+
defmodule ExampleOAuthPlug do
27
-
plug :put_secret_key_base
26
+
plug :put_secret_key_base
32
-
signing_salt: "signing-salt"
31
+
signing_salt: "signing-salt"
37
-
forward "/oauth", to: Atex.OAuth.Plug
36
+
forward "/oauth", to: Atex.OAuth.Plug
39
-
def put_secret_key_base(conn, _) do
41
-
conn.secret_key_base,
42
-
"very long key base with at least 64 bytes"
38
+
def put_secret_key_base(conn, _) do
40
+
conn.secret_key_base,
41
+
"very long key base with at least 64 bytes"
49
-
After successful authentication, the plug stores these in the session:
48
+
After successful authentication, the plug stores these in the session:
51
-
* `:tokens` - The access token response containing access_token,
52
-
refresh_token, did, and expires_at
53
-
* `:dpop_key` - The DPoP JWK for generating DPoP proofs
59
-
alias Atex.{IdentityResolver, IdentityResolver.DIDDocument}
50
+
- `:tokens` - The access token response containing access_token,
51
+
refresh_token, did, and expires_at
53
+
- `:dpop_key` - The DPoP JWK for generating DPoP proofs
59
+
alias Atex.{IdentityResolver, IdentityResolver.DIDDocument}
61
-
@oauth_cookie_opts [path: "/", http_only: true, secure: true, same_site: "lax", max_age: 600]
61
+
@oauth_cookie_opts [path: "/", http_only: true, secure: true, same_site: "lax", max_age: 600]
67
-
conn = fetch_query_params(conn)
68
-
handle = conn.query_params["handle"]
67
+
conn = fetch_query_params(conn)
68
+
handle = conn.query_params["handle"]
71
-
send_resp(conn, 400, "Need `handle` query parameter")
73
-
case IdentityResolver.resolve(handle) do
75
-
pds = DIDDocument.get_pds_endpoint(identity.document)
76
-
{:ok, authz_server} = OAuth.get_authorization_server(pds)
77
-
{:ok, authz_metadata} = OAuth.get_authorization_server_metadata(authz_server)
78
-
state = OAuth.create_nonce()
79
-
code_verifier = OAuth.create_nonce()
71
+
send_resp(conn, 400, "Need `handle` query parameter")
73
+
case IdentityResolver.resolve(handle) do
75
+
pds = DIDDocument.get_pds_endpoint(identity.document)
76
+
{:ok, authz_server} = OAuth.get_authorization_server(pds)
77
+
{:ok, authz_metadata} = OAuth.get_authorization_server_metadata(authz_server)
78
+
state = OAuth.create_nonce()
79
+
code_verifier = OAuth.create_nonce()
81
-
case OAuth.create_authorization_url(
89
-
|> put_resp_cookie("state", state, @oauth_cookie_opts)
90
-
|> put_resp_cookie("code_verifier", code_verifier, @oauth_cookie_opts)
91
-
|> put_resp_cookie("issuer", authz_metadata.issuer, @oauth_cookie_opts)
92
-
|> put_resp_header("location", authz_url)
93
-
|> send_resp(307, "")
81
+
case OAuth.create_authorization_url(
89
+
|> put_resp_cookie("state", state, @oauth_cookie_opts)
90
+
|> put_resp_cookie("code_verifier", code_verifier, @oauth_cookie_opts)
91
+
|> put_resp_cookie("issuer", authz_metadata.issuer, @oauth_cookie_opts)
92
+
|> put_resp_header("location", authz_url)
93
+
|> send_resp(307, "")
96
-
Logger.error("failed to reate authorization url, #{inspect(err)}")
97
-
send_resp(conn, 500, "Internal server error")
96
+
Logger.error("failed to reate authorization url, #{inspect(err)}")
97
+
send_resp(conn, 500, "Internal server error")
101
-
Logger.error("Failed to resolve handle, #{inspect(err)}")
102
-
send_resp(conn, 400, "Invalid handle")
101
+
Logger.error("Failed to resolve handle, #{inspect(err)}")
102
+
send_resp(conn, 400, "Invalid handle")
107
-
get "/client-metadata.json" do
109
-
|> put_resp_content_type("application/json")
110
-
|> send_resp(200, JSON.encode_to_iodata!(OAuth.create_client_metadata()))
107
+
get "/client-metadata.json" do
109
+
|> put_resp_content_type("application/json")
110
+
|> send_resp(200, JSON.encode_to_iodata!(OAuth.create_client_metadata()))
114
-
conn = conn |> fetch_query_params() |> fetch_session()
115
-
cookies = get_cookies(conn)
116
-
stored_state = cookies["state"]
117
-
stored_code_verifier = cookies["code_verifier"]
118
-
stored_issuer = cookies["issuer"]
114
+
conn = conn |> fetch_query_params() |> fetch_session()
115
+
cookies = get_cookies(conn)
116
+
stored_state = cookies["state"]
117
+
stored_code_verifier = cookies["code_verifier"]
118
+
stored_issuer = cookies["issuer"]
120
-
code = conn.query_params["code"]
121
-
state = conn.query_params["state"]
120
+
code = conn.query_params["code"]
121
+
state = conn.query_params["state"]
123
-
if !stored_state || !stored_code_verifier || !stored_issuer || (!code || !state) ||
124
-
stored_state != state do
125
-
send_resp(conn, 400, "Invalid request")
123
+
if !stored_state || !stored_code_verifier || !stored_issuer || (!code || !state) ||
124
+
stored_state != state do
125
+
send_resp(conn, 400, "Invalid request")
127
+
with {:ok, authz_metadata} <- OAuth.get_authorization_server_metadata(stored_issuer),
128
+
dpop_key <- JOSE.JWK.generate_key({:ec, "P-256"}),
129
+
{:ok, tokens, nonce} <-
130
+
OAuth.validate_authorization_code(
134
+
stored_code_verifier
136
+
{:ok, identity} <- IdentityResolver.resolve(tokens.did),
137
+
# Make sure pds' issuer matches the stored one (just in case)
138
+
pds <- DIDDocument.get_pds_endpoint(identity.document),
139
+
{:ok, authz_server} <- OAuth.get_authorization_server(pds),
140
+
true <- authz_server == stored_issuer do
142
+
|> delete_resp_cookie("state", @oauth_cookie_opts)
143
+
|> delete_resp_cookie("code_verifier", @oauth_cookie_opts)
144
+
|> delete_resp_cookie("issuer", @oauth_cookie_opts)
145
+
|> put_session(:atex_oauth, %{
146
+
access_token: tokens.access_token,
147
+
refresh_token: tokens.refresh_token,
150
+
expires_at: tokens.expires_at,
154
+
|> send_resp(200, "success!! hello #{tokens.did}")
127
-
with {:ok, authz_metadata} <- OAuth.get_authorization_server_metadata(stored_issuer),
128
-
dpop_key <- JOSE.JWK.generate_key({:ec, "P-256"}),
129
-
{:ok, tokens, nonce} <-
130
-
OAuth.validate_authorization_code(
134
-
stored_code_verifier
135
-
# TODO: verify did pds issuer is the same as stored issuer
137
-
IO.inspect({tokens, nonce}, label: "OAuth succeeded")
157
+
send_resp(conn, 400, "OAuth issuer does not match your PDS' authorization server")
140
-
|> put_session(:tokens, tokens)
141
-
|> put_session(:dpop_key, dpop_key)
142
-
|> send_resp(200, "success!! hello #{tokens.did}")
145
-
Logger.error("failed to validate oauth callback: #{inspect(err)}")
146
-
send_resp(conn, 500, "Internal server error")
160
+
Logger.error("failed to validate oauth callback: #{inspect(err)}")
161
+
send_resp(conn, 500, "Internal server error")