Chore: Query oauth info from a new instance (#83229)

* query OAuth info from a new instance

* add `hd` validation flag

* add `disable_hd_validation` to settings map

* update documentation

---------

Co-authored-by: Jo <joao.guerreiro@grafana.com>
This commit is contained in:
linoman 2024-02-29 09:48:32 -06:00 committed by GitHub
parent 2a1d4f85c7
commit b02ae375ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 13 deletions

View File

@ -679,6 +679,7 @@ token_url = https://oauth2.googleapis.com/token
api_url = https://openidconnect.googleapis.com/v1/userinfo
signout_redirect_url =
allowed_domains =
validate_hd = true
hosted_domain =
allowed_groups =
role_attribute_path =

View File

@ -643,6 +643,7 @@
;api_url = https://openidconnect.googleapis.com/v1/userinfo
;signout_redirect_url =
;allowed_domains =
;validate_hd =
;hosted_domain =
;allowed_groups =
;role_attribute_path =

View File

@ -111,6 +111,14 @@ automatically signed up.
You may specify a domain to be passed as `hd` query parameter accepted by Google's
OAuth 2.0 authentication API. Refer to Google's OAuth [documentation](https://developers.google.com/identity/openid-connect/openid-connect#hd-param).
{{% admonition type="note" %}}
Since Grafana 10.3.0, the `hd` parameter retrieved from Google ID token is also used to determine the user's hosted domain. The Google Oauth `allowed_domains` configuration option is used to restrict access to users from a specific domain. If the `allowed_domains` configuration option is set, the `hd` parameter from the Google ID token must match the `allowed_domains` configuration option. If the `hd` parameter from the Google ID token does not match the `allowed_domains` configuration option, the user is denied access.
When an account does not belong to a google workspace, the hd claim will not be available.
This validation is enabled by default. To disable this validation, set the `validate_hd` configuration option to `false`. The `allowed_domains` configuration option will use the email claim to validate the domain.
{{% /admonition %}}
#### PKCE
IETF's [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)

View File

@ -24,13 +24,16 @@ const (
legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo"
googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups"
googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly"
validateHDKey = "validate_hd"
)
var _ social.SocialConnector = (*SocialGoogle)(nil)
var _ ssosettings.Reloadable = (*SocialGoogle)(nil)
var ExtraGoogleSettingKeys = []string{validateHDKey}
type SocialGoogle struct {
*SocialBase
validateHD bool
}
type googleUserData struct {
@ -45,6 +48,7 @@ type googleUserData struct {
func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGoogle {
provider := &SocialGoogle{
SocialBase: newSocialBase(social.GoogleProviderName, info, features, cfg),
validateHD: MustBool(info.Extra[validateHDKey], true),
}
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {
@ -89,6 +93,7 @@ func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSetting
defer s.reloadMutex.Unlock()
s.updateInfo(social.GoogleProviderName, newInfo)
s.validateHD = MustBool(newInfo.Extra[validateHDKey], false)
return nil
}
@ -117,7 +122,7 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
return nil, fmt.Errorf("user email is not verified")
}
if err := s.isHDAllowed(data.HD); err != nil {
if err := s.isHDAllowed(data.HD, info); err != nil {
return nil, err
}
@ -163,6 +168,7 @@ type googleAPIData struct {
Name string `json:"name"`
Email string `json:"email"`
EmailVerified bool `json:"verified_email"`
HD string `json:"hd"`
}
func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) {
@ -184,6 +190,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client)
Name: data.Name,
Email: data.Email,
EmailVerified: data.EmailVerified,
HD: data.HD,
rawJSON: response.Body,
}, nil
}
@ -297,12 +304,16 @@ func (s *SocialGoogle) getGroupsPage(ctx context.Context, client *http.Client, u
return &data, nil
}
func (s *SocialGoogle) isHDAllowed(hd string) error {
if len(s.info.AllowedDomains) == 0 {
func (s *SocialGoogle) isHDAllowed(hd string, info *social.OAuthInfo) error {
if s.validateHD {
return nil
}
for _, allowedDomain := range s.info.AllowedDomains {
if len(info.AllowedDomains) == 0 {
return nil
}
for _, allowedDomain := range info.AllowedDomains {
if hd == allowedDomain {
return nil
}

View File

@ -897,6 +897,7 @@ func TestIsHDAllowed(t *testing.T) {
email string
allowedDomains []string
expectedErrorMessage string
validateHD bool
}{
{
name: "should not fail if no allowed domains are set",
@ -916,6 +917,12 @@ func TestIsHDAllowed(t *testing.T) {
allowedDomains: []string{"grafana.com", "example.com"},
expectedErrorMessage: "the hd claim found in the ID token is not present in the allowed domains",
},
{
name: "should not fail if the HD validation is disabled and the email not being from an allowed domain",
email: "mycompany.com",
allowedDomains: []string{"grafana.com", "example.com"},
validateHD: true,
},
}
for _, tc := range testCases {
@ -923,7 +930,8 @@ func TestIsHDAllowed(t *testing.T) {
info := &social.OAuthInfo{}
info.AllowedDomains = tc.allowedDomains
s := NewGoogleProvider(info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.isHDAllowed(tc.email)
s.validateHD = tc.validateHD
err := s.isHDAllowed(tc.email, info)
if tc.expectedErrorMessage != "" {
require.Error(t, err)

View File

@ -19,6 +19,7 @@ var extraKeysByProvider = map[string][]string{
social.AzureADProviderName: connectors.ExtraAzureADSettingKeys,
social.GenericOAuthProviderName: connectors.ExtraGenericOAuthSettingKeys,
social.GitHubProviderName: connectors.ExtraGithubSettingKeys,
social.GoogleProviderName: connectors.ExtraGoogleSettingKeys,
social.GrafanaComProviderName: connectors.ExtraGrafanaComSettingKeys,
social.GrafanaNetProviderName: connectors.ExtraGrafanaComSettingKeys,
}

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/setting"
)
@ -129,6 +130,9 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) {
[auth.grafana_com]
enabled = true
allowed_organizations = org1, org2
[auth.google]
validate_hd = true
`
iniFile, err := ini.Load([]byte(iniWithExtraFields))
@ -139,24 +143,24 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) {
strategy := NewOAuthStrategy(cfg)
t.Run("azuread", func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), "azuread")
t.Run(social.AzureADProviderName, func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), social.AzureADProviderName)
require.NoError(t, err)
require.Equal(t, "true", result["force_use_graph_api"])
require.Equal(t, "org1, org2", result["allowed_organizations"])
})
t.Run("github", func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), "github")
t.Run(social.GitHubProviderName, func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), social.GitHubProviderName)
require.NoError(t, err)
require.Equal(t, "first, second", result["team_ids"])
require.Equal(t, "org1, org2", result["allowed_organizations"])
})
t.Run("generic_oauth", func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), "generic_oauth")
t.Run(social.GenericOAuthProviderName, func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), social.GenericOAuthProviderName)
require.NoError(t, err)
require.Equal(t, "first, second", result["team_ids"])
@ -166,12 +170,19 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) {
require.Equal(t, "id_token", result["id_token_attribute_name"])
})
t.Run("grafana_com", func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), "grafana_com")
t.Run(social.GrafanaComProviderName, func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), social.GrafanaComProviderName)
require.NoError(t, err)
require.Equal(t, "org1, org2", result["allowed_organizations"])
})
t.Run(social.GoogleProviderName, func(t *testing.T) {
result, err := strategy.GetProviderConfig(context.Background(), social.GoogleProviderName)
require.NoError(t, err)
require.Equal(t, "true", result["validate_hd"])
})
}
// TestGetProviderConfig_GrafanaComGrafanaNet tests that the connector is setup using the correct section and it supports