Skip to content

Conversation

zoedsoupe
Copy link
Contributor

@zoedsoupe zoedsoupe commented Jul 18, 2025

This pull request introduces a comprehensive implementation of OAuth 2.1 authorization for the Hermes server, including token validation, audience checks, expiry handling, and support for multiple validation methods (JWT, introspection, etc.). It also adds utility modules for authorization configuration and metadata handling, and updates existing code for consistency with the new functionality.

OAuth 2.1 Authorization Implementation

Core Authorization Features:

  • lib/hermes/server/authorization.ex: Implements token validation logic, including audience checks, expiry validation, and scope enforcement. Provides utility functions to parse authorization configurations and build WWW-Authenticate headers for 401 responses.

Validation Methods:

  • lib/hermes/server/authorization/introspection_validator.ex: Adds support for validating opaque tokens using the OAuth 2.0 Token Introspection endpoint (RFC 7662). Includes real-time revocation detection and client authentication.
  • lib/hermes/server/authorization/jwt_validator.ex: Implements JWT validation using public keys fetched from JWKS endpoints. Supports RSA and ECDSA algorithms, caching, and standard claims validation (e.g., exp, iss, aud).

Plug Integration:

  • lib/hermes/server/authorization/plug.ex: Provides a Plug module for integrating authorization into the request pipeline. Handles token extraction, validation, and metadata responses for well-known paths.

Supporting Changes

Custom Validators:

  • lib/hermes/server/authorization/validator.ex: Defines a behavior for creating custom token validators, enabling extensibility for different validation mechanisms.

Code Consistency:

  • lib/hermes/server/component/resource.ex: Replaces Jason.encode! with JSON.encode! for consistency across the codebase.

Documentation Update:

  • README.md: Adds an example of a plug-based MCP server implementation with OAuth authorization.

@zoedsoupe zoedsoupe self-assigned this Jul 18, 2025
@zoedsoupe zoedsoupe requested a review from Copilot July 18, 2025 19:36
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a comprehensive OAuth 2.1 authorization implementation for the Hermes MCP server, enabling secure authentication and authorization for HTTP transports using bearer tokens, JWT validation, and introspection methods.

  • Implements OAuth 2.1 authorization with pluggable validators (JWT and introspection)
  • Adds comprehensive authentication/authorization utilities and frame helpers
  • Includes a complete example application demonstrating OAuth integration

Reviewed Changes

Copilot reviewed 32 out of 34 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
lib/hermes/server/authorization.ex Core authorization module with config parsing and validation logic
lib/hermes/server/authorization/plug.ex Plug implementation for HTTP transport authorization
lib/hermes/server/authorization/jwt_validator.ex JWT token validator with JWKS support
lib/hermes/server/authorization/introspection_validator.ex OAuth introspection validator
lib/hermes/server/authorization/validator.ex Validator behavior definition
lib/hermes/server/frame.ex Adds auth helper functions to Frame module
lib/hermes/server/transport/streamable_http/plug.ex Integrates authorization into HTTP transport
lib/hermes/server/component/resource.ex Updates JSON encoding for consistency
priv/dev/oauth_example/* Complete OAuth example application
test/* Test updates for JSON encoding consistency

Comment on lines +69 to +76
case Process.get(cache_key) do
{jwks, expires_at} when expires_at > now ->
{:ok, jwks}

_ ->
with {:ok, jwks} <- fetch_jwks_from_uri(jwks_uri) do
expires_at = System.monotonic_time(:millisecond) + @jwks_cache_ttl
Process.put(cache_key, {jwks, expires_at})
Copy link

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Process.put for caching creates process-local storage that won't be shared across different processes. For a production application, consider using ETS or a proper cache like ConCache to enable shared caching.

Suggested change
case Process.get(cache_key) do
{jwks, expires_at} when expires_at > now ->
{:ok, jwks}
_ ->
with {:ok, jwks} <- fetch_jwks_from_uri(jwks_uri) do
expires_at = System.monotonic_time(:millisecond) + @jwks_cache_ttl
Process.put(cache_key, {jwks, expires_at})
case :ets.lookup(:jwks_cache, cache_key) do
[{^cache_key, {jwks, expires_at}}] when expires_at > now ->
{:ok, jwks}
_ ->
with {:ok, jwks} <- fetch_jwks_from_uri(jwks_uri) do
expires_at = System.monotonic_time(:millisecond) + @jwks_cache_ttl
:ets.insert(:jwks_cache, {cache_key, {jwks, expires_at}})

Copilot uses AI. Check for mistakes.

{_, port} -> ":#{port}"
end

"#{scheme}://#{conn.host}#{port_part}#{conn.request_path}"
Copy link

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using conn.request_path in the server URI for audience validation could be manipulated by clients. Consider using a configured base URI or only the scheme/host/port portions.

Suggested change
"#{scheme}://#{conn.host}#{port_part}#{conn.request_path}"
base_uri = config[:base_uri] || "#{scheme}://#{conn.host}#{port_part}"
base_uri

Copilot uses AI. Check for mistakes.

String.replace(string, ~s("), ~s(\\"))
end

defp get_canonical_server_uri do
Copy link

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns a placeholder URI that should be configurable. Consider making this part of the authorization configuration rather than relying on an environment variable.

Copilot uses AI. Check for mistakes.

{:ok,
%{
sub: "user_read_only",
aud: "https://localhost:4001",
Copy link

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded audience URL should be configurable or extracted to a module attribute to avoid duplication and make it easier to change.

Suggested change
aud: "https://localhost:4001",
aud: @audience_url,

Copilot uses AI. Check for mistakes.


children = [
Hermes.Server.Registry,
{OauthExample.Server, transport: {:streamable_http, authorization: auth_config}},
Copy link

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The authorization configuration could be extracted to application config (config/config.exs) to make it easier to manage different environments.

Copilot uses AI. Check for mistakes.

@zoedsoupe zoedsoupe linked an issue Jul 18, 2025 that may be closed by this pull request
@zoedsoupe zoedsoupe marked this pull request as draft July 21, 2025 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Client] Implement Authorization support (MCP 2025-03-26)

2 participants