···122122123123The [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 [ClientAuthStore]:
125125+To log out a user, use the [ClientApp.Logout] helper method, which revokes their tokens (if supported by the AS) and deletes their session from the [ClientAuthStore]:
126126127127- if err := oauthApp.Store.DeleteSession(r.Context(), did, sessionID); err != nil {
127127+ if err := oauthApp.Logout(r.Context(), did, sessionID); err != nil {
128128 return err
129129 }
130130
+9-2
atproto/auth/oauth/oauth.go
···668668 return err
669669 }
670670671671- // Tell the AS to revoke the tokens
672672- sess.RevokeSession(ctx)
671671+ // Tell the AS to revoke the tokens, if supported
672672+ if sess.Data.AuthServerRevocationEndpoint == "" {
673673+ slog.Info("AS does not support token revocation, skipping RevokeSession")
674674+ } else {
675675+ err = sess.RevokeSession(ctx)
676676+ if err != nil {
677677+ slog.Warn("error during session revocation", "err", err)
678678+ }
679679+ }
673680674681 // Delete from our own session store
675682 err = app.Store.DeleteSession(ctx, did, sessionID)
+17-15
atproto/auth/oauth/session.go
···44 "bytes"
55 "context"
66 "encoding/json"
77+ "errors"
78 "fmt"
89 "log/slog"
910 "net/http"
···133134 // already updated nonce value above; loop around and try again
134135 continue
135136 }
136136- return nil, fmt.Errorf("request failed (HTTP %d): %s", resp.StatusCode, reason)
137137+ return nil, fmt.Errorf("auth server request failed (HTTP %d): %s", resp.StatusCode, reason)
137138 }
138139139140 // otherwise process response (success or other error type)
···188189189190// If supported by the AS, use the revocation endpoint to revoke both the access token and the refresh token.
190191// This method always succeeds - any errors during revocation are logged but not returned.
191191-func (sess *ClientSession) RevokeSession(ctx context.Context) {
192192+func (sess *ClientSession) RevokeSession(ctx context.Context) error {
193193+ sess.lk.Lock()
194194+ defer sess.lk.Unlock()
195195+192196 if sess.Data.AuthServerRevocationEndpoint == "" {
193193- slog.Info("AS does not advertise token revocation support, skipping")
194194- return
197197+ return fmt.Errorf("AS does not support token revocation")
195198 }
196199197197- sess.lk.Lock()
198198- defer sess.lk.Unlock()
199199-200200- resp, err := sess.postToAuthServer(ctx, sess.Data.AuthServerRevocationEndpoint, RevocationRequest{
200200+ resp, err1 := sess.postToAuthServer(ctx, sess.Data.AuthServerRevocationEndpoint, RevocationRequest{
201201 ClientID: sess.Config.ClientID,
202202 Token: sess.Data.AccessToken,
203203 TokenTypeHint: "access_token",
204204 })
205205- if err != nil {
206206- slog.Warn("failed revoking access token", "err", err)
205205+ if err1 != nil {
206206+ err1 = fmt.Errorf("failed revoking access token: %w", err1)
207207 } else {
208208 if resp.StatusCode != http.StatusOK {
209209- slog.Warn("bad HTTP status while revoking access token", "status_code", resp.StatusCode)
209209+ err1 = fmt.Errorf("bad HTTP status while revoking access token (%d)", resp.StatusCode)
210210 }
211211 resp.Body.Close()
212212 }
213213214214- resp, err = sess.postToAuthServer(ctx, sess.Data.AuthServerRevocationEndpoint, RevocationRequest{
214214+ resp, err2 := sess.postToAuthServer(ctx, sess.Data.AuthServerRevocationEndpoint, RevocationRequest{
215215 ClientID: sess.Config.ClientID,
216216 Token: sess.Data.RefreshToken,
217217 TokenTypeHint: "refresh_token",
218218 })
219219- if err != nil {
220220- slog.Warn("failed revoking refresh token", "err", err)
219219+ if err2 != nil {
220220+ err2 = fmt.Errorf("failed revoking refresh token: %w", err1)
221221 } else {
222222 if resp.StatusCode != 200 {
223223- slog.Warn("bad HTTP status while revoking refresh token", "status_code", resp.StatusCode)
223223+ err2 = fmt.Errorf("bad HTTP status while revoking refresh token (%d)", resp.StatusCode)
224224 }
225225 resp.Body.Close()
226226 }
227227+228228+ return errors.Join(err1, err2) // returns nil if both errors are nil
227229}
228230229231// Constructs and signs a DPoP JWT to include in request header to Host (aka Resource Server, aka PDS). These tokens are different from those used with Auth Server token endpoints (even if the PDS is filling both roles)
+2
atproto/auth/oauth/types.go
···415415}
416416417417// The fields which are included in a token revocation request. These HTTP POST bodies are form-encoded, so use URL encoding syntax, not JSON.
418418+//
419419+// Per https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
418420type RevocationRequest struct {
419421 // Client ID, aka client metadata URL
420422 ClientID string `url:"client_id"`