aboutsummaryrefslogtreecommitdiff
path: root/auth/jwt.go
diff options
context:
space:
mode:
Diffstat (limited to 'auth/jwt.go')
-rw-r--r--auth/jwt.go123
1 files changed, 123 insertions, 0 deletions
diff --git a/auth/jwt.go b/auth/jwt.go
new file mode 100644
index 0000000..c65cf39
--- /dev/null
+++ b/auth/jwt.go
@@ -0,0 +1,123 @@
1package auth
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "code.crute.us/mcrute/cloud-identity-broker/app/models"
9
10 "gopkg.in/square/go-jose.v2"
11 "gopkg.in/square/go-jose.v2/jwt"
12)
13
14const keyIdHeader = jose.HeaderKey("kid")
15
16type JWTManager struct {
17 Store models.UserStore
18 Audience string
19 TokenExpires time.Duration
20}
21
22// Validate performs validate of a JWT for authentication. If the
23// authentication was successful it will return the user model for the
24// authenticated user.
25//
26// JWTs are signed by a per-user key on the service side. These keys may be
27// generated automatically when issuing a token to the user or, in the case of
28// service accounts, they may be generated by the user and validated on the
29// service side with the user's public key.
30func (m *JWTManager) Validate(j string) (*models.User, error) {
31 ctx := context.Background()
32
33 parsed, err := jwt.ParseSigned(j)
34 if err != nil {
35 return nil, err
36 }
37
38 // With a single signature there should be exactly one header. Without that
39 // we can't get the key id which means we won't be able to get the key and
40 // validate the signature.
41 if len(parsed.Headers) != 1 {
42 return nil, fmt.Errorf("Expected exactly 1 JWT header, got %d", len(parsed.Headers))
43 }
44
45 kid := parsed.Headers[0].KeyID
46 if kid == "" {
47 return nil, fmt.Errorf("No key ID in token header")
48 }
49
50 // We need the subject claim to lookup the user so we can get their key and
51 // validate the token signature.
52 untrustedClaims := jwt.Claims{}
53 if err := parsed.UnsafeClaimsWithoutVerification(&untrustedClaims); err != nil {
54 return nil, err
55 }
56
57 user, err := m.Store.Get(ctx, untrustedClaims.Subject)
58 if err != nil {
59 return nil, err
60 }
61
62 key := user.GetKey(kid) // Will not return invalid keys
63 if key == nil {
64 return nil, fmt.Errorf("No key in user record for key id %s", kid)
65 }
66
67 claims := jwt.Claims{}
68 if err = parsed.Claims(key.PublicKey, &claims); err != nil {
69 return nil, err
70 }
71
72 if err = claims.Validate(jwt.Expected{
73 Audience: jwt.Audience{m.Audience},
74 Time: time.Now(), // +/- 1 minute
75 }); err != nil {
76 return nil, err
77 }
78
79 // If we made it here then the user matches the public key used to sign the
80 // token and the claims are verified so just return the user record we
81 // fetched earlier.
82
83 return user, nil
84}
85
86// CreateForUser creates a new JWT based on the passed user. In some cases it
87// may also need to generate a new SessionKey for the purpose of encrypting
88// those JWTs. Callers should be sure to save the returned SessionKey, if there
89// is one, to the user when setting the token otherwise the token signature
90// will not be able to be validated on subsequent requests.
91func (m *JWTManager) CreateForUser(u *models.User) (string, *models.SessionKey, error) {
92 pk, err := models.GenerateSessionKey(m.TokenExpires)
93 if err != nil {
94 return "", nil, err
95 }
96
97 // The ExtraHeaders bit is kind of an ugly hack but there's no way in the
98 // official API to set the KeyID for single-signer tokens. However, if the
99 // magic "kid" label is set in the extra headers it will be set as the
100 // KeyID in the header when that's deserialized.
101 signer, err := jose.NewSigner(jose.SigningKey{
102 Algorithm: jose.ES256,
103 Key: pk.PrivateKey,
104 }, &jose.SignerOptions{
105 ExtraHeaders: map[jose.HeaderKey]interface{}{
106 keyIdHeader: pk.KeyId,
107 },
108 })
109 if err != nil {
110 return "", nil, err
111 }
112
113 now := time.Now()
114 j, err := jwt.Signed(signer).Claims(jwt.Claims{
115 Issuer: m.Audience,
116 Subject: u.Username,
117 Audience: jwt.Audience{m.Audience},
118 Expiry: jwt.NewNumericDate(now.Add(m.TokenExpires)),
119 IssuedAt: jwt.NewNumericDate(now),
120 }).CompactSerialize()
121
122 return j, pk, err
123}