auth

Paddy 2015-04-11 Parent:762953f6a7f2 Child:73e12d5a1124

162:6f473576c6ae Go to Latest

auth/token.go

Clean up sessions and tokens after Profile is deleted. Add a terminateSessionsByProfile method to our sessionStore to mark Sessions associated with a Profile as inactive. Implement memstore and postgres implementations of the terminateSessionsByProfile method. Add a TerminateSessionsByProfile wrapper method to Context. Add a revokeTokensByProfileID method to our tokenStore to mark Tokens associated with a Profile as revoked. Implement memstore and postgres implementation of the revokeTokensByProfileID method. Add a RevokeTokensByProfileID wrapper method to Context. Call our RevokeTokensByProfileID and TerminateSessionsByProfile methods after a Profile is deleted, to clean up the Tokens and Sessions associated with it.

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@28 8 "time"
paddy@28 9
paddy@107 10 "code.secondbit.org/uuid.hg"
paddy@28 11 )
paddy@28 12
paddy@69 13 const (
paddy@125 14 defaultTokenExpiration = 3600 // one hour
paddy@69 15 )
paddy@69 16
paddy@123 17 func init() {
paddy@123 18 RegisterGrantType("refresh_token", GrantType{
paddy@123 19 Validate: refreshTokenValidate,
paddy@123 20 Invalidate: refreshTokenInvalidate,
paddy@123 21 IssuesRefresh: true,
paddy@123 22 ReturnToken: RenderJSONToken,
paddy@124 23 AuditString: refreshTokenAuditString,
paddy@123 24 })
paddy@123 25 }
paddy@123 26
paddy@28 27 var (
paddy@57 28 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first.
paddy@57 29 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context")
paddy@57 30 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore.
paddy@57 31 ErrTokenNotFound = errors.New("token not found in tokenStore")
paddy@57 32 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with
paddy@57 33 // the same AccessToken property already exists in the tokenStore.
paddy@57 34 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore")
paddy@28 35 )
paddy@28 36
paddy@57 37 // Token represents an access and/or refresh token that the Client can use to access user data
paddy@57 38 // or obtain a new access token.
paddy@28 39 type Token struct {
paddy@125 40 AccessToken string
paddy@125 41 RefreshToken string
paddy@125 42 Created time.Time
paddy@125 43 CreatedFrom string
paddy@125 44 ExpiresIn int32
paddy@125 45 TokenType string
paddy@155 46 Scopes []string `sql_column:"-"`
paddy@125 47 ProfileID uuid.ID
paddy@125 48 ClientID uuid.ID
paddy@125 49 Revoked bool
paddy@125 50 RefreshRevoked bool
paddy@28 51 }
paddy@28 52
paddy@57 53 type tokenStore interface {
paddy@57 54 getToken(token string, refresh bool) (Token, error)
paddy@57 55 saveToken(token Token) error
paddy@91 56 revokeToken(token string, refresh bool) error
paddy@57 57 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
paddy@162 58 revokeTokensByProfileID(profileID uuid.ID) error
paddy@28 59 }
paddy@28 60
paddy@57 61 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
paddy@28 62 if refresh {
paddy@28 63 t, err := m.lookupTokenByRefresh(token)
paddy@28 64 if err != nil {
paddy@28 65 return Token{}, err
paddy@28 66 }
paddy@28 67 token = t
paddy@28 68 }
paddy@28 69 m.tokenLock.RLock()
paddy@28 70 defer m.tokenLock.RUnlock()
paddy@28 71 result, ok := m.tokens[token]
paddy@28 72 if !ok {
paddy@28 73 return Token{}, ErrTokenNotFound
paddy@28 74 }
paddy@28 75 return result, nil
paddy@28 76 }
paddy@28 77
paddy@57 78 func (m *memstore) saveToken(token Token) error {
paddy@28 79 m.tokenLock.Lock()
paddy@28 80 defer m.tokenLock.Unlock()
paddy@28 81 _, ok := m.tokens[token.AccessToken]
paddy@28 82 if ok {
paddy@28 83 return ErrTokenAlreadyExists
paddy@28 84 }
paddy@28 85 m.tokens[token.AccessToken] = token
paddy@28 86 if token.RefreshToken != "" {
paddy@28 87 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
paddy@28 88 }
paddy@28 89 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
paddy@28 90 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
paddy@28 91 } else {
paddy@28 92 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
paddy@28 93 }
paddy@28 94 return nil
paddy@28 95 }
paddy@28 96
paddy@91 97 func (m *memstore) revokeToken(token string, refresh bool) error {
paddy@91 98 if refresh {
paddy@91 99 t, err := m.lookupTokenByRefresh(token)
paddy@91 100 if err != nil {
paddy@91 101 return err
paddy@91 102 }
paddy@91 103 token = t
paddy@91 104 }
paddy@91 105 m.tokenLock.Lock()
paddy@91 106 defer m.tokenLock.Unlock()
paddy@91 107 t, ok := m.tokens[token]
paddy@91 108 if !ok {
paddy@91 109 return ErrTokenNotFound
paddy@91 110 }
paddy@123 111 if refresh {
paddy@123 112 t.RefreshRevoked = true
paddy@123 113 } else {
paddy@123 114 t.Revoked = true
paddy@123 115 }
paddy@91 116 m.tokens[token] = t
paddy@91 117 return nil
paddy@91 118 }
paddy@91 119
paddy@162 120 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error {
paddy@162 121 ids, err := m.lookupTokensByProfileID(profileID.String())
paddy@162 122 if err != nil {
paddy@162 123 return err
paddy@162 124 }
paddy@162 125 if len(ids) < 1 {
paddy@162 126 return ErrProfileNotFound
paddy@162 127 }
paddy@162 128 m.tokenLock.Lock()
paddy@162 129 defer m.tokenLock.Unlock()
paddy@162 130 for _, id := range ids {
paddy@162 131 delete(m.tokens, id)
paddy@162 132 }
paddy@162 133 return nil
paddy@162 134 }
paddy@162 135
paddy@57 136 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
paddy@28 137 ids, err := m.lookupTokensByProfileID(profileID.String())
paddy@28 138 if err != nil {
paddy@28 139 return []Token{}, err
paddy@28 140 }
paddy@28 141 if len(ids) > num+offset {
paddy@28 142 ids = ids[offset : num+offset]
paddy@28 143 } else if len(ids) > offset {
paddy@28 144 ids = ids[offset:]
paddy@28 145 } else {
paddy@28 146 return []Token{}, nil
paddy@28 147 }
paddy@28 148 tokens := []Token{}
paddy@28 149 for _, id := range ids {
paddy@57 150 token, err := m.getToken(id, false)
paddy@28 151 if err != nil {
paddy@28 152 return []Token{}, err
paddy@28 153 }
paddy@28 154 tokens = append(tokens, token)
paddy@28 155 }
paddy@28 156 return tokens, nil
paddy@28 157 }
paddy@123 158
paddy@135 159 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
paddy@123 160 enc := json.NewEncoder(w)
paddy@123 161 refresh := r.PostFormValue("refresh_token")
paddy@123 162 if refresh == "" {
paddy@123 163 w.WriteHeader(http.StatusBadRequest)
paddy@123 164 renderJSONError(enc, "invalid_request")
paddy@123 165 return
paddy@123 166 }
paddy@123 167 token, err := context.GetToken(refresh, true)
paddy@123 168 if err != nil {
paddy@123 169 if err == ErrTokenNotFound {
paddy@123 170 w.WriteHeader(http.StatusBadRequest)
paddy@123 171 renderJSONError(enc, "invalid_grant")
paddy@123 172 return
paddy@123 173 }
paddy@123 174 log.Println("Error exchanging refresh token:", err)
paddy@123 175 w.WriteHeader(http.StatusInternalServerError)
paddy@123 176 renderJSONError(enc, "server_error")
paddy@123 177 return
paddy@123 178 }
paddy@123 179 clientID, _, ok := getClientAuth(w, r, true)
paddy@123 180 if !ok {
paddy@123 181 return
paddy@123 182 }
paddy@123 183 if !token.ClientID.Equal(clientID) {
paddy@123 184 w.WriteHeader(http.StatusBadRequest)
paddy@123 185 renderJSONError(enc, "invalid_grant")
paddy@123 186 return
paddy@123 187 }
paddy@123 188 if token.RefreshRevoked {
paddy@123 189 w.WriteHeader(http.StatusBadRequest)
paddy@123 190 renderJSONError(enc, "invalid_grant")
paddy@123 191 return
paddy@123 192 }
paddy@135 193 return token.Scopes, token.ProfileID, true
paddy@123 194 }
paddy@123 195
paddy@123 196 func refreshTokenInvalidate(r *http.Request, context Context) error {
paddy@123 197 refresh := r.PostFormValue("refresh_token")
paddy@123 198 if refresh == "" {
paddy@123 199 return ErrTokenNotFound
paddy@123 200 }
paddy@123 201 return context.RevokeToken(refresh, true)
paddy@123 202 }
paddy@124 203
paddy@124 204 func refreshTokenAuditString(r *http.Request) string {
paddy@124 205 return "refresh_token:" + r.PostFormValue("refresh_token")
paddy@124 206 }
paddy@128 207
paddy@128 208 // BUG(paddy): We need to implement a handler for revoking a token.
paddy@128 209 // BUG(paddy): We need to implement a handler for listing active tokens.