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.
11 "code.secondbit.org/scopes.hg/types"
12 "code.secondbit.org/uuid.hg"
14 "github.com/dgrijalva/jwt-go"
18 defaultTokenExpiration = 900 // fifteen minutes
22 RegisterGrantType("refresh_token", GrantType{
23 Validate: refreshTokenValidate,
24 Invalidate: refreshTokenInvalidate,
26 ReturnToken: RenderJSONToken,
27 AuditString: refreshTokenAuditString,
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")
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.
50 Scopes scopeTypes.Scopes
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)
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
78 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
80 t, err := m.lookupTokenByRefresh(token)
87 defer m.tokenLock.RUnlock()
88 result, ok := m.tokens[token]
90 return Token{}, ErrTokenNotFound
95 func (m *memstore) saveToken(token Token) error {
97 defer m.tokenLock.Unlock()
98 _, ok := m.tokens[token.AccessToken]
100 return ErrTokenAlreadyExists
102 m.tokens[token.AccessToken] = token
103 if token.RefreshToken != "" {
104 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
106 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
107 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
109 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
114 func (m *memstore) revokeToken(token string) error {
115 token, err := m.lookupTokenByRefresh(token)
120 defer m.tokenLock.Unlock()
121 t, ok := m.tokens[token]
123 return ErrTokenNotFound
130 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error {
131 ids, err := m.lookupTokensByProfileID(profileID.String())
136 return ErrProfileNotFound
139 defer m.tokenLock.Unlock()
140 for _, id := range ids {
141 token := m.tokens[id]
148 func (m *memstore) revokeTokensByClientID(clientID uuid.ID) error {
150 defer m.tokenLock.Unlock()
151 for id, token := range m.tokens {
152 if !token.ClientID.Equal(clientID) {
161 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
162 ids, err := m.lookupTokensByProfileID(profileID.String())
164 return []Token{}, err
166 if len(ids) > num+offset {
167 ids = ids[offset : num+offset]
168 } else if len(ids) > offset {
171 return []Token{}, nil
174 for _, id := range ids {
175 token, err := m.getToken(id, false)
177 return []Token{}, err
179 tokens = append(tokens, token)
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")
188 w.WriteHeader(http.StatusBadRequest)
189 renderJSONError(enc, "invalid_request")
192 token, err := context.GetToken(refresh, true)
194 if err == ErrTokenNotFound {
195 w.WriteHeader(http.StatusBadRequest)
196 renderJSONError(enc, "invalid_grant")
199 log.Println("Error exchanging refresh token:", err)
200 w.WriteHeader(http.StatusInternalServerError)
201 renderJSONError(enc, "server_error")
204 clientID, _, ok := getClientAuth(w, r, true)
208 if !token.ClientID.Equal(clientID) {
209 w.WriteHeader(http.StatusBadRequest)
210 renderJSONError(enc, "invalid_grant")
214 w.WriteHeader(http.StatusBadRequest)
215 renderJSONError(enc, "invalid_grant")
218 return token.Scopes, token.ProfileID, true
221 func refreshTokenInvalidate(r *http.Request, context Context) error {
222 refresh := r.PostFormValue("refresh_token")
224 return ErrTokenNotFound
226 return context.RevokeToken(refresh)
229 func refreshTokenAuditString(r *http.Request) string {
230 return "refresh_token:" + r.PostFormValue("refresh_token")