auth
auth/token.go
Create interfaces for login verification flow. We needed an interface that we could use to say "send the email to verify the user's login" so that we could verify the emails we have are actually valid. This implements an NSQ version that sends an email_verification event. We'll get listener implementations that pull these messages off NSQ and actually send the emails. This also implements, for testing purposes, a version that just echoes the Login Value and the Verification code to stdout.
| paddy@28 | 1 package auth |
| paddy@28 | 2 |
| paddy@28 | 3 import ( |
| paddy@123 | 4 "encoding/json" |
| paddy@28 | 5 "errors" |
| paddy@123 | 6 "log" |
| paddy@123 | 7 "net/http" |
| paddy@168 | 8 "strings" |
| paddy@28 | 9 "time" |
| paddy@28 | 10 |
| paddy@107 | 11 "code.secondbit.org/uuid.hg" |
| paddy@168 | 12 |
| paddy@168 | 13 "github.com/dgrijalva/jwt-go" |
| paddy@28 | 14 ) |
| paddy@28 | 15 |
| paddy@69 | 16 const ( |
| paddy@168 | 17 defaultTokenExpiration = 900 // fifteen minutes |
| paddy@69 | 18 ) |
| paddy@69 | 19 |
| paddy@123 | 20 func init() { |
| paddy@123 | 21 RegisterGrantType("refresh_token", GrantType{ |
| paddy@123 | 22 Validate: refreshTokenValidate, |
| paddy@123 | 23 Invalidate: refreshTokenInvalidate, |
| paddy@123 | 24 IssuesRefresh: true, |
| paddy@123 | 25 ReturnToken: RenderJSONToken, |
| paddy@124 | 26 AuditString: refreshTokenAuditString, |
| paddy@123 | 27 }) |
| paddy@123 | 28 } |
| paddy@123 | 29 |
| paddy@28 | 30 var ( |
| paddy@57 | 31 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first. |
| paddy@57 | 32 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context") |
| paddy@57 | 33 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore. |
| paddy@57 | 34 ErrTokenNotFound = errors.New("token not found in tokenStore") |
| paddy@57 | 35 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with |
| paddy@57 | 36 // the same AccessToken property already exists in the tokenStore. |
| paddy@57 | 37 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore") |
| paddy@28 | 38 ) |
| paddy@28 | 39 |
| paddy@57 | 40 // Token represents an access and/or refresh token that the Client can use to access user data |
| paddy@57 | 41 // or obtain a new access token. |
| paddy@28 | 42 type Token struct { |
| paddy@168 | 43 AccessToken string |
| paddy@168 | 44 RefreshToken string |
| paddy@168 | 45 Created time.Time |
| paddy@168 | 46 CreatedFrom string |
| paddy@168 | 47 ExpiresIn int32 |
| paddy@168 | 48 TokenType string |
| paddy@168 | 49 Scopes Scopes |
| paddy@168 | 50 ProfileID uuid.ID |
| paddy@168 | 51 ClientID uuid.ID |
| paddy@168 | 52 Revoked bool |
| paddy@28 | 53 } |
| paddy@28 | 54 |
| paddy@168 | 55 func (t Token) GenerateAccessToken(privateKey []byte) (string, error) { |
| paddy@168 | 56 access := jwt.New(jwt.SigningMethodHS256) |
| paddy@168 | 57 access.Claims["iss"] = t.ClientID |
| paddy@168 | 58 access.Claims["sub"] = t.ProfileID |
| paddy@168 | 59 access.Claims["exp"] = t.Created.Add(defaultTokenExpiration * time.Second).Unix() |
| paddy@168 | 60 access.Claims["nbf"] = t.Created.Add(-2 * time.Minute).Unix() |
| paddy@168 | 61 access.Claims["iat"] = t.Created.Unix() |
| paddy@168 | 62 access.Claims["scope"] = strings.Join(t.Scopes.Strings(), " ") |
| paddy@168 | 63 return access.SignedString(privateKey) |
| paddy@168 | 64 } |
| paddy@168 | 65 |
| paddy@168 | 66 // BUG(paddy): Now that access tokens are generated and have a meaning, refresh tokens should be the primary key |
| paddy@168 | 67 |
| paddy@57 | 68 type tokenStore interface { |
| paddy@57 | 69 getToken(token string, refresh bool) (Token, error) |
| paddy@57 | 70 saveToken(token Token) error |
| paddy@168 | 71 revokeToken(token string) error |
| paddy@57 | 72 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) |
| paddy@162 | 73 revokeTokensByProfileID(profileID uuid.ID) error |
| paddy@164 | 74 revokeTokensByClientID(clientID uuid.ID) error |
| paddy@28 | 75 } |
| paddy@28 | 76 |
| paddy@57 | 77 func (m *memstore) getToken(token string, refresh bool) (Token, error) { |
| paddy@28 | 78 if refresh { |
| paddy@28 | 79 t, err := m.lookupTokenByRefresh(token) |
| paddy@28 | 80 if err != nil { |
| paddy@28 | 81 return Token{}, err |
| paddy@28 | 82 } |
| paddy@28 | 83 token = t |
| paddy@28 | 84 } |
| paddy@28 | 85 m.tokenLock.RLock() |
| paddy@28 | 86 defer m.tokenLock.RUnlock() |
| paddy@28 | 87 result, ok := m.tokens[token] |
| paddy@28 | 88 if !ok { |
| paddy@28 | 89 return Token{}, ErrTokenNotFound |
| paddy@28 | 90 } |
| paddy@28 | 91 return result, nil |
| paddy@28 | 92 } |
| paddy@28 | 93 |
| paddy@57 | 94 func (m *memstore) saveToken(token Token) error { |
| paddy@28 | 95 m.tokenLock.Lock() |
| paddy@28 | 96 defer m.tokenLock.Unlock() |
| paddy@28 | 97 _, ok := m.tokens[token.AccessToken] |
| paddy@28 | 98 if ok { |
| paddy@28 | 99 return ErrTokenAlreadyExists |
| paddy@28 | 100 } |
| paddy@28 | 101 m.tokens[token.AccessToken] = token |
| paddy@28 | 102 if token.RefreshToken != "" { |
| paddy@28 | 103 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken |
| paddy@28 | 104 } |
| paddy@28 | 105 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok { |
| paddy@28 | 106 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken) |
| paddy@28 | 107 } else { |
| paddy@28 | 108 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken} |
| paddy@28 | 109 } |
| paddy@28 | 110 return nil |
| paddy@28 | 111 } |
| paddy@28 | 112 |
| paddy@168 | 113 func (m *memstore) revokeToken(token string) error { |
| paddy@168 | 114 token, err := m.lookupTokenByRefresh(token) |
| paddy@168 | 115 if err != nil { |
| paddy@168 | 116 return err |
| paddy@91 | 117 } |
| paddy@91 | 118 m.tokenLock.Lock() |
| paddy@91 | 119 defer m.tokenLock.Unlock() |
| paddy@91 | 120 t, ok := m.tokens[token] |
| paddy@91 | 121 if !ok { |
| paddy@91 | 122 return ErrTokenNotFound |
| paddy@91 | 123 } |
| paddy@168 | 124 t.Revoked = true |
| paddy@91 | 125 m.tokens[token] = t |
| paddy@91 | 126 return nil |
| paddy@91 | 127 } |
| paddy@91 | 128 |
| paddy@162 | 129 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error { |
| paddy@162 | 130 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@162 | 131 if err != nil { |
| paddy@162 | 132 return err |
| paddy@162 | 133 } |
| paddy@162 | 134 if len(ids) < 1 { |
| paddy@162 | 135 return ErrProfileNotFound |
| paddy@162 | 136 } |
| paddy@162 | 137 m.tokenLock.Lock() |
| paddy@162 | 138 defer m.tokenLock.Unlock() |
| paddy@162 | 139 for _, id := range ids { |
| paddy@164 | 140 token := m.tokens[id] |
| paddy@164 | 141 token.Revoked = true |
| paddy@164 | 142 m.tokens[id] = token |
| paddy@164 | 143 } |
| paddy@164 | 144 return nil |
| paddy@164 | 145 } |
| paddy@164 | 146 |
| paddy@164 | 147 func (m *memstore) revokeTokensByClientID(clientID uuid.ID) error { |
| paddy@164 | 148 m.tokenLock.Lock() |
| paddy@164 | 149 defer m.tokenLock.Unlock() |
| paddy@164 | 150 for id, token := range m.tokens { |
| paddy@164 | 151 if !token.ClientID.Equal(clientID) { |
| paddy@164 | 152 continue |
| paddy@164 | 153 } |
| paddy@164 | 154 token.Revoked = true |
| paddy@164 | 155 m.tokens[id] = token |
| paddy@162 | 156 } |
| paddy@162 | 157 return nil |
| paddy@162 | 158 } |
| paddy@162 | 159 |
| paddy@57 | 160 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@28 | 161 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@28 | 162 if err != nil { |
| paddy@28 | 163 return []Token{}, err |
| paddy@28 | 164 } |
| paddy@28 | 165 if len(ids) > num+offset { |
| paddy@28 | 166 ids = ids[offset : num+offset] |
| paddy@28 | 167 } else if len(ids) > offset { |
| paddy@28 | 168 ids = ids[offset:] |
| paddy@28 | 169 } else { |
| paddy@28 | 170 return []Token{}, nil |
| paddy@28 | 171 } |
| paddy@28 | 172 tokens := []Token{} |
| paddy@28 | 173 for _, id := range ids { |
| paddy@57 | 174 token, err := m.getToken(id, false) |
| paddy@28 | 175 if err != nil { |
| paddy@28 | 176 return []Token{}, err |
| paddy@28 | 177 } |
| paddy@28 | 178 tokens = append(tokens, token) |
| paddy@28 | 179 } |
| paddy@28 | 180 return tokens, nil |
| paddy@28 | 181 } |
| paddy@123 | 182 |
| paddy@163 | 183 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) { |
| paddy@123 | 184 enc := json.NewEncoder(w) |
| paddy@123 | 185 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 186 if refresh == "" { |
| paddy@123 | 187 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 188 renderJSONError(enc, "invalid_request") |
| paddy@123 | 189 return |
| paddy@123 | 190 } |
| paddy@123 | 191 token, err := context.GetToken(refresh, true) |
| paddy@123 | 192 if err != nil { |
| paddy@123 | 193 if err == ErrTokenNotFound { |
| paddy@123 | 194 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 195 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 196 return |
| paddy@123 | 197 } |
| paddy@123 | 198 log.Println("Error exchanging refresh token:", err) |
| paddy@123 | 199 w.WriteHeader(http.StatusInternalServerError) |
| paddy@123 | 200 renderJSONError(enc, "server_error") |
| paddy@123 | 201 return |
| paddy@123 | 202 } |
| paddy@123 | 203 clientID, _, ok := getClientAuth(w, r, true) |
| paddy@123 | 204 if !ok { |
| paddy@123 | 205 return |
| paddy@123 | 206 } |
| paddy@123 | 207 if !token.ClientID.Equal(clientID) { |
| paddy@123 | 208 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 209 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 210 return |
| paddy@123 | 211 } |
| paddy@168 | 212 if token.Revoked { |
| paddy@123 | 213 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 214 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 215 return |
| paddy@123 | 216 } |
| paddy@135 | 217 return token.Scopes, token.ProfileID, true |
| paddy@123 | 218 } |
| paddy@123 | 219 |
| paddy@123 | 220 func refreshTokenInvalidate(r *http.Request, context Context) error { |
| paddy@123 | 221 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 222 if refresh == "" { |
| paddy@123 | 223 return ErrTokenNotFound |
| paddy@123 | 224 } |
| paddy@168 | 225 return context.RevokeToken(refresh) |
| paddy@123 | 226 } |
| paddy@124 | 227 |
| paddy@124 | 228 func refreshTokenAuditString(r *http.Request) string { |
| paddy@124 | 229 return "refresh_token:" + r.PostFormValue("refresh_token") |
| paddy@124 | 230 } |