Add OAuth files - authentication system
Migrated from app/: - oauth.go - OAuthManager, config loading, handle/DID resolution - oauth_session.go - SessionStore, encrypted cookies, token storage - oauth_middleware.go - RequireAuth middleware, token refresh - oauth_handlers.go - Login, callback, logout, JWKS endpoints Changed *DB to *shared.DB, using shared.StringValue/NullableString helpers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,75 @@ module github.com/1440news/dashboard
|
|||||||
|
|
||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require github.com/1440news/shared v0.0.0
|
require (
|
||||||
|
github.com/1440news/shared v0.0.0
|
||||||
|
github.com/haileyok/atproto-oauth-golang v0.0.3
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.6
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bluesky-social/indigo v0.0.0-20250616202859-d4516ea1d6cf // indirect
|
||||||
|
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
|
github.com/ipfs/bbloom v0.0.4 // indirect
|
||||||
|
github.com/ipfs/go-block-format v0.2.0 // indirect
|
||||||
|
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||||
|
github.com/ipfs/go-datastore v0.6.0 // indirect
|
||||||
|
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
|
||||||
|
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
|
||||||
|
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
|
||||||
|
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
|
||||||
|
github.com/ipfs/go-ipld-format v0.6.0 // indirect
|
||||||
|
github.com/ipfs/go-log v1.0.5 // indirect
|
||||||
|
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||||
|
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
|
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||||
|
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||||
|
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||||
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
|
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
|
||||||
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
|
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
|
lukechampine.com/blake3 v1.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/1440news/shared => ../shared
|
replace github.com/1440news/shared => ../shared
|
||||||
|
|||||||
@@ -1,6 +1,74 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/bluesky-social/indigo v0.0.0-20250616202859-d4516ea1d6cf h1:LFlwtY9r95lAI1yYKolCLTQnwK5VjgWO87mNsKdj3Qs=
|
||||||
|
github.com/bluesky-social/indigo v0.0.0-20250616202859-d4516ea1d6cf/go.mod h1:8FlFpF5cIq3DQG0kEHqyTkPV/5MDQoaWLcVwza5ZPJU=
|
||||||
|
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
|
||||||
|
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/haileyok/atproto-oauth-golang v0.0.3 h1:LdYSl6sgz11wnv8YD5e9WtopANEg4bCfMIXHMMnkOiI=
|
||||||
|
github.com/haileyok/atproto-oauth-golang v0.0.3/go.mod h1:vVRo6BPEmWOZnYk9LtXLzBPzfkY63fUaBahA+o4h55Q=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||||
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||||
|
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||||
|
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
|
||||||
|
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
|
||||||
|
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
|
||||||
|
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
|
||||||
|
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
||||||
|
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
|
||||||
|
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
|
||||||
|
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
|
||||||
|
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
|
||||||
|
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
|
||||||
|
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
|
||||||
|
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
|
||||||
|
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
|
||||||
|
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
|
||||||
|
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
|
||||||
|
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
|
||||||
|
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
|
||||||
|
github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk=
|
||||||
|
github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U=
|
||||||
|
github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg=
|
||||||
|
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
|
||||||
|
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
|
||||||
|
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
|
||||||
|
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
|
||||||
|
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
|
||||||
|
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
|
||||||
|
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -9,20 +77,181 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
|||||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
|
||||||
|
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
|
||||||
|
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
|
||||||
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
|
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||||
|
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||||
|
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||||
|
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||||
|
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||||
|
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||||
|
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||||
|
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||||
|
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||||
|
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
|
||||||
|
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
|
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||||
|
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
|
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
|
||||||
|
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
|
||||||
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||||
|
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
||||||
|
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
neturl "net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1440news/shared"
|
||||||
|
oauth "github.com/haileyok/atproto-oauth-golang"
|
||||||
|
"github.com/haileyok/atproto-oauth-golang/helpers"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthManager handles OAuth 2.0 authentication for the dashboard
|
||||||
|
type OAuthManager struct {
|
||||||
|
client *oauth.Client
|
||||||
|
clientID string
|
||||||
|
redirectURI string
|
||||||
|
privateJWK jwk.Key
|
||||||
|
publicJWK jwk.Key
|
||||||
|
sessions *SessionStore
|
||||||
|
cookieSecret []byte
|
||||||
|
allowedScope string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthConfig holds configuration for the OAuth manager
|
||||||
|
type OAuthConfig struct {
|
||||||
|
ClientID string // URL to client metadata (e.g., https://app.1440.news/.well-known/oauth-client-metadata)
|
||||||
|
RedirectURI string // OAuth callback URL (e.g., https://app.1440.news/auth/callback)
|
||||||
|
CookieSecret string // 32-byte hex string for AES-256-GCM encryption
|
||||||
|
PrivateJWK string // ES256 private key as JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOAuthManager creates a new OAuth manager
|
||||||
|
func NewOAuthManager(cfg OAuthConfig, db *shared.DB) (*OAuthManager, error) {
|
||||||
|
// Parse cookie secret (must be 32 bytes for AES-256)
|
||||||
|
cookieSecret, err := parseHexSecret(cfg.CookieSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid cookie secret: %v", err)
|
||||||
|
}
|
||||||
|
if len(cookieSecret) != 32 {
|
||||||
|
return nil, fmt.Errorf("cookie secret must be 32 bytes, got %d", len(cookieSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse private JWK
|
||||||
|
privateJWK, err := helpers.ParseJWKFromBytes([]byte(cfg.PrivateJWK))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid private JWK: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract public key
|
||||||
|
publicJWK, err := privateJWK.PublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP client with longer timeout
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create OAuth client
|
||||||
|
client, err := oauth.NewClient(oauth.ClientArgs{
|
||||||
|
Http: httpClient,
|
||||||
|
ClientJwk: privateJWK,
|
||||||
|
ClientId: cfg.ClientID,
|
||||||
|
RedirectUri: cfg.RedirectURI,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create OAuth client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OAuthManager{
|
||||||
|
client: client,
|
||||||
|
clientID: cfg.ClientID,
|
||||||
|
redirectURI: cfg.RedirectURI,
|
||||||
|
privateJWK: privateJWK,
|
||||||
|
publicJWK: publicJWK,
|
||||||
|
sessions: NewSessionStore(db),
|
||||||
|
cookieSecret: cookieSecret,
|
||||||
|
allowedScope: "atproto",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOAuthConfig loads OAuth configuration from environment or oauth.env file
|
||||||
|
func LoadOAuthConfig(baseURL string) (*OAuthConfig, error) {
|
||||||
|
cfg := &OAuthConfig{
|
||||||
|
ClientID: baseURL + "/.well-known/oauth-client-metadata",
|
||||||
|
RedirectURI: baseURL + "/auth/callback",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try environment variables first
|
||||||
|
cfg.CookieSecret = os.Getenv("OAUTH_COOKIE_SECRET")
|
||||||
|
cfg.PrivateJWK = os.Getenv("OAUTH_PRIVATE_JWK")
|
||||||
|
|
||||||
|
// Fall back to oauth.env file
|
||||||
|
if cfg.CookieSecret == "" || cfg.PrivateJWK == "" {
|
||||||
|
if data, err := os.ReadFile("oauth.env"); err == nil {
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "#") || line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
switch key {
|
||||||
|
case "OAUTH_COOKIE_SECRET":
|
||||||
|
cfg.CookieSecret = value
|
||||||
|
case "OAUTH_PRIVATE_JWK":
|
||||||
|
cfg.PrivateJWK = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if cfg.CookieSecret == "" {
|
||||||
|
return nil, fmt.Errorf("OAUTH_COOKIE_SECRET not configured")
|
||||||
|
}
|
||||||
|
if cfg.PrivateJWK == "" {
|
||||||
|
return nil, fmt.Errorf("OAUTH_PRIVATE_JWK not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHexSecret converts a hex string to bytes
|
||||||
|
func parseHexSecret(hex string) ([]byte, error) {
|
||||||
|
if len(hex)%2 != 0 {
|
||||||
|
return nil, fmt.Errorf("hex string must have even length")
|
||||||
|
}
|
||||||
|
b := make([]byte, len(hex)/2)
|
||||||
|
for i := 0; i < len(hex); i += 2 {
|
||||||
|
var val byte
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
c := hex[i+j]
|
||||||
|
switch {
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
val = val*16 + (c - '0')
|
||||||
|
case c >= 'a' && c <= 'f':
|
||||||
|
val = val*16 + (c - 'a' + 10)
|
||||||
|
case c >= 'A' && c <= 'F':
|
||||||
|
val = val*16 + (c - 'A' + 10)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid hex character: %c", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b[i/2] = val
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveHandle resolves a Bluesky handle to a DID
|
||||||
|
func resolveHandle(ctx context.Context, handle string) (string, error) {
|
||||||
|
// Normalize handle (remove @ prefix and whitespace)
|
||||||
|
handle = strings.TrimSpace(handle)
|
||||||
|
handle = strings.TrimPrefix(handle, "@")
|
||||||
|
handle = strings.ToLower(handle)
|
||||||
|
|
||||||
|
// Try DNS-based resolution first
|
||||||
|
url := fmt.Sprintf("https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=%s", neturl.QueryEscape(handle))
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", fmt.Errorf("resolve handle failed: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
DID string `json:"did"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.DID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveDIDToHandle resolves a DID to the current handle
|
||||||
|
func resolveDIDToHandle(ctx context.Context, did string) (string, error) {
|
||||||
|
// Fetch DID document
|
||||||
|
var docURL string
|
||||||
|
if strings.HasPrefix(did, "did:plc:") {
|
||||||
|
docURL = fmt.Sprintf("https://plc.directory/%s", did)
|
||||||
|
} else if strings.HasPrefix(did, "did:web:") {
|
||||||
|
domain := strings.TrimPrefix(did, "did:web:")
|
||||||
|
docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("unsupported DID method: %s", did)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", docURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("failed to fetch DID document: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc struct {
|
||||||
|
AlsoKnownAs []string `json:"alsoKnownAs"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the at:// handle
|
||||||
|
for _, aka := range doc.AlsoKnownAs {
|
||||||
|
if strings.HasPrefix(aka, "at://") {
|
||||||
|
return strings.TrimPrefix(aka, "at://"), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no handle found for DID %s", did)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveDIDToService gets the PDS service URL from a DID
|
||||||
|
func resolveDIDToService(ctx context.Context, did string) (string, error) {
|
||||||
|
var docURL string
|
||||||
|
if strings.HasPrefix(did, "did:plc:") {
|
||||||
|
docURL = fmt.Sprintf("https://plc.directory/%s", did)
|
||||||
|
} else if strings.HasPrefix(did, "did:web:") {
|
||||||
|
domain := strings.TrimPrefix(did, "did:web:")
|
||||||
|
docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("unsupported DID method: %s", did)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", docURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("failed to fetch DID document: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc struct {
|
||||||
|
Service []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ServiceEndpoint string `json:"serviceEndpoint"`
|
||||||
|
} `json:"service"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the atproto_pds service
|
||||||
|
for _, svc := range doc.Service {
|
||||||
|
if svc.Type == "AtprotoPersonalDataServer" || svc.ID == "#atproto_pds" {
|
||||||
|
return svc.ServiceEndpoint, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no PDS service found for DID %s", did)
|
||||||
|
}
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/haileyok/atproto-oauth-golang/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var allowedHandles = map[string]bool{
|
||||||
|
"1440.news": true,
|
||||||
|
"wehrv.bsky.social": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleClientMetadata serves the OAuth client metadata
|
||||||
|
func (m *OAuthManager) HandleClientMetadata(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the JWKS URI from the same host
|
||||||
|
scheme := "https"
|
||||||
|
if r.TLS == nil && (r.Host == "localhost" || r.Host == "127.0.0.1" || r.Host == "app.1440.localhost:4321") {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
baseURL := scheme + "://" + r.Host
|
||||||
|
|
||||||
|
metadata := map[string]interface{}{
|
||||||
|
"client_id": m.clientID,
|
||||||
|
"client_name": "1440.news Dashboard",
|
||||||
|
"client_uri": baseURL,
|
||||||
|
"redirect_uris": []string{m.redirectURI},
|
||||||
|
"grant_types": []string{"authorization_code", "refresh_token"},
|
||||||
|
"response_types": []string{"code"},
|
||||||
|
"scope": "atproto",
|
||||||
|
"token_endpoint_auth_method": "private_key_jwt",
|
||||||
|
"token_endpoint_auth_signing_alg": "ES256",
|
||||||
|
"dpop_bound_access_tokens": true,
|
||||||
|
"jwks_uri": baseURL + "/.well-known/jwks.json",
|
||||||
|
"application_type": "web",
|
||||||
|
"subject_type": "public",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleJWKS serves the public JWK set
|
||||||
|
func (m *OAuthManager) HandleJWKS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
jwks := helpers.CreateJwksResponseObject(m.publicJWK)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(jwks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleLogin serves the login page or initiates OAuth flow
|
||||||
|
func (m *OAuthManager) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check if already logged in
|
||||||
|
if session := m.GetSessionFromCookie(r); session != nil {
|
||||||
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If handle is provided, start OAuth flow
|
||||||
|
handle := r.URL.Query().Get("handle")
|
||||||
|
if handle != "" {
|
||||||
|
// Save handle to cookie for prefill on next visit
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "last_handle",
|
||||||
|
Value: handle,
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 365, // 1 year
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
m.startOAuthFlow(w, r, handle)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last handle from cookie for prefill
|
||||||
|
lastHandle := ""
|
||||||
|
if cookie, err := r.Cookie("last_handle"); err == nil {
|
||||||
|
lastHandle = cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve login page
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
tmpl := template.Must(template.New("login").Parse(loginPageHTML))
|
||||||
|
tmpl.Execute(w, map[string]string{"LastHandle": lastHandle})
|
||||||
|
}
|
||||||
|
|
||||||
|
// startOAuthFlow initiates the OAuth flow for a given handle
|
||||||
|
func (m *OAuthManager) startOAuthFlow(w http.ResponseWriter, r *http.Request, handle string) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Auto-append .bsky.social if handle has no dots
|
||||||
|
if !strings.Contains(handle, ".") {
|
||||||
|
handle = handle + ".bsky.social"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("OAuth: starting flow for handle: %s\n", handle)
|
||||||
|
|
||||||
|
// Resolve handle to DID
|
||||||
|
did, err := resolveHandle(ctx, handle)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth: resolved DID: %s\n", did)
|
||||||
|
|
||||||
|
// Resolve DID to PDS service URL
|
||||||
|
pdsURL, err := resolveDIDToService(ctx, did)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to resolve PDS: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth: PDS URL: %s\n", pdsURL)
|
||||||
|
|
||||||
|
// Get auth server from PDS
|
||||||
|
authServerURL, err := m.client.ResolvePdsAuthServer(ctx, pdsURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to resolve auth server: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth: auth server: %s\n", authServerURL)
|
||||||
|
|
||||||
|
// Fetch auth server metadata
|
||||||
|
authMeta, err := m.client.FetchAuthServerMetadata(ctx, authServerURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to fetch auth metadata: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth: auth endpoint: %s\n", authMeta.AuthorizationEndpoint)
|
||||||
|
|
||||||
|
// Generate DPoP private key for this auth flow
|
||||||
|
dpopKey, err := helpers.GenerateKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to generate DPoP key: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dpopKeyBytes, err := json.Marshal(dpopKey)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to marshal DPoP key: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send PAR (Pushed Authorization Request)
|
||||||
|
fmt.Printf("OAuth: sending PAR to %s\n", authServerURL)
|
||||||
|
parResp, err := m.client.SendParAuthRequest(
|
||||||
|
ctx,
|
||||||
|
authServerURL,
|
||||||
|
authMeta,
|
||||||
|
handle,
|
||||||
|
m.allowedScope,
|
||||||
|
dpopKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("OAuth: PAR failed: %v\n", err)
|
||||||
|
http.Error(w, fmt.Sprintf("PAR request failed: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth: PAR success, request_uri: %s\n", parResp.RequestUri)
|
||||||
|
|
||||||
|
// Save pending auth state
|
||||||
|
pending := &PendingAuth{
|
||||||
|
State: parResp.State,
|
||||||
|
PkceVerifier: parResp.PkceVerifier,
|
||||||
|
DpopPrivateJWK: string(dpopKeyBytes),
|
||||||
|
DpopNonce: parResp.DpopAuthserverNonce,
|
||||||
|
DID: did,
|
||||||
|
PdsURL: pdsURL,
|
||||||
|
AuthserverIss: authMeta.Issuer,
|
||||||
|
}
|
||||||
|
m.sessions.SavePending(parResp.State, pending)
|
||||||
|
|
||||||
|
// Build authorization URL
|
||||||
|
authURL, err := url.Parse(authMeta.AuthorizationEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Invalid auth endpoint: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := authURL.Query()
|
||||||
|
q.Set("client_id", m.clientID)
|
||||||
|
q.Set("request_uri", parResp.RequestUri)
|
||||||
|
authURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
fmt.Printf("OAuth: redirecting to: %s\n", authURL.String())
|
||||||
|
|
||||||
|
http.Redirect(w, r, authURL.String(), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCallback handles the OAuth callback
|
||||||
|
func (m *OAuthManager) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Printf("OAuth callback: received request from %s\n", r.URL.String())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Get callback parameters
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
iss := r.URL.Query().Get("iss")
|
||||||
|
errorParam := r.URL.Query().Get("error")
|
||||||
|
errorDesc := r.URL.Query().Get("error_description")
|
||||||
|
|
||||||
|
codePreview := code
|
||||||
|
if len(codePreview) > 10 {
|
||||||
|
codePreview = codePreview[:10]
|
||||||
|
}
|
||||||
|
statePreview := state
|
||||||
|
if len(statePreview) > 10 {
|
||||||
|
statePreview = statePreview[:10]
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth callback: code=%s..., state=%s..., iss=%s, error=%s\n",
|
||||||
|
codePreview, statePreview, iss, errorParam)
|
||||||
|
|
||||||
|
// Check for errors from auth server
|
||||||
|
if errorParam != "" {
|
||||||
|
http.Error(w, fmt.Sprintf("Authorization error: %s - %s", errorParam, errorDesc), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == "" || state == "" {
|
||||||
|
http.Error(w, "Missing code or state parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve pending auth state
|
||||||
|
pending := m.sessions.GetPending(state)
|
||||||
|
if pending == nil {
|
||||||
|
fmt.Printf("OAuth callback: no pending state found for %s\n", state)
|
||||||
|
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth callback: found pending state for DID %s\n", pending.DID)
|
||||||
|
|
||||||
|
// Verify issuer matches
|
||||||
|
if iss != "" && iss != pending.AuthserverIss {
|
||||||
|
http.Error(w, "Issuer mismatch", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse DPoP private key
|
||||||
|
dpopKey, err := helpers.ParseJWKFromBytes([]byte(pending.DpopPrivateJWK))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to parse DPoP key: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange code for tokens
|
||||||
|
fmt.Printf("OAuth callback: exchanging code for tokens at %s\n", pending.AuthserverIss)
|
||||||
|
tokenResp, err := m.client.InitialTokenRequest(
|
||||||
|
ctx,
|
||||||
|
code,
|
||||||
|
pending.AuthserverIss,
|
||||||
|
pending.PkceVerifier,
|
||||||
|
pending.DpopNonce,
|
||||||
|
dpopKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("OAuth callback: token exchange failed: %v\n", err)
|
||||||
|
http.Error(w, fmt.Sprintf("Token exchange failed: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth callback: token exchange success, sub=%s, scope=%s\n", tokenResp.Sub, tokenResp.Scope)
|
||||||
|
|
||||||
|
// Verify scope
|
||||||
|
if tokenResp.Scope != m.allowedScope {
|
||||||
|
fmt.Printf("OAuth callback: scope mismatch: expected %s, got %s\n", m.allowedScope, tokenResp.Scope)
|
||||||
|
http.Error(w, fmt.Sprintf("Invalid scope: expected %s, got %s", m.allowedScope, tokenResp.Scope), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve DID to handle
|
||||||
|
fmt.Printf("OAuth callback: resolving DID %s to handle\n", tokenResp.Sub)
|
||||||
|
handle, err := resolveDIDToHandle(ctx, tokenResp.Sub)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("OAuth callback: failed to resolve handle: %v\n", err)
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth callback: resolved handle: %s\n", handle)
|
||||||
|
|
||||||
|
// CRITICAL: Verify user is allowed
|
||||||
|
if !allowedHandles[handle] {
|
||||||
|
fmt.Printf("OAuth callback: access denied for handle: %s (allowed: %v)\n", handle, allowedHandles)
|
||||||
|
http.Error(w, "Access denied.", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth callback: handle %s is allowed\n", handle)
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
fmt.Printf("OAuth callback: creating session for %s\n", handle)
|
||||||
|
session, err := m.sessions.CreateSession(tokenResp.Sub, handle)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("OAuth callback: failed to create session: %v\n", err)
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to create session: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("OAuth callback: session created with ID %s\n", session.ID)
|
||||||
|
|
||||||
|
// Store token info in session
|
||||||
|
session.AccessToken = tokenResp.AccessToken
|
||||||
|
session.RefreshToken = tokenResp.RefreshToken
|
||||||
|
session.TokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
||||||
|
session.DpopPrivateJWK = pending.DpopPrivateJWK
|
||||||
|
session.DpopAuthserverNonce = tokenResp.DpopAuthserverNonce
|
||||||
|
session.PdsURL = pending.PdsURL
|
||||||
|
session.AuthserverIss = pending.AuthserverIss
|
||||||
|
m.sessions.UpdateSession(session)
|
||||||
|
|
||||||
|
// Set session cookie
|
||||||
|
fmt.Printf("OAuth callback: setting session cookie\n")
|
||||||
|
if err := m.SetSessionCookie(w, r, session.ID); err != nil {
|
||||||
|
fmt.Printf("OAuth callback: failed to set cookie: %v\n", err)
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to set cookie: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to dashboard
|
||||||
|
fmt.Printf("OAuth callback: success! redirecting to /dashboard\n")
|
||||||
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleLogout clears the session and redirects to login
|
||||||
|
func (m *OAuthManager) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get current session
|
||||||
|
session := m.GetSessionFromCookie(r)
|
||||||
|
if session != nil {
|
||||||
|
// Delete session from store
|
||||||
|
m.sessions.DeleteSession(session.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cookie
|
||||||
|
m.ClearSessionCookie(w)
|
||||||
|
|
||||||
|
// Handle API vs browser request
|
||||||
|
if r.Method == http.MethodPost || isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "logged out",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login for browser requests
|
||||||
|
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSessionInfo returns current session info (for API calls)
|
||||||
|
func (m *OAuthManager) HandleSessionInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := m.GetSessionFromCookie(r)
|
||||||
|
if session == nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": "not authenticated",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &SessionInfo{
|
||||||
|
DID: session.DID,
|
||||||
|
Handle: session.Handle,
|
||||||
|
ExpiresAt: session.ExpiresAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginPageHTML = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Sign In - 1440.news Dashboard</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-size: 3em;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.tagline {
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
background: #151515;
|
||||||
|
border: 1px solid #252525;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0af;
|
||||||
|
}
|
||||||
|
input[type="text"]::placeholder {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: #0af;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.login-btn:hover {
|
||||||
|
background: #0cf;
|
||||||
|
}
|
||||||
|
.login-btn:disabled {
|
||||||
|
background: #555;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #2a1515;
|
||||||
|
border: 1px solid #533;
|
||||||
|
color: #f88;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.bluesky-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">1440.news</div>
|
||||||
|
<p class="tagline">Dashboard Authentication</p>
|
||||||
|
|
||||||
|
<div class="login-card">
|
||||||
|
<h2>Sign In with Bluesky</h2>
|
||||||
|
|
||||||
|
<div class="error" id="error"></div>
|
||||||
|
|
||||||
|
<form id="loginForm" action="/auth/login" method="get">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="handle">Bluesky Handle</label>
|
||||||
|
<input type="text" id="handle" name="handle" placeholder="username or full.handle" value="{{.LastHandle}}" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-btn" id="loginBtn">
|
||||||
|
<svg class="bluesky-icon" viewBox="0 0 568 501" fill="currentColor">
|
||||||
|
<path d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C286.251 378.892 284.991 374.834 284 371.019C283.009 374.834 281.749 378.892 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0533 296.954 15.7778 224.501C9.94533 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z"/>
|
||||||
|
</svg>
|
||||||
|
Sign In with Bluesky
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||||
|
const handle = document.getElementById('handle').value.trim();
|
||||||
|
if (!handle) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('error').style.display = 'block';
|
||||||
|
document.getElementById('error').textContent = 'Please enter your handle';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('loginBtn').disabled = true;
|
||||||
|
document.getElementById('loginBtn').textContent = 'Redirecting...';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/haileyok/atproto-oauth-golang/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequireAuth is middleware that protects routes requiring authentication
|
||||||
|
func (m *OAuthManager) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := m.GetSessionFromCookie(r)
|
||||||
|
if session == nil {
|
||||||
|
fmt.Printf("RequireAuth: no session found for %s\n", r.URL.Path)
|
||||||
|
// Check if this is an API call (wants JSON response)
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": "unauthorized",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Redirect to login for browser requests
|
||||||
|
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token needs refresh (refresh when within 5 minutes of expiry)
|
||||||
|
if time.Until(session.TokenExpiry) < 5*time.Minute {
|
||||||
|
if err := m.refreshToken(r.Context(), session); err != nil {
|
||||||
|
// Token refresh failed - clear session and redirect to login
|
||||||
|
m.sessions.DeleteSession(session.ID)
|
||||||
|
m.ClearSessionCookie(w)
|
||||||
|
|
||||||
|
if isAPIRequest(r) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": "session expired",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add session to request context
|
||||||
|
ctx := context.WithValue(r.Context(), sessionContextKey, session)
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessionContextKey is the context key for the OAuth session
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const sessionContextKey contextKey = "oauth_session"
|
||||||
|
|
||||||
|
// GetSession retrieves the session from request context
|
||||||
|
func GetSession(r *http.Request) *OAuthSession {
|
||||||
|
session, _ := r.Context().Value(sessionContextKey).(*OAuthSession)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAPIRequest checks if the request expects JSON response
|
||||||
|
func isAPIRequest(r *http.Request) bool {
|
||||||
|
// Check Accept header
|
||||||
|
accept := r.Header.Get("Accept")
|
||||||
|
if strings.Contains(accept, "application/json") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check URL path
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check X-Requested-With header (for AJAX)
|
||||||
|
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshToken refreshes the OAuth access token
|
||||||
|
func (m *OAuthManager) refreshToken(ctx context.Context, session *OAuthSession) error {
|
||||||
|
if session.RefreshToken == "" {
|
||||||
|
return nil // No refresh token available
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the DPoP private key
|
||||||
|
dpopKey, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJWK))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the token
|
||||||
|
tokenResp, err := m.client.RefreshTokenRequest(
|
||||||
|
ctx,
|
||||||
|
session.RefreshToken,
|
||||||
|
session.AuthserverIss,
|
||||||
|
session.DpopAuthserverNonce,
|
||||||
|
dpopKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session with new tokens
|
||||||
|
session.AccessToken = tokenResp.AccessToken
|
||||||
|
session.RefreshToken = tokenResp.RefreshToken
|
||||||
|
session.TokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
||||||
|
session.DpopAuthserverNonce = tokenResp.DpopAuthserverNonce
|
||||||
|
|
||||||
|
// Save updated session
|
||||||
|
m.sessions.UpdateSession(session)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1440news/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionCookieName = "1440_session"
|
||||||
|
sessionTTL = 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthSession stores the OAuth session state for a user
|
||||||
|
type OAuthSession struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DID string `json:"did"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
|
||||||
|
// OAuth tokens (stored server-side only)
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenExpiry time.Time `json:"token_expiry"`
|
||||||
|
|
||||||
|
// DPoP state
|
||||||
|
DpopPrivateJWK string `json:"dpop_private_jwk"`
|
||||||
|
DpopAuthserverNonce string `json:"dpop_authserver_nonce"`
|
||||||
|
DpopPdsNonce string `json:"dpop_pds_nonce"`
|
||||||
|
|
||||||
|
// Auth server info
|
||||||
|
PdsURL string `json:"pds_url"`
|
||||||
|
AuthserverIss string `json:"authserver_iss"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PendingAuth stores state during the OAuth flow (before callback)
|
||||||
|
type PendingAuth struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
PkceVerifier string `json:"pkce_verifier"`
|
||||||
|
DpopPrivateJWK string `json:"dpop_private_jwk"`
|
||||||
|
DpopNonce string `json:"dpop_nonce"`
|
||||||
|
DID string `json:"did"`
|
||||||
|
PdsURL string `json:"pds_url"`
|
||||||
|
AuthserverIss string `json:"authserver_iss"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStore manages sessions in the database
|
||||||
|
type SessionStore struct {
|
||||||
|
db *shared.DB
|
||||||
|
pending map[string]*PendingAuth // keyed by state (short-lived, kept in memory)
|
||||||
|
mu sync.RWMutex
|
||||||
|
cleanupOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSessionStore creates a new session store
|
||||||
|
func NewSessionStore(db *shared.DB) *SessionStore {
|
||||||
|
s := &SessionStore{
|
||||||
|
db: db,
|
||||||
|
pending: make(map[string]*PendingAuth),
|
||||||
|
}
|
||||||
|
s.startCleanup()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// startCleanup starts a background goroutine to clean up expired sessions
|
||||||
|
func (s *SessionStore) startCleanup() {
|
||||||
|
s.cleanupOnce.Do(func() {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
s.cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup removes expired sessions and pending auths
|
||||||
|
func (s *SessionStore) cleanup() {
|
||||||
|
// Clean up expired sessions from database
|
||||||
|
s.db.Exec("DELETE FROM oauth_sessions WHERE expires_at < NOW()")
|
||||||
|
|
||||||
|
// Clean up old pending auths (10 minute timeout) from memory
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
now := time.Now()
|
||||||
|
for state, pending := range s.pending {
|
||||||
|
if now.Sub(pending.CreatedAt) > 10*time.Minute {
|
||||||
|
delete(s.pending, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSession creates a new session and returns it
|
||||||
|
func (s *SessionStore) CreateSession(did, handle string) (*OAuthSession, error) {
|
||||||
|
id, err := generateRandomID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
session := &OAuthSession{
|
||||||
|
ID: id,
|
||||||
|
DID: did,
|
||||||
|
Handle: handle,
|
||||||
|
CreatedAt: now,
|
||||||
|
ExpiresAt: now.Add(sessionTTL),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.db.Exec(`
|
||||||
|
INSERT INTO oauth_sessions (id, did, handle, created_at, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
`, session.ID, session.DID, session.Handle, session.CreatedAt, session.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSession retrieves a session by ID
|
||||||
|
func (s *SessionStore) GetSession(id string) *OAuthSession {
|
||||||
|
row := s.db.QueryRow(`
|
||||||
|
SELECT id, did, handle, created_at, expires_at,
|
||||||
|
access_token, refresh_token, token_expiry,
|
||||||
|
dpop_private_jwk, dpop_authserver_nonce, dpop_pds_nonce,
|
||||||
|
pds_url, authserver_iss
|
||||||
|
FROM oauth_sessions
|
||||||
|
WHERE id = $1 AND expires_at > NOW()
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
var session OAuthSession
|
||||||
|
var accessToken, refreshToken, dpopJwk, dpopAuthNonce, dpopPdsNonce, pdsURL, authIss *string
|
||||||
|
var tokenExpiry *time.Time
|
||||||
|
|
||||||
|
err := row.Scan(
|
||||||
|
&session.ID, &session.DID, &session.Handle, &session.CreatedAt, &session.ExpiresAt,
|
||||||
|
&accessToken, &refreshToken, &tokenExpiry,
|
||||||
|
&dpopJwk, &dpopAuthNonce, &dpopPdsNonce,
|
||||||
|
&pdsURL, &authIss,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
session.AccessToken = shared.StringValue(accessToken)
|
||||||
|
session.RefreshToken = shared.StringValue(refreshToken)
|
||||||
|
if tokenExpiry != nil {
|
||||||
|
session.TokenExpiry = *tokenExpiry
|
||||||
|
}
|
||||||
|
session.DpopPrivateJWK = shared.StringValue(dpopJwk)
|
||||||
|
session.DpopAuthserverNonce = shared.StringValue(dpopAuthNonce)
|
||||||
|
session.DpopPdsNonce = shared.StringValue(dpopPdsNonce)
|
||||||
|
session.PdsURL = shared.StringValue(pdsURL)
|
||||||
|
session.AuthserverIss = shared.StringValue(authIss)
|
||||||
|
|
||||||
|
return &session
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSession updates a session
|
||||||
|
func (s *SessionStore) UpdateSession(session *OAuthSession) {
|
||||||
|
s.db.Exec(`
|
||||||
|
UPDATE oauth_sessions SET
|
||||||
|
access_token = $2,
|
||||||
|
refresh_token = $3,
|
||||||
|
token_expiry = $4,
|
||||||
|
dpop_private_jwk = $5,
|
||||||
|
dpop_authserver_nonce = $6,
|
||||||
|
dpop_pds_nonce = $7,
|
||||||
|
pds_url = $8,
|
||||||
|
authserver_iss = $9
|
||||||
|
WHERE id = $1
|
||||||
|
`,
|
||||||
|
session.ID,
|
||||||
|
shared.NullableString(session.AccessToken),
|
||||||
|
shared.NullableString(session.RefreshToken),
|
||||||
|
shared.NullableTime(session.TokenExpiry),
|
||||||
|
shared.NullableString(session.DpopPrivateJWK),
|
||||||
|
shared.NullableString(session.DpopAuthserverNonce),
|
||||||
|
shared.NullableString(session.DpopPdsNonce),
|
||||||
|
shared.NullableString(session.PdsURL),
|
||||||
|
shared.NullableString(session.AuthserverIss),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession removes a session
|
||||||
|
func (s *SessionStore) DeleteSession(id string) {
|
||||||
|
s.db.Exec("DELETE FROM oauth_sessions WHERE id = $1", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePending saves pending OAuth state (kept in memory - short lived)
|
||||||
|
func (s *SessionStore) SavePending(state string, pending *PendingAuth) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
pending.CreatedAt = time.Now()
|
||||||
|
s.pending[state] = pending
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPending retrieves and removes pending OAuth state
|
||||||
|
func (s *SessionStore) GetPending(state string) *PendingAuth {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
pending, ok := s.pending[state]
|
||||||
|
if ok {
|
||||||
|
delete(s.pending, state)
|
||||||
|
}
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomID generates a random session ID
|
||||||
|
func generateRandomID() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptSessionID encrypts a session ID using AES-256-GCM
|
||||||
|
func encryptSessionID(sessionID string, key []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := gcm.Seal(nonce, nonce, []byte(sessionID), nil)
|
||||||
|
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptSessionID decrypts a session ID using AES-256-GCM
|
||||||
|
func decryptSessionID(encrypted string, key []byte) (string, error) {
|
||||||
|
ciphertext, err := base64.URLEncoding.DecodeString(encrypted)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ciphertext) < gcm.NonceSize() {
|
||||||
|
return "", fmt.Errorf("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():]
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSessionCookie sets an encrypted session cookie
|
||||||
|
func (m *OAuthManager) SetSessionCookie(w http.ResponseWriter, r *http.Request, sessionID string) error {
|
||||||
|
encrypted, err := encryptSessionID(sessionID, m.cookieSecret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set Secure flag for HTTPS connections
|
||||||
|
secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
|
||||||
|
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: sessionCookieName,
|
||||||
|
Value: encrypted,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: secure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: int(sessionTTL.Seconds()),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionFromCookie retrieves the session from the request cookie
|
||||||
|
func (m *OAuthManager) GetSessionFromCookie(r *http.Request) *OAuthSession {
|
||||||
|
cookie, err := r.Cookie(sessionCookieName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("GetSessionFromCookie: no cookie found: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("GetSessionFromCookie: found cookie, length=%d\n", len(cookie.Value))
|
||||||
|
|
||||||
|
sessionID, err := decryptSessionID(cookie.Value, m.cookieSecret)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("GetSessionFromCookie: decrypt failed: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("GetSessionFromCookie: decrypted session ID: %s\n", sessionID)
|
||||||
|
|
||||||
|
session := m.sessions.GetSession(sessionID)
|
||||||
|
if session == nil {
|
||||||
|
fmt.Printf("GetSessionFromCookie: session not found in store\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("GetSessionFromCookie: found session for %s\n", session.Handle)
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSessionCookie removes the session cookie
|
||||||
|
func (m *OAuthManager) ClearSessionCookie(w http.ResponseWriter) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: sessionCookieName,
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionInfo is the public session info returned to the client
|
||||||
|
type SessionInfo struct {
|
||||||
|
DID string `json:"did"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON converts SessionInfo to JSON
|
||||||
|
func (s *SessionInfo) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias SessionInfo
|
||||||
|
return json.Marshal((*Alias)(s))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user