-
Notifications
You must be signed in to change notification settings - Fork 0
/
tokener.go
225 lines (199 loc) · 6.8 KB
/
tokener.go
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package tokener
import (
"context"
"encoding/binary"
"encoding/json"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/neuronlabs/neuron/auth"
"github.com/neuronlabs/neuron/controller"
"github.com/neuronlabs/neuron/errors"
"github.com/neuronlabs/neuron/log"
"github.com/neuronlabs/neuron/store"
)
// Tokener is neuron auth.Tokener implementation for the jwt.Token.
// It allows to store and inspect token encrypted using HMAC, RSA and ECDSA algorithms.
// This structure requires store.Store to keep the revoked tokens values.
// For production ready services don't use in-memory default store.
type Tokener struct {
Parser jwt.Parser
Store store.Store
Options auth.TokenerOptions
c *controller.Controller
signingKey, validateKey interface{}
}
// New creates new Tokener with provided 'options'.
func New(options ...auth.TokenerOption) (*Tokener, error) {
o := &auth.TokenerOptions{
TokenExpiration: time.Minute * 10,
RefreshTokenExpiration: time.Hour * 24,
}
for _, option := range options {
option(o)
}
t := &Tokener{
Parser: jwt.Parser{SkipClaimsValidation: true},
Options: *o,
}
// Set the signing and validate keys for given options.
switch t.Options.SigningMethod.(type) {
case *jwt.SigningMethodRSA:
if t.Options.RsaPrivateKey == nil {
return nil, errors.Wrap(auth.ErrInvalidRSAKey, "no rsa key provided for given RSA token signing method")
}
t.signingKey = t.Options.RsaPrivateKey
t.validateKey = t.Options.RsaPrivateKey.PublicKey
case *jwt.SigningMethodHMAC:
if len(t.Options.Secret) == 0 {
return nil, errors.Wrap(auth.ErrInvalidSecret, "no secret provided for the HMAC token signing method")
}
t.signingKey, t.validateKey = t.Options.Secret, t.Options.Secret
case *jwt.SigningMethodECDSA:
if t.Options.EcdsaPrivateKey == nil {
return nil, errors.Wrap(auth.ErrInvalidECDSAKey, "no ecdsa key provided for given ECDSA token signing method")
}
t.signingKey = t.Options.EcdsaPrivateKey
t.validateKey = t.Options.EcdsaPrivateKey.PublicKey
default:
return nil, errors.Wrap(auth.ErrInitialization, "provided unsupported signing method")
}
return t, nil
}
// Initialize implements core.Initializer interface.
func (t *Tokener) Initialize(c *controller.Controller) error {
t.c = c
if t.Store == nil {
if c.DefaultStore == nil {
return errors.Wrap(auth.ErrInitialization, "no store found for the authenticator")
}
t.Store = c.DefaultStore
}
return nil
}
// InspectToken inspects given token string and returns provided claims.
func (t *Tokener) InspectToken(ctx context.Context, token string) (auth.Claims, error) {
mapClaims, err := t.inspectToken(ctx, token)
if err != nil {
return nil, err
}
var claims auth.Claims
_, isAccess := mapClaims["account"]
if !isAccess {
if _, ok := mapClaims["account_id"]; !ok {
return nil, errors.Wrap(auth.ErrToken, "provided token with invalid claims")
}
claims = &RefreshClaims{}
} else {
claims = &AccessClaims{}
}
marshaled, err := json.Marshal(mapClaims)
if err != nil {
return nil, errors.Wrap(auth.ErrInternalError, "marshaling map claims failed")
}
if err = json.Unmarshal(marshaled, claims); err != nil {
return nil, errors.Wrap(auth.ErrInternalError, "unmarshaling claims failed")
}
return claims, nil
}
// Token creates an auth.Token from provided options.
func (t *Tokener) Token(account auth.Account, options ...auth.TokenOption) (auth.Token, error) {
o := &auth.TokenOptions{
ExpirationTime: t.Options.TokenExpiration,
RefreshExpirationTime: t.Options.RefreshTokenExpiration,
}
for _, option := range options {
option(o)
}
if account == nil {
return auth.Token{}, errors.Wrap(auth.ErrNoRequiredOption, "provided no account in the token creation")
}
// Set the claims for the full token.
claims := &AccessClaims{
Account: account,
Claims: Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: t.c.Now().Add(o.ExpirationTime).Unix(),
},
},
}
token := jwt.NewWithClaims(t.Options.SigningMethod, claims)
tokenString, err := token.SignedString(t.signingKey)
if err != nil {
return auth.Token{}, errors.Wrapf(auth.ErrInternalError, "writing signed string failed: %v", err)
}
// Get string value for the account's primary key.
stringID, err := account.GetPrimaryKeyStringValue()
if err != nil {
return auth.Token{}, errors.Wrapf(auth.ErrInternalError, "getting account primary key string value failed: %v", err)
}
// Create and sign refresh token.
refClaims := &RefreshClaims{
AccountID: stringID,
Claims: Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: t.c.Now().Add(t.Options.RefreshTokenExpiration).Unix(),
},
},
}
refreshToken := jwt.NewWithClaims(t.Options.SigningMethod, refClaims)
refreshTokenString, err := refreshToken.SignedString(t.signingKey)
if err != nil {
return auth.Token{}, errors.Wrapf(auth.ErrInternalError, "writing signed string failed: %v", err)
}
return auth.Token{
AccessToken: tokenString,
RefreshToken: refreshTokenString,
ExpiresIn: int(o.ExpirationTime / time.Second),
TokenType: "bearer",
}, nil
}
// RevokeToken invalidates provided 'token'.
func (t *Tokener) RevokeToken(ctx context.Context, token string) error {
claims := Claims{}
if _, err := t.inspectToken(ctx, token); err != nil {
return err
}
now := jwt.TimeFunc().Unix()
ttl := time.Unix(now, 0).Sub(time.Unix(claims.ExpiresAt, 0))
value := make([]byte, 8)
binary.BigEndian.PutUint64(value, uint64(now))
err := t.Store.SetWithTTL(ctx, &store.Record{Key: t.revokeKey(token), Value: value, ExpiresAt: t.c.Now().Add(ttl)}, ttl)
if err != nil {
return err
}
return nil
}
func (t *Tokener) inspectToken(ctx context.Context, token string) (jwt.MapClaims, error) {
// Initialize jwt.MapClaims.
claims := jwt.MapClaims{}
_, err := t.Parser.ParseWithClaims(token, claims, func(tk *jwt.Token) (interface{}, error) {
if tk.Method != t.Options.SigningMethod {
return nil, errors.Wrap(auth.ErrToken, "provided invalid signing algorithm for the token")
}
return t.validateKey, nil
})
if err != nil {
if !errors.Is(err, auth.ErrToken) {
return nil, errors.Wrapf(auth.ErrToken, "parsing token failed: %v", err)
}
return nil, err
}
// Check if the token is not set as revoked.
record, err := t.Store.Get(ctx, t.revokeKey(token))
if err != nil {
// If the token was not revoked than the error would be of store.ErrValueNotFound.
if errors.Is(err, store.ErrRecordNotFound) {
return claims, nil
}
log.Errorf("Getting token info from store failed: %v", err)
return nil, err
}
// Set the revoked at field.
revokedAt := binary.BigEndian.Uint64(record.Value)
// The store had marked this token as revoked.
claims["revoked_at"] = revokedAt
return claims, errors.Wrap(auth.ErrTokenRevoked, "provided token had been revoked")
}
func (t *Tokener) revokeKey(token string) string {
return "nrn_jwt_auth_revoked-" + token
}