feat(core): implement dynamic client registration for a2a oauth2

This commit is contained in:
Adam Weidman
2026-03-10 14:17:51 -04:00
parent 556825f81c
commit f787c0fb97
10 changed files with 338 additions and 170 deletions
+19 -10
View File
@@ -615,8 +615,9 @@ auth:
scopes:
- openid
- profile
authorization_url: https://auth.example.com/authorize
token_url: https://auth.example.com/token
endpoints:
authorization_url: https://auth.example.com/authorize
token_url: https://auth.example.com/token
---
`);
const result = await parseAgentMarkdown(filePath);
@@ -629,8 +630,10 @@ auth:
client_id: 'my-client-id',
client_secret: 'my-client-secret',
scopes: ['openid', 'profile'],
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
endpoints: {
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
},
},
});
});
@@ -663,7 +666,8 @@ agent_card_url: https://example.com/card
auth:
type: oauth2
client_id: my-client
authorization_url: not-a-valid-url
endpoints:
authorization_url: not-a-valid-url
---
`);
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(/Invalid url/);
@@ -677,7 +681,8 @@ agent_card_url: https://example.com/card
auth:
type: oauth2
client_id: my-client
token_url: not-a-valid-url
endpoints:
token_url: not-a-valid-url
---
`);
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(/Invalid url/);
@@ -692,8 +697,10 @@ auth:
type: 'oauth2' as const,
client_id: '$MY_CLIENT_ID',
scopes: ['read'],
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
endpoints: {
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
},
},
};
@@ -705,8 +712,10 @@ auth:
type: 'oauth2',
client_id: '$MY_CLIENT_ID',
scopes: ['read'],
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
endpoints: {
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
},
},
});
});
+60 -13
View File
@@ -44,7 +44,7 @@ interface FrontmatterLocalAgentDefinition
* Authentication configuration for remote agents in frontmatter format.
*/
interface FrontmatterAuthConfig {
type: 'apiKey' | 'http' | 'oauth2';
type: string;
agent_card_requires_auth?: boolean;
// API Key
key?: string;
@@ -56,11 +56,23 @@ interface FrontmatterAuthConfig {
password?: string;
value?: string;
// OAuth2
grant_type?: 'authorization_code' | 'client_credentials' | 'device_code';
client_id?: string;
client_secret?: string;
scopes?: string[];
authorization_url?: string;
token_url?: string;
registration_url?: string;
device_authorization_url?: string;
endpoints?: {
authorization_url?: string;
token_url?: string;
device_authorization_url?: string;
registration_url?: string;
};
client_type?: 'static' | 'dynamic';
client_name?: string;
registration_token?: string;
}
interface FrontmatterRemoteAgentDefinition
@@ -153,18 +165,32 @@ const httpAuthSchema = z.object({
value: z.string().min(1).optional(),
});
const oauth2EndpointsSchema = z.object({
authorization_url: z.string().url().optional(),
token_url: z.string().url().optional(),
device_authorization_url: z.string().url().optional(),
registration_url: z.string().url().optional(),
});
/**
* OAuth2 auth schema.
* authorization_url and token_url can be discovered from the agent card if omitted.
* endpoints can be discovered from the agent card if omitted.
*/
const oauth2AuthSchema = z.object({
...baseAuthFields,
type: z.literal('oauth2'),
client_id: z.string().optional(),
client_secret: z.string().optional(),
grant_type: z
.enum(['authorization_code', 'client_credentials', 'device_code'])
.optional(),
scopes: z.array(z.string()).optional(),
authorization_url: z.string().url().optional(),
token_url: z.string().url().optional(),
endpoints: oauth2EndpointsSchema.optional(),
// Client configuration
client_type: z.enum(['static', 'dynamic']).optional(),
client_id: z.string().optional(), // Technically required for 'static', validated at type-level
client_secret: z.string().optional(),
client_name: z.string().optional(),
registration_token: z.string().optional(),
});
const authConfigSchema = z
@@ -419,20 +445,41 @@ function convertFrontmatterAuthToConfig(
}
}
case 'oauth2':
case 'oauth2': {
const endpoints = frontmatter.endpoints || {
authorization_url: frontmatter.authorization_url,
token_url: frontmatter.token_url,
registration_url: frontmatter.registration_url,
device_authorization_url: frontmatter.device_authorization_url,
};
if (frontmatter.client_type === 'dynamic') {
return {
...base,
type: 'oauth2',
grant_type: frontmatter.grant_type,
scopes: frontmatter.scopes,
endpoints,
client_type: 'dynamic',
client_name: frontmatter.client_name,
registration_token: frontmatter.registration_token,
};
}
return {
...base,
type: 'oauth2',
client_id: frontmatter.client_id,
client_secret: frontmatter.client_secret,
grant_type: frontmatter.grant_type,
scopes: frontmatter.scopes,
authorization_url: frontmatter.authorization_url,
token_url: frontmatter.token_url,
endpoints,
client_type: 'static',
client_id: frontmatter.client_id || '',
client_secret: frontmatter.client_secret,
};
}
default: {
const exhaustive: never = frontmatter.type;
throw new Error(`Unknown auth type: ${exhaustive}`);
throw new Error(`Unknown auth type: ${frontmatter.type}`);
}
}
}
@@ -215,6 +215,8 @@ describe('A2AAuthProviderFactory', () => {
it('should match oauth2 config with oauth2 scheme', () => {
const authConfig: A2AAuthConfig = {
type: 'oauth2',
client_type: 'static',
client_id: 'test-client',
};
const securitySchemes: Record<string, SecurityScheme> = {
oauth2Auth: {
@@ -510,8 +512,10 @@ describe('A2AAuthProviderFactory', () => {
authConfig: {
type: 'oauth2',
client_id: 'my-client',
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
endpoints: {
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
},
scopes: ['read'],
},
});
@@ -552,8 +556,10 @@ describe('A2AAuthProviderFactory', () => {
authConfig: {
type: 'oauth2',
client_id: 'my-client',
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
endpoints: {
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
},
},
});
@@ -33,6 +33,7 @@ vi.mock('../../mcp/oauth-token-storage.js', () => {
});
vi.mock('../../utils/oauth-flow.js', () => ({
REDIRECT_PATH: '/oauth/callback',
generatePKCEParams: vi.fn().mockReturnValue({
codeVerifier: 'test-verifier',
codeChallenge: 'test-challenge',
@@ -102,8 +103,10 @@ function createConfig(
return {
type: 'oauth2',
client_id: 'test-client-id',
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
endpoints: {
authorization_url: 'https://auth.example.com/authorize',
token_url: 'https://auth.example.com/token',
},
scopes: ['read', 'write'],
...overrides,
};
@@ -133,8 +136,10 @@ describe('OAuth2AuthProvider', () => {
it('should use config values for authorization_url and token_url', () => {
const config = createConfig({
authorization_url: 'https://custom.example.com/authorize',
token_url: 'https://custom.example.com/token',
endpoints: {
authorization_url: 'https://custom.example.com/authorize',
token_url: 'https://custom.example.com/token',
},
});
const provider = new OAuth2AuthProvider(config, 'test-agent');
// Verify by calling headers which will trigger interactive flow with these URLs.
@@ -143,8 +148,10 @@ describe('OAuth2AuthProvider', () => {
it('should merge agent card defaults when config values are missing', () => {
const config = createConfig({
authorization_url: undefined,
token_url: undefined,
endpoints: {
authorization_url: undefined,
token_url: undefined,
},
scopes: undefined,
});
@@ -169,8 +176,10 @@ describe('OAuth2AuthProvider', () => {
it('should prefer config values over agent card values', async () => {
const config = createConfig({
authorization_url: 'https://config.example.com/authorize',
token_url: 'https://config.example.com/token',
endpoints: {
authorization_url: 'https://config.example.com/authorize',
token_url: 'https://config.example.com/token',
},
scopes: ['custom-scope'],
});
@@ -389,8 +398,10 @@ describe('OAuth2AuthProvider', () => {
it('should throw when authorization_url and token_url are missing', async () => {
const config = createConfig({
authorization_url: undefined,
token_url: undefined,
endpoints: {
authorization_url: undefined,
token_url: undefined,
},
});
const provider = new OAuth2AuthProvider(config, 'test-agent');
await provider.initialize();
@@ -538,8 +549,10 @@ describe('OAuth2AuthProvider', () => {
describe('agent card integration', () => {
it('should discover URLs from agent card when not in config', async () => {
const config = createConfig({
authorization_url: undefined,
token_url: undefined,
endpoints: {
authorization_url: undefined,
token_url: undefined,
},
scopes: undefined,
});
@@ -576,8 +589,10 @@ describe('OAuth2AuthProvider', () => {
it('should discover URLs from agentCardUrl via DefaultAgentCardResolver during initialize', async () => {
const config = createConfig({
authorization_url: undefined,
token_url: undefined,
endpoints: {
authorization_url: undefined,
token_url: undefined,
},
scopes: undefined,
});
@@ -625,8 +640,10 @@ describe('OAuth2AuthProvider', () => {
it('should ignore agent card with no authorizationCode flow', () => {
const config = createConfig({
authorization_url: undefined,
token_url: undefined,
endpoints: {
authorization_url: undefined,
token_url: undefined,
},
});
const agentCard = {
@@ -13,10 +13,11 @@ import type { OAuthToken } from '../../mcp/token-storage/types.js';
import {
generatePKCEParams,
startCallbackServer,
getPortFromUrl,
buildAuthorizationUrl,
exchangeCodeForToken,
refreshAccessToken,
registerDynamicClient,
REDIRECT_PATH,
type OAuthFlowConfig,
} from '../../utils/oauth-flow.js';
import { openBrowserSecurely } from '../../utils/secure-browser-launcher.js';
@@ -38,11 +39,14 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
private readonly tokenStorage: MCPOAuthTokenStorage;
private cachedToken: OAuthToken | null = null;
private dynamicClientId: string | undefined;
private dynamicClientSecret: string | undefined;
/** Resolved OAuth URLs — may come from config or agent card. */
private authorizationUrl: string | undefined;
private tokenUrl: string | undefined;
private scopes: string[] | undefined;
private registrationUrl: string | undefined;
constructor(
private readonly config: OAuth2AuthConfig,
@@ -57,8 +61,9 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
);
// Seed from user config.
this.authorizationUrl = config.authorization_url;
this.tokenUrl = config.token_url;
this.authorizationUrl = config.endpoints?.authorization_url;
this.tokenUrl = config.endpoints?.token_url;
this.registrationUrl = config.endpoints?.registration_url;
this.scopes = config.scopes;
// Fall back to agent card's OAuth2 security scheme if user config is incomplete.
@@ -76,14 +81,32 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
}
const credentials = await this.tokenStorage.getCredentials(this.agentName);
if (credentials && !this.tokenStorage.isTokenExpired(credentials.token)) {
this.cachedToken = credentials.token;
debugLogger.debug(
`[OAuth2AuthProvider] Loaded valid cached token for "${this.agentName}"`,
);
if (credentials) {
if (this.config.client_type === 'dynamic') {
this.dynamicClientId = credentials.clientId;
}
if (!this.tokenStorage.isTokenExpired(credentials.token)) {
this.cachedToken = credentials.token;
debugLogger.debug(
`[OAuth2AuthProvider] Loaded valid cached token for "${this.agentName}"`,
);
}
}
}
private get clientId(): string | undefined {
return this.config.client_type === 'dynamic'
? this.dynamicClientId
: this.config.client_id;
}
private get clientSecret(): string | undefined {
return this.config.client_type === 'dynamic'
? this.dynamicClientSecret
: this.config.client_secret;
}
/**
* Return an Authorization header with a valid Bearer token.
* Refreshes or triggers interactive auth as needed.
@@ -98,16 +121,12 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
}
// 2. Expired but has refresh token → attempt silent refresh.
if (
this.cachedToken?.refreshToken &&
this.tokenUrl &&
this.config.client_id
) {
if (this.cachedToken?.refreshToken && this.tokenUrl && this.clientId) {
try {
const refreshed = await refreshAccessToken(
{
clientId: this.config.client_id,
clientSecret: this.config.client_secret,
clientId: this.clientId,
clientSecret: this.clientSecret,
scopes: this.scopes,
},
this.cachedToken.refreshToken,
@@ -209,12 +228,6 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
* Run a full OAuth 2.0 Authorization Code + PKCE flow through the browser.
*/
private async authenticateInteractively(): Promise<OAuthToken> {
if (!this.config.client_id) {
throw new Error(
`OAuth2 authentication for agent "${this.agentName}" requires a client_id. ` +
'Add client_id to the auth config in your agent definition.',
);
}
if (!this.authorizationUrl || !this.tokenUrl) {
throw new Error(
`OAuth2 authentication for agent "${this.agentName}" requires authorization_url and token_url. ` +
@@ -222,19 +235,66 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
);
}
const pkceParams = generatePKCEParams();
// We don't have a redirectUri in config yet (unlike MCP). Let's use a default one for port allocation
// If we want to allow configuring redirectUri, we can add it to OAuth2AuthConfig later.
const preferredPort = 0; // Let OS assign port
const callbackServer = startCallbackServer(pkceParams.state, preferredPort);
const redirectPort = await callbackServer.port;
if (!this.clientId) {
if (this.config.client_type === 'dynamic') {
if (!this.registrationUrl) {
throw new Error(
`OAuth2 dynamic registration for agent "${this.agentName}" requires registration_url. ` +
'Provide it in the auth config or ensure the agent card exposes an oauth2 security scheme with it.',
);
}
debugLogger.debug(
`[OAuth2AuthProvider] Attempting dynamic client registration for "${this.agentName}"...`,
);
const redirectUri = `http://localhost:${redirectPort}${REDIRECT_PATH}`;
const clientName =
this.config.client_name || `Gemini CLI A2A Agent - ${this.agentName}`;
const clientRegistration = await registerDynamicClient(
this.registrationUrl,
clientName,
redirectUri,
this.scopes,
);
this.dynamicClientId = clientRegistration.client_id;
if (clientRegistration.client_secret) {
this.dynamicClientSecret = clientRegistration.client_secret;
}
debugLogger.debug(
`[OAuth2AuthProvider] ✓ Dynamic client registration successful`,
);
} else {
throw new Error(
`OAuth2 authentication for agent "${this.agentName}" requires a client_id. ` +
'Add client_id to the auth config in your agent definition.',
);
}
}
// After dynamic registration, clientId should be populated
if (!this.clientId) {
throw new Error('Failed to resolve client_id for OAuth2 authentication');
}
const flowConfig: OAuthFlowConfig = {
clientId: this.config.client_id,
clientSecret: this.config.client_secret,
clientId: this.clientId,
clientSecret: this.clientSecret,
authorizationUrl: this.authorizationUrl,
tokenUrl: this.tokenUrl,
scopes: this.scopes,
redirectUri: `http://localhost:${redirectPort}${REDIRECT_PATH}`,
};
const pkceParams = generatePKCEParams();
const preferredPort = getPortFromUrl(flowConfig.redirectUri);
const callbackServer = startCallbackServer(pkceParams.state, preferredPort);
const redirectPort = await callbackServer.port;
const authUrl = buildAuthorizationUrl(
flowConfig,
pkceParams,
@@ -329,11 +389,11 @@ export class OAuth2AuthProvider extends BaseA2AAuthProvider {
* Persist the current cached token to disk.
*/
private async persistToken(): Promise<void> {
if (!this.cachedToken) return;
if (!this.cachedToken || !this.clientId) return;
await this.tokenStorage.saveToken(
this.agentName,
this.cachedToken,
this.config.client_id,
this.clientId,
this.tokenUrl,
);
}
@@ -68,18 +68,32 @@ export type HttpAuthConfig = BaseAuthConfig & {
}
);
/** Client config corresponding to OAuth2SecurityScheme. */
export interface OAuth2AuthConfig extends BaseAuthConfig {
type: 'oauth2';
client_id?: string;
client_secret?: string;
scopes?: string[];
/** Override or provide the authorization endpoint URL. Discovered from agent card if omitted. */
export interface OAuth2Endpoints {
authorization_url?: string;
/** Override or provide the token endpoint URL. Discovered from agent card if omitted. */
token_url?: string;
device_authorization_url?: string;
registration_url?: string;
}
/** Client config corresponding to OAuth2SecurityScheme. */
export type OAuth2AuthConfig = BaseAuthConfig & {
type: 'oauth2';
grant_type?: 'authorization_code' | 'client_credentials' | 'device_code';
scopes?: string[];
endpoints?: OAuth2Endpoints;
} & (
| {
client_type?: 'static';
client_id: string;
client_secret?: string;
}
| {
client_type: 'dynamic';
client_name?: string;
registration_token?: string;
}
);
/** Client config corresponding to OpenIdConnectSecurityScheme. */
export interface OpenIdConnectAuthConfig extends BaseAuthConfig {
type: 'openIdConnect';
@@ -375,10 +375,6 @@ describe('MCPOAuthProvider', () => {
const mockRegistrationResponse: OAuthClientRegistrationResponse = {
client_id: 'dynamic_client_id',
client_secret: 'dynamic_client_secret',
redirect_uris: ['http://localhost:7777/oauth/callback'],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
};
mockFetch.mockResolvedValueOnce(
@@ -447,10 +443,6 @@ describe('MCPOAuthProvider', () => {
const mockRegistrationResponse: OAuthClientRegistrationResponse = {
client_id: 'dynamic_client_id',
client_secret: 'dynamic_client_secret',
redirect_uris: ['http://localhost:7777/oauth/callback'],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
};
const mockAuthServerMetadata: OAuthAuthorizationServerMetadata = {
@@ -550,10 +542,6 @@ describe('MCPOAuthProvider', () => {
const mockRegistrationResponse: OAuthClientRegistrationResponse = {
client_id: 'dynamic_client_id',
client_secret: 'dynamic_client_secret',
redirect_uris: ['http://localhost:7777/oauth/callback'],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
};
mockFetch
+10 -75
View File
@@ -24,12 +24,15 @@ import {
REDIRECT_PATH,
type OAuthFlowConfig,
type OAuthTokenResponse,
registerDynamicClient,
} from '../utils/oauth-flow.js';
// Re-export types that were moved to oauth-flow.ts for backward compatibility.
export type {
OAuthAuthorizationResponse,
OAuthTokenResponse,
OAuthClientRegistrationRequest,
OAuthClientRegistrationResponse,
} from '../utils/oauth-flow.js';
/**
@@ -49,33 +52,6 @@ export interface MCPOAuthConfig {
registrationUrl?: string;
}
/**
* Dynamic client registration request (RFC 7591).
*/
export interface OAuthClientRegistrationRequest {
client_name: string;
redirect_uris: string[];
grant_types: string[];
response_types: string[];
token_endpoint_auth_method: string;
scope?: string;
}
/**
* Dynamic client registration response (RFC 7591).
*/
export interface OAuthClientRegistrationResponse {
client_id: string;
client_secret?: string;
client_id_issued_at?: number;
client_secret_expires_at?: number;
redirect_uris: string[];
grant_types: string[];
response_types: string[];
token_endpoint_auth_method: string;
scope?: string;
}
/**
* Provider for handling OAuth authentication for MCP servers.
*/
@@ -86,51 +62,6 @@ export class MCPOAuthProvider {
this.tokenStorage = tokenStorage;
}
/**
* Register a client dynamically with the OAuth server.
*
* @param registrationUrl The client registration endpoint URL
* @param config OAuth configuration
* @param redirectPort The port to use for the redirect URI
* @returns The registered client information
*/
private async registerClient(
registrationUrl: string,
config: MCPOAuthConfig,
redirectPort: number,
): Promise<OAuthClientRegistrationResponse> {
const redirectUri =
config.redirectUri || `http://localhost:${redirectPort}${REDIRECT_PATH}`;
const registrationRequest: OAuthClientRegistrationRequest = {
client_name: 'Gemini CLI MCP Client',
redirect_uris: [redirectUri],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none', // Public client
scope: config.scopes?.join(' ') || '',
};
// eslint-disable-next-line no-restricted-syntax -- TODO: Migrate to safeFetch for SSRF protection
const response = await fetch(registrationUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(registrationRequest),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Client registration failed: ${response.status} ${response.statusText} - ${errorText}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return (await response.json()) as OAuthClientRegistrationResponse;
}
/**
* Discover OAuth configuration from an MCP server URL.
*
@@ -401,10 +332,14 @@ export class MCPOAuthProvider {
// Register client if registration endpoint is available
if (registrationUrl) {
const clientRegistration = await this.registerClient(
const redirectUri =
config.redirectUri ||
`http://localhost:${redirectPort}${REDIRECT_PATH}`;
const clientRegistration = await registerDynamicClient(
registrationUrl,
config,
redirectPort,
'Gemini CLI MCP Client',
redirectUri,
config.scopes,
);
config.clientId = clientRegistration.client_id;
+66
View File
@@ -67,6 +67,72 @@ export interface OAuthTokenResponse {
scope?: string;
}
/**
* Dynamic client registration request (RFC 7591).
*/
export interface OAuthClientRegistrationRequest {
client_name: string;
redirect_uris: string[];
grant_types: string[];
response_types: string[];
token_endpoint_auth_method: string;
scope?: string;
}
/**
* Dynamic client registration response (RFC 7591).
*/
export interface OAuthClientRegistrationResponse {
client_id: string;
client_secret?: string;
client_id_issued_at?: number;
client_secret_expires_at?: number;
}
/**
* Register a client dynamically with the OAuth server.
*
* @param registrationUrl The client registration endpoint URL
* @param clientName The name of the client to register
* @param redirectUri The callback URI
* @param scopes Optional array of scopes
* @returns The registered client information
*/
export async function registerDynamicClient(
registrationUrl: string,
clientName: string,
redirectUri: string,
scopes?: string[],
): Promise<OAuthClientRegistrationResponse> {
const registrationRequest: OAuthClientRegistrationRequest = {
client_name: clientName,
redirect_uris: [redirectUri],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none', // Public client
scope: scopes?.join(' ') || '',
};
// eslint-disable-next-line no-restricted-syntax -- TODO: Migrate to safeFetch for SSRF protection
const response = await fetch(registrationUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(registrationRequest),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Client registration failed: ${response.status} ${response.statusText} - ${errorText}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return (await response.json()) as OAuthClientRegistrationResponse;
}
/** The path the local callback server listens on. */
export const REDIRECT_PATH = '/oauth/callback';