mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
Restrict OAuth deauthorization and personal access token lifecycle actions to direct user sessions, denying delegated OAuth app tokens, consistent with the existing OAuth authorize and access token creation handlers. Adds regression tests covering the OAuth-session case. Co-authored-by: Hanzo Dev <[email protected]>
704 lines
22 KiB
Go
704 lines
22 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
|
)
|
|
|
|
const (
|
|
callbackHost = "callback"
|
|
)
|
|
|
|
func (w *Web) InitOAuth() {
|
|
// OAuth 2.0 Authorization Server Metadata endpoint (RFC 8414)
|
|
// Match the exact path and any path with additional segments after it
|
|
w.MainRouter.PathPrefix(model.OAuthMetadataEndpoint).Handler(w.APIHandlerTrustRequester(getAuthorizationServerMetadata)).Methods(http.MethodGet)
|
|
|
|
// API version independent OAuth 2.0 as a service provider endpoints
|
|
w.MainRouter.Handle(model.OAuthAuthorizeEndpoint, w.APIHandlerTrustRequester(authorizeOAuthPage)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle(model.OAuthAuthorizeEndpoint, w.APISessionRequired(authorizeOAuthApp)).Methods(http.MethodPost)
|
|
w.MainRouter.Handle(model.OAuthDeauthorizeEndpoint, w.APISessionRequired(deauthorizeOAuthApp)).Methods(http.MethodPost)
|
|
w.MainRouter.Handle(model.OAuthAccessTokenEndpoint, w.APIHandlerTrustRequester(getAccessToken)).Methods(http.MethodPost)
|
|
|
|
// API version independent OAuth as a client endpoints
|
|
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/login", w.APIHandler(loginWithOAuth)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", w.APIHandler(mobileLoginWithOAuth)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", w.APIHandler(signupWithOAuth)).Methods(http.MethodGet)
|
|
|
|
// Intune MAM authentication endpoint
|
|
w.MainRouter.Handle("/oauth/intune", w.APIHandler(loginByIntune)).Methods(http.MethodPost)
|
|
|
|
// Old endpoints for backwards compatibility, needed to not break SSO for any old setups
|
|
w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle("/login/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
|
|
w.MainRouter.Handle("/api/v4/oauth_test", w.APISessionRequired(testHandler)).Methods(http.MethodGet)
|
|
}
|
|
|
|
func testHandler(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func authorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
var authRequest *model.AuthorizeRequest
|
|
err := json.NewDecoder(r.Body).Decode(&authRequest)
|
|
if err != nil || authRequest == nil {
|
|
c.SetInvalidParamWithErr("authorize_request", err)
|
|
return
|
|
}
|
|
|
|
if err := authRequest.IsValid(); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if c.AppContext.Session().IsOAuth {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
c.Err.DetailedError += ", attempted access by oauth app"
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventAuthorizeOAuthApp, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
c.LogAudit("attempt")
|
|
|
|
redirectURL, appErr := c.App.AllowOAuthAppAccessToUser(c.AppContext, c.AppContext.Session().UserId, authRequest)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
_, err = w.Write([]byte(model.MapToJSON(map[string]string{"redirect": redirectURL})))
|
|
if err != nil {
|
|
c.Logger.Warn("Error writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
requestData := model.MapFromJSON(r.Body)
|
|
clientId := requestData["client_id"]
|
|
|
|
if !model.IsValidId(clientId) {
|
|
c.SetInvalidParam("client_id")
|
|
return
|
|
}
|
|
|
|
if c.AppContext.Session().IsOAuth {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
c.Err.DetailedError += ", attempted access by oauth app"
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventDeauthorizeOAuthApp, model.AuditStatusFail)
|
|
auditRec.AddMeta("client_id", clientId)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
err := c.App.DeauthorizeOAuthAppForUser(c.AppContext, c.AppContext.Session().UserId, clientId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider {
|
|
err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
authRequest := &model.AuthorizeRequest{
|
|
ResponseType: r.URL.Query().Get("response_type"),
|
|
ClientId: r.URL.Query().Get("client_id"),
|
|
RedirectURI: r.URL.Query().Get("redirect_uri"),
|
|
Scope: r.URL.Query().Get("scope"),
|
|
State: r.URL.Query().Get("state"),
|
|
CodeChallenge: r.URL.Query().Get("code_challenge"),
|
|
CodeChallengeMethod: r.URL.Query().Get("code_challenge_method"),
|
|
Resource: r.URL.Query().Get("resource"),
|
|
}
|
|
|
|
loginHint := r.URL.Query().Get("login_hint")
|
|
|
|
if err := authRequest.IsValid(); err != nil {
|
|
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
|
|
url.Values{
|
|
"type": []string{"oauth_invalid_param"},
|
|
"message": []string{err.Message},
|
|
}, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventAuthorizeOAuthPage, model.AuditStatusFail)
|
|
auditRec.AddMeta("client_id", authRequest.ClientId)
|
|
auditRec.AddMeta("scope", authRequest.Scope)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId)
|
|
if err != nil {
|
|
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
// here we should check if the user is logged in
|
|
if c.AppContext.Session().UserId == "" {
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
if loginHint == model.UserAuthServiceSaml {
|
|
http.Redirect(w, r, c.GetSiteURLHeader()+"/login/sso/saml?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
|
|
} else {
|
|
http.Redirect(w, r, c.GetSiteURLHeader()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !oauthApp.IsValidRedirectURL(authRequest.RedirectURI) {
|
|
err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
|
|
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
|
|
url.Values{
|
|
"type": []string{"oauth_invalid_redirect_url"},
|
|
"message": []string{i18n.T("api.oauth.allow_oauth.redirect_callback.app_error")},
|
|
}, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
// Validate PKCE requirements for public clients using authorization code flow
|
|
// Implicit flow doesn't require PKCE as it doesn't use code exchange
|
|
if oauthApp.IsPublicClient() && authRequest.ResponseType == model.AuthCodeResponseType && authRequest.CodeChallenge == "" {
|
|
err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.pkce_required_public.app_error", nil, "", http.StatusBadRequest)
|
|
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
|
|
url.Values{
|
|
"type": []string{"oauth_pkce_required"},
|
|
"message": []string{"PKCE is required for public clients using authorization code flow"},
|
|
}, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
isAuthorized := false
|
|
|
|
if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext, c.AppContext.Session().UserId, model.PreferenceCategoryAuthorizedOAuthApp, authRequest.ClientId); err == nil {
|
|
// when we support scopes we should check if the scopes match
|
|
isAuthorized = true
|
|
}
|
|
|
|
// Automatically allow if the app is trusted
|
|
if oauthApp.IsTrusted || isAuthorized {
|
|
redirectURL, err := c.App.AllowOAuthAppAccessToUser(c.AppContext, c.AppContext.Session().UserId, authRequest)
|
|
if err != nil {
|
|
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
|
w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors 'self' %s", *c.App.Config().ServiceSettings.FrameAncestors))
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
|
|
|
|
staticDir, _ := fileutils.FindDir(model.ClientDir)
|
|
http.ServeFile(w, r, filepath.Join(staticDir, "root.html"))
|
|
}
|
|
|
|
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_request.app_error", nil, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
code := r.FormValue("code")
|
|
refreshToken := r.FormValue("refresh_token")
|
|
|
|
grantType := r.FormValue("grant_type")
|
|
switch grantType {
|
|
case model.AccessTokenGrantType:
|
|
if code == "" {
|
|
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
case model.RefreshTokenGrantType:
|
|
if refreshToken == "" {
|
|
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
default:
|
|
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
clientId := r.FormValue("client_id")
|
|
if !model.IsValidId(clientId) {
|
|
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
secret := r.FormValue("client_secret")
|
|
codeVerifier := r.FormValue("code_verifier")
|
|
|
|
// Authentication validation will be done at app layer based on client type
|
|
// For public clients: client_secret should be empty, code_verifier required
|
|
// For confidential clients: client_secret required, code_verifier optional but enforced if used
|
|
|
|
redirectURI := r.FormValue("redirect_uri")
|
|
resource := r.FormValue("resource")
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventGetAccessToken, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
auditRec.AddMeta("grant_type", grantType)
|
|
auditRec.AddMeta("client_id", clientId)
|
|
c.LogAudit("attempt")
|
|
|
|
accessRsp, err := c.App.GetOAuthAccessTokenForCodeFlow(c.AppContext, clientId, grantType, redirectURI, code, secret, refreshToken, codeVerifier, resource)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
if err := json.NewEncoder(w).Encode(accessRsp); err != nil {
|
|
c.Logger.Warn("Error writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireService()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
service := c.Params.Service
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventCompleteOAuth, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "service", service)
|
|
|
|
oauthError := r.URL.Query().Get("error")
|
|
if oauthError == "access_denied" {
|
|
utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
|
|
"type": []string{"oauth_access_denied"},
|
|
"service": []string{strings.Title(service)},
|
|
}, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
code := r.URL.Query().Get("code")
|
|
if code == "" {
|
|
utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
|
|
"type": []string{"oauth_missing_code"},
|
|
"service": []string{strings.Title(service)},
|
|
}, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
state := r.URL.Query().Get("state")
|
|
|
|
uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete"
|
|
|
|
body, props, tokenUser, err := c.App.AuthorizeOAuthUser(c.AppContext, w, r, service, code, state, uri)
|
|
|
|
action := ""
|
|
hasRedirectURL := false
|
|
isMobile := false
|
|
redirectURL := ""
|
|
if props != nil {
|
|
action = props["action"]
|
|
isMobile = action == model.OAuthActionMobile
|
|
if val, ok := props["redirect_to"]; ok {
|
|
redirectURL = val
|
|
hasRedirectURL = redirectURL != ""
|
|
}
|
|
}
|
|
redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL, c.App.Config().NativeAppSettings.AppCustomURLSchemes)
|
|
|
|
renderError := func(err *model.AppError) {
|
|
if isMobile && hasRedirectURL {
|
|
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
|
|
} else {
|
|
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
err.Translate(c.AppContext.T)
|
|
c.LogErrorByCode(err)
|
|
renderError(err)
|
|
return
|
|
}
|
|
|
|
user, err := c.App.CompleteOAuth(c.AppContext, service, body, props, tokenUser)
|
|
if err != nil {
|
|
err.Translate(c.AppContext.T)
|
|
c.LogErrorByCode(err)
|
|
renderError(err)
|
|
return
|
|
}
|
|
|
|
if action == model.OAuthActionEmailToSSO {
|
|
redirectURL = c.GetSiteURLHeader() + "/login?extra=signin_change"
|
|
} else if action == model.OAuthActionSSOToEmail {
|
|
redirectURL = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
|
|
} else {
|
|
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()
|
|
auditRec.Actor.UserId = user.Id
|
|
c.LogAuditWithUserId(user.Id, "authenticated")
|
|
|
|
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, model.LoginOptions{
|
|
IsMobile: isMobile,
|
|
IsOAuthUser: isOAuthUser,
|
|
})
|
|
if err != nil {
|
|
err.Translate(c.AppContext.T)
|
|
c.Logger.Error(err.Error())
|
|
renderError(err)
|
|
return
|
|
}
|
|
c.AppContext = c.AppContext.WithSession(session)
|
|
|
|
// Old mobile version
|
|
if isMobile && !hasRedirectURL {
|
|
c.App.AttachSessionCookies(c.AppContext, w, r)
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
return
|
|
} else
|
|
// New mobile version
|
|
if isMobile && hasRedirectURL {
|
|
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
|
|
model.SessionCookieToken: c.AppContext.Session().Token,
|
|
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
|
|
"srv": c.App.GetSiteURL(), // Server URL for mobile client verification
|
|
})
|
|
utils.RenderMobileAuthComplete(w, redirectURL)
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
return
|
|
}
|
|
// For web
|
|
c.App.AttachSessionCookies(c.AppContext, w, r)
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireService()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
loginHint := r.URL.Query().Get("login_hint")
|
|
redirectURL := r.URL.Query().Get("redirect_to")
|
|
desktopToken := r.URL.Query().Get("desktop_token")
|
|
|
|
if redirectURL != "" {
|
|
if err := utils.ValidateWebAuthRedirectUrl(c.App.Config(), redirectURL); err != nil {
|
|
c.Err = model.NewAppError("loginWithOAuth", "api.invalid_redirect_url", nil, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventLoginWithOAuth, model.AuditStatusFail)
|
|
auditRec.AddMeta("service", c.Params.Service)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
// Get invite token or ID instead of team_id
|
|
tokenID := r.URL.Query().Get("t")
|
|
inviteId := r.URL.Query().Get("id")
|
|
|
|
authURL, err := c.App.GetOAuthLoginEndpoint(c.AppContext, w, r, c.Params.Service, model.OAuthActionLogin, redirectURL, loginHint, false, desktopToken, tokenID, inviteId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
http.Redirect(w, r, authURL, http.StatusFound)
|
|
}
|
|
|
|
func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireService()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
|
|
|
|
if redirectURL != "" && !utils.IsValidMobileAuthRedirectURL(c.App.Config(), redirectURL) {
|
|
err := model.NewAppError("mobileLoginWithOAuth", "api.invalid_custom_url_scheme", nil, "", http.StatusBadRequest)
|
|
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventMobileLoginWithOAuth, model.AuditStatusFail)
|
|
auditRec.AddMeta("service", c.Params.Service)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
// Get invite token or ID instead of team_id
|
|
tokenID := r.URL.Query().Get("t")
|
|
inviteId := r.URL.Query().Get("id")
|
|
|
|
authURL, err := c.App.GetOAuthLoginEndpoint(c.AppContext, w, r, c.Params.Service, model.OAuthActionMobile, redirectURL, "", true, "", tokenID, inviteId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
http.Redirect(w, r, authURL, http.StatusFound)
|
|
}
|
|
|
|
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireService()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !*c.App.Config().TeamSettings.EnableUserCreation {
|
|
utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{
|
|
"message": []string{i18n.T("api.oauth.singup_with_oauth.disabled.app_error")},
|
|
}, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventSignupWithOAuth, model.AuditStatusFail)
|
|
auditRec.AddMeta("service", c.Params.Service)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
// Get invite token or ID instead of team_id
|
|
tokenID := r.URL.Query().Get("t")
|
|
inviteId := r.URL.Query().Get("id")
|
|
|
|
desktopToken := r.URL.Query().Get("desktop_token")
|
|
|
|
authURL, err := c.App.GetOAuthSignupEndpoint(c.AppContext, w, r, c.Params.Service, desktopToken, tokenID, inviteId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
http.Redirect(w, r, authURL, http.StatusFound)
|
|
}
|
|
|
|
func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string, otherValidSchemes []string) string {
|
|
parsed, err := url.Parse(targetURL)
|
|
if err != nil {
|
|
return siteURLPrefix
|
|
}
|
|
prefixParsed, err := url.Parse(siteURLPrefix)
|
|
if err != nil {
|
|
return siteURLPrefix
|
|
}
|
|
// mobile access
|
|
if slices.Contains(otherValidSchemes, fmt.Sprintf("%v://", parsed.Scheme)) &&
|
|
parsed.Host == callbackHost &&
|
|
parsed.Path == "" &&
|
|
parsed.RawQuery == "" &&
|
|
parsed.Fragment == "" {
|
|
return targetURL
|
|
}
|
|
// Check if the targetURL is valid and within the siteURLPrefix, excluding native app schemes like mmauth://
|
|
sameScheme := parsed.Scheme == prefixParsed.Scheme
|
|
sameHost := parsed.Host == prefixParsed.Host
|
|
safePath := strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path))
|
|
|
|
if sameScheme && sameHost && safePath {
|
|
return targetURL
|
|
} else if parsed.Scheme != "" || parsed.Host != "" {
|
|
return siteURLPrefix
|
|
}
|
|
|
|
// For relative URLs, normalize and join with siteURLPrefix
|
|
if targetURL != "" && targetURL[0] != '/' {
|
|
targetURL = "/" + targetURL
|
|
}
|
|
|
|
// Check for path traversal
|
|
joinedURL, err := url.JoinPath(siteURLPrefix, targetURL)
|
|
if err != nil {
|
|
return siteURLPrefix
|
|
}
|
|
unescapedURL, err := url.PathUnescape(joinedURL)
|
|
if err != nil {
|
|
return siteURLPrefix
|
|
}
|
|
parsed, err = url.Parse(unescapedURL)
|
|
if err != nil {
|
|
return siteURLPrefix
|
|
}
|
|
|
|
if !strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) {
|
|
return siteURLPrefix
|
|
}
|
|
|
|
return parsed.String()
|
|
}
|
|
|
|
func getAuthorizationServerMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider {
|
|
c.Err = model.NewAppError("getAuthorizationServerMetadata", "api.oauth.authorization_server_metadata.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
metadata, err := c.App.GetAuthorizationServerMetadata(c.AppContext)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(metadata); err != nil {
|
|
c.Logger.Warn("Error writing authorization server metadata response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
// loginByIntune handles authentication using Microsoft Intune MAM with Entra ID tokens
|
|
func loginByIntune(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
var req model.IntuneLoginRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
c.SetInvalidParamWithErr("request_body", err)
|
|
return
|
|
}
|
|
|
|
if req.AccessToken == "" {
|
|
c.SetInvalidParam("access_token")
|
|
return
|
|
}
|
|
|
|
if req.VoIPDeviceId != "" && !model.IsValidVoIPDeviceId(req.VoIPDeviceId) {
|
|
c.SetInvalidParam("voip_device_id")
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord("login_by_intune", model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
c.LogAudit("attempt")
|
|
|
|
// Authenticate user via Intune MAM
|
|
user, err := c.App.LoginByIntune(c.AppContext, req.AccessToken)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.AddMeta("obtained_user_id", user.Id)
|
|
c.LogAuditWithUserId(user.Id, "obtained user")
|
|
|
|
model.AddEventParameterToAuditRec(auditRec, "device_id", req.DeviceId)
|
|
model.AddEventParameterToAuditRec(auditRec, "voip_device_id", req.VoIPDeviceId)
|
|
|
|
session, err := c.App.DoLogin(c.AppContext, w, r, user, model.LoginOptions{
|
|
DeviceId: req.DeviceId,
|
|
VoIPDeviceId: req.VoIPDeviceId,
|
|
IsOAuthUser: true,
|
|
})
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
c.AppContext = c.AppContext.WithSession(session)
|
|
|
|
c.App.AttachSessionCookies(c.AppContext, w, r)
|
|
|
|
auditRec.Success()
|
|
c.LogAuditWithUserId(user.Id, "success")
|
|
|
|
user.Sanitize(map[string]bool{})
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
if err := json.NewEncoder(w).Encode(user); err != nil {
|
|
c.Logger.Warn("Failed to encode user response", mlog.Err(err))
|
|
}
|
|
}
|