this repo has no description

import OAuth webapp example code

+1
.gitignore
··· 19 19 *.db 20 20 /data/ 21 21 test-coverage.out 22 + .env 22 23 23 24 # Don't ignore this file itself, or other specific dotfiles 24 25 !.gitignore
+45
Makefile
··· 1 + 2 + SHELL = /bin/bash 3 + .SHELLFLAGS = -o pipefail -c 4 + 5 + .PHONY: help 6 + help: ## Print info about all commands 7 + @echo "Commands:" 8 + @echo 9 + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}' 10 + 11 + .PHONY: build 12 + build: ## Build all executables 13 + go build . 14 + 15 + .PHONY: all 16 + all: build 17 + 18 + .PHONY: test 19 + test: ## Run tests 20 + go test ./... 21 + 22 + .PHONY: coverage-html 23 + coverage-html: ## Generate test coverage report and open in browser 24 + go test ./... -coverpkg=./... -coverprofile=test-coverage.out 25 + go tool cover -html=test-coverage.out 26 + 27 + .PHONY: lint 28 + lint: ## Verify code style and run static checks 29 + go vet ./... 30 + test -z $(gofmt -l ./...) 31 + 32 + .PHONY: golangci-lint 33 + golangci-lint: ## Additional static linting 34 + golangci-lint run 35 + 36 + .PHONY: fmt 37 + fmt: ## Run syntax re-formatting (modify in place) 38 + go fmt ./... 39 + 40 + .PHONY: check 41 + check: ## Compile everything, checking syntax (does not output binaries) 42 + go build ./... 43 + 44 + .env: 45 + if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+38
base.html
··· 1 + 2 + <!doctype html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="utf-8"> 6 + <meta name="referrer" content="origin-when-cross-origin"> 7 + <meta name="viewport" content="width=device-width, initial-scale=1"> 8 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.purple.min.css"> 9 + <title>atproto OAuth demo (indigo)</title> 10 + </head> 11 + <body> 12 + <header> 13 + <hgroup> 14 + <h1>atproto OAuth demo (indigo)</h1> 15 + {{ if false }} 16 + <p>Hello <span style="font-family: monospace;">@{{ "handle" }}</span>!</p> 17 + {{ end }} 18 + </hgroup> 19 + <nav> 20 + <ul> 21 + {{ if false }} 22 + <li><a href="/bsky/post">Create Post</a> 23 + <li><a href="/oauth/refresh">Refresh Token</a> 24 + <li><a href="/oauth/logout">Logout</a> 25 + {{ else }} 26 + <li><a href="/oauth/login">Login</a> 27 + {{ end }} 28 + <li><a href="https://github.com/bluesky-social/indigo/tree/main/atproto/auth/oauth">Code</a> 29 + </ul> 30 + </nav> 31 + </header> 32 + <main> 33 + <section class="content"> 34 + {{ template "content" . }} 35 + </section> 36 + </main> 37 + </body> 38 + </html>
+32
go.mod
··· 1 1 module tangled.sh/bnewbold.net/user-intents 2 2 3 3 go 1.24.4 4 + 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20250711090625-c3e99aceec23 7 + github.com/gorilla/sessions v1.2.1 8 + github.com/joho/godotenv v1.5.1 9 + github.com/urfave/cli/v2 v2.25.7 10 + ) 11 + 12 + require ( 13 + github.com/beorn7/perks v1.0.1 // indirect 14 + github.com/carlmjohnson/versioninfo v0.22.5 // indirect 15 + github.com/cespare/xxhash/v2 v2.2.0 // indirect 16 + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 17 + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 18 + github.com/google/go-querystring v1.1.0 // indirect 19 + github.com/gorilla/securecookie v1.1.1 // indirect 20 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 21 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 22 + github.com/mr-tron/base58 v1.2.0 // indirect 23 + github.com/prometheus/client_golang v1.17.0 // indirect 24 + github.com/prometheus/client_model v0.5.0 // indirect 25 + github.com/prometheus/common v0.45.0 // indirect 26 + github.com/prometheus/procfs v0.12.0 // indirect 27 + github.com/russross/blackfriday/v2 v2.1.0 // indirect 28 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 29 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 30 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 31 + golang.org/x/crypto v0.21.0 // indirect 32 + golang.org/x/sys v0.22.0 // indirect 33 + golang.org/x/time v0.3.0 // indirect 34 + google.golang.org/protobuf v1.33.0 // indirect 35 + )
+148
go.sum
··· 1 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 + github.com/bluesky-social/indigo v0.0.0-20250711090625-c3e99aceec23 h1:uLHTZecIuT3zLU3+qr2qwDWMJIW+r7dpk8cuuJptEL0= 4 + github.com/bluesky-social/indigo v0.0.0-20250711090625-c3e99aceec23/go.mod h1:MGLKdNswSvDpcGkkQkUVREBXLwccqsGorapP8R1uifU= 5 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 6 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 7 + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 8 + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 + github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= 10 + github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 11 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 14 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 15 + github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 16 + github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 18 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 19 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 20 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 21 + github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 22 + github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 23 + github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 + github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 25 + github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 26 + github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 27 + github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 28 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 29 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 + github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 31 + github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 32 + github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 33 + github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 34 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 35 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 36 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 37 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 38 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 39 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 40 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 41 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 42 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 43 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 44 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 45 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 46 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 47 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 48 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 49 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 50 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 51 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 52 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 53 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 54 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 55 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 56 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 57 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 58 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 59 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 60 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 61 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 62 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 63 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 64 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 65 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 66 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 67 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 68 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 69 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 70 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 71 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 72 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 73 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 74 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 75 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 76 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 77 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 78 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 79 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 80 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 81 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 82 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 83 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 84 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 85 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 86 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 87 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 88 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 89 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 90 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 91 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 92 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 95 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 96 + github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 97 + github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 98 + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 99 + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 100 + github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 101 + github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 102 + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 103 + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 104 + github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 105 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 106 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 107 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 108 + github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 109 + github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 110 + github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= 111 + github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 112 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 113 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 114 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 115 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 116 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 117 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 118 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 119 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 120 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 121 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 122 + go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 123 + go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 124 + go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 125 + go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 126 + go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 127 + go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 128 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 129 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 130 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 131 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 132 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 133 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 134 + golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 135 + golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 136 + golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 137 + golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 138 + golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 139 + golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 140 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 141 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 142 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 143 + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 144 + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 145 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 146 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 148 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+3
home.html
··· 1 + {{ define "content" }} 2 + This is home! 3 + {{ end }}
+13
login.html
··· 1 + {{ define "content" }} 2 + <article> 3 + <h3>Login with atproto</h3> 4 + <form method="post"> 5 + <p>Provide your handle or DID to authorize an existing account with PDS. 6 + <br>You can also supply a PDS/entryway URL (eg, <code>https://pds.example.com</code>).</p> 7 + <fieldset role="group"> 8 + <input name="username" id="username" placeholder="handle.example.com" style="font-family: monospace,monospace;" required> 9 + <input type="submit" value="Login"> 10 + </fieldset> 11 + </form> 12 + </article> 13 + {{ end }}
+330
main.go
··· 1 + package main 2 + 3 + import ( 4 + _ "embed" 5 + "encoding/json" 6 + "fmt" 7 + "html/template" 8 + "log/slog" 9 + "net/http" 10 + "os" 11 + 12 + _ "github.com/joho/godotenv/autoload" 13 + 14 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 15 + "github.com/bluesky-social/indigo/atproto/crypto" 16 + "github.com/bluesky-social/indigo/atproto/identity" 17 + "github.com/bluesky-social/indigo/atproto/syntax" 18 + 19 + "github.com/gorilla/sessions" 20 + "github.com/urfave/cli/v2" 21 + ) 22 + 23 + func main() { 24 + app := cli.App{ 25 + Name: "oauth-web-demo", 26 + Usage: "atproto OAuth web server demo", 27 + Action: runServer, 28 + Flags: []cli.Flag{ 29 + &cli.StringFlag{ 30 + Name: "session-secret", 31 + Usage: "random string/token used for session cookie security", 32 + Required: true, 33 + EnvVars: []string{"SESSION_SECRET"}, 34 + }, 35 + &cli.StringFlag{ 36 + Name: "hostname", 37 + Usage: "public host name for this client (if not localhost dev mode)", 38 + EnvVars: []string{"CLIENT_HOSTNAME"}, 39 + }, 40 + &cli.StringFlag{ 41 + Name: "client-secret-key", 42 + Usage: "confidential client secret key. should be P-256 private key in multibase encoding", 43 + EnvVars: []string{"CLIENT_SECRET_KEY"}, 44 + }, 45 + &cli.StringFlag{ 46 + Name: "client-secret-key-id", 47 + Usage: "key id for client-secret-key", 48 + Value: "primary", 49 + EnvVars: []string{"CLIENT_SECRET_KEY_ID"}, 50 + }, 51 + }, 52 + } 53 + h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) 54 + slog.SetDefault(slog.New(h)) 55 + app.RunAndExitOnError() 56 + } 57 + 58 + type Server struct { 59 + CookieStore *sessions.CookieStore 60 + Dir identity.Directory 61 + OAuth *oauth.ClientApp 62 + } 63 + 64 + //go:embed "base.html" 65 + var tmplBaseText string 66 + 67 + //go:embed "home.html" 68 + var tmplHomeText string 69 + var tmplHome = template.Must(template.Must(template.New("home.html").Parse(tmplBaseText)).Parse(tmplHomeText)) 70 + 71 + //go:embed "login.html" 72 + var tmplLoginText string 73 + var tmplLogin = template.Must(template.Must(template.New("login.html").Parse(tmplBaseText)).Parse(tmplLoginText)) 74 + 75 + //go:embed "post.html" 76 + var tmplPostText string 77 + var tmplPost = template.Must(template.Must(template.New("post.html").Parse(tmplBaseText)).Parse(tmplPostText)) 78 + 79 + func (s *Server) Homepage(w http.ResponseWriter, r *http.Request) { 80 + tmplHome.Execute(w, nil) 81 + } 82 + 83 + func runServer(cctx *cli.Context) error { 84 + 85 + scope := "atproto transition:generic" 86 + bind := ":8080" 87 + 88 + // TODO: localhost dev mode if hostname is empty 89 + var config oauth.ClientConfig 90 + hostname := cctx.String("hostname") 91 + if hostname == "" { 92 + config = oauth.NewLocalhostConfig( 93 + fmt.Sprintf("http://127.0.0.1%s/oauth/callback", bind), 94 + scope, 95 + ) 96 + slog.Info("configuring localhost OAuth client", "CallbackURL", config.CallbackURL) 97 + } else { 98 + config = oauth.NewPublicConfig( 99 + fmt.Sprintf("https://%s/oauth/client-metadata.json", hostname), 100 + fmt.Sprintf("https://%s/oauth/callback", hostname), 101 + ) 102 + } 103 + 104 + // If a client secret key is provided (as a multibase string), turn this in to a confidential client 105 + if cctx.String("client-secret-key") != "" && hostname != "" { 106 + priv, err := crypto.ParsePrivateMultibase(cctx.String("client-secret-key")) 107 + if err != nil { 108 + return err 109 + } 110 + config.AddClientSecret(priv, cctx.String("client-secret-key-id")) 111 + slog.Info("configuring confidential OAuth client") 112 + } 113 + 114 + oauthClient := oauth.NewClientApp(&config, oauth.NewMemStore()) 115 + 116 + srv := Server{ 117 + CookieStore: sessions.NewCookieStore([]byte(cctx.String("session-secret"))), 118 + Dir: identity.DefaultDirectory(), 119 + OAuth: oauthClient, 120 + } 121 + 122 + http.HandleFunc("GET /", srv.Homepage) 123 + http.HandleFunc("GET /oauth/client-metadata.json", srv.ClientMetadata) 124 + http.HandleFunc("GET /oauth/jwks.json", srv.JWKS) 125 + http.HandleFunc("GET /oauth/login", srv.OAuthLogin) 126 + http.HandleFunc("POST /oauth/login", srv.OAuthLogin) 127 + http.HandleFunc("GET /oauth/callback", srv.OAuthCallback) 128 + http.HandleFunc("GET /oauth/refresh", srv.OAuthRefresh) 129 + http.HandleFunc("GET /oauth/logout", srv.OAuthLogout) 130 + http.HandleFunc("GET /bsky/post", srv.Post) 131 + http.HandleFunc("POST /bsky/post", srv.Post) 132 + 133 + slog.Info("starting http server", "bind", bind) 134 + if err := http.ListenAndServe(bind, nil); err != nil { 135 + slog.Error("http shutdown", "err", err) 136 + } 137 + return nil 138 + } 139 + 140 + func (s *Server) currentSessionDID(r *http.Request) *syntax.DID { 141 + sess, _ := s.CookieStore.Get(r, "oauth-demo") 142 + accountDID, ok := sess.Values["account_did"].(string) 143 + if !ok || accountDID == "" { 144 + return nil 145 + } 146 + did, err := syntax.ParseDID(accountDID) 147 + if err != nil { 148 + return nil 149 + } 150 + 151 + return &did 152 + } 153 + 154 + func strPtr(raw string) *string { 155 + return &raw 156 + } 157 + 158 + func (s *Server) ClientMetadata(w http.ResponseWriter, r *http.Request) { 159 + slog.Info("client metadata request", "url", r.URL, "host", r.Host) 160 + 161 + scope := "atproto transition:generic" 162 + meta := s.OAuth.Config.ClientMetadata(scope) 163 + if s.OAuth.Config.IsConfidential() { 164 + meta.JWKSUri = strPtr(fmt.Sprintf("https://%s/oauth/jwks.json", r.Host)) 165 + } 166 + meta.ClientName = strPtr("indigo atp-oauth-demo") 167 + meta.ClientURI = strPtr(fmt.Sprintf("https://%s", r.Host)) 168 + 169 + // internal consistency check 170 + if err := meta.Validate(s.OAuth.Config.ClientID); err != nil { 171 + slog.Error("validating client metadata", "err", err) 172 + http.Error(w, err.Error(), http.StatusInternalServerError) 173 + return 174 + } 175 + 176 + w.Header().Set("Content-Type", "application/json") 177 + if err := json.NewEncoder(w).Encode(meta); err != nil { 178 + http.Error(w, err.Error(), http.StatusInternalServerError) 179 + return 180 + } 181 + } 182 + 183 + func (s *Server) JWKS(w http.ResponseWriter, r *http.Request) { 184 + w.Header().Set("Content-Type", "application/json") 185 + body := s.OAuth.Config.PublicJWKS() 186 + if err := json.NewEncoder(w).Encode(body); err != nil { 187 + http.Error(w, err.Error(), http.StatusInternalServerError) 188 + return 189 + } 190 + } 191 + 192 + func (s *Server) OAuthLogin(w http.ResponseWriter, r *http.Request) { 193 + ctx := r.Context() 194 + 195 + if r.Method != "POST" { 196 + tmplLogin.Execute(w, nil) 197 + return 198 + } 199 + 200 + if err := r.ParseForm(); err != nil { 201 + http.Error(w, fmt.Errorf("parsing form data: %w", err).Error(), http.StatusBadRequest) 202 + return 203 + } 204 + 205 + username := r.PostFormValue("username") 206 + 207 + slog.Info("OAuthLogin", "client_id", s.OAuth.Config.ClientID, "callback_url", s.OAuth.Config.CallbackURL) 208 + 209 + redirectURL, err := s.OAuth.StartAuthFlow(ctx, username) 210 + if err != nil { 211 + http.Error(w, fmt.Errorf("OAuth login failed: %w", err).Error(), http.StatusBadRequest) 212 + return 213 + } 214 + 215 + http.Redirect(w, r, redirectURL, http.StatusFound) 216 + return 217 + } 218 + 219 + func (s *Server) OAuthCallback(w http.ResponseWriter, r *http.Request) { 220 + ctx := r.Context() 221 + 222 + params := r.URL.Query() 223 + slog.Info("received callback", "params", params) 224 + 225 + sessData, err := s.OAuth.ProcessCallback(ctx, r.URL.Query()) 226 + if err != nil { 227 + http.Error(w, fmt.Errorf("processing OAuth callback: %w", err).Error(), http.StatusBadRequest) 228 + return 229 + } 230 + 231 + // create signed cookie session, indicating account DID 232 + sess, _ := s.CookieStore.Get(r, "oauth-demo") 233 + sess.Values["account_did"] = sessData.AccountDID.String() 234 + if err := sess.Save(r, w); err != nil { 235 + http.Error(w, err.Error(), http.StatusInternalServerError) 236 + return 237 + } 238 + 239 + slog.Info("login successful", "did", sessData.AccountDID.String()) 240 + http.Redirect(w, r, "/bsky/post", http.StatusFound) 241 + } 242 + 243 + func (s *Server) OAuthRefresh(w http.ResponseWriter, r *http.Request) { 244 + ctx := r.Context() 245 + 246 + did := s.currentSessionDID(r) 247 + if did == nil { 248 + // TODO: suppowed to set a WWW header; and could redirect? 249 + http.Error(w, "not authenticated", http.StatusUnauthorized) 250 + return 251 + } 252 + 253 + oauthSess, err := s.OAuth.ResumeSession(ctx, *did) 254 + if err != nil { 255 + http.Error(w, "not authenticated", http.StatusUnauthorized) 256 + return 257 + } 258 + 259 + if err := oauthSess.RefreshTokens(ctx); err != nil { 260 + http.Error(w, err.Error(), http.StatusBadRequest) 261 + return 262 + } 263 + s.OAuth.Store.SaveSession(ctx, *oauthSess.Data) 264 + slog.Info("refreshed tokens") 265 + http.Redirect(w, r, "/", http.StatusFound) 266 + } 267 + 268 + func (s *Server) OAuthLogout(w http.ResponseWriter, r *http.Request) { 269 + // XXX: delete session from auth store 270 + 271 + // wipe all secure cookie session data 272 + sess, _ := s.CookieStore.Get(r, "oauth-demo") 273 + sess.Values = make(map[any]any) 274 + err := sess.Save(r, w) 275 + if err != nil { 276 + http.Error(w, err.Error(), http.StatusInternalServerError) 277 + return 278 + } 279 + slog.Info("logged out") 280 + http.Redirect(w, r, "/", http.StatusFound) 281 + } 282 + 283 + func (s *Server) Post(w http.ResponseWriter, r *http.Request) { 284 + ctx := r.Context() 285 + 286 + slog.Info("in post handler") 287 + 288 + if r.Method != "POST" { 289 + tmplPost.Execute(w, nil) 290 + return 291 + } 292 + 293 + did := s.currentSessionDID(r) 294 + if did == nil { 295 + // TODO: suppowed to set a WWW header; and could redirect? 296 + http.Error(w, "not authenticated", http.StatusUnauthorized) 297 + return 298 + } 299 + 300 + oauthSess, err := s.OAuth.ResumeSession(ctx, *did) 301 + if err != nil { 302 + http.Error(w, "not authenticated", http.StatusUnauthorized) 303 + return 304 + } 305 + c := oauthSess.APIClient() 306 + 307 + if err := r.ParseForm(); err != nil { 308 + http.Error(w, fmt.Errorf("parsing form data: %w", err).Error(), http.StatusBadRequest) 309 + return 310 + } 311 + text := r.PostFormValue("post_text") 312 + 313 + body := map[string]any{ 314 + "repo": c.AccountDID.String(), 315 + "collection": "app.bsky.feed.post", 316 + "record": map[string]any{ 317 + "$type": "app.bsky.feed.post", 318 + "text": text, 319 + "createdAt": syntax.DatetimeNow(), 320 + }, 321 + } 322 + 323 + slog.Info("attempting post...", "text", text) 324 + if err := c.Post(ctx, "com.atproto.repo.createRecord", body, nil); err != nil { 325 + http.Error(w, fmt.Errorf("posting failed: %w", err).Error(), http.StatusBadRequest) 326 + return 327 + } 328 + 329 + http.Redirect(w, r, "/bsky/post", http.StatusFound) 330 + }
+6
post.html
··· 1 + {{ define "content" }} 2 + <form method="post"> 3 + <textarea name="post_text" placeholder="What's up?" id="post_text" required></textarea> 4 + <input type="submit" value="Poast!"> 5 + </form> 6 + {{ end }}