+2
-2
oauth/client.go
oauth/client/client.go
+2
-2
oauth/client.go
oauth/client/client.go
+13
-14
oauth/client_manager/client_manager.go
oauth/client/manager.go
+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
+2
-2
oauth/client_metadata.go
oauth/client/metadata.go
+10
-12
oauth/dpop/dpop_manager/dpop_manager.go
oauth/dpop/manager.go
+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
-1
oauth/dpop/dpop_manager/jti_cache.go
oauth/dpop/jti_cache.go
+3
-3
oauth/dpop/nonce/nonce.go
oauth/dpop/nonce.go
+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
-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
+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
+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
+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) {