auth

Paddy 2015-05-12 Parent:cf1aef6eb81f Child:581c60f8dd23

167:0ff23f3a4ede Go to Latest

auth/token.go

Implement an endpoint for token information. Implement an endpoint that allows us to look up information on a token. We strip the refresh token before the response is sent to avoid leaking the response token.

History
paddy@28 1 package auth
paddy@28 2
paddy@28 3 import (
paddy@123 4 "encoding/json"
paddy@28 5 "errors"
paddy@167 6 "github.com/gorilla/mux"
paddy@123 7 "log"
paddy@123 8 "net/http"
paddy@28 9 "time"
paddy@28 10
paddy@107 11 "code.secondbit.org/uuid.hg"
paddy@28 12 )
paddy@28 13
paddy@69 14 const (
paddy@125 15 defaultTokenExpiration = 3600 // one hour
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@124 24 AuditString: refreshTokenAuditString,
paddy@123 25 })
paddy@123 26 }
paddy@123 27
paddy@28 28 var (
paddy@57 29 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first.
paddy@57 30 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context")
paddy@57 31 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore.
paddy@57 32 ErrTokenNotFound = errors.New("token not found in tokenStore")
paddy@57 33 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with
paddy@57 34 // the same AccessToken property already exists in the tokenStore.
paddy@57 35 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore")
paddy@28 36 )
paddy@28 37
paddy@57 38 // Token represents an access and/or refresh token that the Client can use to access user data
paddy@57 39 // or obtain a new access token.
paddy@28 40 type Token struct {
paddy@167 41 AccessToken string `json:"access_token"`
paddy@167 42 RefreshToken string `json:"refresh_token,omitempty"`
paddy@167 43 Created time.Time `json:"-"`
paddy@167 44 CreatedFrom string `json:"created_from"`
paddy@167 45 ExpiresIn int32 `json:"expires_in"`
paddy@167 46 TokenType string `json:"token_type"`
paddy@167 47 Scopes Scopes `json:"-"`
paddy@167 48 ProfileID uuid.ID `json:"profile_id"`
paddy@167 49 ClientID uuid.ID `json:"client_id"`
paddy@167 50 Revoked bool `json:"revoked,omitempty"`
paddy@167 51 RefreshRevoked bool `json:"refresh_revoked,omitempty"`
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@91 57 revokeToken(token string, refresh bool) error
paddy@57 58 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
paddy@162 59 revokeTokensByProfileID(profileID uuid.ID) error
paddy@164 60 revokeTokensByClientID(clientID uuid.ID) error
paddy@28 61 }
paddy@28 62
paddy@57 63 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
paddy@28 64 if refresh {
paddy@28 65 t, err := m.lookupTokenByRefresh(token)
paddy@28 66 if err != nil {
paddy@28 67 return Token{}, err
paddy@28 68 }
paddy@28 69 token = t
paddy@28 70 }
paddy@28 71 m.tokenLock.RLock()
paddy@28 72 defer m.tokenLock.RUnlock()
paddy@28 73 result, ok := m.tokens[token]
paddy@28 74 if !ok {
paddy@28 75 return Token{}, ErrTokenNotFound
paddy@28 76 }
paddy@28 77 return result, nil
paddy@28 78 }
paddy@28 79
paddy@57 80 func (m *memstore) saveToken(token Token) error {
paddy@28 81 m.tokenLock.Lock()
paddy@28 82 defer m.tokenLock.Unlock()
paddy@28 83 _, ok := m.tokens[token.AccessToken]
paddy@28 84 if ok {
paddy@28 85 return ErrTokenAlreadyExists
paddy@28 86 }
paddy@28 87 m.tokens[token.AccessToken] = token
paddy@28 88 if token.RefreshToken != "" {
paddy@28 89 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
paddy@28 90 }
paddy@28 91 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
paddy@28 92 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
paddy@28 93 } else {
paddy@28 94 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
paddy@28 95 }
paddy@28 96 return nil
paddy@28 97 }
paddy@28 98
paddy@91 99 func (m *memstore) revokeToken(token string, refresh bool) error {
paddy@91 100 if refresh {
paddy@91 101 t, err := m.lookupTokenByRefresh(token)
paddy@91 102 if err != nil {
paddy@91 103 return err
paddy@91 104 }
paddy@91 105 token = t
paddy@91 106 }
paddy@91 107 m.tokenLock.Lock()
paddy@91 108 defer m.tokenLock.Unlock()
paddy@91 109 t, ok := m.tokens[token]
paddy@91 110 if !ok {
paddy@91 111 return ErrTokenNotFound
paddy@91 112 }
paddy@123 113 if refresh {
paddy@123 114 t.RefreshRevoked = true
paddy@123 115 } else {
paddy@123 116 t.Revoked = true
paddy@123 117 }
paddy@91 118 m.tokens[token] = t
paddy@91 119 return nil
paddy@91 120 }
paddy@91 121
paddy@162 122 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error {
paddy@162 123 ids, err := m.lookupTokensByProfileID(profileID.String())
paddy@162 124 if err != nil {
paddy@162 125 return err
paddy@162 126 }
paddy@162 127 if len(ids) < 1 {
paddy@162 128 return ErrProfileNotFound
paddy@162 129 }
paddy@162 130 m.tokenLock.Lock()
paddy@162 131 defer m.tokenLock.Unlock()
paddy@162 132 for _, id := range ids {
paddy@164 133 token := m.tokens[id]
paddy@164 134 token.Revoked = true
paddy@164 135 token.RefreshRevoked = true
paddy@164 136 m.tokens[id] = token
paddy@164 137 }
paddy@164 138 return nil
paddy@164 139 }
paddy@164 140
paddy@164 141 func (m *memstore) revokeTokensByClientID(clientID uuid.ID) error {
paddy@164 142 m.tokenLock.Lock()
paddy@164 143 defer m.tokenLock.Unlock()
paddy@164 144 for id, token := range m.tokens {
paddy@164 145 if !token.ClientID.Equal(clientID) {
paddy@164 146 continue
paddy@164 147 }
paddy@164 148 token.Revoked = true
paddy@164 149 token.RefreshRevoked = true
paddy@164 150 m.tokens[id] = token
paddy@162 151 }
paddy@162 152 return nil
paddy@162 153 }
paddy@162 154
paddy@57 155 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
paddy@28 156 ids, err := m.lookupTokensByProfileID(profileID.String())
paddy@28 157 if err != nil {
paddy@28 158 return []Token{}, err
paddy@28 159 }
paddy@28 160 if len(ids) > num+offset {
paddy@28 161 ids = ids[offset : num+offset]
paddy@28 162 } else if len(ids) > offset {
paddy@28 163 ids = ids[offset:]
paddy@28 164 } else {
paddy@28 165 return []Token{}, nil
paddy@28 166 }
paddy@28 167 tokens := []Token{}
paddy@28 168 for _, id := range ids {
paddy@57 169 token, err := m.getToken(id, false)
paddy@28 170 if err != nil {
paddy@28 171 return []Token{}, err
paddy@28 172 }
paddy@28 173 tokens = append(tokens, token)
paddy@28 174 }
paddy@28 175 return tokens, nil
paddy@28 176 }
paddy@123 177
paddy@163 178 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
paddy@123 179 enc := json.NewEncoder(w)
paddy@123 180 refresh := r.PostFormValue("refresh_token")
paddy@123 181 if refresh == "" {
paddy@123 182 w.WriteHeader(http.StatusBadRequest)
paddy@123 183 renderJSONError(enc, "invalid_request")
paddy@123 184 return
paddy@123 185 }
paddy@123 186 token, err := context.GetToken(refresh, true)
paddy@123 187 if err != nil {
paddy@123 188 if err == ErrTokenNotFound {
paddy@123 189 w.WriteHeader(http.StatusBadRequest)
paddy@123 190 renderJSONError(enc, "invalid_grant")
paddy@123 191 return
paddy@123 192 }
paddy@123 193 log.Println("Error exchanging refresh token:", err)
paddy@123 194 w.WriteHeader(http.StatusInternalServerError)
paddy@123 195 renderJSONError(enc, "server_error")
paddy@123 196 return
paddy@123 197 }
paddy@123 198 clientID, _, ok := getClientAuth(w, r, true)
paddy@123 199 if !ok {
paddy@123 200 return
paddy@123 201 }
paddy@123 202 if !token.ClientID.Equal(clientID) {
paddy@123 203 w.WriteHeader(http.StatusBadRequest)
paddy@123 204 renderJSONError(enc, "invalid_grant")
paddy@123 205 return
paddy@123 206 }
paddy@123 207 if token.RefreshRevoked {
paddy@123 208 w.WriteHeader(http.StatusBadRequest)
paddy@123 209 renderJSONError(enc, "invalid_grant")
paddy@123 210 return
paddy@123 211 }
paddy@135 212 return token.Scopes, token.ProfileID, true
paddy@123 213 }
paddy@123 214
paddy@123 215 func refreshTokenInvalidate(r *http.Request, context Context) error {
paddy@123 216 refresh := r.PostFormValue("refresh_token")
paddy@123 217 if refresh == "" {
paddy@123 218 return ErrTokenNotFound
paddy@123 219 }
paddy@123 220 return context.RevokeToken(refresh, true)
paddy@123 221 }
paddy@124 222
paddy@124 223 func refreshTokenAuditString(r *http.Request) string {
paddy@124 224 return "refresh_token:" + r.PostFormValue("refresh_token")
paddy@124 225 }
paddy@128 226
paddy@167 227 func RegisterTokenHandlers(r *mux.Router, context Context) {
paddy@167 228 r.Handle("/tokens/{id}", wrap(context, GetTokenInfoHandler)).Methods("GET", "OPTIONS")
paddy@167 229 r.Handle("/tokens/{id}", wrap(context, RevokeTokenHandler)).Methods("DELETE", "OPTIONS")
paddy@167 230 }
paddy@167 231
paddy@167 232 // GetTokenInfoHandler is an HTTP handler for retrieving information about a token.
paddy@167 233 func GetTokenInfoHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@167 234 errors := []requestError{}
paddy@167 235 vars := mux.Vars(r)
paddy@167 236 tokenID := vars["id"]
paddy@167 237 if tokenID == "" {
paddy@167 238 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
paddy@167 239 encode(w, r, http.StatusBadRequest, response{Errors: errors})
paddy@167 240 return
paddy@167 241 }
paddy@167 242 token, err := context.GetToken(tokenID, false)
paddy@167 243 if err != nil {
paddy@167 244 if err == ErrTokenNotFound {
paddy@167 245 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
paddy@167 246 encode(w, r, http.StatusNotFound, response{Errors: errors})
paddy@167 247 return
paddy@167 248 }
paddy@167 249 encode(w, r, http.StatusInternalServerError, actOfGodResponse)
paddy@167 250 return
paddy@167 251 }
paddy@167 252 token.RefreshToken = ""
paddy@167 253 expired := int64(time.Now().Sub(token.Created) / time.Second)
paddy@167 254 if expired > int64(token.ExpiresIn) {
paddy@167 255 token.ExpiresIn = 0
paddy@167 256 } else {
paddy@167 257 token.ExpiresIn = token.ExpiresIn - int32(expired)
paddy@167 258 }
paddy@167 259 encode(w, r, http.StatusOK, response{Tokens: []Token{token}})
paddy@167 260 return
paddy@167 261 }
paddy@167 262
paddy@167 263 // RevokeTokenHandler is an HTTP handler for revoking a Token prematurely.
paddy@167 264 func RevokeTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@167 265 //errors := []requestError{}
paddy@167 266 // TODO
paddy@167 267 }