auth
2015-12-14
Parent:b7e685839a1b
auth/token.go
Update nsq import path. go-nsq has moved to nsqio/go-nsq, so we need to update the import path appropriately.
| 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 } |