mirror of https://github.com/dexidp/dex.git
1 changed files with 146 additions and 0 deletions
@ -0,0 +1,146 @@
|
||||
# Proposal: user objects for revoking refresh tokens and merging accounts |
||||
|
||||
Certain operations require tracking users the have logged in through the server |
||||
and storing them in the backend. Namely, allowing end users to revoke refresh |
||||
tokens and merging existing accounts with upstream providers. |
||||
|
||||
While revoking refresh tokens is relatively easy, merging accounts is a |
||||
difficult problem. What if display names or emails are different? What happens |
||||
to a user with two remote identities with the same upstream service? Should |
||||
this be presented differently for a user with remote identities for different |
||||
upstream services? This proposal only covers a minimal merging implementation |
||||
by guaranteeing that merged accounts will always be presented to clients with |
||||
the same user ID. |
||||
|
||||
This proposal defines the following objects and methods to be added to the |
||||
storage package to allow user information to be persisted. |
||||
|
||||
```go |
||||
// User is an end user which has logged in to the server. |
||||
// |
||||
// Users do not hold additional data, such as emails, because claim information |
||||
// is always supplied by an upstream provider during the auth flow. The ID is |
||||
// the only information from this object which overrides the claims produced by |
||||
// connectors. |
||||
// |
||||
// Clients which wish to associate additional data with a user must do so on |
||||
// their own. The server only guarantees that IDs will be constant for an end |
||||
// user, no matter what backend they use to login. |
||||
type User struct { |
||||
// A string which uniquely identifies the user for the server. This overrides |
||||
// the ID provided by the connector in the ID Token claims. |
||||
ID string |
||||
|
||||
// A list of clients who have been issued refresh tokens for this user. |
||||
// |
||||
// When a refresh token is redeemed, the server will check this field to |
||||
// ensure that the client is still on this list. To revoke a client, |
||||
// remove it from here. |
||||
AuthorizedClients []AuthorizedClient |
||||
|
||||
// A set of remote identities which are able to login as this user. |
||||
RemoteIdentities []RemoteIdentity |
||||
} |
||||
|
||||
// AuthorizedClient is a client that has a refresh token out for this user. |
||||
type AuthorizedClient struct { |
||||
// The ID of the client. |
||||
ClientID string |
||||
// The last time a token was refreshed. |
||||
LastRefreshed time.Time |
||||
} |
||||
|
||||
// RemoteIdentity is the smallest amount of information that identifies a user |
||||
// with a remote service. It indicates which remote identities should be able |
||||
// to login as a specific user. |
||||
// |
||||
// RemoteIdentity contains an username so an end user can be displayed this |
||||
// object and reason about what upstream profile it represents. It is not used |
||||
// to cache claims, such as groups or emails, because these are always provided |
||||
// by the upstream identity system during login. |
||||
type RemoteIdentity struct { |
||||
// The ID of the connector used to login the user. |
||||
ConnectorID string |
||||
// A string which uniquely identifies the user with the remote system. |
||||
ConnectorUserID stirng |
||||
|
||||
// Optional, human readable name for this remote identity. Only used when |
||||
// displaying the remote identity to the end user (e.g. when merging |
||||
// accounts). NOT used for determining ID Token claims. |
||||
Username string |
||||
} |
||||
``` |
||||
|
||||
`UserID` fields will be added to the `AuthRequest`, `AuthCode` and `RefreshToken` |
||||
structs. When a user logs in successfully through a connector |
||||
[here](https://github.com/coreos/poke/blob/95a61454b522edd6643ced36b9d4b9baa8059556/server/handlers.go#L227), |
||||
the server will attempt to either get the user, or create one if none exists with |
||||
the remote identity. |
||||
|
||||
`AuthorizedClients` serves two roles. First is makes displaying the set of |
||||
clients a user is logged into easy. Second, because we don't assume multi-object |
||||
transactions, we can't ensure deleting all refresh tokens a client has for a |
||||
user. Between listing the set of refresh tokens and deleting a token, a client |
||||
may have already redeemed the token and created a new one. |
||||
|
||||
When an OAuth2 client exchanges a code for a token, the following steps are |
||||
taken to populate the `AuthorizedClients`: |
||||
|
||||
1. Get token where the user has authorized the `offline_access` scope. |
||||
1. Update the user checking authorized clients. If client is not in the list, |
||||
add it. |
||||
1. Create a refresh token and return the token. |
||||
|
||||
When a OAuth2 client attempts to renew a refresh token, the server ensures that |
||||
the token hasn't been revoked. |
||||
|
||||
1. Check authorized clients and update the `LastRefreshed` timestamp. If client |
||||
isn't in list error out and delete the refresh token. |
||||
1. Continue renewing the refresh token. |
||||
|
||||
When the end user revokes a client, the following steps are used to. |
||||
|
||||
1. Update the authorized clients by removing the client from the list. This |
||||
atomic action causes any renew attempts to fail. |
||||
1. Iterate through list of refresh tokens and garbage collect any tokens issued |
||||
by the user for the client. This isn't atomic, but exists so a user can |
||||
re-authorize a client at a later time without authorizing old refresh tokens. |
||||
|
||||
This is clunky due to the lack of multi-object transactions. E.g. we can't delete |
||||
all the refresh tokens at once because we don't have that guarantee. |
||||
|
||||
Merging accounts becomes extremely simple. Just add another remote identity to |
||||
the user object. |
||||
|
||||
We hope to provide a web interface that a user can login to to perform these |
||||
actions. Perhaps using a well known client issued exclusively for the server. |
||||
|
||||
The new `User` object requires adding the following methods to the storage |
||||
interface, and (as a nice side effect) deleting the `ListRefreshTokens()` method. |
||||
|
||||
```go |
||||
type Storage interface { |
||||
// ... |
||||
|
||||
CreateUser(u User) error |
||||
|
||||
DeleteUser(id string) error |
||||
|
||||
GetUser(id string) error |
||||
GetUserByRemoteIdentity(connectorID, connectorUserID string) (User, error) |
||||
|
||||
// Updates are assumed to be atomic. |
||||
// |
||||
// When a UpdateUser is called, if clients are removed from the |
||||
// AuthorizedClients list, the underlying storage SHOULD clean up refresh |
||||
// tokens issued for the removed clients. This allows backends with |
||||
// multi-transactional capabilities to utilize them, while key-value stores |
||||
// only guarantee best effort. |
||||
UpdateUser(id string, updater func(old User) (User, error)) error |
||||
} |
||||
``` |
||||
|
||||
Importantly, this will be the first object which has a secondary index. |
||||
The Kubernetes client will simply list all the users in memory then iterate over |
||||
them to support this (possibly followed by a "watch" based optimization). SQL |
||||
implementations will have an easier time. |
||||
Loading…
Reference in new issue