auth

Paddy 2015-12-14 Parent:581c60f8dd23

181:b7e685839a1b Go to Latest

auth/token.go

Break out scopes and events. This repo has gotten unwieldy, and there are portions of it that need to be imported by a large number of other packages. For example, scopes will be used in almost every API we write. Rather than importing the entirety of this codebase into every API we write, I've opted to move the scope logic out into a scopes package, with a subpackage for the defined types, which is all most projects actually want to import. We also define some event type constants, and importing those shouldn't require a project to import all our dependencies, either. So I made an events subpackage that just holds those constants. This package has become a little bit of a red-headed stepchild and is do for a refactor, but I'm trying to put that off as long as I can. The refactoring of our scopes stuff has left a bug wherein a token can be granted for scopes that don't exist. I'm going to need to revisit that, and also how to limit scopes to only be granted to the users that should be able to request them. But that's a battle for another day.

History
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "log"
7 "net/http"
8 "strings"
9 "time"
11 "code.secondbit.org/scopes.hg/types"
12 "code.secondbit.org/uuid.hg"
14 "github.com/dgrijalva/jwt-go"
15 )
17 const (
18 defaultTokenExpiration = 900 // fifteen minutes
19 )
21 func init() {
22 RegisterGrantType("refresh_token", GrantType{
23 Validate: refreshTokenValidate,
24 Invalidate: refreshTokenInvalidate,
25 IssuesRefresh: true,
26 ReturnToken: RenderJSONToken,
27 AuditString: refreshTokenAuditString,
28 })
29 }
31 var (
32 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first.
33 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context")
34 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore.
35 ErrTokenNotFound = errors.New("token not found in tokenStore")
36 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with
37 // the same AccessToken property already exists in the tokenStore.
38 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore")
39 )
41 // Token represents an access and/or refresh token that the Client can use to access user data
42 // or obtain a new access token.
43 type Token struct {
44 AccessToken string
45 RefreshToken string
46 Created time.Time
47 CreatedFrom string
48 ExpiresIn int32
49 TokenType string
50 Scopes scopeTypes.Scopes
51 ProfileID uuid.ID
52 ClientID uuid.ID
53 Revoked bool
54 }
56 func (t Token) GenerateAccessToken(privateKey []byte) (string, error) {
57 access := jwt.New(jwt.SigningMethodHS256)
58 access.Claims["iss"] = t.ClientID
59 access.Claims["sub"] = t.ProfileID
60 access.Claims["exp"] = t.Created.Add(defaultTokenExpiration * time.Second).Unix()
61 access.Claims["nbf"] = t.Created.Add(-2 * time.Minute).Unix()
62 access.Claims["iat"] = t.Created.Unix()
63 access.Claims["scope"] = strings.Join(t.Scopes.Strings(), " ")
64 return access.SignedString(privateKey)
65 }
67 // BUG(paddy): Now that access tokens are generated and have a meaning, refresh tokens should be the primary key
69 type tokenStore interface {
70 getToken(token string, refresh bool) (Token, error)
71 saveToken(token Token) error
72 revokeToken(token string) error
73 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
74 revokeTokensByProfileID(profileID uuid.ID) error
75 revokeTokensByClientID(clientID uuid.ID) error
76 }
78 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
79 if refresh {
80 t, err := m.lookupTokenByRefresh(token)
81 if err != nil {
82 return Token{}, err
83 }
84 token = t
85 }
86 m.tokenLock.RLock()
87 defer m.tokenLock.RUnlock()
88 result, ok := m.tokens[token]
89 if !ok {
90 return Token{}, ErrTokenNotFound
91 }
92 return result, nil
93 }
95 func (m *memstore) saveToken(token Token) error {
96 m.tokenLock.Lock()
97 defer m.tokenLock.Unlock()
98 _, ok := m.tokens[token.AccessToken]
99 if ok {
100 return ErrTokenAlreadyExists
101 }
102 m.tokens[token.AccessToken] = token
103 if token.RefreshToken != "" {
104 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
105 }
106 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
107 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
108 } else {
109 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
110 }
111 return nil
112 }
114 func (m *memstore) revokeToken(token string) error {
115 token, err := m.lookupTokenByRefresh(token)
116 if err != nil {
117 return err
118 }
119 m.tokenLock.Lock()
120 defer m.tokenLock.Unlock()
121 t, ok := m.tokens[token]
122 if !ok {
123 return ErrTokenNotFound
124 }
125 t.Revoked = true
126 m.tokens[token] = t
127 return nil
128 }
130 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error {
131 ids, err := m.lookupTokensByProfileID(profileID.String())
132 if err != nil {
133 return err
134 }
135 if len(ids) < 1 {
136 return ErrProfileNotFound
137 }
138 m.tokenLock.Lock()
139 defer m.tokenLock.Unlock()
140 for _, id := range ids {
141 token := m.tokens[id]
142 token.Revoked = true
143 m.tokens[id] = token
144 }
145 return nil
146 }
148 func (m *memstore) revokeTokensByClientID(clientID uuid.ID) error {
149 m.tokenLock.Lock()
150 defer m.tokenLock.Unlock()
151 for id, token := range m.tokens {
152 if !token.ClientID.Equal(clientID) {
153 continue
154 }
155 token.Revoked = true
156 m.tokens[id] = token
157 }
158 return nil
159 }
161 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
162 ids, err := m.lookupTokensByProfileID(profileID.String())
163 if err != nil {
164 return []Token{}, err
165 }
166 if len(ids) > num+offset {
167 ids = ids[offset : num+offset]
168 } else if len(ids) > offset {
169 ids = ids[offset:]
170 } else {
171 return []Token{}, nil
172 }
173 tokens := []Token{}
174 for _, id := range ids {
175 token, err := m.getToken(id, false)
176 if err != nil {
177 return []Token{}, err
178 }
179 tokens = append(tokens, token)
180 }
181 return tokens, nil
182 }
184 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes scopeTypes.Scopes, profileID uuid.ID, valid bool) {
185 enc := json.NewEncoder(w)
186 refresh := r.PostFormValue("refresh_token")
187 if refresh == "" {
188 w.WriteHeader(http.StatusBadRequest)
189 renderJSONError(enc, "invalid_request")
190 return
191 }
192 token, err := context.GetToken(refresh, true)
193 if err != nil {
194 if err == ErrTokenNotFound {
195 w.WriteHeader(http.StatusBadRequest)
196 renderJSONError(enc, "invalid_grant")
197 return
198 }
199 log.Println("Error exchanging refresh token:", err)
200 w.WriteHeader(http.StatusInternalServerError)
201 renderJSONError(enc, "server_error")
202 return
203 }
204 clientID, _, ok := getClientAuth(w, r, true)
205 if !ok {
206 return
207 }
208 if !token.ClientID.Equal(clientID) {
209 w.WriteHeader(http.StatusBadRequest)
210 renderJSONError(enc, "invalid_grant")
211 return
212 }
213 if token.Revoked {
214 w.WriteHeader(http.StatusBadRequest)
215 renderJSONError(enc, "invalid_grant")
216 return
217 }
218 return token.Scopes, token.ProfileID, true
219 }
221 func refreshTokenInvalidate(r *http.Request, context Context) error {
222 refresh := r.PostFormValue("refresh_token")
223 if refresh == "" {
224 return ErrTokenNotFound
225 }
226 return context.RevokeToken(refresh)
227 }
229 func refreshTokenAuditString(r *http.Request) string {
230 return "refresh_token:" + r.PostFormValue("refresh_token")
231 }