diff --git a/.gitignore b/.gitignore index e40acd52..a7ee4162 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ c.out # Folders _obj _test +.DS_Store .idea/ .vscode/* !/.vscode/tasks.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 35685b7a..2aa456e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - [#2615](https://github.com/oauth2-proxy/oauth2-proxy/pull/2615) feat(cookies): add option to set a limit on the number of per-request CSRF cookies oauth2-proxy sets (@bh-tt) - [#2605](https://github.com/oauth2-proxy/oauth2-proxy/pull/2605) fix: show login page on broken cookie (@Primexz) +- [#2743](https://github.com/oauth2-proxy/oauth2-proxy/pull/2743) feat: allow use more possible google admin-sdk api scopes (@BobDu) # V7.10.0 diff --git a/docs/docs/configuration/providers/google.md b/docs/docs/configuration/providers/google.md index e3e819ad..26af87ab 100644 --- a/docs/docs/configuration/providers/google.md +++ b/docs/docs/configuration/providers/google.md @@ -37,18 +37,17 @@ account is still authorized. #### Restrict auth to specific Google groups on your domain. (optional) -1. Create a [service account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) and configure it +1. Create a [service account](https://developers.google.com/identity/protocols/oauth2/service-account) and configure it to use [Application Default Credentials / Workload Identity / Workload Identity Federation (recommended)](#using-application-default-credentials-adc--workload-identity--workload-identity-federation-recommended) or, alternatively download the JSON. 2. Make note of the Client ID for a future step. 3. Under "APIs & Auth", choose APIs. 4. Click on Admin SDK and then Enable API. -5. Follow the steps on https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account +5. Follow the steps on [Set up domain-wide delegation for a service account](https://developers.google.com/workspace/guides/create-credentials#optional_set_up_domain-wide_delegation_for_a_service_account) and give the client id from step 2 the following oauth scopes: ``` - https://www.googleapis.com/auth/admin.directory.group.readonly - https://www.googleapis.com/auth/admin.directory.user.readonly + https://www.googleapis.com/auth/admin.directory.group.member.readonly ``` 6. Follow the steps on https://support.google.com/a/answer/60757 to enable Admin API access. diff --git a/providers/google.go b/providers/google.go index 8b17d090..0e1e2156 100644 --- a/providers/google.go +++ b/providers/google.go @@ -229,38 +229,79 @@ func (p *GoogleProvider) setGroupRestriction(opts options.GoogleOptions) { } } -func getAdminService(opts options.GoogleOptions) *admin.Service { - ctx := context.Background() - var client *http.Client +// https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/hasMember#authorization-scopes +var possibleScopesList = [...]string{ + admin.AdminDirectoryGroupMemberReadonlyScope, + admin.AdminDirectoryGroupReadonlyScope, + admin.AdminDirectoryGroupMemberScope, + admin.AdminDirectoryGroupScope, +} + +func getOauth2TokenSource(ctx context.Context, opts options.GoogleOptions, scope string) oauth2.TokenSource { if opts.UseApplicationDefaultCredentials { ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ TargetPrincipal: getTargetPrincipal(ctx, opts), - Scopes: []string{admin.AdminDirectoryGroupReadonlyScope, admin.AdminDirectoryUserReadonlyScope}, + Scopes: []string{scope}, Subject: opts.AdminEmail, }) if err != nil { logger.Fatal("failed to fetch application default credentials: ", err) } - client = oauth2.NewClient(ctx, ts) - } else { - credentialsReader, err := os.Open(opts.ServiceAccountJSON) - if err != nil { - logger.Fatal("couldn't open Google credentials file: ", err) - return nil - } - - data, err := io.ReadAll(credentialsReader) - if err != nil { - logger.Fatal("can't read Google credentials file:", err) - } - - conf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope) - if err != nil { - logger.Fatal("can't load Google credentials file:", err) - } - conf.Subject = opts.AdminEmail - client = conf.Client(ctx) + return ts } + + credentialsReader, err := os.Open(opts.ServiceAccountJSON) + if err != nil { + logger.Fatal("couldn't open Google credentials file: ", err) + } + + data, err := io.ReadAll(credentialsReader) + if err != nil { + logger.Fatal("can't read Google credentials file:", err) + } + + conf, err := google.JWTConfigFromJSON(data, scope) + if err != nil { + logger.Fatal("can't load Google credentials file:", err) + } + + conf.Subject = opts.AdminEmail + return conf.TokenSource(ctx) +} + +func getAdminService(opts options.GoogleOptions) *admin.Service { + ctx := context.Background() + var client *http.Client + + for _, scope := range possibleScopesList { + + ts := getOauth2TokenSource(ctx, opts, scope) + _, err := ts.Token() + + if err == nil { + client = oauth2.NewClient(ctx, ts) + break + } + + if retrieveErr, ok := err.(*oauth2.RetrieveError); ok { + retrieveErrBody := map[string]interface{}{} + + if err := json.Unmarshal(retrieveErr.Body, &retrieveErrBody); err != nil { + logger.Fatal("error unmarshalling retrieveErr body:", err) + } + + if retrieveErrBody["error"] == "unauthorized_client" && retrieveErrBody["error_description"] == "Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested." { + continue + } + + logger.Fatal("error retrieving token:", err) + } + } + + if client == nil { + logger.Fatal("error: google credentials do not have enough permissions to access admin API scope") + } + adminService, err := admin.NewService(ctx, option.WithHTTPClient(client)) if err != nil { logger.Fatal(err)