An Elixir toolkit for the AT Protocol. hexdocs.pm/atex
elixir bluesky atproto decentralization
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(oauth): add option to plug for callback after login

+73 -18
+25 -2
examples/oauth.ex
··· 1 1 defmodule ExampleOAuthPlug do 2 + require Logger 2 3 use Plug.Router 3 4 alias Atex.OAuth 4 5 alias Atex.XRPC ··· 13 14 plug :match 14 15 plug :dispatch 15 16 16 - forward "/oauth", to: Atex.OAuth.Plug 17 + forward "/oauth", to: Atex.OAuth.Plug, init_opts: [callback: {__MODULE__, :oauth_callback, []}] 18 + 19 + def oauth_callback(conn) do 20 + IO.inspect(conn, label: "callback from oauth!") 21 + 22 + conn 23 + |> put_resp_header("Location", "/whoami") 24 + |> resp(307, "") 25 + |> send_resp() 26 + end 17 27 18 28 get "/whoami" do 19 29 conn = fetch_session(conn) 20 30 31 + case XRPC.OAuthClient.from_conn(conn) do 32 + {:ok, client} -> 33 + send_resp(conn, 200, "hello #{client.did}") 34 + 35 + :error -> 36 + send_resp(conn, 401, "Unauthorized") 37 + end 38 + end 39 + 40 + post "/create-post" do 41 + conn = fetch_session(conn) 42 + 21 43 with {:ok, client} <- XRPC.OAuthClient.from_conn(conn), 22 44 {:ok, response, client} <- 23 45 XRPC.post(client, %Com.Atproto.Repo.CreateRecord{ ··· 25 47 repo: client.did, 26 48 collection: "app.bsky.feed.post", 27 49 rkey: Atex.TID.now() |> to_string(), 28 - record: %App.Bsky.Feed.Post{ 50 + record: %{ 51 + "$type": "app.bsky.feed.post", 29 52 text: "Hello world from atex!", 30 53 createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) 31 54 }
+48 -16
lib/atex/oauth/plug.ex
··· 15 15 `secret_key_base` to have been set on your connections. Ideally it should be 16 16 routed to via `Plug.Router.forward/2`, under a route like "/oauth". 17 17 18 + The plug requires a `:callback` option that must be an MFA tuple (Module, Function, Args). 19 + This callback is invoked after successful OAuth authentication, receiving the connection 20 + with the authenticated session data. 21 + 18 22 ## Example 19 23 20 24 Example implementation showing how to set up the OAuth plug with proper 21 - session handling: 25 + session handling and a callback function: 22 26 23 27 defmodule ExampleOAuthPlug do 24 28 use Plug.Router ··· 33 37 plug :match 34 38 plug :dispatch 35 39 36 - forward "/oauth", to: Atex.OAuth.Plug 40 + forward "/oauth", to: Atex.OAuth.Plug, init_opts: [callback: {__MODULE__, :oauth_callback, []}] 41 + 42 + def oauth_callback(conn) do 43 + # Handle successful OAuth authentication 44 + conn 45 + |> put_resp_header("Location", "/dashboard") 46 + |> resp(307, "") 47 + |> send_resp() 48 + end 37 49 38 50 def put_secret_key_base(conn, _) do 39 51 put_in( ··· 59 71 alias Atex.{IdentityResolver, IdentityResolver.DIDDocument} 60 72 61 73 @oauth_cookie_opts [path: "/", http_only: true, secure: true, same_site: "lax", max_age: 600] 74 + 75 + def init(opts) do 76 + callback = Keyword.get(opts, :callback, nil) 77 + 78 + if !match?({_module, _function, _args}, callback) do 79 + raise "expected callback to be a MFA tuple" 80 + end 81 + 82 + opts 83 + end 84 + 85 + def call(conn, opts) do 86 + conn 87 + |> put_private(:atex_oauth_opts, opts) 88 + |> super(opts) 89 + end 62 90 63 91 plug :match 64 92 plug :dispatch ··· 112 140 113 141 get "/callback" do 114 142 conn = conn |> fetch_query_params() |> fetch_session() 143 + callback = Keyword.get(conn.private.atex_oauth_opts, :callback) 115 144 cookies = get_cookies(conn) 116 145 stored_state = cookies["state"] 117 146 stored_code_verifier = cookies["code_verifier"] ··· 138 167 pds <- DIDDocument.get_pds_endpoint(identity.document), 139 168 {:ok, authz_server} <- OAuth.get_authorization_server(pds), 140 169 true <- authz_server == stored_issuer do 141 - conn 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, 148 - did: tokens.did, 149 - pds: pds, 150 - expires_at: tokens.expires_at, 151 - dpop_nonce: nonce, 152 - dpop_key: dpop_key 153 - }) 154 - |> send_resp(200, "success!! hello #{tokens.did}") 170 + conn = 171 + conn 172 + |> delete_resp_cookie("state", @oauth_cookie_opts) 173 + |> delete_resp_cookie("code_verifier", @oauth_cookie_opts) 174 + |> delete_resp_cookie("issuer", @oauth_cookie_opts) 175 + |> put_session(:atex_oauth, %{ 176 + access_token: tokens.access_token, 177 + refresh_token: tokens.refresh_token, 178 + did: tokens.did, 179 + pds: pds, 180 + expires_at: tokens.expires_at, 181 + dpop_nonce: nonce, 182 + dpop_key: dpop_key 183 + }) 184 + 185 + {mod, func, args} = callback 186 + apply(mod, func, [conn | args]) 155 187 else 156 188 false -> 157 189 send_resp(conn, 400, "OAuth issuer does not match your PDS' authorization server")