Merge pull request #1745 from mattermost/plt-1118

PLT-1118 Add ability to switch between SSO and email account
This commit is contained in:
Christopher Speller
2016-01-04 15:55:51 -05:00
20 changed files with 1213 additions and 266 deletions

View File

@@ -0,0 +1,43 @@
{{define "signin_change_body"}}
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
<tr>
<td style="border: 1px solid #ddd;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You updated your sign-in method</h2>
<p>You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} to {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
</td>
</tr>
<tr>
{{template "email_info" . }}
</tr>
</table>
</td>
</tr>
<tr>
{{template "email_footer" . }}
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
{{end}}

View File

@@ -0,0 +1 @@
{{define "signin_change_subject"}}You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}}

View File

@@ -47,6 +47,8 @@ func InitUser(r *mux.Router) {
sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST")
sr.Handle("/login_ldap", ApiAppHandler(loginLdap)).Methods("POST")
sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST")
sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST")
sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST")
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
@@ -225,6 +227,70 @@ func CreateUser(team *model.Team, user *model.User) (*model.User, *model.AppErro
}
}
func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User {
var user *model.User
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
c.Err = model.NewAppError("CreateOAuthUser", service+" oauth not avlailable on this server", "")
return nil
} else {
user = provider.GetUserFromJson(userData)
}
if user == nil {
c.Err = model.NewAppError("CreateOAuthUser", "Could not create user out of "+service+" user object", "")
return nil
}
suchan := Srv.Store.User().GetByAuth(team.Id, user.AuthData, service)
euchan := Srv.Store.User().GetByEmail(team.Id, user.Email)
if team.Email == "" {
team.Email = user.Email
if result := <-Srv.Store.Team().Update(team); result.Err != nil {
c.Err = result.Err
return nil
}
} else {
found := true
count := 0
for found {
if found = IsUsernameTaken(user.Username, team.Id); c.Err != nil {
return nil
} else if found {
user.Username = user.Username + strconv.Itoa(count)
count += 1
}
}
}
if result := <-suchan; result.Err == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email)
return nil
}
if result := <-euchan; result.Err == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email)
return nil
}
user.TeamId = team.Id
user.EmailVerified = true
ruser, err := CreateUser(team, user)
if err != nil {
c.Err = err
return nil
}
Login(c, w, r, ruser, "")
if c.Err != nil {
return nil
}
return ruser
}
func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
go func() {
@@ -335,6 +401,11 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
} else {
user := result.Data.(*model.User)
if len(user.AuthData) != 0 {
c.Err = model.NewAppError("LoginByEmail", "Please sign in using "+user.AuthService, "")
return nil
}
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
Login(c, w, r, user, deviceId)
return user
@@ -344,10 +415,36 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
return nil
}
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User {
authData := ""
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
c.Err = model.NewAppError("LoginByOAuth", service+" oauth not avlailable on this server", "")
return nil
} else {
authData = provider.GetAuthDataFromJson(userData)
}
if len(authData) == 0 {
c.Err = model.NewAppError("LoginByOAuth", "Could not parse auth data out of "+service+" user object", "")
return nil
}
var user *model.User
if result := <-Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil {
c.Err = result.Err
return nil
} else {
user = result.Data.(*model.User)
Login(c, w, r, user, "")
return user
}
}
func checkUserLoginAttempts(c *Context, user *model.User) bool {
if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts {
c.LogAuditWithUserId(user.Id, "fail")
c.Err = model.NewAppError("checkUserPassword", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id)
c.Err = model.NewAppError("checkUserLoginAttempts", "Your account is locked because of too many failed password attempts. Please reset your password.", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden
return false
}
@@ -1660,21 +1757,22 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri, loginHint string) {
func GetAuthorizationCode(c *Context, service, teamName string, props map[string]string, loginHint string) (string, *model.AppError) {
sso := utils.Cfg.GetSSOService(service)
if sso != nil && !sso.Enable {
c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service)
c.Err.StatusCode = http.StatusBadRequest
return
return "", model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service)
}
clientId := sso.Id
endpoint := sso.AuthEndpoint
scope := sso.Scope
stateProps := map[string]string{"team": teamName, "hash": model.HashPassword(clientId)}
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
props["hash"] = model.HashPassword(clientId)
props["team"] = teamName
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
redirectUri := c.GetSiteURL() + "/" + service + "/complete"
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state)
@@ -1686,18 +1784,18 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, te
authUrl += "&login_hint=" + utils.UrlEncode(loginHint)
}
http.Redirect(w, r, authUrl, http.StatusFound)
return authUrl, nil
}
func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, *model.AppError) {
func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.Team, map[string]string, *model.AppError) {
sso := utils.Cfg.GetSSOService(service)
if sso == nil || !sso.Enable {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
}
stateStr := ""
if b, err := b64.StdEncoding.DecodeString(state); err != nil {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error())
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", err.Error())
} else {
stateStr = string(b)
}
@@ -1705,12 +1803,13 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
stateProps := model.MapFromJson(strings.NewReader(stateStr))
if !model.ComparePassword(stateProps["hash"], sso.Id) {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "")
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "")
}
teamName := stateProps["team"]
if len(teamName) == 0 {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "")
ok := true
teamName := ""
if teamName, ok = stateProps["team"]; !ok {
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state; missing team name", "")
}
tchan := Srv.Store.Team().GetByName(teamName)
@@ -1730,20 +1829,20 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
var ar *model.AccessResponse
if resp, err := client.Do(req); err != nil {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error())
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error())
} else {
ar = model.AccessResponseFromJson(resp.Body)
if ar == nil {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "")
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad response from token request", "")
}
}
if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType)
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType)
}
if len(ar.AccessToken) == 0 {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "")
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "")
}
p = url.Values{}
@@ -1755,12 +1854,12 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
if resp, err := client.Do(req); err != nil {
return nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error())
return nil, nil, nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error())
} else {
if result := <-tchan; result.Err != nil {
return nil, nil, result.Err
return nil, nil, nil, result.Err
} else {
return resp.Body, result.Data.(*model.Team), nil
return resp.Body, result.Data.(*model.Team), stateProps, nil
}
}
@@ -1780,3 +1879,200 @@ func IsUsernameTaken(name string, teamId string) bool {
return false
}
func switchToSSO(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
password := props["password"]
if len(password) == 0 {
c.SetInvalidParam("switchToSSO", "password")
return
}
teamName := props["team_name"]
if len(teamName) == 0 {
c.SetInvalidParam("switchToSSO", "team_name")
return
}
service := props["service"]
if len(service) == 0 {
c.SetInvalidParam("switchToSSO", "service")
return
}
email := props["email"]
if len(email) == 0 {
c.SetInvalidParam("switchToSSO", "email")
return
}
c.LogAudit("attempt")
var team *model.Team
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.LogAudit("fail - couldn't get team")
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
var user *model.User
if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.LogAudit("fail - couldn't get user")
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
if !checkUserLoginAttempts(c, user) || !checkUserPassword(c, user, password) {
c.LogAuditWithUserId(user.Id, "fail - invalid password")
return
}
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO
stateProps["email"] = email
m := map[string]string{}
if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
c.LogAuditWithUserId(user.Id, "fail - oauth issue")
c.Err = err
return
} else {
m["follow_link"] = authUrl
}
c.LogAuditWithUserId(user.Id, "success")
w.Write([]byte(model.MapToJson(m)))
}
func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team, email string) {
authData := ""
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
c.Err = model.NewAppError("CompleteClaimWithOAuth", service+" oauth not avlailable on this server", "")
return
} else {
authData = provider.GetAuthDataFromJson(userData)
}
if len(authData) == 0 {
c.Err = model.NewAppError("CompleteClaimWithOAuth", "Could not parse auth data out of "+service+" user object", "")
return
}
if len(email) == 0 {
c.Err = model.NewAppError("CompleteClaimWithOAuth", "Blank email", "")
return
}
var user *model.User
if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
RevokeAllSession(c, user.Id)
if c.Err != nil {
return
}
if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData); result.Err != nil {
c.Err = result.Err
return
}
sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO")
}
func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
password := props["password"]
if len(password) == 0 {
c.SetInvalidParam("switchToEmail", "password")
return
}
teamName := props["team_name"]
if len(teamName) == 0 {
c.SetInvalidParam("switchToEmail", "team_name")
return
}
email := props["email"]
if len(email) == 0 {
c.SetInvalidParam("switchToEmail", "email")
return
}
c.LogAudit("attempt")
var team *model.Team
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.LogAudit("fail - couldn't get team")
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
var user *model.User
if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.LogAudit("fail - couldn't get user")
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
if user.Id != c.Session.UserId {
c.LogAudit("fail - user ids didn't match")
c.Err = model.NewAppError("switchToEmail", "Update password failed because context user_id did not match provided user's id", "")
c.Err.StatusCode = http.StatusForbidden
return
}
if result := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil {
c.LogAudit("fail - database issue")
c.Err = result.Err
return
}
sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "email and password")
RevokeAllSession(c, c.Session.UserId)
if c.Err != nil {
return
}
m := map[string]string{}
m["follow_link"] = c.GetTeamURL() + "/login?extra=signin_change"
c.LogAudit("success")
w.Write([]byte(model.MapToJson(m)))
}
func sendSignInChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
subjectPage := NewServerTemplatePage("signin_change_subject")
subjectPage.Props["SiteURL"] = siteURL
subjectPage.Props["TeamDisplayName"] = teamDisplayName
bodyPage := NewServerTemplatePage("signin_change_body")
bodyPage.Props["SiteURL"] = siteURL
bodyPage.Props["TeamDisplayName"] = teamDisplayName
bodyPage.Props["TeamURL"] = teamURL
bodyPage.Props["Method"] = method
if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
l4g.Error("Failed to send update password email successfully err=%v", err)
}
}()
}

View File

@@ -1085,3 +1085,106 @@ func TestStatuses(t *testing.T) {
}
}
func TestSwitchToSSO(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
m := map[string]string{}
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - empty data")
}
m["password"] = "pwd"
_, err := Client.SwitchToSSO(m)
if err == nil {
t.Fatal("should have failed - missing team_name, service, email")
}
m["team_name"] = team.Name
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - missing service, email")
}
m["service"] = "someservice"
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - missing email")
}
m["team_name"] = "junk"
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - bad team name")
}
m["team_name"] = team.Name
m["email"] = "junk"
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - bad email")
}
m["email"] = ruser.Email
m["password"] = "junk"
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - bad password")
}
}
func TestSwitchToEmail(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
user2 := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
m := map[string]string{}
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - not logged in")
}
Client.LoginByEmail(team.Name, user.Email, user.Password)
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - empty data")
}
m["password"] = "pwd"
_, err := Client.SwitchToSSO(m)
if err == nil {
t.Fatal("should have failed - missing team_name, service, email")
}
m["team_name"] = team.Name
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - missing email")
}
m["email"] = ruser.Email
m["team_name"] = "junk"
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - bad team name")
}
m["team_name"] = team.Name
m["email"] = "junk"
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - bad email")
}
m["email"] = ruser2.Email
if _, err := Client.SwitchToSSO(m); err == nil {
t.Fatal("should have failed - wrong user")
}
}

View File

@@ -100,4 +100,4 @@
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
}
}

View File

@@ -349,6 +349,24 @@ func (c *Client) GetSessions(id string) (*Result, *AppError) {
}
}
func (c *Client) SwitchToSSO(m map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/switch_to_sso", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) SwitchToEmail(m map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/switch_to_email", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) Command(channelId string, command string, suggest bool) (*Result, *AppError) {
m := make(map[string]string)
m["command"] = command

View File

@@ -10,6 +10,13 @@ import (
"unicode/utf8"
)
const (
OAUTH_ACTION_SIGNUP = "signup"
OAUTH_ACTION_LOGIN = "login"
OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso"
OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email"
)
type OAuthApp struct {
Id string `json:"id"`
CreatorId string `json:"creator_id"`

View File

@@ -266,7 +266,7 @@ func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChanne
updateAt := model.GetMillis()
if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0 WHERE Id = :UserId AND AuthData = ''", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil {
if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, AuthData = '', AuthService = '', FailedAttempts = 0 WHERE Id = :UserId", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil {
result.Err = model.NewAppError("SqlUserStore.UpdatePassword", "We couldn't update the user password", "id="+userId+", "+err.Error())
} else {
result.Data = userId
@@ -298,6 +298,28 @@ func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int)
return storeChannel
}
func (us SqlUserStore) UpdateAuthData(userId, service, authData string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
updateAt := model.GetMillis()
if _, err := us.GetMaster().Exec("UPDATE Users SET Password = '', LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0, AuthService = :AuthService, AuthData = :AuthData WHERE Id = :UserId", map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData}); err != nil {
result.Err = model.NewAppError("SqlUserStore.UpdateAuthData", "We couldn't update the auth data", "id="+userId+", "+err.Error())
} else {
result.Data = userId
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) Get(id string) StoreChannel {
storeChannel := make(StoreChannel)

View File

@@ -390,3 +390,34 @@ func TestUserStoreDelete(t *testing.T) {
t.Fatal(err)
}
}
func TestUserStoreUpdateAuthData(t *testing.T) {
Setup()
u1 := model.User{}
u1.TeamId = model.NewId()
u1.Email = model.NewId()
Must(store.User().Save(&u1))
service := "someservice"
authData := "1"
if err := (<-store.User().UpdateAuthData(u1.Id, service, authData)).Err; err != nil {
t.Fatal(err)
}
if r1 := <-store.User().GetByEmail(u1.TeamId, u1.Email); r1.Err != nil {
t.Fatal(r1.Err)
} else {
user := r1.Data.(*model.User)
if user.AuthService != service {
t.Fatal("AuthService was not updated correctly")
}
if user.AuthData != authData {
t.Fatal("AuthData was not updated correctly")
}
if user.Password != "" {
t.Fatal("Password was not cleared properly")
}
}
}

View File

@@ -111,6 +111,7 @@ type UserStore interface {
UpdateLastActivityAt(userId string, time int64) StoreChannel
UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
UpdatePassword(userId, newPassword string) StoreChannel
UpdateAuthData(userId, service, authData string) StoreChannel
Get(id string) StoreChannel
GetProfiles(teamId string) StoreChannel
GetByEmail(teamId string, email string) StoreChannel

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import EmailToSSO from './email_to_sso.jsx';
import SSOToEmail from './sso_to_email.jsx';
export default class ClaimAccount extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let content;
if (this.props.email === '') {
content = <p>{'No email specified.'}</p>;
} else if (this.props.currentType === '' && this.props.newType !== '') {
content = (
<EmailToSSO
email={this.props.email}
type={this.props.newType}
teamName={this.props.teamName}
teamDisplayName={this.props.teamDisplayName}
/>
);
} else {
content = (
<SSOToEmail
email={this.props.email}
currentType={this.props.currentType}
teamName={this.props.teamName}
teamDisplayName={this.props.teamDisplayName}
/>
);
}
return (
<div>
{content}
</div>
);
}
}
ClaimAccount.defaultProps = {
};
ClaimAccount.propTypes = {
currentType: React.PropTypes.string.isRequired,
newType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
};

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
import * as Client from '../../utils/client.jsx';
export default class EmailToSSO extends React.Component {
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
this.state = {};
}
submit(e) {
e.preventDefault();
var state = {};
var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password) {
state.error = 'Please enter your password.';
this.setState(state);
return;
}
state.error = null;
this.setState(state);
var postData = {};
postData.password = password;
postData.email = this.props.email;
postData.team_name = this.props.teamName;
postData.service = this.props.type;
Client.switchToSSO(postData,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
},
(error) => {
this.setState({error});
}
);
}
render() {
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
}
var formClass = 'form-group';
if (error) {
formClass += ' has-error';
}
const uiType = Utils.toTitleCase(this.props.type) + ' SSO';
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>{'Switch Email/Password Account to ' + uiType}</h3>
<form onSubmit={this.submit}>
<p>{'Upon claiming your account, you will only be able to login with ' + Utils.toTitleCase(this.props.type) + ' SSO.'}</p>
<p>{'Enter the password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p>
<div className={formClass}>
<input
type='password'
className='form-control'
name='password'
ref='password'
placeholder='Password'
spellCheck='false'
/>
</div>
{error}
<button
type='submit'
className='btn btn-primary'
>
{'Switch account to ' + uiType}
</button>
</form>
</div>
</div>
);
}
}
EmailToSSO.defaultProps = {
};
EmailToSSO.propTypes = {
type: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
};

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
import * as Client from '../../utils/client.jsx';
export default class SSOToEmail extends React.Component {
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
this.state = {};
}
submit(e) {
e.preventDefault();
const state = {};
const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password) {
state.error = 'Please enter a password.';
this.setState(state);
return;
}
const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim();
if (!confirmPassword || password !== confirmPassword) {
state.error = 'Passwords do not match.';
this.setState(state);
return;
}
state.error = null;
this.setState(state);
var postData = {};
postData.password = password;
postData.email = this.props.email;
postData.team_name = this.props.teamName;
Client.switchToEmail(postData,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
},
(error) => {
this.setState({error});
}
);
}
render() {
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
}
var formClass = 'form-group';
if (error) {
formClass += ' has-error';
}
const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO';
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>{'Switch ' + uiType + ' Account to Email'}</h3>
<form onSubmit={this.submit}>
<p>{'Upon changing your account type, you will only be able to login with your email and password.'}</p>
<p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p>
<div className={formClass}>
<input
type='password'
className='form-control'
name='password'
ref='password'
placeholder='New Password'
spellCheck='false'
/>
</div>
<div className={formClass}>
<input
type='password'
className='form-control'
name='passwordconfirm'
ref='passwordconfirm'
placeholder='Confirm Password'
spellCheck='false'
/>
</div>
{error}
<button
type='submit'
className='btn btn-primary'
>
{'Switch ' + uiType + ' account to email and password'}
</button>
</form>
</div>
</div>
);
}
}
SSOToEmail.defaultProps = {
};
SSOToEmail.propTypes = {
currentType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
};

View File

@@ -1,10 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
import LoginEmail from './login_email.jsx';
import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
export default class Login extends React.Component {
constructor(props) {
super(props);
@@ -40,15 +42,24 @@ export default class Login extends React.Component {
);
}
const verifiedParam = Utils.getUrlParameter('verified');
let verifiedBox = '';
if (verifiedParam) {
verifiedBox = (
<div className='alert alert-success'>
<i className='fa fa-check' />
{' Email Verified'}
</div>
);
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
let msg;
if (extraParam === Constants.SIGNIN_CHANGE) {
msg = ' Sign-in method changed successfully';
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
msg = ' Email Verified';
}
if (msg != null) {
extraBox = (
<div className='alert alert-success'>
<i className='fa fa-check' />
{msg}
</div>
);
}
}
let emailSignup;
@@ -124,7 +135,7 @@ export default class Login extends React.Component {
<h5 className='margin--less'>{'Sign in to:'}</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2>
{verifiedBox}
{extraBox}
{loginMessage}
{emailSignup}
{ldapLogin}

View File

@@ -6,6 +6,9 @@ import SettingItemMax from '../setting_item_max.jsx';
import AccessHistoryModal from '../access_history_modal.jsx';
import ActivityLogModal from '../activity_log_modal.jsx';
import ToggleModalButton from '../toggle_modal_button.jsx';
import TeamStore from '../../stores/team_store.jsx';
import * as Client from '../../utils/client.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
import Constants from '../../utils/constants.jsx';
@@ -18,9 +21,19 @@ export default class SecurityTab extends React.Component {
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
this.setupInitialState = this.setupInitialState.bind(this);
this.getDefaultState = this.getDefaultState.bind(this);
this.createPasswordSection = this.createPasswordSection.bind(this);
this.createSignInSection = this.createSignInSection.bind(this);
this.state = this.setupInitialState();
this.state = this.getDefaultState();
}
getDefaultState() {
return {
currentPassword: '',
newPassword: '',
confirmPassword: '',
authService: this.props.user.auth_service
};
}
submitPassword(e) {
e.preventDefault();
@@ -51,13 +64,13 @@ export default class SecurityTab extends React.Component {
data.new_password = newPassword;
Client.updatePassword(data,
function success() {
() => {
this.props.updateSection('');
AsyncClient.getMe();
this.setState(this.setupInitialState());
}.bind(this),
function fail(err) {
var state = this.setupInitialState();
this.setState(this.getDefaultState());
},
(err) => {
var state = this.getDefaultState();
if (err.message) {
state.serverError = err.message;
} else {
@@ -65,7 +78,7 @@ export default class SecurityTab extends React.Component {
}
state.passwordError = '';
this.setState(state);
}.bind(this)
}
);
}
updateCurrentPassword(e) {
@@ -77,86 +90,60 @@ export default class SecurityTab extends React.Component {
updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value});
}
setupInitialState() {
return {currentPassword: '', newPassword: '', confirmPassword: ''};
}
render() {
var serverError;
if (this.state.serverError) {
serverError = this.state.serverError;
}
var passwordError;
if (this.state.passwordError) {
passwordError = this.state.passwordError;
}
createPasswordSection() {
let updateSectionStatus;
var updateSectionStatus;
var passwordSection;
if (this.props.activeSection === 'password') {
var inputs = [];
var submit = null;
if (this.props.activeSection === 'password' && this.props.user.auth_service === '') {
const inputs = [];
if (this.props.user.auth_service === '') {
inputs.push(
<div
key='currentPasswordUpdateForm'
className='form-group'
>
<label className='col-sm-5 control-label'>Current Password</label>
<div className='col-sm-7'>
<input
className='form-control'
type='password'
onChange={this.updateCurrentPassword}
value={this.state.currentPassword}
/>
</div>
inputs.push(
<div
key='currentPasswordUpdateForm'
className='form-group'
>
<label className='col-sm-5 control-label'>{'Current Password'}</label>
<div className='col-sm-7'>
<input
className='form-control'
type='password'
onChange={this.updateCurrentPassword}
value={this.state.currentPassword}
/>
</div>
);
inputs.push(
<div
key='newPasswordUpdateForm'
className='form-group'
>
<label className='col-sm-5 control-label'>New Password</label>
<div className='col-sm-7'>
<input
className='form-control'
type='password'
onChange={this.updateNewPassword}
value={this.state.newPassword}
/>
</div>
</div>
);
inputs.push(
<div
key='newPasswordUpdateForm'
className='form-group'
>
<label className='col-sm-5 control-label'>{'New Password'}</label>
<div className='col-sm-7'>
<input
className='form-control'
type='password'
onChange={this.updateNewPassword}
value={this.state.newPassword}
/>
</div>
);
inputs.push(
<div
key='retypeNewPasswordUpdateForm'
className='form-group'
>
<label className='col-sm-5 control-label'>Retype New Password</label>
<div className='col-sm-7'>
<input
className='form-control'
type='password'
onChange={this.updateConfirmPassword}
value={this.state.confirmPassword}
/>
</div>
</div>
);
inputs.push(
<div
key='retypeNewPasswordUpdateForm'
className='form-group'
>
<label className='col-sm-5 control-label'>{'Retype New Password'}</label>
<div className='col-sm-7'>
<input
className='form-control'
type='password'
onChange={this.updateConfirmPassword}
value={this.state.confirmPassword}
/>
</div>
);
submit = this.submitPassword;
} else {
inputs.push(
<div
key='oauthPasswordInfo'
className='form-group'
>
<label className='col-sm-12'>Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label>
</div>
);
}
</div>
);
updateSectionStatus = function resetSection(e) {
this.props.updateSection('');
@@ -164,51 +151,157 @@ export default class SecurityTab extends React.Component {
e.preventDefault();
}.bind(this);
passwordSection = (
return (
<SettingItemMax
title='Password'
inputs={inputs}
submit={submit}
server_error={serverError}
client_error={passwordError}
submit={this.submitPassword}
server_error={this.state.serverError}
client_error={this.state.passwordError}
updateSection={updateSectionStatus}
/>
);
} else {
var describe;
if (this.props.user.auth_service === '') {
var d = new Date(this.props.user.last_password_update);
var hour = '12';
if (d.getHours() % 12) {
hour = String(d.getHours() % 12);
}
var min = String(d.getMinutes());
if (d.getMinutes() < 10) {
min = '0' + d.getMinutes();
}
var timeOfDay = ' am';
if (d.getHours() >= 12) {
timeOfDay = ' pm';
}
}
describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay;
} else {
describe = 'Log in done through GitLab';
var describe;
var d = new Date(this.props.user.last_password_update);
var hour = '12';
if (d.getHours() % 12) {
hour = String(d.getHours() % 12);
}
var min = String(d.getMinutes());
if (d.getMinutes() < 10) {
min = '0' + d.getMinutes();
}
var timeOfDay = ' am';
if (d.getHours() >= 12) {
timeOfDay = ' pm';
}
describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay;
updateSectionStatus = function updateSection() {
this.props.updateSection('password');
}.bind(this);
return (
<SettingItemMin
title='Password'
describe={describe}
updateSection={updateSectionStatus}
/>
);
}
createSignInSection() {
let updateSectionStatus;
const user = this.props.user;
if (this.props.activeSection === 'signin') {
const inputs = [];
const teamName = TeamStore.getCurrent().name;
let emailOption;
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && user.auth_service !== '') {
emailOption = (
<div>
<a
className='btn btn-primary'
href={'/' + teamName + '/claim?email=' + user.email}
>
{'Switch to using email and password'}
</a>
<br/>
</div>
);
}
updateSectionStatus = function updateSection() {
this.props.updateSection('password');
let gitlabOption;
if (global.window.mm_config.EnableSignUpWithGitLab === 'true' && user.auth_service === '') {
gitlabOption = (
<div>
<a
className='btn btn-primary'
href={'/' + teamName + '/claim?email=' + user.email + '&new_type=' + Constants.GITLAB_SERVICE}
>
{'Switch to using GitLab SSO'}
</a>
<br/>
</div>
);
}
let googleOption;
if (global.window.mm_config.EnableSignUpWithGoogle === 'true' && user.auth_service === '') {
googleOption = (
<div>
<a
className='btn btn-primary'
href={'/' + teamName + '/claim?email=' + user.email + '&new_type=' + Constants.GOOGLE_SERVICE}
>
{'Switch to using Google SSO'}
</a>
<br/>
</div>
);
}
inputs.push(
<div key='userSignInOption'>
{emailOption}
{gitlabOption}
<br/>
{googleOption}
</div>
);
updateSectionStatus = function updateSection(e) {
this.props.updateSection('');
this.setState({serverError: null});
e.preventDefault();
}.bind(this);
passwordSection = (
<SettingItemMin
title='Password'
describe={describe}
const extraInfo = <span>{'You may only have one sign-in method at a time. Switching sign-in method will send an email notifying you if the change was successful.'}</span>;
return (
<SettingItemMax
title='Sign-in Method'
extraInfo={extraInfo}
inputs={inputs}
server_error={this.state.serverError}
updateSection={updateSectionStatus}
/>
);
}
updateSectionStatus = function updateSection() {
this.props.updateSection('signin');
}.bind(this);
let describe = 'Email and Password';
if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
describe = 'GitLab SSO';
}
return (
<SettingItemMin
title='Sign-in Method'
describe={describe}
updateSection={updateSectionStatus}
/>
);
}
render() {
const passwordSection = this.createPasswordSection();
let signInSection;
let numMethods = 0;
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) {
signInSection = this.createSignInSection();
}
return (
<div>
<div className='modal-header'>
@@ -233,9 +326,11 @@ export default class SecurityTab extends React.Component {
</h4>
</div>
<div className='user-settings'>
<h3 className='tab-header'>Security Settings</h3>
<h3 className='tab-header'>{'Security Settings'}</h3>
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-light'/>
{signInSection}
<div className='divider-dark'/>
<br></br>
<ToggleModalButton

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ClaimAccount from '../components/claim/claim_account.jsx';
function setupClaimAccountPage(props) {
ReactDOM.render(
<ClaimAccount
email={props.Email}
currentType={props.CurrentType}
newType={props.NewType}
teamName={props.TeamName}
teamDisplayName={props.TeamDisplayName}
/>,
document.getElementById('claim')
);
}
global.window.setup_claim_account_page = setupClaimAccountPage;

View File

@@ -228,6 +228,40 @@ export function resetPassword(data, success, error) {
track('api', 'api_users_reset_password');
}
export function switchToSSO(data, success, error) {
$.ajax({
url: '/api/v1/users/switch_to_sso',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
var e = handleError('switchToSSO', xhr, status, err);
error(e);
}
});
track('api', 'api_users_switch_to_sso');
}
export function switchToEmail(data, success, error) {
$.ajax({
url: '/api/v1/users/switch_to_email',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
var e = handleError('switchToEmail', xhr, status, err);
error(e);
}
});
track('api', 'api_users_switch_to_email');
}
export function logout() {
track('api', 'api_users_logout');
var currentTeamUrl = TeamStore.getCurrentTeamUrl();

View File

@@ -117,6 +117,8 @@ export default {
GITLAB_SERVICE: 'gitlab',
GOOGLE_SERVICE: 'google',
EMAIL_SERVICE: 'email',
SIGNIN_CHANGE: 'signin_change',
SIGNIN_VERIFIED: 'verified',
POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3,
POST_FOCUS_CONTEXT_RADIUS: 10,

View File

@@ -0,0 +1,16 @@
{{define "claim_account"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">
<div class="container-fluid">
<div class="inner__wrap">
<div class="row content" id="claim"></div>
</div>
</div>
<script>
window.setup_claim_account_page({{ .Props }});
</script>
</body>
</html>
{{end}}

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -71,8 +70,7 @@ func InitWeb() {
mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET")
mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET")
mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET")
mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET")
mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET")
mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET")
mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET")
@@ -92,6 +90,7 @@ func InitWeb() {
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET")
mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here.
@@ -565,7 +564,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
} else {
c.LogAudit("Email Verified")
http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?verified=true&email="+url.QueryEscape(email), http.StatusTemporaryRedirect)
http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect)
return
}
}
@@ -687,89 +686,63 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
}
redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete"
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_SIGNUP
api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri, "")
if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil {
c.Err = err
return
} else {
http.Redirect(w, r, authUrl, http.StatusFound)
}
}
func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
uri := c.GetSiteURL() + "/signup/" + service + "/complete"
uri := c.GetSiteURL() + "/" + service + "/complete"
if body, team, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err
return
} else {
var user *model.User
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
c.Err = model.NewAppError("signupCompleteOAuth", service+" oauth not avlailable on this server", "")
return
} else {
user = provider.GetUserFromJson(body)
}
if user == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "Could not create user out of "+service+" user object", "")
return
}
suchan := api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service)
euchan := api.Srv.Store.User().GetByEmail(team.Id, user.Email)
if team.Email == "" {
team.Email = user.Email
if result := <-api.Srv.Store.Team().Update(team); result.Err != nil {
c.Err = result.Err
return
action := props["action"]
switch action {
case model.OAUTH_ACTION_SIGNUP:
api.CreateOAuthUser(c, w, r, service, body, team)
if c.Err == nil {
root(c, w, r)
}
} else {
found := true
count := 0
for found {
if found = api.IsUsernameTaken(user.Username, team.Id); c.Err != nil {
return
} else if found {
user.Username = user.Username + strconv.Itoa(count)
count += 1
}
break
case model.OAUTH_ACTION_LOGIN:
api.LoginByOAuth(c, w, r, service, body, team)
if c.Err == nil {
root(c, w, r)
}
break
case model.OAUTH_ACTION_EMAIL_TO_SSO:
api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"])
if c.Err == nil {
http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_SSO_TO_EMAIL:
api.LoginByOAuth(c, w, r, service, body, team)
if c.Err == nil {
http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
}
break
default:
api.LoginByOAuth(c, w, r, service, body, team)
if c.Err == nil {
root(c, w, r)
}
break
}
if result := <-suchan; result.Err == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email)
return
}
if result := <-euchan; result.Err == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email)
return
}
user.TeamId = team.Id
user.EmailVerified = true
ruser, err := api.CreateUser(team, user)
if err != nil {
c.Err = err
return
}
api.Login(c, w, r, ruser, "")
if c.Err != nil {
return
}
page := NewHtmlTemplatePage("home", "Home")
page.Team = team
page.User = ruser
page.Render(c, w)
}
}
@@ -791,57 +764,14 @@ func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
redirectUri := c.GetSiteURL() + "/login/" + service + "/complete"
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_LOGIN
api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri, loginHint)
}
func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
uri := c.GetSiteURL() + "/login/" + service + "/complete"
if body, team, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil {
c.Err = err
return
} else {
authData := ""
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
c.Err = model.NewAppError("signupCompleteOAuth", service+" oauth not avlailable on this server", "")
return
} else {
authData = provider.GetAuthDataFromJson(body)
}
if len(authData) == 0 {
c.Err = model.NewAppError("loginCompleteOAuth", "Could not parse auth data out of "+service+" user object", "")
return
}
var user *model.User
if result := <-api.Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
api.Login(c, w, r, user, "")
if c.Err != nil {
return
}
page := NewHtmlTemplatePage("home", "Home")
page.Team = team
page.User = user
page.Render(c, w)
root(c, w, r)
}
http.Redirect(w, r, authUrl, http.StatusFound)
}
}
@@ -1172,3 +1102,58 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !CheckBrowserCompatability(c, r) {
return
}
params := mux.Vars(r)
teamName := params["team"]
email := r.URL.Query().Get("email")
newType := r.URL.Query().Get("new_type")
var team *model.Team
if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil {
l4g.Error("Couldn't find team name=%v, err=%v", teamName, tResult.Err.Message)
http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
return
} else {
team = tResult.Data.(*model.Team)
}
authType := ""
if len(email) != 0 {
if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil {
l4g.Error("Couldn't find user teamid=%v, email=%v, err=%v", team.Id, email, uResult.Err.Message)
http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
return
} else {
user := uResult.Data.(*model.User)
authType = user.AuthService
// if user is not logged in to their SSO account, ask them to log in
if len(authType) != 0 && user.Id != c.Session.UserId {
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL
stateProps["email"] = email
if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil {
c.Err = err
return
} else {
http.Redirect(w, r, authUrl, http.StatusFound)
}
}
}
}
page := NewHtmlTemplatePage("claim_account", "Claim Account")
page.Props["Email"] = email
page.Props["CurrentType"] = authType
page.Props["NewType"] = newType
page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = team.Name
page.Render(c, w)
}