auth

Paddy 2015-05-17 Parent:581c60f8dd23 Child:b7e685839a1b

172:8ecb60d29b0d Go to Latest

auth/token.go

Support email verification. The bulk of this commit is auto-modifying files to export variables (mostly our request error types and our response type) so that they can be reused in a Go client for that API. We also implement the beginnings of a Go client for that API, implementing the bare minimum we need for our immediate purposes: the ability to retrieve information about a Login. This, of course, means we need an API endpoint that will return information about a Login, which in turn required us to implement a GetLogin method in our profileStore. Which got in-memory and postgres implementations. That done, we could add the Verification field and Verified field to the Login type, to keep track of whether we've verified the user's ownership of those communication methods (if the Login is, in fact, a communication method). This required us to update sql/postgres_init.sql to account for the new fields we're tracking. It also means that when creating a Login, we had to generate a UUID to use as the Verification field. To make things complete, we needed a verifyLogin method on the profileStore to mark a Login as verified. That, in turn, required an endpoint to control this through the API. While doing so, I lumped things together in an UpdateLogin handler just so we could reuse the endpoint and logic when resending a verification email that may have never reached the user, for whatever reason (the quintessential "send again" button). Finally, we implemented an email_verification listener that will pull email_verification events off NSQ, check for the requisite data integrity, and use mailgun to email out a verification/welcome email.

History
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 }