this repo has no description
0
fork

Configure Feed

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

oauth quick fixes (#1137)

authored by

bnewbold and committed by
GitHub
42e2d5b7 e303f097

+53 -38
+1 -1
atproto/auth/oauth/HACKING.md
··· 11 11 - automates token refresh; for confidential clients requires ref to client secret 12 12 - triggers callback when session data are updated (nonce, tokens) 13 13 14 - `oauth.OAuthStore` 14 + `oauth.ClientAuthStore` 15 15 - interface for persistent storage systems for auth request and session metadata, including secrets and DPoP private keys 16 16 17 17 `oauth.Resolver`
+3 -5
atproto/auth/oauth/cmd/oauth-web-demo/base.html
··· 12 12 <header> 13 13 <hgroup> 14 14 <h1>atproto OAuth demo (indigo)</h1> 15 - {{ if false }} 16 - <p>Hello <span style="font-family: monospace;">@{{ "handle" }}</span>!</p> 17 - {{ end }} 18 15 </hgroup> 19 16 <nav> 20 17 <ul> 21 - {{ if false }} 18 + {{ if . }} 19 + <li><span style="font-family: monospace; font-weight: bold;">{{ . }}</span> 22 20 <li><a href="/bsky/post">Create Post</a> 23 - <li><a href="/oauth/refresh">Refresh Token</a> 21 + <li><a href="/oauth/refresh">Refresh Tokens</a> 24 22 <li><a href="/oauth/logout">Logout</a> 25 23 {{ else }} 26 24 <li><a href="/oauth/login">Login</a>
+3 -1
atproto/auth/oauth/cmd/oauth-web-demo/home.html
··· 1 1 {{ define "content" }} 2 - This is home! 2 + <p>This is a minimal web app showing how to use the <a href="https://pkg.go.dev/github.com/bluesky-social/indigo/atproto/auth/oauth">indigo OAuth client SDK</a> to authenticate users. You can read more in the <a href="https://atproto.com/specs/oauth">atproto OAuth Specification</a> 3 + 4 + <p>Click "Login" above to get started. 3 5 {{ end }}
+23 -9
atproto/auth/oauth/cmd/oauth-web-demo/main.go
··· 76 76 var tmplPostText string 77 77 var tmplPost = template.Must(template.Must(template.New("post.html").Parse(tmplBaseText)).Parse(tmplPostText)) 78 78 79 - func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) { 80 - tmplHome.Execute(w, nil) 81 - } 82 - 83 79 func runServer(cctx *cli.Context) error { 84 80 85 81 scopes := []string{"atproto", "transition:generic"} ··· 194 190 } 195 191 } 196 192 193 + func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) { 194 + ctx := r.Context() 195 + 196 + // attempts to load Session to display links 197 + did, sessionID := s.currentSessionDID(r) 198 + if did == nil { 199 + tmplHome.Execute(w, nil) 200 + return 201 + } 202 + 203 + _, err := s.OAuth.ResumeSession(ctx, *did, sessionID) 204 + if err != nil { 205 + tmplHome.Execute(w, nil) 206 + return 207 + } 208 + tmplHome.Execute(w, did) 209 + } 210 + 197 211 func (s *Server) OAuthLogin(w http.ResponseWriter, r *http.Request) { 198 212 ctx := r.Context() 199 213 ··· 300 314 301 315 slog.Info("in post handler") 302 316 303 - if r.Method != "POST" { 304 - tmplPost.Execute(w, nil) 305 - return 306 - } 307 - 308 317 did, sessionID := s.currentSessionDID(r) 309 318 if did == nil { 310 319 // TODO: supposed to set a WWW header; and could redirect? 311 320 http.Error(w, "not authenticated", http.StatusUnauthorized) 321 + return 322 + } 323 + 324 + if r.Method != "POST" { 325 + tmplPost.Execute(w, did) 312 326 return 313 327 } 314 328
+17 -17
atproto/auth/oauth/doc.go
··· 3 3 4 4 Feature set includes: 5 5 6 - - client and server metadata resolution 7 - - PKCE: computing and verifying challenges 8 - - DPoP client implementation: JWT signing and nonces for requests to Auth Server and Resource Server 9 - - PAR client submission 10 - - both public and confidential clients, with support for signed client attestations in the later case 6 + - client and server metadata resolution 7 + - PKCE: computing and verifying challenges 8 + - DPoP client implementation: JWT signing and nonces for requests to Auth Server and Resource Server 9 + - PAR client submission 10 + - both public and confidential clients, with support for signed client attestations in the later case 11 11 12 12 Most OAuth client applications will use the high-level [ClientApp] and supporting interfaces to manage session logins, persistence, and token refreshes. Lower-level components are designed to be used in isolation if needed. 13 13 14 14 This package does not contain supporting code for atproto permissions or permission sets. It treats scopes as simple strings. 15 15 16 - ## Quickstart 16 + # Quickstart 17 17 18 18 Create a single [ClientApp] instance during service setup that will be used (concurrently) across all users and sessions: 19 19 ··· 36 36 37 37 oauthApp := oauth.NewClientApp(&config, oauth.NewMemStore()) 38 38 39 - For a real service, you would want to use a database or other peristant storage instead of [MemStore]. Otherwise all user sessions are dropped every time the process restarts. 39 + For a real service, you would want to use a database or other peristant implementation of the [ClientAuthStore] interface instead of [MemStore]. Otherwise all user sessions are dropped every time the process restarts. 40 40 41 - The client metadata document needs to be served at the URL indicated by the `client_id`. This can be done statically, or dynamically generated and served from the configuration: 41 + The client metadata document needs to be served at the URL indicated by the 'client_id'. This can be done statically, or dynamically generated and served from the configuration: 42 42 43 43 http.HandleFunc("GET /client-metadata.json", HandleClientMetadata) 44 44 ··· 54 54 } 55 55 } 56 56 57 - The login auth flow starts with a user identifier, which could be an atproto handle, DID, or an auth server URL (eg, a PDS). The high-level [StartAuthFlow()] method will resolve the identifier, send an auth request (PAR) to the server, persist request metadata in the [OAuthStore], and return a redirect URL for the user to visit (usually the PDS): 57 + The login auth flow starts with a user identifier, which could be an atproto handle, DID, or an auth server URL (eg, a PDS). The high-level [ClientApp.StartAuthFlow] method will resolve the identifier, send an auth request (PAR) to the server, persist request metadata in the [ClientAuthStore], and return a redirect URL for the user to visit (usually the PDS): 58 58 59 59 http.HandleFunc("GET /oauth/login", HandleLogin) 60 60 ··· 71 71 http.Redirect(w, r, redirectURL, http.StatusFound) 72 72 } 73 73 74 - The service then waits for a callback request on the configured endpoint. The [ProcessCallback()] method will load the earlier request metadata from the [OAuthStore], send an initial token request to the auth server, and validate that the session is consistent with the identifier from the beginning of the login flow. 74 + The service then waits for a callback request on the configured endpoint. The [ClientApp.ProcessCallback] method will load the earlier request metadata from the [ClientAuthStore], send an initial token request to the auth server, and validate that the session is consistent with the identifier from the beginning of the login flow. 75 75 76 76 http.HandleFunc("GET /oauth/callback", HandleOAuthCallback) 77 77 ··· 120 120 return err 121 121 } 122 122 123 - The [ClientSession] will handle nonce updates and token refreshes, and persist the results in the [OAuthStore]. 123 + The [ClientSession] will handle nonce updates and token refreshes, and persist the results in the [ClientAuthStore]. 124 124 125 - To log out a user, delete their session from the [OAuthStore]: 125 + To log out a user, delete their session from the [ClientAuthStore]: 126 126 127 127 if err := oauthApp.Store.DeleteSession(r.Context(), did, sessionID); err != nil { 128 128 return err 129 129 } 130 130 131 - ## Authorization-only Situations 131 + # Authorization-only Situations 132 132 133 133 Some applications might only use atproto OAuth for authorization (authn). For example, "Login with Atmospehre", where the application does not need to access additional account metadata (such as account email), or access any restricted account resources (eg, write to atproto repository). 134 134 135 135 In this scenario, the client app still needs to do an initial token request, to confirm the account identifier. But the returned session tokens will never be used, and do not need to be persisted. 136 136 137 - In these scenarios, applications could use an implementation of [OAuthStore] which does not actually persist the session data when [OAuthStore.SaveSession] is called. Or, the application could immediately call [OAuthStore.DeleteSession] after [ClientApp.ProcessCallback] returns. 137 + In these scenarios, applications could use an implementation of [ClientAuthStore] which does not actually persist the session data when [ClientAuthStore.SaveSession] is called. Or, the application could immediately call [ClientAuthStore.DeleteSession] after [ClientApp.ProcessCallback] returns. 138 138 139 - ## Multiple Sessions Per Account 139 + # Multiple Sessions Per Account 140 140 141 - In the traditional web app backend scenario, a single account (DID) might have multiple active sessions. For example, a user might log in from a browser on their laptop and on a mobile device at the same time. The user must go through the entire flow on each device (or browser) to authenticate the user. To prevent a new session from "clobbering" existing sessions (including tokens), this package supports multiple concurrent sessions per account, distinguished by a session ID. The random `state` token from the auth flow is re-used by default. 141 + In the traditional web app backend scenario, a single account (DID) might have multiple active sessions. For example, a user might log in from a browser on their laptop and on a mobile device at the same time. The user must go through the entire flow on each device (or browser) to authenticate the user. To prevent a new session from "clobbering" existing sessions (including tokens), this package supports multiple concurrent sessions per account, distinguished by a session ID. The random 'state' token from the auth flow is re-used by default. 142 142 143 - In other scenarious, multiple sessions are not needed or desirable. For example, an integration backend, or tool with very short session lifetimes. In these scenarios, implementations of the [OAuthStore] interface could ignore the session ID. Or the [ClientApp] could be configured with an ephemeral [OAuthStore] (to support auth flows), and managed the session data returned by [ClientApp.ProcessCallback] using separate session storage logic. 143 + In other scenarious, multiple sessions are not needed or desirable. For example, an integration backend, or tool with very short session lifetimes. In these scenarios, implementations of the [ClientAuthStore] interface could ignore the session ID. Or the [ClientApp] could be configured with an ephemeral [ClientAuthStore] (to support auth flows), and managed the session data returned by [ClientApp.ProcessCallback] using separate session storage logic. 144 144 */ 145 145 package oauth
+4 -3
atproto/auth/oauth/oauth.go
··· 78 78 return app 79 79 } 80 80 81 - // Creates a basic [ClientConfig] for use as a public (non-confidential) client. To upgrade to a confidential client, use this method and then [ClientConfig.SetClientSecret()]. 81 + // Creates a basic [ClientConfig] for use as a public (non-confidential) client. To upgrade to a confidential client, use this method and then [ClientConfig.SetClientSecret]. 82 82 // 83 83 // The "scopes" array must include "atproto". 84 84 func NewPublicConfig(clientID, callbackURL string, scopes []string) ClientConfig { ··· 425 425 return &parInfo, nil 426 426 } 427 427 428 - // Lower-level helper. This is usually invoked as part of [ProcessCallback]. 428 + // Lower-level helper. This is usually invoked as part of [ClientApp.ProcessCallback]. 429 429 func (app *ClientApp) SendInitialTokenRequest(ctx context.Context, authCode string, info AuthRequestData) (*TokenResponse, error) { 430 430 431 431 body := InitialTokenRequest{ ··· 639 639 sessData := ClientSessionData{ 640 640 AccountDID: accountDID, 641 641 SessionID: info.State, 642 - Scopes: strings.Split(tokenResp.Scope, " "), 643 642 HostURL: hostURL, 644 643 AuthServerURL: info.AuthServerURL, 644 + AuthServerTokenEndpoint: info.AuthServerTokenEndpoint, 645 + Scopes: strings.Split(tokenResp.Scope, " "), 645 646 AccessToken: tokenResp.AccessToken, 646 647 RefreshToken: tokenResp.RefreshToken, 647 648 DPoPAuthServerNonce: info.DPoPAuthServerNonce,
+2 -2
atproto/auth/oauth/session.go
··· 79 79 80 80 // Requests new tokens from auth server, and returns the new access token on success. 81 81 // 82 - // Internally takes a lock on session data around the entire refresh process, including retries. Persists data using [ClientSession.PersistSessionCallback] if configured. 82 + // Internally takes a lock on session data around the entire refresh process, including retries. Persists data using [PersistSessionCallback] if configured. 83 83 func (sess *ClientSession) RefreshTokens(ctx context.Context) (string, error) { 84 84 sess.lk.Lock() 85 85 defer sess.lk.Unlock() 86 86 87 87 body := RefreshTokenRequest{ 88 88 ClientID: sess.Config.ClientID, 89 - GrantType: "authorization_code", 89 + GrantType: "refresh_token", 90 90 RefreshToken: sess.Data.RefreshToken, 91 91 } 92 92 tokenURL := sess.Data.AuthServerTokenEndpoint