Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OAuth2 PKCE in SSORoleCredentialsProvider #1258

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

patrickod
Copy link

Inspired by AWS CLI's recent change this PR adds support for the OAuth2
"authorization code" grant flow accompanied by Proof Key for Code
Exchange (PKCE) by OAuth Public Clients
and makes it the default.

In the "device code" flow the user is entirely responsible for matching
the one time password displayed by their terminal and browser. This is
error prone at best and a one click penalty for every login at worst.

In the "authorization code" flow we create an ephemeral HTTP server to
host an OAuth2 callback on 127.0.0.1. Users are redirected here after
confirming their login attempt and the code comparison step is automated
for them. As AWS enforces the use of 127.0.0.1 as a callback destination
host a remote attacker who successfully convinces a user to confirm
their malicious login prompt would not be able to access the resulting
credential as the user would be redirected to their own localhost and
not an attacker controlled destination.

If we determine that we are not able to open a browser (either from the
user specifying --stdout or observing that we are executing in an SSH
session) we revert to the previous "device code" flow and print the URLs
for copy/pasting into browsers.

Inspired by AWS CLI's recent change this PR adds support for OAuth2
"authorization code" grant flow accompanied by Proof Key for Code
Exchange (PKCE) by OAuth Public Clients and makes it the default.

In the "device code" flow the user is entirely responsible for matching
the one time password displayed by their terminal and browser. This is
error prone at best and a one click penalty for every login at worst.

In the "authorization code" flow we create an ephemeral HTTP server to
host an OAuth2 callback on 127.0.0.1. Users are redirected here after
confirming their login attempt and the code comparison step is automated
for them. As AWS enforces the use of 127.0.0.1 as a callback destination
host a remote attacker who successfully convinces a user to confirm
their malicious login prompt would not be able to access the resulting
credential as the user would be redirected to their own localhost and
not an attacker controlled destination.

If we determine that we are not able to open a browser (either from the
user specifying --stdout or observing that we are executing in an SSH
session) we revert to the previous "device code" flow and print the URLs
for copy/pasting into browsers.
// generate the code challenge: base64(sha256(codeVerifier))
codeChallengeBytes := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(codeChallengeBytes[:])
log.Printf("Generated PKCE code_challenge: %q", codeChallenge)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this necessarily needs to be displayed by default

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused at first also at the use of log.Printf in this project but these log lines are not displayed unless the user passes --debug as they're otherwise sent to io.Discard

aws-vault/cli/global.go

Lines 166 to 168 in d4706c8

if !a.Debug {
log.SetOutput(io.Discard)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, good point

// If we're in an SSH session, we can't use the browser for SSO so we print
// the URLs to stdout instead.
useStdout := config.SSOUseStdout
if os.Getenv("SSH_CONNECTION") != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like an addition that's independant from the PKCE flow? Overall I wouldn't necessarily expect this "magic" behavior from aws-vault, as a user I prefer a consistent behavior and explicitely passing --stdout if needs be

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to back this out and have it be a separate discussion. My thinking was that given we now have login flows that cater to browser and non-browser setups to direct the user to the one that makes sense in context but I can understand how that would be confusing. A number of us at Tailscale use aws-vault with remote workspaces and run into the no-browser scenario frequently.

// Code Exchange (PKCE) and open the browser automatically. The latter flow
// is more user-friendly and secure because the step comparing the challenge
// code between the CLI and browser is automated entirely.
if p.UseStdout {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commonly use --stdout even when I'm on the same machine as where aws-vault is running, for various purposes (e.g. wanting to login in a non-default browser). I'm not 100% convinced that UseStdout should be the basis for chosing between the device code and PKCE flow, how about instead following the same behavior as the AWS CLI? It seems to have a flag --use-device-code flag aws/aws-cli@130005a#diff-e07a10a6eb1a677e905b0498651e672137b5dff19d357933bd7bc3e36f845a3bL42 which defaults to false

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great shout. I hadn't considered the same device non-default browser path that users might wish to take. I was primarily thinking about remote workstations and in that context printing the URL out to be opened locally would not make sense as the eventual redirect destination would be inaccessible to the browser.

log.Printf("Authorize URL: %s", authorizeURL.String())

// redirect user to the authorize URL
log.Println("Opening SSO authorization page in browser")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(related to my first comment of this PR, I think this should rather honor the --stdout parameter to decide what to do)


// await the authorization code from the callback server once the user has completed the flow.
var code string
timeout := time.After(1 * time.Minute)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW the device code implementation here waits indefinitely (modulo a timeout on the AWS side), so perhaps it's worth increasing it?


// send the authorization code to the channel
code := r.URL.Query().Get("code")
s.code <- code
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps it's worth returning early if there's an error rather than letting the consumer timeout? In which case s.code could be a channel of a struct that can hold either an error or a string, and the consumer would do something like

code, err := <- server.code
if err != nil {
   // error
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants