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
|
package auth
import (
"context"
"fmt"
"time"
"code.crute.us/mcrute/cloud-identity-broker/app/models"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)
const keyIdHeader = jose.HeaderKey("kid")
type JWTManager struct {
Store models.UserStore
Audience string
TokenExpires time.Duration
}
// Validate performs validate of a JWT for authentication. If the
// authentication was successful it will return the user model for the
// authenticated user.
//
// JWTs are signed by a per-user key on the service side. These keys may be
// generated automatically when issuing a token to the user or, in the case of
// service accounts, they may be generated by the user and validated on the
// service side with the user's public key.
func (m *JWTManager) Validate(j string) (*models.User, error) {
ctx := context.Background()
parsed, err := jwt.ParseSigned(j)
if err != nil {
return nil, err
}
// With a single signature there should be exactly one header. Without that
// we can't get the key id which means we won't be able to get the key and
// validate the signature.
if len(parsed.Headers) != 1 {
return nil, fmt.Errorf("Expected exactly 1 JWT header, got %d", len(parsed.Headers))
}
kid := parsed.Headers[0].KeyID
if kid == "" {
return nil, fmt.Errorf("No key ID in token header")
}
// We need the subject claim to lookup the user so we can get their key and
// validate the token signature.
untrustedClaims := jwt.Claims{}
if err := parsed.UnsafeClaimsWithoutVerification(&untrustedClaims); err != nil {
return nil, err
}
user, err := m.Store.Get(ctx, untrustedClaims.Subject)
if err != nil {
return nil, err
}
key := user.GetKey(kid) // Will not return invalid keys
if key == nil {
return nil, fmt.Errorf("No key in user record for key id %s", kid)
}
claims := jwt.Claims{}
if err = parsed.Claims(key.PublicKey, &claims); err != nil {
return nil, err
}
if err = claims.Validate(jwt.Expected{
Audience: jwt.Audience{m.Audience},
Time: time.Now(), // +/- 1 minute
}); err != nil {
return nil, err
}
// If we made it here then the user matches the public key used to sign the
// token and the claims are verified so just return the user record we
// fetched earlier.
return user, nil
}
// CreateForUser creates a new JWT based on the passed user. In some cases it
// may also need to generate a new SessionKey for the purpose of encrypting
// those JWTs. Callers should be sure to save the returned SessionKey, if there
// is one, to the user when setting the token otherwise the token signature
// will not be able to be validated on subsequent requests.
func (m *JWTManager) CreateForUser(u *models.User) (string, *models.SessionKey, error) {
pk, err := models.GenerateSessionKey(m.TokenExpires)
if err != nil {
return "", nil, err
}
// The ExtraHeaders bit is kind of an ugly hack but there's no way in the
// official API to set the KeyID for single-signer tokens. However, if the
// magic "kid" label is set in the extra headers it will be set as the
// KeyID in the header when that's deserialized.
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.ES256,
Key: pk.PrivateKey,
}, &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]interface{}{
keyIdHeader: pk.KeyId,
},
})
if err != nil {
return "", nil, err
}
now := time.Now()
j, err := jwt.Signed(signer).Claims(jwt.Claims{
Issuer: m.Audience,
Subject: u.Username,
Audience: jwt.Audience{m.Audience},
Expiry: jwt.NewNumericDate(now.Add(m.TokenExpires)),
IssuedAt: jwt.NewNumericDate(now),
}).Claims(map[string]interface{}{
"admin": u.IsAdmin, // Advisory, for UI, the server must never trust this
}).CompactSerialize()
return j, pk, err
}
|