forked from mystiq/dex
proposals: user objects for revoking refresh tokens and merging accounts
This commit is contained in:
parent
95a61454b5
commit
5385ca517a
1 changed files with 146 additions and 0 deletions
146
Documentation/proposals/user-object.md
Normal file
146
Documentation/proposals/user-object.md
Normal file
|
@ -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 a new issue