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.
10 "code.secondbit.org/uuid.hg"
14 defaultTokenExpiration = 3600 // one hour
15 defaultRefreshTokenExpiration = 86400 // one day
19 RegisterGrantType("refresh_token", GrantType{
20 Validate: refreshTokenValidate,
21 Invalidate: refreshTokenInvalidate,
23 ReturnToken: RenderJSONToken,
28 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first.
29 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context")
30 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore.
31 ErrTokenNotFound = errors.New("token not found in tokenStore")
32 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with
33 // the same AccessToken property already exists in the tokenStore.
34 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore")
37 // Token represents an access and/or refresh token that the Client can use to access user data
38 // or obtain a new access token.
45 RefreshExpiresIn int32
54 type tokenStore interface {
55 getToken(token string, refresh bool) (Token, error)
56 saveToken(token Token) error
57 removeToken(token string) error
58 revokeToken(token string, refresh bool) error
59 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
62 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
64 t, err := m.lookupTokenByRefresh(token)
71 defer m.tokenLock.RUnlock()
72 result, ok := m.tokens[token]
74 return Token{}, ErrTokenNotFound
79 func (m *memstore) saveToken(token Token) error {
81 defer m.tokenLock.Unlock()
82 _, ok := m.tokens[token.AccessToken]
84 return ErrTokenAlreadyExists
86 m.tokens[token.AccessToken] = token
87 if token.RefreshToken != "" {
88 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
90 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
91 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
93 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
98 func (m *memstore) removeToken(token string) error {
100 defer m.tokenLock.Unlock()
101 t, ok := m.tokens[token]
103 return ErrTokenNotFound
105 delete(m.tokens, token)
106 if t.RefreshToken != "" {
107 delete(m.refreshTokenLookup, t.RefreshToken)
110 for p, item := range m.profileTokenLookup[t.ProfileID.String()] {
117 m.profileTokenLookup[t.ProfileID.String()] = append(m.profileTokenLookup[t.ProfileID.String()][:pos], m.profileTokenLookup[t.ProfileID.String()][pos+1:]...)
122 func (m *memstore) revokeToken(token string, refresh bool) error {
124 t, err := m.lookupTokenByRefresh(token)
131 defer m.tokenLock.Unlock()
132 t, ok := m.tokens[token]
134 return ErrTokenNotFound
137 t.RefreshRevoked = true
145 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
146 ids, err := m.lookupTokensByProfileID(profileID.String())
148 return []Token{}, err
150 if len(ids) > num+offset {
151 ids = ids[offset : num+offset]
152 } else if len(ids) > offset {
155 return []Token{}, nil
158 for _, id := range ids {
159 token, err := m.getToken(id, false)
161 return []Token{}, err
163 tokens = append(tokens, token)
168 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
169 enc := json.NewEncoder(w)
170 refresh := r.PostFormValue("refresh_token")
172 w.WriteHeader(http.StatusBadRequest)
173 renderJSONError(enc, "invalid_request")
176 token, err := context.GetToken(refresh, true)
178 if err == ErrTokenNotFound {
179 w.WriteHeader(http.StatusBadRequest)
180 renderJSONError(enc, "invalid_grant")
183 log.Println("Error exchanging refresh token:", err)
184 w.WriteHeader(http.StatusInternalServerError)
185 renderJSONError(enc, "server_error")
188 clientID, _, ok := getClientAuth(w, r, true)
192 if !token.ClientID.Equal(clientID) {
193 w.WriteHeader(http.StatusBadRequest)
194 renderJSONError(enc, "invalid_grant")
197 if token.RefreshRevoked {
198 w.WriteHeader(http.StatusBadRequest)
199 renderJSONError(enc, "invalid_grant")
202 expires := token.Created.Add(time.Duration(token.RefreshExpiresIn) * time.Second)
203 if expires.Before(time.Now()) {
204 w.WriteHeader(http.StatusBadRequest)
205 renderJSONError(enc, "invalid_grant")
208 return token.Scope, token.ProfileID, true
211 func refreshTokenInvalidate(r *http.Request, context Context) error {
212 refresh := r.PostFormValue("refresh_token")
214 return ErrTokenNotFound
216 return context.RevokeToken(refresh, true)