From 11d196eb6eeb13a695660b20fc06fa727d9666f2 Mon Sep 17 00:00:00 2001 From: Jo Date: Mon, 26 Jun 2023 09:44:57 +0200 Subject: [PATCH] Auth: Support google OIDC and group fetching (#70140) * Auth: Update Google OAuth default configuration based on /.well-known/openid-configuration #69520 Signed-off-by: junya koyama * add id_token parsing add legacy API distinction use google auth oidc connectors add group fetching support and tests * Apply suggestions from code review Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Ieva * implement review feedback * indent docs --------- Signed-off-by: junya koyama Co-authored-by: junya koyama Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Ieva --- conf/defaults.ini | 8 +- conf/sample.ini | 8 +- .../configure-authentication/google/index.md | 30 +- pkg/login/social/google_oauth.go | 184 ++++++- pkg/login/social/google_oauth_test.go | 504 ++++++++++++++++++ pkg/login/social/social.go | 11 +- 6 files changed, 717 insertions(+), 28 deletions(-) create mode 100644 pkg/login/social/google_oauth_test.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 081d07cdcf6..27ad71314ba 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -631,10 +631,10 @@ allow_sign_up = true auto_login = false client_id = some_client_id client_secret = -scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email -auth_url = https://accounts.google.com/o/oauth2/auth -token_url = https://accounts.google.com/o/oauth2/token -api_url = https://www.googleapis.com/oauth2/v1/userinfo +scopes = openid email profile +auth_url = https://accounts.google.com/o/oauth2/v2/auth +token_url = https://oauth2.googleapis.com/token +api_url = https://openidconnect.googleapis.com/v1/userinfo allowed_domains = hosted_domain = skip_org_role_sync = false diff --git a/conf/sample.ini b/conf/sample.ini index 5ffd76dc34f..151a0aa306f 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -616,10 +616,10 @@ ;auto_login = false ;client_id = some_client_id ;client_secret = some_client_secret -;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email -;auth_url = https://accounts.google.com/o/oauth2/auth -;token_url = https://accounts.google.com/o/oauth2/token -;api_url = https://www.googleapis.com/oauth2/v1/userinfo +;scopes = openid email profile +;auth_url = https://accounts.google.com/o/oauth2/v2/auth +;token_url = https://oauth2.googleapis.com/token +;api_url = https://openidconnect.googleapis.com/v1/userinfo ;allowed_domains = ;hosted_domain = ;skip_org_role_sync = false diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 722408a3158..e671c2381d5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -36,9 +36,10 @@ allow_sign_up = true auto_login = false client_id = CLIENT_ID client_secret = CLIENT_SECRET -scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email -auth_url = https://accounts.google.com/o/oauth2/auth -token_url = https://accounts.google.com/o/oauth2/token +scopes = openid email profile +auth_url = https://accounts.google.com/o/oauth2/v2/auth +token_url = https://oauth2.googleapis.com/token +api_url = https://openidconnect.googleapis.com/v1/userinfo allowed_domains = mycompany.com mycompany.org hosted_domain = mycompany.com use_pkce = true @@ -98,3 +99,26 @@ We do not currently sync roles from Google and instead set the AutoAssigned role # .. skip_org_role_sync = true ``` + +### Configure team sync for Google OAuth + +> Available in Grafana v10.1.0 and later versions. + +With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example. + +1. Enable the Google Cloud Identity API on your [organization's dashboard](https://console.cloud.google.com/apis/api/cloudidentity.googleapis.com/). + +1. Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` configuration: + + Example: + + ```ini + [auth.google] + # .. + scopes = openid email profile https://www.googleapis.com/auth/cloud-identity.groups.readonly + ``` + +1. Configure team sync in your Grafana team's `External group sync` tab. + The external group ID for a Google group is the group's email address, such as `dev@grafana.com`. + +To learn more about Team Sync, refer to [Configure Team Sync]({{< relref "../../configure-team-sync" >}}). diff --git a/pkg/login/social/google_oauth.go b/pkg/login/social/google_oauth.go index 9771a0195df..4e78f8a121e 100644 --- a/pkg/login/social/google_oauth.go +++ b/pkg/login/social/google_oauth.go @@ -5,41 +5,112 @@ import ( "encoding/json" "fmt" "net/http" + "strings" + "golang.org/x/exp/slices" "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" ) +const legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo" +const googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups" +const googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly" + type SocialGoogle struct { *SocialBase hostedDomain string apiUrl string } +type googleUserData struct { + ID string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + EmailVerified bool `json:"email_verified"` +} + func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { - var data struct { - Id string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` + data, errToken := s.extractFromToken(ctx, client, token) + if errToken != nil { + return nil, errToken } + if data == nil { + var errAPI error + data, errAPI = s.extractFromAPI(ctx, client) + if errAPI != nil { + return nil, errAPI + } + } + + if data.ID == "" { + return nil, fmt.Errorf("error getting user info: id is empty") + } + + if !data.EmailVerified { + return nil, fmt.Errorf("user email is not verified") + } + + groups, errPage := s.retrieveGroups(ctx, client, data) + if errPage != nil { + s.log.Warn("Error retrieving groups", "error", errPage) + } + + userInfo := &BasicUserInfo{ + Id: data.ID, + Name: data.Name, + Email: data.Email, + Login: data.Email, + Role: "", + IsGrafanaAdmin: nil, + Groups: groups, + } + + s.log.Debug("Resolved user info", "data", fmt.Sprintf("%+v", userInfo)) + + return userInfo, nil +} + +type googleAPIData struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + EmailVerified bool `json:"verified_email"` +} + +func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) { + if strings.HasPrefix(s.apiUrl, legacyAPIURL) { + data := googleAPIData{} + response, err := s.httpGet(ctx, client, s.apiUrl) + if err != nil { + return nil, fmt.Errorf("error retrieving legacy user info: %s", err) + } + + if err := json.Unmarshal(response.Body, &data); err != nil { + return nil, fmt.Errorf("error unmarshalling legacy user info: %s", err) + } + + return &googleUserData{ + ID: data.ID, + Name: data.Name, + Email: data.Email, + EmailVerified: data.EmailVerified, + }, nil + } + + data := googleUserData{} response, err := s.httpGet(ctx, client, s.apiUrl) if err != nil { - return nil, fmt.Errorf("Error getting user info: %s", err) + return nil, fmt.Errorf("error getting user info: %s", err) } - err = json.Unmarshal(response.Body, &data) - if err != nil { - return nil, fmt.Errorf("Error getting user info: %s", err) + if err := json.Unmarshal(response.Body, &data); err != nil { + return nil, fmt.Errorf("error unmarshalling user info: %s", err) } - return &BasicUserInfo{ - Id: data.Id, - Name: data.Name, - Email: data.Email, - Login: data.Email, - }, nil + return &data, nil } func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { @@ -48,3 +119,88 @@ func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) } return s.SocialBase.AuthCodeURL(state, opts...) } + +func (s *SocialGoogle) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*googleUserData, error) { + s.log.Debug("Extracting user info from OAuth token") + + idToken := token.Extra("id_token") + if idToken == nil { + s.log.Debug("No id_token found, defaulting to API access", "token", token) + return nil, nil + } + + rawJSON, err := s.retrieveRawIDToken(idToken) + if err != nil { + s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken)) + return nil, nil + } + + if setting.Env == setting.Dev { + s.log.Debug("Received id_token", "raw_json", string(rawJSON)) + } + + var data googleUserData + if err := json.Unmarshal(rawJSON, &data); err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + return &data, nil +} + +type googleGroupResp struct { + Memberships []struct { + Group string `json:"group"` + GroupKey struct { + ID string `json:"id"` + } `json:"groupKey"` + DisplayName string `json:"displayName"` + } `json:"memberships"` + NextPageToken string `json:"nextPageToken"` +} + +func (s *SocialGoogle) retrieveGroups(ctx context.Context, client *http.Client, userData *googleUserData) ([]string, error) { + s.log.Debug("Retrieving groups", "scopes", s.SocialBase.Config.Scopes) + if !slices.Contains(s.Scopes, googleIAMScope) { + return nil, nil + } + + groups := []string{} + + url := fmt.Sprintf("%s?query=member_key_id=='%s'", googleIAMGroupsEndpoint, userData.Email) + nextPageToken := "" + for page, errPage := s.getGroupsPage(ctx, client, url, nextPageToken); ; page, errPage = s.getGroupsPage(ctx, client, url, nextPageToken) { + if errPage != nil { + return nil, errPage + } + + for _, group := range page.Memberships { + groups = append(groups, group.GroupKey.ID) + } + + nextPageToken = page.NextPageToken + if nextPageToken == "" { + break + } + } + + return groups, nil +} + +func (s *SocialGoogle) getGroupsPage(ctx context.Context, client *http.Client, url, nextPageToken string) (*googleGroupResp, error) { + if nextPageToken != "" { + url = fmt.Sprintf("%s&pageToken=%s", url, nextPageToken) + } + + s.log.Debug("Retrieving groups", "url", url) + resp, err := s.httpGet(ctx, client, url) + if err != nil { + return nil, fmt.Errorf("error getting groups: %s", err) + } + + var data googleGroupResp + if err := json.Unmarshal(resp.Body, &data); err != nil { + return nil, fmt.Errorf("error unmarshalling groups: %s", err) + } + + return &data, nil +} diff --git a/pkg/login/social/google_oauth_test.go b/pkg/login/social/google_oauth_test.go new file mode 100644 index 00000000000..3bf75e6c9c9 --- /dev/null +++ b/pkg/login/social/google_oauth_test.go @@ -0,0 +1,504 @@ +package social + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/infra/log" +) + +func TestSocialGoogle_retrieveGroups(t *testing.T) { + type fields struct { + Scopes []string + } + type args struct { + client *http.Client + userData *googleUserData + } + tests := []struct { + name string + fields fields + args args + want []string + wantErr bool + }{ + { + name: "No scope", + fields: fields{ + Scopes: []string{}, + }, + args: args{ + client: &http.Client{}, + userData: &googleUserData{ + Email: "test@example.com", + }, + }, + want: nil, + wantErr: false, + }, + { + name: "No groups", + fields: fields{ + Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"}, + }, + args: args{ + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "memberships": [ + ], + "nextPageToken": "" + }`) + return resp.Result(), nil + }, + }, + }, + userData: &googleUserData{ + Email: "test@example.com", + }, + }, + want: []string{}, + wantErr: false, + }, + { + name: "error retrieving groups", + fields: fields{ + Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"}, + }, + args: args{ + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + return nil, errors.New("error retrieving groups") + }, + }, + }, + userData: &googleUserData{ + Email: "test@example.com", + }, + }, + want: nil, + wantErr: true, + }, + + { + name: "Has 2 pages to fetch", + fields: fields{ + Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"}, + }, + args: args{ + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + // First page + if req.URL.Query().Get("pageToken") == "" { + _, _ = resp.WriteString(`{ + "memberships": [ + { + "group": "test-group", + "groupKey": { + "id": "test-group@google.com" + }, + "displayName": "Test Group" + } + ], + "nextPageToken": "page-2" + }`) + } else { + // Second page + _, _ = resp.WriteString(`{ + "memberships": [ + { + "group": "test-group-2", + "groupKey": { + "id": "test-group-2@google.com" + }, + "displayName": "Test Group 2" + } + ], + "nextPageToken": "" + }`) + } + return resp.Result(), nil + }, + }, + }, + userData: &googleUserData{ + Email: "test@example.com", + }, + }, + want: []string{"test-group@google.com", "test-group-2@google.com"}, + wantErr: false, + }, + { + name: "Has one page to fetch", + fields: fields{ + Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"}, + }, + args: args{ + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "memberships": [ + { + "group": "test-group", + "groupKey": { + "id": "test-group@google.com" + }, + "displayName": "Test Group" + } + ], + "nextPageToken": "" + }`) + return resp.Result(), nil + }, + }, + }, + userData: &googleUserData{ + Email: "test@example.com", + }, + }, + want: []string{"test-group@google.com"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SocialGoogle{ + SocialBase: &SocialBase{ + Config: &oauth2.Config{Scopes: tt.fields.Scopes}, + log: log.NewNopLogger(), + allowSignup: false, + allowAssignGrafanaAdmin: false, + allowedDomains: []string{}, + roleAttributePath: "", + roleAttributeStrict: false, + autoAssignOrgRole: "", + skipOrgRoleSync: false, + }, + hostedDomain: "", + apiUrl: "", + } + got, err := s.retrieveGroups(context.Background(), tt.args.client, tt.args.userData) + if (err != nil) != tt.wantErr { + t.Errorf("SocialGoogle.retrieveGroups() error = %v, wantErr %v", err, tt.wantErr) + return + } + require.Equal(t, tt.want, got) + }) + } +} + +type roundTripperFunc struct { + fn func(req *http.Request) (*http.Response, error) +} + +func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f.fn(req) +} + +func TestSocialGoogle_UserInfo(t *testing.T) { + cl := jwt.Claims{ + Subject: "88888888888888", + Issuer: "issuer", + NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), + Audience: jwt.Audience{"823123"}, + } + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: []byte("secret")}, (&jose.SignerOptions{}).WithType("JWT")) + require.NoError(t, err) + idMap := map[string]interface{}{ + "email": "test@example.com", + "name": "Test User", + "hd": "example.com", + "email_verified": true, + } + + raw, err := jwt.Signed(sig).Claims(cl).Claims(idMap).CompactSerialize() + require.NoError(t, err) + + tokenWithID := (&oauth2.Token{ + AccessToken: "fake_token", + }).WithExtra(map[string]interface{}{"id_token": raw}) + + tokenWithoutID := &oauth2.Token{} + + type fields struct { + Scopes []string + apiURL string + } + type args struct { + client *http.Client + token *oauth2.Token + } + tests := []struct { + name string + fields fields + args args + wantData *BasicUserInfo + wantErr bool + wantErrMsg string + }{ + { + name: "Success id_token", + fields: fields{ + Scopes: []string{}, + }, + args: args{ + token: tokenWithID, + }, + wantData: &BasicUserInfo{ + Id: "88888888888888", + Login: "test@example.com", + Email: "test@example.com", + Name: "Test User", + }, + wantErr: false, + }, + { + name: "Success id_token - groups requested", + fields: fields{ + Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"}, + }, + args: args{ + token: tokenWithID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "memberships": [ + { + "group": "test-group", + "groupKey": { + "id": "test-group@google.com" + }, + "displayName": "Test Group" + } + ], + "nextPageToken": "" + }`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: &BasicUserInfo{ + Id: "88888888888888", + Login: "test@example.com", + Email: "test@example.com", + Name: "Test User", + Groups: []string{"test-group@google.com"}, + }, + wantErr: false, + }, + { + name: "Legacy API URL", + fields: fields{ + apiURL: legacyAPIURL, + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "id": "99999999999999", + "name": "Test User", + "email": "test@example.com", + "verified_email": true + }`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: &BasicUserInfo{ + Id: "99999999999999", + Login: "test@example.com", + Email: "test@example.com", + Name: "Test User", + }, + wantErr: false, + }, + { + name: "Legacy API URL - no id provided", + fields: fields{ + apiURL: legacyAPIURL, + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "name": "Test User", + "email": "test@example.com", + "verified_email": true + }`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: nil, + wantErr: true, + wantErrMsg: "error getting user info: id is empty", + }, + { + name: "Error unmarshalling legacy user info", + fields: fields{ + apiURL: legacyAPIURL, + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`invalid json`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: nil, + wantErr: true, + wantErrMsg: "error unmarshalling legacy user info", + }, + { + name: "Error getting user info", + fields: fields{ + apiURL: "https://openidconnect.googleapis.com/v1/userinfo", + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + return nil, errors.New("error getting user info") + }, + }, + }, + }, + wantData: nil, + wantErr: true, + wantErrMsg: "error getting user info", + }, + { + name: "Error unmarshalling user info", + fields: fields{ + apiURL: "https://openidconnect.googleapis.com/v1/userinfo", + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`invalid json`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: nil, + wantErr: true, + wantErrMsg: "error unmarshalling user info", + }, + { + name: "Success", + fields: fields{ + apiURL: "https://openidconnect.googleapis.com/v1/userinfo", + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "sub": "92222222222222222", + "name": "Test User", + "email": "test@example.com", + "email_verified": true + }`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: &BasicUserInfo{ + Id: "92222222222222222", + Name: "Test User", + Email: "test@example.com", + Login: "test@example.com", + }, + wantErr: false, + }, { + name: "Unverified Email userinfo", + fields: fields{ + apiURL: "https://openidconnect.googleapis.com/v1/userinfo", + }, + args: args{ + token: tokenWithoutID, + client: &http.Client{ + Transport: &roundTripperFunc{ + fn: func(req *http.Request) (*http.Response, error) { + resp := httptest.NewRecorder() + _, _ = resp.WriteString(`{ + "sub": "92222222222222222", + "name": "Test User", + "email": "test@example.com", + "email_verified": false + }`) + return resp.Result(), nil + }, + }, + }, + }, + wantData: nil, + wantErr: true, + wantErrMsg: "email is not verified", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SocialGoogle{ + apiUrl: tt.fields.apiURL, + SocialBase: &SocialBase{ + Config: &oauth2.Config{Scopes: tt.fields.Scopes}, + log: log.NewNopLogger(), + allowSignup: false, + }, + } + + gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token) + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantData, gotData) + } + }) + } +} diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 92f95878738..8db92e7f7de 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -37,6 +37,7 @@ type SocialService struct { socialMap map[string]SocialConnector oAuthProvider map[string]*OAuthInfo + log log.Logger } type OAuthInfo struct { @@ -78,6 +79,7 @@ func ProvideService(cfg *setting.Cfg, cfg: cfg, oAuthProvider: make(map[string]*OAuthInfo), socialMap: make(map[string]SocialConnector), + log: log.New("login.social"), } usageStats.RegisterMetricsFunc(ss.getUsageStats) @@ -138,7 +140,7 @@ func ProvideService(cfg *setting.Cfg, case "autodetect", "": authStyle = oauth2.AuthStyleAutoDetect default: - logger.Warn("Invalid auth style specified, defaulting to auth style AutoDetect", "auth_style", sec.Key("auth_style").String()) + ss.log.Warn("Invalid auth style specified, defaulting to auth style AutoDetect", "auth_style", sec.Key("auth_style").String()) authStyle = oauth2.AuthStyleAutoDetect } @@ -177,6 +179,9 @@ func ProvideService(cfg *setting.Cfg, // Google. if name == "google" { + if strings.HasPrefix(info.ApiUrl, legacyAPIURL) { + ss.log.Warn("Using legacy Google API URL, please update your configuration") + } ss.socialMap["google"] = &SocialGoogle{ SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), hostedDomain: info.HostedDomain, @@ -475,7 +480,7 @@ func (ss *SocialService) GetOAuthHttpClient(name string) (*http.Client, error) { if info.TlsClientCert != "" || info.TlsClientKey != "" { cert, err := tls.LoadX509KeyPair(info.TlsClientCert, info.TlsClientKey) if err != nil { - logger.Error("Failed to setup TlsClientCert", "oauth", name, "error", err) + ss.log.Error("Failed to setup TlsClientCert", "oauth", name, "error", err) return nil, fmt.Errorf("failed to setup TlsClientCert: %w", err) } @@ -485,7 +490,7 @@ func (ss *SocialService) GetOAuthHttpClient(name string) (*http.Client, error) { if info.TlsClientCa != "" { caCert, err := os.ReadFile(info.TlsClientCa) if err != nil { - logger.Error("Failed to setup TlsClientCa", "oauth", name, "error", err) + ss.log.Error("Failed to setup TlsClientCa", "oauth", name, "error", err) return nil, fmt.Errorf("failed to setup TlsClientCa: %w", err) } caCertPool := x509.NewCertPool()