-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
auth: add support for oauth device-code login
This commit adds support for the oauth [device-code](https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow) login flow when authenticating against the official registry. This is achieved by adding `cli/internal/oauth`, which contains code to manage interacting with the Docker OAuth tenant (`login.docker.com`), including launching the device-code flow, refreshing access using the refresh-token, and logging out. The `OAuthManager` introduced here is also made available through the `command.Cli` interface method `OAuthManager()`. In order to manage storage and retrieval of the oauth credentials, as well as ensuring that the access token retrieved from the credentials store is valid when accessed, this commit also introduces a new credential store wrapper, `OAuthStore`, which defers storage to an underlying store (such as the file or native/keychain/pass/wincred store) and gets a new access token using the refresh token when accessed. Signed-off-by: Laura Brehm <laurabrehm@hey.com>
Showing
57 changed files
with
11,830 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package credentials | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"strings" | ||
"time" | ||
|
||
"github.com/docker/cli/cli/config/types" | ||
"github.com/docker/cli/cli/oauth" | ||
"github.com/docker/docker/registry" | ||
) | ||
|
||
// oauthStore wraps an existing store that transparently handles oauth | ||
// flows, managing authentication/token refresh and piggybacking off an | ||
// existing store for storage/retrieval. | ||
type oauthStore struct { | ||
backingStore Store | ||
manager oauth.Manager | ||
} | ||
|
||
// NewOAuthStore creates a new oauthStore backed by the provided store. | ||
func NewOAuthStore(backingStore Store, manager oauth.Manager) Store { | ||
return &oauthStore{ | ||
backingStore: backingStore, | ||
manager: manager, | ||
} | ||
} | ||
|
||
const minimumTokenLifetime = 50 * time.Minute | ||
|
||
// Get retrieves the credentials from the backing store, refreshing the | ||
// access token if the retrieved token is valid for less than 50 minutes. | ||
// If there are no credentials in the backing store, the device code flow | ||
// is initiated with the tenant in order to log the user in and get | ||
func (c *oauthStore) Get(serverAddress string) (types.AuthConfig, error) { | ||
if serverAddress != registry.IndexServer { | ||
return c.backingStore.Get(serverAddress) | ||
} | ||
|
||
auth, err := c.backingStore.Get(serverAddress) | ||
if err != nil { | ||
// If an error happens here, it's not due to the backing store not | ||
// containing credentials, but rather an actual issue with the backing | ||
// store itself. This should be propagated up. | ||
return types.AuthConfig{}, err | ||
} | ||
|
||
tokenRes, err := c.parseToken(auth.Password) | ||
// if the password is not a token, return the auth config as is | ||
if err != nil { | ||
return auth, nil | ||
} | ||
|
||
// if the access token is valid for less than 50 minutes, refresh it | ||
if tokenRes.RefreshToken != "" && tokenRes.Claims.Expiry.Time().Before(time.Now().Add(minimumTokenLifetime)) { | ||
refreshRes, err := c.manager.RefreshToken(context.TODO(), tokenRes.RefreshToken) | ||
if err != nil { | ||
return types.AuthConfig{}, err | ||
} | ||
tokenRes = refreshRes | ||
} | ||
|
||
err = c.storeInBackingStore(tokenRes) | ||
if err != nil { | ||
return types.AuthConfig{}, err | ||
} | ||
|
||
return types.AuthConfig{ | ||
Username: tokenRes.Claims.Domain.Username, | ||
Password: tokenRes.AccessToken, | ||
Email: tokenRes.Claims.Domain.Email, | ||
ServerAddress: registry.IndexServer, | ||
}, nil | ||
} | ||
|
||
// GetAll returns a map containing solely the auth config for the official | ||
// registry, parsed from the backing store and refreshed if necessary. | ||
func (c *oauthStore) GetAll() (map[string]types.AuthConfig, error) { | ||
allAuths, err := c.backingStore.GetAll() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if _, ok := allAuths[registry.IndexServer]; !ok { | ||
return allAuths, nil | ||
} | ||
|
||
auth, err := c.Get(registry.IndexServer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
allAuths[registry.IndexServer] = auth | ||
return allAuths, err | ||
} | ||
|
||
// Erase removes the credentials from the backing store, logging out of the | ||
// tenant if running | ||
func (c *oauthStore) Erase(serverAddress string) error { | ||
if serverAddress == registry.IndexServer { | ||
// todo(laurazard): should this log out from the tenant | ||
_ = c.manager.Logout(context.TODO()) | ||
} | ||
return c.backingStore.Erase(serverAddress) | ||
} | ||
|
||
// Store stores the provided credentials in the backing store, without any | ||
// additional processing. | ||
func (c *oauthStore) Store(auth types.AuthConfig) error { | ||
return c.backingStore.Store(auth) | ||
} | ||
|
||
func (c *oauthStore) parseToken(password string) (oauth.TokenResult, error) { | ||
parts := strings.Split(password, "..") | ||
if len(parts) != 2 { | ||
return oauth.TokenResult{}, errors.New("failed to parse token") | ||
} | ||
accessToken := parts[0] | ||
refreshToken := parts[1] | ||
claims, err := oauth.GetClaims(parts[0]) | ||
if err != nil { | ||
return oauth.TokenResult{}, err | ||
} | ||
return oauth.TokenResult{ | ||
AccessToken: accessToken, | ||
RefreshToken: refreshToken, | ||
Claims: claims, | ||
}, nil | ||
} | ||
|
||
func (c *oauthStore) storeInBackingStore(tokenRes oauth.TokenResult) error { | ||
auth := types.AuthConfig{ | ||
Username: tokenRes.Claims.Domain.Username, | ||
Password: c.concat(tokenRes.AccessToken, tokenRes.RefreshToken), | ||
Email: tokenRes.Claims.Domain.Email, | ||
ServerAddress: registry.IndexServer, | ||
} | ||
return c.backingStore.Store(auth) | ||
} | ||
|
||
func (c *oauthStore) concat(accessToken, refreshToken string) string { | ||
return accessToken + ".." + refreshToken | ||
} |
Oops, something went wrong.