* 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
@@ -74,6 +74,59 @@
|
|||||||
$ref: "#/components/responses/Unauthorized"
|
$ref: "#/components/responses/Unauthorized"
|
||||||
"403":
|
"403":
|
||||||
$ref: "#/components/responses/Forbidden"
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
/api/v4/users/login/sso/code-exchange:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
summary: Exchange SSO login code for session tokens
|
||||||
|
description: >
|
||||||
|
Exchange a short-lived login_code for session tokens using SAML code exchange (mobile SSO flow).
|
||||||
|
This endpoint is part of the mobile SSO code-exchange flow to prevent tokens
|
||||||
|
from appearing in deep links.
|
||||||
|
|
||||||
|
##### Permissions
|
||||||
|
|
||||||
|
No permission required.
|
||||||
|
operationId: LoginSSOCodeExchange
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- login_code
|
||||||
|
- code_verifier
|
||||||
|
- state
|
||||||
|
properties:
|
||||||
|
login_code:
|
||||||
|
description: Short-lived one-time code from SSO callback
|
||||||
|
type: string
|
||||||
|
code_verifier:
|
||||||
|
description: SAML verifier to prove code possession
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
description: State parameter to prevent CSRF attacks
|
||||||
|
type: string
|
||||||
|
description: SSO code exchange object
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Code exchange successful
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
description: Session token for authentication
|
||||||
|
type: string
|
||||||
|
csrf:
|
||||||
|
description: CSRF token for request validation
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
/api/v4/users/logout:
|
/api/v4/users/logout:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package api4
|
package api4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -63,6 +65,7 @@ func (api *API) InitUser() {
|
|||||||
api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods(http.MethodPost)
|
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", 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/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/switch", api.APIHandler(switchAccountType)).Methods(http.MethodPost)
|
||||||
api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).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)
|
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) {
|
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
var user model.User
|
var user model.User
|
||||||
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
|
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
|
||||||
|
|||||||
@@ -1750,6 +1750,21 @@ func (a *App) GetTokenById(token string) (*model.Token, *model.AppError) {
|
|||||||
return rtoken, nil
|
return rtoken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) ConsumeTokenOnce(tokenStr string) (*model.Token, *model.AppError) {
|
||||||
|
token, err := a.Srv().Store().Token().ConsumeOnce(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
var status int
|
||||||
|
switch err.(type) {
|
||||||
|
case *store.ErrNotFound:
|
||||||
|
status = http.StatusNotFound
|
||||||
|
default:
|
||||||
|
status = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
return nil, model.NewAppError("ConsumeTokenOnce", "api.user.create_user.signup_link_invalid.app_error", nil, "", status).Wrap(err)
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) DeleteToken(token *model.Token) *model.AppError {
|
func (a *App) DeleteToken(token *model.Token) *model.AppError {
|
||||||
err := a.Srv().Store().Token().Delete(token.Token)
|
err := a.Srv().Store().Token().Delete(token.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14138,6 +14138,27 @@ func (s *RetryLayerTokenStore) Cleanup(expiryTime int64) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerTokenStore) ConsumeOnce(tokenStr string) (*model.Token, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.TokenStore.ConsumeOnce(tokenStr)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerTokenStore) Delete(token string) error {
|
func (s *RetryLayerTokenStore) Delete(token string) error {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ func (s SqlTokenStore) GetByToken(tokenString string) (*model.Token, error) {
|
|||||||
return &token, nil
|
return &token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlTokenStore) ConsumeOnce(tokenStr string) (*model.Token, error) {
|
||||||
|
var token model.Token
|
||||||
|
|
||||||
|
query := `DELETE FROM Tokens WHERE Token = ? RETURNING *`
|
||||||
|
|
||||||
|
if err := s.GetMaster().Get(&token, query, tokenStr); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, store.NewErrNotFound("Token", tokenStr)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(err, "failed to consume token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &token, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s SqlTokenStore) Cleanup(expiryTime int64) {
|
func (s SqlTokenStore) Cleanup(expiryTime int64) {
|
||||||
if _, err := s.GetMaster().Exec("DELETE FROM Tokens WHERE CreateAt < ?", expiryTime); err != nil {
|
if _, err := s.GetMaster().Exec("DELETE FROM Tokens WHERE CreateAt < ?", expiryTime); err != nil {
|
||||||
mlog.Error("Unable to cleanup token store.")
|
mlog.Error("Unable to cleanup token store.")
|
||||||
|
|||||||
@@ -693,6 +693,7 @@ type TokenStore interface {
|
|||||||
Save(recovery *model.Token) error
|
Save(recovery *model.Token) error
|
||||||
Delete(token string) error
|
Delete(token string) error
|
||||||
GetByToken(token string) (*model.Token, error)
|
GetByToken(token string) (*model.Token, error)
|
||||||
|
ConsumeOnce(tokenStr string) (*model.Token, error)
|
||||||
Cleanup(expiryTime int64)
|
Cleanup(expiryTime int64)
|
||||||
GetAllTokensByType(tokenType string) ([]*model.Token, error)
|
GetAllTokensByType(tokenType string) ([]*model.Token, error)
|
||||||
RemoveAllTokensByType(tokenType string) error
|
RemoveAllTokensByType(tokenType string) error
|
||||||
|
|||||||
@@ -19,6 +19,36 @@ func (_m *TokenStore) Cleanup(expiryTime int64) {
|
|||||||
_m.Called(expiryTime)
|
_m.Called(expiryTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConsumeOnce provides a mock function with given fields: tokenStr
|
||||||
|
func (_m *TokenStore) ConsumeOnce(tokenStr string) (*model.Token, error) {
|
||||||
|
ret := _m.Called(tokenStr)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for ConsumeOnce")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 *model.Token
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) (*model.Token, error)); ok {
|
||||||
|
return rf(tokenStr)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *model.Token); ok {
|
||||||
|
r0 = rf(tokenStr)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.Token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(tokenStr)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Delete provides a mock function with given fields: token
|
// Delete provides a mock function with given fields: token
|
||||||
func (_m *TokenStore) Delete(token string) error {
|
func (_m *TokenStore) Delete(token string) error {
|
||||||
ret := _m.Called(token)
|
ret := _m.Called(token)
|
||||||
|
|||||||
@@ -11106,6 +11106,22 @@ func (s *TimerLayerTokenStore) Cleanup(expiryTime int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerTokenStore) ConsumeOnce(tokenStr string) (*model.Token, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.TokenStore.ConsumeOnce(tokenStr)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.ConsumeOnce", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerTokenStore) Delete(token string) error {
|
func (s *TimerLayerTokenStore) Delete(token string) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
action := r.URL.Query().Get("action")
|
action := r.URL.Query().Get("action")
|
||||||
isMobile := action == model.OAuthActionMobile
|
isMobile := action == model.OAuthActionMobile
|
||||||
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
|
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
|
||||||
|
// Optional SAML challenge parameters for mobile code-exchange
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
codeChallenge := r.URL.Query().Get("code_challenge")
|
||||||
|
codeChallengeMethod := r.URL.Query().Get("code_challenge_method")
|
||||||
relayProps := map[string]string{}
|
relayProps := map[string]string{}
|
||||||
relayState := ""
|
relayState := ""
|
||||||
|
|
||||||
@@ -61,6 +65,19 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
relayProps["redirect_to"] = redirectURL
|
relayProps["redirect_to"] = redirectURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward SAML challenge values via RelayState so the complete step can prefer code-exchange
|
||||||
|
if isMobile {
|
||||||
|
if state != "" {
|
||||||
|
relayProps["state"] = state
|
||||||
|
}
|
||||||
|
if codeChallenge != "" {
|
||||||
|
relayProps["code_challenge"] = codeChallenge
|
||||||
|
}
|
||||||
|
if codeChallengeMethod != "" {
|
||||||
|
relayProps["code_challenge_method"] = codeChallengeMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
desktopToken := r.URL.Query().Get("desktop_token")
|
desktopToken := r.URL.Query().Get("desktop_token")
|
||||||
if desktopToken != "" {
|
if desktopToken != "" {
|
||||||
relayProps["desktop_token"] = desktopToken
|
relayProps["desktop_token"] = desktopToken
|
||||||
@@ -220,7 +237,33 @@ 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
|
// Decide between legacy token-in-URL vs SAML code-exchange for mobile
|
||||||
|
samlState := relayProps["state"]
|
||||||
|
samlChallenge := relayProps["code_challenge"]
|
||||||
|
samlMethod := relayProps["code_challenge_method"]
|
||||||
|
|
||||||
|
if isMobile && hasRedirectURL && samlChallenge != "" && c.App.Config().FeatureFlags.MobileSSOCodeExchange {
|
||||||
|
// Issue one-time login_code bound to user and SAML challenge values; do not create a session here
|
||||||
|
extra := model.MapToJSON(map[string]string{
|
||||||
|
"user_id": user.Id,
|
||||||
|
"state": samlState,
|
||||||
|
"code_challenge": samlChallenge,
|
||||||
|
"code_challenge_method": samlMethod,
|
||||||
|
})
|
||||||
|
code := model.NewToken(model.TokenTypeSaml, extra)
|
||||||
|
if err := c.App.Srv().Store().Token().Save(code); err != nil {
|
||||||
|
handleError(model.NewAppError("completeSaml", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
|
||||||
|
"login_code": code.Token,
|
||||||
|
})
|
||||||
|
utils.RenderMobileAuthComplete(w, redirectURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy: create a session and attach tokens (web/mobile without SAML code exchange)
|
||||||
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
|
session, err := c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
@@ -235,10 +278,13 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
if hasRedirectURL {
|
if hasRedirectURL {
|
||||||
if isMobile {
|
if isMobile {
|
||||||
// Mobile clients with redirect url support
|
// Mobile clients with redirect url support
|
||||||
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
|
// Legacy mobile path: return tokens only when SAML code exchange was not requested
|
||||||
model.SessionCookieToken: c.AppContext.Session().Token,
|
if samlChallenge == "" {
|
||||||
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
|
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
|
||||||
})
|
model.SessionCookieToken: c.AppContext.Session().Token,
|
||||||
|
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
|
||||||
|
})
|
||||||
|
}
|
||||||
utils.RenderMobileAuthComplete(w, redirectURL)
|
utils.RenderMobileAuthComplete(w, redirectURL)
|
||||||
} else {
|
} else {
|
||||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ type FeatureFlags struct {
|
|||||||
AttributeBasedAccessControl bool
|
AttributeBasedAccessControl bool
|
||||||
|
|
||||||
ContentFlagging bool
|
ContentFlagging bool
|
||||||
|
|
||||||
|
// Enable mobile SSO SAML code-exchange flow (no tokens in deep links)
|
||||||
|
MobileSSOCodeExchange bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FeatureFlags) SetDefaults() {
|
func (f *FeatureFlags) SetDefaults() {
|
||||||
@@ -99,6 +102,7 @@ func (f *FeatureFlags) SetDefaults() {
|
|||||||
f.CustomProfileAttributes = true
|
f.CustomProfileAttributes = true
|
||||||
f.AttributeBasedAccessControl = true
|
f.AttributeBasedAccessControl = true
|
||||||
f.ContentFlagging = false
|
f.ContentFlagging = false
|
||||||
|
f.MobileSSOCodeExchange = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap returns the feature flags as a map[string]string
|
// ToMap returns the feature flags as a map[string]string
|
||||||
|
|||||||
@@ -41,3 +41,7 @@ func (t *Token) IsValid() *AppError {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Token) IsExpired() bool {
|
||||||
|
return GetMillis() > (t.CreateAt + MaxTokenExipryTime)
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user