* MM 65084 server-side (#33861) (#34006) Automatic Merge * Add ConsumeOnce method to store layers --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4b56488fcb
Коммит
375ce229f4
@@ -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 {
|
||||
|
||||
Ссылка в новой задаче
Block a user