auth
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.
| 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 } |