Skip to content
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

Adds gsuite provider specific extension for fetching groups and user information #123

Merged
merged 12 commits into from Jul 21, 2020

Conversation

austingebauer
Copy link
Member

Overview

This PR adds an OIDC provider specific extension for G Suite that allows for fetching of groups and user information during authentication.

Please see the rendered documentation preview and PR (hashicorp/vault#9454) for additional context and explanation of the configuration.

This work is a continuation of #87 and closes #83. Thanks to @adongy and @onetwopunch for the help getting this work started.

Example configuration:

vault write auth/oidc/config -<<EOF
{
    "oidc_discovery_url": "https://accounts.google.com",
    "oidc_client_id": "your_client_id",
    "oidc_client_secret": "your_client_secret",
    "default_role": "your_default_role",
    "provider_config": {
        "provider": "gsuite",
        "gsuite_service_account": "/path/to/service-account.json",
        "gsuite_admin_impersonate": "admin@gsuitedomain.com",
        "fetch_groups": true,
        "fetch_user_info": true,
        "groups_recurse_max_depth": 5,
        "user_custom_schemas": "Education,Preferences"
    }
}
EOF

Example role:

vault write auth/oidc/role/your_default_role \
    allowed_redirect_uris="http://localhost:8200/ui/vault/auth/oidc/oidc/callback,http://localhost:8250/oidc/callback" \
    user_claim="sub" \
    groups_claim="groups" \
    claim_mappings="/Education/graduation_date"="graduation_date" \
    claim_mappings="/Preferences/shirt_size"="shirt_size"

Design of Change

This change uses the provider specific extension handling introduced in #118.

Related Issues/Pull Requests

Contributor Checklist

Test output
$ make test        
VAULT_ACC=1 go test -tags='vault-plugin-auth-jwt' $(go list ./... | grep -v /vendor/) -v  -timeout 10m
=== RUN   TestGetClaim
--- PASS: TestGetClaim (0.00s)
=== RUN   TestExtractMetadata
--- PASS: TestExtractMetadata (0.00s)
=== RUN   TestValidateAudience
--- PASS: TestValidateAudience (0.00s)
=== RUN   TestValidateBoundClaims
--- PASS: TestValidateBoundClaims (0.00s)
=== RUN   Test_normalizeList
--- PASS: Test_normalizeList (0.00s)
=== RUN   TestParseHelp
=== RUN   TestParseHelp/#00
=== RUN   TestParseHelp/#01
=== RUN   TestParseHelp/#02
=== RUN   TestParseHelp/#03
=== RUN   TestParseHelp/#04
=== RUN   TestParseHelp/#05
--- PASS: TestParseHelp (0.00s)
    --- PASS: TestParseHelp/#00 (0.00s)
    --- PASS: TestParseHelp/#01 (0.00s)
    --- PASS: TestParseHelp/#02 (0.00s)
    --- PASS: TestParseHelp/#03 (0.00s)
    --- PASS: TestParseHelp/#04 (0.00s)
    --- PASS: TestParseHelp/#05 (0.00s)
=== RUN   TestConfig_JWT_Read
--- PASS: TestConfig_JWT_Read (0.00s)
=== RUN   TestConfig_JWT_Write
--- PASS: TestConfig_JWT_Write (0.00s)
=== RUN   TestConfig_JWKS_Update
--- PASS: TestConfig_JWKS_Update (0.00s)
=== RUN   TestConfig_JWKS_Update_Invalid
--- PASS: TestConfig_JWKS_Update_Invalid (0.00s)
=== RUN   TestConfig_ResponseMode
--- PASS: TestConfig_ResponseMode (0.00s)
=== RUN   TestConfig_OIDC_Write
--- PASS: TestConfig_OIDC_Write (0.34s)
=== RUN   TestConfig_OIDC_Write_ProviderConfig
=== RUN   TestConfig_OIDC_Write_ProviderConfig/valid_provider_config
=== RUN   TestConfig_OIDC_Write_ProviderConfig/unknown_provider_in_provider_config
=== RUN   TestConfig_OIDC_Write_ProviderConfig/provider_config_missing_provider
=== RUN   TestConfig_OIDC_Write_ProviderConfig/provider_config_not_set
--- PASS: TestConfig_OIDC_Write_ProviderConfig (0.13s)
    --- PASS: TestConfig_OIDC_Write_ProviderConfig/valid_provider_config (0.03s)
    --- PASS: TestConfig_OIDC_Write_ProviderConfig/unknown_provider_in_provider_config (0.04s)
    --- PASS: TestConfig_OIDC_Write_ProviderConfig/provider_config_missing_provider (0.04s)
    --- PASS: TestConfig_OIDC_Write_ProviderConfig/provider_config_not_set (0.02s)
=== RUN   TestLogin_JWT
--- PASS: TestLogin_JWT (0.03s)
=== RUN   TestLogin_Leeways
--- PASS: TestLogin_Leeways (0.18s)
=== RUN   TestLogin_OIDC
--- SKIP: TestLogin_OIDC (0.11s)
=== RUN   TestLogin_NestedGroups
--- PASS: TestLogin_NestedGroups (0.00s)
=== RUN   TestLogin_OIDC_StringGroupClaim
--- SKIP: TestLogin_OIDC_StringGroupClaim (0.03s)
=== RUN   TestLogin_JWKS_Concurrent
--- PASS: TestLogin_JWKS_Concurrent (0.28s)
=== RUN   TestOIDC_AuthURL
=== RUN   TestOIDC_AuthURL/normal_case
=== PAUSE TestOIDC_AuthURL/normal_case
=== RUN   TestOIDC_AuthURL/missing_role
=== PAUSE TestOIDC_AuthURL/missing_role
=== RUN   TestOIDC_AuthURL/valid_redirect_uri
=== PAUSE TestOIDC_AuthURL/valid_redirect_uri
=== RUN   TestOIDC_AuthURL/invalid_redirect_uri
=== PAUSE TestOIDC_AuthURL/invalid_redirect_uri
=== CONT  TestOIDC_AuthURL/normal_case
=== CONT  TestOIDC_AuthURL/valid_redirect_uri
=== CONT  TestOIDC_AuthURL/invalid_redirect_uri
=== CONT  TestOIDC_AuthURL/missing_role
2020-07-10T14:00:37.985-0700 [WARN]  unauthorized redirect_uri: redirect_uri=http://bitc0in-4-less.cx
--- PASS: TestOIDC_AuthURL (0.04s)
    --- PASS: TestOIDC_AuthURL/invalid_redirect_uri (0.00s)
    --- PASS: TestOIDC_AuthURL/valid_redirect_uri (0.03s)
    --- PASS: TestOIDC_AuthURL/missing_role (0.03s)
    --- PASS: TestOIDC_AuthURL/normal_case (0.03s)
=== RUN   TestOIDC_Callback
=== RUN   TestOIDC_Callback/successful_login
=== RUN   TestOIDC_Callback/failed_login_-_bad_nonce
=== RUN   TestOIDC_Callback/failed_login_-_bound_claim_mismatch
=== RUN   TestOIDC_Callback/missing_state
=== RUN   TestOIDC_Callback/unknown_state
=== RUN   TestOIDC_Callback/valid_state,_missing_code
=== RUN   TestOIDC_Callback/failed_code_exchange
=== RUN   TestOIDC_Callback/no_response_from_provider
=== RUN   TestOIDC_Callback/test_bad_address
=== RUN   TestOIDC_Callback/test_invalid_client_id
=== RUN   TestOIDC_Callback/client_nonce
--- PASS: TestOIDC_Callback (0.05s)
    --- PASS: TestOIDC_Callback/successful_login (0.01s)
    --- PASS: TestOIDC_Callback/failed_login_-_bad_nonce (0.00s)
    --- PASS: TestOIDC_Callback/failed_login_-_bound_claim_mismatch (0.01s)
    --- PASS: TestOIDC_Callback/missing_state (0.00s)
    --- PASS: TestOIDC_Callback/unknown_state (0.00s)
    --- PASS: TestOIDC_Callback/valid_state,_missing_code (0.00s)
    --- PASS: TestOIDC_Callback/failed_code_exchange (0.00s)
    --- PASS: TestOIDC_Callback/no_response_from_provider (0.00s)
    --- PASS: TestOIDC_Callback/test_bad_address (0.00s)
    --- PASS: TestOIDC_Callback/test_invalid_client_id (0.00s)
    --- PASS: TestOIDC_Callback/client_nonce (0.01s)
=== RUN   TestOIDC_ValidRedirect
--- PASS: TestOIDC_ValidRedirect (0.00s)
=== RUN   TestParseMount
--- PASS: TestParseMount (0.00s)
=== RUN   TestPath_Create
=== RUN   TestPath_Create/happy_path
=== RUN   TestPath_Create/no_user_claim
=== RUN   TestPath_Create/no_binding
=== RUN   TestPath_Create/has_bound_subject
=== RUN   TestPath_Create/has_audience
=== RUN   TestPath_Create/has_cidr
=== RUN   TestPath_Create/has_bound_claims
=== RUN   TestPath_Create/has_expiration,_not_before_custom_leeways
=== RUN   TestPath_Create/storing_zero_leeways
=== RUN   TestPath_Create/storing_negative_leeways
=== RUN   TestPath_Create/storing_an_invalid_bound_claim_type
=== RUN   TestPath_Create/role_with_invalid_glob_in_claim
=== RUN   TestPath_Create/role_with_invalid_glob_in_claim_array
--- PASS: TestPath_Create (0.00s)
    --- PASS: TestPath_Create/happy_path (0.00s)
    --- PASS: TestPath_Create/no_user_claim (0.00s)
    --- PASS: TestPath_Create/no_binding (0.00s)
    --- PASS: TestPath_Create/has_bound_subject (0.00s)
    --- PASS: TestPath_Create/has_audience (0.00s)
    --- PASS: TestPath_Create/has_cidr (0.00s)
    --- PASS: TestPath_Create/has_bound_claims (0.00s)
    --- PASS: TestPath_Create/has_expiration,_not_before_custom_leeways (0.00s)
    --- PASS: TestPath_Create/storing_zero_leeways (0.00s)
    --- PASS: TestPath_Create/storing_negative_leeways (0.00s)
    --- PASS: TestPath_Create/storing_an_invalid_bound_claim_type (0.00s)
    --- PASS: TestPath_Create/role_with_invalid_glob_in_claim (0.00s)
    --- PASS: TestPath_Create/role_with_invalid_glob_in_claim_array (0.00s)
=== RUN   TestPath_OIDCCreate
=== RUN   TestPath_OIDCCreate/both_explicit_and_default_role_type
=== RUN   TestPath_OIDCCreate/invalid_reserved_metadata_key_role
=== RUN   TestPath_OIDCCreate/invalid_duplicate_metadata_destination
=== RUN   TestPath_OIDCCreate/custom_expiration_leeway_and_not_before_leeway_values
--- PASS: TestPath_OIDCCreate (0.00s)
    --- PASS: TestPath_OIDCCreate/both_explicit_and_default_role_type (0.00s)
    --- PASS: TestPath_OIDCCreate/invalid_reserved_metadata_key_role (0.00s)
    --- PASS: TestPath_OIDCCreate/invalid_duplicate_metadata_destination (0.00s)
    --- PASS: TestPath_OIDCCreate/custom_expiration_leeway_and_not_before_leeway_values (0.00s)
=== RUN   TestPath_Read
--- PASS: TestPath_Read (0.00s)
=== RUN   TestPath_Delete
--- PASS: TestPath_Delete (0.00s)
=== RUN   TestLogin_fetchGroups
2020-07-10T14:00:38.076-0700 [DEBUG] found Azure Graph API endpoint for group membership: https://127.0.0.1:55372/getMemberObjects
2020-07-10T14:00:38.079-0700 [DEBUG] groups claim raw is [group1 group2]
--- PASS: TestLogin_fetchGroups (0.01s)
=== RUN   Test_getClaimSources
=== RUN   Test_getClaimSources/normal_case
=== RUN   Test_getClaimSources/no__claim_names
2020-07-10T14:00:38.079-0700 [WARN]  unable to locate /_claim_names/groups in claims: /_claim_names/groups at part 0: couldn't find key "_claim_names"
=== RUN   Test_getClaimSources/no__claim_sources
2020-07-10T14:00:38.079-0700 [WARN]  unable to locate /_claim_sources/src1/endpoint in claims: /_claim_sources/src1/endpoint at part 0: couldn't find key "_claim_sources"
--- PASS: Test_getClaimSources (0.00s)
    --- PASS: Test_getClaimSources/normal_case (0.00s)
    --- PASS: Test_getClaimSources/no__claim_names (0.00s)
    --- PASS: Test_getClaimSources/no__claim_sources (0.00s)
=== RUN   TestNewProviderConfig
=== RUN   TestNewProviderConfig/normal_case
=== RUN   TestNewProviderConfig/no_provider_config
=== RUN   TestNewProviderConfig/provider_field_not_present_in_provider_config
=== RUN   TestNewProviderConfig/unknown_provider
=== RUN   TestNewProviderConfig/provider_name_not_present_in_provider_config
=== RUN   TestNewProviderConfig/error_in_Initialize
--- PASS: TestNewProviderConfig (0.00s)
    --- PASS: TestNewProviderConfig/normal_case (0.00s)
    --- PASS: TestNewProviderConfig/no_provider_config (0.00s)
    --- PASS: TestNewProviderConfig/provider_field_not_present_in_provider_config (0.00s)
    --- PASS: TestNewProviderConfig/unknown_provider (0.00s)
    --- PASS: TestNewProviderConfig/provider_name_not_present_in_provider_config (0.00s)
    --- PASS: TestNewProviderConfig/error_in_Initialize (0.00s)
=== RUN   TestGSuiteProvider_initialize
=== RUN   TestGSuiteProvider_initialize/invalid_config:_required_service_account_key_is_empty
=== RUN   TestGSuiteProvider_initialize/invalid_config:_required_admin_impersonate_email_is_empty
=== RUN   TestGSuiteProvider_initialize/invalid_config:_recurse_max_depth_negative_number
=== RUN   TestGSuiteProvider_initialize/valid_config:_all_options
=== RUN   TestGSuiteProvider_initialize/valid_config:_no_custom_schemas
=== RUN   TestGSuiteProvider_initialize/valid_config:_no_recurse_max_depth
=== RUN   TestGSuiteProvider_initialize/valid_config:_fetch_groups_and_user_info
--- PASS: TestGSuiteProvider_initialize (0.00s)
    --- PASS: TestGSuiteProvider_initialize/invalid_config:_required_service_account_key_is_empty (0.00s)
    --- PASS: TestGSuiteProvider_initialize/invalid_config:_required_admin_impersonate_email_is_empty (0.00s)
    --- PASS: TestGSuiteProvider_initialize/invalid_config:_recurse_max_depth_negative_number (0.00s)
    --- PASS: TestGSuiteProvider_initialize/valid_config:_all_options (0.00s)
    --- PASS: TestGSuiteProvider_initialize/valid_config:_no_custom_schemas (0.00s)
    --- PASS: TestGSuiteProvider_initialize/valid_config:_no_recurse_max_depth (0.00s)
    --- PASS: TestGSuiteProvider_initialize/valid_config:_fetch_groups_and_user_info (0.00s)
PASS
ok      github.com/hashicorp/vault-plugin-auth-jwt      (cached)
?       github.com/hashicorp/vault-plugin-auth-jwt/cmd/vault-plugin-auth-jwt    [no test files]

@austingebauer
Copy link
Member Author

austingebauer commented Jul 10, 2020

Additional test writing is still in progress. This is ready for some feedback in the meantime. I'll be updating this PR as I add more tests.

@austingebauer austingebauer requested review from kalafut, tvoran and a team July 10, 2020 21:31
provider_gsuite.go Outdated Show resolved Hide resolved
provider_gsuite.go Outdated Show resolved Hide resolved
provider_gsuite.go Outdated Show resolved Hide resolved
Copy link
Contributor

@kalafut kalafut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Just some minor comments.

Copy link

@adongy adongy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for rewriting it!

I had a concern about provisioning files on instances (I now need to securely deliver files onto the Vault isntances), would it be possible to expose the JSON file in the request body (and mark it as sensitive)?

For example, the SSH certificate backend allows you to choose between Vault managing the private key generation, or the user can submit the full private key and let Vault store it.

provider_gsuite.go Show resolved Hide resolved
}

// Create the google JWT config from the service account key file
if g.jwtConfig, err = google.JWTConfigFromJSON(config.serviceAccountKeyJSON,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admin providing credentials with only the required scopes could have issues here:

For example. with fetch_groups = true and fetch_user_info = false, the minimal permission is admin.AdminDirectoryGroupReadonlyScope (no need to read user data).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this may be the minimal set, I think it makes sense to assume (and state in the docs) that the SA must have readonly on Users and Groups since most orgs would use this plugin to scope Vault tokens to group claims and not directly on users. There doesn't seem to be much of a use case for just scoping to individual users and not groups.

Copy link

@onetwopunch onetwopunch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a few suggestions, thanks for this work!

provider_gsuite.go Outdated Show resolved Hide resolved
}

// Create the google JWT config from the service account key file
if g.jwtConfig, err = google.JWTConfigFromJSON(config.serviceAccountKeyJSON,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this may be the minimal set, I think it makes sense to assume (and state in the docs) that the SA must have readonly on Users and Groups since most orgs would use this plugin to scope Vault tokens to group claims and not directly on users. There doesn't seem to be much of a use case for just scoping to individual users and not groups.

provider_gsuite.go Show resolved Hide resolved
provider_gsuite.go Show resolved Hide resolved
provider_gsuite_test.go Show resolved Hide resolved
@onetwopunch
Copy link

@kalafut @austingebauer Thanks for putting this all together. I'm not super familiar with the release cycle for this plugin or Vault in general, but once this is merged, approximately when will this be released?

@kalafut
Copy link
Contributor

kalafut commented Jul 13, 2020

@onetwopunch We're approaching a Vault release soon™, though bundling of this update vs. the timing of that is a little unclear. Regardless of the Vault release/bundled version though, this will be tagged in the plugin probably after the merge (I don't think we'd wait on anything else), and can be used as an external plugin. Note too that the external plugin can be used with existing data, and then subsequently you can revert to the bundled version when available. So there is some flexibility there.

@austingebauer
Copy link
Member Author

I had a concern about provisioning files on instances (I now need to securely deliver files onto the Vault instances), would it be possible to expose the JSON file in the request body (and mark it as sensitive)?

I think it's a valid concern, @adongy. If we were to take in the gsuite_service_account as a JSON string instead of a file path, then I would also add it to SensitiveKeys().

@onetwopunch, any thoughts on this?

@onetwopunch
Copy link

onetwopunch commented Jul 13, 2020

@austingebauer From a security perspective, there's a couple reasons I like having the file on the host as opposed to a sensitive value:

  1. The Vault host should already hardened and have some secure mechanism for delivering the TLS private key file anyway. I used Client Side encrypted files in GCS encrypted with Cloud KMS but whatever that mechanism is, can be replicated.
  2. From a separation of duties perspective, if the file was delivered as a JSON doc, the operator would need to be able to see it in clear text, whereas they wouldn't if it just references a known path like /etc/vault.d/gsuite_credentials.json. If Vault is running on GCP, you could even automate this by allowing the Vault Service Account to generate keys for itself and adding that logic into the startup script.
  3. If you want to configure Vault with Terraform, the Service Account would exist in plaintext in the tfstate.
  4. Also JSON files don't play nicely with Terraform in my experience. Until the terraform provider was brought up to date, this couldn't really be configured with a vault_generic_secret since it'd have to use nested heredocs.

In my opinion, there should be a clear delineation of path configuration and the secrets stored in them. Users should be able to safely configure Vault with Terraform without worrying about secrets finding their way into the tfstate. Obviously this is not always possible and isn't super consistent across the plugin ecosystem, but I think it's a goal that we should shoot for when possible such as in this case.


All that said, and to @adongy point, there is a precedence for setting GCP credentials in the path config in the GCP secret backend, though I would argue the security of that implementation as well with the points above.

@austingebauer
Copy link
Member Author

austingebauer commented Jul 14, 2020

Thanks for sharing your thoughts, @onetwopunch. I generally agree with the points you've made. Some of those reasons are why I chose to take in a file path to begin with for this PR.

I am mostly convinced to keep things as is by taking the file path in for the service account key file. I'm happy to hear others' thoughts though (@adongy).

@ademariag
Copy link

I have been looking for this for a long time, and happy to help with testing. Thank you @austingebauer

@adongy
Copy link

adongy commented Jul 15, 2020

While I agree with most points listed by @onetwopunch, I would prefer having the choice of posting the entire file or securely introducing it to the Vault instances.

One key difference with letting Vault manage the file and reading it from the local filesystem is the storage location: the storage backend would have everything needed, making the Vault instances slightly less specialized. Rotation of the service account is immediately propagated to all instances when it's handled by the storage backend.

Implementation wise, it could be done by exposing the serviceAccountKeyJSON in the struct, loading it from ServiceAccountFilePath if empty and finally erroring if both are missing.

@austingebauer
Copy link
Member Author

@adongy - I like the idea of providing the choice of configuring this via a file path or by providing the JSON contents directly. That would make this more consistent with other Google related Vault plugins. See auth/gcp#credentials and secret/gcp#credentials.

I'm thinking this can be done by either:

  1. Accepting a file path or JSON string via gsuite_service_account. This would first stat for the file. If it's successful, it would read the file and use it's contents. Otherwise, we'd assume the contents are JSON and use them directly.
  2. Only accepting a JSON string via gsuite_service_account. If the value is empty and not configured, check to see if an environment variable (for example, GOOGLE_APPLICATION_CREDENTIALS) is set to provide the file path to the service account key file. This would be similar to the application default credentials approach used in secrets/gcp#authentication.

Looking to hear some thoughts or other approaches related to this.

@adongy
Copy link

adongy commented Jul 17, 2020

My personal opinion on this matter is that providing strong guarantees about the type of values makes it easier for the end user to understand what to provide without looking at some documentation and allows developers to write things like LSPs for autocompletion more easily.

As I do not use the GCP part of Vault, I cannot chime in on whether using environment variables for paths is the preferred way or not.

The AWS equivalent allows you to either hardcode the variable value for the access keys, default to environment variables, or specify a profile file as describe in the AWS CLI docs.

@onetwopunch
Copy link

@adongy FWIW I wouldn't oppose the option of passing this in it's entirety, however I definitely think the documentation should clearly suggest loading the service account on the filesystem using the same mechanism for TLS certificates. For something like a service account that has GSuite admin access, many organizations would not be comfortable letting an operator see that in clear text to load it into Vault, in fact they'd probably say by virtue of that operator seeing it, the key is already compromised. Since it would allow that operator to dump all user and group info from the entire organization, it would be a valuable target for an insider threat. Separating the duties of the Vault server to load the file from an encrypted source and the operator that configures it would solve this problem in a low friction way.

In regards to your question of key rotation, this is a valid concern that I think could be addressed in code and the documentation. First, @austingebauer could we add a check on that path_config on the shasum of the service account file if one already exists in the logical storage? This way we wouldn't have to mess with renaming the file and only have to execute a config/write again for the plugin to pull in the updated config into the logical storage. Then in the docs something like:

For rotation of the Service Account Key the following steps should be taken in this order:
1. Pull new service account onto the Vault host, overriding the old one.
2. Ensure new key exists on all hosts in the cluster
3. Execute vault write auth/oidc/config ...

Thoughts?

@austingebauer
Copy link
Member Author

@onetwopunch - I also don't oppose having the option to pass the credentials in as JSON. The way that option is provided in the API is what needs to be settled. I agree that the documentation can suggest providing them via a file path for reasons that you've mentioned.

Regarding the key rotation discussion, the steps you've provided above are already how a rotation would work. The configuration supplied via provider_config is what gets written to logical storage. Right now, that is the file path to the service account key. Every time a config write happens, the GSuiteProvider.Initialize() method is called (see: path_config.go#L284), which reads the contents of that file into memory to be provided to google.JWTConfigFromJSON(). So I don't think we need to add a shasum check.

@austingebauer
Copy link
Member Author

FWIW the GCP terraform provider has an option similar to one that I proposed above. It allows for supplying the credentials as a JSON string or a file path (see: credentials). I could see gsuite_service_account taking a similar behavior here.

All that said, I don't think we need to hold up this PR to decide. I'm happy to open an issue and continue the discussion. I can always add flexibility to the service account configuration in a separate PR. I want to make sure this is carefully considered. Thoughts?

Copy link

@onetwopunch onetwopunch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Member

@tvoran tvoran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Left a couple small comments.

provider_gsuite_test.go Outdated Show resolved Hide resolved
provider_gsuite_test.go Outdated Show resolved Hide resolved
@austingebauer austingebauer merged commit 5815ce5 into master Jul 21, 2020
@austingebauer austingebauer deleted the gsuite-oidc-provider branch July 21, 2020 23:08
@onetwopunch
Copy link

@austingebauer do we have a process for adding this change to the Terraform Vault provider now that it's merged?

@austingebauer
Copy link
Member Author

@onetwopunch - I've opened up an issue to track getting this added (hashicorp/terraform-provider-vault#828). We try to get new features into the provider quickly, but it can become a balance of priority and demand. Note that this G Suite PR has yet to be bundled into an official Vault release, so it's not mentioned in the issue.

@m1keil
Copy link

m1keil commented Jul 26, 2020

Hi @austingebauer,
I was researching into Vault and OIDC and stumbled upon this functionality that was just added by you. A small question - why does a service account require to perform the group lookup? Isn't the access token can be used as the API key to access the group SDK (assuming the scope was configured correctly)?

I'm a complete noob at the whole OAuth2.0/OIDC stuff and probably missing something fundamental here.

@onetwopunch
Copy link

@m1keil you are correct, however access tokens are short lived (1 hour) so Vault would need a service account key to regenerate them. Think of a service account key as an identity that allows robots to login with OAuth without having to interact with the browser or redirects. I believe it's called a two-legged flow or something.

@asmontas-i
Copy link

Hi,

I'm a complete noob with OIDC as well, so hopefully someone could point me in the right direction.

I'm trying to follow the documentation preview and retrieve g-suite group membership which would then map with Vault External group with additional policies (similarly as described in these instructions step 5 & 6).

So far I have setup the OAuth2 app and service account with domain delegation with 2 required scopes as per docs. I can login via OIDC successfully, however, it looks like no additional group membership is retrieved or is somewhat incorrectly mapped via Identity group alias (which, btw, would be nice to have an additional example in this documentation for identity group alias configuration for mapping with the g-suite group):

vault login -method=oidc role="vault"
Complete the login via your OIDC provider. Launching browser to:

    https://accounts.google.com/<REDACTED>

Key                  Value
---                  -----
token                <REDACTED>
token_accessor       <REDACTED>
token_duration       768h
token_renewable      true
token_policies       ["default" "reader"]
identity_policies    []
policies             ["default" "reader"]
token_meta_role      vault

So I'd expect that if everything configured correctly, aliased external group policy would be listed in the identity_policies, however it is empty.

So couple of questions:

  1. What could be the potential reasons for this behaviour?
  2. How could I verify that group membership is actually retrieved from G-suite IdP?
  3. Is my understanding correct, that claim_mappings for user_custom_schemas is not required for G-suite group membership mapping with aliased vault External Groups?
  4. Does G-suite group Access type (e.g. Public, Team, Announcement Only) has any effect on this?

Any help is highly appreciated!

calvn pushed a commit that referenced this pull request Aug 17, 2020
calvn added a commit that referenced this pull request Aug 17, 2020
…information (#123) (#133)

Co-authored-by: Austin Gebauer <34121980+austingebauer@users.noreply.github.com>
kalafut pushed a commit that referenced this pull request Aug 17, 2020
…information (#123) (#133)

Co-authored-by: Austin Gebauer <34121980+austingebauer@users.noreply.github.com>
kalafut pushed a commit to kalafut/vault-plugin-auth-jwt that referenced this pull request Aug 17, 2020
@cloudowski
Copy link

@asmontas-i
I had the same issue and please check if you have defined your group in both group and group-alias using a full name with a domain name (group@example.com). In my case, it solved this problem. However, I wasn't able to read (e.g. by using bound_claims) groups from a claim - "groups" attribute seems to be unset.

@azelezni
Copy link

@cloudowski
Thank you very much I was getting very frustrated and creating external group & alias finally allowed me to map policies to specific groups.

Any idea how I can make group memeber have the name/email from Google? currently it's just a random string.

@cloudowski
Copy link

@azelezni Just use email attribute instead of sub. You can also try to use custom attributes fetched in the same way as groups, but I haven't tested it yet.

@azelezni
Copy link

azelezni commented Aug 26, 2020

@cloudowski Doesn't seem to work, I've tried all of the supported claim by Google https://accounts.google.com/.well-known/openid-configuration, but nothing worked, either getting claim not found, or failed to fetch groups: googleapi: Error 400: Invalid Input: memberKey, invalid.
I've checked in Google Developer console and the project has email scope enabled.
This has got to be the hardest time I had configuring Google auth, documentation is lacking and no clear examples.

I did manage to update the entities metadata, but only for the alias and it doesn't actually change the name of the entity

vault write auth/oidc/config -<<EOF
{
    "oidc_discovery_url": "https://accounts.google.com",
    "oidc_client_id": "...",
    "oidc_client_secret": "...",
    "default_role": "default_role",
    "provider_config": {
        "provider": "gsuite",
        "gsuite_service_account": "/tmp/service-account.json",
        "gsuite_admin_impersonate": "...",
        "fetch_groups": true,
        "fetch_user_info": true,
        "groups_recurse_max_depth": 5,
        "user_custom_schemas": "email,Email,mail,Mail"
    }
}
EOF


vault write auth/oidc/role/default_role -<<EOF
{
    "role_type": "oidc",
    "policies": "default",
    "allowed_redirect_uris": "https://vault.example.com/ui/vault/auth/oidc/oidc/callback,https://example.com/oidc/callback",
    "user_claim": "name",
    "claim_mappings": {
        "aud": "aud",
        "email": "email",
        "email_verified": "email_verified",
        "family_name": "family_name",
        "given_name": "given_name",
        "iss": "iss",
        "locale": "locale",
        "name": "name",
        "picture": "picture",
        "sub": "sub"
    }
}
EOF

@austingebauer
Copy link
Member Author

@azelezni - user_custom_schemas are used for getting custom schema field values from G Suite as additional user information for things like token metadata. In order to accomplish using the Google email address for group membership (entity alias name), you'll need to appropriately set oidc_scopes and user_claim on the role.

See example below with "oidc_scopes": "email" and "user_claim": "email" added to your example. I've tested that this results in the Google email address used as the entity alias name:

vault write auth/oidc/config -<<EOF
{
    "oidc_discovery_url": "https://accounts.google.com",
    "oidc_client_id": "...",
    "oidc_client_secret": "...",
    "default_role": "default_role",
    "provider_config": {
        "provider": "gsuite",
        "gsuite_service_account": "/tmp/service-account.json",
        "gsuite_admin_impersonate": "...",
        "fetch_groups": true,
        "groups_recurse_max_depth": 5,
    }
}
EOF

vault write auth/oidc/role/default_role -<<EOF
{
    "role_type": "oidc",
    "policies": "default",
    "allowed_redirect_uris": "https://vault.example.com/ui/vault/auth/oidc/oidc/callback,https://example.com/oidc/callback",
    "oidc_scopes": "email",
    "user_claim": "email",
    "claim_mappings": {
        "aud": "aud",
        "email": "email",
        "email_verified": "email_verified",
        "family_name": "family_name",
        "given_name": "given_name",
        "iss": "iss",
        "locale": "locale",
        "name": "name",
        "picture": "picture",
        "sub": "sub"
    }
}
EOF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for group claims in GSuite
10 participants