package oauth

import (
	"bufio"
	"bytes"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/cli/crypto/randutil"
	"github.com/smallstep/cli/exec"
	"github.com/smallstep/cli/flags"
	"github.com/smallstep/cli/jose"
	"github.com/smallstep/cli/utils"
	"github.com/urfave/cli"
	"go.step.sm/cli-utils/command"
	"go.step.sm/cli-utils/errs"
)

// These are the OAuth2.0 client IDs from the Step CLI. This application is
// using the OAuth2.0 flow for installed applications described on
// https://developers.google.com/identity/protocols/OAuth2InstalledApp
//
// The Step CLI app and these client IDs do not have any APIs or services are
// enabled and it should be only used for OAuth 2.0 authorization.
//
// Due to the fact that the app cannot keep the client_secret confidential,
// incremental authorization with installed apps are not supported by Google.
//
// Google is also distributing the client ID and secret on the cloud SDK
// available here https://cloud.google.com/sdk/docs/quickstarts
const (
	defaultClientID = "1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com"
	//nolint:gosec // This is a client meant for open source testing. The client has no security access or roles.
	defaultClientNotSoSecret = "udTrOT3gzrO7W9fDPgZQLfYJ"

	defaultDeviceAuthzClientID = "1087160488420-1u0jqoulmv3mfomfh6fhkfs4vk4bdjih.apps.googleusercontent.com"
	//nolint:gosec // This is a client meant for open source testing. The client has no security access or roles.
	defaultDeviceAuthzClientNotSoSecret = "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"

	defaultDeviceAuthzInterval  = 5
	defaultDeviceAuthzExpiresIn = time.Minute * 5

	// The URN for getting verification token offline
	oobCallbackUrn = "urn:ietf:wg:oauth:2.0:oob"
	// The URN for token request grant type jwt-bearer
	//nolint:gosec // This is a resource identifier (not a secret).
	jwtBearerUrn = "urn:ietf:params:oauth:grant-type:jwt-bearer"
)

type token struct {
	AccessToken  string `json:"access_token"`
	IDToken      string `json:"id_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int    `json:"expires_in"`
	TokenType    string `json:"token_type"`
	Err          string `json:"error,omitempty"`
	ErrDesc      string `json:"error_description,omitempty"`
}

func init() {
	cmd := cli.Command{
		Name:  "oauth",
		Usage: "authorization and single sign-on using OAuth & OIDC",
		UsageText: `**step oauth**
[**--provider**=<provider>] [**--client-id**=<client-id> **--client-secret**=<client-secret>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
[**--prompt**=<prompt>] [**--auth-param**=<key=value>]

**step oauth**
**--authorization-endpoint**=<authorization-endpoint>
**--token-endpoint**=<token-endpoint>
**--client-id**=<client-id> **--client-secret**=<client-secret>
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
[**--prompt**=<prompt>] [**--auth-param**=<key=value>]

**step oauth** [**--account**=<account>]
[**--authorization-endpoint**=<authorization-endpoint>]
[**--token-endpoint**=<token-endpoint>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
[**--prompt**=<prompt>] [**--auth-param**=<key=value>]

**step oauth** **--account**=<account> **--jwt**
[**--scope**=<scope> ...] [**--header**] [**-bare**] [**--prompt**=<prompt>]
[**--auth-param**=<key=value>]`,
		Description: `**step oauth** command implements the OAuth 2.0 authorization flow.

OAuth is an open standard for access delegation, commonly used as a way for
Internet users to grant websites or applications access to their information on
other websites but without giving them the passwords. This mechanism is used by
companies such as Amazon, Google, Facebook, Microsoft and Twitter to permit the
users to share information about their accounts with third party applications or
websites. Learn more at https://en.wikipedia.org/wiki/OAuth.

This command by default performs the authorization flow with a preconfigured
Google application, but a custom one can be set combining the flags
**--client-id**, **--client-secret**, and **--provider**. The provider value
must be set to the OIDC discovery document (.well-known/openid-configuration)
endpoint. If Google is used this flag is not necessary, but the appropriate
value would be be https://accounts.google.com or
https://accounts.google.com/.well-known/openid-configuration

## EXAMPLES

Do the OAuth 2.0 flow using the default client:
'''
$ step oauth
'''

Redirect to localhost instead of 127.0.0.1:
'''
$ step oauth --listen localhost:0
'''

Redirect to a fixed port instead of random one:
'''
$ step oauth --listen :10000
'''

Redirect to a fixed url but listen on all the interfaces:
'''
$ step oauth --listen 0.0.0.0:10000 --listen-url http://127.0.0.1:10000
'''

Get just the access token:
'''
$ step oauth --bare
'''

Get just the OIDC token:
'''
$ step oauth --oidc --bare
'''

Use a custom OAuth2.0 server:
'''
$ step oauth --client-id my-client-id --client-secret my-client-secret \
  --provider https://example.org
'''

Use the Device Authorization Grant flow for input constrained clients:
'''
$ step oauth --client-id my-client-id --client-secret my-client-secret --console-flow device
'''

Use the Out Of Band flow for input constrained clients:
'''
$ step oauth --client-id my-client-id --client-secret my-client-secret --console-flow oob
'''

Use the default OAuth flow for input constrained clients:
'''
$ step oauth --client-id my-client-id --client-secret my-client-secret --console
'''

Use additional authentication parameters:
'''
$ step oauth --client-id my-client-id --client-secret my-client-secret \
  --provider https://example.org --auth-param "access_type=offline"
'''`,
		Flags: []cli.Flag{
			cli.StringFlag{
				Name:  "provider, idp",
				Usage: "OAuth provider for authentication",
				Value: "google",
			},
			cli.StringFlag{
				Name:  "email, e",
				Usage: "Email to authenticate",
			},
			cli.BoolFlag{
				Name: "console, c",
				Usage: `Complete the flow while remaining only inside the terminal.

NOTE: This flag will continue to use the Out of Band (OOB) flow for Google OAuth clients
until Oct 3, 2022 when the OOB flow will be shut off. All other OAuth clients
will default to using the Device Authorization Grant flow
(https://datatracker.ietf.org/doc/html/rfc8628#section-3.2).`,
			},
			cli.StringFlag{
				Name: "console-flow",
				Usage: `The alternative OAuth <flow> to use for input constrained devices.

: <console-flow> is a case-insensitive string and must be one of:

    **device**
    :  Use the Device Authorization Grant (https://datatracker.ietf.org/doc/html/rfc8628#section-3.2) flow

    **oob**
    :  Use the Out of Band (OOB) flow`,
			},
			cli.StringFlag{
				Name:  "client-id",
				Usage: "OAuth Client ID",
			},
			cli.StringFlag{
				Name:  "client-secret",
				Usage: "OAuth Client Secret",
			},
			cli.StringFlag{
				Name:  "account",
				Usage: "JSON file containing account details",
			},
			cli.StringFlag{
				Name:  "authorization-endpoint",
				Usage: "OAuth Authorization Endpoint",
			},
			cli.StringFlag{
				Name:  "device-authorization-endpoint",
				Usage: "OAuth Device Authorization Endpoint",
			},
			cli.StringFlag{
				Name:  "token-endpoint",
				Usage: "OAuth Token Endpoint",
			},
			cli.BoolFlag{
				Name:  "header",
				Usage: "Output HTTP Authorization Header (suitable for use with curl)",
			},
			cli.BoolFlag{
				Name:  "oidc",
				Usage: "Output OIDC Token instead of OAuth Access Token",
			},
			cli.BoolFlag{
				Name:  "bare",
				Usage: "Only output the token",
			},
			cli.StringSliceFlag{
				Name:  "scope",
				Usage: "OAuth scopes",
			},
			cli.StringSliceFlag{
				Name: "auth-param",
				Usage: `OAuth additional authentication parameters to include as part of the URL query.
Use this flag multiple times to add multiple parameters. This flag expects a
'key' and 'value' in the format '--auth-param "key=value"'.`,
			},
			cli.StringFlag{
				Name: "prompt",
				Usage: `Whether the Authorization Server prompts the End-User for reauthentication and consent.
OpenID standard defines the following values, but your provider may support some or none of them:

    **none**
    :   The Authorization Server MUST NOT display any authentication or consent user interface pages.
        An error is returned if an End-User is not already authenticated or the Client does not have
        pre-configured consent for the requested Claims or does not fulfill other conditions for
        processing the request.

    **login**
    :   The Authorization Server SHOULD prompt the End-User for reauthentication. If it cannot
        reauthenticate the End-User, it MUST return an error, typically login_required.

    **consent**
    :   The Authorization Server SHOULD prompt the End-User for consent before returning information
        to the Client. If it cannot obtain consent, it MUST return an error, typically consent_required.

    **select_account**
    :   The Authorization Server SHOULD prompt the End-User to select a user account. This enables an
        End-User who has multiple accounts at the Authorization Server to select amongst the multiple
        accounts that they might have current sessions for. If it cannot obtain an account selection
        choice made by the End-User, it MUST return an error, typically account_selection_required.
`,
			},
			cli.BoolFlag{
				Name:  "jwt",
				Usage: "Generate a JWT Auth token instead of an OAuth Token (only works with service accounts)",
			},
			cli.StringFlag{
				Name:  "listen",
				Usage: "Callback listener <address> (e.g. \":10000\")",
			},
			cli.StringFlag{
				Name:  "listen-url",
				Usage: "The redirect_uri <url> in the authorize request (e.g. \"http://127.0.0.1:10000\")",
			},
			cli.BoolFlag{
				Name:   "implicit",
				Usage:  "Uses the implicit flow to authenticate the user. Requires **--insecure** and **--client-id** flags.",
				Hidden: true,
			},
			cli.BoolFlag{
				Name:   "insecure",
				Usage:  "Allows the use of insecure flows.",
				Hidden: true,
			},
			cli.StringFlag{
				Name:   "browser",
				Usage:  "Path to browser for OAuth flow (macOS only).",
				Hidden: true,
			},
			flags.RedirectURL,
		},
		Action: oauthCmd,
	}

	command.Register(cmd)
}

type consoleFlow int

const (
	oobConsoleFlow consoleFlow = iota
	deviceConsoleFlow
)

type options struct {
	Provider            string
	Email               string
	Console             bool
	ConsoleFlow         consoleFlow
	Implicit            bool
	CallbackListener    string
	CallbackListenerURL string
	CallbackPath        string
	TerminalRedirect    string
	Browser             string
}

// Validate validates the options.
func (o *options) Validate() error {
	if o.Provider != "google" && !strings.HasPrefix(o.Provider, "https://") {
		return errors.New("use a valid provider: google")
	}
	if o.CallbackListener != "" {
		if _, _, err := net.SplitHostPort(o.CallbackListener); err != nil {
			return errors.Wrapf(err, "invalid value '%s' for flag '--listen'", o.CallbackListener)
		}
	}
	if o.CallbackListenerURL != "" {
		u, err := url.Parse(o.CallbackListenerURL)
		if err != nil || u.Scheme == "" {
			return errors.Wrapf(err, "invalid value '%s' for flag '--listen-url'", o.CallbackListenerURL)
		}
		if u.Path != "" {
			o.CallbackPath = u.Path
		}
	}
	return nil
}

func oauthCmd(c *cli.Context) error {
	opts := &options{
		Provider:            c.String("provider"),
		Email:               c.String("email"),
		Console:             c.Bool("console"),
		Implicit:            c.Bool("implicit"),
		CallbackListener:    c.String("listen"),
		CallbackListenerURL: c.String("listen-url"),
		CallbackPath:        "/",
		TerminalRedirect:    c.String("redirect-url"),
		Browser:             c.String("browser"),
	}
	if err := opts.Validate(); err != nil {
		return err
	}
	if (opts.Provider != "google" || c.IsSet("authorization-endpoint")) && !c.IsSet("client-id") {
		return errors.New("flag '--client-id' required with '--provider'")
	}

	isOOBFlow, isDeviceFlow := false, false
	consoleFlowInput := c.String("console-flow")
	switch {
	case strings.EqualFold(consoleFlowInput, "device"):
		opts.Console = true
		opts.ConsoleFlow = deviceConsoleFlow
		isDeviceFlow = true
	case strings.EqualFold(consoleFlowInput, "oob"):
		opts.Console = true
		opts.ConsoleFlow = oobConsoleFlow
		isOOBFlow = true
	case c.IsSet("console-flow"):
		return errs.InvalidFlagValue(c, "console-flow", consoleFlowInput, "device, oob")
	case c.Bool("console"):
		oobDeprecationDate := time.Date(2022, time.October, 3, 0, 0, 0, 0, time.UTC)
		if time.Now().Before(oobDeprecationDate) && (opts.Provider == "google" || strings.HasPrefix(opts.Provider, "https://accounts.google.com")) {
			isOOBFlow = true
			opts.ConsoleFlow = oobConsoleFlow
		} else {
			isDeviceFlow = true
			opts.ConsoleFlow = deviceConsoleFlow
		}
	}

	var clientID, clientSecret string
	switch {
	case opts.Implicit:
		if !c.Bool("insecure") {
			return errs.RequiredInsecureFlag(c, "implicit")
		}
		if !c.IsSet("client-id") {
			return errs.RequiredWithFlag(c, "implicit", "client-id")
		}
	case isDeviceFlow:
		clientID = defaultDeviceAuthzClientID
		clientSecret = defaultDeviceAuthzClientNotSoSecret
	default:
		clientID = defaultClientID
		clientSecret = defaultClientNotSoSecret
	}

	if c.IsSet("client-id") {
		clientID = c.String("client-id")
		clientSecret = c.String("client-secret")
	}

	authzEp := ""
	deviceAuthzEp := ""
	tokenEp := ""
	if c.IsSet("authorization-endpoint") {
		if !c.IsSet("token-endpoint") {
			return errors.New("flag '--authorization-endpoint' requires flag '--token-endpoint'")
		}
		opts.Provider = ""
		authzEp = c.String("authorization-endpoint")
		tokenEp = c.String("token-endpoint")
	}
	if c.IsSet("device-authorization-endpoint") {
		if !c.IsSet("token-endpoint") {
			return errors.New("flag '--device-authorization-endpoint' requires flag '--token-endpoint'")
		}
		opts.Provider = ""
		deviceAuthzEp = c.String("device-authorization-endpoint")
		tokenEp = c.String("token-endpoint")
	}

	do2lo := false
	issuer := ""
	// This code supports Google service accounts. Probably maybe also support JWKs?
	if c.IsSet("account") {
		opts.Provider = ""
		filename := c.String("account")
		b, err := os.ReadFile(filename)
		if err != nil {
			return errors.Wrapf(err, "error reading account from %s", filename)
		}
		account := make(map[string]interface{})
		if err = json.Unmarshal(b, &account); err != nil {
			return errors.Wrapf(err, "error reading %s: unsupported format", filename)
		}

		if _, ok := account["installed"]; ok {
			details := account["installed"].(map[string]interface{})
			authzEp = details["auth_uri"].(string)
			tokenEp = details["token_uri"].(string)
			clientID = details["client_id"].(string)
			clientSecret = details["client_secret"].(string)
		} else if accountType, ok := account["type"]; ok && accountType == "service_account" {
			authzEp = account["auth_uri"].(string)
			tokenEp = account["token_uri"].(string)
			clientID = account["private_key_id"].(string)
			clientSecret = account["private_key"].(string)
			issuer = account["client_email"].(string)
			do2lo = true
		} else {
			return errors.Wrapf(err, "error reading %s: unsupported account type", filename)
		}
	}

	scope := "openid email"
	if c.IsSet("scope") {
		scope = strings.Join(c.StringSlice("scope"), " ")
	}
	prompt := ""
	if c.IsSet("prompt") {
		prompt = c.String("prompt")
	}

	authParams := url.Values{}
	for _, keyval := range c.StringSlice("auth-param") {
		parts := strings.SplitN(keyval, "=", 2)
		var k, v string
		switch len(parts) {
		case 1:
			k, v = parts[0], ""
		case 2:
			k, v = parts[0], parts[1]
		default:
			return errs.InvalidFlagValue(c, "auth-param", keyval, "")
		}
		if k == "" {
			return errs.InvalidFlagValue(c, "auth-param", keyval, "")
		}
		authParams.Add(k, v)
	}

	o, err := newOauth(opts.Provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp, scope, prompt, authParams, opts)
	if err != nil {
		return err
	}

	var tok *token
	switch {
	case do2lo:
		if c.Bool("jwt") {
			tok, err = o.DoJWTAuthorization(issuer, scope)
		} else {
			tok, err = o.DoTwoLeggedAuthorization(issuer)
		}
	case isDeviceFlow:
		tok, err = o.DoDeviceAuthorization()
	case isOOBFlow:
		tok, err = o.DoManualAuthorization()
	default:
		tok, err = o.DoLoopbackAuthorization()
	}

	if err != nil {
		return err
	}

	if c.Bool("header") {
		if c.Bool("oidc") {
			fmt.Println("Authorization: Bearer", tok.IDToken)
		} else {
			fmt.Println("Authorization: Bearer", tok.AccessToken)
		}
	} else {
		if c.Bool("bare") {
			if c.Bool("oidc") {
				fmt.Println(tok.IDToken)
			} else {
				fmt.Println(tok.AccessToken)
			}
		} else {
			b, err := json.MarshalIndent(tok, "", "  ")
			if err != nil {
				return errors.Wrapf(err, "error marshaling token data")
			}
			fmt.Println(string(b))
		}
	}

	return nil
}

type oauth struct {
	provider            string
	clientID            string
	clientSecret        string
	scope               string
	prompt              string
	loginHint           string
	redirectURI         string
	tokenEndpoint       string
	authzEndpoint       string
	deviceAuthzEndpoint string
	userInfoEndpoint    string // For testing
	state               string
	codeChallenge       string
	nonce               string
	implicit            bool
	CallbackListener    string
	CallbackListenerURL string
	CallbackPath        string
	terminalRedirect    string
	browser             string
	authParams          url.Values
	errCh               chan error
	tokCh               chan *token
}

func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp, scope, prompt string, authParams url.Values, opts *options) (*oauth, error) {
	state, err := randutil.Alphanumeric(32)
	if err != nil {
		return nil, err
	}

	challenge, err := randutil.Alphanumeric(64)
	if err != nil {
		return nil, err
	}

	nonce, err := randutil.Hex(64) // 256 bits
	if err != nil {
		return nil, err
	}

	switch provider {
	case "google":
		return &oauth{
			provider:            provider,
			clientID:            clientID,
			clientSecret:        clientSecret,
			scope:               scope,
			prompt:              prompt,
			authzEndpoint:       "https://accounts.google.com/o/oauth2/v2/auth",
			deviceAuthzEndpoint: "https://oauth2.googleapis.com/device/code",
			tokenEndpoint:       "https://www.googleapis.com/oauth2/v4/token",
			userInfoEndpoint:    "https://www.googleapis.com/oauth2/v3/userinfo",
			loginHint:           opts.Email,
			state:               state,
			codeChallenge:       challenge,
			nonce:               nonce,
			implicit:            opts.Implicit,
			CallbackListener:    opts.CallbackListener,
			CallbackListenerURL: opts.CallbackListenerURL,
			CallbackPath:        opts.CallbackPath,
			terminalRedirect:    opts.TerminalRedirect,
			browser:             opts.Browser,
			authParams:          authParams,
			errCh:               make(chan error),
			tokCh:               make(chan *token),
		}, nil
	default:
		userinfoEp := ""

		isDeviceFlow := opts.Console && opts.ConsoleFlow == deviceConsoleFlow

		if (isDeviceFlow && deviceAuthzEp == "" && tokenEp == "") ||
			(!isDeviceFlow && authzEp == "" && tokenEp == "") {
			d, err := disco(provider)
			if err != nil {
				return nil, err
			}

			if v, ok := d["device_authorization_endpoint"].(string); !ok && isDeviceFlow {
				return nil, errors.New("missing or invalid 'device_authorization_endpoint' in provider metadata")
			} else if ok {
				deviceAuthzEp = v
			}
			if v, ok := d["authorization_endpoint"].(string); !ok && !isDeviceFlow {
				return nil, errors.New("missing or invalid 'authorization_endpoint' in provider metadata")
			} else if ok {
				authzEp = v
			}
			v, ok := d["token_endpoint"].(string)
			if !ok {
				return nil, errors.New("missing or invalid 'token_endpoint' in provider metadata")
			}
			tokenEp, userinfoEp = v, v
		}

		return &oauth{
			provider:            provider,
			clientID:            clientID,
			clientSecret:        clientSecret,
			scope:               scope,
			prompt:              prompt,
			authzEndpoint:       authzEp,
			deviceAuthzEndpoint: deviceAuthzEp,
			tokenEndpoint:       tokenEp,
			userInfoEndpoint:    userinfoEp,
			loginHint:           opts.Email,
			state:               state,
			codeChallenge:       challenge,
			nonce:               nonce,
			implicit:            opts.Implicit,
			CallbackListener:    opts.CallbackListener,
			CallbackListenerURL: opts.CallbackListenerURL,
			CallbackPath:        opts.CallbackPath,
			terminalRedirect:    opts.TerminalRedirect,
			browser:             opts.Browser,
			authParams:          authParams,
			errCh:               make(chan error),
			tokCh:               make(chan *token),
		}, nil
	}
}

func disco(provider string) (map[string]interface{}, error) {
	u, err := url.Parse(provider)
	if err != nil {
		return nil, err
	}
	// TODO: OIDC and OAuth specify two different ways of constructing this
	// URL. This is the OIDC way. Probably want to try both. See
	// https://tools.ietf.org/html/rfc8414#section-5
	if !strings.Contains(u.Path, "/.well-known/openid-configuration") {
		u.Path = path.Join(u.Path, "/.well-known/openid-configuration")
	}
	resp, err := http.Get(u.String())
	if err != nil {
		return nil, errors.Wrapf(err, "error retrieving %s", u.String())
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Wrapf(err, "error retrieving %s", u.String())
	}
	details := make(map[string]interface{})
	if err = json.Unmarshal(b, &details); err != nil {
		return nil, errors.Wrapf(err, "error reading %s: unsupported format", u.String())
	}
	return details, err
}

// NewServer creates http server
func (o *oauth) NewServer() (*httptest.Server, error) {
	if o.CallbackListener == "" {
		return httptest.NewServer(o), nil
	}
	host, port, err := net.SplitHostPort(o.CallbackListener)
	if err != nil {
		return nil, err
	}
	if host == "" {
		host = "127.0.0.1"
	}
	l, err := net.Listen("tcp", net.JoinHostPort(host, port))
	if err != nil {
		return nil, errors.Wrapf(err, "error listening on %s", o.CallbackListener)
	}
	srv := &httptest.Server{
		Listener: l,
		Config: &http.Server{
			Handler:           o,
			ReadHeaderTimeout: 15 * time.Second,
		},
	}
	srv.Start()

	// Update server url to use for example http://localhost:port
	if host != "127.0.0.1" {
		_, p, err := net.SplitHostPort(l.Addr().String())
		if err != nil {
			return nil, errors.Wrapf(err, "error parsing %s", l.Addr().String())
		}
		srv.URL = "http://" + host + ":" + p
	}

	return srv, nil
}

// DoLoopbackAuthorization performs the log in into the identity provider
// opening a browser and using a redirect_uri in a loopback IP address
// (http://127.0.0.1:port or http://[::1]:port).
func (o *oauth) DoLoopbackAuthorization() (*token, error) {
	srv, err := o.NewServer()
	if err != nil {
		return nil, err
	}
	// Update server url if --listen-url is set
	if o.CallbackListenerURL != "" {
		o.redirectURI = o.CallbackListenerURL
	} else {
		o.redirectURI = srv.URL
	}
	defer srv.Close()

	// Get auth url and open it in a browser
	authURL, err := o.Auth()
	if err != nil {
		return nil, err
	}

	if err := exec.OpenInBrowser(authURL, o.browser); err != nil {
		fmt.Fprintln(os.Stderr, "Cannot open a web browser on your platform.")
		fmt.Fprintln(os.Stderr)
		fmt.Fprintln(os.Stderr, "Open a local web browser and visit:")
		fmt.Fprintln(os.Stderr)
		fmt.Fprintln(os.Stderr, authURL)
		fmt.Fprintln(os.Stderr)
	} else {
		fmt.Fprintln(os.Stderr, "Your default web browser has been opened to visit:")
		fmt.Fprintln(os.Stderr)
		fmt.Fprintln(os.Stderr, authURL)
		fmt.Fprintln(os.Stderr)
	}

	// Wait for response and return the token
	select {
	case tok := <-o.tokCh:
		return tok, nil
	case err := <-o.errCh:
		return nil, err
	case <-time.After(2 * time.Minute):
		return nil, errors.New("oauth command timed out, please try again")
	}
}

// DoManualAuthorization performs the log in into the identity provider
// allowing the user to open a browser on a different system and then entering
// the authorization code on the Step CLI.
func (o *oauth) DoManualAuthorization() (*token, error) {
	o.redirectURI = oobCallbackUrn
	authURL, err := o.Auth()
	if err != nil {
		return nil, err
	}

	fmt.Fprintln(os.Stderr, "Open a local web browser and visit:")
	fmt.Fprintln(os.Stderr)
	fmt.Fprintln(os.Stderr, authURL)
	fmt.Fprintln(os.Stderr)

	// Read from the command line
	fmt.Fprint(os.Stderr, "Enter verification code: ")
	code, err := utils.ReadString(os.Stdin)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	tok, err := o.Exchange(o.tokenEndpoint, code)
	if err != nil {
		return nil, err
	}
	if tok.Err != "" || tok.ErrDesc != "" {
		return nil, errors.Errorf("Error exchanging authorization code: %s. %s", tok.Err, tok.ErrDesc)
	}
	return tok, nil
}

type identifyDeviceResponse struct {
	DeviceCode      string `json:"device_code"`
	UserCode        string `json:"user_code"`
	VerificationURI string `json:"verification_uri"`
	// NOTE Google returns `verification_url` which is incorrect
	// according to the spec (https://datatracker.ietf.org/doc/html/rfc8628#section-3.2)
	// but we'll try to accommodate for that here.
	VerificationURL         string `json:"verification_url"`
	VerificationURIComplete string `json:"verification_uri_complete"`
	ExpiresIn               int    `json:"expires_in"`
	Interval                int    `json:"interval"`
}

// DoDeviceAuthorization gets a token from the IDP using the OAuth 2.0
// Device Authorization Grant. https://datatracker.ietf.org/doc/html/rfc8628
func (o *oauth) DoDeviceAuthorization() (*token, error) {
	// Identify the Device
	data := url.Values{}
	data.Set("client_id", o.clientID)
	data.Set("client_secret", o.clientSecret)
	data.Set("scope", o.scope)

	resp, err := http.PostForm(o.deviceAuthzEndpoint, data)
	if err != nil {
		return nil, errors.Wrap(err, "http failure to identify device")
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode >= 400 {
		var e struct {
			Error string
		}
		if err := json.NewDecoder(bytes.NewReader(b)).Decode(&e); err != nil {
			return nil, errors.Wrapf(err, "could not parse http body: %s", string(b))
		}
	}

	var idr identifyDeviceResponse
	if err := json.NewDecoder(bytes.NewReader(b)).Decode(&idr); err != nil {
		return nil, errors.Wrap(err, "failure decoding device authz response to JWON")
	}

	switch {
	case idr.VerificationURI != "":
		// do nothing
	case idr.VerificationURL != "":
		// NOTE this is a hack for Google, because their API returns the attribute
		// 'verification_url` rather than `verification_uri`.
		idr.VerificationURI = idr.VerificationURL
	default:
		return nil, errors.Errorf("device code response from server missing 'verification_uri' parameter. http body response: %s", string(b))
	}

	if idr.Interval <= 0 {
		idr.Interval = defaultDeviceAuthzInterval
	}

	fmt.Fprintf(os.Stderr, "Visit %s and enter the code: (press 'ENTER' to open default browser)\n", idr.VerificationURI)
	fmt.Fprintln(os.Stderr, idr.UserCode)

	go openBrowserIfAsked(o, idr.VerificationURI)

	// Poll the Token endpoint until the user completes the flow.
	data = url.Values{}
	data.Set("client_id", o.clientID)
	data.Set("client_secret", o.clientSecret)
	data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
	data.Set("device_code", idr.DeviceCode)

	endPollIn := defaultDeviceAuthzExpiresIn
	if idr.ExpiresIn > 0 {
		expiresIn := time.Duration(idr.ExpiresIn) * time.Second
		if expiresIn < endPollIn {
			endPollIn = expiresIn
		}
	}

	t := time.NewTimer(endPollIn)
	defer t.Stop()
	for {
		select {
		case <-time.After(time.Duration(idr.Interval) * time.Second):
			tok, err := o.deviceAuthzTokenPoll(data)
			if errors.Is(err, errHTTPToken) {
				continue
			} else if err != nil {
				return nil, err
			}
			return tok, nil
		case <-t.C:
			return nil, errors.New("device authorization grant expired")
		}
	}
}

func openBrowserIfAsked(o *oauth, u string) {
	reader := bufio.NewReader(os.Stdin)
	reader.ReadString('\n')

	exec.OpenInBrowser(u, o.browser)
}

var errHTTPToken = errors.New("bad request; token not returned")

func (o *oauth) deviceAuthzTokenPoll(data url.Values) (*token, error) {
	resp, err := http.PostForm(o.tokenEndpoint, data)
	if err != nil {
		return nil, errors.Wrap(err, "error doing POST to /token endpoint")
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Wrap(err, "error reading HTTP response body from /token endpoint")
	}

	switch {
	case resp.StatusCode == http.StatusOK:
		tok := token{}
		if err := json.NewDecoder(bytes.NewReader(b)).Decode(&tok); err != nil {
			return nil, errors.Wrap(err, "error parsing JSON /token response")
		}
		return &tok, nil
	case resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError:
		return nil, errHTTPToken
	default:
		return nil, errors.New(string(b))
	}
}

// DoTwoLeggedAuthorization performs two-legged OAuth using the jwt-bearer
// grant type.
func (o *oauth) DoTwoLeggedAuthorization(issuer string) (*token, error) {
	pemBytes := []byte(o.clientSecret)
	block, _ := pem.Decode(pemBytes)
	if block == nil {
		return nil, fmt.Errorf("failed to read private key pem block")
	}
	priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, errors.Wrap(err, "error parsing private key")
	}

	// Add claims
	now := int(time.Now().Unix())
	c := map[string]interface{}{
		"aud":   o.tokenEndpoint,
		"nbf":   now,
		"iat":   now,
		"exp":   now + 3600,
		"iss":   issuer,
		"scope": o.scope,
	}

	so := new(jose.SignerOptions)
	so.WithType("JWT")
	so.WithHeader("kid", o.clientID)

	// Sign JWT
	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: "RS256",
		Key:       priv,
	}, so)
	if err != nil {
		return nil, errors.Wrapf(err, "error creating JWT signer")
	}

	raw, err := jose.Signed(signer).Claims(c).CompactSerialize()
	if err != nil {
		return nil, errors.Wrapf(err, "error serializing JWT")
	}

	// Construct the POST request to fetch the OAuth token.
	params := url.Values{
		"assertion":  []string{raw},
		"grant_type": []string{jwtBearerUrn},
	}

	// Send the POST request and return token.
	resp, err := http.PostForm(o.tokenEndpoint, params)
	if err != nil {
		return nil, errors.Wrapf(err, "error from token endpoint")
	}
	defer resp.Body.Close()

	var tok token
	if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {
		return nil, errors.WithStack(err)
	}

	return &tok, nil
}

// DoJWTAuthorization generates a JWT instead of an OAuth token. Only works for
// certain APIs. See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#jwt-auth.
func (o *oauth) DoJWTAuthorization(issuer, aud string) (*token, error) {
	pemBytes := []byte(o.clientSecret)
	block, _ := pem.Decode(pemBytes)
	if block == nil {
		return nil, fmt.Errorf("failed to read private key pem block")
	}
	priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, errors.Wrap(err, "error parsing private key")
	}

	// Add claims
	now := int(time.Now().Unix())
	c := map[string]interface{}{
		"aud": aud,
		"nbf": now,
		"iat": now,
		"exp": now + 3600,
		"iss": issuer,
		"sub": issuer,
	}

	so := new(jose.SignerOptions)
	so.WithType("JWT")
	so.WithHeader("kid", o.clientID)

	// Sign JWT
	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: "RS256",
		Key:       priv,
	}, so)
	if err != nil {
		return nil, errors.Wrapf(err, "error creating JWT signer")
	}

	raw, err := jose.Signed(signer).Claims(c).CompactSerialize()
	if err != nil {
		return nil, errors.Wrapf(err, "error serializing JWT")
	}

	tok := &token{raw, "", "", 3600, "Bearer", "", ""}
	return tok, nil
}

// ServeHTTP is the handler that performs the OAuth 2.0 dance and returns the
// tokens using channels.
func (o *oauth) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if req.URL.Path != o.CallbackPath {
		http.NotFound(w, req)
		return
	}

	q := req.URL.Query()
	errStr := q.Get("error")
	if errStr != "" {
		o.badRequest(w, "Failed to authenticate: "+errStr)
		return
	}

	if o.implicit {
		o.implicitHandler(w, req)
		return
	}

	code, state := q.Get("code"), q.Get("state")
	if code == "" || state == "" {
		fmt.Fprintf(os.Stderr, "Invalid request received: http://%s%s\n", req.RemoteAddr, req.URL.String())
		fmt.Fprintf(os.Stderr, "You may have an app or browser plugin that needs to be turned off\n")
		http.Error(w, "400 bad request", http.StatusBadRequest)
		return
	}

	if code == "" {
		o.badRequest(w, "Failed to authenticate: missing or invalid code")
		return
	}

	if state == "" || state != o.state {
		o.badRequest(w, "Failed to authenticate: missing or invalid state")
		return
	}

	tok, err := o.Exchange(o.tokenEndpoint, code)
	if err != nil {
		o.badRequest(w, "Failed exchanging authorization code: "+err.Error())
		return
	}
	if tok.Err != "" || tok.ErrDesc != "" {
		o.badRequest(w, fmt.Sprintf("Failed exchanging authorization code: %s. %s", tok.Err, tok.ErrDesc))
		return
	}

	if o.terminalRedirect != "" {
		http.Redirect(w, req, o.terminalRedirect, http.StatusFound)
	} else {
		o.success(w)
	}
	o.tokCh <- tok
}

func (o *oauth) implicitHandler(w http.ResponseWriter, req *http.Request) {
	q := req.URL.Query()
	if hash := q.Get("urlhash"); hash == "true" {
		state := q.Get("state")
		if state == "" || state != o.state {
			o.badRequest(w, "Failed to authenticate: missing or invalid state")
			return
		}
		accessToken := q.Get("access_token")
		if accessToken == "" {
			o.badRequest(w, "Failed to authenticate: missing access token")
			return
		}

		if o.terminalRedirect != "" {
			http.Redirect(w, req, o.terminalRedirect, http.StatusFound)
		} else {
			o.success(w)
		}

		expiresIn, _ := strconv.Atoi(q.Get("expires_in"))
		o.tokCh <- &token{
			AccessToken:  accessToken,
			IDToken:      q.Get("id_token"),
			RefreshToken: q.Get("refresh_token"),
			ExpiresIn:    expiresIn,
			TokenType:    q.Get("token_type"),
		}
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Header().Add("Content-Type", "text/html; charset=utf-8")
	w.Write([]byte(`<html><head><title>Processing OAuth Request</title>`))
	w.Write([]byte(`</head>`))
	w.Write([]byte(`<script type="text/javascript">`))
	fmt.Fprintf(w, `function redirect(){var hash = window.location.hash.substr(1); document.location.href = "%s?urlhash=true&"+hash;}`, o.redirectURI)
	w.Write([]byte(`if (window.addEventListener) window.addEventListener("load", redirect, false); else if (window.attachEvent) window.attachEvent("onload", redirect); else window.onload = redirect;`))
	w.Write([]byte("</script>"))
	w.Write([]byte(`<body><p style='font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 22px; color: #333; width: 400px; margin: 0 auto; text-align: center; line-height: 1.7; padding: 20px;'>`))
	w.Write([]byte(`<strong style='font-size: 28px; color: #000;'>Success</strong><br />`))
	w.Write([]byte(`Click <a href="javascript:redirect();">here</a> if your browser does not automatically redirect you`))
	w.Write([]byte(`</p></body></html>`))
}

// Auth returns the OAuth 2.0 authentication url.
func (o *oauth) Auth() (string, error) {
	u, err := url.Parse(o.authzEndpoint)
	if err != nil {
		return "", errors.WithStack(err)
	}

	q := u.Query()
	q.Add("client_id", o.clientID)
	q.Add("redirect_uri", o.redirectURI)
	if o.implicit {
		q.Add("response_type", "id_token token")
	} else {
		q.Add("response_type", "code")
		q.Add("code_challenge_method", "S256")
		s256 := sha256.Sum256([]byte(o.codeChallenge))
		q.Add("code_challenge", base64.RawURLEncoding.EncodeToString(s256[:]))
	}
	q.Add("scope", o.scope)
	if o.prompt != "" {
		q.Add("prompt", o.prompt)
	}
	q.Add("state", o.state)
	q.Add("nonce", o.nonce)
	if o.loginHint != "" {
		q.Add("login_hint", o.loginHint)
	}
	for k, vs := range o.authParams {
		for _, v := range vs {
			q.Add(k, v)
		}
	}
	u.RawQuery = q.Encode()
	return u.String(), nil
}

// Exchange exchanges the authorization code for refresh and access tokens.
func (o *oauth) Exchange(tokenEndpoint, code string) (*token, error) {
	data := url.Values{}
	data.Set("code", code)
	data.Set("client_id", o.clientID)
	data.Set("client_secret", o.clientSecret)
	data.Set("redirect_uri", o.redirectURI)
	data.Set("grant_type", "authorization_code")
	data.Set("code_verifier", o.codeChallenge)

	//nolint:gosec // Tainted url deemed acceptable. Not used to store any
	// backend data.
	resp, err := http.PostForm(tokenEndpoint, data)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()

	var tok token
	if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {
		return nil, errors.WithStack(err)
	}

	return &tok, nil
}

func (o *oauth) success(w http.ResponseWriter) {
	w.WriteHeader(http.StatusOK)
	w.Header().Add("Content-Type", "text/plain; charset=utf-8")
	w.Write([]byte(`<html><head><title>OAuth Request Successful</title>`))
	w.Write([]byte(`</head><body><p style='font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 22px; color: #333; width: 400px; margin: 0 auto; text-align: center; line-height: 1.7; padding: 20px;'>`))
	w.Write([]byte(`<strong style='font-size: 28px; color: #000;'>Success</strong><br />Look for the token on the command line`))
	w.Write([]byte(`</p></body></html>`))
}

func (o *oauth) badRequest(w http.ResponseWriter, msg string) {
	w.WriteHeader(http.StatusBadRequest)
	w.Header().Add("Content-Type", "text/plain; charset=utf-8")
	w.Write([]byte(`<html><head><title>OAuth Request Unsuccessful</title>`))
	w.Write([]byte(`</head><body><p style='font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 22px; color: #333; width: 400px; margin: 0 auto; text-align: center; line-height: 1.7; padding: 20px;'>`))
	w.Write([]byte(`<strong style='font-size: 28px; color: red;'>Failure</strong><br />`))
	w.Write([]byte(msg))
	w.Write([]byte(`</p></body></html>`))
	o.errCh <- errors.New(msg)
}
