snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
263
server/public/model/oauth_dcr.go
Normal file
263
server/public/model/oauth_dcr.go
Normal file
@@ -0,0 +1,263 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ClientRegistrationRequest struct {
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"`
|
||||
ClientName *string `json:"client_name,omitempty"`
|
||||
ClientURI *string `json:"client_uri,omitempty"`
|
||||
}
|
||||
|
||||
type ClientRegistrationResponse struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret *string `json:"client_secret,omitempty"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
ClientName *string `json:"client_name,omitempty"`
|
||||
ClientURI *string `json:"client_uri,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
DCRErrorInvalidRedirectURI = "invalid_redirect_uri"
|
||||
DCRErrorInvalidClientMetadata = "invalid_client_metadata"
|
||||
DCRErrorUnsupportedOperation = "unsupported_operation"
|
||||
)
|
||||
|
||||
type DCRError struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
type dcrRedirectURIPattern struct {
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
rawQuery string
|
||||
}
|
||||
|
||||
func (r *ClientRegistrationRequest) IsValid() *AppError {
|
||||
if len(r.RedirectURIs) == 0 {
|
||||
return NewAppError("ClientRegistrationRequest.IsValid", "model.dcr.is_valid.redirect_uris.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
for _, uri := range r.RedirectURIs {
|
||||
if !IsValidDCRRedirectURI(uri) {
|
||||
return NewAppError("ClientRegistrationRequest.IsValid", "model.dcr.is_valid.redirect_uri_format.app_error", nil, "uri="+uri, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
if r.ClientName != nil && len(*r.ClientName) > 64 {
|
||||
return NewAppError("ClientRegistrationRequest.IsValid", "model.dcr.is_valid.client_name.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if r.ClientURI != nil {
|
||||
if !IsValidHTTPURL(*r.ClientURI) {
|
||||
return NewAppError("ClientRegistrationRequest.IsValid", "model.dcr.is_valid.client_uri_format.app_error", nil, "uri="+*r.ClientURI, http.StatusBadRequest)
|
||||
}
|
||||
if len(*r.ClientURI) > 256 {
|
||||
return NewAppError("ClientRegistrationRequest.IsValid", "model.dcr.is_valid.client_uri_length.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
if r.TokenEndpointAuthMethod != nil && *r.TokenEndpointAuthMethod != ClientAuthMethodClientSecretPost && *r.TokenEndpointAuthMethod != ClientAuthMethodNone {
|
||||
return NewAppError("ClientRegistrationRequest.IsValid", "model.dcr.is_valid.unsupported_auth_method.app_error", nil, "method="+*r.TokenEndpointAuthMethod, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDCRError(errorType, description string) *DCRError {
|
||||
return &DCRError{
|
||||
Error: errorType,
|
||||
ErrorDescription: description,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDefaultGrantTypes() []string {
|
||||
return []string{GrantTypeAuthorizationCode, GrantTypeRefreshToken}
|
||||
}
|
||||
|
||||
func GetDefaultResponseTypes() []string {
|
||||
return []string{ResponseTypeCode}
|
||||
}
|
||||
|
||||
// IsValidDCRRedirectURI validates a concrete DCR redirect URI. Unlike
|
||||
// IsValidHTTPURL, it accepts custom (non-HTTP) schemes so that desktop OAuth
|
||||
// clients can use their own URI schemes (e.g. cursor://anysphere.cursor-mcp/oauth/callback).
|
||||
// The URI must be absolute with both a scheme and a host. Dangerous schemes
|
||||
// (javascript, data, vbscript, file, blob, about) are rejected even in
|
||||
// authority form (e.g. "javascript://evil.example.com/x").
|
||||
func IsValidDCRRedirectURI(rawURL string) bool {
|
||||
u, err := url.ParseRequestURI(rawURL)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "javascript", "data", "vbscript", "file", "blob", "about":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidDCRRedirectURIPattern validates a DCR redirect URI allowlist pattern.
|
||||
// Patterns must be absolute URIs with a scheme and host and be well-formed for
|
||||
// glob matching. Custom schemes (e.g. cursor://) are permitted in addition to
|
||||
// http:// and https://.
|
||||
func IsValidDCRRedirectURIPattern(pattern string) bool {
|
||||
// Reject control characters and other invalid chars
|
||||
for _, r := range pattern {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Reject malformed wildcard runs. Supported tokens are "*" and "**".
|
||||
if strings.Contains(pattern, "***") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Replace wildcard tokens with concrete placeholders so URL parsing can validate
|
||||
// overall shape (scheme, host, and URI formatting).
|
||||
normalized := strings.ReplaceAll(pattern, "**", "mmdoublewildcard")
|
||||
normalized = strings.ReplaceAll(normalized, "*", "mmsinglewildcard")
|
||||
// Use a numeric placeholder so wildcarded port values (e.g. localhost:*)
|
||||
// normalize to a URI shape accepted by URL parsing (localhost:1).
|
||||
normalized = strings.ReplaceAll(normalized, "mmdoublewildcard", "1")
|
||||
normalized = strings.ReplaceAll(normalized, "mmsinglewildcard", "1")
|
||||
|
||||
return IsValidDCRRedirectURI(normalized)
|
||||
}
|
||||
|
||||
// RedirectURIMatchesGlob returns true if uri matches the glob pattern.
|
||||
// Matching is URL-component aware, so host, path, and query wildcards cannot
|
||||
// satisfy requirements from another component.
|
||||
func RedirectURIMatchesGlob(uri, pattern string) bool {
|
||||
candidate, err := url.ParseRequestURI(uri)
|
||||
if err != nil || candidate.Scheme == "" || candidate.Host == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !IsValidDCRRedirectURIPattern(pattern) {
|
||||
return false
|
||||
}
|
||||
|
||||
parsedPattern, ok := parseDCRRedirectURIPattern(pattern)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if candidate.Scheme != parsedPattern.scheme {
|
||||
return false
|
||||
}
|
||||
if !redirectURIMatchesGlobRecur(candidate.Host, parsedPattern.host, 0, 0) {
|
||||
return false
|
||||
}
|
||||
if !redirectURIMatchesGlobRecur(candidate.EscapedPath(), parsedPattern.path, 0, 0) {
|
||||
return false
|
||||
}
|
||||
if parsedPattern.rawQuery == "" {
|
||||
return candidate.RawQuery == ""
|
||||
}
|
||||
if candidate.RawQuery == "" {
|
||||
return false
|
||||
}
|
||||
return redirectURIMatchesGlobRecur(candidate.RawQuery, parsedPattern.rawQuery, 0, 0)
|
||||
}
|
||||
|
||||
func parseDCRRedirectURIPattern(pattern string) (dcrRedirectURIPattern, bool) {
|
||||
scheme, rest, ok := strings.Cut(pattern, "://")
|
||||
if !ok {
|
||||
return dcrRedirectURIPattern{}, false
|
||||
}
|
||||
|
||||
hostEnd := len(rest)
|
||||
for _, separator := range []string{"/", "?"} {
|
||||
if i := strings.Index(rest, separator); i >= 0 && i < hostEnd {
|
||||
hostEnd = i
|
||||
}
|
||||
}
|
||||
|
||||
host := rest[:hostEnd]
|
||||
if host == "" {
|
||||
return dcrRedirectURIPattern{}, false
|
||||
}
|
||||
|
||||
remainder := rest[hostEnd:]
|
||||
path := ""
|
||||
rawQuery := ""
|
||||
if strings.HasPrefix(remainder, "/") {
|
||||
path, rawQuery, _ = strings.Cut(remainder, "?")
|
||||
} else if strings.HasPrefix(remainder, "?") {
|
||||
rawQuery = remainder[1:]
|
||||
}
|
||||
|
||||
return dcrRedirectURIPattern{
|
||||
scheme: scheme,
|
||||
host: host,
|
||||
path: path,
|
||||
rawQuery: rawQuery,
|
||||
}, true
|
||||
}
|
||||
|
||||
func redirectURIMatchesGlobRecur(uri, pattern string, ui, pi int) bool {
|
||||
for pi < len(pattern) {
|
||||
if pattern[pi] == '*' {
|
||||
if pi+1 < len(pattern) && pattern[pi+1] == '*' {
|
||||
// ** matches any chars including /
|
||||
pi += 2
|
||||
if pi >= len(pattern) {
|
||||
return true
|
||||
}
|
||||
for ui <= len(uri) {
|
||||
if redirectURIMatchesGlobRecur(uri, pattern, ui, pi) {
|
||||
return true
|
||||
}
|
||||
ui++
|
||||
}
|
||||
return false
|
||||
}
|
||||
// * matches zero or more chars except /
|
||||
if redirectURIMatchesGlobRecur(uri, pattern, ui, pi+1) {
|
||||
return true
|
||||
}
|
||||
for ui < len(uri) && uri[ui] != '/' {
|
||||
ui++
|
||||
if redirectURIMatchesGlobRecur(uri, pattern, ui, pi+1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if ui >= len(uri) || uri[ui] != pattern[pi] {
|
||||
return false
|
||||
}
|
||||
ui++
|
||||
pi++
|
||||
}
|
||||
return ui == len(uri)
|
||||
}
|
||||
|
||||
// RedirectURIMatchesAllowlist returns true if uri matches at least one pattern in allowlist.
|
||||
// If allowlist is empty, returns true (no restriction).
|
||||
func RedirectURIMatchesAllowlist(uri string, allowlist []string) bool {
|
||||
if len(allowlist) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, p := range allowlist {
|
||||
trimmed := strings.TrimSpace(p)
|
||||
if trimmed != "" && RedirectURIMatchesGlob(uri, trimmed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user