+1
.gitignore
+1
.gitignore
+45
Makefile
+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
+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
+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
+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=
+13
login.html
+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
+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
+
}