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