diff --git a/server/channels/api4/oauth.go b/server/channels/api4/oauth.go index 7c8519200b5..9a4243baae0 100644 --- a/server/channels/api4/oauth.go +++ b/server/channels/api4/oauth.go @@ -342,6 +342,9 @@ func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) 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") @@ -353,6 +356,18 @@ func registerOAuthClient(c *Context, w http.ResponseWriter, r *http.Request) { 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") @@ -400,6 +415,11 @@ func registerOAuthClient(c *Context, w http.ResponseWriter, r *http.Request) { 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) diff --git a/server/channels/api4/oauth_test.go b/server/channels/api4/oauth_test.go index 0f6723d46ff..02f5c6c2229 100644 --- a/server/channels/api4/oauth_test.go +++ b/server/channels/api4/oauth_test.go @@ -5,7 +5,9 @@ package api4 import ( "context" + "io" "net/http" + "os" "testing" "time" @@ -13,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/channels/app" ) func TestCreateOAuthApp(t *testing.T) { @@ -810,3 +813,81 @@ func TestRegisterOAuthClient_PublicClient_Success(t *testing.T) { assert.Equal(t, *request.ClientURI, *response.ClientURI) assert.Equal(t, "user", response.Scope) } + +func TestRegisterOAuthClientAudit(t *testing.T) { + logFile, err := os.CreateTemp("", "dcr_audit.log") + require.NoError(t, err) + defer os.Remove(logFile.Name()) + + os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED", "true") + os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME", logFile.Name()) + defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED") + defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME") + + options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))} + th := SetupWithServerOptions(t, options) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableOAuthServiceProvider = true + cfg.ServiceSettings.EnableDynamicClientRegistration = model.NewPointer(true) + }) + + t.Run("Successful DCR registration is audited", func(t *testing.T) { + clientName := "Test Audit Client" + request := &model.ClientRegistrationRequest{ + RedirectURIs: []string{"https://example.com/callback"}, + ClientName: model.NewPointer(clientName), + } + + response, resp, err := th.Client.RegisterOAuthClient(context.Background(), request) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + require.NotNil(t, response) + + // Flush audit logs + err = th.Server.Audit.Flush() + require.NoError(t, err) + require.NoError(t, logFile.Sync()) + + // Read and verify audit log + data, err := io.ReadAll(logFile) + require.NoError(t, err) + require.NotEmpty(t, data) + + auditLog := string(data) + assert.Contains(t, auditLog, "registerOAuthClient") + assert.Contains(t, auditLog, clientName) + assert.Contains(t, auditLog, response.ClientID) + assert.Contains(t, auditLog, "success") + }) + + t.Run("Failed DCR registration is audited", func(t *testing.T) { + // Truncate log file for this test + require.NoError(t, logFile.Truncate(0)) + _, err := logFile.Seek(0, 0) + require.NoError(t, err) + + // Invalid request (missing redirect URIs) + request := &model.ClientRegistrationRequest{ + ClientName: model.NewPointer("Invalid Client"), + } + + _, resp, err := th.Client.RegisterOAuthClient(context.Background(), request) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + // Flush audit logs + err = th.Server.Audit.Flush() + require.NoError(t, err) + require.NoError(t, logFile.Sync()) + + // Read and verify audit log + data, err := io.ReadAll(logFile) + require.NoError(t, err) + require.NotEmpty(t, data) + + auditLog := string(data) + assert.Contains(t, auditLog, "registerOAuthClient") + assert.Contains(t, auditLog, "fail") + }) +} diff --git a/server/public/model/audit_events.go b/server/public/model/audit_events.go index 06efacce91f..212b9dd7985 100644 --- a/server/public/model/audit_events.go +++ b/server/public/model/audit_events.go @@ -222,6 +222,7 @@ const ( AuditEventLoginWithOAuth = "loginWithOAuth" // login using OAuth authentication provider AuditEventMobileLoginWithOAuth = "mobileLoginWithOAuth" // mobile application login using OAuth authentication provider AuditEventRegenerateOAuthAppSecret = "regenerateOAuthAppSecret" // regenerate secret key for OAuth app + AuditEventRegisterOAuthClient = "registerOAuthClient" // register OAuth client via dynamic client registration (RFC 7591) AuditEventSignupWithOAuth = "signupWithOAuth" // create account using OAuth authentication provider AuditEventUpdateOAuthApp = "updateOAuthApp" // update OAuth app AuditEventUpdateOutgoingOAuthConnection = "updateOutgoingOAuthConnection" // update outgoing OAuth connection