auth
auth/token.go
Clean up after Client deletion, finish cleaning up after Profile deletion. 6f473576c6ae started cleaning up after Profiles when they're deleted, but didn't clean up the Clients created by that Profile. This fixes that, and also fixes a BUG note about cleaning up after a Client when it's deleted. Extend the authorizationCodeStore to have a deleteAuthorizationCodesByClientID method that will delete the AuthorizationCodes that have been granted by the Client specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. Extend the clientStore to have a deleteClientsByOwner method that will delete the Clients that were created by the Profile specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. Extend the clientStore to have a removeEndpointsByClientID method that will delete the Endpoints that belong(ed) to a the Client specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. Extend the tokenStore to have a revokeTokensByClientID method that will revoke all the Tokens that were granted to the Client specified by the passed ID. We also implemented this in memstore and postgres, so tests continue to pass. When listing Clients by their owner, allow setting the num argument (which controls how many to return) to 0 or lower, and using that to signal "return all Clients belonging to this owner", instead of paging. This is useful when deleting the Clients belonging to a Profile as part of the cleanup after deleting the Profile. Create a cleanUpAfterClientDeletion helper function that will delete the Endpoints and AuthorizationCodes belonging to a Client, and revoke the Tokens belonging to a Client, as part of cleaning up after a Client has been deleted. Add a check in the handler for listing Clients owned by a Profile to disallow the num argument to be lower than 1, because the API should be forced to page. Call our cleanUpAfterClientDeletion once the Client has been deleted in the appropriate handler. Fill out our Context with new methods to wrap all the new methods we're adding to our *Stores. In cleanUpAfterProfileDeletion, obtain a list of clients belonging to the owner, use our new DeleteClientsByOwner method to remove all of them, and then use the list to run our new cleanUpAfterClientDeletion function to clear away the final remnants of a Profile when it's deleted.
| 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@125 | 14 defaultTokenExpiration = 3600 // one hour |
| paddy@69 | 15 ) |
| paddy@69 | 16 |
| paddy@123 | 17 func init() { |
| paddy@123 | 18 RegisterGrantType("refresh_token", GrantType{ |
| paddy@123 | 19 Validate: refreshTokenValidate, |
| paddy@123 | 20 Invalidate: refreshTokenInvalidate, |
| paddy@123 | 21 IssuesRefresh: true, |
| paddy@123 | 22 ReturnToken: RenderJSONToken, |
| paddy@124 | 23 AuditString: refreshTokenAuditString, |
| 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@125 | 40 AccessToken string |
| paddy@125 | 41 RefreshToken string |
| paddy@125 | 42 Created time.Time |
| paddy@125 | 43 CreatedFrom string |
| paddy@125 | 44 ExpiresIn int32 |
| paddy@125 | 45 TokenType string |
| paddy@163 | 46 Scopes Scopes |
| paddy@125 | 47 ProfileID uuid.ID |
| paddy@125 | 48 ClientID uuid.ID |
| paddy@125 | 49 Revoked bool |
| paddy@125 | 50 RefreshRevoked bool |
| paddy@28 | 51 } |
| paddy@28 | 52 |
| paddy@57 | 53 type tokenStore interface { |
| paddy@57 | 54 getToken(token string, refresh bool) (Token, error) |
| paddy@57 | 55 saveToken(token Token) error |
| paddy@91 | 56 revokeToken(token string, refresh bool) error |
| paddy@57 | 57 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) |
| paddy@162 | 58 revokeTokensByProfileID(profileID uuid.ID) error |
| paddy@164 | 59 revokeTokensByClientID(clientID uuid.ID) 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@91 | 98 func (m *memstore) revokeToken(token string, refresh bool) error { |
| paddy@91 | 99 if refresh { |
| paddy@91 | 100 t, err := m.lookupTokenByRefresh(token) |
| paddy@91 | 101 if err != nil { |
| paddy@91 | 102 return err |
| paddy@91 | 103 } |
| paddy@91 | 104 token = t |
| paddy@91 | 105 } |
| paddy@91 | 106 m.tokenLock.Lock() |
| paddy@91 | 107 defer m.tokenLock.Unlock() |
| paddy@91 | 108 t, ok := m.tokens[token] |
| paddy@91 | 109 if !ok { |
| paddy@91 | 110 return ErrTokenNotFound |
| paddy@91 | 111 } |
| paddy@123 | 112 if refresh { |
| paddy@123 | 113 t.RefreshRevoked = true |
| paddy@123 | 114 } else { |
| paddy@123 | 115 t.Revoked = true |
| paddy@123 | 116 } |
| paddy@91 | 117 m.tokens[token] = t |
| paddy@91 | 118 return nil |
| paddy@91 | 119 } |
| paddy@91 | 120 |
| paddy@162 | 121 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error { |
| paddy@162 | 122 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@162 | 123 if err != nil { |
| paddy@162 | 124 return err |
| paddy@162 | 125 } |
| paddy@162 | 126 if len(ids) < 1 { |
| paddy@162 | 127 return ErrProfileNotFound |
| paddy@162 | 128 } |
| paddy@162 | 129 m.tokenLock.Lock() |
| paddy@162 | 130 defer m.tokenLock.Unlock() |
| paddy@162 | 131 for _, id := range ids { |
| paddy@164 | 132 token := m.tokens[id] |
| paddy@164 | 133 token.Revoked = true |
| paddy@164 | 134 token.RefreshRevoked = true |
| paddy@164 | 135 m.tokens[id] = token |
| paddy@164 | 136 } |
| paddy@164 | 137 return nil |
| paddy@164 | 138 } |
| paddy@164 | 139 |
| paddy@164 | 140 func (m *memstore) revokeTokensByClientID(clientID uuid.ID) error { |
| paddy@164 | 141 m.tokenLock.Lock() |
| paddy@164 | 142 defer m.tokenLock.Unlock() |
| paddy@164 | 143 for id, token := range m.tokens { |
| paddy@164 | 144 if !token.ClientID.Equal(clientID) { |
| paddy@164 | 145 continue |
| paddy@164 | 146 } |
| paddy@164 | 147 token.Revoked = true |
| paddy@164 | 148 token.RefreshRevoked = true |
| paddy@164 | 149 m.tokens[id] = token |
| paddy@162 | 150 } |
| paddy@162 | 151 return nil |
| paddy@162 | 152 } |
| paddy@162 | 153 |
| paddy@57 | 154 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@28 | 155 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@28 | 156 if err != nil { |
| paddy@28 | 157 return []Token{}, err |
| paddy@28 | 158 } |
| paddy@28 | 159 if len(ids) > num+offset { |
| paddy@28 | 160 ids = ids[offset : num+offset] |
| paddy@28 | 161 } else if len(ids) > offset { |
| paddy@28 | 162 ids = ids[offset:] |
| paddy@28 | 163 } else { |
| paddy@28 | 164 return []Token{}, nil |
| paddy@28 | 165 } |
| paddy@28 | 166 tokens := []Token{} |
| paddy@28 | 167 for _, id := range ids { |
| paddy@57 | 168 token, err := m.getToken(id, false) |
| paddy@28 | 169 if err != nil { |
| paddy@28 | 170 return []Token{}, err |
| paddy@28 | 171 } |
| paddy@28 | 172 tokens = append(tokens, token) |
| paddy@28 | 173 } |
| paddy@28 | 174 return tokens, nil |
| paddy@28 | 175 } |
| paddy@123 | 176 |
| paddy@163 | 177 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) { |
| paddy@123 | 178 enc := json.NewEncoder(w) |
| paddy@123 | 179 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 180 if refresh == "" { |
| paddy@123 | 181 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 182 renderJSONError(enc, "invalid_request") |
| paddy@123 | 183 return |
| paddy@123 | 184 } |
| paddy@123 | 185 token, err := context.GetToken(refresh, true) |
| paddy@123 | 186 if err != nil { |
| paddy@123 | 187 if err == ErrTokenNotFound { |
| paddy@123 | 188 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 189 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 190 return |
| paddy@123 | 191 } |
| paddy@123 | 192 log.Println("Error exchanging refresh token:", err) |
| paddy@123 | 193 w.WriteHeader(http.StatusInternalServerError) |
| paddy@123 | 194 renderJSONError(enc, "server_error") |
| paddy@123 | 195 return |
| paddy@123 | 196 } |
| paddy@123 | 197 clientID, _, ok := getClientAuth(w, r, true) |
| paddy@123 | 198 if !ok { |
| paddy@123 | 199 return |
| paddy@123 | 200 } |
| paddy@123 | 201 if !token.ClientID.Equal(clientID) { |
| paddy@123 | 202 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 203 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 204 return |
| paddy@123 | 205 } |
| paddy@123 | 206 if token.RefreshRevoked { |
| paddy@123 | 207 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 208 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 209 return |
| paddy@123 | 210 } |
| paddy@135 | 211 return token.Scopes, token.ProfileID, true |
| paddy@123 | 212 } |
| paddy@123 | 213 |
| paddy@123 | 214 func refreshTokenInvalidate(r *http.Request, context Context) error { |
| paddy@123 | 215 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 216 if refresh == "" { |
| paddy@123 | 217 return ErrTokenNotFound |
| paddy@123 | 218 } |
| paddy@123 | 219 return context.RevokeToken(refresh, true) |
| paddy@123 | 220 } |
| paddy@124 | 221 |
| paddy@124 | 222 func refreshTokenAuditString(r *http.Request) string { |
| paddy@124 | 223 return "refresh_token:" + r.PostFormValue("refresh_token") |
| paddy@124 | 224 } |
| paddy@128 | 225 |
| paddy@128 | 226 // BUG(paddy): We need to implement a handler for revoking a token. |
| paddy@128 | 227 // BUG(paddy): We need to implement a handler for listing active tokens. |