feat: Implement OAuth with DPoP using @atproto/oauth-client-node

Replace manual OAuth implementation with official @atproto/oauth-client-node library to properly support DPoP (Demonstrating Proof of Possession) authentication.

Changes:
- Added @atproto/oauth-client-node dependency
- Created OAuth state store (SurrealDB-backed) for CSRF protection
- Created OAuth session store (SurrealDB-backed) for token persistence
- Created OAuth client singleton with localhost exception for development
- Rewrote /api/auth/login to use client.authorize()
- Rewrote /api/auth/callback to use client.callback() with DPoP
- Updated lib/auth/session.ts with getAuthenticatedAgent() for ATproto API calls
- Updated db/schema.surql with oauth_state and oauth_session tables
- Added scripts/apply-schema.js for database schema management
- Created plans/oauth-dpop-implementation.md with detailed implementation plan
- Removed legacy lib/auth/atproto.ts and lib/auth/oauth-state.ts
- Updated .env to use localhost exception (removed BLUESKY_CLIENT_ID)

The OAuth client now handles:
- PKCE code generation and verification
- DPoP proof generation and signing
- Automatic token refresh
- Session persistence across server restarts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-09 01:40:04 +00:00
parent bc9bbe12de
commit d7f3bcd338
13 changed files with 2104 additions and 333 deletions

182
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@atproto/api':
specifier: latest
version: 0.18.0
'@atproto/oauth-client-node':
specifier: ^0.3.10
version: 0.3.10
'@deepgram/sdk':
specifier: latest
version: 4.11.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
@@ -146,15 +149,66 @@ packages:
zod:
optional: true
'@atproto-labs/did-resolver@0.2.2':
resolution: {integrity: sha512-ca2B7xR43tVoQ8XxBvha58DXwIH8cIyKQl6lpOKGkPUrJuFoO4iCLlDiSDi2Ueh+yE1rMDPP/qveHdajgDX3WQ==}
'@atproto-labs/fetch-node@0.2.0':
resolution: {integrity: sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q==}
engines: {node: '>=18.7.0'}
'@atproto-labs/fetch@0.2.3':
resolution: {integrity: sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==}
'@atproto-labs/handle-resolver-node@0.1.21':
resolution: {integrity: sha512-fuJy5Px5pGF3lJX/ATdurbT8tbmaFWtf+PPxAQDFy7ot2no3t+iaAgymhyxYymrssOuWs6BwOP8tyF3VrfdwtQ==}
engines: {node: '>=18.7.0'}
'@atproto-labs/handle-resolver@0.3.2':
resolution: {integrity: sha512-KIerCzh3qb+zZoqWbIvTlvBY0XPq0r56kwViaJY/LTe/3oPO2JaqlYKS/F4dByWBhHK6YoUOJ0sWrh6PMJl40A==}
'@atproto-labs/identity-resolver@0.3.2':
resolution: {integrity: sha512-MYxO9pe0WsFyi5HFdKAwqIqHfiF2kBPoVhAIuH/4PYHzGr799ED47xLhNMxR3ZUYrJm5+TQzWXypGZ0Btw1Ffw==}
'@atproto-labs/pipe@0.1.1':
resolution: {integrity: sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==}
'@atproto-labs/simple-store-memory@0.1.4':
resolution: {integrity: sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==}
'@atproto-labs/simple-store@0.3.0':
resolution: {integrity: sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==}
'@atproto/api@0.18.0':
resolution: {integrity: sha512-2GxKPhhvMocDjRU7VpNj+cvCdmCHVAmRwyfNgRLMrJtPZvrosFoi9VATX+7eKN0FZvYvy8KdLSkCcpP2owH3IA==}
'@atproto/common-web@0.4.3':
resolution: {integrity: sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==}
'@atproto/did@0.2.1':
resolution: {integrity: sha512-1i5BTU2GnBaaeYWhxUOnuEKFVq9euT5+dQPFabHpa927BlJ54PmLGyBBaOI7/NbLmN5HWwBa18SBkMpg3jGZRA==}
'@atproto/jwk-jose@0.1.11':
resolution: {integrity: sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==}
'@atproto/jwk-webcrypto@0.2.0':
resolution: {integrity: sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==}
'@atproto/jwk@0.6.0':
resolution: {integrity: sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==}
'@atproto/lexicon@0.5.1':
resolution: {integrity: sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==}
'@atproto/oauth-client-node@0.3.10':
resolution: {integrity: sha512-6khKlJqu1Ed5rt3rzcTD5hymB6JUjKdOHWYXwiphw4inkAIo6GxLCighI4eGOqZorYk2j8ueeTNB6KsgH0kcRw==}
engines: {node: '>=18.7.0'}
'@atproto/oauth-client@0.5.8':
resolution: {integrity: sha512-7YEym6d97+Dd73qGdkQTXi5La8xvCQxwRUDzzlR/NVAARa9a4YP7MCmqBJVeP2anT0By+DSAPyPDLTsxcjIcCg==}
'@atproto/oauth-types@0.5.0':
resolution: {integrity: sha512-33xz7HcXhbl+XRqbIMVu3GE02iK1nKe2oMWENASsfZEYbCz2b9ZOarOFuwi7g4LKqpGowGp0iRKsQHFcq4SDaQ==}
'@atproto/syntax@0.4.1':
resolution: {integrity: sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==}
@@ -1460,6 +1514,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@@ -2028,6 +2085,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
ipaddr.js@2.2.0:
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
engines: {node: '>= 10'}
is-any-array@0.1.1:
resolution: {integrity: sha512-qTiELO+kpTKqPgxPYbshMERlzaFu29JDnpB8s3bjg+JkxBpw29/qqSaOdKv2pCdaG92rLGeG/zG2GauX58hfoA==}
@@ -2182,6 +2243,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@5.10.0:
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
jose@6.1.0:
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
@@ -2293,6 +2357,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
@@ -3126,6 +3193,10 @@ packages:
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici@6.22.0:
resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==}
engines: {node: '>=18.17'}
undici@7.16.0:
resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==}
engines: {node: '>=20.18.1'}
@@ -3378,6 +3449,53 @@ snapshots:
optionalDependencies:
zod: 4.1.12
'@atproto-labs/did-resolver@0.2.2':
dependencies:
'@atproto-labs/fetch': 0.2.3
'@atproto-labs/pipe': 0.1.1
'@atproto-labs/simple-store': 0.3.0
'@atproto-labs/simple-store-memory': 0.1.4
'@atproto/did': 0.2.1
zod: 3.25.76
'@atproto-labs/fetch-node@0.2.0':
dependencies:
'@atproto-labs/fetch': 0.2.3
'@atproto-labs/pipe': 0.1.1
ipaddr.js: 2.2.0
undici: 6.22.0
'@atproto-labs/fetch@0.2.3':
dependencies:
'@atproto-labs/pipe': 0.1.1
'@atproto-labs/handle-resolver-node@0.1.21':
dependencies:
'@atproto-labs/fetch-node': 0.2.0
'@atproto-labs/handle-resolver': 0.3.2
'@atproto/did': 0.2.1
'@atproto-labs/handle-resolver@0.3.2':
dependencies:
'@atproto-labs/simple-store': 0.3.0
'@atproto-labs/simple-store-memory': 0.1.4
'@atproto/did': 0.2.1
zod: 3.25.76
'@atproto-labs/identity-resolver@0.3.2':
dependencies:
'@atproto-labs/did-resolver': 0.2.2
'@atproto-labs/handle-resolver': 0.3.2
'@atproto-labs/pipe@0.1.1': {}
'@atproto-labs/simple-store-memory@0.1.4':
dependencies:
'@atproto-labs/simple-store': 0.3.0
lru-cache: 10.4.3
'@atproto-labs/simple-store@0.3.0': {}
'@atproto/api@0.18.0':
dependencies:
'@atproto/common-web': 0.4.3
@@ -3396,6 +3514,26 @@ snapshots:
uint8arrays: 3.0.0
zod: 3.25.76
'@atproto/did@0.2.1':
dependencies:
zod: 3.25.76
'@atproto/jwk-jose@0.1.11':
dependencies:
'@atproto/jwk': 0.6.0
jose: 5.10.0
'@atproto/jwk-webcrypto@0.2.0':
dependencies:
'@atproto/jwk': 0.6.0
'@atproto/jwk-jose': 0.1.11
zod: 3.25.76
'@atproto/jwk@0.6.0':
dependencies:
multiformats: 9.9.0
zod: 3.25.76
'@atproto/lexicon@0.5.1':
dependencies:
'@atproto/common-web': 0.4.3
@@ -3404,6 +3542,40 @@ snapshots:
multiformats: 9.9.0
zod: 3.25.76
'@atproto/oauth-client-node@0.3.10':
dependencies:
'@atproto-labs/did-resolver': 0.2.2
'@atproto-labs/handle-resolver-node': 0.1.21
'@atproto-labs/simple-store': 0.3.0
'@atproto/did': 0.2.1
'@atproto/jwk': 0.6.0
'@atproto/jwk-jose': 0.1.11
'@atproto/jwk-webcrypto': 0.2.0
'@atproto/oauth-client': 0.5.8
'@atproto/oauth-types': 0.5.0
'@atproto/oauth-client@0.5.8':
dependencies:
'@atproto-labs/did-resolver': 0.2.2
'@atproto-labs/fetch': 0.2.3
'@atproto-labs/handle-resolver': 0.3.2
'@atproto-labs/identity-resolver': 0.3.2
'@atproto-labs/simple-store': 0.3.0
'@atproto-labs/simple-store-memory': 0.1.4
'@atproto/did': 0.2.1
'@atproto/jwk': 0.6.0
'@atproto/oauth-types': 0.5.0
'@atproto/xrpc': 0.7.5
core-js: 3.46.0
multiformats: 9.9.0
zod: 3.25.76
'@atproto/oauth-types@0.5.0':
dependencies:
'@atproto/did': 0.2.1
'@atproto/jwk': 0.6.0
zod: 3.25.76
'@atproto/syntax@0.4.1': {}
'@atproto/xrpc@0.7.5':
@@ -4626,6 +4798,8 @@ snapshots:
convert-source-map@2.0.0: {}
core-js@3.46.0: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@@ -5345,6 +5519,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
ipaddr.js@2.2.0: {}
is-any-array@0.1.1: {}
is-any-array@2.0.1: {}
@@ -5503,6 +5679,8 @@ snapshots:
jiti@2.6.1: {}
jose@5.10.0: {}
jose@6.1.0: {}
joycon@3.1.1: {}
@@ -5613,6 +5791,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
lru-cache@10.4.3: {}
lru-cache@11.2.2: {}
lru-cache@5.1.1:
@@ -6594,6 +6774,8 @@ snapshots:
undici-types@7.16.0: {}
undici@6.22.0: {}
undici@7.16.0: {}
unrs-resolver@1.11.1: