auth

Paddy 2015-01-18 Parent:c03b5eb3179e Child:d14f0a81498c

123:0a1e16b9c141 Go to Latest

auth/token.go

Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.

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@88 14 defaultTokenExpiration = 3600 // one hour
paddy@88 15 defaultRefreshTokenExpiration = 86400 // one day
paddy@69 16 )
paddy@69 17
paddy@123 18 func init() {
paddy@123 19 RegisterGrantType("refresh_token", GrantType{
paddy@123 20 Validate: refreshTokenValidate,
paddy@123 21 Invalidate: refreshTokenInvalidate,
paddy@123 22 IssuesRefresh: true,
paddy@123 23 ReturnToken: RenderJSONToken,
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@88 40 AccessToken string
paddy@88 41 RefreshToken string
paddy@88 42 Created time.Time
paddy@88 43 CreatedFrom string
paddy@88 44 ExpiresIn int32
paddy@88 45 RefreshExpiresIn int32
paddy@88 46 TokenType string
paddy@88 47 Scope string
paddy@88 48 ProfileID uuid.ID
paddy@123 49 ClientID uuid.ID
paddy@88 50 Revoked bool
paddy@123 51 RefreshRevoked bool
paddy@28 52 }
paddy@28 53
paddy@57 54 type tokenStore interface {
paddy@57 55 getToken(token string, refresh bool) (Token, error)
paddy@57 56 saveToken(token Token) error
paddy@57 57 removeToken(token string) error
paddy@91 58 revokeToken(token string, refresh bool) error
paddy@57 59 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
paddy@28 60 }
paddy@28 61
paddy@57 62 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
paddy@28 63 if refresh {
paddy@28 64 t, err := m.lookupTokenByRefresh(token)
paddy@28 65 if err != nil {
paddy@28 66 return Token{}, err
paddy@28 67 }
paddy@28 68 token = t
paddy@28 69 }
paddy@28 70 m.tokenLock.RLock()
paddy@28 71 defer m.tokenLock.RUnlock()
paddy@28 72 result, ok := m.tokens[token]
paddy@28 73 if !ok {
paddy@28 74 return Token{}, ErrTokenNotFound
paddy@28 75 }
paddy@28 76 return result, nil
paddy@28 77 }
paddy@28 78
paddy@57 79 func (m *memstore) saveToken(token Token) error {
paddy@28 80 m.tokenLock.Lock()
paddy@28 81 defer m.tokenLock.Unlock()
paddy@28 82 _, ok := m.tokens[token.AccessToken]
paddy@28 83 if ok {
paddy@28 84 return ErrTokenAlreadyExists
paddy@28 85 }
paddy@28 86 m.tokens[token.AccessToken] = token
paddy@28 87 if token.RefreshToken != "" {
paddy@28 88 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
paddy@28 89 }
paddy@28 90 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
paddy@28 91 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
paddy@28 92 } else {
paddy@28 93 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
paddy@28 94 }
paddy@28 95 return nil
paddy@28 96 }
paddy@28 97
paddy@57 98 func (m *memstore) removeToken(token string) error {
paddy@28 99 m.tokenLock.Lock()
paddy@28 100 defer m.tokenLock.Unlock()
paddy@28 101 t, ok := m.tokens[token]
paddy@28 102 if !ok {
paddy@28 103 return ErrTokenNotFound
paddy@28 104 }
paddy@28 105 delete(m.tokens, token)
paddy@28 106 if t.RefreshToken != "" {
paddy@28 107 delete(m.refreshTokenLookup, t.RefreshToken)
paddy@28 108 }
paddy@28 109 pos := -1
paddy@28 110 for p, item := range m.profileTokenLookup[t.ProfileID.String()] {
paddy@28 111 if item == token {
paddy@28 112 pos = p
paddy@28 113 break
paddy@28 114 }
paddy@28 115 }
paddy@28 116 if pos >= 0 {
paddy@28 117 m.profileTokenLookup[t.ProfileID.String()] = append(m.profileTokenLookup[t.ProfileID.String()][:pos], m.profileTokenLookup[t.ProfileID.String()][pos+1:]...)
paddy@28 118 }
paddy@28 119 return nil
paddy@28 120 }
paddy@28 121
paddy@91 122 func (m *memstore) revokeToken(token string, refresh bool) error {
paddy@91 123 if refresh {
paddy@91 124 t, err := m.lookupTokenByRefresh(token)
paddy@91 125 if err != nil {
paddy@91 126 return err
paddy@91 127 }
paddy@91 128 token = t
paddy@91 129 }
paddy@91 130 m.tokenLock.Lock()
paddy@91 131 defer m.tokenLock.Unlock()
paddy@91 132 t, ok := m.tokens[token]
paddy@91 133 if !ok {
paddy@91 134 return ErrTokenNotFound
paddy@91 135 }
paddy@123 136 if refresh {
paddy@123 137 t.RefreshRevoked = true
paddy@123 138 } else {
paddy@123 139 t.Revoked = true
paddy@123 140 }
paddy@91 141 m.tokens[token] = t
paddy@91 142 return nil
paddy@91 143 }
paddy@91 144
paddy@57 145 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
paddy@28 146 ids, err := m.lookupTokensByProfileID(profileID.String())
paddy@28 147 if err != nil {
paddy@28 148 return []Token{}, err
paddy@28 149 }
paddy@28 150 if len(ids) > num+offset {
paddy@28 151 ids = ids[offset : num+offset]
paddy@28 152 } else if len(ids) > offset {
paddy@28 153 ids = ids[offset:]
paddy@28 154 } else {
paddy@28 155 return []Token{}, nil
paddy@28 156 }
paddy@28 157 tokens := []Token{}
paddy@28 158 for _, id := range ids {
paddy@57 159 token, err := m.getToken(id, false)
paddy@28 160 if err != nil {
paddy@28 161 return []Token{}, err
paddy@28 162 }
paddy@28 163 tokens = append(tokens, token)
paddy@28 164 }
paddy@28 165 return tokens, nil
paddy@28 166 }
paddy@123 167
paddy@123 168 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
paddy@123 169 enc := json.NewEncoder(w)
paddy@123 170 refresh := r.PostFormValue("refresh_token")
paddy@123 171 if refresh == "" {
paddy@123 172 w.WriteHeader(http.StatusBadRequest)
paddy@123 173 renderJSONError(enc, "invalid_request")
paddy@123 174 return
paddy@123 175 }
paddy@123 176 token, err := context.GetToken(refresh, true)
paddy@123 177 if err != nil {
paddy@123 178 if err == ErrTokenNotFound {
paddy@123 179 w.WriteHeader(http.StatusBadRequest)
paddy@123 180 renderJSONError(enc, "invalid_grant")
paddy@123 181 return
paddy@123 182 }
paddy@123 183 log.Println("Error exchanging refresh token:", err)
paddy@123 184 w.WriteHeader(http.StatusInternalServerError)
paddy@123 185 renderJSONError(enc, "server_error")
paddy@123 186 return
paddy@123 187 }
paddy@123 188 clientID, _, ok := getClientAuth(w, r, true)
paddy@123 189 if !ok {
paddy@123 190 return
paddy@123 191 }
paddy@123 192 if !token.ClientID.Equal(clientID) {
paddy@123 193 w.WriteHeader(http.StatusBadRequest)
paddy@123 194 renderJSONError(enc, "invalid_grant")
paddy@123 195 return
paddy@123 196 }
paddy@123 197 if token.RefreshRevoked {
paddy@123 198 w.WriteHeader(http.StatusBadRequest)
paddy@123 199 renderJSONError(enc, "invalid_grant")
paddy@123 200 return
paddy@123 201 }
paddy@123 202 expires := token.Created.Add(time.Duration(token.RefreshExpiresIn) * time.Second)
paddy@123 203 if expires.Before(time.Now()) {
paddy@123 204 w.WriteHeader(http.StatusBadRequest)
paddy@123 205 renderJSONError(enc, "invalid_grant")
paddy@123 206 return
paddy@123 207 }
paddy@123 208 return token.Scope, token.ProfileID, true
paddy@123 209 }
paddy@123 210
paddy@123 211 func refreshTokenInvalidate(r *http.Request, context Context) error {
paddy@123 212 refresh := r.PostFormValue("refresh_token")
paddy@123 213 if refresh == "" {
paddy@123 214 return ErrTokenNotFound
paddy@123 215 }
paddy@123 216 return context.RevokeToken(refresh, true)
paddy@123 217 }