An atproto PDS written in Go

refactor: cleanup oauth package

+2 -2
oauth/client.go oauth/client/client.go
··· 1 - package oauth 1 + package client 2 2 3 3 import "github.com/lestrrat-go/jwx/v2/jwk" 4 4 5 5 type Client struct { 6 - Metadata *ClientMetadata 6 + Metadata *Metadata 7 7 JWKS jwk.Key 8 8 }
+13 -14
oauth/client_manager/client_manager.go oauth/client/manager.go
··· 1 - package client_manager 1 + package client 2 2 3 3 import ( 4 4 "context" ··· 15 15 16 16 cache "github.com/go-pkgz/expirable-cache/v3" 17 17 "github.com/haileyok/cocoon/internal/helpers" 18 - "github.com/haileyok/cocoon/oauth" 19 18 "github.com/lestrrat-go/jwx/v2/jwk" 20 19 ) 21 20 22 - type ClientManager struct { 21 + type Manager struct { 23 22 cli *http.Client 24 23 logger *slog.Logger 25 24 jwksCache cache.Cache[string, jwk.Key] 26 - metadataCache cache.Cache[string, oauth.ClientMetadata] 25 + metadataCache cache.Cache[string, Metadata] 27 26 } 28 27 29 - type Args struct { 28 + type ManagerArgs struct { 30 29 Cli *http.Client 31 30 Logger *slog.Logger 32 31 } 33 32 34 - func New(args Args) *ClientManager { 33 + func NewManager(args ManagerArgs) *Manager { 35 34 if args.Logger == nil { 36 35 args.Logger = slog.Default() 37 36 } ··· 41 40 } 42 41 43 42 jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute) 44 - metadataCache := cache.NewCache[string, oauth.ClientMetadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute) 43 + metadataCache := cache.NewCache[string, Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute) 45 44 46 - return &ClientManager{ 45 + return &Manager{ 47 46 cli: args.Cli, 48 47 logger: args.Logger, 49 48 jwksCache: jwksCache, ··· 51 50 } 52 51 } 53 52 54 - func (cm *ClientManager) GetClient(ctx context.Context, clientId string) (*oauth.Client, error) { 53 + func (cm *Manager) GetClient(ctx context.Context, clientId string) (*Client, error) { 55 54 metadata, err := cm.getClientMetadata(ctx, clientId) 56 55 if err != nil { 57 56 return nil, err ··· 75 74 jwks = maybeJwks 76 75 } 77 76 78 - return &oauth.Client{ 77 + return &Client{ 79 78 Metadata: metadata, 80 79 JWKS: jwks, 81 80 }, nil 82 81 } 83 82 84 - func (cm *ClientManager) getClientMetadata(ctx context.Context, clientId string) (*oauth.ClientMetadata, error) { 83 + func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) { 85 84 metadataCached, ok := cm.metadataCache.Get(clientId) 86 85 if !ok { 87 86 req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil) ··· 116 115 } 117 116 } 118 117 119 - func (cm *ClientManager) getClientJwks(ctx context.Context, clientId, jwksUri string) (jwk.Key, error) { 118 + func (cm *Manager) getClientJwks(ctx context.Context, clientId, jwksUri string) (jwk.Key, error) { 120 119 jwks, ok := cm.jwksCache.Get(clientId) 121 120 if !ok { 122 121 req, err := http.NewRequestWithContext(ctx, "GET", jwksUri, nil) ··· 165 164 return jwks, nil 166 165 } 167 166 168 - func validateAndParseMetadata(clientId string, b []byte) (*oauth.ClientMetadata, error) { 167 + func validateAndParseMetadata(clientId string, b []byte) (*Metadata, error) { 169 168 var metadataMap map[string]any 170 169 if err := json.Unmarshal(b, &metadataMap); err != nil { 171 170 return nil, fmt.Errorf("error unmarshaling metadata: %w", err) ··· 192 191 } 193 192 } 194 193 195 - var metadata oauth.ClientMetadata 194 + var metadata Metadata 196 195 if err := json.Unmarshal(b, &metadata); err != nil { 197 196 return nil, fmt.Errorf("error unmarshaling metadata: %w", err) 198 197 }
+2 -2
oauth/client_metadata.go oauth/client/metadata.go
··· 1 - package oauth 1 + package client 2 2 3 - type ClientMetadata struct { 3 + type Metadata struct { 4 4 ClientID string `json:"client_id"` 5 5 ClientName string `json:"client_name"` 6 6 ClientURI string `json:"client_uri"`
+10 -12
oauth/dpop/dpop_manager/dpop_manager.go oauth/dpop/manager.go
··· 1 - package dpop_manager 1 + package dpop 2 2 3 3 import ( 4 4 "crypto" ··· 16 16 "github.com/golang-jwt/jwt/v4" 17 17 "github.com/haileyok/cocoon/internal/helpers" 18 18 "github.com/haileyok/cocoon/oauth/constants" 19 - "github.com/haileyok/cocoon/oauth/dpop" 20 - "github.com/haileyok/cocoon/oauth/dpop/nonce" 21 19 "github.com/lestrrat-go/jwx/v2/jwa" 22 20 "github.com/lestrrat-go/jwx/v2/jwk" 23 21 ) 24 22 25 - type DpopManager struct { 26 - nonce *nonce.Nonce 23 + type Manager struct { 24 + nonce *Nonce 27 25 jtiCache *jtiCache 28 26 logger *slog.Logger 29 27 hostname string 30 28 } 31 29 32 - type Args struct { 30 + type ManagerArgs struct { 33 31 NonceSecret []byte 34 32 NonceRotationInterval time.Duration 35 33 OnNonceSecretCreated func([]byte) ··· 38 36 Hostname string 39 37 } 40 38 41 - func New(args Args) *DpopManager { 39 + func NewManager(args ManagerArgs) *Manager { 42 40 if args.Logger == nil { 43 41 args.Logger = slog.Default() 44 42 } ··· 51 49 args.Logger.Warn("nonce secret passed to dpop manager was nil. existing sessions may break. consider saving and restoring your nonce.") 52 50 } 53 51 54 - return &DpopManager{ 55 - nonce: nonce.NewNonce(nonce.Args{ 52 + return &Manager{ 53 + nonce: NewNonce(NonceArgs{ 56 54 RotationInterval: args.NonceRotationInterval, 57 55 Secret: args.NonceSecret, 58 56 OnSecretCreated: args.OnNonceSecretCreated, ··· 63 61 } 64 62 } 65 63 66 - func (dm *DpopManager) CheckProof(reqMethod, reqUrl string, headers http.Header, accessToken *string) (*dpop.Proof, error) { 64 + func (dm *Manager) CheckProof(reqMethod, reqUrl string, headers http.Header, accessToken *string) (*Proof, error) { 67 65 if reqMethod == "" { 68 66 return nil, errors.New("HTTP method is required") 69 67 } ··· 226 224 227 225 thumb := base64.RawURLEncoding.EncodeToString(thumbBytes) 228 226 229 - return &dpop.Proof{ 227 + return &Proof{ 230 228 JTI: jti, 231 229 JKT: thumb, 232 230 HTM: htm, ··· 246 244 } 247 245 } 248 246 249 - func (dm *DpopManager) NextNonce() string { 247 + func (dm *Manager) NextNonce() string { 250 248 return dm.nonce.NextNonce() 251 249 }
+1 -1
oauth/dpop/dpop_manager/jti_cache.go oauth/dpop/jti_cache.go
··· 1 - package dpop_manager 1 + package dpop 2 2 3 3 import ( 4 4 "sync"
+3 -3
oauth/dpop/nonce/nonce.go oauth/dpop/nonce.go
··· 1 - package nonce 1 + package dpop 2 2 3 3 import ( 4 4 "crypto/hmac" ··· 24 24 next string 25 25 } 26 26 27 - type Args struct { 27 + type NonceArgs struct { 28 28 RotationInterval time.Duration 29 29 Secret []byte 30 30 OnSecretCreated func([]byte) 31 31 } 32 32 33 - func NewNonce(args Args) *Nonce { 33 + func NewNonce(args NonceArgs) *Nonce { 34 34 if args.RotationInterval == 0 { 35 35 args.RotationInterval = constants.NonceMaxRotationInterval / 3 36 36 }
+3 -26
oauth/provider/client_auth.go
··· 3 3 import ( 4 4 "context" 5 5 "crypto" 6 - "database/sql/driver" 7 6 "encoding/base64" 8 - "encoding/json" 9 7 "errors" 10 8 "fmt" 11 9 "time" 12 10 13 11 "github.com/golang-jwt/jwt/v4" 14 - "github.com/haileyok/cocoon/oauth" 12 + "github.com/haileyok/cocoon/oauth/client" 15 13 "github.com/haileyok/cocoon/oauth/constants" 16 14 "github.com/haileyok/cocoon/oauth/dpop" 17 15 ) 18 16 19 - type ClientAuth struct { 20 - Method string 21 - Alg string 22 - Kid string 23 - Jkt string 24 - Jti string 25 - Exp *float64 26 - } 27 - 28 - func (ca *ClientAuth) Scan(value any) error { 29 - b, ok := value.([]byte) 30 - if !ok { 31 - return fmt.Errorf("failed to unmarshal OauthParRequest value") 32 - } 33 - return json.Unmarshal(b, ca) 34 - } 35 - 36 - func (ca ClientAuth) Value() (driver.Value, error) { 37 - return json.Marshal(ca) 38 - } 39 - 40 17 type AuthenticateClientOptions struct { 41 18 AllowMissingDpopProof bool 42 19 } ··· 47 24 ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty"` 48 25 } 49 26 50 - func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*oauth.Client, *ClientAuth, error) { 27 + func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) { 51 28 client, err := p.ClientManager.GetClient(ctx, req.ClientID) 52 29 if err != nil { 53 30 return nil, nil, fmt.Errorf("failed to get client: %w", err) ··· 69 46 return client, clientAuth, nil 70 47 } 71 48 72 - func (p *Provider) Authenticate(_ context.Context, req AuthenticateClientRequestBase, client *oauth.Client) (*ClientAuth, error) { 49 + func (p *Provider) Authenticate(_ context.Context, req AuthenticateClientRequestBase, client *client.Client) (*ClientAuth, error) { 73 50 metadata := client.Metadata 74 51 75 52 if metadata.TokenEndpointAuthMethod == "none" {
+81
oauth/provider/models.go
··· 1 + package provider 2 + 3 + import ( 4 + "database/sql/driver" 5 + "encoding/json" 6 + "fmt" 7 + "time" 8 + 9 + "gorm.io/gorm" 10 + ) 11 + 12 + type ClientAuth struct { 13 + Method string 14 + Alg string 15 + Kid string 16 + Jkt string 17 + Jti string 18 + Exp *float64 19 + } 20 + 21 + func (ca *ClientAuth) Scan(value any) error { 22 + b, ok := value.([]byte) 23 + if !ok { 24 + return fmt.Errorf("failed to unmarshal OauthParRequest value") 25 + } 26 + return json.Unmarshal(b, ca) 27 + } 28 + 29 + func (ca ClientAuth) Value() (driver.Value, error) { 30 + return json.Marshal(ca) 31 + } 32 + 33 + type ParRequest struct { 34 + AuthenticateClientRequestBase 35 + ResponseType string `form:"response_type" json:"response_type" validate:"required"` 36 + CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"` 37 + CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"` 38 + State string `form:"state" json:"state" validate:"required"` 39 + RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"` 40 + Scope string `form:"scope" json:"scope" validate:"required"` 41 + LoginHint *string `form:"login_hint" json:"login_hint,omitempty"` 42 + DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"` 43 + } 44 + 45 + func (opr *ParRequest) Scan(value any) error { 46 + b, ok := value.([]byte) 47 + if !ok { 48 + return fmt.Errorf("failed to unmarshal OauthParRequest value") 49 + } 50 + return json.Unmarshal(b, opr) 51 + } 52 + 53 + func (opr ParRequest) Value() (driver.Value, error) { 54 + return json.Marshal(opr) 55 + } 56 + 57 + type OauthToken struct { 58 + gorm.Model 59 + ClientId string `gorm:"index"` 60 + ClientAuth ClientAuth `gorm:"type:json"` 61 + Parameters ParRequest `gorm:"type:json"` 62 + ExpiresAt time.Time `gorm:"index"` 63 + DeviceId string 64 + Sub string `gorm:"index"` 65 + Code string `gorm:"index"` 66 + Token string `gorm:"uniqueIndex"` 67 + RefreshToken string `gorm:"uniqueIndex"` 68 + } 69 + 70 + type OauthAuthorizationRequest struct { 71 + gorm.Model 72 + RequestId string `gorm:"primaryKey"` 73 + ClientId string `gorm:"index"` 74 + ClientAuth ClientAuth `gorm:"type:json"` 75 + Parameters ParRequest `gorm:"type:json"` 76 + ExpiresAt time.Time `gorm:"index"` 77 + DeviceId *string 78 + Sub *string 79 + Code *string 80 + Accepted *bool 81 + }
+8 -64
oauth/provider/provider.go
··· 1 1 package provider 2 2 3 3 import ( 4 - "database/sql/driver" 5 - "encoding/json" 6 - "fmt" 7 - "time" 8 - 9 - "github.com/haileyok/cocoon/oauth/client_manager" 10 - "github.com/haileyok/cocoon/oauth/dpop/dpop_manager" 11 - "gorm.io/gorm" 4 + "github.com/haileyok/cocoon/oauth/client" 5 + "github.com/haileyok/cocoon/oauth/dpop" 12 6 ) 13 7 14 8 type Provider struct { 15 - ClientManager *client_manager.ClientManager 16 - DpopManager *dpop_manager.DpopManager 9 + ClientManager *client.Manager 10 + DpopManager *dpop.Manager 17 11 18 12 hostname string 19 13 } 20 14 21 15 type Args struct { 22 16 Hostname string 23 - ClientManagerArgs client_manager.Args 24 - DpopManagerArgs dpop_manager.Args 17 + ClientManagerArgs client.ManagerArgs 18 + DpopManagerArgs dpop.ManagerArgs 25 19 } 26 20 27 21 func NewProvider(args Args) *Provider { 28 22 return &Provider{ 29 - ClientManager: client_manager.New(args.ClientManagerArgs), 30 - DpopManager: dpop_manager.New(args.DpopManagerArgs), 23 + ClientManager: client.NewManager(args.ClientManagerArgs), 24 + DpopManager: dpop.NewManager(args.DpopManagerArgs), 31 25 hostname: args.Hostname, 32 26 } 33 27 } ··· 35 29 func (p *Provider) NextNonce() string { 36 30 return p.DpopManager.NextNonce() 37 31 } 38 - 39 - type ParRequest struct { 40 - AuthenticateClientRequestBase 41 - ResponseType string `form:"response_type" json:"response_type" validate:"required"` 42 - CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"` 43 - CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"` 44 - State string `form:"state" json:"state" validate:"required"` 45 - RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"` 46 - Scope string `form:"scope" json:"scope" validate:"required"` 47 - LoginHint *string `form:"login_hint" json:"login_hint,omitempty"` 48 - DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"` 49 - } 50 - 51 - func (opr *ParRequest) Scan(value any) error { 52 - b, ok := value.([]byte) 53 - if !ok { 54 - return fmt.Errorf("failed to unmarshal OauthParRequest value") 55 - } 56 - return json.Unmarshal(b, opr) 57 - } 58 - 59 - func (opr ParRequest) Value() (driver.Value, error) { 60 - return json.Marshal(opr) 61 - } 62 - 63 - type OauthToken struct { 64 - gorm.Model 65 - ClientId string `gorm:"index"` 66 - ClientAuth ClientAuth `gorm:"type:json"` 67 - Parameters ParRequest `gorm:"type:json"` 68 - ExpiresAt time.Time `gorm:"index"` 69 - DeviceId string 70 - Sub string `gorm:"index"` 71 - Code string `gorm:"index"` 72 - Token string `gorm:"uniqueIndex"` 73 - RefreshToken string `gorm:"uniqueIndex"` 74 - } 75 - 76 - type OauthAuthorizationRequest struct { 77 - gorm.Model 78 - RequestId string `gorm:"primaryKey"` 79 - ClientId string `gorm:"index"` 80 - ClientAuth ClientAuth `gorm:"type:json"` 81 - Parameters ParRequest `gorm:"type:json"` 82 - ExpiresAt time.Time `gorm:"index"` 83 - DeviceId *string 84 - Sub *string 85 - Code *string 86 - Accepted *bool 87 - }
+4 -4
server/server.go
··· 38 38 "github.com/haileyok/cocoon/internal/db" 39 39 "github.com/haileyok/cocoon/internal/helpers" 40 40 "github.com/haileyok/cocoon/models" 41 - "github.com/haileyok/cocoon/oauth/client_manager" 41 + "github.com/haileyok/cocoon/oauth/client" 42 42 "github.com/haileyok/cocoon/oauth/constants" 43 - "github.com/haileyok/cocoon/oauth/dpop/dpop_manager" 43 + "github.com/haileyok/cocoon/oauth/dpop" 44 44 "github.com/haileyok/cocoon/oauth/provider" 45 45 "github.com/haileyok/cocoon/plc" 46 46 echo_session "github.com/labstack/echo-contrib/session" ··· 611 611 612 612 oauthProvider: provider.NewProvider(provider.Args{ 613 613 Hostname: args.Hostname, 614 - ClientManagerArgs: client_manager.Args{ 614 + ClientManagerArgs: client.ManagerArgs{ 615 615 Cli: oauthCli, 616 616 Logger: args.Logger, 617 617 }, 618 - DpopManagerArgs: dpop_manager.Args{ 618 + DpopManagerArgs: dpop.ManagerArgs{ 619 619 NonceSecret: nonceSecret, 620 620 NonceRotationInterval: constants.NonceMaxRotationInterval / 3, 621 621 OnNonceSecretCreated: func(newNonce []byte) {