Conversation
| scope, | ||
| dpopJkt, | ||
| accessTokenTtl = ACCESS_TOKEN_TTL, | ||
| refreshTokenTtl = REFRESH_TOKEN_TTL, |
There was a problem hiding this comment.
Interestingly, REFRESH_TOKEN_TTL was completely unused before this PR (it was just exported by the package).
| refreshExpiresAt: rotateRefreshToken | ||
| ? now + refreshTokenTtl | ||
| : existingData.refreshExpiresAt, |
There was a problem hiding this comment.
Alternatively, we could simply do:
| refreshExpiresAt: rotateRefreshToken | |
| ? now + refreshTokenTtl | |
| : existingData.refreshExpiresAt, | |
| refreshExpiresAt: existingData.refreshExpiresAt, |
That way, even though the refresh token is rotated, it still expires 90 days from the original auth. That way, at some point, we force users to re-auth, rather than the token being infinitely refreshable.
I'm curious to know what you think would be best!
| ## Post-Completion Updates | ||
|
|
||
| - ✅ PDS auth middleware now returns DPoP `WWW-Authenticate` invalid_token challenges on 401 responses so OAuth clients can trigger automatic refresh. | ||
| - ✅ OAuth token storage now uses separate access/refresh expiries, and cleanup prunes by refresh expiry so refresh remains possible after access expiry. |
There was a problem hiding this comment.
Wasn't sure where the most appropriate place to document these changes was - happy to put this information somewhere else if you prefer!
| this.sql.exec("DELETE FROM oauth_auth_codes WHERE expires_at < ?", now); | ||
| this.sql.exec( | ||
| "DELETE FROM oauth_tokens WHERE expires_at < ? AND revoked = 0", | ||
| "DELETE FROM oauth_tokens WHERE refresh_expires_at < ?", |
There was a problem hiding this comment.
From the PR description:
When
cleanup()runs, determine which rows inoauth_tokensto delete based onrefresh_expires_at, notaccess_expires_at.Also, a mostly unrelated change that I think makes sense, but would be happy to undo: delete expired
oauth_tokenseven if they have been revoked.
The two bugs:
The user-facing bug I observed
After I set up my PDS using cirrus, I logged into tangled.org using OAuth. Everything worked great for a while, but the next morning I was unable to edit my profile even though I was still logged into the site. Logging out and then back in fixed the issue, but I repeatedly saw this issue. It would occur 60 minutes after logging in.
OAuth token expiry
I realized: although every row in the
oauth_tokenstable has anaccess_tokenand arefresh_token, it only has a singleexpires_atcolumn which represents when theaccess_tokenexpires. Thisexpires_atcolumn is used to determine whether or not a row in theoauth_tokenstable should be deleted in thecleanup()function.If the
cleanup()function runs before the third party application (in this case tangled.org) attempts to use therefresh_tokento refresh, then refreshing will fail because the corresponding row in theoauth_tokenstable is gone.WWW-AuthenticateheaderAlso, when a third party application makes a request to the PDS with an expired
access_token, we respond with a 401, but it's not clear from the response why the request is unauthorized. According to the AT Protocol spec:tangled.org uses indigo as its OAuth client; here's how it handles 401 responses from a PDS:
WWW-Authenticateheader is blank, simply return the 401 response. Otherwise, continue. (code link)WWW-Authenticateheader forerror=invalid_token. (code link)That means tangled.org (and other spec-compliant apps) will only attempt to refresh a token if we attach the
WWW-Authenticateheader witherror=invalid_tokento the 401 response.My changes:
8616521
Add
WWW-Authenticateheader witherror=invalid_tokento 401 responses when the token is invalid.091b469
Replace
oauth_tokens.expires_atwithaccess_expires_atandrefresh_expires_at.When
cleanup()runs, determine which rows inoauth_tokensto delete based onrefresh_expires_at, notaccess_expires_at.Also, a mostly unrelated change that I think makes sense, but would be happy to undo: delete expired
oauth_tokenseven if they have been revoked.Demo
This version is running right now at https://pds.aidanlavis.dev/
Before:
Screen.Recording.2026-04-12.at.12.35.05.AM.mov
After:
Screen.Recording.2026-04-12.at.12.22.46.PM.mov