diff --git a/api4/user.go b/api4/user.go index e7a9760e03..b4a9c2dbd2 100644 --- a/api4/user.go +++ b/api4/user.go @@ -1691,11 +1691,12 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { enableUsername := *config.EmailSettings.EnableSignInWithUsername enableEmail := *config.EmailSettings.EnableSignInWithEmail samlEnabled := *config.SamlSettings.Enable - gitlabEnabled := *config.GetSSOService("gitlab").Enable - googleEnabled := *config.GetSSOService("google").Enable + gitlabEnabled := *config.GitLabSettings.Enable + openidEnabled := *config.OpenIdSettings.Enable + googleEnabled := *config.GoogleSettings.Enable office365Enabled := *config.Office365Settings.Enable - if samlEnabled || gitlabEnabled || googleEnabled || office365Enabled { + if samlEnabled || gitlabEnabled || googleEnabled || office365Enabled || openidEnabled { c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_sso", nil, "", http.StatusUnauthorized) return } diff --git a/app/app_iface.go b/app/app_iface.go index 60f8652b67..6776391d76 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -376,7 +376,7 @@ type AppIface interface { AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError AttachSessionCookies(w http.ResponseWriter, r *http.Request) AuthenticateUserForLogin(id, loginId, password, mfaToken, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) - AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) + AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) AutocompleteUsersInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) @@ -412,8 +412,8 @@ type AppIface interface { Cluster() einterfaces.ClusterInterface CompareAndDeletePluginKey(pluginId string, key string, oldValue []byte) (bool, *model.AppError) CompareAndSetPluginKey(pluginId string, key string, oldValue, newValue []byte) (bool, *model.AppError) - CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string) (*model.User, *model.AppError) - CompleteSwitchWithOAuth(service string, userData io.Reader, email string) (*model.User, *model.AppError) + CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) + CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) Compliance() einterfaces.ComplianceInterface Config() *model.Config Context() context.Context @@ -429,7 +429,7 @@ type AppIface interface { CreateJob(job *model.Job) (*model.Job, *model.AppError) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) - CreateOAuthUser(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) + CreateOAuthUser(service string, userData io.Reader, teamId string, tokenUser *model.User) (*model.User, *model.AppError) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) CreatePasswordRecoveryToken(userId, email string) (*model.Token, *model.AppError) CreatePost(post *model.Post, channel *model.Channel, triggerWebhooks, setOnline bool) (savedPost *model.Post, err *model.AppError) @@ -778,7 +778,7 @@ type AppIface interface { ListPluginKeys(pluginId string, page, perPage int) ([]string, *model.AppError) ListTeamCommands(teamId string) ([]*model.Command, *model.AppError) Log() *mlog.Logger - LoginByOAuth(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) + LoginByOAuth(service string, userData io.Reader, teamId string, tokenUser *model.User) (*model.User, *model.AppError) MakePermissionError(permissions []*model.Permission) *model.AppError MarkChannelsAsViewed(channelIds []string, userId string, currentSessionId string) (map[string]int64, *model.AppError) MaxPostSize() int @@ -982,7 +982,7 @@ type AppIface interface { UpdateLastActivityAtIfNeeded(session model.Session) UpdateMfa(activate bool, userId, token string) *model.AppError UpdateMobileAppBadge(userId string) - UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string) *model.AppError + UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string, tokenUser *model.User) *model.AppError UpdateOauthApp(oldApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) UpdateOutgoingWebhook(oldHook, updatedHook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) UpdatePassword(user *model.User, newPassword string) *model.AppError diff --git a/app/oauth.go b/app/oauth.go index b0bf8d494b..cfb6eedf1a 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -26,6 +26,7 @@ import ( const ( OAUTH_COOKIE_MAX_AGE_SECONDS = 30 * 60 // 30 minutes COOKIE_OAUTH = "MMOAUTH" + OPENID_SCOPE = "openid" ) func (a *App) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) { @@ -544,52 +545,69 @@ func (a *App) RevokeAccessToken(token string) *model.AppError { return nil } -func (a *App) CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string) (*model.User, *model.AppError) { +func (a *App) CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) { defer body.Close() action := props["action"] switch action { case model.OAUTH_ACTION_SIGNUP: - return a.CreateOAuthUser(service, body, teamId) + return a.CreateOAuthUser(service, body, teamId, tokenUser) case model.OAUTH_ACTION_LOGIN: - return a.LoginByOAuth(service, body, teamId) + return a.LoginByOAuth(service, body, teamId, tokenUser) case model.OAUTH_ACTION_EMAIL_TO_SSO: - return a.CompleteSwitchWithOAuth(service, body, props["email"]) + return a.CompleteSwitchWithOAuth(service, body, props["email"], tokenUser) case model.OAUTH_ACTION_SSO_TO_EMAIL: - return a.LoginByOAuth(service, body, teamId) + return a.LoginByOAuth(service, body, teamId, tokenUser) default: - return a.LoginByOAuth(service, body, teamId) + return a.LoginByOAuth(service, body, teamId, tokenUser) } } -func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) { - provider := einterfaces.GetOauthProvider(service) +func (a *App) getSSOProvider(service string) (einterfaces.OauthProvider, *model.AppError) { + sso := a.Config().GetSSOService(service) + if sso == nil || !*sso.Enable { + return nil, model.NewAppError("getSSOProvider", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented) + } + providerType := service + if strings.Contains(*sso.Scope, OPENID_SCOPE) { + providerType = model.SERVICE_OPENID + } + provider := einterfaces.GetOauthProvider(providerType) if provider == nil { - return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.not_available.app_error", + return nil, model.NewAppError("getSSOProvider", "api.user.login_by_oauth.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented) } + return provider, nil +} + +func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string, tokenUser *model.User) (*model.User, *model.AppError) { + provider, e := a.getSSOProvider(service) + if e != nil { + return nil, e + } buf := bytes.Buffer{} if _, err := buf.ReadFrom(userData); err != nil { - return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error", + return nil, model.NewAppError("LoginByOAuth2", "api.user.login_by_oauth.parse.app_error", map[string]interface{}{"Service": service}, "", http.StatusBadRequest) } - authUser, err1 := provider.GetUserFromJson(bytes.NewReader(buf.Bytes())) + authUser, err1 := provider.GetUserFromJson(bytes.NewReader(buf.Bytes()), tokenUser) if err1 != nil { return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error", map[string]interface{}{"Service": service}, err1.Error(), http.StatusBadRequest) } if *authUser.AuthData == "" { - return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error", + return nil, model.NewAppError("LoginByOAuth3", "api.user.login_by_oauth.parse.app_error", map[string]interface{}{"Service": service}, "", http.StatusBadRequest) } + user, err := a.GetUserByAuth(model.NewString(*authUser.AuthData), service) if err != nil { if err.Id == MISSING_AUTH_ACCOUNT_ERROR { - user, err = a.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), teamId) + user, err = a.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), teamId, tokenUser) } else { return nil, err } @@ -601,7 +619,7 @@ func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string) (* return nil, model.NewAppError("loginByOAuth", "api.user.login_by_oauth.bot_login_forbidden.app_error", nil, "", http.StatusForbidden) } - if err = a.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil { + if err = a.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service, tokenUser); err != nil { return nil, err } if len(teamId) > 0 { @@ -616,18 +634,17 @@ func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string) (* return user, nil } -func (a *App) CompleteSwitchWithOAuth(service string, userData io.Reader, email string) (*model.User, *model.AppError) { - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error", - map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented) +func (a *App) CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) { + provider, e := a.getSSOProvider(service) + if e != nil { + return nil, e } if email == "" { return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "", http.StatusBadRequest) } - ssoUser, err1 := provider.GetUserFromJson(userData) + ssoUser, err1 := provider.GetUserFromJson(userData, tokenUser) if err1 != nil { return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error", map[string]interface{}{"Service": service}, err1.Error(), http.StatusBadRequest) @@ -696,9 +713,14 @@ func (a *App) GetOAuthStateToken(token string) (*model.Token, *model.AppError) { } func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) { - sso := a.Config().GetSSOService(service) - if sso == nil || !*sso.Enable { - return "", model.NewAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented) + provider, e := a.getSSOProvider(service) + if e != nil { + return "", e + } + + sso, e2 := provider.GetSSOSettings(a.Config(), service) + if e2 != nil { + return "", model.NewAppError("GetAuthorizationCode.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, e.Error(), http.StatusNotImplemented) } secure := false @@ -755,40 +777,44 @@ func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, servi return authUrl, nil } -func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) { - sso := a.Config().GetSSOService(service) - if sso == nil || !*sso.Enable { - return nil, "", nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented) +func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) { + provider, e := a.getSSOProvider(service) + if e != nil { + return nil, "", nil, nil, e + } + + sso, e2 := provider.GetSSOSettings(a.Config(), service) + if e2 != nil { + return nil, "", nil, nil, model.NewAppError("GetAuthorizationCode.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, e.Error(), http.StatusNotImplemented) } b, strErr := b64.StdEncoding.DecodeString(state) if strErr != nil { - return nil, "", nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, strErr.Error(), http.StatusBadRequest) + return nil, "", nil, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, strErr.Error(), http.StatusBadRequest) } stateStr := string(b) - stateProps := model.MapFromJson(strings.NewReader(stateStr)) expectedToken, appErr := a.GetOAuthStateToken(stateProps["token"]) if appErr != nil { - return nil, "", stateProps, appErr + return nil, "", stateProps, nil, appErr } stateEmail := stateProps["email"] stateAction := stateProps["action"] if stateAction == model.OAUTH_ACTION_EMAIL_TO_SSO && stateEmail == "" { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest) } cookie, cookieErr := r.Cookie(COOKIE_OAUTH) if cookieErr != nil { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest) } expectedTokenExtra := generateOAuthStateTokenExtra(stateEmail, stateAction, cookie.Value) if expectedTokenExtra != expectedToken.Extra { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest) } appErr = a.DeleteToken(expectedToken) @@ -819,7 +845,7 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service req, requestErr := http.NewRequest("POST", *sso.TokenEndpoint, strings.NewReader(p.Encode())) if requestErr != nil { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, requestErr.Error(), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, requestErr.Error(), http.StatusInternalServerError) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -827,31 +853,39 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service resp, err := a.HTTPService().MakeClient(true).Do(req) if err != nil { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error(), http.StatusInternalServerError) } defer resp.Body.Close() var buf bytes.Buffer tee := io.TeeReader(resp.Body, &buf) ar := model.AccessResponseFromJson(tee) - if ar == nil || resp.StatusCode != http.StatusOK { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, fmt.Sprintf("response_body=%s, status_code=%d", buf.String(), resp.StatusCode), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, fmt.Sprintf("response_body=%s, status_code=%d", buf.String(), resp.StatusCode), http.StatusInternalServerError) } if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+buf.String(), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+buf.String(), http.StatusInternalServerError) } if len(ar.AccessToken) == 0 { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "response_body="+buf.String(), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "response_body="+buf.String(), http.StatusInternalServerError) } p = url.Values{} p.Set("access_token", ar.AccessToken) + + var userFromToken *model.User + if ar.IdToken != "" { + userFromToken, err = provider.GetUserFromIdToken(ar.IdToken) + if err != nil { + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, e.Error(), http.StatusInternalServerError) + } + } + req, requestErr = http.NewRequest("GET", *sso.UserApiEndpoint, strings.NewReader("")) if requestErr != nil { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, requestErr.Error(), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, requestErr.Error(), http.StatusInternalServerError) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -860,7 +894,7 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service resp, err = a.HTTPService().MakeClient(true).Do(req) if err != nil { - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, err.Error(), http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, err.Error(), http.StatusInternalServerError) } else if resp.StatusCode != http.StatusOK { defer resp.Body.Close() @@ -868,18 +902,18 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service bodyBytes, _ := ioutil.ReadAll(resp.Body) bodyString := string(bodyBytes) - mlog.Error("Error getting OAuth user", mlog.String("body_string", bodyString)) + mlog.Error("Error getting OAuth user", mlog.Int("response", resp.StatusCode), mlog.String("body_string", bodyString)) if service == model.SERVICE_GITLAB && resp.StatusCode == http.StatusForbidden && strings.Contains(bodyString, "Terms of Service") { // Return a nicer error when the user hasn't accepted GitLab's terms of service - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "oauth.gitlab.tos.error", nil, "", http.StatusBadRequest) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "oauth.gitlab.tos.error", nil, "", http.StatusBadRequest) } - return nil, "", stateProps, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.response.app_error", nil, "response_body="+bodyString, http.StatusInternalServerError) + return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.response.app_error", nil, "response_body="+bodyString, http.StatusInternalServerError) } // Note that resp.Body is not closed here, so it must be closed by the caller - return resp.Body, teamId, stateProps, nil + return resp.Body, teamId, stateProps, userFromToken, nil } func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError) { diff --git a/app/oauth_test.go b/app/oauth_test.go index 60e28db747..93a8452d04 100644 --- a/app/oauth_test.go +++ b/app/oauth_test.go @@ -191,7 +191,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { th := setup(t, false, true, true, "") defer th.TearDown() - _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", "", "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", "", "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.unsupported.app_error", err.Id) }) @@ -202,7 +202,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { state := "!" - _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) @@ -215,7 +215,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { "token": model.NewId(), }))) - _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.oauth.invalid_state_token.app_error", err.Id) assert.NotEqual(t, "", err.DetailedError) @@ -230,7 +230,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { state := makeState(token) - _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.oauth.invalid_state_token.app_error", err.Id) assert.Equal(t, "", err.DetailedError) @@ -253,7 +253,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { "token": token.Token, }))) - _, _, _, err = th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err = th.App.AuthorizeOAuthUser(nil, nil, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) @@ -266,7 +266,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, "") state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(nil, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(nil, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) @@ -283,7 +283,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(token) - _, _, _, err = th.App.AuthorizeOAuthUser(nil, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err = th.App.AuthorizeOAuthUser(nil, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.invalid_state.app_error", err.Id) }) @@ -296,7 +296,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.token_failed.app_error", err.Id) }) @@ -314,7 +314,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_response.app_error", err.Id) assert.Contains(t, err.DetailedError, "status_code=418") @@ -333,7 +333,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_response.app_error", err.Id) assert.Contains(t, err.DetailedError, "response_body=invalid") @@ -355,7 +355,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.bad_token.app_error", err.Id) }) @@ -376,7 +376,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.missing.app_error", err.Id) }) @@ -397,7 +397,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.service.app_error", err.Id) }) @@ -425,7 +425,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "api.user.authorize_oauth_user.response.app_error", err.Id) }) @@ -454,7 +454,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { request := makeRequest(t, cookie) state := makeState(makeToken(th, cookie)) - _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") + _, _, _, _, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, err) assert.Equal(t, "oauth.gitlab.tos.error", err.Id) }) @@ -504,7 +504,7 @@ func TestAuthorizeOAuthUser(t *testing.T) { state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) recorder := httptest.ResponseRecorder{} - body, receivedTeamId, receivedStateProps, err := th.App.AuthorizeOAuthUser(&recorder, request, model.SERVICE_GITLAB, "", state, "") + body, receivedTeamId, receivedStateProps, _, err := th.App.AuthorizeOAuthUser(&recorder, request, model.SERVICE_GITLAB, "", state, "") require.NotNil(t, body) bodyBytes, bodyErr := ioutil.ReadAll(body) @@ -533,7 +533,8 @@ func TestGetAuthorizationCode(t *testing.T) { _, err := th.App.GetAuthorizationCode(nil, nil, model.SERVICE_GITLAB, map[string]string{}, "") require.NotNil(t, err) - assert.Equal(t, "api.user.get_authorization_code.unsupported.app_error", err.Id) + + assert.Equal(t, "api.user.authorize_oauth_user.unsupported.app_error", err.Id) }) t.Run("enabled and properly configured", func(t *testing.T) { diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index a2f6d4a26d..7749db6fd2 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -687,7 +687,7 @@ func (a *OpenTracingAppLayer) AuthenticateUserForLogin(id string, loginId string return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service string, code string, state string, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) { +func (a *OpenTracingAppLayer) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service string, code string, state string, redirectUri string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthorizeOAuthUser") @@ -699,14 +699,14 @@ func (a *OpenTracingAppLayer) AuthorizeOAuthUser(w http.ResponseWriter, r *http. }() defer span.Finish() - resultVar0, resultVar1, resultVar2, resultVar3 := a.app.AuthorizeOAuthUser(w, r, service, code, state, redirectUri) + resultVar0, resultVar1, resultVar2, resultVar3, resultVar4 := a.app.AuthorizeOAuthUser(w, r, service, code, state, redirectUri) - if resultVar3 != nil { - span.LogFields(spanlog.Error(resultVar3)) + if resultVar4 != nil { + span.LogFields(spanlog.Error(resultVar4)) ext.Error.Set(span, true) } - return resultVar0, resultVar1, resultVar2, resultVar3 + return resultVar0, resultVar1, resultVar2, resultVar3, resultVar4 } func (a *OpenTracingAppLayer) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) { @@ -1427,7 +1427,7 @@ func (a *OpenTracingAppLayer) CompareAndSetPluginKey(pluginId string, key string return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string) (*model.User, *model.AppError) { +func (a *OpenTracingAppLayer) CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteOAuth") @@ -1439,7 +1439,7 @@ func (a *OpenTracingAppLayer) CompleteOAuth(service string, body io.ReadCloser, }() defer span.Finish() - resultVar0, resultVar1 := a.app.CompleteOAuth(service, body, teamId, props) + resultVar0, resultVar1 := a.app.CompleteOAuth(service, body, teamId, props, tokenUser) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -1449,7 +1449,7 @@ func (a *OpenTracingAppLayer) CompleteOAuth(service string, body io.ReadCloser, return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) CompleteSwitchWithOAuth(service string, userData io.Reader, email string) (*model.User, *model.AppError) { +func (a *OpenTracingAppLayer) CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteSwitchWithOAuth") @@ -1461,7 +1461,7 @@ func (a *OpenTracingAppLayer) CompleteSwitchWithOAuth(service string, userData i }() defer span.Finish() - resultVar0, resultVar1 := a.app.CompleteSwitchWithOAuth(service, userData, email) + resultVar0, resultVar1 := a.app.CompleteSwitchWithOAuth(service, userData, email, tokenUser) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -1932,7 +1932,7 @@ func (a *OpenTracingAppLayer) CreateOAuthStateToken(extra string) (*model.Token, return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) CreateOAuthUser(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) { +func (a *OpenTracingAppLayer) CreateOAuthUser(service string, userData io.Reader, teamId string, tokenUser *model.User) (*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthUser") @@ -1944,7 +1944,7 @@ func (a *OpenTracingAppLayer) CreateOAuthUser(service string, userData io.Reader }() defer span.Finish() - resultVar0, resultVar1 := a.app.CreateOAuthUser(service, userData, teamId) + resultVar0, resultVar1 := a.app.CreateOAuthUser(service, userData, teamId, tokenUser) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -10392,7 +10392,7 @@ func (a *OpenTracingAppLayer) LogAuditRecWithLevel(rec *audit.Record, level mlog a.app.LogAuditRecWithLevel(rec, level, err) } -func (a *OpenTracingAppLayer) LoginByOAuth(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) { +func (a *OpenTracingAppLayer) LoginByOAuth(service string, userData io.Reader, teamId string, tokenUser *model.User) (*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LoginByOAuth") @@ -10404,7 +10404,7 @@ func (a *OpenTracingAppLayer) LoginByOAuth(service string, userData io.Reader, t }() defer span.Finish() - resultVar0, resultVar1 := a.app.LoginByOAuth(service, userData, teamId) + resultVar0, resultVar1 := a.app.LoginByOAuth(service, userData, teamId, tokenUser) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -14864,7 +14864,7 @@ func (a *OpenTracingAppLayer) UpdateMobileAppBadge(userId string) { a.app.UpdateMobileAppBadge(userId) } -func (a *OpenTracingAppLayer) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string) *model.AppError { +func (a *OpenTracingAppLayer) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string, tokenUser *model.User) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOAuthUserAttrs") @@ -14876,7 +14876,7 @@ func (a *OpenTracingAppLayer) UpdateOAuthUserAttrs(userData io.Reader, user *mod }() defer span.Finish() - resultVar0 := a.app.UpdateOAuthUserAttrs(userData, user, provider, service) + resultVar0 := a.app.UpdateOAuthUserAttrs(userData, user, provider, service, tokenUser) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) diff --git a/app/user.go b/app/user.go index e1cf755963..5d305a1c1c 100644 --- a/app/user.go +++ b/app/user.go @@ -335,19 +335,22 @@ func (a *App) createUser(user *model.User) (*model.User, *model.AppError) { return ruser, nil } -func (a *App) CreateOAuthUser(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) { +func (a *App) CreateOAuthUser(service string, userData io.Reader, teamId string, tokenUser *model.User) (*model.User, *model.AppError) { if !*a.Config().TeamSettings.EnableUserCreation { return nil, model.NewAppError("CreateOAuthUser", "api.user.create_user.disabled.app_error", nil, "", http.StatusNotImplemented) } - provider := einterfaces.GetOauthProvider(service) - if provider == nil { - return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented) + provider, e := a.getSSOProvider(service) + if e != nil { + return nil, e } - user, err1 := provider.GetUserFromJson(userData) + user, err1 := provider.GetUserFromJson(userData, tokenUser) if err1 != nil { return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, err1.Error(), http.StatusInternalServerError) } + if user.AuthService == "" { + user.AuthService = service + } suchan := make(chan store.StoreResult, 1) euchan := make(chan store.StoreResult, 1) @@ -1990,8 +1993,8 @@ func (a *App) AutocompleteUsersInTeam(teamId string, term string, options *model return autocomplete, nil } -func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string) *model.AppError { - oauthUser, err1 := provider.GetUserFromJson(userData) +func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string, tokenUser *model.User) *model.AppError { + oauthUser, err1 := provider.GetUserFromJson(userData, tokenUser) if err1 != nil { return model.NewAppError("UpdateOAuthUserAttrs", "api.user.update_oauth_user_attrs.get_user.app_error", map[string]interface{}{"Service": service}, err1.Error(), http.StatusBadRequest) } diff --git a/app/user_test.go b/app/user_test.go index e68216e366..10ff5933be 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -76,10 +76,14 @@ func TestCreateOAuthUser(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.GitLabSettings.Enable = true + }) + glUser := oauthgitlab.GitLabUser{Id: 42, Username: "o" + model.NewId(), Email: model.NewId() + "@simulator.amazonses.com", Name: "Joram Wilander"} json := glUser.ToJson() - user, err := th.App.CreateOAuthUser(model.USER_AUTH_SERVICE_GITLAB, strings.NewReader(json), th.BasicTeam.Id) + user, err := th.App.CreateOAuthUser(model.USER_AUTH_SERVICE_GITLAB, strings.NewReader(json), th.BasicTeam.Id, nil) require.Nil(t, err) require.Equal(t, glUser.Username, user.Username, "usernames didn't match") @@ -88,7 +92,7 @@ func TestCreateOAuthUser(t *testing.T) { *th.App.Config().TeamSettings.EnableUserCreation = false - _, err = th.App.CreateOAuthUser(model.USER_AUTH_SERVICE_GITLAB, strings.NewReader(json), th.BasicTeam.Id) + _, err = th.App.CreateOAuthUser(model.USER_AUTH_SERVICE_GITLAB, strings.NewReader(json), th.BasicTeam.Id, nil) require.NotNil(t, err, "should have failed - user creation disabled") } @@ -269,6 +273,9 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { id := model.NewId() id2 := model.NewId() + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.GitLabSettings.Enable = true + }) gitlabProvider := einterfaces.GetOauthProvider("gitlab") username := "user" + id @@ -289,7 +296,7 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { data := bytes.NewReader(gitlabUser) user = getUserFromDB(th.App, user.Id, t) - th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab") + th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab", nil) user = getUserFromDB(th.App, user.Id, t) require.Equal(t, gitlabUserObj.Username, user.Username, "user's username is not updated") @@ -302,7 +309,7 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { data := bytes.NewReader(gitlabUser) user = getUserFromDB(th.App, user.Id, t) - th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab") + th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab", nil) user = getUserFromDB(th.App, user.Id, t) require.NotEqual(t, gitlabUserObj.Username, user.Username, "user's username is updated though there already exists another user with the same username") @@ -316,7 +323,7 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { data := bytes.NewReader(gitlabUser) user = getUserFromDB(th.App, user.Id, t) - th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab") + th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab", nil) user = getUserFromDB(th.App, user.Id, t) require.Equal(t, gitlabUserObj.Email, user.Email, "user's email is not updated") @@ -331,7 +338,7 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { data := bytes.NewReader(gitlabUser) user = getUserFromDB(th.App, user.Id, t) - th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab") + th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab", nil) user = getUserFromDB(th.App, user.Id, t) require.NotEqual(t, gitlabUserObj.Email, user.Email, "user's email is updated though there already exists another user with the same email") @@ -344,7 +351,7 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { data := bytes.NewReader(gitlabUser) user = getUserFromDB(th.App, user.Id, t) - th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab") + th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab", nil) user = getUserFromDB(th.App, user.Id, t) require.Equal(t, "Updated", user.FirstName, "user's first name is not updated") @@ -356,7 +363,7 @@ func TestUpdateOAuthUserAttrs(t *testing.T) { data := bytes.NewReader(gitlabUser) user = getUserFromDB(th.App, user.Id, t) - th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab") + th.App.UpdateOAuthUserAttrs(data, user, gitlabProvider, "gitlab", nil) user = getUserFromDB(th.App, user.Id, t) require.Equal(t, "Lastname", user.LastName, "user's last name is not updated") @@ -532,7 +539,7 @@ func createGitlabUser(t *testing.T, a *App, id int64, username string, email str var user *model.User var err *model.AppError - user, err = a.CreateOAuthUser("gitlab", bytes.NewReader(gitlabUser), "") + user, err = a.CreateOAuthUser("gitlab", bytes.NewReader(gitlabUser), "", nil) require.Nil(t, err, "unable to create the user", err) return user, gitlabUserObj diff --git a/config/client.go b/config/client.go index 846e64d067..45ed9c3e2d 100644 --- a/config/client.go +++ b/config/client.go @@ -288,6 +288,9 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m props["SamlLoginButtonTextColor"] = "" props["EnableSignUpWithGoogle"] = "false" props["EnableSignUpWithOffice365"] = "false" + props["EnableSignUpWithOpenId"] = "false" + props["OpenIdButtonText"] = "" + props["OpenIdButtonColor"] = "" props["CWSUrl"] = "" props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand) props["CustomBrandText"] = *c.TeamSettings.CustomBrandText @@ -322,6 +325,12 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable) } + if *license.Features.OpenId { + props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable) + props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor + props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText + } + if *license.Features.CustomTermsOfService { props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled) props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10) diff --git a/config/utils.go b/config/utils.go index 7e7998f86f..4bbaa1fdda 100644 --- a/config/utils.go +++ b/config/utils.go @@ -42,6 +42,10 @@ func desanitize(actual, target *model.Config) { target.Office365Settings.Secret = actual.Office365Settings.Secret } + if target.OpenIdSettings.Secret != nil && *target.OpenIdSettings.Secret == model.FAKE_SETTING { + target.OpenIdSettings.Secret = actual.OpenIdSettings.Secret + } + if *target.SqlSettings.DataSource == model.FAKE_SETTING { *target.SqlSettings.DataSource = *actual.SqlSettings.DataSource } diff --git a/config/utils_test.go b/config/utils_test.go index 08f2c54415..03393cad5c 100644 --- a/config/utils_test.go +++ b/config/utils_test.go @@ -25,6 +25,7 @@ func TestDesanitize(t *testing.T) { actual.FileSettings.AmazonS3SecretAccessKey = sToP("amazon_s3_secret_access_key") actual.EmailSettings.SMTPPassword = sToP("smtp_password") actual.GitLabSettings.Secret = sToP("secret") + actual.OpenIdSettings.Secret = sToP("secret") actual.SqlSettings.DataSource = sToP("data_source") actual.SqlSettings.AtRestEncryptKey = sToP("at_rest_encrypt_key") actual.ElasticsearchSettings.Password = sToP("password") @@ -46,6 +47,7 @@ func TestDesanitize(t *testing.T) { target.FileSettings.AmazonS3SecretAccessKey = sToP(model.FAKE_SETTING) target.EmailSettings.SMTPPassword = sToP(model.FAKE_SETTING) target.GitLabSettings.Secret = sToP(model.FAKE_SETTING) + target.OpenIdSettings.Secret = sToP(model.FAKE_SETTING) target.SqlSettings.DataSource = sToP(model.FAKE_SETTING) target.SqlSettings.AtRestEncryptKey = sToP(model.FAKE_SETTING) target.ElasticsearchSettings.Password = sToP(model.FAKE_SETTING) @@ -66,6 +68,7 @@ func TestDesanitize(t *testing.T) { assert.Equal(t, *actual.FileSettings.AmazonS3SecretAccessKey, *target.FileSettings.AmazonS3SecretAccessKey) assert.Equal(t, *actual.EmailSettings.SMTPPassword, *target.EmailSettings.SMTPPassword) assert.Equal(t, *actual.GitLabSettings.Secret, *target.GitLabSettings.Secret) + assert.Equal(t, *actual.OpenIdSettings.Secret, *target.OpenIdSettings.Secret) assert.Equal(t, *actual.SqlSettings.DataSource, *target.SqlSettings.DataSource) assert.Equal(t, *actual.SqlSettings.AtRestEncryptKey, *target.SqlSettings.AtRestEncryptKey) assert.Equal(t, *actual.ElasticsearchSettings.Password, *target.ElasticsearchSettings.Password) diff --git a/einterfaces/oauthproviders.go b/einterfaces/oauthproviders.go index d33b7d2d37..def190c78b 100644 --- a/einterfaces/oauthproviders.go +++ b/einterfaces/oauthproviders.go @@ -10,7 +10,9 @@ import ( ) type OauthProvider interface { - GetUserFromJson(data io.Reader) (*model.User, error) + GetUserFromJson(data io.Reader, tokenUser *model.User) (*model.User, error) + GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) + GetUserFromIdToken(idToken string) (*model.User, error) } var oauthProviders = make(map[string]OauthProvider) diff --git a/i18n/en.json b/i18n/en.json index 25d218d278..e489c7ba48 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3148,7 +3148,7 @@ }, { "id": "api.user.authorize_oauth_user.unsupported.app_error", - "translation": "Unsupported OAuth service provider." + "translation": "{{.Service}} SSO through OAuth 2.0 not available on this server." }, { "id": "api.user.autocomplete_users.missing_team_id.app_error", @@ -3174,10 +3174,6 @@ "id": "api.user.complete_switch_with_oauth.parse.app_error", "translation": "Could not parse auth data out of {{.Service}} user object." }, - { - "id": "api.user.complete_switch_with_oauth.unavailable.app_error", - "translation": "{{.Service}} SSO through OAuth 2.0 not available on this server." - }, { "id": "api.user.create_email_token.error", "translation": "Failed to create token data for email verification" @@ -3190,10 +3186,6 @@ "id": "api.user.create_oauth_user.create.app_error", "translation": "Could not create user out of {{.Service}} user object." }, - { - "id": "api.user.create_oauth_user.not_available.app_error", - "translation": "{{.Service}} SSO through OAuth 2.0 not available on this server." - }, { "id": "api.user.create_password_token.error", "translation": "Unable to create password recovery token" @@ -3271,8 +3263,8 @@ "translation": "Authentication Transfer not configured or available on this server." }, { - "id": "api.user.get_authorization_code.unsupported.app_error", - "translation": "Unsupported OAuth service provider." + "id": "api.user.get_authorization_code.endpoint.app_error", + "translation": "" }, { "id": "api.user.get_uploads_for_user.forbidden.app_error", diff --git a/model/access.go b/model/access.go index bbac36016d..d6b06f4dfb 100644 --- a/model/access.go +++ b/model/access.go @@ -31,6 +31,7 @@ type AccessResponse struct { ExpiresIn int32 `json:"expires_in"` Scope string `json:"scope"` RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` } // IsValid validates the AccessData and returns an error if it isn't configured diff --git a/model/config.go b/model/config.go index 94e7ee5c47..5ae21a088f 100644 --- a/model/config.go +++ b/model/config.go @@ -45,6 +45,7 @@ const ( SERVICE_GITLAB = "gitlab" SERVICE_GOOGLE = "google" SERVICE_OFFICE365 = "office365" + SERVICE_OPENID = "openid" GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel" GENERIC_NOTIFICATION = "generic" @@ -230,6 +231,7 @@ const ( OFFICE365_SETTINGS_DEFAULT_USER_API_ENDPOINT = "https://graph.microsoft.com/v1.0/me" CLOUD_SETTINGS_DEFAULT_CWS_URL = "https://customers.mattermost.com" + OPENID_SETTINGS_DEFAULT_SCOPE = "profile openid email" LOCAL_MODE_SOCKET_PATH = "/var/tmp/mattermost_local.socket" ) @@ -960,16 +962,19 @@ func (s *AnalyticsSettings) SetDefaults() { } type SSOSettings struct { - Enable *bool `access:"authentication"` - Secret *string `access:"authentication"` - Id *string `access:"authentication"` - Scope *string `access:"authentication"` - AuthEndpoint *string `access:"authentication"` - TokenEndpoint *string `access:"authentication"` - UserApiEndpoint *string `access:"authentication"` + Enable *bool `access:"authentication"` + Secret *string `access:"authentication"` + Id *string `access:"authentication"` + Scope *string `access:"authentication"` + AuthEndpoint *string `access:"authentication"` + TokenEndpoint *string `access:"authentication"` + UserApiEndpoint *string `access:"authentication"` + DiscoveryEndpoint *string `access:"authentication"` + ButtonText *string `access:"authentication"` + ButtonColor *string `access:"authentication"` } -func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userApiEndpoint string) { +func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userApiEndpoint, buttonColor string) { if s.Enable == nil { s.Enable = NewBool(false) } @@ -986,6 +991,10 @@ func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userApiEnd s.Scope = NewString(scope) } + if s.DiscoveryEndpoint == nil { + s.DiscoveryEndpoint = NewString("") + } + if s.AuthEndpoint == nil { s.AuthEndpoint = NewString(authEndpoint) } @@ -997,17 +1006,26 @@ func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userApiEnd if s.UserApiEndpoint == nil { s.UserApiEndpoint = NewString(userApiEndpoint) } + + if s.ButtonText == nil { + s.ButtonText = NewString("") + } + + if s.ButtonColor == nil { + s.ButtonColor = NewString(buttonColor) + } } type Office365Settings struct { - Enable *bool `access:"authentication"` - Secret *string `access:"authentication"` - Id *string `access:"authentication"` - Scope *string `access:"authentication"` - AuthEndpoint *string `access:"authentication"` - TokenEndpoint *string `access:"authentication"` - UserApiEndpoint *string `access:"authentication"` - DirectoryId *string `access:"authentication"` + Enable *bool `access:"authentication"` + Secret *string `access:"authentication"` + Id *string `access:"authentication"` + Scope *string `access:"authentication"` + AuthEndpoint *string `access:"authentication"` + TokenEndpoint *string `access:"authentication"` + UserApiEndpoint *string `access:"authentication"` + DiscoveryEndpoint *string `access:"authentication"` + DirectoryId *string `access:"authentication"` } func (s *Office365Settings) setDefaults() { @@ -1027,6 +1045,10 @@ func (s *Office365Settings) setDefaults() { s.Scope = NewString(OFFICE365_SETTINGS_DEFAULT_SCOPE) } + if s.DiscoveryEndpoint == nil { + s.DiscoveryEndpoint = NewString("") + } + if s.AuthEndpoint == nil { s.AuthEndpoint = NewString(OFFICE365_SETTINGS_DEFAULT_AUTH_ENDPOINT) } @@ -1050,6 +1072,7 @@ func (s *Office365Settings) SSOSettings() *SSOSettings { ssoSettings.Secret = s.Secret ssoSettings.Id = s.Id ssoSettings.Scope = s.Scope + ssoSettings.DiscoveryEndpoint = s.DiscoveryEndpoint ssoSettings.AuthEndpoint = s.AuthEndpoint ssoSettings.TokenEndpoint = s.TokenEndpoint ssoSettings.UserApiEndpoint = s.UserApiEndpoint @@ -2948,6 +2971,7 @@ type Config struct { GitLabSettings SSOSettings GoogleSettings SSOSettings Office365Settings Office365Settings + OpenIdSettings SSOSettings LdapSettings LdapSettings ComplianceSettings ComplianceSettings LocalizationSettings LocalizationSettings @@ -3004,6 +3028,8 @@ func (o *Config) GetSSOService(service string) *SSOSettings { return &o.GoogleSettings case SERVICE_OFFICE365: return o.Office365Settings.SSOSettings() + case SERVICE_OPENID: + return &o.OpenIdSettings } return nil @@ -3039,8 +3065,10 @@ func (o *Config) SetDefaults() { o.EmailSettings.SetDefaults(isUpdate) o.PrivacySettings.setDefaults() o.Office365Settings.setDefaults() - o.GitLabSettings.setDefaults("", "", "", "") - o.GoogleSettings.setDefaults(GOOGLE_SETTINGS_DEFAULT_SCOPE, GOOGLE_SETTINGS_DEFAULT_AUTH_ENDPOINT, GOOGLE_SETTINGS_DEFAULT_TOKEN_ENDPOINT, GOOGLE_SETTINGS_DEFAULT_USER_API_ENDPOINT) + o.Office365Settings.setDefaults() + o.GitLabSettings.setDefaults("", "", "", "", "") + o.GoogleSettings.setDefaults(GOOGLE_SETTINGS_DEFAULT_SCOPE, GOOGLE_SETTINGS_DEFAULT_AUTH_ENDPOINT, GOOGLE_SETTINGS_DEFAULT_TOKEN_ENDPOINT, GOOGLE_SETTINGS_DEFAULT_USER_API_ENDPOINT, "") + o.OpenIdSettings.setDefaults(OPENID_SETTINGS_DEFAULT_SCOPE, "", "", "", "#145DBF") o.ServiceSettings.SetDefaults(isUpdate) o.PasswordSettings.SetDefaults() o.TeamSettings.SetDefaults() @@ -3691,6 +3719,10 @@ func (o *Config) Sanitize() { *o.Office365Settings.Secret = FAKE_SETTING } + if o.OpenIdSettings.Secret != nil && len(*o.OpenIdSettings.Secret) > 0 { + *o.OpenIdSettings.Secret = FAKE_SETTING + } + *o.SqlSettings.DataSource = FAKE_SETTING *o.SqlSettings.AtRestEncryptKey = FAKE_SETTING diff --git a/model/config_test.go b/model/config_test.go index 9db0758f2f..3d80a621d1 100644 --- a/model/config_test.go +++ b/model/config_test.go @@ -1322,6 +1322,7 @@ func TestConfigSanitize(t *testing.T) { *c.FileSettings.AmazonS3SecretAccessKey = "bar" *c.EmailSettings.SMTPPassword = "baz" *c.GitLabSettings.Secret = "bingo" + *c.OpenIdSettings.Secret = "secret" c.SqlSettings.DataSourceReplicas = []string{"stuff"} c.SqlSettings.DataSourceSearchReplicas = []string{"stuff"} @@ -1332,6 +1333,7 @@ func TestConfigSanitize(t *testing.T) { assert.Equal(t, FAKE_SETTING, *c.FileSettings.AmazonS3SecretAccessKey) assert.Equal(t, FAKE_SETTING, *c.EmailSettings.SMTPPassword) assert.Equal(t, FAKE_SETTING, *c.GitLabSettings.Secret) + assert.Equal(t, FAKE_SETTING, *c.OpenIdSettings.Secret) assert.Equal(t, FAKE_SETTING, *c.SqlSettings.DataSource) assert.Equal(t, FAKE_SETTING, *c.SqlSettings.AtRestEncryptKey) assert.Equal(t, FAKE_SETTING, *c.ElasticsearchSettings.Password) diff --git a/model/gitlab/gitlab.go b/model/gitlab/gitlab.go index f100535497..c8f3ae7a5e 100644 --- a/model/gitlab/gitlab.go +++ b/model/gitlab/gitlab.go @@ -91,7 +91,7 @@ func (glu *GitLabUser) getAuthData() string { return strconv.FormatInt(glu.Id, 10) } -func (m *GitLabProvider) GetUserFromJson(data io.Reader) (*model.User, error) { +func (m *GitLabProvider) GetUserFromJson(data io.Reader, tokenUser *model.User) (*model.User, error) { glu, err := gitLabUserFromJson(data) if err != nil { return nil, err @@ -102,3 +102,11 @@ func (m *GitLabProvider) GetUserFromJson(data io.Reader) (*model.User, error) { return userFromGitLabUser(glu), nil } + +func (m *GitLabProvider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) { + return &config.GitLabSettings, nil +} + +func (m *GitLabProvider) GetUserFromIdToken(idToken string) (*model.User, error) { + return nil, nil +} diff --git a/model/license.go b/model/license.go index 3de4aba83e..f9de44a64e 100644 --- a/model/license.go +++ b/model/license.go @@ -63,6 +63,7 @@ type Features struct { MFA *bool `json:"mfa"` GoogleOAuth *bool `json:"google_oauth"` Office365OAuth *bool `json:"office365_oauth"` + OpenId *bool `json:"openid"` Compliance *bool `json:"compliance"` Cluster *bool `json:"cluster"` Metrics *bool `json:"metrics"` @@ -95,6 +96,7 @@ func (f *Features) ToMap() map[string]interface{} { "mfa": *f.MFA, "google": *f.GoogleOAuth, "office365": *f.Office365OAuth, + "openid": *f.OpenId, "compliance": *f.Compliance, "cluster": *f.Cluster, "metrics": *f.Metrics, @@ -145,6 +147,10 @@ func (f *Features) SetDefaults() { f.Office365OAuth = NewBool(*f.FutureFeatures) } + if f.OpenId == nil { + f.OpenId = NewBool(*f.FutureFeatures) + } + if f.Compliance == nil { f.Compliance = NewBool(*f.FutureFeatures) } diff --git a/model/switch_request.go b/model/switch_request.go index 0ec4db7d1e..bdb90045c8 100644 --- a/model/switch_request.go +++ b/model/switch_request.go @@ -34,14 +34,16 @@ func (o *SwitchRequest) EmailToOAuth() bool { (o.NewService == USER_AUTH_SERVICE_SAML || o.NewService == USER_AUTH_SERVICE_GITLAB || o.NewService == SERVICE_GOOGLE || - o.NewService == SERVICE_OFFICE365) + o.NewService == SERVICE_OFFICE365 || + o.NewService == SERVICE_OPENID) } func (o *SwitchRequest) OAuthToEmail() bool { return (o.CurrentService == USER_AUTH_SERVICE_SAML || o.CurrentService == USER_AUTH_SERVICE_GITLAB || o.CurrentService == SERVICE_GOOGLE || - o.CurrentService == SERVICE_OFFICE365) && o.NewService == USER_AUTH_SERVICE_EMAIL + o.CurrentService == SERVICE_OFFICE365 || + o.CurrentService == SERVICE_OPENID) && o.NewService == USER_AUTH_SERVICE_EMAIL } func (o *SwitchRequest) EmailToLdap() bool { diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index d083b414a2..d0fb5f8093 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -588,8 +588,12 @@ func (ts *TelemetryService) trackConfig() { ts.sendTelemetry(TRACK_CONFIG_OAUTH, map[string]interface{}{ "enable_gitlab": cfg.GitLabSettings.Enable, + "openid_gitlab": *cfg.GitLabSettings.Enable && strings.Contains(*cfg.GitLabSettings.Scope, model.SERVICE_OPENID), "enable_google": cfg.GoogleSettings.Enable, + "openid_google": *cfg.GoogleSettings.Enable && strings.Contains(*cfg.GoogleSettings.Scope, model.SERVICE_OPENID), "enable_office365": cfg.Office365Settings.Enable, + "openid_office365": *cfg.Office365Settings.Enable && strings.Contains(*cfg.Office365Settings.Scope, model.SERVICE_OPENID), + "enable_openid": cfg.OpenIdSettings.Enable, }) ts.sendTelemetry(TRACK_CONFIG_SUPPORT, map[string]interface{}{ diff --git a/utils/license.go b/utils/license.go index b4b350cad7..5e17f95053 100644 --- a/utils/license.go +++ b/utils/license.go @@ -139,6 +139,7 @@ func GetClientLicense(l *model.License) map[string]string { props["Metrics"] = strconv.FormatBool(*l.Features.Metrics) props["GoogleOAuth"] = strconv.FormatBool(*l.Features.GoogleOAuth) props["Office365OAuth"] = strconv.FormatBool(*l.Features.Office365OAuth) + props["OpenId"] = strconv.FormatBool(*l.Features.OpenId) props["Compliance"] = strconv.FormatBool(*l.Features.Compliance) props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS) props["Announcement"] = strconv.FormatBool(*l.Features.Announcement) diff --git a/web/oauth.go b/web/oauth.go index 1c68191662..534303a3c0 100644 --- a/web/oauth.go +++ b/web/oauth.go @@ -270,7 +270,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete" - body, teamId, props, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri) + body, teamId, props, tokenUser, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri) action := "" if props != nil { @@ -288,7 +288,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - user, err := c.App.CompleteOAuth(service, body, teamId, props) + user, err := c.App.CompleteOAuth(service, body, teamId, props, tokenUser) if err != nil { err.Translate(c.App.T) mlog.Error(err.Error()) diff --git a/web/oauth_test.go b/web/oauth_test.go index d9241f2c03..bc64568cba 100644 --- a/web/oauth_test.go +++ b/web/oauth_test.go @@ -549,12 +549,20 @@ func closeBody(r *http.Response) { type MattermostTestProvider struct { } -func (m *MattermostTestProvider) GetUserFromJson(data io.Reader) (*model.User, error) { +func (m *MattermostTestProvider) GetUserFromJson(data io.Reader, tokenUser *model.User) (*model.User, error) { user := model.UserFromJson(data) user.AuthData = &user.Email return user, nil } +func (m *MattermostTestProvider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) { + return &config.GitLabSettings, nil +} + +func (m *MattermostTestProvider) GetUserFromIdToken(token string) (*model.User, error) { + return nil, nil +} + func GenerateTestAppName() string { return "fakeoauthapp" + model.NewRandomString(10) }