diff options
Diffstat (limited to 'auth/jwt.go')
-rw-r--r-- | auth/jwt.go | 123 |
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 @@ | |||
1 | package auth | ||
2 | |||
3 | import ( | ||
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 | |||
14 | const keyIdHeader = jose.HeaderKey("kid") | ||
15 | |||
16 | type 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. | ||
30 | func (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. | ||
91 | func (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 | } | ||