mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 21:17:49 -04: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>
150 lines
5.9 KiB
Go
150 lines
5.9 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestClientRegistrationRequestIsValid(t *testing.T) {
|
|
t.Run("PublicClient_Valid", func(t *testing.T) {
|
|
req := &ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
TokenEndpointAuthMethod: NewPointer(ClientAuthMethodNone),
|
|
ClientName: NewPointer("Test Public Client"),
|
|
}
|
|
|
|
require.Nil(t, req.IsValid())
|
|
})
|
|
|
|
t.Run("PublicClient_AuthMethodValidation", func(t *testing.T) {
|
|
req := &ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
TokenEndpointAuthMethod: NewPointer(ClientAuthMethodNone),
|
|
ClientName: NewPointer("Test Public Client"),
|
|
}
|
|
|
|
require.Nil(t, req.IsValid())
|
|
|
|
req.TokenEndpointAuthMethod = NewPointer("invalid_method")
|
|
require.NotNil(t, req.IsValid())
|
|
})
|
|
|
|
t.Run("PublicClient_RedirectURIValidation", func(t *testing.T) {
|
|
req := &ClientRegistrationRequest{
|
|
TokenEndpointAuthMethod: NewPointer(ClientAuthMethodNone),
|
|
ClientName: NewPointer("Test Public Client"),
|
|
}
|
|
|
|
require.NotNil(t, req.IsValid())
|
|
|
|
req.RedirectURIs = []string{"https://example.com/callback"}
|
|
require.Nil(t, req.IsValid())
|
|
|
|
req.RedirectURIs = []string{"http://localhost:3000/callback"}
|
|
require.Nil(t, req.IsValid())
|
|
|
|
req.RedirectURIs = []string{"invalid-uri"}
|
|
require.NotNil(t, req.IsValid())
|
|
})
|
|
}
|
|
|
|
func TestNewOAuthAppFromClientRegistration(t *testing.T) {
|
|
t.Run("PublicClient", func(t *testing.T) {
|
|
req := &ClientRegistrationRequest{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
TokenEndpointAuthMethod: NewPointer(ClientAuthMethodNone),
|
|
ClientName: NewPointer("Test Public Client"),
|
|
}
|
|
|
|
creatorId := NewId()
|
|
app := NewOAuthAppFromClientRegistration(req, creatorId)
|
|
|
|
require.Equal(t, creatorId, app.CreatorId)
|
|
require.Equal(t, req.RedirectURIs, []string(app.CallbackUrls))
|
|
require.Equal(t, *req.TokenEndpointAuthMethod, app.GetTokenEndpointAuthMethod())
|
|
require.Equal(t, *req.ClientName, app.Name)
|
|
require.True(t, app.IsDynamicallyRegistered)
|
|
|
|
app.PreSave()
|
|
require.Nil(t, app.IsValid())
|
|
|
|
require.Empty(t, app.ClientSecret)
|
|
})
|
|
}
|
|
|
|
func TestRedirectURIMatchesGlob(t *testing.T) {
|
|
t.Run("direct match", func(t *testing.T) {
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/cb", "https://example.com/cb"))
|
|
require.False(t, RedirectURIMatchesGlob("https://example.com/cb", "https://example.com/cb2"))
|
|
require.False(t, RedirectURIMatchesGlob("https://example.com/cb2", "https://example.com/cb"))
|
|
})
|
|
|
|
t.Run("full-string anchored", func(t *testing.T) {
|
|
require.False(t, RedirectURIMatchesGlob("https://example.com/cb/evil", "https://example.com/cb"))
|
|
require.False(t, RedirectURIMatchesGlob("https://evil.example.com/cb", "https://example.com/cb"))
|
|
})
|
|
|
|
t.Run("single star matches non-slash chars", func(t *testing.T) {
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/cb", "https://example.com/*"))
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/segment", "https://example.com/*"))
|
|
require.False(t, RedirectURIMatchesGlob("https://example.com/a/b", "https://example.com/*"))
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/", "https://example.com/*"))
|
|
})
|
|
|
|
t.Run("double star matches including slash", func(t *testing.T) {
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/a/b/c", "https://example.com/**"))
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/callback", "https://example.com/**"))
|
|
require.True(t, RedirectURIMatchesGlob("https://example.com/", "https://example.com/**"))
|
|
require.False(t, RedirectURIMatchesGlob("https://evil.example.com/", "https://example.com/**"))
|
|
})
|
|
|
|
t.Run("host wildcard", func(t *testing.T) {
|
|
require.True(t, RedirectURIMatchesGlob("https://app.example.com/cb", "https://*.example.com/cb"))
|
|
require.True(t, RedirectURIMatchesGlob("https://foo.example.com/path", "https://*.example.com/*"))
|
|
require.False(t, RedirectURIMatchesGlob("https://example.com.evil/cb", "https://*.example.com/cb"))
|
|
})
|
|
|
|
t.Run("port wildcard", func(t *testing.T) {
|
|
require.True(t, RedirectURIMatchesGlob("https://localhost:3000/cb", "https://localhost:*/cb"))
|
|
require.False(t, RedirectURIMatchesGlob("https://localhost:3000/cb", "https://localhost:8080/cb"))
|
|
})
|
|
|
|
t.Run("multiple patterns one match suffices", func(t *testing.T) {
|
|
allowlist := []string{"https://a.com/**", "https://b.com/**"}
|
|
require.True(t, RedirectURIMatchesAllowlist("https://a.com/x", allowlist))
|
|
require.True(t, RedirectURIMatchesAllowlist("https://b.com/y", allowlist))
|
|
require.False(t, RedirectURIMatchesAllowlist("https://c.com/z", allowlist))
|
|
})
|
|
|
|
t.Run("empty allowlist permits all", func(t *testing.T) {
|
|
require.True(t, RedirectURIMatchesAllowlist("https://any.com/cb", []string{}))
|
|
})
|
|
|
|
t.Run("one bad URI rejects request", func(t *testing.T) {
|
|
allowlist := []string{"https://allowed.com/**"}
|
|
uris := []string{"https://allowed.com/cb1", "https://disallowed.com/cb2"}
|
|
allMatch := true
|
|
for _, uri := range uris {
|
|
if !RedirectURIMatchesAllowlist(uri, allowlist) {
|
|
allMatch = false
|
|
break
|
|
}
|
|
}
|
|
require.False(t, allMatch)
|
|
})
|
|
}
|
|
|
|
func TestIsValidDCRRedirectURIPattern(t *testing.T) {
|
|
require.True(t, IsValidDCRRedirectURIPattern("https://example.com/**"))
|
|
require.True(t, IsValidDCRRedirectURIPattern("http://localhost:3000/cb"))
|
|
require.True(t, IsValidDCRRedirectURIPattern("http://localhost:*"))
|
|
require.True(t, IsValidDCRRedirectURIPattern("http://x")) // minimum valid http URL (8 chars)
|
|
require.True(t, IsValidDCRRedirectURIPattern("https://x")) // minimum valid https URL (9 chars)
|
|
require.False(t, IsValidDCRRedirectURIPattern("https://"))
|
|
require.False(t, IsValidDCRRedirectURIPattern("ftp://example.com"))
|
|
require.False(t, IsValidDCRRedirectURIPattern("https://example.com/***"))
|
|
}
|