-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathjwt_manager.go
More file actions
181 lines (148 loc) · 5.88 KB
/
Copy pathjwt_manager.go
File metadata and controls
181 lines (148 loc) · 5.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package auth
import (
"crypto/ed25519"
"errors"
"fmt"
"strings"
"time"
jwtgo "github.com/golang-jwt/jwt/v5"
"github.com/stellar/go-stellar-sdk/keypair"
"github.com/stellar/go-stellar-sdk/strkey"
)
const DefaultMaxTimeout = 15 * time.Second
type JWTManager struct {
PrivateKey string
PublicKey string
MaxTimeout time.Duration
}
type JWTTokenParser interface {
// ParseJWT parses a JWT token and returns it with the claims.
ParseJWT(tokenString, methodAndPath string, body []byte) (*jwtgo.Token, *customClaims, error)
}
type JWTTokenGenerator interface {
// GenerateJWT generates a JWT token with the given body and expiration time.
GenerateJWT(methodAndPath string, body []byte, expiresAt time.Time) (string, error)
}
// ParseJWT parses a JWT token and returns it with the claims. It also checks if the token expiration is within [now,
// now+MaxTimeout], and if the claims' hashed_body matches the requestBody's hash.
func (m *JWTManager) ParseJWT(tokenString, methodAndPath string, body []byte) (*jwtgo.Token, *customClaims, error) {
claims := &customClaims{}
err := claims.DecodeTokenString(tokenString)
if err != nil {
return nil, nil, fmt.Errorf("decoding JWT token: %w", err)
}
err = claims.Validate(methodAndPath, body, m.MaxTimeout)
if err != nil {
return nil, nil, fmt.Errorf("pre-validating JWT token claims: %w", err)
}
if claims.Subject != m.PublicKey {
return nil, nil, fmt.Errorf("the JWT token is not signed by the expected Stellar public key")
}
token, err := jwtgo.ParseWithClaims(tokenString, claims, func(t *jwtgo.Token) (interface{}, error) {
pubKeyBytes, innerErr := strkey.Decode(strkey.VersionByteAccountID, m.PublicKey)
if innerErr != nil {
return nil, fmt.Errorf("decoding Stellar public key: %w", innerErr)
}
return ed25519.PublicKey(pubKeyBytes), nil
})
if err != nil {
if errors.Is(err, jwtgo.ErrTokenExpired) {
return nil, nil, &ExpiredTokenError{ExpiredBy: time.Since(claims.ExpiresAt.Time), InnerErr: err}
}
return nil, nil, fmt.Errorf("parsing JWT token with claims: %w", err)
}
return token, claims, nil
}
// GenerateJWT generates a JWT token with the given body and expiration time.
func (m *JWTManager) GenerateJWT(methodAndPath string, body []byte, expiresAt time.Time) (string, error) {
claims := &customClaims{
BodyHash: HashBody(body),
MethodAndPath: strings.TrimSpace(methodAndPath),
RegisteredClaims: jwtgo.RegisteredClaims{
IssuedAt: jwtgo.NewNumericDate(time.Now()),
ExpiresAt: jwtgo.NewNumericDate(expiresAt),
Subject: m.PublicKey,
},
}
token := jwtgo.NewWithClaims(jwtgo.SigningMethodEdDSA, claims)
privateKeyBytes, err := strkey.Decode(strkey.VersionByteSeed, m.PrivateKey)
if err != nil {
return "", fmt.Errorf("decoding Stellar private key: %w", err)
}
tokenString, err := token.SignedString(ed25519.NewKeyFromSeed(privateKeyBytes))
if err != nil {
return "", fmt.Errorf("signing JWT token with claims: %w", err)
}
return tokenString, nil
}
// NewJWTManager creates a new JWT token manager that can generate and parse JWT tokens.
func NewJWTManager(stellarPrivateKey string, stellarPublicKey string, maxTimeout time.Duration) (*JWTManager, error) {
if !strkey.IsValidEd25519PublicKey(stellarPublicKey) {
return nil, fmt.Errorf("invalid Stellar public key")
}
if !strkey.IsValidEd25519SecretSeed(stellarPrivateKey) {
return nil, fmt.Errorf("invalid Stellar private key")
}
if maxTimeout <= 0 {
maxTimeout = DefaultMaxTimeout
}
return &JWTManager{
PrivateKey: stellarPrivateKey,
PublicKey: stellarPublicKey,
MaxTimeout: maxTimeout,
}, nil
}
// NewJWTTokenParser creates a new JWT token parser that can parse a JWT token as long as it has been signed by the provided Stellar public key.
func NewJWTTokenParser(maxTimeout time.Duration, stellarPublicKey string) (JWTTokenParser, error) {
if !strkey.IsValidEd25519PublicKey(stellarPublicKey) {
return nil, fmt.Errorf("invalid Stellar public key")
}
if maxTimeout <= 0 {
maxTimeout = DefaultMaxTimeout
}
return &JWTManager{PublicKey: stellarPublicKey, MaxTimeout: maxTimeout}, nil
}
// NewMultiJWTTokenParser creates a new JWT token parser that can parse a JWT token as long as it has been signed by an least one of the provided Stellar public keys.
func NewMultiJWTTokenParser(maxTimeout time.Duration, stellarPublicKeys ...string) (JWTTokenParser, error) {
if len(stellarPublicKeys) == 0 {
return nil, fmt.Errorf("no Stellar public keys provided")
}
jwtParsers := map[string]JWTTokenParser{}
for _, pubKey := range stellarPublicKeys {
jwtParser, err := NewJWTTokenParser(maxTimeout, pubKey)
if err != nil {
return nil, fmt.Errorf("creating JWT parser: %w", err)
}
jwtParsers[pubKey] = jwtParser
}
return &MultiJWTTokenParser{jwtParsers}, nil
}
type MultiJWTTokenParser struct {
// jwtParsers is a map of Stellar public keys to JWT token parsers.
jwtParsers map[string]JWTTokenParser
}
func (m MultiJWTTokenParser) ParseJWT(tokenString, methodAndPath string, body []byte) (*jwtgo.Token, *customClaims, error) {
claims := &customClaims{}
err := claims.DecodeTokenString(tokenString)
if err != nil {
return nil, nil, fmt.Errorf("decoding JWT token: %w", err)
}
jwtParser, ok := m.jwtParsers[claims.Subject]
if !ok {
return nil, nil, fmt.Errorf("the JWT token subject %s is not one of the expected Stellar public keys", claims.Subject)
}
//nolint:wrapcheck // we're ok not wrapping the error here because we're not adding any additional context
return jwtParser.ParseJWT(tokenString, methodAndPath, body)
}
func NewJWTTokenGenerator(stellarPrivateKey string) (JWTTokenGenerator, error) {
kp, err := keypair.ParseFull(stellarPrivateKey)
if err != nil {
return nil, fmt.Errorf("invalid Stellar private key: %w", err)
}
return &JWTManager{PrivateKey: kp.Seed(), PublicKey: kp.Address()}, nil
}
var (
_ JWTTokenParser = (*JWTManager)(nil)
_ JWTTokenParser = (*MultiJWTTokenParser)(nil)
_ JWTTokenGenerator = (*JWTManager)(nil)
)