this repo has no description

par request working

+1 -1
.env.example
···
OAUTH_TEST_SERVER_ADDR=":7070"
-
OAUTH_TEST_SERVER_URL_ROOT="http://localhost:7070"
+
OAUTH_TEST_SERVER_URL_ROOT="http://127.0.0.1:7070"
OAUTH_TEST_PDS_URL="https://pds.haileyok.com"
+1
.gitignore
···
cmd/client_test/client_test
+
jwks.json
.env
+6 -7
Makefile
···
.PHONY: test
test: ## Run tests
-
go test -v ./...
-
-
.PHONY: coverage-html
-
coverage-html: ## Generate test coverage report and open in browser
-
go test ./... -coverpkg=./... -coverprofile=test-coverage.out
-
go tool cover -html=test-coverage.out
+
go clean -testcache && go test -v ./...
.PHONY: lint
lint: ## Verify code style and run static checks
···
go build ./...
.PHONY: test-server
-
test-server:
+
test-server: ## Run the test server
go run ./cmd/client_test
+
+
.PHONY: test-jwks
+
test-jwks: ## Create a test jwks file
+
go run ./cmd/cmd generate-jwks --prefix demo
.env:
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+86 -20
cmd/client_test/main.go
···
package main
import (
+
"context"
"fmt"
"log/slog"
"net/http"
+
"os"
+
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"
+
)
+
+
var (
+
ctx = context.Background()
+
serverAddr = os.Getenv("OAUTH_TEST_SERVER_ADDR")
+
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 main() {
···
Action: run,
}
+
if serverUrlRoot == "" {
+
panic(fmt.Errorf("no server url root set in env file"))
+
}
+
app.RunAndExitOnError()
}
+
type TestServer struct {
+
httpd *http.Server
+
e *echo.Echo
+
jwksResponse *oauth.JwksResponseObject
+
}
+
func run(cmd *cli.Context) error {
+
s, err := NewServer()
+
if err != nil {
+
panic(err)
+
}
+
+
s.run()
+
+
return nil
+
}
+
+
func NewServer() (*TestServer, error) {
e := echo.New()
e.Use(slogecho.New(slog.Default()))
fmt.Println("atproto oauth golang tester server")
-
e.GET("/oauth/client-metadata.json", handleClientMetadata)
+
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, err
+
}
-
httpd := http.Server{
-
Addr: ":7070",
+
k, err := jwk.ParseKey(b)
+
if err != nil {
+
return nil, err
+
}
+
+
pubKey, err := k.PublicKey()
+
if err != nil {
+
return nil, err
+
}
+
+
httpd := &http.Server{
+
Addr: serverAddr,
Handler: e,
}
fmt.Println("starting http server...")
-
if err := httpd.ListenAndServe(); err != nil {
+
return &TestServer{
+
httpd: httpd,
+
e: e,
+
jwksResponse: oauth.CreateJwksResponseObject(pubKey),
+
}, nil
+
}
+
+
func (s *TestServer) run() error {
+
s.e.GET("/oauth/client-metadata.json", s.handleClientMetadata)
+
s.e.GET("/oauth/jwks.json", s.handleJwks)
+
+
if err := s.httpd.ListenAndServe(); err != nil {
return err
}
return nil
}
-
func handleClientMetadata(e echo.Context) error {
-
e.Response().Header().Add("Content-Type", "application/json")
-
+
func (s *TestServer) handleClientMetadata(e echo.Context) error {
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",
+
"client_id": serverMetadataUrl,
+
"client_name": "Atproto Oauth Golang Tester",
+
"client_uri": serverUrlRoot,
+
"logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot),
+
"tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot),
+
"policy_url": fmt.Sprintf("%s/policy", serverUrlRoot),
+
"redirect_uris": []string{serverCallbackUrl},
+
"grant_types": []string{"authorization_code", "refresh_token"},
+
"response_types": []string{"code"},
+
"application_type": "web",
+
"dpop_bound_access_tokens": true,
+
"jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot),
+
"scope": "atproto transition:generic",
+
"token_endpoint_auth_method": "private_key_jwt",
+
"token_endpoint_auth_signing_alg": "ES256",
}
return e.JSON(200, metadata)
}
+
+
func (s *TestServer) handleJwks(e echo.Context) error {
+
return e.JSON(200, s.jwksResponse)
+
}
+52
cmd/cmd/main.go
···
+
package main
+
+
import (
+
"encoding/json"
+
"os"
+
+
oauth "github.com/haileyok/atproto-oauth-golang"
+
"github.com/urfave/cli/v2"
+
)
+
+
func main() {
+
app := &cli.App{
+
Name: "Atproto Oauth Golang Helper",
+
Commands: []*cli.Command{
+
runGenerateJwks,
+
},
+
}
+
+
app.RunAndExitOnError()
+
}
+
+
var runGenerateJwks = &cli.Command{
+
Name: "generate-jwks",
+
Flags: []cli.Flag{
+
&cli.StringFlag{
+
Name: "prefix",
+
Required: false,
+
},
+
},
+
Action: func(cmd *cli.Context) error {
+
var prefix *string
+
if cmd.String("prefix") != "" {
+
inputPrefix := cmd.String("prefix")
+
prefix = &inputPrefix
+
}
+
key, err := oauth.GenerateKey(prefix)
+
if err != nil {
+
return err
+
}
+
+
b, err := json.Marshal(key)
+
if err != nil {
+
return err
+
}
+
+
if err := os.WriteFile("./jwks.json", b, 0644); err != nil {
+
return err
+
}
+
+
return nil
+
},
+
}
+21 -4
generic.go
···
return nil, err
}
+
var kid string
if kidPrefix != nil {
-
kid := fmt.Sprintf("%s-%d", *kidPrefix, time.Now().Unix())
+
kid = fmt.Sprintf("%s-%d", *kidPrefix, time.Now().Unix())
-
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
-
return nil, err
-
}
+
} else {
+
kid = fmt.Sprintf("%d", time.Now().Unix())
}
+
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
+
return nil, err
+
}
return key, nil
}
···
return &pkey, nil
}
+
+
type JwksResponseObject struct {
+
Keys []jwk.Key `json:"keys"`
+
}
+
+
func CreateJwksResponseObject(key jwk.Key) *JwksResponseObject {
+
return &JwksResponseObject{
+
Keys: []jwk.Key{key},
+
}
+
}
+
+
func ParseKeyFromBytes(b []byte) (jwk.Key, error) {
+
return jwk.ParseKey(b)
+
}
+38 -25
oauth.go
···
type OauthClientArgs struct {
H *http.Client
-
ClientJwk []byte
+
ClientJwk jwk.Key
ClientId string
RedirectUri string
}
···
}
}
-
clientJwk, err := jwk.ParseKey(args.ClientJwk)
-
if err != nil {
-
return nil, err
-
}
-
-
clientPkey, err := getPrivateKey(clientJwk)
+
clientPkey, err := getPrivateKey(args.ClientJwk)
if err != nil {
return nil, fmt.Errorf("could not load private key from provided client jwk: %w", err)
}
-
kid := clientJwk.KeyID()
+
kid := args.ClientJwk.KeyID()
return &OauthClient{
h: args.H,
···
}
func (c *OauthClient) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) {
-
raw, err := jwk.PublicKeyOf(privateJwk)
-
if err != nil {
-
return "", err
-
}
-
-
pubJwk, err := jwk.FromRaw(raw)
+
pubJwk, err := privateJwk.PublicKey()
if err != nil {
return "", err
}
···
return "", err
}
-
var pubMap map[string]interface{}
+
var pubMap map[string]any
if err := json.Unmarshal(b, &pubMap); err != nil {
return "", err
}
···
token.Header["alg"] = "ES256"
token.Header["jwk"] = pubMap
-
var rawKey interface{}
+
var rawKey any
if err := privateJwk.Raw(&rawKey); err != nil {
return "", err
}
···
PkceVerifier string
State string
DpopAuthserverNonce string
-
Resp map[string]string
+
Resp map[string]any
}
func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
···
}
// TODO: ??
-
nonce := ""
-
dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, nonce, dpopPrivateKey)
+
dpopAuthserverNonce := ""
+
dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
if err != nil {
-
return nil, err
+
return nil, fmt.Errorf("error getting dpop proof: %w", err)
}
params := url.Values{
···
}
defer resp.Body.Close()
-
var rmap map[string]string
+
var rmap map[string]any
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
+
if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" {
+
dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
+
dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
+
if err != nil {
+
return nil, err
+
}
+
+
req2, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
+
if err != nil {
+
return nil, err
+
}
+
+
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
req2.Header.Set("DPoP", dpopProof)
+
+
resp2, err := c.h.Do(req2)
+
if err != nil {
+
return nil, err
+
}
+
defer resp2.Body.Close()
+
+
rmap = map[string]any{}
+
if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil {
+
return nil, err
+
}
+
}
return &SendParAuthResponse{
PkceVerifier: pkceVerifier,
State: state,
-
DpopAuthserverNonce: "", // add here later
+
DpopAuthserverNonce: dpopAuthserverNonce,
Resp: rmap,
}, nil
}
+28 -5
oauth_test.go
···
import (
"context"
-
"encoding/json"
"fmt"
"io"
"net/http"
···
)
func newTestOauthClient() *OauthClient {
-
prefix := "testing"
-
testKey, err := GenerateKey(&prefix)
+
b, err := os.ReadFile("./jwks.json")
if err != nil {
panic(err)
}
-
b, err := json.Marshal(testKey)
+
k, err := ParseKeyFromBytes(b)
if err != nil {
panic(err)
}
c, err := NewOauthClient(OauthClientArgs{
-
ClientJwk: b,
+
ClientJwk: k,
ClientId: serverMetadataUrl,
RedirectUri: serverCallbackUrl,
})
···
_, err := GenerateKey(&prefix)
assert.NoError(err)
}
+
+
func TestSendParAuthRequest(t *testing.T) {
+
assert := assert.New(t)
+
+
authserverUrl, err := oauthClient.ResolvePDSAuthServer(ctx, pdsUrl)
+
meta, err := oauthClient.FetchAuthServerMetadata(ctx, pdsUrl)
+
if err != nil {
+
panic(err)
+
}
+
+
prefix := "testing"
+
dpopPriv, err := GenerateKey(&prefix)
+
if err != nil {
+
panic(err)
+
}
+
+
parResp, err := oauthClient.SendParAuthRequest(ctx, authserverUrl, meta, "transition:generic", "atproto", dpopPriv)
+
if err != nil {
+
panic(err)
+
}
+
+
assert.NoError(err)
+
assert.Equal(float64(299), parResp.Resp["expires_in"])
+
assert.NotEmpty(parResp.Resp["request_uri"])
+
}