[MM-58549] Desktop login (#27732)
* desktop oauth and saml login --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -1979,7 +1979,15 @@ func loginWithDesktopToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, false, false)
|
isOAuthUser := user.IsOAuthUser()
|
||||||
|
isSamlUser := user.IsSAMLUser()
|
||||||
|
|
||||||
|
if !isOAuthUser && !isSamlUser {
|
||||||
|
c.Err = model.NewAppError("loginWithDesktopToken", "api.user.login_with_desktop_token.not_oauth_or_saml_user.app_error", nil, "", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, isOAuthUser, isSamlUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7776,3 +7776,75 @@ func TestUserUpdateEvents(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoginWithDesktopToken(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
t.Run("login SAML User with desktop token", func(t *testing.T) {
|
||||||
|
samlUser := th.CreateUserWithAuth(model.UserAuthServiceSaml)
|
||||||
|
|
||||||
|
token, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), samlUser)
|
||||||
|
assert.Nil(t, appErr)
|
||||||
|
|
||||||
|
user, _, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, samlUser.Id, user.Id)
|
||||||
|
|
||||||
|
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), samlUser.Id, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, sessions, 1)
|
||||||
|
assert.Equal(t, "true", sessions[0].Props["isSaml"])
|
||||||
|
assert.Equal(t, "false", sessions[0].Props["isOAuthUser"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("login OAuth User with desktop token", func(t *testing.T) {
|
||||||
|
gitlabUser := th.CreateUserWithAuth(model.UserAuthServiceGitlab)
|
||||||
|
|
||||||
|
token, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), gitlabUser)
|
||||||
|
assert.Nil(t, appErr)
|
||||||
|
|
||||||
|
user, _, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, gitlabUser.Id, user.Id)
|
||||||
|
|
||||||
|
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), gitlabUser.Id, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, sessions, 1)
|
||||||
|
assert.Equal(t, "false", sessions[0].Props["isSaml"])
|
||||||
|
assert.Equal(t, "true", sessions[0].Props["isOAuthUser"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("login email user with desktop token", func(t *testing.T) {
|
||||||
|
// Sleep to avoid rate limit error
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
user := th.CreateUser()
|
||||||
|
|
||||||
|
token, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
||||||
|
assert.Nil(t, appErr)
|
||||||
|
|
||||||
|
_, resp, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckUnauthorizedStatus(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid desktop token on login", func(t *testing.T) {
|
||||||
|
user := th.CreateUser()
|
||||||
|
|
||||||
|
_, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
||||||
|
assert.Nil(t, appErr)
|
||||||
|
|
||||||
|
invalidToken := "testinvalidToken"
|
||||||
|
token := &invalidToken
|
||||||
|
|
||||||
|
_, _, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), user.Id, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, sessions, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -339,7 +339,45 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else if action == model.OAuthActionSSOToEmail {
|
} else if action == model.OAuthActionSSOToEmail {
|
||||||
redirectURL = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
|
redirectURL = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
|
||||||
} else {
|
} else {
|
||||||
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, false)
|
desktopToken := ""
|
||||||
|
if val, ok := props["desktop_token"]; ok {
|
||||||
|
desktopToken = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a desktop login we generate a token and redirect to another endpoint to handle session creation
|
||||||
|
if desktopToken != "" {
|
||||||
|
serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
||||||
|
if serverTokenErr != nil {
|
||||||
|
serverTokenErr.Translate(c.AppContext.T)
|
||||||
|
c.LogErrorByCode(serverTokenErr)
|
||||||
|
renderError(serverTokenErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queryString := map[string]string{
|
||||||
|
"client_token": desktopToken,
|
||||||
|
"server_token": *serverToken,
|
||||||
|
}
|
||||||
|
if val, ok := props["redirect_to"]; ok {
|
||||||
|
queryString["redirect_to"] = val
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(desktopToken, "dev-") {
|
||||||
|
queryString["isDesktopDev"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString)
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
c.LogAudit("success")
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isOAuthUser := user.IsOAuthUser()
|
||||||
|
|
||||||
|
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, isOAuthUser, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err.Translate(c.AppContext.T)
|
err.Translate(c.AppContext.T)
|
||||||
c.Logger.Error(err.Error())
|
c.Logger.Error(err.Error())
|
||||||
@@ -372,34 +410,6 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// For web
|
// For web
|
||||||
c.App.AttachSessionCookies(c.AppContext, w, r)
|
c.App.AttachSessionCookies(c.AppContext, w, r)
|
||||||
|
|
||||||
desktopToken := ""
|
|
||||||
if val, ok := props["desktop_token"]; ok {
|
|
||||||
desktopToken = val
|
|
||||||
}
|
|
||||||
|
|
||||||
if desktopToken != "" {
|
|
||||||
serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
|
||||||
if serverTokenErr != nil {
|
|
||||||
serverTokenErr.Translate(c.AppContext.T)
|
|
||||||
c.LogErrorByCode(serverTokenErr)
|
|
||||||
renderError(serverTokenErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queryString := map[string]string{
|
|
||||||
"client_token": desktopToken,
|
|
||||||
"server_token": *serverToken,
|
|
||||||
}
|
|
||||||
if val, ok := props["redirect_to"]; ok {
|
|
||||||
queryString["redirect_to"] = val
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(desktopToken, "dev-") {
|
|
||||||
queryString["isDesktopDev"] = "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
|
|||||||
@@ -178,19 +178,9 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.AddMeta("obtained_user_id", user.Id)
|
auditRec.AddMeta("obtained_user_id", user.Id)
|
||||||
c.LogAuditWithUserId(user.Id, "obtained user")
|
c.LogAuditWithUserId(user.Id, "obtained user")
|
||||||
|
|
||||||
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.AppContext = c.AppContext.WithSession(session)
|
|
||||||
|
|
||||||
auditRec.Success()
|
|
||||||
c.LogAuditWithUserId(user.Id, "success")
|
|
||||||
|
|
||||||
c.App.AttachSessionCookies(c.AppContext, w, r)
|
|
||||||
|
|
||||||
desktopToken := relayProps["desktop_token"]
|
desktopToken := relayProps["desktop_token"]
|
||||||
|
|
||||||
|
// If it's a desktop login we generate a token and redirect to another endpoint to handle session creation
|
||||||
if desktopToken != "" {
|
if desktopToken != "" {
|
||||||
serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
||||||
if serverTokenErr != nil {
|
if serverTokenErr != nil {
|
||||||
@@ -214,6 +204,18 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it's not a desktop login we create a session for this SAML User that will be used in their browser or mobile app
|
||||||
|
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.AppContext = c.AppContext.WithSession(session)
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
c.LogAuditWithUserId(user.Id, "success")
|
||||||
|
c.App.AttachSessionCookies(c.AppContext, w, r)
|
||||||
|
|
||||||
if hasRedirectURL {
|
if hasRedirectURL {
|
||||||
if isMobile {
|
if isMobile {
|
||||||
// Mobile clients with redirect url support
|
// Mobile clients with redirect url support
|
||||||
|
|||||||
@@ -4066,6 +4066,10 @@
|
|||||||
"id": "api.user.login_ldap.not_available.app_error",
|
"id": "api.user.login_ldap.not_available.app_error",
|
||||||
"translation": "AD/LDAP not available on this server."
|
"translation": "AD/LDAP not available on this server."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.user.login_with_desktop_token.not_oauth_or_saml_user.app_error",
|
||||||
|
"translation": "User is not an OAuth or SAML user."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.oauth_to_email.context.app_error",
|
"id": "api.user.oauth_to_email.context.app_error",
|
||||||
"translation": "Update password failed because context user_id did not match provided user's id."
|
"translation": "Update password failed because context user_id did not match provided user's id."
|
||||||
|
|||||||
@@ -848,6 +848,25 @@ func (c *Client4) login(ctx context.Context, m map[string]string) (*User, *Respo
|
|||||||
return &user, BuildResponse(r), nil
|
return &user, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client4) LoginWithDesktopToken(ctx context.Context, token, deviceId string) (*User, *Response, error) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
m["token"] = token
|
||||||
|
m["deviceId"] = deviceId
|
||||||
|
r, err := c.DoAPIPost(ctx, "/users/login/desktop_token", MapToJSON(m))
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
c.AuthToken = r.Header.Get(HeaderToken)
|
||||||
|
c.AuthType = HeaderBearer
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||||
|
return nil, nil, NewAppError("loginWithDesktopToken", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
return &user, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Logout terminates the current user's session.
|
// Logout terminates the current user's session.
|
||||||
func (c *Client4) Logout(ctx context.Context) (*Response, error) {
|
func (c *Client4) Logout(ctx context.Context) (*Response, error) {
|
||||||
r, err := c.DoAPIPost(ctx, "/users/logout", "")
|
r, err := c.DoAPIPost(ctx, "/users/logout", "")
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user