auth

Paddy 2015-07-15 Parent:581c60f8dd23 Child:b7e685839a1b

178:0a2c3d677161 Go to Latest

auth/token.go

Update to use a generic event emitter. Rather can creating a purpose-built event emitter for each and every event we need to emit (I'm looking at you, login verification event) which is _downright silly_, we're now using a generic event publisher that's based on saying "HEY A MODEL UPDATED". This means we need to change all our setup code in authd to use events.NewNSQPublisher or events.NewStdoutPublisher instead of our homegrown solutions. Which also means updating our config to take an events.Publisher instead of our LoginVerificationNotifier (blergh). Our Context also now uses an events.Publisher instead of a LoginVerificationNotifier. Party all around! We also replaced our SendLoginVerification helper method on Context with a SendModelEvent helper method on Context, which is just a light wrapper around events.PublishModelEvent. Of course, all this means we need to update our email_verification listener to listen to the correct channel (based on the model we want updates about) and filter down to a Created action or our new custom action for "the customer wants their verification resent", which I'm OK making a special case and not generic, because c'mon. But we had a subtle change to all our constants, some of which are unofficial constants now. I'm unsure how I feel about this. We also updated our email_verification listener so that we're unmarshalling to a custom loginEvent, which is just an events.Event that overwrites the Data property to be an auth.Login instance. This is to make sure we don't need to wrangle a map[string]interface{}, which is no fun. I'm also OK with special-casing like this, because it's 1) a tiny amount of code, 2) properly utilising composition, and 3) the only way I can think of to cleanly accomplish what I want. I also added a note about GetLogin's deficient handling of logins, namely that it doesn't recognise admins and return Verification codes to them, which would be a useful property for internal tools to take advantage of. Ah well. I updated the Profile and Login implementations so they're now event.Model instances, mainly by just exporting some strings from them through getters that will let us automatically build an Event from them. This lets us use the PublishModelEvent helper. I updated our CreateProfileHandler to properly mangle the login Verification property, and to fire off the ActionCreated events for the new Login and the new Profile. I updated our GetLoginHandler and UpdateLoginHandler to properly mangle the loginVerification property. God that's annoying. :-/ You'll note I didn't start publishing the events.ActionUpdated or events.ActionDeleted events for Profiles or Logins yet, and didn't bother publishing any events for literally any other type. That's because I'm a lazy piece of crap and will end up publishing them when I absolutely have to. Part of that is because if a channel isn't created/being read for a topic, the messages will just stack up in NSQ, and I don't want that. But mostly I'm lazy. Finally, I got to delete the entire profile_verification.go file, because we're no longer special-casing that. Hooray!

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 }