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
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 }