auth

Paddy 2015-04-19 Parent:6f473576c6ae Child:cf1aef6eb81f

163:73e12d5a1124 Go to Latest

auth/token.go

Use postgres arrays for scope associations. Use the new pqarrays library I wrote to store Scope associations for Tokens and AuthorizationCodes, instead of using our hacky and abstraction-breaking many-to-many code. We also created the authStore.deleteAuthorizationCodesByProfileID method, to clear out the AuthorizationCodes that belong to a Profile (used when the Profile is deleted). So we added the implementation for memstore and for our postgres store. Call Context.DeleteAuthorizationCodesByProfileID when deleting a Profile to clean up after it. Rename sortedScopes to Scopes, which we use pqarrays.StringArray's methods on to fulfill the sql.Scanner and driver.Valuer interfaces. This lets us store Scopes in postgres arrays. Create a stringsToScopes helper function that creates Scope objects, with their IDs filled by the strings specified. Update our GrantType.Validate function signature to return Scopes instead of []string. Create a Scopes.Strings() helper method that returns a []string of the IDs of the Scopes. Update our SQL init file to use the new postgres array definition, instead of the many-to-many definition.

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@163 46 Scopes Scopes
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@163 159 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, 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.