An atproto PDS written in Go

refactor: cleanup oauth package

+2 -2
oauth/client.go oauth/client/client.go
···
-
package oauth
+
package client
import "github.com/lestrrat-go/jwx/v2/jwk"
type Client struct {
-
Metadata *ClientMetadata
+
Metadata *Metadata
JWKS jwk.Key
}
+13 -14
oauth/client_manager/client_manager.go oauth/client/manager.go
···
-
package client_manager
+
package client
import (
"context"
···
cache "github.com/go-pkgz/expirable-cache/v3"
"github.com/haileyok/cocoon/internal/helpers"
-
"github.com/haileyok/cocoon/oauth"
"github.com/lestrrat-go/jwx/v2/jwk"
)
-
type ClientManager struct {
+
type Manager struct {
cli *http.Client
logger *slog.Logger
jwksCache cache.Cache[string, jwk.Key]
-
metadataCache cache.Cache[string, oauth.ClientMetadata]
+
metadataCache cache.Cache[string, Metadata]
}
-
type Args struct {
+
type ManagerArgs struct {
Cli *http.Client
Logger *slog.Logger
}
-
func New(args Args) *ClientManager {
+
func NewManager(args ManagerArgs) *Manager {
if args.Logger == nil {
args.Logger = slog.Default()
}
···
}
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
-
metadataCache := cache.NewCache[string, oauth.ClientMetadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
+
metadataCache := cache.NewCache[string, Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
-
return &ClientManager{
+
return &Manager{
cli: args.Cli,
logger: args.Logger,
jwksCache: jwksCache,
···
}
}
-
func (cm *ClientManager) GetClient(ctx context.Context, clientId string) (*oauth.Client, error) {
+
func (cm *Manager) GetClient(ctx context.Context, clientId string) (*Client, error) {
metadata, err := cm.getClientMetadata(ctx, clientId)
if err != nil {
return nil, err
···
jwks = maybeJwks
}
-
return &oauth.Client{
+
return &Client{
Metadata: metadata,
JWKS: jwks,
}, nil
}
-
func (cm *ClientManager) getClientMetadata(ctx context.Context, clientId string) (*oauth.ClientMetadata, error) {
+
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
metadataCached, ok := cm.metadataCache.Get(clientId)
if !ok {
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
···
}
}
-
func (cm *ClientManager) getClientJwks(ctx context.Context, clientId, jwksUri string) (jwk.Key, error) {
+
func (cm *Manager) getClientJwks(ctx context.Context, clientId, jwksUri string) (jwk.Key, error) {
jwks, ok := cm.jwksCache.Get(clientId)
if !ok {
req, err := http.NewRequestWithContext(ctx, "GET", jwksUri, nil)
···
return jwks, nil
}
-
func validateAndParseMetadata(clientId string, b []byte) (*oauth.ClientMetadata, error) {
+
func validateAndParseMetadata(clientId string, b []byte) (*Metadata, error) {
var metadataMap map[string]any
if err := json.Unmarshal(b, &metadataMap); err != nil {
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
···
}
}
-
var metadata oauth.ClientMetadata
+
var metadata Metadata
if err := json.Unmarshal(b, &metadata); err != nil {
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
}
+2 -2
oauth/client_metadata.go oauth/client/metadata.go
···
-
package oauth
+
package client
-
type ClientMetadata struct {
+
type Metadata struct {
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
ClientURI string `json:"client_uri"`
+10 -12
oauth/dpop/dpop_manager/dpop_manager.go oauth/dpop/manager.go
···
-
package dpop_manager
+
package dpop
import (
"crypto"
···
"github.com/golang-jwt/jwt/v4"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/oauth/constants"
-
"github.com/haileyok/cocoon/oauth/dpop"
-
"github.com/haileyok/cocoon/oauth/dpop/nonce"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
)
-
type DpopManager struct {
-
nonce *nonce.Nonce
+
type Manager struct {
+
nonce *Nonce
jtiCache *jtiCache
logger *slog.Logger
hostname string
}
-
type Args struct {
+
type ManagerArgs struct {
NonceSecret []byte
NonceRotationInterval time.Duration
OnNonceSecretCreated func([]byte)
···
Hostname string
}
-
func New(args Args) *DpopManager {
+
func NewManager(args ManagerArgs) *Manager {
if args.Logger == nil {
args.Logger = slog.Default()
}
···
args.Logger.Warn("nonce secret passed to dpop manager was nil. existing sessions may break. consider saving and restoring your nonce.")
}
-
return &DpopManager{
-
nonce: nonce.NewNonce(nonce.Args{
+
return &Manager{
+
nonce: NewNonce(NonceArgs{
RotationInterval: args.NonceRotationInterval,
Secret: args.NonceSecret,
OnSecretCreated: args.OnNonceSecretCreated,
···
}
}
-
func (dm *DpopManager) CheckProof(reqMethod, reqUrl string, headers http.Header, accessToken *string) (*dpop.Proof, error) {
+
func (dm *Manager) CheckProof(reqMethod, reqUrl string, headers http.Header, accessToken *string) (*Proof, error) {
if reqMethod == "" {
return nil, errors.New("HTTP method is required")
}
···
thumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
-
return &dpop.Proof{
+
return &Proof{
JTI: jti,
JKT: thumb,
HTM: htm,
···
}
}
-
func (dm *DpopManager) NextNonce() string {
+
func (dm *Manager) NextNonce() string {
return dm.nonce.NextNonce()
}
+1 -1
oauth/dpop/dpop_manager/jti_cache.go oauth/dpop/jti_cache.go
···
-
package dpop_manager
+
package dpop
import (
"sync"
+3 -3
oauth/dpop/nonce/nonce.go oauth/dpop/nonce.go
···
-
package nonce
+
package dpop
import (
"crypto/hmac"
···
next string
}
-
type Args struct {
+
type NonceArgs struct {
RotationInterval time.Duration
Secret []byte
OnSecretCreated func([]byte)
}
-
func NewNonce(args Args) *Nonce {
+
func NewNonce(args NonceArgs) *Nonce {
if args.RotationInterval == 0 {
args.RotationInterval = constants.NonceMaxRotationInterval / 3
}
+3 -26
oauth/provider/client_auth.go
···
import (
"context"
"crypto"
-
"database/sql/driver"
"encoding/base64"
-
"encoding/json"
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
-
"github.com/haileyok/cocoon/oauth"
+
"github.com/haileyok/cocoon/oauth/client"
"github.com/haileyok/cocoon/oauth/constants"
"github.com/haileyok/cocoon/oauth/dpop"
)
-
type ClientAuth struct {
-
Method string
-
Alg string
-
Kid string
-
Jkt string
-
Jti string
-
Exp *float64
-
}
-
-
func (ca *ClientAuth) Scan(value any) error {
-
b, ok := value.([]byte)
-
if !ok {
-
return fmt.Errorf("failed to unmarshal OauthParRequest value")
-
}
-
return json.Unmarshal(b, ca)
-
}
-
-
func (ca ClientAuth) Value() (driver.Value, error) {
-
return json.Marshal(ca)
-
}
-
type AuthenticateClientOptions struct {
AllowMissingDpopProof bool
}
···
ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty"`
}
-
func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*oauth.Client, *ClientAuth, error) {
+
func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) {
client, err := p.ClientManager.GetClient(ctx, req.ClientID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get client: %w", err)
···
return client, clientAuth, nil
}
-
func (p *Provider) Authenticate(_ context.Context, req AuthenticateClientRequestBase, client *oauth.Client) (*ClientAuth, error) {
+
func (p *Provider) Authenticate(_ context.Context, req AuthenticateClientRequestBase, client *client.Client) (*ClientAuth, error) {
metadata := client.Metadata
if metadata.TokenEndpointAuthMethod == "none" {
+81
oauth/provider/models.go
···
+
package provider
+
+
import (
+
"database/sql/driver"
+
"encoding/json"
+
"fmt"
+
"time"
+
+
"gorm.io/gorm"
+
)
+
+
type ClientAuth struct {
+
Method string
+
Alg string
+
Kid string
+
Jkt string
+
Jti string
+
Exp *float64
+
}
+
+
func (ca *ClientAuth) Scan(value any) error {
+
b, ok := value.([]byte)
+
if !ok {
+
return fmt.Errorf("failed to unmarshal OauthParRequest value")
+
}
+
return json.Unmarshal(b, ca)
+
}
+
+
func (ca ClientAuth) Value() (driver.Value, error) {
+
return json.Marshal(ca)
+
}
+
+
type ParRequest struct {
+
AuthenticateClientRequestBase
+
ResponseType string `form:"response_type" json:"response_type" validate:"required"`
+
CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"`
+
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"`
+
State string `form:"state" json:"state" validate:"required"`
+
RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"`
+
Scope string `form:"scope" json:"scope" validate:"required"`
+
LoginHint *string `form:"login_hint" json:"login_hint,omitempty"`
+
DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"`
+
}
+
+
func (opr *ParRequest) Scan(value any) error {
+
b, ok := value.([]byte)
+
if !ok {
+
return fmt.Errorf("failed to unmarshal OauthParRequest value")
+
}
+
return json.Unmarshal(b, opr)
+
}
+
+
func (opr ParRequest) Value() (driver.Value, error) {
+
return json.Marshal(opr)
+
}
+
+
type OauthToken struct {
+
gorm.Model
+
ClientId string `gorm:"index"`
+
ClientAuth ClientAuth `gorm:"type:json"`
+
Parameters ParRequest `gorm:"type:json"`
+
ExpiresAt time.Time `gorm:"index"`
+
DeviceId string
+
Sub string `gorm:"index"`
+
Code string `gorm:"index"`
+
Token string `gorm:"uniqueIndex"`
+
RefreshToken string `gorm:"uniqueIndex"`
+
}
+
+
type OauthAuthorizationRequest struct {
+
gorm.Model
+
RequestId string `gorm:"primaryKey"`
+
ClientId string `gorm:"index"`
+
ClientAuth ClientAuth `gorm:"type:json"`
+
Parameters ParRequest `gorm:"type:json"`
+
ExpiresAt time.Time `gorm:"index"`
+
DeviceId *string
+
Sub *string
+
Code *string
+
Accepted *bool
+
}
+8 -64
oauth/provider/provider.go
···
package provider
import (
-
"database/sql/driver"
-
"encoding/json"
-
"fmt"
-
"time"
-
-
"github.com/haileyok/cocoon/oauth/client_manager"
-
"github.com/haileyok/cocoon/oauth/dpop/dpop_manager"
-
"gorm.io/gorm"
+
"github.com/haileyok/cocoon/oauth/client"
+
"github.com/haileyok/cocoon/oauth/dpop"
)
type Provider struct {
-
ClientManager *client_manager.ClientManager
-
DpopManager *dpop_manager.DpopManager
+
ClientManager *client.Manager
+
DpopManager *dpop.Manager
hostname string
}
type Args struct {
Hostname string
-
ClientManagerArgs client_manager.Args
-
DpopManagerArgs dpop_manager.Args
+
ClientManagerArgs client.ManagerArgs
+
DpopManagerArgs dpop.ManagerArgs
}
func NewProvider(args Args) *Provider {
return &Provider{
-
ClientManager: client_manager.New(args.ClientManagerArgs),
-
DpopManager: dpop_manager.New(args.DpopManagerArgs),
+
ClientManager: client.NewManager(args.ClientManagerArgs),
+
DpopManager: dpop.NewManager(args.DpopManagerArgs),
hostname: args.Hostname,
}
}
···
func (p *Provider) NextNonce() string {
return p.DpopManager.NextNonce()
}
-
-
type ParRequest struct {
-
AuthenticateClientRequestBase
-
ResponseType string `form:"response_type" json:"response_type" validate:"required"`
-
CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"`
-
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"`
-
State string `form:"state" json:"state" validate:"required"`
-
RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"`
-
Scope string `form:"scope" json:"scope" validate:"required"`
-
LoginHint *string `form:"login_hint" json:"login_hint,omitempty"`
-
DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"`
-
}
-
-
func (opr *ParRequest) Scan(value any) error {
-
b, ok := value.([]byte)
-
if !ok {
-
return fmt.Errorf("failed to unmarshal OauthParRequest value")
-
}
-
return json.Unmarshal(b, opr)
-
}
-
-
func (opr ParRequest) Value() (driver.Value, error) {
-
return json.Marshal(opr)
-
}
-
-
type OauthToken struct {
-
gorm.Model
-
ClientId string `gorm:"index"`
-
ClientAuth ClientAuth `gorm:"type:json"`
-
Parameters ParRequest `gorm:"type:json"`
-
ExpiresAt time.Time `gorm:"index"`
-
DeviceId string
-
Sub string `gorm:"index"`
-
Code string `gorm:"index"`
-
Token string `gorm:"uniqueIndex"`
-
RefreshToken string `gorm:"uniqueIndex"`
-
}
-
-
type OauthAuthorizationRequest struct {
-
gorm.Model
-
RequestId string `gorm:"primaryKey"`
-
ClientId string `gorm:"index"`
-
ClientAuth ClientAuth `gorm:"type:json"`
-
Parameters ParRequest `gorm:"type:json"`
-
ExpiresAt time.Time `gorm:"index"`
-
DeviceId *string
-
Sub *string
-
Code *string
-
Accepted *bool
-
}
+4 -4
server/server.go
···
"github.com/haileyok/cocoon/internal/db"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
-
"github.com/haileyok/cocoon/oauth/client_manager"
+
"github.com/haileyok/cocoon/oauth/client"
"github.com/haileyok/cocoon/oauth/constants"
-
"github.com/haileyok/cocoon/oauth/dpop/dpop_manager"
+
"github.com/haileyok/cocoon/oauth/dpop"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/haileyok/cocoon/plc"
echo_session "github.com/labstack/echo-contrib/session"
···
oauthProvider: provider.NewProvider(provider.Args{
Hostname: args.Hostname,
-
ClientManagerArgs: client_manager.Args{
+
ClientManagerArgs: client.ManagerArgs{
Cli: oauthCli,
Logger: args.Logger,
},
-
DpopManagerArgs: dpop_manager.Args{
+
DpopManagerArgs: dpop.ManagerArgs{
NonceSecret: nonceSecret,
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
OnNonceSecretCreated: func(newNonce []byte) {