Live video on the AT Protocol

oatproxy: split oatproxy as a library and utilize it (#200)

* oproxy: move to HMAC nonces so they can used more than once

* oproxy: move migration to getter

* oproxy: implement JTI caching

* desktop: add AQD_NO_UPDATE

* oproxy: add standalone version

* oproxy: loadOAuthSession --> getOAuthSession

* oproxy: client metadata implementation

* oproxy: add echo, keys, other things

* oproxy: working standalone flow!

* extract oatproxy

* chore: gofmt

* oproxy: bump to latest version

authored by

Eli Mallon and committed by
GitHub
88572304 e265e900

+97 -2075
+16 -34
go.mod
··· 10 10 11 11 replace github.com/AxisCommunications/go-dpop => github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 12 12 13 - replace github.com/haileyok/atproto-oauth-golang => github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab 14 - 15 13 require ( 16 14 firebase.google.com/go/v4 v4.14.1 17 15 git.stream.place/streamplace/c2pa-go v0.7.0 18 16 github.com/99designs/gqlgen v0.17.64 19 - github.com/AxisCommunications/go-dpop v1.1.2 20 17 github.com/NYTimes/gziphandler v1.1.1 21 18 github.com/ThalesGroup/crypto11 v0.0.0-00010101000000-000000000000 22 19 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d 23 - github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb 20 + github.com/bluesky-social/indigo v0.0.0-20250520232546-236dd575c91e 24 21 github.com/decred/dcrd/dcrec/secp256k1 v1.0.4 25 22 github.com/dunglas/httpsfv v1.0.2 26 23 github.com/ethereum/go-ethereum v1.14.7 27 24 github.com/go-git/go-git/v5 v5.12.0 28 25 github.com/go-gst/go-glib v1.4.0 29 26 github.com/go-gst/go-gst v1.4.0 30 - github.com/golang-jwt/jwt/v5 v5.2.1 31 27 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 32 28 github.com/golang/glog v1.2.4 33 29 github.com/google/uuid v1.6.0 34 30 github.com/gorilla/websocket v1.5.3 35 - github.com/haileyok/atproto-oauth-golang v0.0.2 36 31 github.com/ipfs/go-cid v0.4.1 37 32 github.com/johncgriffin/overflow v0.0.0-20211019200055-46fa312c352c 38 33 github.com/julienschmidt/httprouter v1.3.0 39 34 github.com/labstack/echo/v4 v4.13.3 40 - github.com/lestrrat-go/jwx/v2 v2.0.12 35 + github.com/lestrrat-go/jwx/v2 v2.1.6 41 36 github.com/livepeer/lpms v0.0.0-20240812093642-b5181eb92cb2 42 - github.com/lmittmann/tint v1.0.4 37 + github.com/lmittmann/tint v1.1.0 43 38 github.com/mr-tron/base58 v1.2.0 44 - github.com/orandin/slog-gorm v1.3.2 45 - github.com/peterbourgon/ff/v3 v3.3.1 39 + github.com/multiformats/go-multihash v0.2.3 40 + github.com/orandin/slog-gorm v1.4.0 41 + github.com/peterbourgon/ff/v3 v3.4.0 46 42 github.com/pion/interceptor v0.1.37 47 43 github.com/pion/rtcp v1.2.14 48 44 github.com/pion/webrtc/v4 v4.0.5 ··· 51 47 github.com/rs/cors v1.7.0 52 48 github.com/samber/slog-http v1.4.0 53 49 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 50 + github.com/streamplace/atproto-oauth-golang v0.0.0-20250521042753-9cfa9e504155 51 + github.com/streamplace/oatproxy v0.0.0-20250522204300-ccdf0c639572 54 52 github.com/stretchr/testify v1.10.0 55 53 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 56 54 gitlab.com/gitlab-org/release-cli v0.18.0 ··· 76 74 github.com/jinzhu/inflection v1.0.0 // indirect 77 75 github.com/jinzhu/now v1.1.5 // indirect 78 76 github.com/mattn/go-isatty v0.0.20 79 - golang.org/x/sys v0.30.0 // indirect 80 - gorm.io/gorm v1.25.11 77 + golang.org/x/sys v0.31.0 // indirect 78 + gorm.io/gorm v1.26.1 81 79 ) 82 80 83 81 require ( ··· 98 96 cloud.google.com/go/storage v1.41.0 // indirect 99 97 dario.cat/mergo v1.0.0 // indirect 100 98 filippo.io/edwards25519 v1.1.0 // indirect 99 + github.com/AxisCommunications/go-dpop v1.1.2 // indirect 101 100 github.com/BurntSushi/toml v1.4.0 // indirect 102 - github.com/DataDog/zstd v1.4.5 // indirect 103 101 github.com/MicahParks/keyfunc v1.9.0 // indirect 104 102 github.com/Microsoft/go-winio v0.6.2 // indirect 105 103 github.com/ProtonMail/go-crypto v1.0.0 // indirect ··· 112 110 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 113 111 github.com/cespare/xxhash/v2 v2.3.0 // indirect 114 112 github.com/cloudflare/circl v1.3.7 // indirect 115 - github.com/cockroachdb/errors v1.11.3 // indirect 116 - github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect 117 - github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect 118 - github.com/cockroachdb/pebble v1.1.2 // indirect 119 - github.com/cockroachdb/redact v1.1.5 // indirect 120 - github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect 121 113 github.com/consensys/bavard v0.1.13 // indirect 122 114 github.com/consensys/gnark-crypto v0.12.1 // indirect 123 115 github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect ··· 125 117 github.com/cyphar/filepath-securejoin v0.2.4 // indirect 126 118 github.com/deckarep/golang-set/v2 v2.6.0 // indirect 127 119 github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 // indirect 128 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 120 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 129 121 github.com/emirpasic/gods v1.18.1 // indirect 130 122 github.com/ethereum/c-kzg-4844 v1.0.0 // indirect 131 123 github.com/felixge/httpsnoop v1.0.4 // indirect 132 124 github.com/fsnotify/fsnotify v1.6.0 // indirect 133 - github.com/getsentry/sentry-go v0.27.0 // indirect 134 125 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 135 126 github.com/go-git/go-billy/v5 v5.5.0 // indirect 136 127 github.com/go-logr/logr v1.4.2 // indirect 137 128 github.com/go-logr/stdr v1.2.2 // indirect 138 129 github.com/go-sql-driver/mysql v1.8.1 // indirect 139 - github.com/goccy/go-json v0.10.2 // indirect 130 + github.com/goccy/go-json v0.10.3 // indirect 140 131 github.com/gocql/gocql v1.7.0 // indirect 141 132 github.com/gogo/protobuf v1.3.2 // indirect 142 133 github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 134 + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 143 135 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 144 136 github.com/golang/protobuf v1.5.4 // indirect 145 137 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect ··· 154 146 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 155 147 github.com/hashicorp/go-version v1.4.0 // indirect 156 148 github.com/hashicorp/golang-lru v1.0.2 // indirect 157 - github.com/hashicorp/golang-lru/arc/v2 v2.0.6 // indirect 158 149 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 159 150 github.com/holiman/uint256 v1.3.0 // indirect 160 151 github.com/ipfs/bbloom v0.0.4 // indirect ··· 175 166 github.com/ipfs/go-metrics-interface v0.0.1 // indirect 176 167 github.com/ipfs/go-verifcid v0.0.3 // indirect 177 168 github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 // indirect 178 - github.com/ipld/go-car/v2 v2.13.1 // indirect 179 169 github.com/ipld/go-codec-dagpb v1.6.0 // indirect 180 170 github.com/ipld/go-ipld-prime v0.21.0 // indirect 181 171 github.com/jackc/pgpassfile v1.0.0 // indirect ··· 186 176 github.com/jbenet/goprocess v0.1.4 // indirect 187 177 github.com/jstemmer/go-junit-report v1.0.0 // indirect 188 178 github.com/kevinburke/ssh_config v1.2.0 // indirect 189 - github.com/klauspost/compress v1.17.3 // indirect 190 179 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 191 - github.com/kr/pretty v0.3.1 // indirect 192 - github.com/kr/text v0.2.0 // indirect 193 180 github.com/labstack/gommon v0.4.2 // indirect 194 - github.com/lestrrat-go/blackmagic v1.0.2 // indirect 181 + github.com/lestrrat-go/blackmagic v1.0.3 // indirect 195 182 github.com/lestrrat-go/httpcc v1.0.1 // indirect 196 - github.com/lestrrat-go/httprc v1.0.4 // indirect 183 + github.com/lestrrat-go/httprc v1.0.6 // indirect 197 184 github.com/lestrrat-go/iter v1.0.2 // indirect 198 185 github.com/lestrrat-go/option v1.0.1 // indirect 199 186 github.com/livepeer/m3u8 v0.11.1 // indirect ··· 208 195 github.com/multiformats/go-base32 v0.1.0 // indirect 209 196 github.com/multiformats/go-base36 v0.2.0 // indirect 210 197 github.com/multiformats/go-multibase v0.2.0 // indirect 211 - github.com/multiformats/go-multicodec v0.9.0 // indirect 212 - github.com/multiformats/go-multihash v0.2.3 // indirect 213 198 github.com/multiformats/go-varint v0.0.7 // indirect 214 199 github.com/opentracing/opentracing-go v1.2.0 // indirect 215 - github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect 216 200 github.com/pion/datachannel v1.5.9 // indirect 217 201 github.com/pion/dtls/v3 v3.0.4 // indirect 218 202 github.com/pion/ice/v4 v4.0.3 // indirect ··· 233 217 github.com/prometheus/client_model v0.5.0 // indirect 234 218 github.com/prometheus/common v0.45.0 // indirect 235 219 github.com/prometheus/procfs v0.12.0 // indirect 236 - github.com/rogpeppe/go-internal v1.13.1 // indirect 237 220 github.com/russross/blackfriday/v2 v2.1.0 // indirect 238 221 github.com/segmentio/asm v1.2.0 // indirect 239 222 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 248 231 github.com/valyala/bytebufferpool v1.0.0 // indirect 249 232 github.com/valyala/fasttemplate v1.2.2 // indirect 250 233 github.com/vektah/gqlparser/v2 v2.5.22 // indirect 251 - github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect 252 234 github.com/wlynxg/anet v0.0.5 // indirect 253 235 github.com/xanzy/ssh-agent v0.3.3 // indirect 254 236 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
+30 -62
go.sum
··· 72 72 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 73 73 github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= 74 74 github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 75 - github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 76 - github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 77 - github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb h1:qfkNGUq//RzFBRFNBVwRKcFwyXS+1jQ5VnLW9Jfh6Vc= 78 - github.com/bluesky-social/indigo v0.0.0-20250512184841-3edc6e261feb/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 75 + github.com/bluesky-social/indigo v0.0.0-20250520232546-236dd575c91e h1:DVD+HxQsDCVJtAkjfIKZVaBNc3kayHaU+A2TJZkFdp4= 76 + github.com/bluesky-social/indigo v0.0.0-20250520232546-236dd575c91e/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 79 77 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 80 78 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 81 79 github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= ··· 97 95 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 98 96 github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 99 97 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 100 - github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= 101 - github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= 102 98 github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= 103 99 github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= 104 100 github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= ··· 122 118 github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= 123 119 github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= 124 120 github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= 125 - github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 126 121 github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 127 122 github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 128 123 github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= ··· 136 131 github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= 137 132 github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= 138 133 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 139 - github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 140 - github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 134 + github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= 135 + github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 141 136 github.com/decred/dcrd/dcrec/secp256k1 v1.0.4 h1:0XErmfJBiVbl0NvyclGn4jr+1hIylDf5beFi9W0o7Fc= 142 137 github.com/decred/dcrd/dcrec/secp256k1 v1.0.4/go.mod h1:00z7mJdugt+GBAzPN1QrDRGCXxyKUiexEHu6ukxEw3k= 143 138 github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= 144 139 github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= 145 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 146 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 140 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 141 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 147 142 github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= 148 143 github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 149 144 github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= ··· 172 167 github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= 173 168 github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 174 169 github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 175 - github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 176 - github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 177 170 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 178 171 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 179 172 github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= ··· 201 194 github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 202 195 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 203 196 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 204 - github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 205 - github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 197 + github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 198 + github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 206 199 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 207 200 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 208 201 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= ··· 210 203 github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 211 204 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= 212 205 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 213 - github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 214 - github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 206 + github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 207 + github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 215 208 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 216 209 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 217 210 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= ··· 289 282 github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 290 283 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 291 284 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 292 - github.com/hashicorp/golang-lru/arc/v2 v2.0.6 h1:4NU7uP5vSoK6TbaMj3NtY478TTAWLso/vL1gpNrInHg= 293 - github.com/hashicorp/golang-lru/arc/v2 v2.0.6/go.mod h1:cfdDIX05DWvYV6/shsxDfa/OVcRieOt+q4FnM8x+Xno= 294 285 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 295 286 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 296 287 github.com/holiman/uint256 v1.3.0 h1:4wdcm/tnd0xXdu7iS3ruNvxkWwrb4aeBQv19ayYn8F4= ··· 299 290 github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 300 291 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 301 292 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 302 - github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= 303 - github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= 304 293 github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 305 294 github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 306 295 github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= ··· 321 310 github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 322 311 github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 323 312 github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 324 - github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8= 325 - github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8= 326 313 github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 327 314 github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 328 315 github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= ··· 356 343 github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 357 344 github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg= 358 345 github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU= 359 - github.com/ipfs/go-unixfsnode v1.8.0 h1:yCkakzuE365glu+YkgzZt6p38CSVEBPgngL9ZkfnyQU= 360 - github.com/ipfs/go-unixfsnode v1.8.0/go.mod h1:HxRu9HYHOjK6HUqFBAi++7DVoWAHn0o4v/nZ/VA+0g8= 361 346 github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 362 347 github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 363 348 github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI= ··· 368 353 github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 369 354 github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 370 355 github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 371 - github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo= 372 - github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw= 373 356 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 374 357 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 375 358 github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= ··· 424 407 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 425 408 github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= 426 409 github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= 427 - github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 428 - github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 429 - github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 410 + github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= 411 + github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= 430 412 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 431 413 github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 432 - github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 433 - github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 414 + github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= 415 + github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 434 416 github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 435 417 github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 436 - github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 437 - github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 438 - github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 418 + github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= 419 + github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= 439 420 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 440 421 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 441 422 github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= ··· 459 440 github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded/go.mod h1:xkDdm+akniYxVT9KW1Y2Y7Hso6aW+rZObz3nrA9yTHw= 460 441 github.com/livepeer/m3u8 v0.11.1 h1:VkUJzfNTyjy9mqsgp5JPvouwna8wGZMvd/gAfT5FinU= 461 442 github.com/livepeer/m3u8 v0.11.1/go.mod h1:IUqAtwWPAG2CblfQa4SVzTQoDcEMPyfNOaBSxqHMS04= 462 - github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= 463 - github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 443 + github.com/lmittmann/tint v1.1.0 h1:0hDmvuGv3U+Cep/jHpPxwjrCFjT6syam7iY7nTmA7ug= 444 + github.com/lmittmann/tint v1.1.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 464 445 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 465 446 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 466 447 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= ··· 517 498 github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 518 499 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 519 500 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 520 - github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g= 521 - github.com/orandin/slog-gorm v1.3.2/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= 501 + github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU= 502 + github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= 522 503 github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= 523 504 github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= 524 - github.com/peterbourgon/ff/v3 v3.3.1 h1:XSWvXxeNdgeppLNGGJEAOiXRdX2YMF/LuZfdnqQ1SNc= 525 - github.com/peterbourgon/ff/v3 v3.3.1/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= 526 - github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 527 - github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 505 + github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= 506 + github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= 528 507 github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= 529 508 github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= 530 509 github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= ··· 561 540 github.com/piprate/json-gold v0.5.0/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs= 562 541 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 563 542 github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 564 - github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 565 543 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 566 544 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 567 545 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= ··· 584 562 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 585 563 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 586 564 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 587 - github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 588 565 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 589 566 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 590 567 github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= ··· 618 595 github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= 619 596 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 620 597 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 621 - github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab h1:cXikoKjZxnUiRQ+IY4+3XU4eJL1g0JJ5TzRA2keUNBg= 622 - github.com/streamplace/atproto-oauth-golang v0.0.0-20250512021024-291d7209d3ab/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 598 + github.com/streamplace/atproto-oauth-golang v0.0.0-20250521042753-9cfa9e504155 h1:OAJ2Hh9XQcU77aUdxgT5tTcGKLYzwCXvPWR22V5xV5o= 599 + github.com/streamplace/atproto-oauth-golang v0.0.0-20250521042753-9cfa9e504155/go.mod h1:/AUT+i6CBJJ13AWx89XyvFdSkmDT3IoEg8pl34MaUgs= 623 600 github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc= 624 601 github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0= 602 + github.com/streamplace/oatproxy v0.0.0-20250522204300-ccdf0c639572 h1:O4F2nuV+dAsLSVOzclnqLsysVe/TlPDXjZkpmG8B3LU= 603 + github.com/streamplace/oatproxy v0.0.0-20250522204300-ccdf0c639572/go.mod h1:YlNOtaWvIoAZ5rxKENNFIsa5aWtAWdMY3Slm3lzWGbU= 625 604 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 626 605 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 627 606 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= ··· 666 645 github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= 667 646 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 668 647 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 669 - github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= 670 - github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= 671 648 github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= 672 649 github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 673 650 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= ··· 732 709 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 733 710 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 734 711 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 735 - golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 736 712 golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 737 713 golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 738 714 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= ··· 770 746 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 771 747 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 772 748 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 773 - golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 774 749 golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 775 750 golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 776 751 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= ··· 798 773 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 799 774 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 800 775 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 801 - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 802 776 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 803 777 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 804 778 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 807 781 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 808 782 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 809 783 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 810 - golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 811 - golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 812 - golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 813 - golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 784 + golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 785 + golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 814 786 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 815 787 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 816 788 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 817 789 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 818 790 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 819 - golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 820 - golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 821 791 golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 822 792 golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 823 793 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= ··· 827 797 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 828 798 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 829 799 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 830 - golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 831 - golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 832 800 golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 833 801 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 834 802 golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= ··· 920 888 gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= 921 889 gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= 922 890 gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 923 - gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= 924 - gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 891 + gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= 892 + gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 925 893 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 926 894 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 927 895 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+4 -5
pkg/api/api.go
··· 24 24 sloghttp "github.com/samber/slog-http" 25 25 "golang.org/x/time/rate" 26 26 27 + "github.com/streamplace/oatproxy/pkg/oatproxy" 27 28 "stream.place/streamplace/js/app" 28 29 "stream.place/streamplace/pkg/atproto" 29 30 "stream.place/streamplace/pkg/bus" ··· 37 38 "stream.place/streamplace/pkg/mist/mistconfig" 38 39 "stream.place/streamplace/pkg/model" 39 40 "stream.place/streamplace/pkg/notifications" 40 - "stream.place/streamplace/pkg/oproxy" 41 41 "stream.place/streamplace/pkg/spmetrics" 42 42 "stream.place/streamplace/pkg/spxrpc" 43 43 "stream.place/streamplace/pkg/streamplace" ··· 64 64 limitersMu sync.Mutex 65 65 SignerCache map[string]media.MediaSigner 66 66 SignerCacheMu sync.Mutex 67 - op *oproxy.OProxy 67 + op *oatproxy.OATProxy 68 68 } 69 69 70 70 type WebsocketTracker struct { ··· 73 73 mu sync.RWMutex 74 74 } 75 75 76 - func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, signer *eip712.EIP712Signer, noter notifications.FirebaseNotifier, mm *media.MediaManager, ms media.MediaSigner, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer, d *director.Director, op *oproxy.OProxy) (*StreamplaceAPI, error) { 76 + func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, signer *eip712.EIP712Signer, noter notifications.FirebaseNotifier, mm *media.MediaManager, ms media.MediaSigner, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer, d *director.Director, op *oatproxy.OATProxy) (*StreamplaceAPI, error) { 77 77 updater, err := PrepareUpdater(cli) 78 78 if err != nil { 79 79 return nil, err ··· 129 129 func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) { 130 130 131 131 var xrpc http.Handler 132 - xrpc, err := spxrpc.NewServer(ctx, a.CLI, a.Model) 132 + xrpc, err := spxrpc.NewServer(ctx, a.CLI, a.Model, a.op) 133 133 if err != nil { 134 134 return nil, err 135 135 } 136 - xrpc = a.op.OAuthMiddleware(xrpc) 137 136 router := httprouter.New() 138 137 139 138 router.Handler("GET", "/oauth/*anything", a.op.Handler())
+1 -1
pkg/atproto/jwks.go
··· 5 5 "encoding/json" 6 6 "os" 7 7 8 - oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers" 9 8 "github.com/lestrrat-go/jwx/v2/jwk" 9 + oauth_helpers "github.com/streamplace/atproto-oauth-golang/helpers" 10 10 "stream.place/streamplace/pkg/log" 11 11 ) 12 12
+13 -3
pkg/cmd/streamplace.go
··· 16 16 "syscall" 17 17 "time" 18 18 19 + "github.com/streamplace/oatproxy/pkg/oatproxy" 19 20 "golang.org/x/term" 20 21 "stream.place/streamplace/pkg/aqhttp" 21 22 "stream.place/streamplace/pkg/atproto" ··· 26 27 "stream.place/streamplace/pkg/log" 27 28 "stream.place/streamplace/pkg/media" 28 29 "stream.place/streamplace/pkg/notifications" 29 - "stream.place/streamplace/pkg/oproxy" 30 30 "stream.place/streamplace/pkg/replication" 31 31 "stream.place/streamplace/pkg/replication/boring" 32 32 "stream.place/streamplace/pkg/rtmps" ··· 336 336 return err 337 337 } 338 338 339 - op := oproxy.New(&oproxy.Config{ 339 + clientMetadata := &oatproxy.OAuthClientMetadata{ 340 + Scope: "atproto transition:generic", 341 + ClientName: "Streamplace", 342 + RedirectURIs: []string{ 343 + fmt.Sprintf("https://%s/login", cli.PublicHost), 344 + fmt.Sprintf("https://%s/api/app-return", cli.PublicHost), 345 + }, 346 + } 347 + 348 + op := oatproxy.New(&oatproxy.Config{ 340 349 Host: cli.PublicHost, 341 350 CreateOAuthSession: mod.CreateOAuthSession, 342 351 UpdateOAuthSession: mod.UpdateOAuthSession, 343 - LoadOAuthSession: mod.LoadOAuthSession, 352 + GetOAuthSession: mod.LoadOAuthSession, 344 353 Scope: "atproto transition:generic", 345 354 UpstreamJWK: cli.JWK, 346 355 DownstreamJWK: cli.AccessJWK, 356 + ClientMetadata: clientMetadata, 347 357 }) 348 358 d := director.NewDirector(mm, mod, &cli, b, op) 349 359 a, err := api.MakeStreamplaceAPI(&cli, mod, eip712signer, noter, mm, ms, b, atsync, d, op)
+3 -3
pkg/director/director.go
··· 5 5 "fmt" 6 6 "sync" 7 7 8 + "github.com/streamplace/oatproxy/pkg/oatproxy" 8 9 "golang.org/x/sync/errgroup" 9 10 "stream.place/streamplace/pkg/bus" 10 11 "stream.place/streamplace/pkg/config" 11 12 "stream.place/streamplace/pkg/log" 12 13 "stream.place/streamplace/pkg/media" 13 14 "stream.place/streamplace/pkg/model" 14 - "stream.place/streamplace/pkg/oproxy" 15 15 ) 16 16 17 17 // director is responsible for managing the lifecycle of a stream, making business ··· 27 27 bus *bus.Bus 28 28 streamSessions map[string]*StreamSession 29 29 streamSessionsMu sync.Mutex 30 - op *oproxy.OProxy 30 + op *oatproxy.OATProxy 31 31 } 32 32 33 - func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oproxy.OProxy) *Director { 33 + func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy) *Director { 34 34 return &Director{ 35 35 mm: mm, 36 36 mod: mod,
+2 -2
pkg/director/stream_session.go
··· 11 11 "github.com/bluesky-social/indigo/api/bsky" 12 12 "github.com/bluesky-social/indigo/lex/util" 13 13 "github.com/bluesky-social/indigo/xrpc" 14 + "github.com/streamplace/oatproxy/pkg/oatproxy" 14 15 "golang.org/x/sync/errgroup" 15 16 "stream.place/streamplace/pkg/aqtime" 16 17 "stream.place/streamplace/pkg/bus" ··· 20 21 "stream.place/streamplace/pkg/media" 21 22 "stream.place/streamplace/pkg/media/segchanman" 22 23 "stream.place/streamplace/pkg/model" 23 - "stream.place/streamplace/pkg/oproxy" 24 24 "stream.place/streamplace/pkg/renditions" 25 25 "stream.place/streamplace/pkg/spmetrics" 26 26 "stream.place/streamplace/pkg/streamplace" ··· 32 32 mod model.Model 33 33 cli *config.CLI 34 34 bus *bus.Bus 35 - op *oproxy.OProxy 35 + op *oatproxy.OATProxy 36 36 hls *media.M3U8 37 37 lp *livepeer.LivepeerSession 38 38 repoDID string
+7 -7
pkg/model/model.go
··· 11 11 "github.com/bluesky-social/indigo/api/bsky" 12 12 "github.com/lmittmann/tint" 13 13 slogGorm "github.com/orandin/slog-gorm" 14 + "github.com/streamplace/oatproxy/pkg/oatproxy" 14 15 "gorm.io/driver/sqlite" 15 16 "gorm.io/gorm" 16 17 "stream.place/streamplace/pkg/config" 17 18 "stream.place/streamplace/pkg/log" 18 - "stream.place/streamplace/pkg/oproxy" 19 19 "stream.place/streamplace/pkg/streamplace" 20 20 ) 21 21 ··· 85 85 CreateChatProfile(ctx context.Context, profile *ChatProfile) error 86 86 GetChatProfile(ctx context.Context, repoDID string) (*ChatProfile, error) 87 87 88 - CreateOAuthSession(id string, session *oproxy.OAuthSession) error 89 - LoadOAuthSession(id string) (*oproxy.OAuthSession, error) 90 - UpdateOAuthSession(id string, session *oproxy.OAuthSession) error 91 - ListOAuthSessions() ([]oproxy.OAuthSession, error) 92 - GetSessionByDID(did string) (*oproxy.OAuthSession, error) 88 + CreateOAuthSession(id string, session *oatproxy.OAuthSession) error 89 + LoadOAuthSession(id string) (*oatproxy.OAuthSession, error) 90 + UpdateOAuthSession(id string, session *oatproxy.OAuthSession) error 91 + ListOAuthSessions() ([]oatproxy.OAuthSession, error) 92 + GetSessionByDID(did string) (*oatproxy.OAuthSession, error) 93 93 } 94 94 95 95 func MakeDB(dbURL string) (Model, error) { ··· 146 146 Block{}, 147 147 ChatMessage{}, 148 148 ChatProfile{}, 149 - oproxy.OAuthSession{}, 149 + oatproxy.OAuthSession{}, 150 150 } { 151 151 err = db.AutoMigrate(model) 152 152 if err != nil {
+10 -10
pkg/model/oauth_session.go
··· 3 3 import ( 4 4 "errors" 5 5 6 + "github.com/streamplace/oatproxy/pkg/oatproxy" 6 7 "gorm.io/gorm" 7 - "stream.place/streamplace/pkg/oproxy" 8 8 ) 9 9 10 - func (m *DBModel) CreateOAuthSession(id string, session *oproxy.OAuthSession) error { 10 + func (m *DBModel) CreateOAuthSession(id string, session *oatproxy.OAuthSession) error { 11 11 return m.DB.Create(session).Error 12 12 } 13 13 14 - func (m *DBModel) LoadOAuthSession(id string) (*oproxy.OAuthSession, error) { 15 - var session oproxy.OAuthSession 14 + func (m *DBModel) LoadOAuthSession(id string) (*oatproxy.OAuthSession, error) { 15 + var session oatproxy.OAuthSession 16 16 if err := m.DB.Where("downstream_dpop_jkt = ?", id).First(&session).Error; err != nil { 17 17 if errors.Is(err, gorm.ErrRecordNotFound) { 18 18 return nil, nil ··· 22 22 return &session, nil 23 23 } 24 24 25 - func (m *DBModel) UpdateOAuthSession(id string, session *oproxy.OAuthSession) error { 26 - res := m.DB.Model(&oproxy.OAuthSession{}).Where("downstream_dpop_jkt = ?", id).Updates(session) 25 + func (m *DBModel) UpdateOAuthSession(id string, session *oatproxy.OAuthSession) error { 26 + res := m.DB.Model(&oatproxy.OAuthSession{}).Where("downstream_dpop_jkt = ?", id).Updates(session) 27 27 if res.Error != nil { 28 28 return res.Error 29 29 } ··· 33 33 return nil 34 34 } 35 35 36 - func (m *DBModel) ListOAuthSessions() ([]oproxy.OAuthSession, error) { 37 - var sessions []oproxy.OAuthSession 36 + func (m *DBModel) ListOAuthSessions() ([]oatproxy.OAuthSession, error) { 37 + var sessions []oatproxy.OAuthSession 38 38 if err := m.DB.Find(&sessions).Error; err != nil { 39 39 return nil, err 40 40 } 41 41 return sessions, nil 42 42 } 43 43 44 - func (m *DBModel) GetSessionByDID(did string) (*oproxy.OAuthSession, error) { 45 - var session oproxy.OAuthSession 44 + func (m *DBModel) GetSessionByDID(did string) (*oatproxy.OAuthSession, error) { 45 + var session oatproxy.OAuthSession 46 46 if err := m.DB.Where("repo_did = ? AND revoked_at IS NULL", did).Order("updated_at DESC").First(&session).Error; err != nil { 47 47 return nil, err 48 48 }
-197
pkg/oproxy/dpop_helpers.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "crypto/ecdsa" 5 - "crypto/ed25519" 6 - "crypto/elliptic" 7 - "crypto/rsa" 8 - "encoding/base64" 9 - "encoding/json" 10 - "math/big" 11 - "strings" 12 - 13 - "github.com/AxisCommunications/go-dpop" 14 - "github.com/golang-jwt/jwt/v5" 15 - ) 16 - 17 - // all of this code borrowed from https://github.com/AxisCommunications/go-dpop 18 - // MIT license 19 - func keyFunc(t *jwt.Token) (interface{}, error) { 20 - // Return the required jwkHeader header. See https://datatracker.ietf.org/doc/html/rfc9449#section-4.2 21 - // Used to validate the signature of the DPoP proof. 22 - jwkHeader := t.Header["jwk"] 23 - if jwkHeader == nil { 24 - return nil, dpop.ErrMissingJWK 25 - } 26 - 27 - jwkMap, ok := jwkHeader.(map[string]interface{}) 28 - if !ok { 29 - return nil, dpop.ErrMissingJWK 30 - } 31 - 32 - return parseJwk(jwkMap) 33 - } 34 - 35 - // Parses a JWK and inherently strips it of optional fields 36 - func parseJwk(jwkMap map[string]interface{}) (interface{}, error) { 37 - // Ensure that JWK kty is present and is a string. 38 - kty, ok := jwkMap["kty"].(string) 39 - if !ok { 40 - return nil, dpop.ErrInvalidProof 41 - } 42 - switch kty { 43 - case "EC": 44 - // Ensure that the required fields are present and are strings. 45 - x, ok := jwkMap["x"].(string) 46 - if !ok { 47 - return nil, dpop.ErrInvalidProof 48 - } 49 - y, ok := jwkMap["y"].(string) 50 - if !ok { 51 - return nil, dpop.ErrInvalidProof 52 - } 53 - crv, ok := jwkMap["crv"].(string) 54 - if !ok { 55 - return nil, dpop.ErrInvalidProof 56 - } 57 - 58 - // Decode the coordinates from Base64. 59 - // 60 - // According to RFC 7518, they are Base64 URL unsigned integers. 61 - // https://tools.ietf.org/html/rfc7518#section-6.3 62 - xCoordinate, err := base64urlTrailingPadding(x) 63 - if err != nil { 64 - return nil, err 65 - } 66 - yCoordinate, err := base64urlTrailingPadding(y) 67 - if err != nil { 68 - return nil, err 69 - } 70 - 71 - // Read the specified curve of the key. 72 - var curve elliptic.Curve 73 - switch crv { 74 - case "P-256": 75 - curve = elliptic.P256() 76 - case "P-384": 77 - curve = elliptic.P384() 78 - case "P-521": 79 - curve = elliptic.P521() 80 - default: 81 - return nil, dpop.ErrUnsupportedCurve 82 - } 83 - 84 - return &ecdsa.PublicKey{ 85 - X: big.NewInt(0).SetBytes(xCoordinate), 86 - Y: big.NewInt(0).SetBytes(yCoordinate), 87 - Curve: curve, 88 - }, nil 89 - case "RSA": 90 - // Ensure that the required fields are present and are strings. 91 - e, ok := jwkMap["e"].(string) 92 - if !ok { 93 - return nil, dpop.ErrInvalidProof 94 - } 95 - n, ok := jwkMap["n"].(string) 96 - if !ok { 97 - return nil, dpop.ErrInvalidProof 98 - } 99 - 100 - // Decode the exponent and modulus from Base64. 101 - // 102 - // According to RFC 7518, they are Base64 URL unsigned integers. 103 - // https://tools.ietf.org/html/rfc7518#section-6.3 104 - exponent, err := base64urlTrailingPadding(e) 105 - if err != nil { 106 - return nil, err 107 - } 108 - modulus, err := base64urlTrailingPadding(n) 109 - if err != nil { 110 - return nil, err 111 - } 112 - return &rsa.PublicKey{ 113 - N: big.NewInt(0).SetBytes(modulus), 114 - E: int(big.NewInt(0).SetBytes(exponent).Uint64()), 115 - }, nil 116 - case "OKP": 117 - // Ensure that the required fields are present and are strings. 118 - x, ok := jwkMap["x"].(string) 119 - if !ok { 120 - return nil, dpop.ErrInvalidProof 121 - } 122 - 123 - publicKey, err := base64urlTrailingPadding(x) 124 - if err != nil { 125 - return nil, err 126 - } 127 - 128 - return ed25519.PublicKey(publicKey), nil 129 - case "OCT": 130 - return nil, dpop.ErrUnsupportedKeyAlgorithm 131 - default: 132 - return nil, dpop.ErrUnsupportedKeyAlgorithm 133 - } 134 - } 135 - 136 - // Borrowed from MicahParks/keyfunc See: https://github.com/MicahParks/keyfunc/blob/master/keyfunc.go#L56 137 - // 138 - // base64urlTrailingPadding removes trailing padding before decoding a string from base64url. Some non-RFC compliant 139 - // JWKS contain padding at the end values for base64url encoded public keys. 140 - // 141 - // Trailing padding is required to be removed from base64url encoded keys. 142 - // RFC 7517 Section 1.1 defines base64url the same as RFC 7515 Section 2: 143 - // https://datatracker.ietf.org/doc/html/rfc7517#section-1.1 144 - // https://datatracker.ietf.org/doc/html/rfc7515#section-2 145 - func base64urlTrailingPadding(s string) ([]byte, error) { 146 - s = strings.TrimRight(s, "=") 147 - return base64.RawURLEncoding.DecodeString(s) 148 - } 149 - 150 - // Strips eventual optional members of a JWK in order to be able to compute the thumbprint of it 151 - // https://datatracker.ietf.org/doc/html/rfc7638#section-3.2 152 - func getThumbprintableJwkJSONbytes(jwk map[string]interface{}) ([]byte, error) { 153 - minimalJwk, err := parseJwk(jwk) 154 - if err != nil { 155 - return nil, err 156 - } 157 - jwkHeaderJSONBytes, err := getKeyStringRepresentation(minimalJwk) 158 - if err != nil { 159 - return nil, err 160 - } 161 - return jwkHeaderJSONBytes, nil 162 - } 163 - 164 - // Returns the string representation of a key in JSON format. 165 - func getKeyStringRepresentation(key interface{}) ([]byte, error) { 166 - var keyParts interface{} 167 - switch key := key.(type) { 168 - case *ecdsa.PublicKey: 169 - // Calculate the size of the byte array representation of an elliptic curve coordinate 170 - // and ensure that the byte array representation of the key is padded correctly. 171 - bits := key.Curve.Params().BitSize 172 - keyCurveBytesSize := bits/8 + bits%8 173 - 174 - keyParts = map[string]interface{}{ 175 - "kty": "EC", 176 - "crv": key.Curve.Params().Name, 177 - "x": base64.RawURLEncoding.EncodeToString(key.X.FillBytes(make([]byte, keyCurveBytesSize))), 178 - "y": base64.RawURLEncoding.EncodeToString(key.Y.FillBytes(make([]byte, keyCurveBytesSize))), 179 - } 180 - case *rsa.PublicKey: 181 - keyParts = map[string]interface{}{ 182 - "kty": "RSA", 183 - "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.E)).Bytes()), 184 - "n": base64.RawURLEncoding.EncodeToString(key.N.Bytes()), 185 - } 186 - case ed25519.PublicKey: 187 - keyParts = map[string]interface{}{ 188 - "kty": "OKP", 189 - "crv": "Ed25519", 190 - "x": base64.RawURLEncoding.EncodeToString(key), 191 - } 192 - default: 193 - return nil, dpop.ErrUnsupportedKeyAlgorithm 194 - } 195 - 196 - return json.Marshal(keyParts) 197 - }
-99
pkg/oproxy/helpers.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "crypto/sha256" 5 - "encoding/base64" 6 - "errors" 7 - "fmt" 8 - "strings" 9 - 10 - "github.com/AxisCommunications/go-dpop" 11 - "github.com/golang-jwt/jwt/v5" 12 - "github.com/google/uuid" 13 - ) 14 - 15 - func boolPtr(b bool) *bool { 16 - return &b 17 - } 18 - 19 - func codeUUID(prefix string) string { 20 - uu, err := uuid.NewV7() 21 - if err != nil { 22 - panic(err) 23 - } 24 - return fmt.Sprintf("%s-%s", prefix, uu.String()) 25 - } 26 - 27 - var urnPrefix = "urn:ietf:params:oauth:request_uri:" 28 - 29 - const UUID_LENGTH = 37 30 - 31 - func makeURN(jkt string) string { 32 - uu, err := uuid.NewV7() 33 - if err != nil { 34 - panic(err) 35 - } 36 - return fmt.Sprintf("%s%s-%s", urnPrefix, uu.String(), jkt) 37 - } 38 - 39 - // urn --> jkt, uu 40 - func parseURN(urn string) (string, string, error) { 41 - if !strings.HasPrefix(urn, urnPrefix) { 42 - return "", "", fmt.Errorf("invalid URN: %s", urn) 43 - } 44 - withoutPrefix := urn[len(urnPrefix):] 45 - uu := withoutPrefix[:UUID_LENGTH] 46 - suffix := withoutPrefix[UUID_LENGTH:] 47 - return suffix, uu, nil 48 - } 49 - 50 - func makeState(jkt string) string { 51 - uu, err := uuid.NewV7() 52 - if err != nil { 53 - panic(err) 54 - } 55 - return fmt.Sprintf("%s-%s", uu.String(), jkt) 56 - } 57 - 58 - func parseState(state string) (string, string, error) { 59 - if len(state) < UUID_LENGTH { 60 - return "", "", fmt.Errorf("invalid state: %s", state) 61 - } 62 - uu := state[:UUID_LENGTH] 63 - suffix := state[UUID_LENGTH:] 64 - return suffix, uu, nil 65 - } 66 - 67 - func makeNonce() string { 68 - uu, err := uuid.NewV7() 69 - if err != nil { 70 - panic(err) 71 - } 72 - return fmt.Sprintf("nonce-%s", uu.String()) 73 - } 74 - 75 - // returns jkt, nonce, error 76 - func getJKT(dpopJWT string) (string, string, error) { 77 - var claims dpop.ProofTokenClaims 78 - token, err := jwt.ParseWithClaims(dpopJWT, &claims, keyFunc) 79 - if err != nil { 80 - return "", "", err 81 - } 82 - jwk, ok := token.Header["jwk"].(map[string]any) 83 - if !ok { 84 - return "", "", fmt.Errorf("missing jwk in DPoP JWT header") 85 - } 86 - jwkJSONbytes, err := getThumbprintableJwkJSONbytes(jwk) 87 - if err != nil { 88 - // keyFunc used with parseWithClaims should ensure that this can not happen but better safe than sorry. 89 - return "", "", errors.Join(dpop.ErrInvalidProof, err) 90 - } 91 - h := sha256.New() 92 - _, err = h.Write(jwkJSONbytes) 93 - if err != nil { 94 - return "", "", errors.Join(dpop.ErrInvalidProof, err) 95 - } 96 - b64URLjwkHash := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 97 - 98 - return b64URLjwkHash, claims.Nonce, nil 99 - }
-155
pkg/oproxy/oauth_0_metadata.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "net/http" 7 - 8 - "github.com/haileyok/atproto-oauth-golang/helpers" 9 - "github.com/labstack/echo/v4" 10 - ) 11 - 12 - func (o *OProxy) HandleOAuthAuthorizationServer(c echo.Context) error { 13 - c.Response().Header().Set("Access-Control-Allow-Origin", "*") 14 - c.Response().Header().Set("Content-Type", "application/json") 15 - c.Response().WriteHeader(200) 16 - json.NewEncoder(c.Response().Writer).Encode(generateOAuthServerMetadata(o.host)) 17 - return nil 18 - } 19 - 20 - func (o *OProxy) HandleOAuthProtectedResource(c echo.Context) error { 21 - return c.JSON(200, map[string]interface{}{ 22 - "resource": fmt.Sprintf("https://%s", o.host), 23 - "authorization_servers": []string{ 24 - fmt.Sprintf("https://%s", o.host), 25 - }, 26 - "scopes_supported": []string{}, 27 - "bearer_methods_supported": []string{ 28 - "header", 29 - }, 30 - "resource_documentation": "https://atproto.com", 31 - }) 32 - } 33 - 34 - func (o *OProxy) HandleClientMetadataUpstream(c echo.Context) error { 35 - meta := o.GetUpstreamMetadata() 36 - return c.JSON(200, meta) 37 - } 38 - 39 - func (o *OProxy) HandleJwksUpstream(c echo.Context) error { 40 - pubKey, err := o.upstreamJWK.PublicKey() 41 - if err != nil { 42 - return echo.NewHTTPError(http.StatusInternalServerError, "could not get public key") 43 - } 44 - return c.JSON(200, helpers.CreateJwksResponseObject(pubKey)) 45 - } 46 - 47 - func (o *OProxy) HandleClientMetadataDownstream(c echo.Context) error { 48 - redirectURI := c.QueryParam("redirect_uri") 49 - meta, err := o.GetDownstreamMetadata(redirectURI) 50 - if err != nil { 51 - return err 52 - } 53 - return c.JSON(200, meta) 54 - } 55 - 56 - func (o *OProxy) GetUpstreamMetadata() *OAuthClientMetadata { 57 - // publicKey, err := o.upstreamJWK.PublicKey() 58 - // if err != nil { 59 - // panic(err) 60 - // } 61 - // jwks := jwk.NewSet() 62 - // err = jwks.AddKey(publicKey) 63 - // if err != nil { 64 - // panic(err) 65 - // } 66 - // ro := helpers.CreateJwksResponseObject(publicKey) 67 - meta := &OAuthClientMetadata{ 68 - ClientID: fmt.Sprintf("https://%s/oauth/upstream/client-metadata.json", o.host), 69 - JwksURI: fmt.Sprintf("https://%s/oauth/upstream/jwks.json", o.host), 70 - ClientURI: fmt.Sprintf("https://%s", o.host), 71 - // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)}, 72 - Scope: "atproto transition:generic", 73 - TokenEndpointAuthMethod: "private_key_jwt", 74 - ClientName: "Streamplace", 75 - ResponseTypes: []string{"code"}, 76 - GrantTypes: []string{"authorization_code", "refresh_token"}, 77 - DPoPBoundAccessTokens: boolPtr(true), 78 - TokenEndpointAuthSigningAlg: "ES256", 79 - RedirectURIs: []string{fmt.Sprintf("https://%s/oauth/return", o.host)}, 80 - // Jwks: ro, 81 - } 82 - return meta 83 - } 84 - 85 - func generateOAuthServerMetadata(host string) map[string]any { 86 - oauthServerMetadata := map[string]any{ 87 - "issuer": fmt.Sprintf("https://%s", host), 88 - "request_parameter_supported": true, 89 - "request_uri_parameter_supported": true, 90 - "require_request_uri_registration": true, 91 - "scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"}, 92 - "subject_types_supported": []string{"public"}, 93 - "response_types_supported": []string{"code"}, 94 - "response_modes_supported": []string{"query", "fragment", "form_post"}, 95 - "grant_types_supported": []string{"authorization_code", "refresh_token"}, 96 - "code_challenge_methods_supported": []string{"S256"}, 97 - "ui_locales_supported": []string{"en-US"}, 98 - "display_values_supported": []string{"page", "popup", "touch"}, 99 - "authorization_response_iss_parameter_supported": true, 100 - "request_object_encryption_alg_values_supported": []string{}, 101 - "request_object_encryption_enc_values_supported": []string{}, 102 - "jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host), 103 - "authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host), 104 - "token_endpoint": fmt.Sprintf("https://%s/oauth/token", host), 105 - "token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"}, 106 - "revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host), 107 - "introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host), 108 - "pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host), 109 - "require_pushed_authorization_requests": true, 110 - "client_id_metadata_document_supported": true, 111 - "request_object_signing_alg_values_supported": []string{ 112 - "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", 113 - "ES256", "ES256K", "ES384", "ES512", "none", 114 - }, 115 - "token_endpoint_auth_signing_alg_values_supported": []string{ 116 - "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", 117 - "ES256", "ES256K", "ES384", "ES512", 118 - }, 119 - "dpop_signing_alg_values_supported": []string{ 120 - "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", 121 - "ES256", "ES256K", "ES384", "ES512", 122 - }, 123 - } 124 - return oauthServerMetadata 125 - } 126 - 127 - func (o *OProxy) GetDownstreamMetadata(redirectURI string) (*OAuthClientMetadata, error) { 128 - meta := &OAuthClientMetadata{ 129 - ClientID: fmt.Sprintf("https://%s/oauth/downstream/client-metadata.json", o.host), 130 - ClientURI: fmt.Sprintf("https://%s", o.host), 131 - // RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)}, 132 - Scope: "atproto transition:generic", 133 - TokenEndpointAuthMethod: "none", 134 - ClientName: "Streamplace", 135 - ResponseTypes: []string{"code"}, 136 - GrantTypes: []string{"authorization_code", "refresh_token"}, 137 - DPoPBoundAccessTokens: boolPtr(true), 138 - RedirectURIs: []string{fmt.Sprintf("https://%s/login", o.host), fmt.Sprintf("https://%s/api/app-return", o.host)}, 139 - ApplicationType: "web", 140 - } 141 - if redirectURI != "" { 142 - found := false 143 - for _, uri := range meta.RedirectURIs { 144 - if uri == redirectURI { 145 - found = true 146 - break 147 - } 148 - } 149 - if !found { 150 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", redirectURI)) 151 - } 152 - meta.RedirectURIs = []string{redirectURI} 153 - } 154 - return meta, nil 155 - }
-178
pkg/oproxy/oauth_1_par.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "errors" 7 - "fmt" 8 - "net/http" 9 - "net/url" 10 - "slices" 11 - 12 - "github.com/AxisCommunications/go-dpop" 13 - "github.com/labstack/echo/v4" 14 - "go.opentelemetry.io/otel" 15 - ) 16 - 17 - type PAR struct { 18 - ClientID string `json:"client_id"` 19 - RedirectURI string `json:"redirect_uri"` 20 - CodeChallenge string `json:"code_challenge"` 21 - CodeChallengeMethod string `json:"code_challenge_method"` 22 - State string `json:"state"` 23 - LoginHint string `json:"login_hint"` 24 - ResponseMode string `json:"response_mode"` 25 - ResponseType string `json:"response_type"` 26 - Scope string `json:"scope"` 27 - } 28 - 29 - type PARResponse struct { 30 - RequestURI string `json:"request_uri"` 31 - ExpiresIn int `json:"expires_in"` 32 - } 33 - 34 - var ErrFirstNonce = echo.NewHTTPError(http.StatusBadRequest, "first time seeing this key, come back with a nonce") 35 - 36 - func (o *OProxy) HandleOAuthPAR(c echo.Context) error { 37 - ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthPAR") 38 - defer span.End() 39 - c.Response().Header().Set("Access-Control-Allow-Origin", "*") 40 - var par PAR 41 - if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil { 42 - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 43 - } 44 - 45 - dpopHeader := c.Request().Header.Get("DPoP") 46 - if dpopHeader == "" { 47 - return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 48 - } 49 - 50 - resp, err := o.NewPAR(ctx, c, &par, dpopHeader) 51 - if errors.Is(err, ErrFirstNonce) { 52 - res := map[string]interface{}{ 53 - "error": "use_dpop_nonce", 54 - "error_description": "Authorization server requires nonce in DPoP proof", 55 - } 56 - return c.JSON(http.StatusBadRequest, res) 57 - } else if err != nil { 58 - return err 59 - } 60 - return c.JSON(http.StatusCreated, resp) 61 - } 62 - 63 - func (o *OProxy) NewPAR(ctx context.Context, c echo.Context, par *PAR, dpopHeader string) (*PARResponse, error) { 64 - jkt, nonce, err := getJKT(dpopHeader) 65 - if err != nil { 66 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get JKT from DPoP header header=%s: %s", dpopHeader, err)) 67 - } 68 - session, err := o.loadOAuthSession(jkt) 69 - if err != nil { 70 - return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to load OAuth session: %s", err)) 71 - } 72 - // special case - if this is the first request, we need to send it back for a new nonce 73 - if session == nil { 74 - _, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{ 75 - Nonce: nonce, // normally this would be bad! but on the first request we're revalidating nonce anyway 76 - TimeWindow: &dpopTimeWindow, 77 - }) 78 - if err != nil { 79 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse DPoP header: %s", err)) 80 - } 81 - newNonce := makeNonce() 82 - err = o.createOAuthSession(jkt, &OAuthSession{ 83 - DownstreamDPoPJKT: jkt, 84 - DownstreamDPoPNonce: newNonce, 85 - }) 86 - if err != nil { 87 - return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth session: %s", err)) 88 - } 89 - // come back later, nerd 90 - c.Response().Header().Set("DPoP-Nonce", newNonce) 91 - return nil, ErrFirstNonce 92 - } 93 - if session.DownstreamDPoPNonce != nonce { 94 - return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid nonce") 95 - } 96 - proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{ 97 - Nonce: session.DownstreamDPoPNonce, 98 - TimeWindow: &dpopTimeWindow, 99 - }) 100 - // Check the error type to determine response 101 - if err != nil { 102 - // if ok := errors.Is(err, dpop.ErrInvalidProof); ok { 103 - // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil) 104 - // return 105 - // } 106 - // apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err) 107 - // return 108 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid DPoP proof: %s", err)) 109 - } 110 - if proof.PublicKey() != jkt { 111 - panic("invalid code path: parsed DPoP proof twice and got different keys?!") 112 - } 113 - 114 - clientMetadata, err := o.GetDownstreamMetadata(par.RedirectURI) 115 - if err != nil { 116 - return nil, err 117 - } 118 - if par.ClientID != clientMetadata.ClientID { 119 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid client_id: expected %s, got %s", clientMetadata.ClientID, par.ClientID)) 120 - } 121 - 122 - if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) { 123 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", par.RedirectURI)) 124 - } 125 - 126 - if par.CodeChallengeMethod != "S256" { 127 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid code challenge method: expected S256, got %s", par.CodeChallengeMethod)) 128 - } 129 - 130 - if par.ResponseMode != "query" { 131 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response mode: expected query, got %s", par.ResponseMode)) 132 - } 133 - 134 - if par.ResponseType != "code" { 135 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response type: expected code, got %s", par.ResponseType)) 136 - } 137 - 138 - if par.Scope != o.scope { 139 - return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope") 140 - } 141 - 142 - if par.LoginHint == "" { 143 - return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS") 144 - } 145 - 146 - if par.State == "" { 147 - return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required") 148 - } 149 - 150 - if par.Scope != o.scope { 151 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope)) 152 - } 153 - 154 - urn := makeURN(jkt) 155 - 156 - newNonce := makeNonce() 157 - 158 - err = o.updateOAuthSession(jkt, &OAuthSession{ 159 - DownstreamDPoPJKT: jkt, 160 - DownstreamDPoPNonce: newNonce, 161 - DownstreamPARRequestURI: urn, 162 - DownstreamCodeChallenge: par.CodeChallenge, 163 - DownstreamState: par.State, 164 - DownstreamRedirectURI: par.RedirectURI, 165 - Handle: par.LoginHint, 166 - }) 167 - if err != nil { 168 - return nil, fmt.Errorf("could not create oauth session: %w", err) 169 - } 170 - c.Response().Header().Set("DPoP-Nonce", newNonce) 171 - 172 - resp := &PARResponse{ 173 - RequestURI: urn, 174 - ExpiresIn: int(dpopTimeWindow.Seconds()), 175 - } 176 - 177 - return resp, nil 178 - }
-165
pkg/oproxy/oauth_2_authorize.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "net/http" 8 - "net/url" 9 - "time" 10 - 11 - oauth "github.com/haileyok/atproto-oauth-golang" 12 - "github.com/haileyok/atproto-oauth-golang/helpers" 13 - "github.com/labstack/echo/v4" 14 - "go.opentelemetry.io/otel" 15 - ) 16 - 17 - func (o *OProxy) HandleOAuthAuthorize(c echo.Context) error { 18 - ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthAuthorize") 19 - defer span.End() 20 - c.Response().Header().Set("Access-Control-Allow-Origin", "*") 21 - requestURI := c.QueryParam("request_uri") 22 - if requestURI == "" { 23 - return echo.NewHTTPError(http.StatusBadRequest, "request_uri is required") 24 - } 25 - clientID := c.QueryParam("client_id") 26 - if clientID == "" { 27 - return echo.NewHTTPError(http.StatusBadRequest, "client_id is required") 28 - } 29 - redirectURL, redirectErr := o.Authorize(ctx, requestURI, clientID) 30 - if redirectErr != nil { 31 - // we're a redirect; if we fail we need to send the user back 32 - jkt, _, err := parseURN(requestURI) 33 - if err != nil { 34 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err)) 35 - } 36 - 37 - session, err := o.loadOAuthSession(jkt) 38 - if err != nil { 39 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err)) 40 - } 41 - 42 - u, err := url.Parse(session.DownstreamRedirectURI) 43 - if err != nil { 44 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err)) 45 - } 46 - q := u.Query() 47 - q.Set("error", "authorize_failed") 48 - q.Set("error_description", redirectErr.Error()) 49 - u.RawQuery = q.Encode() 50 - return c.Redirect(http.StatusTemporaryRedirect, u.String()) 51 - } 52 - return c.Redirect(http.StatusTemporaryRedirect, redirectURL) 53 - } 54 - 55 - // downstream --> upstream transition; attempt to send user to the upstream auth server 56 - func (o *OProxy) Authorize(ctx context.Context, requestURI, clientID string) (string, *echo.HTTPError) { 57 - downstreamMeta, err := o.GetDownstreamMetadata("") 58 - if err != nil { 59 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get downstream metadata: %s", err)) 60 - } 61 - if downstreamMeta.ClientID != clientID { 62 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("client ID mismatch: %s != %s", downstreamMeta.ClientID, clientID)) 63 - } 64 - 65 - jkt, _, err := parseURN(requestURI) 66 - if err != nil { 67 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err)) 68 - } 69 - 70 - session, err := o.loadOAuthSession(jkt) 71 - if err != nil { 72 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err)) 73 - } 74 - 75 - if session == nil { 76 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no session found for jkt=%s", jkt)) 77 - } 78 - 79 - if session.Status() != OAuthSessionStatePARCreated { 80 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in par-created state: %s", session.Status())) 81 - } 82 - 83 - if session.DownstreamPARRequestURI != requestURI { 84 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("request URI mismatch: %s != %s", session.DownstreamPARRequestURI, requestURI)) 85 - } 86 - 87 - now := time.Now() 88 - session.DownstreamPARUsedAt = &now 89 - err = o.updateOAuthSession(jkt, session) 90 - if err != nil { 91 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err)) 92 - } 93 - 94 - upstreamMeta := o.GetUpstreamMetadata() 95 - oclient, err := oauth.NewClient(oauth.ClientArgs{ 96 - ClientJwk: o.upstreamJWK, 97 - ClientId: upstreamMeta.ClientID, 98 - RedirectUri: upstreamMeta.RedirectURIs[0], 99 - }) 100 - if err != nil { 101 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth client: %s", err)) 102 - } 103 - 104 - did, err := ResolveHandle(ctx, session.Handle) 105 - if err != nil { 106 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve handle '%s': %s", session.DID, err)) 107 - } 108 - 109 - service, err := ResolveService(ctx, did) 110 - if err != nil { 111 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve service for DID '%s': %s", did, err)) 112 - } 113 - 114 - authserver, err := oclient.ResolvePdsAuthServer(ctx, service) 115 - if err != nil { 116 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve PDS auth server for service '%s': %s", service, err)) 117 - } 118 - 119 - authmeta, err := oclient.FetchAuthServerMetadata(ctx, authserver) 120 - if err != nil { 121 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to fetch auth server metadata from '%s': %s", authserver, err)) 122 - } 123 - 124 - k, err := helpers.GenerateKey(nil) 125 - if err != nil { 126 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate DPoP key: %s", err)) 127 - } 128 - 129 - state := makeState(jkt) 130 - 131 - opts := oauth.ParAuthRequestOpts{ 132 - State: state, 133 - } 134 - parResp, err := oclient.SendParAuthRequest(ctx, authserver, authmeta, session.Handle, upstreamMeta.Scope, k, opts) 135 - if err != nil { 136 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to send PAR auth request to '%s': %s", authserver, err)) 137 - } 138 - 139 - jwkJSON, err := json.Marshal(k) 140 - if err != nil { 141 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to marshal DPoP key to JSON: %s", err)) 142 - } 143 - 144 - u, err := url.Parse(authmeta.AuthorizationEndpoint) 145 - if err != nil { 146 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse auth server metadata: %s", err)) 147 - } 148 - u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(upstreamMeta.ClientID), parResp.RequestUri) 149 - str := u.String() 150 - 151 - session.DID = did 152 - session.PDSUrl = service 153 - session.UpstreamState = parResp.State 154 - session.UpstreamAuthServerIssuer = authserver 155 - session.UpstreamPKCEVerifier = parResp.PkceVerifier 156 - session.UpstreamDPoPNonce = parResp.DpopAuthserverNonce 157 - session.UpstreamDPoPPrivateJWK = string(jwkJSON) 158 - 159 - err = o.updateOAuthSession(jkt, session) 160 - if err != nil { 161 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err)) 162 - } 163 - 164 - return str, nil 165 - }
-156
pkg/oproxy/oauth_3_return.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - "net/url" 8 - "time" 9 - 10 - "github.com/bluesky-social/indigo/api/atproto" 11 - "github.com/bluesky-social/indigo/xrpc" 12 - oauth "github.com/haileyok/atproto-oauth-golang" 13 - "github.com/labstack/echo/v4" 14 - "github.com/lestrrat-go/jwx/v2/jwk" 15 - "go.opentelemetry.io/otel" 16 - ) 17 - 18 - func (o *OProxy) HandleOAuthReturn(c echo.Context) error { 19 - ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthReturn") 20 - defer span.End() 21 - code := c.QueryParam("code") 22 - iss := c.QueryParam("iss") 23 - state := c.QueryParam("state") 24 - errorCode := c.QueryParam("error") 25 - errorDescription := c.QueryParam("error_description") 26 - var httpError *echo.HTTPError 27 - var redirectURL string 28 - if errorCode != "" { 29 - httpError = echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("%s (%s)", errorDescription, errorCode)) 30 - } else { 31 - redirectURL, httpError = o.Return(ctx, code, iss, state) 32 - } 33 - if httpError != nil { 34 - // we're a redirect; if we fail we need to send the user back 35 - jkt, _, err := parseState(state) 36 - if err != nil { 37 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err)) 38 - } 39 - 40 - session, err := o.loadOAuthSession(jkt) 41 - if err != nil { 42 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err)) 43 - } 44 - 45 - u, err := url.Parse(session.DownstreamRedirectURI) 46 - if err != nil { 47 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err)) 48 - } 49 - q := u.Query() 50 - q.Set("error", "return_failed") 51 - q.Set("error_description", httpError.Error()) 52 - u.RawQuery = q.Encode() 53 - return c.Redirect(http.StatusTemporaryRedirect, u.String()) 54 - } 55 - return c.Redirect(http.StatusTemporaryRedirect, redirectURL) 56 - } 57 - 58 - func (o *OProxy) Return(ctx context.Context, code string, iss string, state string) (string, *echo.HTTPError) { 59 - upstreamMeta := o.GetUpstreamMetadata() 60 - oclient, err := oauth.NewClient(oauth.ClientArgs{ 61 - ClientJwk: o.upstreamJWK, 62 - ClientId: upstreamMeta.ClientID, 63 - RedirectUri: upstreamMeta.RedirectURIs[0], 64 - }) 65 - 66 - jkt, _, err := parseState(state) 67 - if err != nil { 68 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse state: %s", err)) 69 - } 70 - 71 - session, err := o.loadOAuthSession(jkt) 72 - if err != nil { 73 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get OAuth session: %s", err)) 74 - } 75 - if session == nil { 76 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no OAuth session found for state: %s", state)) 77 - } 78 - 79 - if session.Status() != OAuthSessionStateUpstream { 80 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in upstream state: %s", session.Status())) 81 - } 82 - 83 - if session.UpstreamState != state { 84 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("state mismatch: %s != %s", session.UpstreamState, state)) 85 - } 86 - 87 - if iss != session.UpstreamAuthServerIssuer { 88 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("issuer mismatch: %s != %s", iss, session.UpstreamAuthServerIssuer)) 89 - } 90 - 91 - key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 92 - if err != nil { 93 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to parse DPoP private JWK: %s", err)) 94 - } 95 - 96 - itResp, err := oclient.InitialTokenRequest(ctx, code, iss, session.UpstreamPKCEVerifier, session.UpstreamDPoPNonce, key) 97 - if err != nil { 98 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to request initial token: %s", err)) 99 - } 100 - now := time.Now() 101 - 102 - if itResp.Sub != session.DID { 103 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("sub mismatch: %s != %s", itResp.Sub, session.DID)) 104 - } 105 - 106 - if itResp.Scope != upstreamMeta.Scope { 107 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("scope mismatch: %s != %s", itResp.Scope, upstreamMeta.Scope)) 108 - } 109 - 110 - downstreamCode, err := generateAuthorizationCode() 111 - if err != nil { 112 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate downstream code: %s", err)) 113 - } 114 - 115 - expiry := now.Add(time.Second * time.Duration(itResp.ExpiresIn)).UTC() 116 - session.UpstreamAccessToken = itResp.AccessToken 117 - session.UpstreamAccessTokenExp = &expiry 118 - session.UpstreamRefreshToken = itResp.RefreshToken 119 - session.DownstreamAuthorizationCode = downstreamCode 120 - 121 - authArgs := &oauth.XrpcAuthedRequestArgs{ 122 - Did: session.DID, 123 - AccessToken: session.UpstreamAccessToken, 124 - PdsUrl: session.PDSUrl, 125 - Issuer: session.UpstreamAuthServerIssuer, 126 - DpopPdsNonce: session.UpstreamDPoPNonce, 127 - DpopPrivateJwk: key, 128 - } 129 - 130 - xrpcClient := &oauth.XrpcClient{ 131 - OnDpopPdsNonceChanged: func(did, newNonce string) {}, 132 - } 133 - 134 - // brief check to make sure we can actually do stuff 135 - var out atproto.ServerCheckAccountStatus_Output 136 - if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.server.checkAccountStatus", nil, nil, &out); err != nil { 137 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to check account status: %s", err)) 138 - } 139 - 140 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 141 - if err != nil { 142 - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err)) 143 - } 144 - 145 - u, err := url.Parse(session.DownstreamRedirectURI) 146 - if err != nil { 147 - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err)) 148 - } 149 - q := u.Query() 150 - q.Set("iss", fmt.Sprintf("https://%s", o.host)) 151 - q.Set("state", session.DownstreamState) 152 - q.Set("code", session.DownstreamAuthorizationCode) 153 - u.RawQuery = q.Encode() 154 - 155 - return u.String(), nil 156 - }
-221
pkg/oproxy/oauth_4_token.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "crypto/sha256" 6 - "encoding/base64" 7 - "encoding/json" 8 - "fmt" 9 - "net/http" 10 - "net/url" 11 - "time" 12 - 13 - "github.com/AxisCommunications/go-dpop" 14 - "github.com/golang-jwt/jwt/v5" 15 - "github.com/google/uuid" 16 - "github.com/labstack/echo/v4" 17 - "go.opentelemetry.io/otel" 18 - ) 19 - 20 - type TokenRequest struct { 21 - GrantType string `json:"grant_type"` 22 - RedirectURI string `json:"redirect_uri"` 23 - Code string `json:"code"` 24 - CodeVerifier string `json:"code_verifier"` 25 - ClientID string `json:"client_id"` 26 - RefreshToken string `json:"refresh_token"` 27 - } 28 - 29 - type RevokeRequest struct { 30 - Token string `json:"token"` 31 - ClientID string `json:"client_id"` 32 - } 33 - 34 - type TokenResponse struct { 35 - AccessToken string `json:"access_token"` 36 - TokenType string `json:"token_type"` 37 - RefreshToken string `json:"refresh_token"` 38 - Scope string `json:"scope"` 39 - ExpiresIn int `json:"expires_in"` 40 - Sub string `json:"sub"` 41 - } 42 - 43 - var OAuthTokenExpiry = time.Hour * 24 44 - 45 - var dpopTimeWindow = time.Duration(30 * time.Second) 46 - 47 - func (o *OProxy) HandleOAuthToken(c echo.Context) error { 48 - ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthToken") 49 - defer span.End() 50 - var tokenRequest TokenRequest 51 - if err := json.NewDecoder(c.Request().Body).Decode(&tokenRequest); err != nil { 52 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err)) 53 - } 54 - 55 - dpopHeader := c.Request().Header.Get("DPoP") 56 - if dpopHeader == "" { 57 - return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 58 - } 59 - 60 - res, err := o.Token(ctx, &tokenRequest, dpopHeader) 61 - if err != nil { 62 - return err 63 - } 64 - jkt, _, err := getJKT(dpopHeader) 65 - if err != nil { 66 - return err 67 - } 68 - sess, err := o.loadOAuthSession(jkt) 69 - if err != nil { 70 - return err 71 - } 72 - sess.DownstreamDPoPNonce = makeNonce() 73 - err = o.updateOAuthSession(sess.DownstreamDPoPJKT, sess) 74 - if err != nil { 75 - return err 76 - } 77 - c.Response().Header().Set("DPoP-Nonce", sess.DownstreamDPoPNonce) 78 - 79 - return c.JSON(http.StatusOK, res) 80 - } 81 - 82 - func (o *OProxy) Token(ctx context.Context, tokenRequest *TokenRequest, dpopHeader string) (*TokenResponse, error) { 83 - proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/token"}, dpop.ParseOptions{ 84 - Nonce: "", 85 - TimeWindow: &dpopTimeWindow, 86 - }) 87 - if err != nil { 88 - return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof") 89 - } 90 - 91 - jkt := proof.PublicKey() 92 - session, err := o.loadOAuthSession(jkt) 93 - if err != nil { 94 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not get oauth session: %s", err)) 95 - } 96 - 97 - if tokenRequest.GrantType == "authorization_code" { 98 - return o.AccessToken(ctx, tokenRequest, session) 99 - } else if tokenRequest.GrantType == "refresh_token" { 100 - return o.RefreshToken(ctx, tokenRequest, session) 101 - } 102 - return nil, echo.NewHTTPError(http.StatusBadRequest, "unsupported grant type") 103 - } 104 - 105 - func (o *OProxy) AccessToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) { 106 - if session.Status() != OAuthSessionStateDownstream { 107 - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in downstream state: %s", session.Status())) 108 - } 109 - 110 - // Hash the code verifier using SHA-256 111 - hasher := sha256.New() 112 - hasher.Write([]byte(tokenRequest.CodeVerifier)) 113 - codeChallenge := hasher.Sum(nil) 114 - 115 - encodedChallenge := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(codeChallenge) 116 - 117 - if session.DownstreamCodeChallenge != encodedChallenge { 118 - return nil, fmt.Errorf("invalid code challenge") 119 - } 120 - 121 - if session.DownstreamAuthorizationCode != tokenRequest.Code { 122 - return nil, fmt.Errorf("invalid authorization code") 123 - } 124 - 125 - accessToken, err := o.generateJWT(session) 126 - if err != nil { 127 - return nil, fmt.Errorf("could not generate access token: %w", err) 128 - } 129 - 130 - refreshToken, err := generateRefreshToken() 131 - if err != nil { 132 - return nil, fmt.Errorf("could not generate refresh token: %w", err) 133 - } 134 - 135 - session.DownstreamAccessToken = accessToken 136 - session.DownstreamRefreshToken = refreshToken 137 - 138 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 139 - if err != nil { 140 - return nil, fmt.Errorf("could not update downstream session: %w", err) 141 - } 142 - 143 - return &TokenResponse{ 144 - AccessToken: accessToken, 145 - TokenType: "DPoP", 146 - RefreshToken: refreshToken, 147 - Scope: "atproto transition:generic", 148 - ExpiresIn: int(OAuthTokenExpiry.Seconds()), 149 - Sub: session.DID, 150 - }, nil 151 - } 152 - 153 - func (o *OProxy) RefreshToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) { 154 - 155 - if session.Status() != OAuthSessionStateReady { 156 - return nil, echo.NewHTTPError(http.StatusBadRequest, "session is not in ready state") 157 - } 158 - 159 - if session.DownstreamRefreshToken != tokenRequest.RefreshToken { 160 - return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid refresh token") 161 - } 162 - 163 - newJWT, err := o.generateJWT(session) 164 - if err != nil { 165 - return nil, fmt.Errorf("could not generate new access token: %w", err) 166 - } 167 - 168 - session.DownstreamAccessToken = newJWT 169 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 170 - if err != nil { 171 - return nil, fmt.Errorf("could not update downstream session: %w", err) 172 - } 173 - 174 - return &TokenResponse{ 175 - AccessToken: newJWT, 176 - TokenType: "DPoP", 177 - RefreshToken: session.DownstreamRefreshToken, 178 - Scope: "atproto transition:generic", 179 - ExpiresIn: int(OAuthTokenExpiry.Seconds()), 180 - Sub: session.DID, 181 - }, nil 182 - } 183 - 184 - func (o *OProxy) generateJWT(session *OAuthSession) (string, error) { 185 - uu, err := uuid.NewV7() 186 - if err != nil { 187 - return "", err 188 - } 189 - downstreamMeta, err := o.GetDownstreamMetadata("") 190 - if err != nil { 191 - return "", err 192 - } 193 - now := time.Now() 194 - token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ 195 - "jti": uu.String(), 196 - "sub": session.DID, 197 - "exp": now.Add(OAuthTokenExpiry).Unix(), 198 - "iat": now.Unix(), 199 - "nbf": now.Unix(), 200 - "cnf": map[string]any{ 201 - "jkt": session.DownstreamDPoPJKT, 202 - }, 203 - "aud": fmt.Sprintf("did:web:%s", o.host), 204 - "scope": downstreamMeta.Scope, 205 - "client_id": downstreamMeta.ClientID, 206 - "iss": fmt.Sprintf("https://%s", o.host), 207 - }) 208 - 209 - var rawKey any 210 - if err := o.downstreamJWK.Raw(&rawKey); err != nil { 211 - return "", err 212 - } 213 - 214 - tokenString, err := token.SignedString(rawKey) 215 - 216 - if err != nil { 217 - return "", err 218 - } 219 - 220 - return tokenString, nil 221 - }
-56
pkg/oproxy/oauth_5_revoke.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "net/http" 8 - "net/url" 9 - "time" 10 - 11 - "github.com/AxisCommunications/go-dpop" 12 - "github.com/labstack/echo/v4" 13 - "go.opentelemetry.io/otel" 14 - ) 15 - 16 - func (o *OProxy) HandleOAuthRevoke(c echo.Context) error { 17 - ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthRevoke") 18 - defer span.End() 19 - var revokeRequest RevokeRequest 20 - if err := json.NewDecoder(c.Request().Body).Decode(&revokeRequest); err != nil { 21 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err)) 22 - } 23 - dpopHeader := c.Request().Header.Get("DPoP") 24 - if dpopHeader == "" { 25 - return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required") 26 - } 27 - err := o.Revoke(ctx, dpopHeader, &revokeRequest) 28 - if err != nil { 29 - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not handle oauth revoke: %s", err)) 30 - } 31 - return c.JSON(http.StatusOK, map[string]interface{}{}) 32 - } 33 - 34 - func (o *OProxy) Revoke(ctx context.Context, dpopHeader string, revokeRequest *RevokeRequest) error { 35 - proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/revoke"}, dpop.ParseOptions{ 36 - Nonce: "", 37 - TimeWindow: &dpopTimeWindow, 38 - }) 39 - if err != nil { 40 - return echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof") 41 - } 42 - 43 - session, err := o.loadOAuthSession(proof.PublicKey()) 44 - if err != nil { 45 - return fmt.Errorf("could not get downstream session: %w", err) 46 - } 47 - 48 - now := time.Now() 49 - session.RevokedAt = &now 50 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 51 - if err != nil { 52 - return fmt.Errorf("could not update downstream session: %w", err) 53 - } 54 - 55 - return nil 56 - }
-237
pkg/oproxy/oauth_middleware.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "crypto/ecdsa" 6 - "crypto/sha256" 7 - "encoding/base64" 8 - "encoding/json" 9 - "errors" 10 - "fmt" 11 - "net/http" 12 - "net/url" 13 - "strings" 14 - 15 - "github.com/AxisCommunications/go-dpop" 16 - "github.com/golang-jwt/jwt/v5" 17 - "github.com/labstack/echo/v4" 18 - ) 19 - 20 - var OAuthSessionContextKey = oauthSessionContextKeyType{} 21 - 22 - type oauthSessionContextKeyType struct{} 23 - 24 - var OProxyContextKey = oproxyContextKeyType{} 25 - 26 - type oproxyContextKeyType struct{} 27 - 28 - func GetOAuthSession(ctx context.Context) (*OAuthSession, *XrpcClient) { 29 - o, ok := ctx.Value(OProxyContextKey).(*OProxy) 30 - if !ok { 31 - return nil, nil 32 - } 33 - session, ok := ctx.Value(OAuthSessionContextKey).(*OAuthSession) 34 - if !ok { 35 - return nil, nil 36 - } 37 - client, err := o.GetXrpcClient(session) 38 - if err != nil { 39 - return nil, nil 40 - } 41 - return session, client 42 - } 43 - 44 - func (o *OProxy) OAuthMiddleware(next http.Handler) http.Handler { 45 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 - // todo: see what these were set to before it got to us. 47 - w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh 48 - w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP") 49 - w.Header().Set("Access-Control-Allow-Methods", "*") 50 - w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce") 51 - 52 - ctx := r.Context() 53 - session, err := o.getOAuthSession(r, w) 54 - if err != nil { 55 - if errors.Is(err, dpop.ErrIncorrectNonce) { 56 - // w.Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce", error_description="Invalid nonce"`) 57 - w.Header().Set("content-type", "application/json") 58 - w.WriteHeader(http.StatusUnauthorized) 59 - bs, _ := json.Marshal(map[string]interface{}{ 60 - "error": "use_dpop_nonce", 61 - "error_description": "Authorization server requires nonce in DPoP proof", 62 - }) 63 - w.Write(bs) 64 - return 65 - } 66 - o.slog.Error("oauth error", "error", err) 67 - w.WriteHeader(http.StatusInternalServerError) 68 - w.Write([]byte(err.Error())) 69 - return 70 - } 71 - if session == nil { 72 - next.ServeHTTP(w, r) 73 - return 74 - } 75 - ctx = context.WithValue(ctx, OAuthSessionContextKey, session) 76 - ctx = context.WithValue(ctx, OProxyContextKey, o) 77 - next.ServeHTTP(w, r.WithContext(ctx)) 78 - }) 79 - } 80 - 81 - func getMethod(method string) (dpop.HTTPVerb, error) { 82 - switch method { 83 - case "POST": 84 - return dpop.POST, nil 85 - case "GET": 86 - return dpop.GET, nil 87 - } 88 - return "", fmt.Errorf("invalid method") 89 - } 90 - 91 - func (o *OProxy) getOAuthSession(r *http.Request, w http.ResponseWriter) (*OAuthSession, error) { 92 - 93 - authHeader := r.Header.Get("Authorization") 94 - if authHeader == "" { 95 - return nil, nil 96 - } 97 - if !strings.HasPrefix(authHeader, "DPoP ") { 98 - return nil, nil 99 - } 100 - token := strings.TrimPrefix(authHeader, "DPoP ") 101 - 102 - dpopHeader := r.Header.Get("DPoP") 103 - if dpopHeader == "" { 104 - return nil, fmt.Errorf("missing DPoP header") 105 - } 106 - 107 - dpopMethod, err := getMethod(r.Method) 108 - if err != nil { 109 - return nil, fmt.Errorf("invalid method: %w", err) 110 - } 111 - 112 - u, err := url.Parse(r.URL.String()) 113 - if err != nil { 114 - return nil, fmt.Errorf("invalid url: %w", err) 115 - } 116 - u.Scheme = "https" 117 - u.Host = r.Host 118 - u.RawQuery = "" 119 - u.Fragment = "" 120 - 121 - jkt, nonce, err := getJKT(dpopHeader) 122 - 123 - session, err := o.loadOAuthSession(jkt) 124 - if err != nil { 125 - return nil, fmt.Errorf("could not get oauth session: %w", err) 126 - } 127 - if session == nil { 128 - // this can happen for stuff like getFeedSkeleton where they've submitted oauth credentials 129 - // but they're not actually for this server 130 - return nil, nil 131 - } 132 - if session.RevokedAt != nil { 133 - return nil, fmt.Errorf("oauth session revoked") 134 - } 135 - if session.DownstreamDPoPNonce != nonce { 136 - w.Header().Set("WWW-Authenticate", `DPoP algs="RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES256K ES384 ES512", error="use_dpop_nonce", error_description="Authorization server requires nonce in DPoP proof"`) 137 - w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce) 138 - return nil, dpop.ErrIncorrectNonce 139 - } 140 - 141 - session.DownstreamDPoPNonce = makeNonce() 142 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 143 - if err != nil { 144 - return nil, fmt.Errorf("could not update downstream session: %w", err) 145 - } 146 - w.Header().Set("DPoP-Nonce", session.DownstreamDPoPNonce) 147 - 148 - proof, err := dpop.Parse(dpopHeader, dpopMethod, u, dpop.ParseOptions{ 149 - Nonce: nonce, 150 - TimeWindow: &dpopTimeWindow, 151 - }) 152 - // Check the error type to determine response 153 - if err != nil { 154 - if ok := errors.Is(err, dpop.ErrInvalidProof); ok { 155 - // Return 'invalid_dpop_proof' 156 - return nil, fmt.Errorf("invalid DPoP proof: %w", err) 157 - } 158 - return nil, fmt.Errorf("error validating proof proof: %w", err) 159 - } 160 - 161 - // Hash the token with base64 and SHA256 162 - // Get the access token JWT (introspect if needed) 163 - // Parse the access token JWT and verify the signature 164 - // Hash the access token with SHA-256 165 - hasher := sha256.New() 166 - hasher.Write([]byte(token)) 167 - hash := hasher.Sum(nil) 168 - 169 - // Encode the hash in URL-safe base64 format without padding 170 - // accessTokenHash := base64.RawURLEncoding.EncodeToString(hash) 171 - accessTokenHash := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash) 172 - pubKey, err := o.downstreamJWK.PublicKey() 173 - if err != nil { 174 - return nil, fmt.Errorf("could not get access jwk public key: %w", err) 175 - } 176 - var pubKeyECDSA ecdsa.PublicKey 177 - err = pubKey.Raw(&pubKeyECDSA) 178 - if err != nil { 179 - return nil, fmt.Errorf("could not get access jwk public key: %w", err) 180 - } 181 - 182 - // Parse the access token JWT 183 - claims := &dpop.BoundAccessTokenClaims{} 184 - accessTokenJWT, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) { 185 - return &pubKeyECDSA, nil 186 - }) 187 - 188 - if err != nil { 189 - return nil, fmt.Errorf("could not parse access token: %w", err) 190 - } 191 - 192 - err = proof.Validate([]byte(accessTokenHash), accessTokenJWT) 193 - // Check the error type to determine response 194 - if err != nil { 195 - return nil, fmt.Errorf("invalid proof: %w", err) 196 - } 197 - 198 - return session, nil 199 - } 200 - 201 - func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 202 - return func(c echo.Context) error { 203 - dpopHeader := c.Request().Header.Get("DPoP") 204 - if dpopHeader == "" { 205 - return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header") 206 - } 207 - 208 - jkt, _, err := getJKT(dpopHeader) 209 - if err != nil { 210 - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 211 - } 212 - 213 - session, err := o.loadOAuthSession(jkt) 214 - if err != nil { 215 - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 216 - } 217 - 218 - c.Set("session", session) 219 - return next(c) 220 - } 221 - } 222 - 223 - func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 224 - return func(c echo.Context) error { 225 - err := next(c) 226 - if err == nil { 227 - return nil 228 - } 229 - httpError, ok := err.(*echo.HTTPError) 230 - if ok { 231 - o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal) 232 - return err 233 - } 234 - o.slog.Error("unhandled error", "error", err) 235 - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 236 - } 237 - }
-156
pkg/oproxy/oauth_session.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "time" 8 - 9 - oauth "github.com/haileyok/atproto-oauth-golang" 10 - "github.com/lestrrat-go/jwx/v2/jwk" 11 - ) 12 - 13 - var refreshWhenRemaining = time.Minute * 15 14 - 15 - // OAuthSession stores authentication data needed during the OAuth flow 16 - type OAuthSession struct { 17 - DID string `json:"did" gorm:"column:repo_did;index"` 18 - Handle string `json:"handle" gorm:"column:handle;index"` // possibly also did if they have no handle 19 - PDSUrl string `json:"pds_url" gorm:"column:pds_url;index"` 20 - 21 - // Upstream fields 22 - UpstreamState string `json:"upstream_state" gorm:"column:upstream_state;index"` 23 - UpstreamAuthServerIssuer string `json:"upstream_auth_server_issuer" gorm:"column:upstream_auth_server_issuer"` 24 - UpstreamPKCEVerifier string `json:"upstream_pkce_verifier" gorm:"column:upstream_pkce_verifier"` 25 - UpstreamDPoPNonce string `json:"upstream_dpop_nonce" gorm:"column:upstream_dpop_nonce"` 26 - UpstreamDPoPPrivateJWK string `json:"upstream_dpop_private_jwk" gorm:"column:upstream_dpop_private_jwk;type:text"` 27 - UpstreamAccessToken string `json:"upstream_access_token" gorm:"column:upstream_access_token"` 28 - UpstreamAccessTokenExp *time.Time `json:"upstream_access_token_exp" gorm:"column:upstream_access_token_exp"` 29 - UpstreamRefreshToken string `json:"upstream_refresh_token" gorm:"column:upstream_refresh_token"` 30 - 31 - // Downstream fields 32 - DownstreamDPoPNonce string `json:"downstream_dpop_nonce" gorm:"column:downstream_dpop_nonce"` 33 - DownstreamDPoPJKT string `json:"downstream_dpop_jkt" gorm:"column:downstream_dpop_jkt;primaryKey"` 34 - DownstreamAccessToken string `json:"downstream_access_token" gorm:"column:downstream_access_token;index"` 35 - DownstreamRefreshToken string `json:"downstream_refresh_token" gorm:"column:downstream_refresh_token;index"` 36 - DownstreamAuthorizationCode string `json:"downstream_authorization_code" gorm:"column:downstream_authorization_code;index"` 37 - DownstreamState string `json:"downstream_state" gorm:"column:downstream_state"` 38 - DownstreamScope string `json:"downstream_scope" gorm:"column:downstream_scope"` 39 - DownstreamCodeChallenge string `json:"downstream_code_challenge" gorm:"column:downstream_code_challenge"` 40 - DownstreamPARRequestURI string `json:"downstream_par_request_uri" gorm:"column:downstream_par_request_uri"` 41 - DownstreamPARUsedAt *time.Time `json:"downstream_par_used_at" gorm:"column:downstream_par_used_at"` 42 - DownstreamRedirectURI string `json:"downstream_redirect_uri" gorm:"column:downstream_redirect_uri"` 43 - 44 - RevokedAt *time.Time `json:"revoked_at" gorm:"column:revoked_at"` 45 - CreatedAt time.Time `json:"created_at"` 46 - UpdatedAt time.Time `json:"updated_at"` 47 - } 48 - 49 - // for gorm. this is prettier than "o_auth_sessions" 50 - func (o *OAuthSession) TableName() string { 51 - return "oauth_sessions" 52 - } 53 - 54 - type OAuthSessionStatus string 55 - 56 - const ( 57 - // We've gotten the first request and sent it back for a new nonce 58 - OAuthSessionStatePARPending OAuthSessionStatus = "par-pending" 59 - // PAR has been created, but not yet used 60 - OAuthSessionStatePARCreated OAuthSessionStatus = "par-created" 61 - // PAR has been used, but maybe upstream will fail for some reason 62 - OAuthSessionStatePARUsed OAuthSessionStatus = "par-used" 63 - // PAR has been used, we're waiting to hear back from upstream 64 - OAuthSessionStateUpstream OAuthSessionStatus = "upstream" 65 - // Upstream came back, we've issued the user a code but it hasn't been used yet 66 - OAuthSessionStateDownstream OAuthSessionStatus = "downstream" 67 - // Code has been used, everything is good 68 - OAuthSessionStateReady OAuthSessionStatus = "ready" 69 - // For any reason we're done. Revoked or expired 70 - OAuthSessionStateRejected OAuthSessionStatus = "rejected" 71 - ) 72 - 73 - func (o *OAuthSession) Status() OAuthSessionStatus { 74 - if o.RevokedAt != nil { 75 - return OAuthSessionStateRejected 76 - } 77 - if o.DownstreamAccessToken != "" { 78 - return OAuthSessionStateReady 79 - } 80 - if o.DownstreamAuthorizationCode != "" { 81 - return OAuthSessionStateDownstream 82 - } 83 - if o.UpstreamDPoPPrivateJWK != "" { 84 - return OAuthSessionStateUpstream 85 - } 86 - if o.DownstreamPARUsedAt != nil { 87 - return OAuthSessionStatePARUsed 88 - } 89 - if o.DownstreamPARRequestURI != "" { 90 - return OAuthSessionStatePARCreated 91 - } 92 - if o.DownstreamDPoPNonce != "" { 93 - return OAuthSessionStatePARPending 94 - } 95 - bs, _ := json.Marshal(o) 96 - fmt.Printf("unknown oauth session status: %s\n", string(bs)) 97 - // todo: this should never happen, log a warning? panic? 98 - return OAuthSessionStateRejected 99 - } 100 - 101 - func (o *OProxy) loadOAuthSession(jkt string) (*OAuthSession, error) { 102 - session, err := o.userLoadOAuthSession(jkt) 103 - if err != nil { 104 - return nil, err 105 - } 106 - if session == nil { 107 - return nil, nil 108 - } 109 - if session.Status() != OAuthSessionStateReady { 110 - return session, nil 111 - } 112 - if session.UpstreamAccessTokenExp.Sub(time.Now()) > refreshWhenRemaining { 113 - return session, nil 114 - } 115 - 116 - upstreamMeta := o.GetUpstreamMetadata() 117 - 118 - oclient, err := oauth.NewClient(oauth.ClientArgs{ 119 - ClientJwk: o.upstreamJWK, 120 - ClientId: upstreamMeta.ClientID, 121 - RedirectUri: upstreamMeta.RedirectURIs[0], 122 - }) 123 - 124 - dpopKey, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 125 - if err != nil { 126 - return nil, fmt.Errorf("failed to parse upstream dpop private key: %w", err) 127 - } 128 - 129 - // refresh upstream before returning 130 - resp, err := oclient.RefreshTokenRequest(context.Background(), session.UpstreamRefreshToken, session.UpstreamAuthServerIssuer, session.UpstreamDPoPNonce, dpopKey) 131 - if err != nil { 132 - // revoke, probably 133 - o.slog.Error("failed to refresh upstream token, revoking downstream session", "error", err) 134 - now := time.Now() 135 - session.RevokedAt = &now 136 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 137 - if err != nil { 138 - o.slog.Error("after upstream token refresh, failed to revoke downstream session", "error", err) 139 - } 140 - return nil, fmt.Errorf("failed to refresh upstream token: %w", err) 141 - } 142 - 143 - exp := time.Now().Add(time.Second * time.Duration(resp.ExpiresIn)).UTC() 144 - session.UpstreamAccessToken = resp.AccessToken 145 - session.UpstreamAccessTokenExp = &exp 146 - session.UpstreamRefreshToken = resp.RefreshToken 147 - 148 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 149 - if err != nil { 150 - return nil, fmt.Errorf("failed to update downstream session after upstream token refresh: %w", err) 151 - } 152 - 153 - o.slog.Debug("refreshed upstream token", "session", session.DownstreamDPoPJKT) 154 - 155 - return session, nil 156 - }
-74
pkg/oproxy/oproxy.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "log/slog" 5 - "net/http" 6 - "os" 7 - 8 - "github.com/labstack/echo/v4" 9 - "github.com/lestrrat-go/jwx/v2/jwk" 10 - ) 11 - 12 - type OProxy struct { 13 - createOAuthSession func(id string, session *OAuthSession) error 14 - updateOAuthSession func(id string, session *OAuthSession) error 15 - userLoadOAuthSession func(id string) (*OAuthSession, error) 16 - e *echo.Echo 17 - host string 18 - scope string 19 - upstreamJWK jwk.Key 20 - downstreamJWK jwk.Key 21 - slog *slog.Logger 22 - } 23 - 24 - type Config struct { 25 - CreateOAuthSession func(id string, session *OAuthSession) error 26 - UpdateOAuthSession func(id string, session *OAuthSession) error 27 - LoadOAuthSession func(id string) (*OAuthSession, error) 28 - Host string 29 - Scope string 30 - UpstreamJWK jwk.Key 31 - DownstreamJWK jwk.Key 32 - Slog *slog.Logger 33 - } 34 - 35 - func New(conf *Config) *OProxy { 36 - e := echo.New() 37 - mySlog := conf.Slog 38 - if mySlog == nil { 39 - mySlog = slog.New(slog.NewTextHandler(os.Stderr, nil)) 40 - } 41 - o := &OProxy{ 42 - createOAuthSession: conf.CreateOAuthSession, 43 - updateOAuthSession: conf.UpdateOAuthSession, 44 - userLoadOAuthSession: conf.LoadOAuthSession, 45 - e: e, 46 - host: conf.Host, 47 - scope: conf.Scope, 48 - upstreamJWK: conf.UpstreamJWK, 49 - downstreamJWK: conf.DownstreamJWK, 50 - slog: mySlog, 51 - } 52 - o.e.GET("/.well-known/oauth-authorization-server", o.HandleOAuthAuthorizationServer) 53 - o.e.GET("/.well-known/oauth-protected-resource", o.HandleOAuthProtectedResource) 54 - o.e.POST("/oauth/par", o.HandleOAuthPAR) 55 - o.e.GET("/oauth/authorize", o.HandleOAuthAuthorize) 56 - o.e.GET("/oauth/return", o.HandleOAuthReturn) 57 - o.e.POST("/oauth/token", o.DPoPNonceMiddleware(o.HandleOAuthToken)) 58 - o.e.POST("/oauth/revoke", o.DPoPNonceMiddleware(o.HandleOAuthRevoke)) 59 - o.e.GET("/oauth/upstream/client-metadata.json", o.HandleClientMetadataUpstream) 60 - o.e.GET("/oauth/upstream/jwks.json", o.HandleJwksUpstream) 61 - o.e.GET("/oauth/downstream/client-metadata.json", o.HandleClientMetadataDownstream) 62 - o.e.Use(o.ErrorHandlingMiddleware) 63 - return o 64 - } 65 - 66 - func (o *OProxy) Handler() http.Handler { 67 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 - w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh 69 - w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP") 70 - w.Header().Set("Access-Control-Allow-Methods", "*") 71 - w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce") 72 - o.e.ServeHTTP(w, r) 73 - }) 74 - }
-124
pkg/oproxy/resolution.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "io" 8 - "net" 9 - "net/http" 10 - "strings" 11 - 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 - ) 14 - 15 - // mostly borrowed from github.com/haileyok/atproto-oauth-golang, MIT license 16 - func ResolveHandle(ctx context.Context, handle string) (string, error) { 17 - var did string 18 - 19 - _, err := syntax.ParseHandle(handle) 20 - if err != nil { 21 - return "", err 22 - } 23 - 24 - recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle)) 25 - if err == nil { 26 - for _, rec := range recs { 27 - if strings.HasPrefix(rec, "did=") { 28 - did = strings.Split(rec, "did=")[1] 29 - break 30 - } 31 - } 32 - } 33 - 34 - if did == "" { 35 - req, err := http.NewRequestWithContext( 36 - ctx, 37 - "GET", 38 - fmt.Sprintf("https://%s/.well-known/atproto-did", handle), 39 - nil, 40 - ) 41 - if err != nil { 42 - return "", err 43 - } 44 - 45 - resp, err := http.DefaultClient.Do(req) 46 - if err != nil { 47 - return "", err 48 - } 49 - defer resp.Body.Close() 50 - 51 - if resp.StatusCode != http.StatusOK { 52 - io.Copy(io.Discard, resp.Body) 53 - return "", fmt.Errorf("unable to resolve handle") 54 - } 55 - 56 - b, err := io.ReadAll(resp.Body) 57 - if err != nil { 58 - return "", err 59 - } 60 - 61 - maybeDid := string(b) 62 - 63 - if _, err := syntax.ParseDID(maybeDid); err != nil { 64 - return "", fmt.Errorf("unable to resolve handle") 65 - } 66 - 67 - did = maybeDid 68 - } 69 - 70 - return did, nil 71 - } 72 - 73 - func ResolveService(ctx context.Context, did string) (string, error) { 74 - type Identity struct { 75 - Service []struct { 76 - ID string `json:"id"` 77 - Type string `json:"type"` 78 - ServiceEndpoint string `json:"serviceEndpoint"` 79 - } `json:"service"` 80 - } 81 - 82 - var ustr string 83 - if strings.HasPrefix(did, "did:plc:") { 84 - ustr = fmt.Sprintf("https://plc.directory/%s", did) 85 - } else if strings.HasPrefix(did, "did:web:") { 86 - ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 87 - } else { 88 - return "", fmt.Errorf("did was not a supported did type") 89 - } 90 - 91 - req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 92 - if err != nil { 93 - return "", err 94 - } 95 - 96 - resp, err := http.DefaultClient.Do(req) 97 - if err != nil { 98 - return "", err 99 - } 100 - defer resp.Body.Close() 101 - 102 - if resp.StatusCode != 200 { 103 - io.Copy(io.Discard, resp.Body) 104 - return "", fmt.Errorf("could not find identity in plc registry") 105 - } 106 - 107 - var identity Identity 108 - if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil { 109 - return "", err 110 - } 111 - 112 - var service string 113 - for _, svc := range identity.Service { 114 - if svc.ID == "#atproto_pds" { 115 - service = svc.ServiceEndpoint 116 - } 117 - } 118 - 119 - if service == "" { 120 - return "", fmt.Errorf("could not find atproto_pds service in identity services") 121 - } 122 - 123 - return service, nil 124 - }
-23
pkg/oproxy/token_generation.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "fmt" 5 - 6 - "github.com/google/uuid" 7 - ) 8 - 9 - func generateRefreshToken() (string, error) { 10 - uu, err := uuid.NewV7() 11 - if err != nil { 12 - return "", err 13 - } 14 - return fmt.Sprintf("refresh-%s", uu.String()), nil 15 - } 16 - 17 - func generateAuthorizationCode() (string, error) { 18 - uu, err := uuid.NewV7() 19 - if err != nil { 20 - return "", err 21 - } 22 - return fmt.Sprintf("code-%s", uu.String()), nil 23 - }
-35
pkg/oproxy/types.go
··· 1 - package oproxy 2 - 3 - import "github.com/haileyok/atproto-oauth-golang/helpers" 4 - 5 - type OAuthClientMetadata struct { 6 - RedirectURIs []string `json:"redirect_uris"` 7 - ResponseTypes []string `json:"response_types,omitempty"` 8 - GrantTypes []string `json:"grant_types,omitempty"` 9 - Scope string `json:"scope,omitempty"` 10 - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` 11 - TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"` 12 - UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"` 13 - UserinfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"` 14 - JwksURI string `json:"jwks_uri,omitempty"` 15 - ApplicationType string `json:"application_type,omitempty"` // "web" or "native" 16 - SubjectType string `json:"subject_type,omitempty"` // "public" or "pairwise" 17 - RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"` 18 - IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"` 19 - AuthorizationSignedResponseAlg string `json:"authorization_signed_response_alg,omitempty"` 20 - AuthorizationEncryptedResponseEnc string `json:"authorization_encrypted_response_enc,omitempty"` 21 - AuthorizationEncryptedResponseAlg string `json:"authorization_encrypted_response_alg,omitempty"` 22 - ClientID string `json:"client_id,omitempty"` 23 - ClientName string `json:"client_name,omitempty"` 24 - ClientURI string `json:"client_uri,omitempty"` 25 - PolicyURI string `json:"policy_uri,omitempty"` 26 - TosURI string `json:"tos_uri,omitempty"` 27 - LogoURI string `json:"logo_uri,omitempty"` 28 - DefaultMaxAge int `json:"default_max_age,omitempty"` 29 - RequireAuthTime *bool `json:"require_auth_time,omitempty"` 30 - Contacts []string `json:"contacts,omitempty"` 31 - TLSClientCertificateBoundAccessTokens *bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` 32 - DPoPBoundAccessTokens *bool `json:"dpop_bound_access_tokens,omitempty"` 33 - AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"` 34 - Jwks *helpers.JwksResponseObject `json:"jwks,omitempty"` 35 - }
-63
pkg/oproxy/xrpc_client.go
··· 1 - package oproxy 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - 8 - "github.com/bluesky-social/indigo/xrpc" 9 - oauth "github.com/haileyok/atproto-oauth-golang" 10 - "github.com/labstack/echo/v4" 11 - "github.com/lestrrat-go/jwx/v2/jwk" 12 - ) 13 - 14 - var xrpcClient *oauth.XrpcClient 15 - 16 - type XrpcClient struct { 17 - client *oauth.XrpcClient 18 - authArgs *oauth.XrpcAuthedRequestArgs 19 - } 20 - 21 - func (o *OProxy) GetXrpcClient(session *OAuthSession) (*XrpcClient, error) { 22 - key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 23 - if err != nil { 24 - return nil, fmt.Errorf("failed to parse DPoP private JWK: %w", err) 25 - } 26 - authArgs := &oauth.XrpcAuthedRequestArgs{ 27 - Did: session.DID, 28 - AccessToken: session.UpstreamAccessToken, 29 - PdsUrl: session.PDSUrl, 30 - Issuer: session.UpstreamAuthServerIssuer, 31 - DpopPdsNonce: session.UpstreamDPoPNonce, 32 - DpopPrivateJwk: key, 33 - } 34 - 35 - xrpcClient := &oauth.XrpcClient{ 36 - OnDpopPdsNonceChanged: func(did, newNonce string) { 37 - sess, err := o.loadOAuthSession(session.DownstreamDPoPJKT) 38 - if err != nil { 39 - o.slog.Error("failed to get OAuth session in OnDpopPdsNonceChanged", "error", err) 40 - return 41 - } 42 - sess.UpstreamDPoPNonce = newNonce 43 - err = o.updateOAuthSession(session.DownstreamDPoPJKT, sess) 44 - if err != nil { 45 - o.slog.Error("failed to update OAuth session in OnDpopPdsNonceChanged", "error", err) 46 - } 47 - o.slog.Info("updated OAuth session in OnDpopPdsNonceChanged", "session", sess) 48 - }, 49 - } 50 - return &XrpcClient{client: xrpcClient, authArgs: authArgs}, nil 51 - } 52 - 53 - func (c *XrpcClient) Do(ctx context.Context, kind xrpc.XRPCRequestType, inpenc, method string, params map[string]any, bodyobj any, out any) error { 54 - err := c.client.Do(ctx, c.authArgs, kind, inpenc, method, params, bodyobj, out) 55 - if err == nil { 56 - return nil 57 - } 58 - xErr, ok := err.(*xrpc.Error) 59 - if !ok { 60 - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 61 - } 62 - return xErr 63 - }
+2 -2
pkg/spxrpc/app_bsky_actor.go
··· 7 7 appbskytypes "github.com/bluesky-social/indigo/api/bsky" 8 8 "github.com/bluesky-social/indigo/xrpc" 9 9 "github.com/labstack/echo/v4" 10 - "stream.place/streamplace/pkg/oproxy" 10 + "github.com/streamplace/oatproxy/pkg/oatproxy" 11 11 ) 12 12 13 13 func (s *Server) handleAppBskyActorGetProfile(ctx context.Context, actor string) (*appbskytypes.ActorDefs_ProfileViewDetailed, error) { 14 - session, client := oproxy.GetOAuthSession(ctx) 14 + session, client := oatproxy.GetOAuthSession(ctx) 15 15 if session == nil { 16 16 return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") 17 17 }
+2 -2
pkg/spxrpc/com_atproto_identity.go
··· 4 4 "context" 5 5 6 6 comatprototypes "github.com/bluesky-social/indigo/api/atproto" 7 - "stream.place/streamplace/pkg/oproxy" 7 + "github.com/streamplace/oatproxy/pkg/oatproxy" 8 8 ) 9 9 10 10 func (s *Server) handleComAtprotoIdentityResolveHandle(ctx context.Context, handle string) (*comatprototypes.IdentityResolveHandle_Output, error) { 11 - did, err := oproxy.ResolveHandle(ctx, handle) 11 + did, err := oatproxy.ResolveHandle(ctx, handle) 12 12 if err != nil { 13 13 return nil, err 14 14 }
+2 -2
pkg/spxrpc/com_atproto_repo.go
··· 8 8 comatprototypes "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/xrpc" 10 10 "github.com/labstack/echo/v4" 11 + "github.com/streamplace/oatproxy/pkg/oatproxy" 11 12 "go.opentelemetry.io/otel" 12 13 "stream.place/streamplace/pkg/log" 13 - "stream.place/streamplace/pkg/oproxy" 14 14 ) 15 15 16 16 func (s *Server) handleComAtprotoRepoUploadBlob(ctx context.Context, r io.Reader, contentType string) (*comatprototypes.RepoUploadBlob_Output, error) { 17 17 ctx, span := otel.Tracer("server").Start(ctx, "handleComAtprotoRepoUploadBlob") 18 18 defer span.End() 19 19 20 - session, client := oproxy.GetOAuthSession(ctx) 20 + session, client := oatproxy.GetOAuthSession(ctx) 21 21 if session == nil { 22 22 return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") 23 23 }
+3 -1
pkg/spxrpc/spxrpc.go
··· 5 5 "net/http" 6 6 7 7 "github.com/labstack/echo/v4" 8 + "github.com/streamplace/oatproxy/pkg/oatproxy" 8 9 "stream.place/streamplace/pkg/config" 9 10 "stream.place/streamplace/pkg/log" 10 11 "stream.place/streamplace/pkg/model" ··· 16 17 model model.Model 17 18 } 18 19 19 - func NewServer(ctx context.Context, cli *config.CLI, model model.Model) (*Server, error) { 20 + func NewServer(ctx context.Context, cli *config.CLI, model model.Model, op *oatproxy.OATProxy) (*Server, error) { 20 21 e := echo.New() 21 22 s := &Server{ 22 23 e: e, ··· 24 25 model: model, 25 26 } 26 27 e.Use(s.ErrorHandlingMiddleware()) 28 + e.Use(op.OAuthMiddleware) 27 29 err := s.RegisterHandlersPlaceStream(e) 28 30 if err != nil { 29 31 return nil, err
+2 -2
pkg/spxrpc/wildcard.go
··· 7 7 8 8 "github.com/bluesky-social/indigo/xrpc" 9 9 "github.com/labstack/echo/v4" 10 + "github.com/streamplace/oatproxy/pkg/oatproxy" 10 11 "go.opentelemetry.io/otel" 11 12 "stream.place/streamplace/pkg/log" 12 - "stream.place/streamplace/pkg/oproxy" 13 13 ) 14 14 15 15 func (s *Server) HandleWildcard(c echo.Context) error { 16 16 ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleWildcard") 17 17 defer span.End() 18 18 19 - session, client := oproxy.GetOAuthSession(ctx) 19 + session, client := oatproxy.GetOAuthSession(ctx) 20 20 if session == nil { 21 21 return echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") 22 22 }