Conversation
There was a problem hiding this comment.
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 |
| 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}) |
There was a problem hiding this comment.
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.
| 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}}) |
| {_, port} -> ":#{port}" | ||
| end | ||
|
|
||
| "#{scheme}://#{conn.host}#{port_part}#{conn.request_path}" |
There was a problem hiding this comment.
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.
| "#{scheme}://#{conn.host}#{port_part}#{conn.request_path}" | |
| base_uri = config[:base_uri] || "#{scheme}://#{conn.host}#{port_part}" | |
| base_uri |
| String.replace(string, ~s("), ~s(\\")) | ||
| end | ||
|
|
||
| defp get_canonical_server_uri do |
There was a problem hiding this comment.
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.
| {:ok, | ||
| %{ | ||
| sub: "user_read_only", | ||
| aud: "https://localhost:4001", |
There was a problem hiding this comment.
The hardcoded audience URL should be configurable or extracted to a module attribute to avoid duplication and make it easier to change.
| aud: "https://localhost:4001", | |
| aud: @audience_url, |
|
|
||
| children = [ | ||
| Hermes.Server.Registry, | ||
| {OauthExample.Server, transport: {:streamable_http, authorization: auth_config}}, |
There was a problem hiding this comment.
[nitpick] The authorization configuration could be extracted to application config (config/config.exs) to make it easier to manage different environments.
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 aPlugmodule 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: ReplacesJason.encode!withJSON.encode!for consistency across the codebase.Documentation Update:
README.md: Adds an example of aplug-based MCP server implementation with OAuth authorization.