auth

Paddy 2015-04-19 Parent:6f473576c6ae Child:cf1aef6eb81f

163:73e12d5a1124 Go to Latest

auth/token.go

Use postgres arrays for scope associations. Use the new pqarrays library I wrote to store Scope associations for Tokens and AuthorizationCodes, instead of using our hacky and abstraction-breaking many-to-many code. We also created the authStore.deleteAuthorizationCodesByProfileID method, to clear out the AuthorizationCodes that belong to a Profile (used when the Profile is deleted). So we added the implementation for memstore and for our postgres store. Call Context.DeleteAuthorizationCodesByProfileID when deleting a Profile to clean up after it. Rename sortedScopes to Scopes, which we use pqarrays.StringArray's methods on to fulfill the sql.Scanner and driver.Valuer interfaces. This lets us store Scopes in postgres arrays. Create a stringsToScopes helper function that creates Scope objects, with their IDs filled by the strings specified. Update our GrantType.Validate function signature to return Scopes instead of []string. Create a Scopes.Strings() helper method that returns a []string of the IDs of the Scopes. Update our SQL init file to use the new postgres array definition, instead of the many-to-many definition.

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 )
17 func init() {
18 RegisterGrantType("refresh_token", GrantType{
19 Validate: refreshTokenValidate,
20 Invalidate: refreshTokenInvalidate,
21 IssuesRefresh: true,
22 ReturnToken: RenderJSONToken,
23 AuditString: refreshTokenAuditString,
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 TokenType string
46 Scopes Scopes
47 ProfileID uuid.ID
48 ClientID uuid.ID
49 Revoked bool
50 RefreshRevoked bool
51 }
53 type tokenStore interface {
54 getToken(token string, refresh bool) (Token, error)
55 saveToken(token Token) error
56 revokeToken(token string, refresh bool) error
57 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
58 revokeTokensByProfileID(profileID uuid.ID) error
59 }
61 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
62 if refresh {
63 t, err := m.lookupTokenByRefresh(token)
64 if err != nil {
65 return Token{}, err
66 }
67 token = t
68 }
69 m.tokenLock.RLock()
70 defer m.tokenLock.RUnlock()
71 result, ok := m.tokens[token]
72 if !ok {
73 return Token{}, ErrTokenNotFound
74 }
75 return result, nil
76 }
78 func (m *memstore) saveToken(token Token) error {
79 m.tokenLock.Lock()
80 defer m.tokenLock.Unlock()
81 _, ok := m.tokens[token.AccessToken]
82 if ok {
83 return ErrTokenAlreadyExists
84 }
85 m.tokens[token.AccessToken] = token
86 if token.RefreshToken != "" {
87 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
88 }
89 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
90 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
91 } else {
92 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
93 }
94 return nil
95 }
97 func (m *memstore) revokeToken(token string, refresh bool) error {
98 if refresh {
99 t, err := m.lookupTokenByRefresh(token)
100 if err != nil {
101 return err
102 }
103 token = t
104 }
105 m.tokenLock.Lock()
106 defer m.tokenLock.Unlock()
107 t, ok := m.tokens[token]
108 if !ok {
109 return ErrTokenNotFound
110 }
111 if refresh {
112 t.RefreshRevoked = true
113 } else {
114 t.Revoked = true
115 }
116 m.tokens[token] = t
117 return nil
118 }
120 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error {
121 ids, err := m.lookupTokensByProfileID(profileID.String())
122 if err != nil {
123 return err
124 }
125 if len(ids) < 1 {
126 return ErrProfileNotFound
127 }
128 m.tokenLock.Lock()
129 defer m.tokenLock.Unlock()
130 for _, id := range ids {
131 delete(m.tokens, id)
132 }
133 return nil
134 }
136 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
137 ids, err := m.lookupTokensByProfileID(profileID.String())
138 if err != nil {
139 return []Token{}, err
140 }
141 if len(ids) > num+offset {
142 ids = ids[offset : num+offset]
143 } else if len(ids) > offset {
144 ids = ids[offset:]
145 } else {
146 return []Token{}, nil
147 }
148 tokens := []Token{}
149 for _, id := range ids {
150 token, err := m.getToken(id, false)
151 if err != nil {
152 return []Token{}, err
153 }
154 tokens = append(tokens, token)
155 }
156 return tokens, nil
157 }
159 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
160 enc := json.NewEncoder(w)
161 refresh := r.PostFormValue("refresh_token")
162 if refresh == "" {
163 w.WriteHeader(http.StatusBadRequest)
164 renderJSONError(enc, "invalid_request")
165 return
166 }
167 token, err := context.GetToken(refresh, true)
168 if err != nil {
169 if err == ErrTokenNotFound {
170 w.WriteHeader(http.StatusBadRequest)
171 renderJSONError(enc, "invalid_grant")
172 return
173 }
174 log.Println("Error exchanging refresh token:", err)
175 w.WriteHeader(http.StatusInternalServerError)
176 renderJSONError(enc, "server_error")
177 return
178 }
179 clientID, _, ok := getClientAuth(w, r, true)
180 if !ok {
181 return
182 }
183 if !token.ClientID.Equal(clientID) {
184 w.WriteHeader(http.StatusBadRequest)
185 renderJSONError(enc, "invalid_grant")
186 return
187 }
188 if token.RefreshRevoked {
189 w.WriteHeader(http.StatusBadRequest)
190 renderJSONError(enc, "invalid_grant")
191 return
192 }
193 return token.Scopes, token.ProfileID, true
194 }
196 func refreshTokenInvalidate(r *http.Request, context Context) error {
197 refresh := r.PostFormValue("refresh_token")
198 if refresh == "" {
199 return ErrTokenNotFound
200 }
201 return context.RevokeToken(refresh, true)
202 }
204 func refreshTokenAuditString(r *http.Request) string {
205 return "refresh_token:" + r.PostFormValue("refresh_token")
206 }
208 // BUG(paddy): We need to implement a handler for revoking a token.
209 // BUG(paddy): We need to implement a handler for listing active tokens.