auth
auth/token.go
Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.
| 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@28 | 8 "time" |
| paddy@28 | 9 |
| paddy@107 | 10 "code.secondbit.org/uuid.hg" |
| paddy@28 | 11 ) |
| paddy@28 | 12 |
| paddy@69 | 13 const ( |
| paddy@88 | 14 defaultTokenExpiration = 3600 // one hour |
| paddy@88 | 15 defaultRefreshTokenExpiration = 86400 // one day |
| paddy@69 | 16 ) |
| paddy@69 | 17 |
| paddy@123 | 18 func init() { |
| paddy@123 | 19 RegisterGrantType("refresh_token", GrantType{ |
| paddy@123 | 20 Validate: refreshTokenValidate, |
| paddy@123 | 21 Invalidate: refreshTokenInvalidate, |
| paddy@123 | 22 IssuesRefresh: true, |
| paddy@123 | 23 ReturnToken: RenderJSONToken, |
| paddy@123 | 24 }) |
| paddy@123 | 25 } |
| paddy@123 | 26 |
| paddy@28 | 27 var ( |
| paddy@57 | 28 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first. |
| paddy@57 | 29 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context") |
| paddy@57 | 30 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore. |
| paddy@57 | 31 ErrTokenNotFound = errors.New("token not found in tokenStore") |
| paddy@57 | 32 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with |
| paddy@57 | 33 // the same AccessToken property already exists in the tokenStore. |
| paddy@57 | 34 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore") |
| paddy@28 | 35 ) |
| paddy@28 | 36 |
| paddy@57 | 37 // Token represents an access and/or refresh token that the Client can use to access user data |
| paddy@57 | 38 // or obtain a new access token. |
| paddy@28 | 39 type Token struct { |
| paddy@88 | 40 AccessToken string |
| paddy@88 | 41 RefreshToken string |
| paddy@88 | 42 Created time.Time |
| paddy@88 | 43 CreatedFrom string |
| paddy@88 | 44 ExpiresIn int32 |
| paddy@88 | 45 RefreshExpiresIn int32 |
| paddy@88 | 46 TokenType string |
| paddy@88 | 47 Scope string |
| paddy@88 | 48 ProfileID uuid.ID |
| paddy@123 | 49 ClientID uuid.ID |
| paddy@88 | 50 Revoked bool |
| paddy@123 | 51 RefreshRevoked bool |
| paddy@28 | 52 } |
| paddy@28 | 53 |
| paddy@57 | 54 type tokenStore interface { |
| paddy@57 | 55 getToken(token string, refresh bool) (Token, error) |
| paddy@57 | 56 saveToken(token Token) error |
| paddy@57 | 57 removeToken(token string) error |
| paddy@91 | 58 revokeToken(token string, refresh bool) error |
| paddy@57 | 59 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) |
| paddy@28 | 60 } |
| paddy@28 | 61 |
| paddy@57 | 62 func (m *memstore) getToken(token string, refresh bool) (Token, error) { |
| paddy@28 | 63 if refresh { |
| paddy@28 | 64 t, err := m.lookupTokenByRefresh(token) |
| paddy@28 | 65 if err != nil { |
| paddy@28 | 66 return Token{}, err |
| paddy@28 | 67 } |
| paddy@28 | 68 token = t |
| paddy@28 | 69 } |
| paddy@28 | 70 m.tokenLock.RLock() |
| paddy@28 | 71 defer m.tokenLock.RUnlock() |
| paddy@28 | 72 result, ok := m.tokens[token] |
| paddy@28 | 73 if !ok { |
| paddy@28 | 74 return Token{}, ErrTokenNotFound |
| paddy@28 | 75 } |
| paddy@28 | 76 return result, nil |
| paddy@28 | 77 } |
| paddy@28 | 78 |
| paddy@57 | 79 func (m *memstore) saveToken(token Token) error { |
| paddy@28 | 80 m.tokenLock.Lock() |
| paddy@28 | 81 defer m.tokenLock.Unlock() |
| paddy@28 | 82 _, ok := m.tokens[token.AccessToken] |
| paddy@28 | 83 if ok { |
| paddy@28 | 84 return ErrTokenAlreadyExists |
| paddy@28 | 85 } |
| paddy@28 | 86 m.tokens[token.AccessToken] = token |
| paddy@28 | 87 if token.RefreshToken != "" { |
| paddy@28 | 88 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken |
| paddy@28 | 89 } |
| paddy@28 | 90 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok { |
| paddy@28 | 91 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken) |
| paddy@28 | 92 } else { |
| paddy@28 | 93 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken} |
| paddy@28 | 94 } |
| paddy@28 | 95 return nil |
| paddy@28 | 96 } |
| paddy@28 | 97 |
| paddy@57 | 98 func (m *memstore) removeToken(token string) error { |
| paddy@28 | 99 m.tokenLock.Lock() |
| paddy@28 | 100 defer m.tokenLock.Unlock() |
| paddy@28 | 101 t, ok := m.tokens[token] |
| paddy@28 | 102 if !ok { |
| paddy@28 | 103 return ErrTokenNotFound |
| paddy@28 | 104 } |
| paddy@28 | 105 delete(m.tokens, token) |
| paddy@28 | 106 if t.RefreshToken != "" { |
| paddy@28 | 107 delete(m.refreshTokenLookup, t.RefreshToken) |
| paddy@28 | 108 } |
| paddy@28 | 109 pos := -1 |
| paddy@28 | 110 for p, item := range m.profileTokenLookup[t.ProfileID.String()] { |
| paddy@28 | 111 if item == token { |
| paddy@28 | 112 pos = p |
| paddy@28 | 113 break |
| paddy@28 | 114 } |
| paddy@28 | 115 } |
| paddy@28 | 116 if pos >= 0 { |
| paddy@28 | 117 m.profileTokenLookup[t.ProfileID.String()] = append(m.profileTokenLookup[t.ProfileID.String()][:pos], m.profileTokenLookup[t.ProfileID.String()][pos+1:]...) |
| paddy@28 | 118 } |
| paddy@28 | 119 return nil |
| paddy@28 | 120 } |
| paddy@28 | 121 |
| paddy@91 | 122 func (m *memstore) revokeToken(token string, refresh bool) error { |
| paddy@91 | 123 if refresh { |
| paddy@91 | 124 t, err := m.lookupTokenByRefresh(token) |
| paddy@91 | 125 if err != nil { |
| paddy@91 | 126 return err |
| paddy@91 | 127 } |
| paddy@91 | 128 token = t |
| paddy@91 | 129 } |
| paddy@91 | 130 m.tokenLock.Lock() |
| paddy@91 | 131 defer m.tokenLock.Unlock() |
| paddy@91 | 132 t, ok := m.tokens[token] |
| paddy@91 | 133 if !ok { |
| paddy@91 | 134 return ErrTokenNotFound |
| paddy@91 | 135 } |
| paddy@123 | 136 if refresh { |
| paddy@123 | 137 t.RefreshRevoked = true |
| paddy@123 | 138 } else { |
| paddy@123 | 139 t.Revoked = true |
| paddy@123 | 140 } |
| paddy@91 | 141 m.tokens[token] = t |
| paddy@91 | 142 return nil |
| paddy@91 | 143 } |
| paddy@91 | 144 |
| paddy@57 | 145 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@28 | 146 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@28 | 147 if err != nil { |
| paddy@28 | 148 return []Token{}, err |
| paddy@28 | 149 } |
| paddy@28 | 150 if len(ids) > num+offset { |
| paddy@28 | 151 ids = ids[offset : num+offset] |
| paddy@28 | 152 } else if len(ids) > offset { |
| paddy@28 | 153 ids = ids[offset:] |
| paddy@28 | 154 } else { |
| paddy@28 | 155 return []Token{}, nil |
| paddy@28 | 156 } |
| paddy@28 | 157 tokens := []Token{} |
| paddy@28 | 158 for _, id := range ids { |
| paddy@57 | 159 token, err := m.getToken(id, false) |
| paddy@28 | 160 if err != nil { |
| paddy@28 | 161 return []Token{}, err |
| paddy@28 | 162 } |
| paddy@28 | 163 tokens = append(tokens, token) |
| paddy@28 | 164 } |
| paddy@28 | 165 return tokens, nil |
| paddy@28 | 166 } |
| paddy@123 | 167 |
| paddy@123 | 168 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { |
| paddy@123 | 169 enc := json.NewEncoder(w) |
| paddy@123 | 170 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 171 if refresh == "" { |
| paddy@123 | 172 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 173 renderJSONError(enc, "invalid_request") |
| paddy@123 | 174 return |
| paddy@123 | 175 } |
| paddy@123 | 176 token, err := context.GetToken(refresh, true) |
| paddy@123 | 177 if err != nil { |
| paddy@123 | 178 if err == ErrTokenNotFound { |
| paddy@123 | 179 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 180 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 181 return |
| paddy@123 | 182 } |
| paddy@123 | 183 log.Println("Error exchanging refresh token:", err) |
| paddy@123 | 184 w.WriteHeader(http.StatusInternalServerError) |
| paddy@123 | 185 renderJSONError(enc, "server_error") |
| paddy@123 | 186 return |
| paddy@123 | 187 } |
| paddy@123 | 188 clientID, _, ok := getClientAuth(w, r, true) |
| paddy@123 | 189 if !ok { |
| paddy@123 | 190 return |
| paddy@123 | 191 } |
| paddy@123 | 192 if !token.ClientID.Equal(clientID) { |
| paddy@123 | 193 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 194 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 195 return |
| paddy@123 | 196 } |
| paddy@123 | 197 if token.RefreshRevoked { |
| paddy@123 | 198 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 199 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 200 return |
| paddy@123 | 201 } |
| paddy@123 | 202 expires := token.Created.Add(time.Duration(token.RefreshExpiresIn) * time.Second) |
| paddy@123 | 203 if expires.Before(time.Now()) { |
| paddy@123 | 204 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 205 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 206 return |
| paddy@123 | 207 } |
| paddy@123 | 208 return token.Scope, token.ProfileID, true |
| paddy@123 | 209 } |
| paddy@123 | 210 |
| paddy@123 | 211 func refreshTokenInvalidate(r *http.Request, context Context) error { |
| paddy@123 | 212 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 213 if refresh == "" { |
| paddy@123 | 214 return ErrTokenNotFound |
| paddy@123 | 215 } |
| paddy@123 | 216 return context.RevokeToken(refresh, true) |
| paddy@123 | 217 } |