* MM 65084 server-side (#33861) (#34006)

Automatic Merge

* Add ConsumeOnce method to store layers

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
JG Heithcock
2025-10-06 15:20:08 -07:00
коммит произвёл GitHub
родитель 4b56488fcb
Коммит 375ce229f4
11 изменённых файлов: 309 добавлений и 5 удалений

Просмотреть файл

@@ -4,6 +4,8 @@
package api4
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -63,6 +65,7 @@ func (api *API) InitUser() {
api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login", api.APIHandler(login)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/sso/code-exchange", api.APIHandler(loginSSOCodeExchange)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/desktop_token", api.RateLimitedHandler(api.APIHandler(loginWithDesktopToken), model.RateLimitSettings{PerSec: model.NewPointer(2), MaxBurst: model.NewPointer(1)})).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/switch", api.APIHandler(switchAccountType)).Methods(http.MethodPost)
api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).Methods(http.MethodPost)
@@ -110,6 +113,102 @@ func (api *API) InitUser() {
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods(http.MethodPost)
}
// loginSSOCodeExchange exchanges a short-lived login_code for session tokens (mobile SAML code exchange)
func loginSSOCodeExchange(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Config().FeatureFlags.MobileSSOCodeExchange {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "feature disabled", http.StatusBadRequest)
return
}
props := model.MapFromJSON(r.Body)
loginCode := props["login_code"]
codeVerifier := props["code_verifier"]
state := props["state"]
if loginCode == "" || codeVerifier == "" || state == "" {
c.SetInvalidParam("login_code | code_verifier | state")
return
}
// Consume one-time code atomically
token, appErr := c.App.ConsumeTokenOnce(loginCode)
if appErr != nil {
c.Err = appErr
return
}
// Check token expiration as fallback to cleanup process
if token.IsExpired() {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "token expired", http.StatusBadRequest)
return
}
// Parse extra JSON
extra := model.MapFromJSON(strings.NewReader(token.Extra))
userID := extra["user_id"]
codeChallenge := extra["code_challenge"]
method := strings.ToUpper(extra["code_challenge_method"])
expectedState := extra["state"]
if userID == "" || codeChallenge == "" || expectedState == "" {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "", http.StatusBadRequest)
return
}
if state != expectedState {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "state mismatch", http.StatusBadRequest)
return
}
// Verify SAML challenge
var computed string
switch strings.ToUpper(method) {
case "S256":
sum := sha256.Sum256([]byte(codeVerifier))
computed = base64.RawURLEncoding.EncodeToString(sum[:])
case "":
computed = codeVerifier
case "PLAIN":
// Explicitly reject plain method for security
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "plain SAML challenge method not supported",
http.StatusBadRequest)
return
default:
// Reject unknown methods
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "unsupported SAML challenge method", http.StatusBadRequest)
return
}
if computed != codeChallenge {
c.Err = model.NewAppError("loginSSOCodeExchange", "api.oauth.get_access_token.bad_request.app_error", nil, "SAML challenge mismatch", http.StatusBadRequest)
return
}
// Create session for this user
user, err := c.App.GetUser(userID)
if err != nil {
c.Err = err
return
}
isMobile := utils.IsMobileRequest(r)
session, err2 := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
if err2 != nil {
c.Err = err2
return
}
c.AppContext = c.AppContext.WithSession(session)
c.App.AttachSessionCookies(c.AppContext, w, r)
// Respond with tokens for mobile client to set
resp := map[string]string{
"token": session.Token,
"csrf": session.GetCSRF(),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {