this repo has no description

Compare changes

Choose any two refs to compare.

+1
.gitignore
···
jwks.json
.env
oauth.db
+
oauth
+7 -7
README.md
···
return err
}
-
k, err := oauth.ParseJWKFromBytes(b)
+
k, err := helpers.ParseJWKFromBytes(b)
if err != nil {
return err
}
···
return err
}
-
return e.JSON(200, oauth.CreateJwksResponseObject(pubKey))
+
return e.JSON(200, helpers.CreateJwksResponseObject(pubKey))
}
```
···
return err
}
-
k, err := oauth.ParseJWKFromBytes(b)
+
k, err := helpers.ParseJWKFromBytes(b)
if err != nil {
return err
}
-
cli, err := oauth.NewClient(oauth.ClientArgs{
+
cli, err := helpers.NewClient(oauth.ClientArgs{
ClientJwk: k,
ClientId: clientId,
RedirectUri: callbackUrl,
···
You'll need to create a private DPoP JWK for the user before directing them to their PDS to authenticate. You'll need to store this in a later step, and you will need to pass it along inside the PAR request, so go ahead and marshal it as well.
```go
-
k, err := oauth.GenerateKey(nil)
+
k, err := helpers.GenerateKey(nil)
if err != nil {
return err
}
···
```go
u, _ := url.Parse(meta.AuthorizationEndpoint)
-
u.RawQuery = fmt.Sprintf("client_id=%s&requires_uri=%s", url.QueryEscape(yourClientId), parResp.RequestUri)
+
u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(yourClientId), parResp.RequestUri)
// Redirect the user to created url
```
···
oauthSession, err := s.getOauthSession(e.Request().Context(), did)
// Parse the user's JWK to pass into arguments
-
privateJwk, err := oauth.ParseJWKFromBytes([]byte(oauthSession.DpopPrivateJwk))
+
privateJwk, err := helpers.ParseJWKFromBytes([]byte(oauthSession.DpopPrivateJwk))
if err != nil {
return nil, false, err
}
+2 -2
cmd/helper/main.go
···
"encoding/json"
"os"
-
oauth "github.com/haileyok/atproto-oauth-golang"
+
oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers"
"github.com/urfave/cli/v2"
)
···
inputPrefix := cmd.String("prefix")
prefix = &inputPrefix
}
-
key, err := oauth.GenerateKey(prefix)
+
key, err := oauth_helpers.GenerateKey(prefix)
if err != nil {
return err
}
cmd/web_server_demo/client_test

This is a binary file and will not be displayed.

+22 -13
cmd/web_server_demo/handle_auth.go
···
import (
"encoding/json"
"fmt"
+
"log/slog"
"net/url"
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/gorilla/sessions"
-
oauth "github.com/haileyok/atproto-oauth-golang"
+
oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"gorm.io/gorm/clause"
···
return err
}
-
dpopPrivateKey, err := oauth.GenerateKey(nil)
+
dpopPrivateKey, err := oauth_helpers.GenerateKey(nil)
if err != nil {
return err
}
···
return err
}
+
params := url.Values{
+
"client_id": {s.args.UrlRoot + serverMetadataPath},
+
"request_uri": {parResp.RequestUri},
+
}
+
u, _ := url.Parse(meta.AuthorizationEndpoint)
-
u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(serverMetadataUrl), parResp.RequestUri)
+
u.RawQuery = params.Encode()
sess, err := session.Get("session", e)
if err != nil {
···
}
// make sure the session is empty
-
sess.Values = map[interface{}]interface{}{}
+
sess.Values = map[any]any{}
sess.Values["oauth_state"] = parResp.State
sess.Values["oauth_did"] = did
···
}
func (s *TestServer) handleCallback(e echo.Context) error {
-
resState := e.QueryParam("state")
-
resIss := e.QueryParam("iss")
-
resCode := e.QueryParam("code")
+
params := e.QueryParams()
+
state := params.Get("state")
+
iss := params.Get("iss")
+
code := params.Get("code")
sess, err := session.Get("session", e)
if err != nil {
···
sessState := sess.Values["oauth_state"]
-
if resState == "" || resIss == "" || resCode == "" {
+
if state == "" || iss == "" || code == "" {
return fmt.Errorf("request missing needed parameters")
}
-
if resState != sessState {
+
if state != sessState {
return fmt.Errorf("session state does not match response state")
}
···
return err
}
-
if resIss != oauthRequest.AuthserverIss {
+
if iss != oauthRequest.AuthserverIss {
return fmt.Errorf("incoming iss did not match authserver iss")
}
-
jwk, err := oauth.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
+
jwk, err := oauth_helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
if err != nil {
return err
}
-
initialTokenResp, err := s.oauthClient.InitialTokenRequest(e.Request().Context(), resCode, resIss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
+
initialTokenResp, err := s.oauthClient.InitialTokenRequest(e.Request().Context(), code, iss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
if err != nil {
return err
}
···
}
// make sure the session is empty
-
sess.Values = map[interface{}]interface{}{}
+
sess.Values = map[any]any{}
sess.Values["did"] = oauthRequest.Did
if err := sess.Save(e.Request(), e.Response()); err != nil {
return err
}
+
+
slog.Default().Info("handled callback", "params", params)
return e.Redirect(302, "/")
}
+1 -1
cmd/web_server_demo/handle_post.go
···
return err
}
-
return e.File(getFilePath("make-post.html"))
+
return e.File(s.getFilePath("make-post.html"))
}
+74 -42
cmd/web_server_demo/main.go
···
"github.com/gorilla/sessions"
oauth "github.com/haileyok/atproto-oauth-golang"
+
oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers"
_ "github.com/joho/godotenv/autoload"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
···
)
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")
-
sessionSecret = os.Getenv("OAUTH_TEST_SESSION_SECRET")
-
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"
+
ctx = context.Background()
+
serverMetadataPath = "/oauth/client-metadata.json"
+
serverCallbackPath = "/callback"
+
scope = "atproto transition:generic"
)
func main() {
app := &cli.App{
Name: "atproto-goauth-demo-webserver",
Action: run,
-
}
-
-
if serverUrlRoot == "" {
-
panic(fmt.Errorf("no server url root set in env file"))
+
Flags: []cli.Flag{
+
&cli.StringFlag{
+
Name: "addr",
+
Value: ":8080",
+
EnvVars: []string{"OAUTH_TEST_SERVER_ADDR"},
+
},
+
&cli.StringFlag{
+
Name: "url-root",
+
Required: true,
+
EnvVars: []string{"OAUTH_TEST_SERVER_URL_ROOT"},
+
},
+
&cli.StringFlag{
+
Name: "static-file-path",
+
Value: "./cmd/web_server_demo/html",
+
EnvVars: []string{"OAUTH_TEST_SERVER_STATIC_PATH"},
+
},
+
&cli.StringFlag{
+
Name: "session-secret",
+
Value: "session-secret",
+
EnvVars: []string{"OAUTH_TEST_SERVER_SESSION_SECRET"},
+
},
+
},
}
-
app.RunAndExitOnError()
+
app.Run(os.Args)
}
type TestServer struct {
···
db *gorm.DB
oauthClient *oauth.Client
xrpcCli *oauth.XrpcClient
-
jwksResponse *oauth.JwksResponseObject
+
jwksResponse *oauth_helpers.JwksResponseObject
+
args ServerArgs
}
type TemplateRenderer struct {
templates *template.Template
}
-
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
-
if viewContext, isMap := data.(map[string]interface{}); isMap {
+
func (t *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error {
+
if viewContext, isMap := data.(map[string]any); isMap {
viewContext["reverse"] = c.Echo().Reverse
}
···
}
func run(cmd *cli.Context) error {
-
s, err := NewServer()
+
s, err := NewServer(ServerArgs{
+
Addr: cmd.String("addr"),
+
UrlRoot: cmd.String("url-root"),
+
StaticFilePath: cmd.String("static-file-path"),
+
SessionSecret: cmd.String("session-secret"),
+
})
if err != nil {
panic(err)
}
···
return nil
}
-
func NewServer() (*TestServer, error) {
+
type ServerArgs struct {
+
Addr string
+
UrlRoot string
+
StaticFilePath string
+
SessionSecret string
+
}
+
+
func NewServer(args ServerArgs) (*TestServer, error) {
e := echo.New()
e.Use(slogecho.New(slog.Default()))
-
e.Use(session.Middleware(sessions.NewCookieStore([]byte(sessionSecret))))
-
-
renderer := &TemplateRenderer{
-
templates: template.Must(template.ParseGlob(getFilePath("*.html"))),
-
}
-
e.Renderer = renderer
+
e.Use(session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
fmt.Println("atproto goauth demo webserver")
···
return nil, err
}
-
k, err := oauth.ParseJWKFromBytes(b)
+
k, err := oauth_helpers.ParseJWKFromBytes(b)
if err != nil {
return nil, err
}
···
c, err := oauth.NewClient(oauth.ClientArgs{
ClientJwk: k,
-
ClientId: serverMetadataUrl,
-
RedirectUri: serverCallbackUrl,
+
ClientId: args.UrlRoot + serverMetadataPath,
+
RedirectUri: args.UrlRoot + serverCallbackPath,
})
if err != nil {
return nil, err
}
httpd := &http.Server{
-
Addr: serverAddr,
+
Addr: args.Addr,
Handler: e,
}
···
},
}
-
return &TestServer{
+
s := &TestServer{
httpd: httpd,
e: e,
db: db,
oauthClient: c,
xrpcCli: xrpcCli,
-
jwksResponse: oauth.CreateJwksResponseObject(pubKey),
-
}, nil
+
jwksResponse: oauth_helpers.CreateJwksResponseObject(pubKey),
+
args: args,
+
}
+
+
renderer := &TemplateRenderer{
+
templates: template.Must(template.ParseGlob(s.getFilePath("*.html"))),
+
}
+
e.Renderer = renderer
+
+
return s, nil
}
func (s *TestServer) run() error {
s.e.GET("/", s.handleHome)
-
s.e.File("/login", getFilePath("login.html"))
+
s.e.File("/login", s.getFilePath("login.html"))
s.e.POST("/login", s.handleLoginSubmit)
s.e.GET("/logout", s.handleLogout)
s.e.GET("/profile", s.handleProfile)
···
s.e.GET("/callback", s.handleCallback)
s.e.GET("/oauth/client-metadata.json", s.handleClientMetadata)
s.e.GET("/oauth/jwks.json", s.handleJwks)
+
+
slog.Default().Info("starting http server", "addr", s.args.Addr)
if err := s.httpd.ListenAndServe(); err != nil {
return err
···
func (s *TestServer) handleClientMetadata(e echo.Context) error {
metadata := map[string]any{
-
"client_id": serverMetadataUrl,
+
"client_id": s.args.UrlRoot + serverMetadataPath,
"client_name": "Atproto GoAuth Demo Webserver",
-
"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},
+
"client_uri": s.args.UrlRoot,
+
"logo_uri": fmt.Sprintf("%s/logo.png", s.args.UrlRoot),
+
"tos_uri": fmt.Sprintf("%s/tos", s.args.UrlRoot),
+
"policy_url": fmt.Sprintf("%s/policy", s.args.UrlRoot),
+
"redirect_uris": []string{s.args.UrlRoot + serverCallbackPath},
"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),
+
"jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", s.args.UrlRoot),
"scope": "atproto transition:generic",
"token_endpoint_auth_method": "private_key_jwt",
"token_endpoint_auth_signing_alg": "ES256",
···
return e.JSON(200, s.jwksResponse)
}
-
func getFilePath(file string) string {
-
return fmt.Sprintf("%s/%s", staticFilePath, file)
+
func (s *TestServer) getFilePath(file string) string {
+
return fmt.Sprintf("%s/%s", s.args.StaticFilePath, file)
}
+3 -2
cmd/web_server_demo/user.go
···
"time"
oauth "github.com/haileyok/atproto-oauth-golang"
+
oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
···
}
if oauthSession.Expiration.Sub(time.Now()) <= 5*time.Minute {
-
privateJwk, err := oauth.ParseJWKFromBytes([]byte(oauthSession.DpopPrivateJwk))
+
privateJwk, err := oauth_helpers.ParseJWKFromBytes([]byte(oauthSession.DpopPrivateJwk))
if err != nil {
return nil, err
}
···
oauthSession, err := s.getOauthSession(e.Request().Context(), did)
-
privateJwk, err := oauth.ParseJWKFromBytes([]byte(oauthSession.DpopPrivateJwk))
+
privateJwk, err := oauth_helpers.ParseJWKFromBytes([]byte(oauthSession.DpopPrivateJwk))
if err != nil {
return nil, false, err
}
-113
generic.go
···
-
package oauth
-
-
import (
-
"crypto/ecdsa"
-
"crypto/elliptic"
-
"crypto/rand"
-
"crypto/sha256"
-
"encoding/base64"
-
"encoding/hex"
-
"fmt"
-
"net/url"
-
"time"
-
-
"github.com/lestrrat-go/jwx/v2/jwk"
-
)
-
-
func GenerateKey(kidPrefix *string) (jwk.Key, error) {
-
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-
if err != nil {
-
return nil, err
-
}
-
-
key, err := jwk.FromRaw(privKey)
-
if err != nil {
-
return nil, err
-
}
-
-
var kid string
-
if kidPrefix != nil {
-
kid = fmt.Sprintf("%s-%d", *kidPrefix, time.Now().Unix())
-
-
} else {
-
kid = fmt.Sprintf("%d", time.Now().Unix())
-
}
-
-
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
-
return nil, err
-
}
-
return key, nil
-
}
-
-
func isSafeAndParsed(ustr string) (*url.URL, error) {
-
u, err := url.Parse(ustr)
-
if err != nil {
-
return nil, err
-
}
-
-
if u.Scheme != "https" {
-
return nil, fmt.Errorf("input url is not https")
-
}
-
-
if u.Hostname() == "" {
-
return nil, fmt.Errorf("url hostname was empty")
-
}
-
-
if u.User != nil {
-
return nil, fmt.Errorf("url user was not empty")
-
}
-
-
if u.Port() != "" {
-
return nil, fmt.Errorf("url port was not empty")
-
}
-
-
return u, nil
-
}
-
-
func getPrivateKey(key jwk.Key) (*ecdsa.PrivateKey, error) {
-
var pkey ecdsa.PrivateKey
-
if err := key.Raw(&pkey); err != nil {
-
return nil, err
-
}
-
-
return &pkey, nil
-
}
-
-
func getPublicKey(key jwk.Key) (*ecdsa.PublicKey, error) {
-
var pkey ecdsa.PublicKey
-
if err := key.Raw(&pkey); err != nil {
-
return nil, err
-
}
-
-
return &pkey, nil
-
}
-
-
type JwksResponseObject struct {
-
Keys []jwk.Key `json:"keys"`
-
}
-
-
func CreateJwksResponseObject(key jwk.Key) *JwksResponseObject {
-
return &JwksResponseObject{
-
Keys: []jwk.Key{key},
-
}
-
}
-
-
func ParseJWKFromBytes(b []byte) (jwk.Key, error) {
-
return jwk.ParseKey(b)
-
}
-
-
func generateToken(len int) (string, error) {
-
b := make([]byte, len)
-
if _, err := rand.Read(b); err != nil {
-
return "", err
-
}
-
-
return hex.EncodeToString(b), nil
-
}
-
-
func generateCodeChallenge(pkceVerifier string) string {
-
h := sha256.New()
-
h.Write([]byte(pkceVerifier))
-
hash := h.Sum(nil)
-
return base64.RawURLEncoding.EncodeToString(hash)
-
}
+2 -2
go.mod
···
go 1.24.0
require (
-
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188
+
github.com/bluesky-social/indigo v0.0.0-20250616202859-d4516ea1d6cf
github.com/carlmjohnson/versioninfo v0.22.5
-
github.com/golang-jwt/jwt/v5 v5.2.1
+
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.4.0
github.com/joho/godotenv v1.5.1
+4 -4
go.sum
···
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-
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/bluesky-social/indigo v0.0.0-20250616202859-d4516ea1d6cf h1:LFlwtY9r95lAI1yYKolCLTQnwK5VjgWO87mNsKdj3Qs=
+
github.com/bluesky-social/indigo v0.0.0-20250616202859-d4516ea1d6cf/go.mod h1:8FlFpF5cIq3DQG0kEHqyTkPV/5MDQoaWLcVwza5ZPJU=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
···
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-
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/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+
github.com/golang-jwt/jwt/v5 v5.2.2/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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+94
helpers/generic.go
···
+
package helpers
+
+
import (
+
"crypto/ecdsa"
+
"crypto/elliptic"
+
"crypto/rand"
+
"fmt"
+
"net/url"
+
"time"
+
+
"github.com/lestrrat-go/jwx/v2/jwk"
+
)
+
+
func GenerateKey(kidPrefix *string) (jwk.Key, error) {
+
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+
if err != nil {
+
return nil, err
+
}
+
+
key, err := jwk.FromRaw(privKey)
+
if err != nil {
+
return nil, err
+
}
+
+
var kid string
+
if kidPrefix != nil {
+
kid = fmt.Sprintf("%s-%d", *kidPrefix, time.Now().Unix())
+
+
} else {
+
kid = fmt.Sprintf("%d", time.Now().Unix())
+
}
+
+
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
+
return nil, err
+
}
+
return key, nil
+
}
+
+
func IsUrlSafeAndParsed(ustr string) (*url.URL, error) {
+
u, err := url.Parse(ustr)
+
if err != nil {
+
return nil, err
+
}
+
+
if u.Scheme != "https" {
+
return nil, fmt.Errorf("input url is not https")
+
}
+
+
if u.Hostname() == "" {
+
return nil, fmt.Errorf("url hostname was empty")
+
}
+
+
if u.User != nil {
+
return nil, fmt.Errorf("url user was not empty")
+
}
+
+
if u.Port() != "" {
+
return nil, fmt.Errorf("url port was not empty")
+
}
+
+
return u, nil
+
}
+
+
func GetPrivateKey(key jwk.Key) (*ecdsa.PrivateKey, error) {
+
var pkey ecdsa.PrivateKey
+
if err := key.Raw(&pkey); err != nil {
+
return nil, err
+
}
+
+
return &pkey, nil
+
}
+
+
func GetPublicKey(key jwk.Key) (*ecdsa.PublicKey, error) {
+
var pkey ecdsa.PublicKey
+
if err := key.Raw(&pkey); err != nil {
+
return nil, err
+
}
+
+
return &pkey, nil
+
}
+
+
type JwksResponseObject struct {
+
Keys []jwk.Key `json:"keys"`
+
}
+
+
func CreateJwksResponseObject(key jwk.Key) *JwksResponseObject {
+
return &JwksResponseObject{
+
Keys: []jwk.Key{key},
+
}
+
}
+
+
func ParseJWKFromBytes(b []byte) (jwk.Key, error) {
+
return jwk.ParseKey(b)
+
}
+24
internal/helpers/generic.go
···
+
package helpers
+
+
import (
+
"crypto/rand"
+
"crypto/sha256"
+
"encoding/base64"
+
"encoding/hex"
+
)
+
+
func GenerateToken(len int) (string, error) {
+
b := make([]byte, len)
+
if _, err := rand.Read(b); err != nil {
+
return "", err
+
}
+
+
return hex.EncodeToString(b), nil
+
}
+
+
func GenerateCodeChallenge(pkceVerifier string) string {
+
h := sha256.New()
+
h.Write([]byte(pkceVerifier))
+
hash := h.Sum(nil)
+
return base64.RawURLEncoding.EncodeToString(hash)
+
}
+25 -20
oauth.go
···
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
+
"github.com/haileyok/atproto-oauth-golang/helpers"
+
internal_helpers "github.com/haileyok/atproto-oauth-golang/internal/helpers"
"github.com/lestrrat-go/jwx/v2/jwk"
)
···
}
}
-
clientPkey, err := getPrivateKey(args.ClientJwk)
+
clientPkey, err := helpers.GetPrivateKey(args.ClientJwk)
if err != nil {
return nil, fmt.Errorf("could not load private key from provided client jwk: %w", err)
}
···
}
func (c *Client) ResolvePdsAuthServer(ctx context.Context, ustr string) (string, error) {
-
u, err := isSafeAndParsed(ustr)
+
u, err := helpers.IsUrlSafeAndParsed(ustr)
if err != nil {
return "", err
}
···
return "", fmt.Errorf("received non-200 response from pds. code was %d", resp.StatusCode)
}
-
b, err := io.ReadAll(resp.Body)
-
if err != nil {
-
return "", fmt.Errorf("could not read body: %w", err)
-
}
-
var resource OauthProtectedResource
-
if err := resource.UnmarshalJSON(b); err != nil {
+
if err := json.NewDecoder(resp.Body).Decode(&resource); err != nil {
return "", fmt.Errorf("could not unmarshal json: %w", err)
}
···
}
func (c *Client) FetchAuthServerMetadata(ctx context.Context, ustr string) (*OauthAuthorizationMetadata, error) {
-
u, err := isSafeAndParsed(ustr)
+
u, err := helpers.IsUrlSafeAndParsed(ustr)
if err != nil {
return nil, err
}
···
return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode)
}
-
b, err := io.ReadAll(resp.Body)
-
if err != nil {
-
return nil, fmt.Errorf("could not read body for authserver metadata response: %w", err)
-
}
-
var metadata OauthAuthorizationMetadata
-
if err := metadata.UnmarshalJSON(b); err != nil {
+
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
return nil, fmt.Errorf("could not unmarshal authserver metadata: %w", err)
}
···
return tokenString, nil
}
-
func (c *Client) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
+
type ParAuthRequestExtra struct {
+
Name string
+
Value string
+
}
+
+
func (c *Client) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key, extras ...ParAuthRequestExtra) (*SendParAuthResponse, error) {
if authServerMeta == nil {
return nil, fmt.Errorf("nil metadata provided")
}
parUrl := authServerMeta.PushedAuthorizationRequestEndpoint
-
state, err := generateToken(10)
+
state, err := internal_helpers.GenerateToken(10)
if err != nil {
return nil, fmt.Errorf("could not generate state token: %w", err)
}
-
pkceVerifier, err := generateToken(48)
+
pkceVerifier, err := internal_helpers.GenerateToken(48)
if err != nil {
return nil, fmt.Errorf("could not generate pkce verifier: %w", err)
}
-
codeChallenge := generateCodeChallenge(pkceVerifier)
+
codeChallenge := internal_helpers.GenerateCodeChallenge(pkceVerifier)
codeChallengeMethod := "S256"
clientAssertion, err := c.ClientAssertionJwt(authServerUrl)
···
params.Set("login_hint", loginHint)
}
-
_, err = isSafeAndParsed(parUrl)
+
for _, e := range extras {
+
if !strings.HasPrefix(e.Name, "ext-") {
+
e.Name = "ext-" + e.Name
+
}
+
e.Value = url.QueryEscape(e.Value)
+
params.Set(e.Name, e.Value)
+
}
+
+
_, err = helpers.IsUrlSafeAndParsed(parUrl)
if err != nil {
return nil, err
}
+4 -3
oauth_test.go
···
"os"
"testing"
+
"github.com/haileyok/atproto-oauth-golang/helpers"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/assert"
)
···
panic(err)
}
-
k, err := ParseJWKFromBytes(b)
+
k, err := helpers.ParseJWKFromBytes(b)
if err != nil {
panic(err)
}
···
assert := assert.New(t)
prefix := "testing"
-
_, err := GenerateKey(&prefix)
+
_, err := helpers.GenerateKey(&prefix)
assert.NoError(err)
}
···
}
prefix := "testing"
-
dpopPriv, err := GenerateKey(&prefix)
+
dpopPriv, err := helpers.GenerateKey(&prefix)
if err != nil {
panic(err)
}
-27
types.go
···
package oauth
import (
-
"encoding/json"
"fmt"
"net/url"
"slices"
···
ScopesSupported []string `json:"scopes_supported"`
BearerMethodsSupported []string `json:"bearer_methods_supported"`
ResourceDocumentation string `json:"resource_documentation"`
-
}
-
-
func (opr *OauthProtectedResource) UnmarshalJSON(b []byte) error {
-
type Tmp OauthProtectedResource
-
var tmp Tmp
-
-
if err := json.Unmarshal(b, &tmp); err != nil {
-
return err
-
}
-
-
*opr = OauthProtectedResource(tmp)
-
-
return nil
}
type OauthAuthorizationMetadata struct {
···
DpopSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported"`
ProtectedResources []string `json:"protected_resources"`
ClientIDMetadataDocumentSupported bool `json:"client_id_metadata_document_supported"`
-
}
-
-
func (oam *OauthAuthorizationMetadata) UnmarshalJSON(b []byte) error {
-
type Tmp OauthAuthorizationMetadata
-
var tmp Tmp
-
-
if err := json.Unmarshal(b, &tmp); err != nil {
-
return err
-
}
-
-
*oam = OauthAuthorizationMetadata(tmp)
-
-
return nil
}
func (oam *OauthAuthorizationMetadata) Validate(fetch_url *url.URL) error {
+4 -3
xrpc.go
···
"github.com/carlmjohnson/versioninfo"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
+
"github.com/haileyok/atproto-oauth-golang/internal/helpers"
"github.com/lestrrat-go/jwx/v2/jwk"
)
···
"jti": uuid.NewString(),
"htm": method,
"htu": url,
-
"ath": generateCodeChallenge(accessToken),
+
"ath": helpers.GenerateCodeChallenge(accessToken),
}
if nonce != "" {
···
return tokenString, nil
}
-
func (c *XrpcClient) Do(ctx context.Context, authedArgs *XrpcAuthedRequestArgs, kind xrpc.XRPCRequestType, inpenc, method string, params map[string]any, bodyobj any, out any) error {
+
func (c *XrpcClient) Do(ctx context.Context, authedArgs *XrpcAuthedRequestArgs, kind string, inpenc, method string, params map[string]any, bodyobj any, out any) error {
// we might have to retry the request if we get a new nonce from the server
for range 2 {
var body io.Reader
···
case xrpc.Procedure:
m = "POST"
default:
-
return fmt.Errorf("unsupported request kind: %d", kind)
+
return fmt.Errorf("unsupported request kind: %s", kind)
}
var paramStr string