···1111- automates token refresh; for confidential clients requires ref to client secret
1212- triggers callback when session data are updated (nonce, tokens)
13131414-`oauth.OAuthStore`
1414+`oauth.ClientAuthStore`
1515- interface for persistent storage systems for auth request and session metadata, including secrets and DPoP private keys
16161717`oauth.Resolver`
···11{{ define "content" }}
22-This is home!
22+<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>
33+44+<p>Click "Login" above to get started.
35{{ end }}
+23-9
atproto/auth/oauth/cmd/oauth-web-demo/main.go
···7676var tmplPostText string
7777var tmplPost = template.Must(template.Must(template.New("post.html").Parse(tmplBaseText)).Parse(tmplPostText))
78787979-func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) {
8080- tmplHome.Execute(w, nil)
8181-}
8282-8379func runServer(cctx *cli.Context) error {
84808581 scopes := []string{"atproto", "transition:generic"}
···194190 }
195191}
196192193193+func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) {
194194+ ctx := r.Context()
195195+196196+ // attempts to load Session to display links
197197+ did, sessionID := s.currentSessionDID(r)
198198+ if did == nil {
199199+ tmplHome.Execute(w, nil)
200200+ return
201201+ }
202202+203203+ _, err := s.OAuth.ResumeSession(ctx, *did, sessionID)
204204+ if err != nil {
205205+ tmplHome.Execute(w, nil)
206206+ return
207207+ }
208208+ tmplHome.Execute(w, did)
209209+}
210210+197211func (s *Server) OAuthLogin(w http.ResponseWriter, r *http.Request) {
198212 ctx := r.Context()
199213···300314301315 slog.Info("in post handler")
302316303303- if r.Method != "POST" {
304304- tmplPost.Execute(w, nil)
305305- return
306306- }
307307-308317 did, sessionID := s.currentSessionDID(r)
309318 if did == nil {
310319 // TODO: supposed to set a WWW header; and could redirect?
311320 http.Error(w, "not authenticated", http.StatusUnauthorized)
321321+ return
322322+ }
323323+324324+ if r.Method != "POST" {
325325+ tmplPost.Execute(w, did)
312326 return
313327 }
314328
+17-17
atproto/auth/oauth/doc.go
···3344Feature set includes:
5566-- client and server metadata resolution
77-- PKCE: computing and verifying challenges
88-- DPoP client implementation: JWT signing and nonces for requests to Auth Server and Resource Server
99-- PAR client submission
1010-- both public and confidential clients, with support for signed client attestations in the later case
66+ - client and server metadata resolution
77+ - PKCE: computing and verifying challenges
88+ - DPoP client implementation: JWT signing and nonces for requests to Auth Server and Resource Server
99+ - PAR client submission
1010+ - both public and confidential clients, with support for signed client attestations in the later case
11111212Most 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.
13131414This package does not contain supporting code for atproto permissions or permission sets. It treats scopes as simple strings.
15151616-## Quickstart
1616+# Quickstart
17171818Create a single [ClientApp] instance during service setup that will be used (concurrently) across all users and sessions:
1919···36363737 oauthApp := oauth.NewClientApp(&config, oauth.NewMemStore())
38383939-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.
3939+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.
40404141-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:
4141+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:
42424343 http.HandleFunc("GET /client-metadata.json", HandleClientMetadata)
4444···5454 }
5555 }
56565757-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):
5757+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):
58585959 http.HandleFunc("GET /oauth/login", HandleLogin)
6060···7171 http.Redirect(w, r, redirectURL, http.StatusFound)
7272 }
73737474-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.
7474+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.
75757676 http.HandleFunc("GET /oauth/callback", HandleOAuthCallback)
7777···120120 return err
121121 }
122122123123-The [ClientSession] will handle nonce updates and token refreshes, and persist the results in the [OAuthStore].
123123+The [ClientSession] will handle nonce updates and token refreshes, and persist the results in the [ClientAuthStore].
124124125125-To log out a user, delete their session from the [OAuthStore]:
125125+To log out a user, delete their session from the [ClientAuthStore]:
126126127127 if err := oauthApp.Store.DeleteSession(r.Context(), did, sessionID); err != nil {
128128 return err
129129 }
130130131131-## Authorization-only Situations
131131+# Authorization-only Situations
132132133133Some 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).
134134135135In 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.
136136137137-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.
137137+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.
138138139139-## Multiple Sessions Per Account
139139+# Multiple Sessions Per Account
140140141141-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.
141141+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.
142142143143-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.
143143+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.
144144*/
145145package oauth
+4-3
atproto/auth/oauth/oauth.go
···7878 return app
7979}
80808181-// 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()].
8181+// 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].
8282//
8383// The "scopes" array must include "atproto".
8484func NewPublicConfig(clientID, callbackURL string, scopes []string) ClientConfig {
···425425 return &parInfo, nil
426426}
427427428428-// Lower-level helper. This is usually invoked as part of [ProcessCallback].
428428+// Lower-level helper. This is usually invoked as part of [ClientApp.ProcessCallback].
429429func (app *ClientApp) SendInitialTokenRequest(ctx context.Context, authCode string, info AuthRequestData) (*TokenResponse, error) {
430430431431 body := InitialTokenRequest{
···639639 sessData := ClientSessionData{
640640 AccountDID: accountDID,
641641 SessionID: info.State,
642642- Scopes: strings.Split(tokenResp.Scope, " "),
643642 HostURL: hostURL,
644643 AuthServerURL: info.AuthServerURL,
644644+ AuthServerTokenEndpoint: info.AuthServerTokenEndpoint,
645645+ Scopes: strings.Split(tokenResp.Scope, " "),
645646 AccessToken: tokenResp.AccessToken,
646647 RefreshToken: tokenResp.RefreshToken,
647648 DPoPAuthServerNonce: info.DPoPAuthServerNonce,
+2-2
atproto/auth/oauth/session.go
···79798080// Requests new tokens from auth server, and returns the new access token on success.
8181//
8282-// Internally takes a lock on session data around the entire refresh process, including retries. Persists data using [ClientSession.PersistSessionCallback] if configured.
8282+// Internally takes a lock on session data around the entire refresh process, including retries. Persists data using [PersistSessionCallback] if configured.
8383func (sess *ClientSession) RefreshTokens(ctx context.Context) (string, error) {
8484 sess.lk.Lock()
8585 defer sess.lk.Unlock()
86868787 body := RefreshTokenRequest{
8888 ClientID: sess.Config.ClientID,
8989- GrantType: "authorization_code",
8989+ GrantType: "refresh_token",
9090 RefreshToken: sess.Data.RefreshToken,
9191 }
9292 tokenURL := sess.Data.AuthServerTokenEndpoint