···139139 return nil
140140}
141141142142-func (s *Server) currentSessionDID(r *http.Request) *syntax.DID {
142142+func (s *Server) currentSessionDID(r *http.Request) (*syntax.DID, string) {
143143 sess, _ := s.CookieStore.Get(r, "oauth-demo")
144144 accountDID, ok := sess.Values["account_did"].(string)
145145 if !ok || accountDID == "" {
146146- return nil
146146+ return nil, ""
147147 }
148148 did, err := syntax.ParseDID(accountDID)
149149 if err != nil {
150150- return nil
150150+ return nil, ""
151151+ }
152152+ sessionID, ok := sess.Values["session_id"].(string)
153153+ if !ok || sessionID == "" {
154154+ return nil, ""
151155 }
152156153153- return &did
157157+ return &did, sessionID
154158}
155159156160func strPtr(raw string) *string {
···232236 // create signed cookie session, indicating account DID
233237 sess, _ := s.CookieStore.Get(r, "oauth-demo")
234238 sess.Values["account_did"] = sessData.AccountDID.String()
239239+ sess.Values["session_id"] = sessData.SessionID
235240 if err := sess.Save(r, w); err != nil {
236241 http.Error(w, err.Error(), http.StatusInternalServerError)
237242 return
···244249func (s *Server) OAuthRefresh(w http.ResponseWriter, r *http.Request) {
245250 ctx := r.Context()
246251247247- did := s.currentSessionDID(r)
252252+ did, sessionID := s.currentSessionDID(r)
248253 if did == nil {
249254 // TODO: supposed to set a WWW header; and could redirect?
250255 http.Error(w, "not authenticated", http.StatusUnauthorized)
251256 return
252257 }
253258254254- oauthSess, err := s.OAuth.ResumeSession(ctx, *did)
259259+ oauthSess, err := s.OAuth.ResumeSession(ctx, *did, sessionID)
255260 if err != nil {
256261 http.Error(w, "not authenticated", http.StatusUnauthorized)
257262 return
···270275func (s *Server) OAuthLogout(w http.ResponseWriter, r *http.Request) {
271276272277 // delete session from auth store
273273- did := s.currentSessionDID(r)
278278+ did, sessionID := s.currentSessionDID(r)
274279 if did != nil {
275275- if err := s.OAuth.Store.DeleteSession(r.Context(), *did); err != nil {
280280+ if err := s.OAuth.Store.DeleteSession(r.Context(), *did, sessionID); err != nil {
276281 slog.Error("failed to delete session", "did", did, "err", err)
277282 }
278283 }
···300305 return
301306 }
302307303303- did := s.currentSessionDID(r)
308308+ did, sessionID := s.currentSessionDID(r)
304309 if did == nil {
305310 // TODO: supposed to set a WWW header; and could redirect?
306311 http.Error(w, "not authenticated", http.StatusUnauthorized)
307312 return
308313 }
309314310310- oauthSess, err := s.OAuth.ResumeSession(ctx, *did)
315315+ oauthSess, err := s.OAuth.ResumeSession(ctx, *did, sessionID)
311316 if err != nil {
312317 http.Error(w, "not authenticated", http.StatusUnauthorized)
313318 return
+5-3
atproto/auth/oauth/doc.go
···8080 http.Error(w, err.Error(), http.StatusInternalServerError)
8181 }
82828383- // web services might record the DID in a secure session cookie
8383+ // web services might record the DID and session ID in a secure session cookie
8484 _ = sessData.AccountDID
8585+ _ = sessData.SessionID
85868687 http.Redirect(w, r, "/app", http.StatusFound)
8788 }
···90919192 // web services might use a secure session cookie to determine user's DID for a request
9293 did := syntax.DID("did:plc:abc123")
9494+ sessionID := "xyz"
93959494- sess, err := oauthApp.ResumeSession(ctx, did)
9696+ sess, err := oauthApp.ResumeSession(ctx, did, sessionID)
9597 if err != nil {
9698 return err
9799 }
···116118117119To log out a user, delete their session from the [OAuthStore]:
118120119119- if err := oauthApp.Store.DeleteSession(r.Context(), did); err != nil {
121121+ if err := oauthApp.Store.DeleteSession(r.Context(), did, sessionID); err != nil {
120122 return err
121123 }
122124*/
···2727 // Account DID for this session. Assuming only one active session per account, this can be used as "primary key" for storing and retrieving this infromation.
2828 AccountDID syntax.DID `json:"account_did"`
29293030+ // Identifier to distinguish this particular session for the account. Server backends generally support multiple sessions for the same account. This package will re-use the random 'state' token from the auth flow as the session ID.
3131+ SessionID string `json:"session_id"`
3232+3033 // Base URL of the "resource server" (eg, PDS). Should include scheme, hostname, port; no path or auth info.
3134 HostURL string `json:"host_url"`
3235···157160 if sess.PersistSessionCallback != nil {
158161 sess.PersistSessionCallback(ctx, sess.Data)
159162 } else {
160160- slog.Warn("not saving updated session data", "did", sess.Data.AccountDID)
163163+ slog.Warn("not saving updated session data", "did", sess.Data.AccountDID, "session_id", sess.Data.SessionID)
161164 }
162165163166 return sess.Data.AccessToken, nil
···246249 if sess.PersistSessionCallback != nil {
247250 sess.PersistSessionCallback(ctx, sess.Data)
248251 } else {
249249- slog.Warn("not saving updated host DPoP nonce", "did", sess.Data.AccountDID)
252252+ slog.Warn("not saving updated host DPoP nonce", "did", sess.Data.AccountDID, "session_id", sess.Data.SessionID)
250253 }
251254}
252255
+6-4
atproto/auth/oauth/store.go
···8899// Interface for persisting session data and auth request data, required as part of an OAuth client app.
1010//
1111-// Note that this interface assumes that there is only a single session per account (by DID).
1111+// This interface supports multiple sessions for a single account (DID). This is helpful for traditional web app backends where a single user might log in and have concurrent sessions from mutiple browsers/devices. For situations where multiple sessions are not required, implementations of this interface could ignore the `sessionID` parameters, though this could result in clobbering of active sessions.
1212+//
1313+// For authorization-only (authn-only) applications, the `SaveSession()` method could be a no-op.
1214//
1313-// Implementations should allow for concurrent access.
1515+// Implementations should generally allow for concurrent access.
1416type ClientAuthStore interface {
1515- GetSession(ctx context.Context, did syntax.DID) (*ClientSessionData, error)
1717+ GetSession(ctx context.Context, did syntax.DID, sessionID string) (*ClientSessionData, error)
1618 SaveSession(ctx context.Context, sess ClientSessionData) error
1717- DeleteSession(ctx context.Context, did syntax.DID) error
1919+ DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error
18201921 GetAuthRequestInfo(ctx context.Context, state string) (*AuthRequestData, error)
2022 SaveAuthRequestInfo(ctx context.Context, info AuthRequestData) error