The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Use MCP OAuth2 Flow to access Quarkus MCP Server

Introducción

Back in April 2025, in the Getting ready for secure MCP with Quarkus MCP Server blog post, we explained how to enforce MCP client authentication with the Quarkus MCP Server by configuring it to verify bearer access tokens.

At the time, we worked against the old 2025-03-26 version of the MCP Authorization specification that expected compliant MCP servers to manage OAuth2 flows themselves either directly or via the delegation, with that idea being disputed due to its complexity, and with no MCP clients providing the OAuth2 authorization code flow support being available. Therefore, in the Getting ready for secure MCP with Quarkus MCP Server blog post, the access tokens were acquired out of band: we used Keycloak DevUI to get an access token and copy it to MCP Server DevUI to test it in devmode, and did a GitHub login to the Quarkus REST endpoint in order to copy and test a GitHub access token with both MCP Inspector and curl in prod mode.

The latest 2025-03-26 version of the MCP Authorization specification offers a simpler, better version of how OAuth2 must be supported in MCP. The focus has shifted to MCP clients that are now expected to drive the OAuth2 flows, while MCP servers are only required to support automating such flows by providing OAuth2 Protected Resource Metadata, as well as correctly verifying the actual access tokens.

In this blog post, we will explain how MCP clients compliant with the latest 2025-03-26 version of the MCP Authorization specification can login users using an OAuth2 authorization code flow, acquire access tokens and use them to access secure Quarkus MCP Streamable HTTP servers on behalf of the logged-in users.

Currently, MCP Inspector offers the most advanced, adaptable, and accessible MCP OAuth2 authorization code flow support, even if somewhat unstable between its different versions, and therefore we will work with it in this post. You are welcome to experiment with other MCP client implementations.

We will demonstrate a great Quarkus MCP Server capability to support multiple MCP HTTP configurations, each one with their own unique OAuth2 or OpenId Connect security constraints, effectively allowing for a multi-tenant security control of tools, prompts and resources.

Keycloak will be used to support two distint security realms, with the security of each of the MCP HTTP configurations controlled by its own Keycloak realm. You are welcome to try to secure Quarkus MCP Server with other preferred OAuth2 or OpenID Connect providers by replacing the Keycloak specific configurations.

Demo MCP OAuth2 Flow Diagram

You can read all about the MCP OAuth2 Authorization Flow in the Authorization Flow section of the latest specification.

In this section, we are going to have a look at a simplified diagram showing how MCP Inspector can use OAuth 2.0 Flow to login a user to Keycloak, get an access token and use it to access a secure Quarkus MCP Server endpoint.

Demo Flow Diagram

MCP Client such as MCP Inspector requires configuring an MCP Streamable HTTP endpoint URL, OAuth2 Client ID, and optional scopes to access the MCP server securely. And as you can see, a lot happens from the moment you press Connect until a valid access token is sent to the MCP server.

MCP Client starts by accessing the MCP server without a token and gets back HTTP 401 with a WWW-Authenticate resource_metadata parameter that links to the MCP server’s OAuth2 Protected Resource Metadata route. The client now fetches a base URL of the Keycloak realm that secures the MCP server as well as the MCP server’s resource identifier.

Next, MCP Client uses the Keycloak realm’s URL to discover this realm’s authorization and token endpoint URLs, supported Proof Key for Code Exchange (PKCE) methods, and other metadata properties.

The user is now redirected to Keycloak to login into the required realm. The Keycloak redirect URL includes the configured OAuth2 client id, scopes, callback URI which points to the http://localhost:6274/oauth/callback endpoint managed by the MCP client, as well as the earlier discovered MCP Server’s resource identifier as an OAuth2 Resource Indicator. Generated PKCE code challenge and state parameters are also included in the redirect.

The user logs in, is redirected back to the http://localhost:6274/oauth/callback endpoint, MCP client exchanges the returned code to get ID and access tokens, and uses the access token to access the MCP server, allowing the user to select and run the tool.

MCP Authorization Specification also recommends that MCP clients support OAuth2 Dynamic Client Registration and MCP Inspector does support it.

In this post, we are only going to look at a case where OAuth2 Client ID is already known in advance, which is likely to be a typical case in production where OIDC client applications are created in advance.

We will also look at how MCP Inspector does OAuth2 Dynamic Client Registration in the next post in this MCP Security series.

MCP Authorization Flow is rather neatly defined, requiring the use of such OAuth2 specifications as OAuth2 Protected Resource Metadata, OAuth2 Resource Indicator, and also recommending the use of OAuth2 Dynamic Client Registration.

Please note though that the actual flow is not that unique to the MCP Authorization. It is a typical Single-page application (SPA) OAuth2 authorization code flow in action:

Typical SPA OAuth2 Flow

SPA uses a provider such as Keycloak to login users and use acquired access tokens to access Quarkus Service on their behalf - typical OAuth2 done at the SPA level. In this diagram, you can replace SPA with MCP Client, Quarkus Service with MCP Server and you’ll get a close enough match with the demo flow diagram in the previous image.

The comparison between the MCP Authorization and SPA OAuth2 flows implies that the MCP Authorization specification targets generic SPA AI and MCP client applications such as MCP Inspector, Claude AI, Cursor, and others that can plugin MCP servers. It does not currently apply to Quarkus MCP Client which typically runs in scope of the higher-level Quarkus LangChain4j server application with its own authentication requirements, you can read more about it in the Use Quarkus MCP client to access secure MCP HTTP servers blog post.

We are now ready to have a look at how it works in the demo.

You can find the complete project source in the Multiple Secure Quarkus MCP HTTP Servers sample.

Step 1 - Create and start MCP server with two secure Streamable HTTP endpoints

First, let’s create a secure Quarkus MCP server and configure two Streamable HTTP endpoints with their own unique security authentication controls.

MCP server maven dependencies

Add the following dependencies:

<dependency>
    <groupId>io.quarkiverse.mcp</groupId>
    <artifactId>quarkus-mcp-server-sse</artifactId> (1)
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId> (2)
</dependency>
1 quarkus-mcp-server-sse is required to support both MCP Streamable HTTP and SSE transports.
2 quarkus-oidc is required to secure access to MCP Server endpoints. Its version is defined in the Quarkus BOM.

MCP Server Configuration

Let’s configure the MCP server:

# First and default MCP server endpoint that we refer to as `alpha`
# Alternatively, we can have a named `alpha` endpoint, similarly to the second `bravo` endpoint

quarkus.mcp.server.sse.root-path=/mcp (1)

# Second MCP server endpoint that is explicitly named as `bravo`

quarkus.mcp.server.bravo.sse.root-path=/bravo/mcp (2)

# Require an authenticated access to both Streamable HTTP endpoints

quarkus.http.auth.permission.authenticated.paths=/mcp/*,/bravo/mcp/* (3)
quarkus.http.auth.permission.authenticated.policy=authenticated

# Default OIDC tenant that secures the default `alpha` Streamable HTTP endpoint
# Its required `quarkus.oidc.auth-server-url` property is set by Keycloak Dev Service
# and points to the Keycloak `alpha` realm endpoint

quarkus.oidc.tenant-paths=/mcp/* (4)
quarkus.oidc.token.audience=quarkus-mcp-alpha (5)
quarkus.oidc.resource-metadata.enabled=true (6)
quarkus.oidc.resource-metadata.force-https-scheme=false

# `Bravo` OIDC tenant that secures the `bravo` Streamable HTTP endpoint

quarkus.oidc.bravo.auth-server-url=${keycloak.url}/realms/bravo (7)
quarkus.oidc.bravo.tenant-paths=/bravo/mcp/* (7)
quarkus.oidc.bravo.token.audience=quarkus-mcp-bravo (8)
quarkus.oidc.bravo.resource-metadata.enabled=true (9)
quarkus.oidc.bravo.resource-metadata.resource=bravo/mcp (10)
quarkus.oidc.bravo.resource-metadata.force-https-scheme=false

# Keycloak devservice that supports both the default and `bravo` OIDC tenants.

quarkus.keycloak.devservices.realm-path=alpha-realm.json,bravo-realm.json (11)
quarkus.keycloak.devservices.realm-name=alpha (12)
quarkus.keycloak.devservices.create-client=false (13)

# CORS configuration to allow MCP Inspector's SPA script calls

quarkus.http.cors.enabled=true
quarkus.http.cors.origins=http://localhost:6274 (14)
1 Root path for the default alpha MCP server endpoint, with both Streamable HTTP and SSE endpoints available under this path.
2 Root path for the bravo MCP server endpoint, with both Streamable HTTP and SSE endpoints available under this path.
3 Require authentication for all requests to the alpha and bravo MCP server endpoints. This authentication policy is enforced by the matching OIDC tenant configurations.
4 Default OIDC tenant secures the default MCP server alpha endpoint, Keycloak DevService inserts a missing quarkus.oidc.auth-server-url property that links to the Keycloak alpha realm endpoint.
5 Require that tokens that are allowed to access the default MCP server alpha endpoint must have an audience (aud) claim that contains a quarkus-mcp-alpha value.
6 Enable the OAuth2 Protected Resource Metadata route for the default OIDC tenant. It will help MCP Inspector to find out about the authorization server that secures the default MCP server alpha endpoint.
7 OIDC bravo tenant secures the MCP server bravo endpoint. Its quarkus.oidc.bravo.auth-server-url property links to the Keycloak bravo realm endpoint.
8 Require that tokens that are allowed to access the MCP server bravo endpoint must have an audience (aud) claim that contains a quarkus-mcp-bravo value.
9 Enable the OAuth2 Protected Resource Metadata route for the OIDC bravo tenant. It will help MCP Inspector to find out about the the authorization server that secures the MCP server bravo endpoint.
10 Customize the relative path for OAuth2 Protected Resource Metadata route for the OIDC bravo tenant. By default, it is http://localhost:8080/bravo, however, MCP Inspector can not find this route and expects http://localhost:8080/bravo/mcp, so we just tune it a bit to make MCP Inspector happy.
11 Ask Keycloak DevService to upload two realms to the Keycloak container, alpha-realm.json and bravo-realm.json.
12 Keycloak DevService must set the default OIDC tenant properies, we point to alpha-realm.json for Keycloak DevService to use it to set properties such as quarkus.oidc.auth-server-url.
13 Ask Keycloak not to add quarkus.oidc.client-id. Using the realm verification keys, the configured audience, expiry checks is sufficient to verify Keycloak JWT access tokens; we also plan to deal with dynamically registered OIDC clients in the next blog post.
14 Allow MCP Inspector CORS requests.

You can read about how OAuth2 Protected Resource Metadata is supported in Quarkus OIDC in the Expanded OpenId Connect Configuration guide.

The Keycloak alpha and bravo realms represent unique, non-intersecting security configurations backed up by Keycloak. Both of these realms are represented by default and bravo OIDC tenants respectively. Quarkus OIDC uses its path-based tenant resolver to decide which OIDC tenant should handle the current MCP Server request.

You are welcome to update the default and bravo OIDC tenant configurations to point to your preferred providers instead of Keycloak, for example, to multiple Entra ID or Auth0 tenants, etc.

Please also check the Why was Keycloak preferred to GitHub in the demo ? section about the reasons behind preferring to use Keycloak in this demo, instead of GitHub that was used in the earlier Getting ready for secure MCP with Quarkus MCP Server blog post.

MCP Authorization specification requires that the token audience is validated. The specification prefers OAuth2 Resource Indicators to control the token audience.

For example, by default, the resource identifier of the default MCP server alpha endpoint is calculated as http://localhost:8080 and MCP Inspector includes it as a OAuth2 Resource Indicator resource parameter in the Keycloak redirect URL. The providers that already support the OAuth2 Resource Indicator specification can add the http://localhost:8080 resource indicator to the access token’s audience (aud) claim.

Keycloak does not support the OAuth2 Resource Indicator specification yet therefore we configure Keycloak to use predefined audience values specific to MCP server alpha and bravo endpoints. For our demo, the use of the custom audience values is non-ambiguous and sufficient.

When your OAuth2 provider start supporting the OAuth2 Resource Indicator specification, all you need to do to align with the MCP Authorization specification's requirement to use resource indicators is to update the OIDC tenant token audience configuration to contain an audience such as http://localhost:8080.

You can also harden it by requiring a token to have both a custom audience value such as quarkus-mcp-alpha and a resource value such as http://localhost:8080.

MCP User Name Provider tools

MCP Server has two Streamable HTTP endpoints. The MCP and security configuration for each of these endpoints allows to group tools, resources and prompts according to specific deployment requirements.

Let’s create two tools that can return a name of the current MCP Client user, one per each endpoint:

package org.acme;

import io.quarkiverse.mcp.server.TextContent;
import io.quarkiverse.mcp.server.Tool;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.McpServer;

public class ServerFeatures {

    @Inject
    SecurityIdentity identity; (1)

    @Tool(name = "alpha-user-name-provider", description = "Provides a name of the current user in the Alpha realm") (2)
    TextContent provideUserName() {
        return new TextContent(identity.getPrincipal().getName());
    }

    @Tool(name = "bravo-user-name-provider", description = "Provides a name of the current user in the Bravo realm") (3)
    @McpServer("bravo")
    TextContent provideUserName2() {
        return new TextContent(identity.getPrincipal().getName());
    }
}
1 Capture a security identity represented by the verified access token
2 The alpha-user-name-provider tool is accessible via the default Streamable HTTP alpha endpoint.
3 The bravo-user-name-provider tool is accessible via the bravo Streamable HTTP endpoint.

Both the alpha-user-name-provider and bravo-user-name-provider tools are very simple tools designed to show that the identities of MCP client users on whose behalf these tools are called by MCP clients is available to tools to perform a user identity specific action, an important element for a secure agentic AI system. Of course, the real world tool implementations will be more interesting.

Keycloak Configuration

The Keycloak configuration has already been prepared in the alpha-realm.json and bravo-realm.json realm files that Keycloak DevService uploads to Keycloak at the start-up time.

Let’s have a closer look. Please go to http://localhost:8080/q/dev-ui and select an OpenId Connect card:

Keycloak Admin

Click on Keycloak Admin, login as admin:admin and check the alpha and bravo realm configurations.

The alpha-realm.json has a single alpha-client client and a single user, alice with a password alice.

The alpha-client is a public client because its Client authentication option is disabled:

Public Keycloak Client

Typically, public SPA applications work with the public clients, to avoid having to deal with managing the confidential client’s secret.

The alpha-client is configured to support a callback URL provided by MCP Inspector:

Keycloak Alpha Client settings

The alpha-realm.json also has a custom quarkus-mcp-alpha client scope with an audience mapping, and it is assigned to the alfa-client client. It was done similarly to how it was done in the Use Quarkus MCP client to access secure MCP HTTP server from command line blog post. We start with creating a quarkus-mcp-server client scope:

Keycloak Client quarkus-mcp-alpha scope

Next, we create an audience mapping for this scope:

Keycloak Client quarkus-mcp-alpha scope mapping

Finally, we assign this client scope as an optional scope to the alpha-client client:

Keycloak alpha-client scope assignment

Similarly, the bravo-realm.json has a public bravo-client client, and a single user, jdoe with a password jdoe. It also has a custom quarkus-mcp-bravo client scope with an audience mapping.

Both realms have the client scopes with the audience mappings to let users request the correct token audience by configuring a custom scope in the MCP Inspector's OAuth2 Flow configuration. As implied in the MCP Server Configuration, it will be no longer necessary once the OAuth2 Resource Indicator specification is supported by Keycloak and other providers.

Why was Keycloak preferred to GitHub in the demo ?

You may be wondering, why did we choose Keycloak for this demo, instead of GitHub that we used in the earlier Getting ready for secure MCP with Quarkus MCP Server blog post ?

The main reason behind this is that the access tokens that are targeting MCP servers are expected to be designed to target MCP servers only. It is a good OAuth2 security recommendation. GitHub access tokens are meant to be used to access GitHub API, on behalf of the logged-in user, at the point where the login has happened, not via an MCP server indirection. For example, Claude AI offers a direct GitHub MCP integration.

This consideration applies to other social providers such as Google.

It is formally expressed in the MCP Authorization Access Token Privilege Restriction section: MCP servers MUST only accept tokens specifically intended for themselves…​.

If your MCP server really needs to accept a token that it will not use itself, for example, in order to forward it further downstream, then consider an option of exchanging tokens for the audiences to be correct through the whole distributed token call chain. Please check the Use Quarkus MCP client to access secure MCP HTTP server from command line blog post where we use the standard OAuth2 Token Exchange.

Start the MCP server in dev mode

Now let’s start the MCP server in dev mode:

mvn quarkus:dev
MCP server dev mode

You can see that default Streamable HTTP and SSE endpoints are available at http://localhost:8080/mcp and http://localhost:8080/mcp/sse respectively, while the bravo Streamable HTTP and SSE endpoints are available at http://localhost:8080/bravo/mcp and http://localhost:8080/bravo/mcp/sse respectively.

Step 2: Use MCP Inspector to access two secure MCP server endpoints

Start the MCP Inspector

npx @modelcontextprotocol/inspector@0.16.7

While MCP Inspector provides a very good OAuth2 Flow support, it is still a very active project and at the moment, you may observe MCP Inspector failing to connect to the OAuth2 provider in some versions.

MCP Inspector v0.16.7 has been proven to connect to Keycloak successfully and therefore we recommend you to use this version when working with this blog post.

We are now going to connect to two individual MCP Streamable HTTP endpoints in turn.

See the Demo MCP OAuth2 Flow Diagram section for an overview of how MCP Inspector performs a Connect request.

Please keep your browser’s Developer Tools Network tab open if you would like to observe how MCP Inspector probes various MCP server and Keycloak endpoints and eventually succeeds in getting a user logged in and acquiring the access token.

Connect to the default MCP Server alpha endpoint

MCP Inspector Alpha Connect

If your browser does not show an OAuth 2.0 Flow in the Authentication view in the loaded MCP Inspector v0.16.7, try latest Firefox.

Set Transport Type to Streamable HTTP, URL to the http://localhost:8080/mcp address of the default MCP server alpha endpoint.

In the OAuth 2.0 Flow authentication section, set the Client ID to alpha-client, and Scope to openid quarkus-mcp-alpha.

Requesting an openid scope is not strictly necessary in this demo, but OpenId Connect providers will not issue an ID token without it, only the access token, and you’ll likely need an SPA MCP Client to have access to the ID token in prod.

Requesting a quarkus-mcp-alpha scope is necessary for Keycloak to add a quarkus-mcp-alpha audience to the access token, please see how the quarkus-mcp-alpha client scope was created in the Keycloak Configuration section.

The Redirect URI is preconfigured by MCP Inspector and points to the MCP Inspector-managed http://localhost:6274/oauth callback endpoint where Keycloak will redirect the user to after the user login is complete.

Now press Connect.

As explained in the the Demo MCP OAuth2 Flow Diagram section, MCP Inspector starts by trying to access the default MCP Server Streamable HTTP alpha endpoint without a valid token and gets a 401 WWW-Authenticate challenge, with the resource_metadata parameter pointing to the alpha endpoint’s OAuth2 Protected Resource Metadata route.

MCP Inspector fetches the alpha endpoint’s protected resource metadata and finds out that it is secured by the Keycloak’s alpha realm.

MCP Inspector now discovers the Keycloak alpha realm’s metadata, and redirects you to Keycloak alpha realm’s authorization endpoint where you will see a Keycloak Alpha realm login challenge:

Alpha Realm Login

Login as alice:alice. Keycloak redirects you back to the MCP Inspector's http://localhost:6274/oauth endpoint. MCP Inspector exchanges the returned code for tokens and completes the authorization code flow.

The access token with a quarkus-mcp-alpha audience is now available, you can capture it using your browser’s Web Developer Tools and decode in JWT.io:

Alpha Client JWT

MCP Inspector uses this token to let you select and run the alpha-user-name-provider tool:

Alpha Tool Run

The way MCP Inspector was able to acquire the access token, knowing only the OAuth2 Client ID and the MCP server’s endpoint address was interesting. See the Demo MCP OAuth2 Flow Diagram section for the overview of how the whole OAuth2 flow works.

Now disconnect MCP Inspector from the MCP Server alpha endpoint by pressing a Disconnect button.

Connect to the MCP Server bravo endpoint

Connecting to the MCP Server bravo endpoint works exactly the same as with the default alpha endpoint, as explained in the Connect to the default MCP Server alpha endpoint section, we only need to use the MCP Server bravo endpoint related properties.

Set Transport Type to Streamable HTTP, URL to the http://localhost:8080/bravo/mcp address of the MCP server bravo endpoint.

In the OAuth 2.0 Flow authentication section, set the Client ID to bravo-client, and Scope to openid quarkus-mcp-bravo.

Keep Redirect URI set to http://localhost:6274/oauth.

Now press Connect.

MCP Inspector starts by trying to access the MCP Server bravo endpoint without a valid token and gets a 401 WWW-Authenticate challenge, with the resource_metadata parameter pointing to the `bravo’s OAuth2 Protected Resource Metadata route.

MCP Inspector fetches the bravo endpoint’s protected resource metadata and finds out that it is secured by the Keycloak’s bravo realm.

MCP Inspector now discovers the Keycloak bravo realm’s metadata, and redirects you to Keycloak bravo realm’s authorization endpoint where you will see a Keycloak Bravo realm login challenge:

Bravo Realm Login

Login as jdoe:jdoe. Keycloak redirects you back to the MCP Inspector's http://localhost:6274/oauth endpoint. MCP Inspector exchanges the returned code for tokens and completes the authorization code flow.

The access token with a quarkus-mcp-bravo audience is now available. MCP Inspector uses this token to let you select and run the bravo-user-name-provider tool:

Bravo Tool Run

See the Connect to the default MCP Server alpha endpoint section for more explanations of how MCP Inspector manages to connect to the MCP Server endpoint knowing only its URL and the OAuth2 Client ID.

Security Considerations

The main security consideration for secure Quarkus MCP server deployments is to ensure that access tokens have a correct audience, for the MCP Server to assert that the current token is meant to access this MCP server only. MCP Servers that propagate tokens further should consider exchanging such tokens, for a new token to target the downstream service correctly.

A token audience claim can have several values, and it must contain an OAuth2 Resource Indicator that points to a specific HTTP resource location or a custom audience value or both the resource indicator and the custom audience values.

One should also consider carefully if an MCP server should enable its OAuth2 Protected Resource Metadata route which allows a public access to the information about the authorization server that secures this MCP Server.

Please keep in mind that it might be considered sensitive information, especially when no SPA MCP Client applications are used, when the provider login themes can be customized to make it less obvious to users what is the actual provider that is used to log them in.

Conclusion

In this blog, we used MCP Inspector to demonstrate how MCP Client can use OAuth2 Flow to login users and access secure Quarkus MCP Streamable HTTP servers, when only an MCP Server address and OAuth2 Client ID can provide enough context for the flow to succeed.

We also demonstrated how Quarkus MCP Server can support multiple MCP HTTP configurations with their own unique security constraints supported with the Quarkus OIDC multi-tenancy resolver.

In the next blog post in this series, we will look at how MCP Authorization OAuth2 Flow can use OAuth Dynamic Client Registration and how Quarkus OIDC Proxy can play its part in securing Quarkus MCP Servers.

Enjoy, and stay tuned !