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