auth

Paddy 2015-04-11 Parent:762953f6a7f2 Child:73e12d5a1124

162:6f473576c6ae Go to Latest

auth/token.go

Clean up sessions and tokens after Profile is deleted. Add a terminateSessionsByProfile method to our sessionStore to mark Sessions associated with a Profile as inactive. Implement memstore and postgres implementations of the terminateSessionsByProfile method. Add a TerminateSessionsByProfile wrapper method to Context. Add a revokeTokensByProfileID method to our tokenStore to mark Tokens associated with a Profile as revoked. Implement memstore and postgres implementation of the revokeTokensByProfileID method. Add a RevokeTokensByProfileID wrapper method to Context. Call our RevokeTokensByProfileID and TerminateSessionsByProfile methods after a Profile is deleted, to clean up the Tokens and Sessions associated with it.

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 []string `sql_column:"-"`
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 []string, 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.