mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-20 00:10:19 -05:00
* [MM-67605] Add DCR redirect URI allowlist enforcement Introduce ServiceSettings.DCRRedirectURIAllowlist with glob-based validation and enforce it during OAuth dynamic client registration to block unapproved redirect URIs. Add System Console wiring and tests for config validation, wildcard matching semantics, API error behavior, and localhost wildcard support. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix pre-commit checks: TypeScript type assertion, gofmt, and regenerate CI artifacts - admin_definition_dcr_allowlist.test.tsx: Add AdminDefinitionSettingInput type assertion for 'multiple' property - oauth_dcr_test.go: Fix comment spacing (gofmt) - Regenerate mocks, go.sum, gen-serialized, mmctl-docs per CI requirements Co-authored-by: Cursor <cursoragent@cursor.com> * Revert unnecessary pre-commit regenerations Revert mmctl docs, mocks, go.sum, and gen-serialized to master. Keep only the TypeScript and gofmt fixes from the previous commit. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix import order in admin_definition_dcr_allowlist.test.tsx Co-authored-by: Cursor <cursoragent@cursor.com> * Fix i18n * Update server/public/model/oauth_dcr.go Co-authored-by: Eva Sarafianou <eva.sarafianou@gmail.com> * Fix --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Eva Sarafianou <eva.sarafianou@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
446 lines
14 KiB
Go
446 lines
14 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
func (api *API) InitOAuth() {
|
|
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(createOAuthApp)).Methods(http.MethodPost)
|
|
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(updateOAuthApp)).Methods(http.MethodPut)
|
|
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(getOAuthApps)).Methods(http.MethodGet)
|
|
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(getOAuthApp)).Methods(http.MethodGet)
|
|
api.BaseRoutes.OAuthApp.Handle("/info", api.APISessionRequired(getOAuthAppInfo)).Methods(http.MethodGet)
|
|
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(deleteOAuthApp)).Methods(http.MethodDelete)
|
|
api.BaseRoutes.OAuthApp.Handle("/regen_secret", api.APISessionRequired(regenerateOAuthAppSecret)).Methods(http.MethodPost)
|
|
|
|
// DCR (Dynamic Client Registration) endpoints as per RFC 7591
|
|
api.BaseRoutes.OAuthApps.Handle("/register", api.RateLimitedHandler(api.APIHandler(registerOAuthClient), model.RateLimitSettings{PerSec: model.NewPointer(2), MaxBurst: model.NewPointer(1)})).Methods(http.MethodPost)
|
|
|
|
api.BaseRoutes.User.Handle("/oauth/apps/authorized", api.APISessionRequired(getAuthorizedOAuthApps)).Methods(http.MethodGet)
|
|
}
|
|
|
|
func createOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
var appRequest model.OAuthAppRequest
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&appRequest); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("oauth_app", jsonErr)
|
|
return
|
|
}
|
|
|
|
// Build OAuthApp from request
|
|
oauthApp := model.OAuthApp{
|
|
Name: appRequest.Name,
|
|
Description: appRequest.Description,
|
|
IconURL: appRequest.IconURL,
|
|
CallbackUrls: appRequest.CallbackUrls,
|
|
Homepage: appRequest.Homepage,
|
|
IsTrusted: appRequest.IsTrusted,
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventCreateOAuthApp, model.AuditStatusFail)
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "oauth_app", &oauthApp)
|
|
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
c.SetPermissionError(model.PermissionManageOAuth)
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
oauthApp.IsTrusted = false
|
|
}
|
|
|
|
oauthApp.CreatorId = c.AppContext.Session().UserId
|
|
oauthApp.IsDynamicallyRegistered = false
|
|
|
|
// Use internal method to control secret generation
|
|
// Public clients: generateSecret = false (keeps empty secret)
|
|
// Confidential clients: generateSecret = true (auto-generates secret)
|
|
generateSecret := !appRequest.IsPublic
|
|
rapp, err := c.App.CreateOAuthAppInternal(&oauthApp, generateSecret)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(rapp)
|
|
auditRec.AddEventObjectType("oauth_app")
|
|
c.LogAudit("client_id=" + rapp.Id)
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
if err := json.NewEncoder(w).Encode(rapp); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func updateOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireAppId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUpdateOAuthApp, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "oauth_app_id", c.Params.AppId)
|
|
c.LogAudit("attempt")
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
c.SetPermissionError(model.PermissionManageOAuth)
|
|
return
|
|
}
|
|
|
|
var oauthApp model.OAuthApp
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("oauth_app", jsonErr)
|
|
return
|
|
}
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "oauth_app", &oauthApp)
|
|
|
|
// The app being updated in the payload must be the same one as indicated in the URL.
|
|
if oauthApp.Id != c.Params.AppId {
|
|
c.SetInvalidParam("app_id")
|
|
return
|
|
}
|
|
|
|
oldOAuthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(oldOAuthApp)
|
|
|
|
if c.AppContext.Session().UserId != oldOAuthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
|
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
oauthApp.IsTrusted = oldOAuthApp.IsTrusted
|
|
}
|
|
|
|
updatedOAuthApp, err := c.App.UpdateOAuthApp(oldOAuthApp, &oauthApp)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.AddEventResultState(updatedOAuthApp)
|
|
auditRec.AddEventObjectType("oauth_app")
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
if err := json.NewEncoder(w).Encode(updatedOAuthApp); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var apps []*model.OAuthApp
|
|
var appErr *model.AppError
|
|
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
|
apps, appErr = c.App.GetOAuthApps(c.Params.Page, c.Params.PerPage)
|
|
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
apps, appErr = c.App.GetOAuthAppsByCreator(c.AppContext.Session().UserId, c.Params.Page, c.Params.PerPage)
|
|
} else {
|
|
c.SetPermissionError(model.PermissionManageOAuth)
|
|
return
|
|
}
|
|
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
js, err := json.Marshal(apps)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write(js); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireAppId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
c.SetPermissionError(model.PermissionManageOAuth)
|
|
return
|
|
}
|
|
|
|
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
|
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
|
return
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireAppId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
oauthApp.Sanitize()
|
|
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireAppId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventDeleteOAuthApp, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "oauth_app_id", c.Params.AppId)
|
|
c.LogAudit("attempt")
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
c.SetPermissionError(model.PermissionManageOAuth)
|
|
return
|
|
}
|
|
|
|
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(oauthApp)
|
|
auditRec.AddEventObjectType("oauth_app")
|
|
|
|
if c.AppContext.Session().UserId != oauthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
|
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
|
return
|
|
}
|
|
|
|
err = c.App.DeleteOAuthApp(c.AppContext, oauthApp.Id)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func regenerateOAuthAppSecret(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireAppId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventRegenerateOAuthAppSecret, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "oauth_app_id", c.Params.AppId)
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
|
|
c.SetPermissionError(model.PermissionManageOAuth)
|
|
return
|
|
}
|
|
|
|
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(oauthApp)
|
|
auditRec.AddEventObjectType("oauth_app")
|
|
|
|
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
|
|
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
|
|
return
|
|
}
|
|
|
|
// Prevent regenerating secrets for public clients
|
|
if oauthApp.IsPublicClient() {
|
|
c.Err = model.NewAppError("regenerateOAuthAppSecret", "api.oauth.regenerate_secret.public_client.app_error", nil, "app_id="+oauthApp.Id, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
oauthApp, err = c.App.RegenerateOAuthAppSecret(oauthApp)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.AddEventResultState(oauthApp)
|
|
auditRec.Success()
|
|
c.LogAudit("success")
|
|
|
|
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
|
|
apps, appErr := c.App.GetAuthorizedAppsForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
js, err := json.Marshal(apps)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getAuthorizedOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write(js); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
// DCR (Dynamic Client Registration) endpoint handlers as per RFC 7591
|
|
func registerOAuthClient(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
// Session and permission checks removed for DCR endpoint to allow external client registration
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventRegisterOAuthClient, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
var clientRequest model.ClientRegistrationRequest
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&clientRequest); jsonErr != nil {
|
|
dcrError := model.NewDCRError(model.DCRErrorInvalidClientMetadata, "Invalid JSON in request body")
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
if err := json.NewEncoder(w).Encode(dcrError); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Add DCR request parameters to audit record
|
|
model.AddEventParameterToAuditRec(auditRec, "redirect_uris", clientRequest.RedirectURIs)
|
|
if clientRequest.ClientName != nil {
|
|
model.AddEventParameterToAuditRec(auditRec, "client_name", *clientRequest.ClientName)
|
|
}
|
|
if clientRequest.TokenEndpointAuthMethod != nil {
|
|
model.AddEventParameterToAuditRec(auditRec, "token_endpoint_auth_method", *clientRequest.TokenEndpointAuthMethod)
|
|
}
|
|
if clientRequest.ClientURI != nil {
|
|
model.AddEventParameterToAuditRec(auditRec, "client_uri", *clientRequest.ClientURI)
|
|
}
|
|
|
|
// Check if OAuth service provider is enabled
|
|
if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider {
|
|
dcrError := model.NewDCRError(model.DCRErrorUnsupportedOperation, "OAuth service provider is disabled")
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
if err := json.NewEncoder(w).Encode(dcrError); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if DCR is enabled
|
|
if c.App.Config().ServiceSettings.EnableDynamicClientRegistration == nil || !*c.App.Config().ServiceSettings.EnableDynamicClientRegistration {
|
|
dcrError := model.NewDCRError(model.DCRErrorUnsupportedOperation, "Dynamic client registration is disabled")
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
if err := json.NewEncoder(w).Encode(dcrError); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Validate the request
|
|
if err := clientRequest.IsValid(); err != nil {
|
|
dcrError := model.NewDCRError(model.DCRErrorInvalidClientMetadata, err.Message)
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
if err := json.NewEncoder(w).Encode(dcrError); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Enforce DCR redirect URI allowlist if configured
|
|
allowlist := c.App.Config().ServiceSettings.DCRRedirectURIAllowlist
|
|
if len(allowlist) > 0 {
|
|
for _, uri := range clientRequest.RedirectURIs {
|
|
if !model.RedirectURIMatchesAllowlist(uri, allowlist) {
|
|
dcrError := model.NewDCRError(model.DCRErrorInvalidRedirectURI, "One or more redirect URIs do not match the allowlist")
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
if err := json.NewEncoder(w).Encode(dcrError); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// No user ID for DCR
|
|
userID := ""
|
|
|
|
app, appErr := c.App.RegisterOAuthClient(c.AppContext, &clientRequest, userID)
|
|
if appErr != nil {
|
|
dcrError := model.NewDCRError(model.DCRErrorInvalidClientMetadata, appErr.Message)
|
|
|
|
w.WriteHeader(appErr.StatusCode)
|
|
if err := json.NewEncoder(w).Encode(dcrError); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(app)
|
|
auditRec.AddEventObjectType("oauth_app")
|
|
c.LogAudit("client_id=" + app.Id)
|
|
|
|
siteURL := *c.App.Config().ServiceSettings.SiteURL
|
|
response := app.ToClientRegistrationResponse(siteURL)
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
c.Logger.Warn("Error writing DCR response", mlog.Err(err))
|
|
}
|
|
}
|