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