Skip to content

Add support for 3LO loopback flow #132

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

Merged
merged 30 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
77b4169
initial commit
ulisesL Jun 21, 2022
5c06a86
Added DisableAutoOpenConsentPage option. Removed ConsentPageAllowRedi…
ulisesL Jun 22, 2022
af6fde4
Modify getTimeDuration to handle incorrect units.
ulisesL Jun 22, 2022
5a36623
Improve comments for OverriddenURI.
ulisesL Jun 22, 2022
6f719ce
Introduce maxWaitForListenAndServe constant.
ulisesL Jun 22, 2022
4d833c0
Fix comment for consent page parameters.
ulisesL Jun 22, 2022
d9850f6
Remove Test3LOFlow fetch; 3lo; old interface --scope flag
ulisesL Jun 22, 2022
436cd0c
Improve '--credentials' description.
ulisesL Jun 22, 2022
5324b06
Move consent page options.
ulisesL Jun 22, 2022
e2035a8
Move consent page parameters.
ulisesL Jun 22, 2022
7636cb6
Rename localhost.go to loopback.go and add description.
ulisesL Jun 25, 2022
6b3169e
Add TODO to remove Test3LOFlow and rename tests in test3LOLoopbackFlow.
ulisesL Jun 25, 2022
7748c3e
Add --scope option in 'old interface' test in Test3LOLoopbackFlow.
ulisesL Jun 25, 2022
d3b8729
Fix usage of --disableAutoOpenConsentPage and documentation.
ulisesL Jun 25, 2022
ca8ff9f
Create clientIDFile util file.
ulisesL Jun 26, 2022
b7e21a0
Remove empty lines in cli_test.go and browser.go
ulisesL Jun 26, 2022
ba2e96e
Remove OverriddenURI logic.
ulisesL Jun 26, 2022
5359ff5
Fix regex expression in createKey in cache.go.
ulisesL Jun 26, 2022
affb2a1
Fix redirect_uris replacement in createKey in cache.go
ulisesL Jun 26, 2022
e1c9371
Move authorization handlers out of main. Remove extra empty lines.
ulisesL Jun 27, 2022
c0720d7
Rename clientIdFile.go.
ulisesL Jun 27, 2022
b9c9033
Fix typos.
ulisesL Jun 28, 2022
9359ce2
Improve comments in auth-handlers.go.
ulisesL Jun 28, 2022
2f44228
Reduce branching when handling 3LO loopback in main.
ulisesL Jun 28, 2022
90adb9f
Move consentPageSettings logic inside 3LO loopback case.
ulisesL Jun 28, 2022
0d26dec
Change scope of defer function in main. It should be inside the 3LO l…
ulisesL Jun 28, 2022
c4ea4be
Remove empty lines in loopback.go
ulisesL Jun 28, 2022
fd3c1ba
Print something saying could not auto open URL if OpenURL errors.
ulisesL Jun 28, 2022
2b43ee5
Replace ReplaceContentAll with strings.Replace.
ulisesL Jun 28, 2022
8ff4908
Fix TestServiceAccountImpersonationFlow.
ulisesL Jun 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ $ export GOOGLE_APPLICATION_CREDENTIALS="~/service_account.json"
$ oauth2l fetch --scope cloud-platform
```

When using an OAuth client ID file, the following applies:

If the first `redirect_uris` in the `--credentials client_id.json` is set to `urn:ietf:wg:oauth:2.0:oob`,
the 3LO out of band flow is activated. NOTE: 3LO out of band flow has been deprecated and will stop working entirely in Oct 2022.

If the first `redirect_uris` in the `--credentials client_id.json` is set to `http://localhost[:PORT]`,
the 3LO loopback flow is activated. When the port is omitted, an available port will be used to spin up the localhost.
When a port is provided, oauth2l will attempt to use such port. If the port cannot be used, oauth2l will stop.

### --type

The authentication type. The currently supported types are "oauth", "jwt", or
Expand Down Expand Up @@ -390,6 +399,28 @@ Impersonation [here](https://cloud.google.com/iam/docs/impersonating-service-acc
$ oauth2l fetch --credentials ~/client_credentials.json --scope cloud-platform,pubsub --impersonate-service-account 113258942105700140798
```

### --disableAutoOpenConsentPage

Disables the feature to automatically open the consent page in 3LO loopback flows.
When this option is used, the user will be provided with a URL to manually interact with the consent page.
This flag does not take any arguments. Simply add the option to disable this feature.

```bash
$ oauth2l fetch --credentials ~/client_credentials.json --disableAutoOpenConsentPage --consentPageInteractionTimeout 60 --consentPageInteractionTimeoutUnits seconds --scope cloud-platform
```

### --consentPageInteractionTimeout

Amount of time to wait for a user to interact with the consent page in 3LO loopback flows.
Once the time has lapsed, the localhost at the `redirect_uri` will no longer be available.
Its default value is 2. See `--consentPageInteractionTimeoutUnits` to change the units.

### --consentPageInteractionTimeoutUnits

Units of measurement to use when `--consentPageInteractionTimeout` is set.
Its default value is `minutes`. Valid inputs are `seconds` and `minutes`.
This option only affects 3LO loopback flows.

### fetch --output_format

Token's output format for "fetch" command. One of bare, header, json, json_compact, pretty, or refresh_token. Default is bare.
Expand Down
119 changes: 118 additions & 1 deletion integration/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"os/exec"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"testing"
Expand Down Expand Up @@ -217,6 +218,8 @@ func TestCLI(t *testing.T) {
runTestScenarios(t, tests)
}

// TODO: Remove this flow when the 3LO flow is deprecated. A replicated set of test is now part of Test3LOLoopbackFlow.
// tests in Test3LOLoopbackFlow have been updated to account for new outputs.
// Test OAuth 3LO flow with fake client secrets. Fake verification code is injected to stdin to advance the flow.
func Test3LOFlow(t *testing.T) {
tests := []testCase{
Expand Down Expand Up @@ -290,6 +293,100 @@ func Test3LOFlow(t *testing.T) {
runTestScenariosWithInput(t, tests, newFixture(t, "fake-verification-code.fixture").asFile())
}

// TODO: Enhance tests so that the entire loopback flow can be tested
// TODO: Once enhanced, uncomment and fix cache tests in this flow
// TODO: Remove Test3LOFlow once the 3LO flow is deprecated
// Test OAuth 3LO loopback flow with fake client secrets. Stops waiting for consent page interaction to advance the flow.
func Test3LOLoopbackFlow(t *testing.T) {
tests := []testCase{
{
"fetch; 3lo loopback",
[]string{"fetch", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--cache", "",
"--disableAutoOpenConsentPage",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds"},
"fetch-3lo-loopback.golden",
false,
},
{
"fetch; 3lo loopback; old interface",
[]string{"fetch", "--json", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--cache", "", "pubsub",
"--disableAutoOpenConsentPage",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds"},
"fetch-3lo-loopback.golden",
false,
},
{
"fetch; 3lo loopback; userinfo scopes",
[]string{"fetch", "--scope", "userinfo.profile,userinfo.email", "--credentials", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--cache", "",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds",
"--disableAutoOpenConsentPage"},
"fetch-3lo-loopback-userinfo.golden",
false,
},
{
"header; 3lo loopback",
[]string{"header", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--cache", "",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds",
"--disableAutoOpenConsentPage"},
"header-3lo-loopback.golden",
false,
},
{
"fetch; 3lo loopback; refresh token output format",
[]string{"fetch", "--output_format", "refresh_token", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--cache", "",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds",
"--disableAutoOpenConsentPage"},
"fetch-3lo-loopback-refresh-token.golden",
false,
},
{
"curl; 3lo loopback",
[]string{"curl", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--url", "http://localhost:8080/curl",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds",
"--disableAutoOpenConsentPage"},
"curl-3lo-loopback.golden",
false,
},
/*
{
"fetch; 3lo loopback cached",
[]string{"fetch", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-3lo-loopback.json", "--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds"},
"fetch-3lo-cached.golden",
false,
},
{
"fetch; 3lo loopback insert expired token into cache",
[]string{"fetch", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-expired-token-3lo-loopback.json",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds"},
"fetch-3lo.golden",
false,
},
{
"fetch; 3lo loopback cached; token expired",
[]string{"fetch", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-expired-token-3lo-loopback.json",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds"},
"fetch-3lo.golden",
false,
},
{
"fetch; 3lo loopback cached; refresh expired token",
[]string{"fetch", "--scope", "pubsub", "--credentials", "integration/fixtures/fake-client-secrets-expired-token-3lo-loopback.json", "--refresh",
"--consentPageInteractionTimeout", "1", "--consentPageInteractionTimeoutUnits", "seconds"},
"fetch-3lo-cached.golden",
false,
},*/
}

process3LOOutput := func(output string) string {
re := regexp.MustCompile("redirect_uri=http%3A%2F%2Flocalhost%3A\\d+")
match := re.FindString(output)
output = strings.Replace(output, match, "redirect_uri=http%3A%2F%2Flocalhost", 1)
return output
}

runTestScenariosWithInputAndProcessedOutput(t, tests, nil, process3LOOutput)
}

// Test OAuth 2LO Flow with fake service account.
func Test2LOFlow(t *testing.T) {
tests := []testCase{
Expand Down Expand Up @@ -408,6 +505,7 @@ func TestStsFlow(t *testing.T) {
// Test Service Account Impersonation Flow.
// This currently sends request to the real IAM endpoint, which will return 401 for having invalid user access token, which is expected.
func TestServiceAccountImpersonationFlow(t *testing.T) {

tests := []testCase{
{
"fetch; sso; impersonation",
Expand All @@ -416,7 +514,26 @@ func TestServiceAccountImpersonationFlow(t *testing.T) {
false,
},
}
runTestScenarios(t, tests)

processOutput := func(output string) string {

method := "\"method\": \"google.iam.credentials.v1.IAMCredentials.GenerateAccessToken\""
service := "\"service\": \"iamcredentials.googleapis.com\""

mPos := strings.Index(output, method)
sPos := strings.Index(output, service)

// If service appears later than method, revert order to match output
if sPos > mPos {
output = strings.Replace(output, method, "**MARKER-1**", 1)
output = strings.Replace(output, service, method, 1)
output = strings.Replace(output, "**MARKER-1**", service, 1)
}

return output
}

runTestScenariosWithInputAndProcessedOutput(t, tests, nil, processOutput)
}

func readFile(path string) string {
Expand Down
16 changes: 16 additions & 0 deletions integration/fixtures/fake-client-secrets-3lo-loopback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"installed": {
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"client_email": "",
"client_id": "144169.apps.googleusercontent.com",
"project_id":"awesomeproject",
"client_secret": "awesomesecret",
"client_x509_cert_url": "",
"redirect_uris": [
"http://localhost",
"urn:ietf:wg:oauth:2.0:oob"
],
"token_uri": "http://localhost:8080/token"
}
}
4 changes: 4 additions & 0 deletions integration/golden/curl-3lo-loopback.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Go to the following link in your browser:

https://accounts.google.com/o/oauth2/auth?client_id=144169.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpubsub&state=state
Authorization code not yet set.
4 changes: 4 additions & 0 deletions integration/golden/fetch-3lo-loopback-refresh-token.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Go to the following link in your browser:

https://accounts.google.com/o/oauth2/auth?client_id=144169.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpubsub&state=state
Authorization code not yet set.
4 changes: 4 additions & 0 deletions integration/golden/fetch-3lo-loopback-userinfo.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Go to the following link in your browser:

https://accounts.google.com/o/oauth2/auth?client_id=144169.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=state
Authorization code not yet set.
4 changes: 4 additions & 0 deletions integration/golden/fetch-3lo-loopback.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Go to the following link in your browser:

https://accounts.google.com/o/oauth2/auth?client_id=144169.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpubsub&state=state
Authorization code not yet set.
16 changes: 15 additions & 1 deletion integration/golden/fetch-impersonation.golden
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
"status": "UNAUTHENTICATED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.DebugInfo",
"detail": "Authentication error: 16; Error Details: Credential sent is invalid. Unknown token version 0 for token string: ya29.GltDB_y~"
},
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "ACCESS_TOKEN_TYPE_UNSUPPORTED",
"metadata": {
"service": "iamcredentials.googleapis.com",
"method": "google.iam.credentials.v1.IAMCredentials.GenerateAccessToken"
}
}
]
}
}

4 changes: 4 additions & 0 deletions integration/golden/header-3lo-loopback.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Go to the following link in your browser:

https://accounts.google.com/o/oauth2/auth?client_id=144169.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpubsub&state=state
Authorization code not yet set.
76 changes: 56 additions & 20 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ import (
"os"
"regexp"
"strings"
"time"

"github.com/google/oauth2l/util"
"github.com/jessevdk/go-flags"

"golang.org/x/oauth2/authhandler"
)

const (
Expand Down Expand Up @@ -84,6 +83,11 @@ type commonFetchOptions struct {
// Refresh is used for 3LO flow. When used in conjunction with caching, the user can avoid re-authorizing.
Refresh bool `long:"refresh" description:"If the cached access token is expired, attempt to refresh it using refreshToken."`

// Consent page parameters.
DisableAutoOpenConsentPage bool `long:"disableAutoOpenConsentPage" description:"Disables the ability to open the consent page automatically."`
ConsentPageInteractionTimeout int `long:"consentPageInteractionTimeout" description:"Maximum wait time for user to interact with consent page." default:"2"`
ConsentPageInteractionTimeoutUnits string `long:"consentPageInteractionTimeoutUnits" choice:"seconds" choice:"minutes" description:"Consent page timeout units." default:"minutes"`

// Deprecated flags kept for backwards compatibility. Hidden from help page.
Json string `long:"json" description:"Deprecated. Same as --credentials." hidden:"true"`
Jwt bool `long:"jwt" description:"Deprecated. Same as --type jwt." hidden:"true"`
Expand Down Expand Up @@ -138,22 +142,6 @@ func readJSON(file string) (string, error) {
return "", nil
}

// Default 3LO authorization handler. Prints the authorization URL on stdout
// and reads the authorization code from stdin.
//
// Note that the "state" parameter is used to prevent CSRF attacks.
// For convenience, CmdAuthorizationHandler returns a pre-configured state
// instead of requiring the user to copy it from the browser.
func cmdAuthorizationHandler(state string) authhandler.AuthorizationHandler {
return func(authCodeURL string) (string, string, error) {
fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL)
fmt.Println("Enter authorization code:")
var code string
fmt.Scanln(&code)
return code, state, nil
}
}

// Append Google OAuth scope prefix if not provided and joins
// the slice into a whitespace-separated string.
func parseScopes(scopes []string) string {
Expand Down Expand Up @@ -193,6 +181,18 @@ func getCommonFetchOptions(cmdOpts commandOptions, cmd string) commonFetchOption
return commonOpts
}

// Generates a time duration
func getTimeDuration(quantity int, units string) (time.Duration, error) {
switch units {
case "seconds":
return time.Duration(quantity) * time.Second, nil
case "minutes":
return time.Duration(quantity) * time.Minute, nil
default:
return time.Duration(0), fmt.Errorf("Invalid units: %s", units)
}
}

// Get the authentication type, with backward compatibility.
func getAuthTypeWithFallback(commonOpts commonFetchOptions) string {
authType := commonOpts.AuthType
Expand Down Expand Up @@ -371,12 +371,48 @@ func main() {
return
}

var authCodeServer util.AuthorizationCodeServer = nil
var consentPageSettings util.ConsentPageSettings
redirectUri, err := util.GetFirstRedirectURI(json)
// 3LO Loopback case
if err == nil && strings.Contains(redirectUri, "localhost") {
interactionTimeout, err := getTimeDuration(commonOpts.ConsentPageInteractionTimeout, commonOpts.ConsentPageInteractionTimeoutUnits)
if err != nil {
fmt.Println("Failed to create time.Duration: " + err.Error())
return
}
consentPageSettings = util.ConsentPageSettings{
DisableAutoOpenConsentPage: commonOpts.DisableAutoOpenConsentPage,
InteractionTimeout: interactionTimeout,
}
authCodeServer = &util.AuthorizationCodeLocalhost{
ConsentPageSettings: consentPageSettings,
AuthCodeReqStatus: util.AuthorizationCodeStatus{
Status: util.WAITING, Details: "Authorization code not yet set."},
}

// Start localhost server
adr, err := authCodeServer.ListenAndServe(redirectUri)
if err != nil {
fmt.Println(err)
return
}
// Close localhost server's port on exit
defer authCodeServer.Close()

// If a different dynamic redirect uri was created, replace the redirect uri in file.
// this happens if the original redirect does not have a port for the localhost.
redirectUri = fmt.Sprintf("\"%s\"", redirectUri)
adr = fmt.Sprintf("\"%s\"", adr)
json = strings.Replace(json, redirectUri, adr, -1)
}

// 3LO or 2LO depending on the credential type.
// For 2LO flow AuthHandler and State are not needed.
// For 2LO flow AuthHandler, State and ConsentPageSettings are not needed.
settings = &util.Settings{
CredentialsJSON: json,
Scope: parseScopes(scopes),
AuthHandler: cmdAuthorizationHandler(defaultState),
AuthHandler: util.Get3LOAuthorizationHandler(defaultState, consentPageSettings, &authCodeServer),
State: defaultState,
Audience: audience,
QuotaProject: quotaProject,
Expand Down
Loading