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)
|
||||
- [#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)
|
||||
- [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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@ title: Google (default)
|
|||
|
||||
## Config Options
|
||||
|
||||
| Flag | Toml Field | Type | Description | Default |
|
||||
| ---------------------------------------------- | -------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
||||
| `--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-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-target-principal` | `google_target_principal` | bool | the target principal to impersonate when using ADC | defaults to the service account configured for ADC |
|
||||
| Flag | Toml Field | Type | Description | Default |
|
||||
|-------------------------------------------------|----------------------------------------------| ------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
|
||||
| `--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-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-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
|
||||
|
||||
|
|
@ -73,3 +75,10 @@ can be leveraged through a feature called Workload Identity. Follow Google's [gu
|
|||
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.
|
||||
|
||||
##### 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"`
|
||||
GoogleUseApplicationDefaultCredentials bool `flag:"google-use-application-default-credentials" cfg:"google_use_application_default_credentials"`
|
||||
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
|
||||
// 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-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-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
|
||||
}
|
||||
|
|
@ -770,6 +774,8 @@ func (l *LegacyProvider) convert() (Providers, error) {
|
|||
ServiceAccountJSON: l.GoogleServiceAccountJSON,
|
||||
UseApplicationDefaultCredentials: l.GoogleUseApplicationDefaultCredentials,
|
||||
TargetPrincipal: l.GoogleTargetPrincipal,
|
||||
UseOrganizationId: l.GoogleUseOrganizationId,
|
||||
AdminApiUserScope: l.GoogleAdminApiUserScope,
|
||||
}
|
||||
case "entra-id":
|
||||
provider.MicrosoftEntraIDConfig = MicrosoftEntraIDOptions{
|
||||
|
|
|
|||
|
|
@ -230,6 +230,10 @@ type GoogleOptions struct {
|
|||
UseApplicationDefaultCredentials bool `json:"useApplicationDefaultCredentials,omitempty"`
|
||||
// TargetPrincipal is the Google Service Account used for Application Default Credentials
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ type GoogleProvider struct {
|
|||
// Refresh. `Authorize` uses the results of this saved in `session.Groups`
|
||||
// Since it is called on every request.
|
||||
groupValidator func(*sessions.SessionState) bool
|
||||
|
||||
setPreferredUsername func(s *sessions.SessionState) error
|
||||
}
|
||||
|
||||
var _ Provider = (*GoogleProvider)(nil)
|
||||
|
|
@ -100,17 +102,59 @@ func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProv
|
|||
groupValidator: func(*sessions.SessionState) bool {
|
||||
return true
|
||||
},
|
||||
|
||||
setPreferredUsername: func(state *sessions.SessionState) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
||||
provider.configureGroups(opts)
|
||||
}
|
||||
if opts.UseOrganizationId || opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (p *GoogleProvider) configureGroups(opts options.GoogleOptions) {
|
||||
adminService := getAdminService(opts)
|
||||
// by default can be readonly user scope
|
||||
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
|
||||
if len(opts.Groups) > 0 {
|
||||
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
|
||||
// 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 {
|
||||
// TODO (@NickMeves) - Move to pure EnrichSession logic and stop
|
||||
// reusing legacy `groupValidator`.
|
||||
|
|
@ -212,7 +257,7 @@ func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionSta
|
|||
// populating logic.
|
||||
p.groupValidator(s)
|
||||
|
||||
return nil
|
||||
return p.setPreferredUsername(s)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
|
||||
TargetPrincipal: getTargetPrincipal(ctx, opts),
|
||||
Scopes: []string{scope},
|
||||
Scopes: strings.Split(scope, " "),
|
||||
Subject: opts.AdminEmail,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -364,6 +409,30 @@ func getTargetPrincipal(ctx context.Context, opts options.GoogleOptions) (target
|
|||
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
|
||||
func getUserGroups(service *admin.Service, email string) ([]string, error) {
|
||||
var allGroups []string
|
||||
|
|
|
|||
|
|
@ -325,3 +325,83 @@ func TestGoogleProvider_getUserGroups(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
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