Merge 4b6d7a2428 into 110d51d1d7
This commit is contained in:
commit
bc089d1691
|
|
@ -11,6 +11,7 @@
|
||||||
- [#3228](https://github.com/oauth2-proxy/oauth2-proxy/pull/3228) fix: use GetSecret() in ticket.go makeCookie to respect cookie-secret-file (@stagswtf)
|
- [#3228](https://github.com/oauth2-proxy/oauth2-proxy/pull/3228) fix: use GetSecret() in ticket.go makeCookie to respect cookie-secret-file (@stagswtf)
|
||||||
- [#3244](https://github.com/oauth2-proxy/oauth2-proxy/pull/3244) chore(deps): upgrade to latest go1.25.3 (@tuunit)
|
- [#3244](https://github.com/oauth2-proxy/oauth2-proxy/pull/3244) chore(deps): upgrade to latest go1.25.3 (@tuunit)
|
||||||
- [#3238](https://github.com/oauth2-proxy/oauth2-proxy/pull/3238) chore: Replace pkg/clock with narrowly targeted stub clocks (@dsymonds)
|
- [#3238](https://github.com/oauth2-proxy/oauth2-proxy/pull/3238) chore: Replace pkg/clock with narrowly targeted stub clocks (@dsymonds)
|
||||||
|
- [3237](https://github.com/oauth2-proxy/oauth2-proxy/pull/3237) - Option to use organization id for preferred username in Google Provider (@pixeldrew)
|
||||||
|
|
||||||
# V7.12.0
|
# V7.12.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,8 @@ Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||||
| `serviceAccountJson` | _string_ | ServiceAccountJSON is the path to the service account json credentials |
|
| `serviceAccountJson` | _string_ | ServiceAccountJSON is the path to the service account json credentials |
|
||||||
| `useApplicationDefaultCredentials` | _bool_ | UseApplicationDefaultCredentials is a boolean whether to use Application Default Credentials instead of a ServiceAccountJSON |
|
| `useApplicationDefaultCredentials` | _bool_ | UseApplicationDefaultCredentials is a boolean whether to use Application Default Credentials instead of a ServiceAccountJSON |
|
||||||
| `targetPrincipal` | _string_ | TargetPrincipal is the Google Service Account used for Application Default Credentials |
|
| `targetPrincipal` | _string_ | TargetPrincipal is the Google Service Account used for Application Default Credentials |
|
||||||
|
| `useOrganizationId` | _bool_ | UseOrganizationId indicates whether to use the organization ID as the UserName claim |
|
||||||
|
| `adminApiUserScope` | _string_ | user scope needed for fetching user organization information from admin api, can be one of cloud, user or readonly |
|
||||||
|
|
||||||
### Header
|
### Header
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ title: Google (default)
|
||||||
|
|
||||||
## Config Options
|
## Config Options
|
||||||
|
|
||||||
| Flag | Toml Field | Type | Description | Default |
|
| Flag | Toml Field | Type | Description | Default |
|
||||||
| ---------------------------------------------- | -------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
|-------------------------------------------------|----------------------------------------------| ------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
|
||||||
| `--google-admin-email` | `google_admin_email` | string | the google admin to impersonate for api calls | |
|
| `--google-admin-email` | `google_admin_email` | string | the google admin to impersonate for api calls | |
|
||||||
| `--google-group` | `google_groups` | string | restrict logins to members of this google group (may be given multiple times). If not specified and service account or default credentials are configured, all user groups will be allowed. | |
|
| `--google-group` | `google_groups` | string | restrict logins to members of this google group (may be given multiple times). If not specified and service account or default credentials are configured, all user groups will be allowed. | |
|
||||||
| `--google-service-account-json` | `google_service_account_json` | string | the path to the service account json credentials | |
|
| `--google-service-account-json` | `google_service_account_json` | string | the path to the service account json credentials | |
|
||||||
| `--google-use-application-default-credentials` | `google_use_application_default_credentials` | bool | use application default credentials instead of service account json (i.e. GKE Workload Identity) | |
|
| `--google-use-application-default-credentials` | `google_use_application_default_credentials` | bool | use application default credentials instead of service account json (i.e. GKE Workload Identity) | |
|
||||||
| `--google-target-principal` | `google_target_principal` | bool | the target principal to impersonate when using ADC | defaults to the service account configured for ADC |
|
| `--google-target-principal` | `google_target_principal` | bool | the target principal to impersonate when using ADC | defaults to the service account configured for ADC |
|
||||||
|
| `--google-use-organization-id` | `google_use_organization_id` | bool | use organization id as preferred username | false |
|
||||||
|
| `--google-admin-api-user-scope` | `google_admin_api_user_scope` | string | the OAuth scope to use when querying the Google Admin SDK for organization id, can be 'readonly', 'user' or 'cloud'<br/> | `readonly` |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
@ -73,3 +75,10 @@ can be leveraged through a feature called Workload Identity. Follow Google's [gu
|
||||||
to set up Workload Identity.
|
to set up Workload Identity.
|
||||||
|
|
||||||
When deployed outside of GCP, [Workload Identity Federation](https://cloud.google.com/docs/authentication/provide-credentials-adc#wlif) might be an option.
|
When deployed outside of GCP, [Workload Identity Federation](https://cloud.google.com/docs/authentication/provide-credentials-adc#wlif) might be an option.
|
||||||
|
|
||||||
|
##### Using Organization ID as Preferred Username (optional)
|
||||||
|
By default, the google provider uses the google id as username. If you would like to use an organization id instead, you can set the `google-use-organization-id` flag to true.
|
||||||
|
This requires that the service account used to query the Google Admin SDK has one of the following scopes granted in step 5 above:
|
||||||
|
- `https://www.googleapis.com/auth/admin.directory.user.readonly`,
|
||||||
|
- `https://www.googleapis.com/auth/admin.directory.user`
|
||||||
|
- `https://www.googleapis.com/auth/cloud-platform`
|
||||||
|
|
|
||||||
|
|
@ -510,6 +510,8 @@ type LegacyProvider struct {
|
||||||
GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
|
GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
|
||||||
GoogleUseApplicationDefaultCredentials bool `flag:"google-use-application-default-credentials" cfg:"google_use_application_default_credentials"`
|
GoogleUseApplicationDefaultCredentials bool `flag:"google-use-application-default-credentials" cfg:"google_use_application_default_credentials"`
|
||||||
GoogleTargetPrincipal string `flag:"google-target-principal" cfg:"google_target_principal"`
|
GoogleTargetPrincipal string `flag:"google-target-principal" cfg:"google_target_principal"`
|
||||||
|
GoogleUseOrganizationId bool `flag:"google-use-organization-id" cfg:"google_use_organization_id"`
|
||||||
|
GoogleAdminApiUserScope string `flag:"google-admin-api-user-scope" cfg:"google_admin_api_user_scope"`
|
||||||
|
|
||||||
// These options allow for other providers besides Google, with
|
// These options allow for other providers besides Google, with
|
||||||
// potential overrides.
|
// potential overrides.
|
||||||
|
|
@ -623,6 +625,8 @@ func legacyGoogleFlagSet() *pflag.FlagSet {
|
||||||
flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
|
flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
|
||||||
flagSet.String("google-use-application-default-credentials", "", "use application default credentials instead of service account json (i.e. GKE Workload Identity)")
|
flagSet.String("google-use-application-default-credentials", "", "use application default credentials instead of service account json (i.e. GKE Workload Identity)")
|
||||||
flagSet.String("google-target-principal", "", "the target principal to impersonate when using ADC")
|
flagSet.String("google-target-principal", "", "the target principal to impersonate when using ADC")
|
||||||
|
flagSet.String("google-use-organization-id", "", "use organization id as preferred username")
|
||||||
|
flagSet.String("google-admin-api-user-scope", "", "authorization scope required to call users.get, can be one of ")
|
||||||
|
|
||||||
return flagSet
|
return flagSet
|
||||||
}
|
}
|
||||||
|
|
@ -770,6 +774,8 @@ func (l *LegacyProvider) convert() (Providers, error) {
|
||||||
ServiceAccountJSON: l.GoogleServiceAccountJSON,
|
ServiceAccountJSON: l.GoogleServiceAccountJSON,
|
||||||
UseApplicationDefaultCredentials: l.GoogleUseApplicationDefaultCredentials,
|
UseApplicationDefaultCredentials: l.GoogleUseApplicationDefaultCredentials,
|
||||||
TargetPrincipal: l.GoogleTargetPrincipal,
|
TargetPrincipal: l.GoogleTargetPrincipal,
|
||||||
|
UseOrganizationId: l.GoogleUseOrganizationId,
|
||||||
|
AdminApiUserScope: l.GoogleAdminApiUserScope,
|
||||||
}
|
}
|
||||||
case "entra-id":
|
case "entra-id":
|
||||||
provider.MicrosoftEntraIDConfig = MicrosoftEntraIDOptions{
|
provider.MicrosoftEntraIDConfig = MicrosoftEntraIDOptions{
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,10 @@ type GoogleOptions struct {
|
||||||
UseApplicationDefaultCredentials bool `json:"useApplicationDefaultCredentials,omitempty"`
|
UseApplicationDefaultCredentials bool `json:"useApplicationDefaultCredentials,omitempty"`
|
||||||
// TargetPrincipal is the Google Service Account used for Application Default Credentials
|
// TargetPrincipal is the Google Service Account used for Application Default Credentials
|
||||||
TargetPrincipal string `json:"targetPrincipal,omitempty"`
|
TargetPrincipal string `json:"targetPrincipal,omitempty"`
|
||||||
|
// UseOrganizationId indicates whether to use the organization ID as the UserName claim
|
||||||
|
UseOrganizationId bool `json:"useOrganizationId,omitempty"`
|
||||||
|
// admin scope needed for fetching user organization information from admin api, can be one of cloud, user or defaults to readonly
|
||||||
|
AdminApiUserScope string `json:"adminApiUserScope,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDCOptions struct {
|
type OIDCOptions struct {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ type GoogleProvider struct {
|
||||||
// Refresh. `Authorize` uses the results of this saved in `session.Groups`
|
// Refresh. `Authorize` uses the results of this saved in `session.Groups`
|
||||||
// Since it is called on every request.
|
// Since it is called on every request.
|
||||||
groupValidator func(*sessions.SessionState) bool
|
groupValidator func(*sessions.SessionState) bool
|
||||||
|
|
||||||
|
setPreferredUsername func(s *sessions.SessionState) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Provider = (*GoogleProvider)(nil)
|
var _ Provider = (*GoogleProvider)(nil)
|
||||||
|
|
@ -100,17 +102,59 @@ func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProv
|
||||||
groupValidator: func(*sessions.SessionState) bool {
|
groupValidator: func(*sessions.SessionState) bool {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setPreferredUsername: func(state *sessions.SessionState) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
if opts.UseOrganizationId || opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
||||||
provider.configureGroups(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// reuse admin service to avoid multiple calls for token
|
||||||
|
var adminService *admin.Service
|
||||||
|
|
||||||
|
if opts.UseOrganizationId {
|
||||||
|
// add user scopes to admin api
|
||||||
|
userScope := getAdminApiUserScope(opts.AdminApiUserScope)
|
||||||
|
for index, scope := range possibleScopesList {
|
||||||
|
possibleScopesList[index] = scope + " " + userScope
|
||||||
|
}
|
||||||
|
|
||||||
|
adminService = getAdminService(opts)
|
||||||
|
|
||||||
|
provider.setPreferredUsername = func(s *sessions.SessionState) error {
|
||||||
|
userName, err := getUserInfo(adminService, s.Email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.PreferredUsername = userName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
||||||
|
if adminService == nil {
|
||||||
|
adminService = getAdminService(opts)
|
||||||
|
}
|
||||||
|
provider.configureGroups(opts, adminService)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GoogleProvider) configureGroups(opts options.GoogleOptions) {
|
// by default can be readonly user scope
|
||||||
adminService := getAdminService(opts)
|
func getAdminApiUserScope(adminApiUserScope string) string {
|
||||||
|
switch adminApiUserScope {
|
||||||
|
case "cloud":
|
||||||
|
return admin.CloudPlatformScope
|
||||||
|
case "user":
|
||||||
|
return admin.AdminDirectoryUserScope
|
||||||
|
}
|
||||||
|
return admin.AdminDirectoryUserReadonlyScope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GoogleProvider) configureGroups(opts options.GoogleOptions, adminService *admin.Service) {
|
||||||
// Backwards compatibility with `--google-group` option
|
// Backwards compatibility with `--google-group` option
|
||||||
if len(opts.Groups) > 0 {
|
if len(opts.Groups) > 0 {
|
||||||
p.setAllowedGroups(opts.Groups)
|
p.setAllowedGroups(opts.Groups)
|
||||||
|
|
@ -204,6 +248,7 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code, codeVeri
|
||||||
|
|
||||||
// EnrichSession checks the listed Google Groups configured and adds any
|
// EnrichSession checks the listed Google Groups configured and adds any
|
||||||
// that the user is a member of to session.Groups.
|
// that the user is a member of to session.Groups.
|
||||||
|
// if preferred username is configured to be organization ID, it sets that as well.
|
||||||
func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionState) error {
|
func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionState) error {
|
||||||
// TODO (@NickMeves) - Move to pure EnrichSession logic and stop
|
// TODO (@NickMeves) - Move to pure EnrichSession logic and stop
|
||||||
// reusing legacy `groupValidator`.
|
// reusing legacy `groupValidator`.
|
||||||
|
|
@ -212,7 +257,7 @@ func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionSta
|
||||||
// populating logic.
|
// populating logic.
|
||||||
p.groupValidator(s)
|
p.groupValidator(s)
|
||||||
|
|
||||||
return nil
|
return p.setPreferredUsername(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetGroupRestriction configures the GoogleProvider to restrict access to the
|
// SetGroupRestriction configures the GoogleProvider to restrict access to the
|
||||||
|
|
@ -262,7 +307,7 @@ func getOauth2TokenSource(ctx context.Context, opts options.GoogleOptions, scope
|
||||||
if opts.UseApplicationDefaultCredentials {
|
if opts.UseApplicationDefaultCredentials {
|
||||||
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
|
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
|
||||||
TargetPrincipal: getTargetPrincipal(ctx, opts),
|
TargetPrincipal: getTargetPrincipal(ctx, opts),
|
||||||
Scopes: []string{scope},
|
Scopes: strings.Split(scope, " "),
|
||||||
Subject: opts.AdminEmail,
|
Subject: opts.AdminEmail,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -364,6 +409,30 @@ func getTargetPrincipal(ctx context.Context, opts options.GoogleOptions) (target
|
||||||
return targetPrincipal
|
return targetPrincipal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUserInfo(service *admin.Service, email string) (string, error) {
|
||||||
|
req := service.Users.Get(email)
|
||||||
|
user, err := req.Do()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user details for %s: %v", email, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext, _ := user.ExternalIds.([]interface{})
|
||||||
|
for _, v := range ext {
|
||||||
|
m, _ := v.(map[string]interface{})
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t, _ := m["type"].(string); t != "organization" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if val, _ := m["value"].(string); val != "" {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("failed to get organization id for %s", email)
|
||||||
|
}
|
||||||
|
|
||||||
// getUserGroups retrieves all groups that a user is a member of using the Google Admin Directory API
|
// getUserGroups retrieves all groups that a user is a member of using the Google Admin Directory API
|
||||||
func getUserGroups(service *admin.Service, email string) ([]string, error) {
|
func getUserGroups(service *admin.Service, email string) ([]string, error) {
|
||||||
var allGroups []string
|
var allGroups []string
|
||||||
|
|
|
||||||
|
|
@ -325,3 +325,83 @@ func TestGoogleProvider_getUserGroups(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []string{"group1@example.com", "group2@example.com"}, groups)
|
assert.Equal(t, []string{"group1@example.com", "group2@example.com"}, groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoogleProvider_getUserInfo(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/admin/directory/v1/users/test@example.com" {
|
||||||
|
response := `{
|
||||||
|
"kind": "admin#directory#user",
|
||||||
|
"id": "",
|
||||||
|
"etag": "\"\"",
|
||||||
|
"primaryEmail": "test@example.com",
|
||||||
|
"name": {
|
||||||
|
"givenName": "Test",
|
||||||
|
"familyName": "User",
|
||||||
|
"fullName": "Test User"
|
||||||
|
},
|
||||||
|
"isAdmin": false,
|
||||||
|
"isDelegatedAdmin": false,
|
||||||
|
"lastLoginTime": "",
|
||||||
|
"creationTime": "",
|
||||||
|
"agreedToTerms": true,
|
||||||
|
"suspended": false,
|
||||||
|
"archived": false,
|
||||||
|
"changePasswordAtNextLogin": false,
|
||||||
|
"ipWhitelisted": false,
|
||||||
|
"emails": [
|
||||||
|
{
|
||||||
|
"address": "test@example.com",
|
||||||
|
"primary": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"externalIds": [
|
||||||
|
{
|
||||||
|
"value": "test.user",
|
||||||
|
"type": "organization"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"organizations": [
|
||||||
|
],
|
||||||
|
"phones": [
|
||||||
|
],
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"languageCode": "en",
|
||||||
|
"preference": "preferred"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliases": [
|
||||||
|
"test.user@example.com"
|
||||||
|
],
|
||||||
|
"nonEditableAliases": [
|
||||||
|
"test.user@example.com"
|
||||||
|
],
|
||||||
|
"gender": {
|
||||||
|
"type": "male"
|
||||||
|
},
|
||||||
|
"customerId": "",
|
||||||
|
"orgUnitPath": "/",
|
||||||
|
"isMailboxSetup": true,
|
||||||
|
"isEnrolledIn2Sv": true,
|
||||||
|
"isEnforcedIn2Sv": false,
|
||||||
|
"includeInGlobalAddressList": true,
|
||||||
|
"thumbnailPhotoUrl": "",
|
||||||
|
"thumbnailPhotoEtag": "\"\"",
|
||||||
|
"recoveryEmail": "test.user@gmail.com",
|
||||||
|
"recoveryPhone": "+55555555555"
|
||||||
|
}`
|
||||||
|
fmt.Fprintln(w, response)
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
adminService, err := admin.NewService(context.Background(), option.WithHTTPClient(client), option.WithEndpoint(ts.URL))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
info, err := getUserInfo(adminService, "test@example.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "test.user", info)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue