Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ demo_token.json
## misc
AuthTokens/
Sample/AuthTokens/

.nuget/
temp/
16 changes: 0 additions & 16 deletions MCPify/Core/Auth/IAccessTokenValidator.cs

This file was deleted.

203 changes: 0 additions & 203 deletions MCPify/Core/Auth/JwtAccessTokenValidator.cs

This file was deleted.

100 changes: 35 additions & 65 deletions MCPify/Core/Auth/ScopeRequirement.cs
Original file line number Diff line number Diff line change
@@ -1,99 +1,69 @@
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authorization;

namespace MCPify.Core.Auth;

/// <summary>
/// Defines scope requirements for a tool or endpoint.
/// Authorization requirement that enforces the presence of a matching OAuth scope.
/// Supports simple glob-style patterns ("*" and "?").
/// </summary>
public class ScopeRequirement
public sealed class ScopeRequirement : IAuthorizationRequirement, IAuthorizationRequirementData
{
/// <summary>
/// Tool name pattern to match. Supports wildcards:
/// - '*' matches any sequence of characters
/// - '?' matches any single character
/// Examples: "admin_*", "api_get_*", "tool_name"
/// </summary>
public required string Pattern { get; init; }
private string _pattern = "*";

/// <summary>
/// Scopes that must ALL be present in the token.
/// If empty, only <see cref="AnyOfScopes"/> is checked.
/// Initializes a new instance of the <see cref="ScopeRequirement"/> class.
/// </summary>
public List<string> RequiredScopes { get; init; } = new();
public ScopeRequirement()
{
}

/// <summary>
/// At least ONE of these scopes must be present in the token.
/// If empty, only <see cref="RequiredScopes"/> is checked.
/// Initializes a new instance of the <see cref="ScopeRequirement"/> class.
/// </summary>
public List<string> AnyOfScopes { get; init; } = new();

private Regex? _compiledPattern;
/// <param name="pattern">The scope pattern that must match the granted scopes.</param>
public ScopeRequirement(string pattern)
{
Pattern = pattern;
}

/// <summary>
/// Checks if this requirement applies to the given tool name.
/// Gets or sets the scope pattern required for the protected resource.
/// The pattern supports '*' (zero or more characters) and '?' (single character) wildcards.
/// </summary>
public bool Matches(string toolName)
public string Pattern
{
_compiledPattern ??= CompilePattern(Pattern);
return _compiledPattern.IsMatch(toolName);
get => _pattern;
init => _pattern = string.IsNullOrWhiteSpace(value) ? "*" : value;
}

/// <summary>
/// Validates that the provided scopes satisfy this requirement.
/// Evaluates whether the supplied scope value satisfies this requirement.
/// </summary>
/// <param name="tokenScopes">Scopes from the access token.</param>
/// <returns>True if the scopes satisfy the requirement, false otherwise.</returns>
public bool IsSatisfiedBy(IEnumerable<string> tokenScopes)
/// <param name="scope">The scope value to evaluate.</param>
public bool Matches(string? scope)
{
var scopeSet = new HashSet<string>(tokenScopes, StringComparer.OrdinalIgnoreCase);

// Check RequiredScopes - all must be present
if (RequiredScopes.Count > 0)
if (scope is null)
{
foreach (var required in RequiredScopes)
{
if (!scopeSet.Contains(required))
{
return false;
}
}
return false;
}

// Check AnyOfScopes - at least one must be present
if (AnyOfScopes.Count > 0)
if (_pattern == "*")
{
var hasAny = AnyOfScopes.Any(s => scopeSet.Contains(s));
if (!hasAny)
{
return false;
}
return true;
}

return true;
}
var comparison = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
var regexPattern = "^" + Regex.Escape(_pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$";

/// <summary>
/// Gets all scopes that are required by this requirement (for error messages).
/// </summary>
public IEnumerable<string> GetAllRequiredScopes()
{
foreach (var scope in RequiredScopes)
{
yield return scope;
}
foreach (var scope in AnyOfScopes)
{
yield return scope;
}
return Regex.IsMatch(scope, regexPattern, comparison);
}

private static Regex CompilePattern(string pattern)
/// <inheritdoc />
public IEnumerable<IAuthorizationRequirement> GetRequirements()
{
// Escape regex special characters except * and ?
var escaped = Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".");

return new Regex($"^{escaped}$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
yield return this;
}
}
Loading