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