this repo has no description

lots of progress

+3
.env.example
···
+
OAUTH_TEST_SERVER_ADDR=":7070"
+
OAUTH_TEST_SERVER_URL_ROOT="http://localhost:7070"
+
OAUTH_TEST_PDS_URL="https://pds.haileyok.com"
+2
.gitignore
···
+
cmd/client_test/client_test
+
.env
+4
Makefile
···
check: ## Compile everything, checking syntax (does not output binaries)
go build ./...
+
.PHONY: test-server
+
test-server:
+
go run ./cmd/client_test
+
.env:
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+65
cmd/client_test/main.go
···
+
package main
+
+
import (
+
"fmt"
+
"log/slog"
+
"net/http"
+
+
"github.com/labstack/echo/v4"
+
slogecho "github.com/samber/slog-echo"
+
"github.com/urfave/cli/v2"
+
)
+
+
func main() {
+
app := &cli.App{
+
Name: "atproto-oauth-golang-tester",
+
Action: run,
+
}
+
+
app.RunAndExitOnError()
+
}
+
+
func run(cmd *cli.Context) error {
+
e := echo.New()
+
+
e.Use(slogecho.New(slog.Default()))
+
+
fmt.Println("atproto oauth golang tester server")
+
+
e.GET("/oauth/client-metadata.json", handleClientMetadata)
+
+
httpd := http.Server{
+
Addr: ":7070",
+
Handler: e,
+
}
+
+
fmt.Println("starting http server...")
+
+
if err := httpd.ListenAndServe(); err != nil {
+
return err
+
}
+
+
return nil
+
}
+
+
func handleClientMetadata(e echo.Context) error {
+
e.Response().Header().Add("Content-Type", "application/json")
+
+
metadata := map[string]any{
+
"client_id": "http://localhost:7070/oauth/oauth-metadata.json",
+
"client_name": "Atproto Oauth Golang Tester",
+
"client_uri": "http://localhost:7070",
+
"logo_uri": "http://localhost:7070/logo.png",
+
"tos_uri": "http://localhost:7070/tos",
+
"policy_url": "http://localhost:7070/policy",
+
"redirect_uris": []string{"http://localhost:7070/callback"},
+
"grant_types": []string{"authorization_code", "refresh_token"},
+
"response_types": []string{"code"},
+
"application_type": "web",
+
"token_endpoint_auth_method": "private_key_jwt",
+
"dpop_bound_accesss_tokens": true,
+
"jwks_uri": "http://localhost:7070/jwks.json",
+
}
+
+
return e.JSON(200, metadata)
+
}
+17 -11
go.mod
···
require (
github.com/golang-jwt/jwt/v5 v5.2.1
-
github.com/google/uuid v1.4.0
+
github.com/google/uuid v1.6.0
+
github.com/labstack/echo/v4 v4.13.3
github.com/lestrrat-go/jwx/v2 v2.0.12
github.com/stretchr/testify v1.10.0
+
github.com/urfave/cli/v2 v2.27.5
)
require (
-
github.com/bluesky-social/indigo v0.0.0-20250222003125-2503553ea604 // indirect
-
github.com/davecgh/go-spew v1.1.1 // indirect
+
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
-
github.com/gorilla/context v1.1.2 // indirect
-
github.com/gorilla/securecookie v1.1.2 // indirect
-
github.com/gorilla/sessions v1.4.0 // indirect
-
github.com/labstack/echo-contrib v0.17.2 // indirect
-
github.com/labstack/echo/v4 v4.13.3 // indirect
+
github.com/joho/godotenv v1.5.1 // indirect
+
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
-
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
-
github.com/pmezard/go-difflib v1.0.0 // indirect
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+
github.com/rogpeppe/go-internal v1.13.1 // indirect
+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
+
github.com/samber/lo v1.47.0 // indirect
+
github.com/samber/slog-echo v1.15.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
+
go.opentelemetry.io/otel v1.29.0 // indirect
+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
-
golang.org/x/time v0.8.0 // indirect
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+40 -18
go.sum
···
-
github.com/bluesky-social/indigo v0.0.0-20250222003125-2503553ea604 h1:rceaPCufVEkobTmyISJhvY4kzaPKtSujN427CGWpvHw=
-
github.com/bluesky-social/indigo v0.0.0-20250222003125-2503553ea604/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA=
+
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
+
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
-
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
-
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
-
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
-
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
-
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
-
github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w=
-
github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E=
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
-
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
···
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
+
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+
github.com/samber/slog-echo v1.15.1 h1:mzeQNPYPxmpehIRtgQJRgJMVvrRbZHp5D2maxSljTBw=
+
github.com/samber/slog-echo v1.15.1/go.mod h1:K21nbusPmai/MYm8PFactmZoFctkMmkeaTdXXyvhY1c=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
···
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
+
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
···
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
-
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+181 -20
oauth.go
···
package oauth
import (
-
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
···
"fmt"
"io"
"net/http"
+
"net/url"
+
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
···
return resource.AuthorizationServers[0], nil
}
-
func (c *OauthClient) FetchAuthServerMetadata(ctx context.Context, ustr string) (any, error) {
+
func (c *OauthClient) FetchAuthServerMetadata(ctx context.Context, ustr string) (*OauthAuthorizationMetadata, error) {
u, err := isSafeAndParsed(ustr)
if err != nil {
return nil, err
···
return nil, fmt.Errorf("could not validate metadata: %w", err)
}
-
return metadata, nil
+
return &metadata, nil
}
func (c *OauthClient) ClientAssertionJwt(authServerUrl string) (string, error) {
···
return tokenString, nil
}
-
func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (any, error) {
+
type SendParAuthResponse struct {
+
PkceVerifier string
+
State string
+
DpopAuthserverNonce string
+
Resp map[string]string
+
}
+
+
func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
if authServerMeta == nil {
return nil, fmt.Errorf("nil metadata provided")
}
···
return nil, err
}
-
parBody := map[string]string{
-
"response_type": "code",
-
"code_challenge": codeChallenge,
-
"code_challenge_method": codeChallengeMethod,
-
"client_id": c.clientId,
-
"state": state,
-
"redirect_uri": c.redirectUri,
-
"scope": scope,
-
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
-
"client_assertion": clientAssertion,
+
params := url.Values{
+
"response_type": {"code"},
+
"code_challenge": {codeChallenge},
+
"code_challenge_method": {codeChallengeMethod},
+
"client_id": {c.clientId},
+
"state": {state},
+
"redirect_uri": {c.redirectUri},
+
"scope": {scope},
+
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
+
"client_assertion": {clientAssertion},
}
if loginHint != "" {
-
parBody["login_hint"] = loginHint
+
params.Set("login_hint", loginHint)
}
_, err = isSafeAndParsed(parUrl)
···
return nil, err
}
-
b, err := json.Marshal(parBody)
+
req, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
+
if err != nil {
+
return nil, err
+
}
+
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
req.Header.Set("DPoP", dpopProof)
+
+
resp, err := c.h.Do(req)
+
if err != nil {
+
return nil, err
+
}
+
defer resp.Body.Close()
+
+
var rmap map[string]string
+
if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
+
return nil, err
+
}
+
+
// TODO: there's some logic in the flask example where we retry if the server
+
// asks us to use a dpop nonce. we should add that here eventually, but for now
+
// we'll skip that
+
+
return &SendParAuthResponse{
+
PkceVerifier: pkceVerifier,
+
State: state,
+
DpopAuthserverNonce: "", // add here later
+
Resp: rmap,
+
}, nil
+
}
+
+
type TokenResponse struct {
+
DpopAuthserverNonce string
+
Resp map[string]string
+
}
+
+
func (c *OauthClient) InitialTokenRequest(ctx context.Context, authRequest map[string]string, code, appUrl string) (*TokenResponse, error) {
+
authserverUrl := authRequest["authserver_iss"]
+
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverUrl)
+
if err != nil {
+
return nil, err
+
}
+
+
clientAssertion, err := c.ClientAssertionJwt(authserverUrl)
+
if err != nil {
+
return nil, err
+
}
+
+
params := url.Values{
+
"client_id": {c.clientId},
+
"redirect_uri": {c.redirectUri},
+
"grant_type": {"authorization_code"},
+
"code": {code},
+
"code_verifier": {authRequest["pkce_verifier"]},
+
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
+
"client_assertion": {clientAssertion},
+
}
+
+
dpopPrivateJwk, err := parsePrivateJwkFromString(authRequest["dpop_private_jwk"])
+
if err != nil {
+
return nil, err
+
}
+
+
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, authRequest["dpop_authserver_nonce"], dpopPrivateJwk)
+
if err != nil {
+
return nil, err
+
}
+
+
dpopAuthserverNonce := authRequest["dpop_authserver_nonce"]
+
+
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
if err != nil {
return nil, err
}
-
req, err := http.NewRequestWithContext(ctx, "POST", parUrl, bytes.NewReader(b))
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
req.Header.Set("DPoP", dpopProof)
+
+
resp, err := c.h.Do(req)
if err != nil {
return nil, err
}
+
defer resp.Body.Close()
-
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-
req.Header.Add("DPoP", dpopProof)
+
// TODO: use nonce if needed, same as in par
-
return nil, nil
+
var rmap map[string]string
+
if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
+
return nil, err
+
}
+
+
return &TokenResponse{
+
DpopAuthserverNonce: dpopAuthserverNonce,
+
Resp: rmap,
+
}, nil
+
}
+
+
type RefreshTokenArgs struct {
+
AuthserverUrl string
+
RefreshToken string
+
DpopPrivateJwk string
+
DpopAuthserverNonce string
+
}
+
+
func (c *OauthClient) RefreshTokenRequest(ctx context.Context, args RefreshTokenArgs, appUrl string) (any, error) {
+
authserverMeta, err := c.FetchAuthServerMetadata(ctx, args.AuthserverUrl)
+
if err != nil {
+
return nil, err
+
}
+
+
clientAssertion, err := c.ClientAssertionJwt(args.AuthserverUrl)
+
if err != nil {
+
return nil, err
+
}
+
+
params := url.Values{
+
"client_id": {c.clientId},
+
"grant_type": {"refresh_token"},
+
"refresh_token": {args.RefreshToken},
+
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
+
"client_assertion": {clientAssertion},
+
}
+
+
dpopPrivateJwk, err := parsePrivateJwkFromString(args.DpopPrivateJwk)
+
if err != nil {
+
return nil, err
+
}
+
+
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, args.DpopAuthserverNonce, dpopPrivateJwk)
+
if err != nil {
+
return nil, err
+
}
+
+
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
+
if err != nil {
+
return nil, err
+
}
+
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
req.Header.Set("DPoP", dpopProof)
+
+
resp, err := c.h.Do(req)
+
if err != nil {
+
return nil, err
+
}
+
defer resp.Body.Close()
+
+
// TODO: handle same thing as above...
+
+
if resp.StatusCode != 200 && resp.StatusCode != 201 {
+
b, _ := io.ReadAll(resp.Body)
+
return nil, fmt.Errorf("token refresh error: %s", string(b))
+
}
+
+
var rmap map[string]string
+
if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
+
return nil, err
+
}
+
+
return &TokenResponse{
+
DpopAuthserverNonce: args.DpopAuthserverNonce,
+
Resp: rmap,
+
}, nil
}
func generateToken(len int) (string, error) {
···
hash := h.Sum(nil)
return base64.RawURLEncoding.EncodeToString(hash)
}
+
+
func parsePrivateJwkFromString(str string) (jwk.Key, error) {
+
return jwk.ParseKey([]byte(str))
+
}
+33 -7
oauth_test.go
···
import (
"context"
"encoding/json"
+
"fmt"
+
"io"
+
"net/http"
+
"os"
"testing"
+
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/assert"
)
var (
-
ctx = context.Background()
-
oauthClient = newTestOauthClient()
+
ctx = context.Background()
+
oauthClient = newTestOauthClient()
+
serverUrlRoot = os.Getenv("OAUTH_TEST_SERVER_URL_ROOT")
+
serverMetadataUrl = fmt.Sprintf("%s/oauth/client-metadata.json", serverUrlRoot)
+
serverCallbackUrl = fmt.Sprintf("%s/callback", serverUrlRoot)
+
pdsUrl = os.Getenv("OAUTH_TEST_PDS_URL")
)
func newTestOauthClient() *OauthClient {
···
}
c, err := NewOauthClient(OauthClientArgs{
-
ClientJwk: b,
+
ClientJwk: b,
+
ClientId: serverMetadataUrl,
+
RedirectUri: serverCallbackUrl,
})
if err != nil {
panic(err)
}
+
// make sure the server is running
+
+
req, err := http.NewRequest("GET", serverMetadataUrl, nil)
+
if err != nil {
+
panic(err)
+
}
+
+
resp, err := http.DefaultClient.Do(req)
+
if err != nil {
+
panic(fmt.Errorf("could not connect to test server. are you sure you started it?"))
+
}
+
defer resp.Body.Close()
+
+
io.Copy(io.Discard, resp.Body)
+
return c
}
func TestResolvePDSAuthServer(t *testing.T) {
assert := assert.New(t)
-
authServer, err := oauthClient.ResolvePDSAuthServer(ctx, "https://pds.haileyok.com")
+
authServer, err := oauthClient.ResolvePDSAuthServer(ctx, pdsUrl)
assert.NoError(err)
assert.NotEmpty(authServer)
-
assert.Equal("https://pds.haileyok.com", authServer)
+
assert.Equal(pdsUrl, authServer)
}
func TestFetchAuthServerMetadata(t *testing.T) {
assert := assert.New(t)
-
meta, err := oauthClient.FetchAuthServerMetadata(ctx, "https://pds.haileyok.com")
+
meta, err := oauthClient.FetchAuthServerMetadata(ctx, pdsUrl)
assert.NoError(err)
-
assert.IsType(OauthAuthorizationMetadata{}, meta)
+
assert.IsType(&OauthAuthorizationMetadata{}, meta)
}
func TestGenerateKey(t *testing.T) {