this repo has no description

add redirection to pds

Changed files
+386 -19
cmd
+1
.gitignore
···
cmd/client_test/client_test
jwks.json
.env
+
oauth.db
+9
cmd/client_test/html/index.html
···
+
<!doctype html>
+
<html>
+
<p>welcome to atproto oauth golang tester!</p>
+
<ul>
+
<li>
+
<a href="/login">Login</a>
+
</li>
+
</ul>
+
</html>
+7
cmd/client_test/html/login.html
···
+
<!doctype html>
+
<html>
+
<form action="/login" method="post">
+
<input name="handle" id="handle" placeholder="Enter your handle" />
+
<button type="submit" value="Submit">Submit</button>
+
</form>
+
</html>
+276 -2
cmd/client_test/main.go
···
import (
"context"
+
"encoding/json"
"fmt"
+
"io"
"log/slog"
+
"net"
"net/http"
+
"net/url"
"os"
+
"strings"
+
"github.com/bluesky-social/indigo/atproto/syntax"
oauth "github.com/haileyok/atproto-oauth-golang"
_ "github.com/joho/godotenv/autoload"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwk"
slogecho "github.com/samber/slog-echo"
"github.com/urfave/cli/v2"
+
"gorm.io/driver/sqlite"
+
"gorm.io/gorm"
)
var (
ctx = context.Background()
serverAddr = os.Getenv("OAUTH_TEST_SERVER_ADDR")
serverUrlRoot = os.Getenv("OAUTH_TEST_SERVER_URL_ROOT")
+
staticFilePath = os.Getenv("OAUTH_TEST_SERVER_STATIC_PATH")
serverMetadataUrl = fmt.Sprintf("%s/oauth/client-metadata.json", serverUrlRoot)
serverCallbackUrl = fmt.Sprintf("%s/callback", serverUrlRoot)
pdsUrl = os.Getenv("OAUTH_TEST_PDS_URL")
+
scope = "atproto transition:generic"
)
func main() {
···
type TestServer struct {
httpd *http.Server
e *echo.Echo
+
db *gorm.DB
+
oauthClient *oauth.OauthClient
jwksResponse *oauth.JwksResponseObject
}
···
b, err := os.ReadFile("./jwks.json")
if err != nil {
if os.IsNotExist(err) {
-
return nil, fmt.Errorf("could not find jwks.json. does it exist? hint: run `go run ./cmd/cmd generate-jwks --prefix demo` to create one.")
+
return nil, fmt.Errorf(
+
"could not find jwks.json. does it exist? hint: run `go run ./cmd/cmd generate-jwks --prefix demo` to create one.",
+
)
}
return nil, err
}
···
return nil, err
}
+
c, err := oauth.NewOauthClient(oauth.OauthClientArgs{
+
ClientJwk: k,
+
ClientId: serverMetadataUrl,
+
RedirectUri: serverCallbackUrl,
+
})
+
if err != nil {
+
return nil, err
+
}
+
httpd := &http.Server{
Addr: serverAddr,
Handler: e,
}
-
fmt.Println("starting http server...")
+
db, err := gorm.Open(sqlite.Open("oauth.db"), &gorm.Config{})
+
if err != nil {
+
return nil, err
+
}
+
+
db.AutoMigrate(&OauthRequest{})
return &TestServer{
httpd: httpd,
e: e,
+
db: db,
+
oauthClient: c,
jwksResponse: oauth.CreateJwksResponseObject(pubKey),
}, nil
}
func (s *TestServer) run() error {
+
s.e.File("/", s.getFilePath("index.html"))
+
s.e.File("/login", s.getFilePath("login.html"))
+
s.e.POST("/login", s.handleLoginSubmit)
s.e.GET("/oauth/client-metadata.json", s.handleClientMetadata)
s.e.GET("/oauth/jwks.json", s.handleJwks)
···
func (s *TestServer) handleJwks(e echo.Context) error {
return e.JSON(200, s.jwksResponse)
}
+
+
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
+
handle := e.FormValue("handle")
+
if handle == "" {
+
return e.Redirect(302, "/login?e=handle-empty")
+
}
+
+
_, herr := syntax.ParseHandle(handle)
+
_, derr := syntax.ParseDID(handle)
+
+
if herr != nil && derr != nil {
+
return e.Redirect(302, "/login?e=handle-invalid")
+
}
+
+
var did string
+
+
if derr == nil {
+
did = handle
+
} else {
+
maybeDid, err := resolveHandle(e.Request().Context(), handle)
+
if err != nil {
+
return err
+
}
+
+
did = maybeDid
+
}
+
+
service, err := resolveService(ctx, did)
+
if err != nil {
+
return err
+
}
+
+
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
+
if err != nil {
+
return err
+
}
+
+
meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver)
+
if err != nil {
+
return err
+
}
+
+
dpopPrivateKey, err := oauth.GenerateKey(nil)
+
if err != nil {
+
return err
+
}
+
+
dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey)
+
if err != nil {
+
return err
+
}
+
+
parResp, err := s.oauthClient.SendParAuthRequest(
+
ctx,
+
authserver,
+
meta,
+
"",
+
scope,
+
dpopPrivateKey,
+
)
+
+
oauthRequest := OauthRequest{
+
State: "",
+
AuthserverIss: meta.Issuer,
+
Did: did,
+
PdsUrl: service,
+
PkceVerifier: parResp.PkceVerifier,
+
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
+
DpopPrivateJwk: string(dpopPrivateKeyJson),
+
}
+
+
if err := s.db.Create(&oauthRequest).Error; err != nil {
+
return err
+
}
+
+
u, _ := url.Parse(meta.AuthorizationEndpoint)
+
u.RawQuery = fmt.Sprintf(
+
"client_id=%s&request_uri=%s",
+
url.QueryEscape(serverMetadataUrl),
+
parResp.Resp["request_uri"].(string),
+
)
+
+
return e.Redirect(302, u.String())
+
}
+
+
func resolveHandle(ctx context.Context, handle string) (string, error) {
+
var did string
+
+
_, err := syntax.ParseHandle(handle)
+
if err != nil {
+
return "", err
+
}
+
+
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
+
if err != nil {
+
return "", err
+
}
+
+
for _, rec := range recs {
+
if strings.HasPrefix(rec, "did=") {
+
did = strings.Split(rec, "did=")[1]
+
break
+
}
+
}
+
+
if did == "" {
+
req, err := http.NewRequestWithContext(
+
ctx,
+
"GET",
+
fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
+
nil,
+
)
+
if err != nil {
+
return "", err
+
}
+
+
resp, err := http.DefaultClient.Do(req)
+
if err != nil {
+
return "", err
+
}
+
defer resp.Body.Close()
+
+
if resp.StatusCode != http.StatusOK {
+
io.Copy(io.Discard, resp.Body)
+
return "", fmt.Errorf("unable to resolve handle")
+
}
+
+
b, err := io.ReadAll(resp.Body)
+
if err != nil {
+
return "", err
+
}
+
+
maybeDid := string(b)
+
+
if _, err := syntax.ParseDID(maybeDid); err != nil {
+
return "", fmt.Errorf("unable to resolve handle")
+
}
+
+
did = maybeDid
+
}
+
+
// TODO: we can also support did:web here
+
+
if did == "" {
+
return "", fmt.Errorf("unable to resolve handle")
+
}
+
+
return did, nil
+
}
+
+
func resolveService(ctx context.Context, did string) (string, error) {
+
type Identity struct {
+
Service []struct {
+
ID string `json:"id"`
+
Type string `json:"type"`
+
ServiceEndpoint string `json:"serviceEndpoint"`
+
} `json:"service"`
+
}
+
+
if strings.HasPrefix(did, "did:plc:") {
+
req, err := http.NewRequestWithContext(
+
ctx,
+
"GET",
+
fmt.Sprintf("https://plc.directory/%s", did),
+
nil,
+
)
+
if err != nil {
+
return "", err
+
}
+
+
resp, err := http.DefaultClient.Do(req)
+
if err != nil {
+
return "", err
+
}
+
defer resp.Body.Close()
+
+
if resp.StatusCode != 200 {
+
io.Copy(io.Discard, resp.Body)
+
return "", fmt.Errorf("could not find identity in plc registry")
+
}
+
+
var identity Identity
+
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
+
return "", err
+
}
+
+
var service string
+
for _, svc := range identity.Service {
+
if svc.ID == "#atproto_pds" {
+
service = svc.ServiceEndpoint
+
}
+
}
+
+
if service == "" {
+
return "", fmt.Errorf("could not find atproto_pds service in identity services")
+
}
+
+
return service, nil
+
} else if strings.HasPrefix(did, "did:web:") {
+
// TODO: needs more work
+
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/.well-known/did.json", did), nil)
+
if err != nil {
+
return "", err
+
}
+
+
resp, err := http.DefaultClient.Do(req)
+
if err != nil {
+
return "", err
+
}
+
defer resp.Body.Close()
+
+
if resp.StatusCode != 200 {
+
io.Copy(io.Discard, resp.Body)
+
return "", fmt.Errorf("could not find identity in plc registry")
+
}
+
+
var identity Identity
+
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
+
return "", err
+
}
+
+
var service string
+
for _, svc := range identity.Service {
+
if svc.ID == "#atproto_pds" {
+
service = svc.ServiceEndpoint
+
}
+
}
+
+
if service == "" {
+
return "", fmt.Errorf("could not find atproto_pds service in identity services")
+
}
+
+
return service, nil
+
} else {
+
return "", fmt.Errorf("did was not a supported did type")
+
}
+
}
+
+
func (s *TestServer) getFilePath(file string) string {
+
return fmt.Sprintf("%s/%s", staticFilePath, file)
+
}
+12
cmd/client_test/types.go
···
+
package main
+
+
type OauthRequest struct {
+
ID uint
+
AuthserverIss string
+
State string
+
Did string `gorm:"index"`
+
PdsUrl string
+
PkceVerifier string
+
DpopAuthserverNonce string
+
DpopPrivateJwk string
+
}
+8 -3
go.mod
···
go 1.24.0
require (
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
+
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.13.3
github.com/lestrrat-go/jwx/v2 v2.0.12
+
github.com/samber/slog-echo v1.15.1
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.5
+
gorm.io/driver/sqlite v1.5.7
+
gorm.io/gorm v1.25.9
)
require (
···
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/joho/godotenv v1.5.1 // indirect
-
github.com/kr/pretty v0.3.1 // indirect
+
github.com/jinzhu/inflection v1.0.0 // indirect
+
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // 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/mattn/go-sqlite3 v1.14.22 // 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
+14 -3
go.sum
···
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk=
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
···
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
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/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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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=
···
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=
+
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
+
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+59 -11
oauth.go
···
return resource.AuthorizationServers[0], nil
}
-
func (c *OauthClient) FetchAuthServerMetadata(ctx context.Context, ustr string) (*OauthAuthorizationMetadata, error) {
+
func (c *OauthClient) FetchAuthServerMetadata(
+
ctx context.Context,
+
ustr string,
+
) (*OauthAuthorizationMetadata, error) {
u, err := isSafeAndParsed(ustr)
if err != nil {
return nil, err
···
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
-
return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode)
+
return nil, fmt.Errorf(
+
"received non-200 response from pds. status code was %d",
+
resp.StatusCode,
+
)
}
b, err := io.ReadAll(resp.Body)
···
return tokenString, nil
}
-
func (c *OauthClient) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) {
+
func (c *OauthClient) AuthServerDpopJwt(
+
method, url, nonce string,
+
privateJwk jwk.Key,
+
) (string, error) {
pubJwk, err := privateJwk.PublicKey()
if err != nil {
return "", err
···
Resp map[string]any
}
-
func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
+
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
}
-
req2, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
+
req2, err := http.NewRequestWithContext(
+
ctx,
+
"POST",
+
parUrl,
+
strings.NewReader(params.Encode()),
+
)
if err != nil {
return nil, err
}
···
Resp map[string]string
}
-
func (c *OauthClient) InitialTokenRequest(ctx context.Context, authRequest map[string]string, code, appUrl string) (*TokenResponse, error) {
+
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
}
-
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, authRequest["dpop_authserver_nonce"], dpopPrivateJwk)
+
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()))
+
req, err := http.NewRequestWithContext(
+
ctx,
+
"POST",
+
authserverMeta.TokenEndpoint,
+
strings.NewReader(params.Encode()),
+
)
if err != nil {
return nil, err
}
···
DpopAuthserverNonce string
}
-
func (c *OauthClient) RefreshTokenRequest(ctx context.Context, args RefreshTokenArgs, appUrl string) (any, error) {
+
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
···
return nil, err
}
-
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, args.DpopAuthserverNonce, dpopPrivateJwk)
+
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()))
+
req, err := http.NewRequestWithContext(
+
ctx,
+
"POST",
+
authserverMeta.TokenEndpoint,
+
strings.NewReader(params.Encode()),
+
)
if err != nil {
return nil, err
}