Add full OAuth provider functionality and enhance flows

- Implemented OAuth 2.0 Authorization Server endpoints per RFCs, including token, introspection, revocation, and metadata discovery.
- Added user consent submission, listing, and revocation APIs alongside frontend integration for improved UX.
- Enforced stricter OAuth security measures (PKCE, state validation, scopes).
- Refactored schemas and services for consistency and expanded coverage of OAuth workflows.
- Updated documentation and type definitions for new API behaviors.
This commit is contained in:
Felipe Cardoso
2025-11-26 13:23:44 +01:00
parent 707315facd
commit b3f0dd4005
14 changed files with 720 additions and 76 deletions

View File

View File

@@ -27,7 +27,11 @@ from slowapi import Limiter
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import get_current_active_user, get_current_superuser from app.api.dependencies.auth import (
get_current_active_user,
get_current_superuser,
get_optional_current_user,
)
from app.core.config import settings from app.core.config import settings
from app.core.database import get_db from app.core.database import get_db
from app.crud import oauth_client as oauth_client_crud from app.crud import oauth_client as oauth_client_crud
@@ -42,6 +46,8 @@ from app.schemas.oauth import (
from app.services import oauth_provider_service as provider_service from app.services import oauth_provider_service as provider_service
router = APIRouter() router = APIRouter()
# Separate router for RFC 8414 well-known endpoint (registered at root level)
wellknown_router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
limiter = Limiter(key_func=get_remote_address) limiter = Limiter(key_func=get_remote_address)
@@ -60,7 +66,7 @@ def require_provider_enabled():
# ============================================================================ # ============================================================================
@router.get( @wellknown_router.get(
"/.well-known/oauth-authorization-server", "/.well-known/oauth-authorization-server",
response_model=OAuthServerMetadata, response_model=OAuthServerMetadata,
summary="OAuth Server Metadata", summary="OAuth Server Metadata",
@@ -69,6 +75,8 @@ def require_provider_enabled():
Returns server metadata including supported endpoints, scopes, Returns server metadata including supported endpoints, scopes,
and capabilities. MCP clients use this to discover the server. and capabilities. MCP clients use this to discover the server.
Note: This endpoint is at the root level per RFC 8414.
""", """,
operation_id="get_oauth_server_metadata", operation_id="get_oauth_server_metadata",
tags=["OAuth Provider"], tags=["OAuth Provider"],
@@ -153,7 +161,7 @@ async def authorize(
nonce: str | None = Query(default=None, description="OpenID Connect nonce"), nonce: str | None = Query(default=None, description="OpenID Connect nonce"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_: None = Depends(require_provider_enabled), _: None = Depends(require_provider_enabled),
current_user: User | None = Depends(get_current_active_user), current_user: User | None = Depends(get_optional_current_user),
) -> Any: ) -> Any:
""" """
Authorization endpoint - initiates OAuth flow. Authorization endpoint - initiates OAuth flow.

View File

@@ -14,6 +14,7 @@ from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
from app.api.main import api_router from app.api.main import api_router
from app.api.routes.oauth_provider import wellknown_router as oauth_wellknown_router
from app.core.config import settings from app.core.config import settings
from app.core.database import check_database_health from app.core.database import check_database_health
from app.core.exceptions import ( from app.core.exceptions import (
@@ -324,3 +325,7 @@ async def health_check() -> JSONResponse:
app.include_router(api_router, prefix=settings.API_V1_STR) app.include_router(api_router, prefix=settings.API_V1_STR)
# OAuth 2.0 well-known endpoint at root level per RFC 8414
# This allows MCP clients to discover the OAuth server metadata at /.well-known/oauth-authorization-server
app.include_router(oauth_wellknown_router)

0
backend/app/models/oauth_account.py Normal file → Executable file
View File

0
backend/app/models/oauth_authorization_code.py Normal file → Executable file
View File

0
backend/app/models/oauth_client.py Normal file → Executable file
View File

0
backend/app/models/oauth_provider_token.py Normal file → Executable file
View File

0
backend/app/models/oauth_state.py Normal file → Executable file
View File

0
backend/app/services/oauth_provider_service.py Normal file → Executable file
View File

View File

@@ -291,9 +291,8 @@ class TestOAuthProviderEndpoints:
with patch("app.api.routes.oauth_provider.settings") as mock_settings: with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = False mock_settings.OAUTH_PROVIDER_ENABLED = False
response = await client.get( # RFC 8414: well-known endpoint is at root level
"/api/v1/oauth/.well-known/oauth-authorization-server" response = await client.get("/.well-known/oauth-authorization-server")
)
assert response.status_code == 404 assert response.status_code == 404
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -303,9 +302,8 @@ class TestOAuthProviderEndpoints:
mock_settings.OAUTH_PROVIDER_ENABLED = True mock_settings.OAUTH_PROVIDER_ENABLED = True
mock_settings.OAUTH_ISSUER = "https://api.example.com" mock_settings.OAUTH_ISSUER = "https://api.example.com"
response = await client.get( # RFC 8414: well-known endpoint is at root level
"/api/v1/oauth/.well-known/oauth-authorization-server" response = await client.get("/.well-known/oauth-authorization-server")
)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["issuer"] == "https://api.example.com" assert data["issuer"] == "https://api.example.com"
@@ -344,8 +342,10 @@ class TestOAuthProviderEndpoints:
assert response.status_code == 404 assert response.status_code == 404
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_provider_authorize_requires_auth(self, client, async_test_db): async def test_provider_authorize_public_client_requires_pkce(
"""Test provider authorize requires authentication.""" self, client, async_test_db
):
"""Test provider authorize requires PKCE for public clients."""
_test_engine, AsyncTestingSessionLocal = async_test_db _test_engine, AsyncTestingSessionLocal = async_test_db
# Create a test client # Create a test client
@@ -373,9 +373,54 @@ class TestOAuthProviderEndpoints:
"client_id": test_client_id, "client_id": test_client_id,
"redirect_uri": "http://localhost:3000/callback", "redirect_uri": "http://localhost:3000/callback",
}, },
follow_redirects=False,
) )
# Authorize endpoint requires authentication # Public client without PKCE gets redirect with error
assert response.status_code == 401 assert response.status_code == 302
assert "error=invalid_request" in response.headers.get("location", "")
assert "PKCE" in response.headers.get("location", "")
@pytest.mark.asyncio
async def test_provider_authorize_redirects_to_login(self, client, async_test_db):
"""Test provider authorize redirects unauthenticated users to login."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create a test client
from app.crud.oauth import oauth_client
from app.schemas.oauth import OAuthClientCreate
async with AsyncTestingSessionLocal() as session:
client_data = OAuthClientCreate(
client_name="Test App",
redirect_uris=["http://localhost:3000/callback"],
allowed_scopes=["read:users"],
)
test_client, _ = await oauth_client.create_client(
session, obj_in=client_data
)
test_client_id = test_client.client_id
with patch("app.api.routes.oauth_provider.settings") as mock_settings:
mock_settings.OAUTH_PROVIDER_ENABLED = True
mock_settings.FRONTEND_URL = "http://localhost:3000"
# Include PKCE parameters for public client
response = await client.get(
"/api/v1/oauth/provider/authorize",
params={
"response_type": "code",
"client_id": test_client_id,
"redirect_uri": "http://localhost:3000/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
},
follow_redirects=False,
)
# Unauthenticated users get redirected to login
assert response.status_code == 302
location = response.headers.get("location", "")
assert "/login" in location
assert "return_to" in location
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_provider_token_requires_client_id(self, client): async def test_provider_token_requires_client_id(self, client):

View File

@@ -95,7 +95,7 @@ export default function OAuthConsentPage() {
// Note: t is available for future i18n use // Note: t is available for future i18n use
const _t = useTranslations('auth.oauth'); const _t = useTranslations('auth.oauth');
void _t; // Suppress unused warning - ready for i18n void _t; // Suppress unused warning - ready for i18n
const { isAuthenticated, isLoading: authLoading } = useAuth(); const { isAuthenticated, isLoading: authLoading, accessToken } = useAuth();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -181,9 +181,14 @@ export default function OAuthConsentPage() {
// Submit consent to backend // Submit consent to backend
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
const headers: HeadersInit = {};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const response = await fetch(`${apiUrl}/api/v1/oauth/provider/authorize/consent`, { const response = await fetch(`${apiUrl}/api/v1/oauth/provider/authorize/consent`, {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers,
credentials: 'include', credentials: 'include',
}); });

View File

@@ -3,7 +3,7 @@
import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client'; import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetStatsData, AdminGetStatsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListSessionsData, AdminListSessionsErrors, AdminListSessionsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOauthAuthorizationUrlData, GetOauthAuthorizationUrlErrors, GetOauthAuthorizationUrlResponses, GetOauthServerMetadataData, GetOauthServerMetadataResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HandleOauthCallbackData, HandleOauthCallbackErrors, HandleOauthCallbackResponses, HealthCheckData, HealthCheckResponses, ListMySessionsData, ListMySessionsResponses, ListOauthAccountsData, ListOauthAccountsResponses, ListOauthProvidersData, ListOauthProvidersResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, OauthProviderAuthorizeData, OauthProviderAuthorizeErrors, OauthProviderAuthorizeResponses, OauthProviderRevokeData, OauthProviderRevokeErrors, OauthProviderRevokeResponses, OauthProviderTokenData, OauthProviderTokenErrors, OauthProviderTokenResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterOauthClientData, RegisterOauthClientErrors, RegisterOauthClientResponses, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, StartOauthLinkData, StartOauthLinkErrors, StartOauthLinkResponses, UnlinkOauthAccountData, UnlinkOauthAccountErrors, UnlinkOauthAccountResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen'; import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetStatsData, AdminGetStatsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListSessionsData, AdminListSessionsErrors, AdminListSessionsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteOauthClientData, DeleteOauthClientErrors, DeleteOauthClientResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOauthAuthorizationUrlData, GetOauthAuthorizationUrlErrors, GetOauthAuthorizationUrlResponses, GetOauthServerMetadataData, GetOauthServerMetadataResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HandleOauthCallbackData, HandleOauthCallbackErrors, HandleOauthCallbackResponses, HealthCheckData, HealthCheckResponses, ListMyOauthConsentsData, ListMyOauthConsentsResponses, ListMySessionsData, ListMySessionsResponses, ListOauthAccountsData, ListOauthAccountsResponses, ListOauthClientsData, ListOauthClientsResponses, ListOauthProvidersData, ListOauthProvidersResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, OauthProviderAuthorizeData, OauthProviderAuthorizeErrors, OauthProviderAuthorizeResponses, OauthProviderConsentData, OauthProviderConsentErrors, OauthProviderConsentResponses, OauthProviderIntrospectData, OauthProviderIntrospectErrors, OauthProviderIntrospectResponses, OauthProviderRevokeData, OauthProviderRevokeErrors, OauthProviderRevokeResponses, OauthProviderTokenData, OauthProviderTokenErrors, OauthProviderTokenResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterOauthClientData, RegisterOauthClientErrors, RegisterOauthClientResponses, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeMyOauthConsentData, RevokeMyOauthConsentErrors, RevokeMyOauthConsentResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, StartOauthLinkData, StartOauthLinkErrors, StartOauthLinkResponses, UnlinkOauthAccountData, UnlinkOauthAccountErrors, UnlinkOauthAccountResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/** /**
@@ -353,34 +353,26 @@ export const startOauthLink = <ThrowOnError extends boolean = false>(options: Op
}; };
/** /**
* OAuth Server Metadata * Authorization Endpoint
*
* OAuth 2.0 Authorization Server Metadata (RFC 8414).
*
* Returns server metadata including supported endpoints, scopes,
* and capabilities for MCP clients.
*/
export const getOauthServerMetadata = <ThrowOnError extends boolean = false>(options?: Options<GetOauthServerMetadataData, ThrowOnError>) => {
return (options?.client ?? client).get<GetOauthServerMetadataResponses, unknown, ThrowOnError>({
responseType: 'json',
url: '/api/v1/oauth/.well-known/oauth-authorization-server',
...options
});
};
/**
* Authorization Endpoint (Skeleton)
* *
* OAuth 2.0 Authorization Endpoint. * OAuth 2.0 Authorization Endpoint.
* *
* **NOTE**: This is a skeleton implementation. In a full implementation, * Initiates the authorization code flow:
* this would: * 1. Validates client and parameters
* 1. Validate client_id and redirect_uri * 2. Checks if user is authenticated (redirects to login if not)
* 2. Display consent screen to user * 3. Checks existing consent
* 3. Generate authorization code * 4. Redirects to consent page if needed
* 4. Redirect back to client with code * 5. Issues authorization code and redirects back to client
* *
* Currently returns a 501 Not Implemented response. * Required parameters:
* - response_type: Must be "code"
* - client_id: Registered client ID
* - redirect_uri: Must match registered URI
*
* Recommended parameters:
* - state: CSRF protection
* - code_challenge + code_challenge_method: PKCE (required for public clients)
* - scope: Requested permissions
*/ */
export const oauthProviderAuthorize = <ThrowOnError extends boolean = false>(options: Options<OauthProviderAuthorizeData, ThrowOnError>) => { export const oauthProviderAuthorize = <ThrowOnError extends boolean = false>(options: Options<OauthProviderAuthorizeData, ThrowOnError>) => {
return (options.client ?? client).get<OauthProviderAuthorizeResponses, OauthProviderAuthorizeErrors, ThrowOnError>({ return (options.client ?? client).get<OauthProviderAuthorizeResponses, OauthProviderAuthorizeErrors, ThrowOnError>({
@@ -391,14 +383,43 @@ export const oauthProviderAuthorize = <ThrowOnError extends boolean = false>(opt
}; };
/** /**
* Token Endpoint (Skeleton) * Submit Authorization Consent
*
* Submit user consent for OAuth authorization.
*
* Called by the consent page after user approves or denies.
*/
export const oauthProviderConsent = <ThrowOnError extends boolean = false>(options: Options<OauthProviderConsentData, ThrowOnError>) => {
return (options.client ?? client).post<OauthProviderConsentResponses, OauthProviderConsentErrors, ThrowOnError>({
...urlSearchParamsBodySerializer,
responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/oauth/provider/authorize/consent',
...options,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...options.headers
}
});
};
/**
* Token Endpoint
* *
* OAuth 2.0 Token Endpoint. * OAuth 2.0 Token Endpoint.
* *
* **NOTE**: This is a skeleton implementation. In a full implementation, * Supports:
* this would exchange authorization codes for access tokens. * - authorization_code: Exchange code for tokens
* - refresh_token: Refresh access token
* *
* Currently returns a 501 Not Implemented response. * Client authentication:
* - Confidential clients: client_secret (Basic auth or POST body)
* - Public clients: No secret, but PKCE code_verifier required
*/ */
export const oauthProviderToken = <ThrowOnError extends boolean = false>(options: Options<OauthProviderTokenData, ThrowOnError>) => { export const oauthProviderToken = <ThrowOnError extends boolean = false>(options: Options<OauthProviderTokenData, ThrowOnError>) => {
return (options.client ?? client).post<OauthProviderTokenResponses, OauthProviderTokenErrors, ThrowOnError>({ return (options.client ?? client).post<OauthProviderTokenResponses, OauthProviderTokenErrors, ThrowOnError>({
@@ -414,13 +435,12 @@ export const oauthProviderToken = <ThrowOnError extends boolean = false>(options
}; };
/** /**
* Token Revocation Endpoint (Skeleton) * Token Revocation Endpoint
* *
* OAuth 2.0 Token Revocation Endpoint (RFC 7009). * OAuth 2.0 Token Revocation Endpoint (RFC 7009).
* *
* **NOTE**: This is a skeleton implementation. * Revokes an access token or refresh token.
* * Always returns 200 OK (even if token is invalid) per spec.
* Currently returns a 501 Not Implemented response.
*/ */
export const oauthProviderRevoke = <ThrowOnError extends boolean = false>(options: Options<OauthProviderRevokeData, ThrowOnError>) => { export const oauthProviderRevoke = <ThrowOnError extends boolean = false>(options: Options<OauthProviderRevokeData, ThrowOnError>) => {
return (options.client ?? client).post<OauthProviderRevokeResponses, OauthProviderRevokeErrors, ThrowOnError>({ return (options.client ?? client).post<OauthProviderRevokeResponses, OauthProviderRevokeErrors, ThrowOnError>({
@@ -436,19 +456,65 @@ export const oauthProviderRevoke = <ThrowOnError extends boolean = false>(option
}; };
/** /**
* Register OAuth Client (Admin) * Token Introspection Endpoint
*
* OAuth 2.0 Token Introspection Endpoint (RFC 7662).
*
* Allows resource servers to query the authorization server
* to determine the active state and metadata of a token.
*/
export const oauthProviderIntrospect = <ThrowOnError extends boolean = false>(options: Options<OauthProviderIntrospectData, ThrowOnError>) => {
return (options.client ?? client).post<OauthProviderIntrospectResponses, OauthProviderIntrospectErrors, ThrowOnError>({
...urlSearchParamsBodySerializer,
responseType: 'json',
url: '/api/v1/oauth/provider/introspect',
...options,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...options.headers
}
});
};
/**
* List OAuth Clients
*
* List all registered OAuth clients (admin only).
*/
export const listOauthClients = <ThrowOnError extends boolean = false>(options?: Options<ListOauthClientsData, ThrowOnError>) => {
return (options?.client ?? client).get<ListOauthClientsResponses, unknown, ThrowOnError>({
responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/oauth/provider/clients',
...options
});
};
/**
* Register OAuth Client
* *
* Register a new OAuth client (admin only). * Register a new OAuth client (admin only).
* *
* This endpoint allows creating MCP clients that can authenticate * Creates an MCP client that can authenticate against this API.
* against this API. * Returns client_id and client_secret (for confidential clients).
* *
* **NOTE**: This is a minimal implementation. * **Important:** Store the client_secret securely - it won't be shown again!
*/ */
export const registerOauthClient = <ThrowOnError extends boolean = false>(options: Options<RegisterOauthClientData, ThrowOnError>) => { export const registerOauthClient = <ThrowOnError extends boolean = false>(options: Options<RegisterOauthClientData, ThrowOnError>) => {
return (options.client ?? client).post<RegisterOauthClientResponses, RegisterOauthClientErrors, ThrowOnError>({ return (options.client ?? client).post<RegisterOauthClientResponses, RegisterOauthClientErrors, ThrowOnError>({
...urlSearchParamsBodySerializer, ...urlSearchParamsBodySerializer,
responseType: 'json', responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/oauth/provider/clients', url: '/api/v1/oauth/provider/clients',
...options, ...options,
headers: { headers: {
@@ -458,6 +524,61 @@ export const registerOauthClient = <ThrowOnError extends boolean = false>(option
}); });
}; };
/**
* Delete OAuth Client
*
* Delete an OAuth client (admin only). Revokes all tokens.
*/
export const deleteOauthClient = <ThrowOnError extends boolean = false>(options: Options<DeleteOauthClientData, ThrowOnError>) => {
return (options.client ?? client).delete<DeleteOauthClientResponses, DeleteOauthClientErrors, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/oauth/provider/clients/{client_id}',
...options
});
};
/**
* List My Consents
*
* List OAuth applications the current user has authorized.
*/
export const listMyOauthConsents = <ThrowOnError extends boolean = false>(options?: Options<ListMyOauthConsentsData, ThrowOnError>) => {
return (options?.client ?? client).get<ListMyOauthConsentsResponses, unknown, ThrowOnError>({
responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/oauth/provider/consents',
...options
});
};
/**
* Revoke My Consent
*
* Revoke authorization for an OAuth application. Also revokes all tokens.
*/
export const revokeMyOauthConsent = <ThrowOnError extends boolean = false>(options: Options<RevokeMyOauthConsentData, ThrowOnError>) => {
return (options.client ?? client).delete<RevokeMyOauthConsentResponses, RevokeMyOauthConsentErrors, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/oauth/provider/consents/{client_id}',
...options
});
};
/** /**
* List Users * List Users
* *
@@ -1166,3 +1287,21 @@ export const getOrganizationMembers = <ThrowOnError extends boolean = false>(opt
...options ...options
}); });
}; };
/**
* OAuth Server Metadata
*
* OAuth 2.0 Authorization Server Metadata (RFC 8414).
*
* Returns server metadata including supported endpoints, scopes,
* and capabilities. MCP clients use this to discover the server.
*
* Note: This endpoint is at the root level per RFC 8414.
*/
export const getOauthServerMetadata = <ThrowOnError extends boolean = false>(options?: Options<GetOauthServerMetadataData, ThrowOnError>) => {
return (options?.client ?? client).get<GetOauthServerMetadataResponses, unknown, ThrowOnError>({
responseType: 'json',
url: '/.well-known/oauth-authorization-server',
...options
});
};

View File

@@ -145,6 +145,84 @@ export type BodyLoginOauth = {
client_secret?: string | null; client_secret?: string | null;
}; };
/**
* Body_oauth_provider_consent
*/
export type BodyOauthProviderConsent = {
/**
* Approved
*
* Whether user approved
*/
approved: boolean;
/**
* Client Id
*
* OAuth client ID
*/
client_id: string;
/**
* Redirect Uri
*
* Redirect URI
*/
redirect_uri: string;
/**
* Scope
*
* Granted scopes
*/
scope?: string;
/**
* State
*
* CSRF state parameter
*/
state?: string;
/**
* Code Challenge
*/
code_challenge?: string | null;
/**
* Code Challenge Method
*/
code_challenge_method?: string | null;
/**
* Nonce
*/
nonce?: string | null;
};
/**
* Body_oauth_provider_introspect
*/
export type BodyOauthProviderIntrospect = {
/**
* Token
*
* Token to introspect
*/
token: string;
/**
* Token Type Hint
*
* Token type hint (access_token, refresh_token)
*/
token_type_hint?: string | null;
/**
* Client Id
*
* Client ID
*/
client_id?: string | null;
/**
* Client Secret
*
* Client secret
*/
client_secret?: string | null;
};
/** /**
* Body_oauth_provider_revoke * Body_oauth_provider_revoke
*/ */
@@ -182,7 +260,7 @@ export type BodyOauthProviderToken = {
/** /**
* Grant Type * Grant Type
* *
* Grant type (authorization_code) * Grant type
*/ */
grant_type: string; grant_type: string;
/** /**
@@ -221,6 +299,12 @@ export type BodyOauthProviderToken = {
* Refresh token * Refresh token
*/ */
refresh_token?: string | null; refresh_token?: string | null;
/**
* Scope
*
* Scope (for refresh)
*/
scope?: string | null;
}; };
/** /**
@@ -236,7 +320,7 @@ export type BodyRegisterOauthClient = {
/** /**
* Redirect Uris * Redirect Uris
* *
* Comma-separated list of redirect URIs * Comma-separated redirect URIs
*/ */
redirect_uris: string; redirect_uris: string;
/** /**
@@ -245,6 +329,18 @@ export type BodyRegisterOauthClient = {
* public or confidential * public or confidential
*/ */
client_type?: string; client_type?: string;
/**
* Scopes
*
* Allowed scopes (space-separated)
*/
scopes?: string;
/**
* Mcp Server Url
*
* MCP server URL
*/
mcp_server_url?: string | null;
}; };
/** /**
@@ -454,6 +550,60 @@ export type OAuthCallbackResponse = {
is_new_user?: boolean; is_new_user?: boolean;
}; };
/**
* OAuthClientResponse
*
* Schema for OAuth client response.
*/
export type OAuthClientResponse = {
/**
* Client Name
*
* Client application name
*/
client_name: string;
/**
* Client Description
*
* Client description
*/
client_description?: string | null;
/**
* Redirect Uris
*
* Allowed redirect URIs
*/
redirect_uris?: Array<string>;
/**
* Allowed Scopes
*
* Allowed OAuth scopes
*/
allowed_scopes?: Array<string>;
/**
* Id
*/
id: string;
/**
* Client Id
*
* OAuth client ID
*/
client_id: string;
/**
* Client Type
*/
client_type: string;
/**
* Is Active
*/
is_active: boolean;
/**
* Created At
*/
created_at: string;
};
/** /**
* OAuthProviderInfo * OAuthProviderInfo
* *
@@ -536,6 +686,12 @@ export type OAuthServerMetadata = {
* Token revocation endpoint * Token revocation endpoint
*/ */
revocation_endpoint?: string | null; revocation_endpoint?: string | null;
/**
* Introspection Endpoint
*
* Token introspection endpoint (RFC 7662)
*/
introspection_endpoint?: string | null;
/** /**
* Scopes Supported * Scopes Supported
* *
@@ -560,6 +716,124 @@ export type OAuthServerMetadata = {
* Supported PKCE methods * Supported PKCE methods
*/ */
code_challenge_methods_supported?: Array<string>; code_challenge_methods_supported?: Array<string>;
/**
* Token Endpoint Auth Methods Supported
*
* Supported client authentication methods
*/
token_endpoint_auth_methods_supported?: Array<string>;
};
/**
* OAuthTokenIntrospectionResponse
*
* OAuth 2.0 Token Introspection Response (RFC 7662).
*/
export type OAuthTokenIntrospectionResponse = {
/**
* Active
*
* Whether the token is currently active
*/
active: boolean;
/**
* Scope
*
* Space-separated list of scopes
*/
scope?: string | null;
/**
* Client Id
*
* Client identifier for the token
*/
client_id?: string | null;
/**
* Username
*
* Human-readable identifier for the resource owner
*/
username?: string | null;
/**
* Token Type
*
* Type of the token (e.g., 'Bearer')
*/
token_type?: string | null;
/**
* Exp
*
* Token expiration time (Unix timestamp)
*/
exp?: number | null;
/**
* Iat
*
* Token issue time (Unix timestamp)
*/
iat?: number | null;
/**
* Nbf
*
* Token not-before time (Unix timestamp)
*/
nbf?: number | null;
/**
* Sub
*
* Subject of the token (user ID)
*/
sub?: string | null;
/**
* Aud
*
* Intended audience of the token
*/
aud?: string | null;
/**
* Iss
*
* Issuer of the token
*/
iss?: string | null;
};
/**
* OAuthTokenResponse
*
* OAuth 2.0 Token Response (RFC 6749 Section 5.1).
*/
export type OAuthTokenResponse = {
/**
* Access Token
*
* The access token issued by the server
*/
access_token: string;
/**
* Token Type
*
* The type of token (typically 'Bearer')
*/
token_type?: string;
/**
* Expires In
*
* Token lifetime in seconds
*/
expires_in?: number | null;
/**
* Refresh Token
*
* Refresh token for obtaining new access tokens
*/
refresh_token?: string | null;
/**
* Scope
*
* Space-separated list of granted scopes
*/
scope?: string | null;
}; };
/** /**
@@ -1610,24 +1884,14 @@ export type StartOauthLinkResponses = {
export type StartOauthLinkResponse = StartOauthLinkResponses[keyof StartOauthLinkResponses]; export type StartOauthLinkResponse = StartOauthLinkResponses[keyof StartOauthLinkResponses];
export type GetOauthServerMetadataData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/oauth/.well-known/oauth-authorization-server';
};
export type GetOauthServerMetadataResponses = {
/**
* Successful Response
*/
200: OAuthServerMetadata;
};
export type GetOauthServerMetadataResponse = GetOauthServerMetadataResponses[keyof GetOauthServerMetadataResponses];
export type OauthProviderAuthorizeData = { export type OauthProviderAuthorizeData = {
body?: never; body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string;
};
path?: never; path?: never;
query: { query: {
/** /**
@@ -1651,7 +1915,7 @@ export type OauthProviderAuthorizeData = {
/** /**
* Scope * Scope
* *
* Requested scopes * Requested scopes (space-separated)
*/ */
scope?: string; scope?: string;
/** /**
@@ -1672,6 +1936,12 @@ export type OauthProviderAuthorizeData = {
* PKCE method (S256) * PKCE method (S256)
*/ */
code_challenge_method?: string | null; code_challenge_method?: string | null;
/**
* Nonce
*
* OpenID Connect nonce
*/
nonce?: string | null;
}; };
url: '/api/v1/oauth/provider/authorize'; url: '/api/v1/oauth/provider/authorize';
}; };
@@ -1694,6 +1964,31 @@ export type OauthProviderAuthorizeResponses = {
200: unknown; 200: unknown;
}; };
export type OauthProviderConsentData = {
body: BodyOauthProviderConsent;
path?: never;
query?: never;
url: '/api/v1/oauth/provider/authorize/consent';
};
export type OauthProviderConsentErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type OauthProviderConsentError = OauthProviderConsentErrors[keyof OauthProviderConsentErrors];
export type OauthProviderConsentResponses = {
/**
* Response Oauth Provider Consent
*
* Successful Response
*/
200: unknown;
};
export type OauthProviderTokenData = { export type OauthProviderTokenData = {
body: BodyOauthProviderToken; body: BodyOauthProviderToken;
path?: never; path?: never;
@@ -1712,13 +2007,13 @@ export type OauthProviderTokenError = OauthProviderTokenErrors[keyof OauthProvid
export type OauthProviderTokenResponses = { export type OauthProviderTokenResponses = {
/** /**
* Response Oauth Provider Token
*
* Successful Response * Successful Response
*/ */
200: unknown; 200: OAuthTokenResponse;
}; };
export type OauthProviderTokenResponse = OauthProviderTokenResponses[keyof OauthProviderTokenResponses];
export type OauthProviderRevokeData = { export type OauthProviderRevokeData = {
body: BodyOauthProviderRevoke; body: BodyOauthProviderRevoke;
path?: never; path?: never;
@@ -1741,9 +2036,56 @@ export type OauthProviderRevokeResponses = {
* *
* Successful Response * Successful Response
*/ */
200: unknown; 200: {
[key: string]: string;
};
}; };
export type OauthProviderRevokeResponse = OauthProviderRevokeResponses[keyof OauthProviderRevokeResponses];
export type OauthProviderIntrospectData = {
body: BodyOauthProviderIntrospect;
path?: never;
query?: never;
url: '/api/v1/oauth/provider/introspect';
};
export type OauthProviderIntrospectErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type OauthProviderIntrospectError = OauthProviderIntrospectErrors[keyof OauthProviderIntrospectErrors];
export type OauthProviderIntrospectResponses = {
/**
* Successful Response
*/
200: OAuthTokenIntrospectionResponse;
};
export type OauthProviderIntrospectResponse = OauthProviderIntrospectResponses[keyof OauthProviderIntrospectResponses];
export type ListOauthClientsData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/oauth/provider/clients';
};
export type ListOauthClientsResponses = {
/**
* Response List Oauth Clients
*
* Successful Response
*/
200: Array<OAuthClientResponse>;
};
export type ListOauthClientsResponse = ListOauthClientsResponses[keyof ListOauthClientsResponses];
export type RegisterOauthClientData = { export type RegisterOauthClientData = {
body: BodyRegisterOauthClient; body: BodyRegisterOauthClient;
path?: never; path?: never;
@@ -1766,9 +2108,93 @@ export type RegisterOauthClientResponses = {
* *
* Successful Response * Successful Response
*/ */
200: unknown; 200: {
[key: string]: unknown;
};
}; };
export type RegisterOauthClientResponse = RegisterOauthClientResponses[keyof RegisterOauthClientResponses];
export type DeleteOauthClientData = {
body?: never;
path: {
/**
* Client Id
*/
client_id: string;
};
query?: never;
url: '/api/v1/oauth/provider/clients/{client_id}';
};
export type DeleteOauthClientErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeleteOauthClientError = DeleteOauthClientErrors[keyof DeleteOauthClientErrors];
export type DeleteOauthClientResponses = {
/**
* Successful Response
*/
204: void;
};
export type DeleteOauthClientResponse = DeleteOauthClientResponses[keyof DeleteOauthClientResponses];
export type ListMyOauthConsentsData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/oauth/provider/consents';
};
export type ListMyOauthConsentsResponses = {
/**
* Response List My Oauth Consents
*
* Successful Response
*/
200: Array<{
[key: string]: unknown;
}>;
};
export type ListMyOauthConsentsResponse = ListMyOauthConsentsResponses[keyof ListMyOauthConsentsResponses];
export type RevokeMyOauthConsentData = {
body?: never;
path: {
/**
* Client Id
*/
client_id: string;
};
query?: never;
url: '/api/v1/oauth/provider/consents/{client_id}';
};
export type RevokeMyOauthConsentErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RevokeMyOauthConsentError = RevokeMyOauthConsentErrors[keyof RevokeMyOauthConsentErrors];
export type RevokeMyOauthConsentResponses = {
/**
* Successful Response
*/
204: void;
};
export type RevokeMyOauthConsentResponse = RevokeMyOauthConsentResponses[keyof RevokeMyOauthConsentResponses];
export type ListUsersData = { export type ListUsersData = {
body?: never; body?: never;
path?: never; path?: never;
@@ -2759,3 +3185,19 @@ export type GetOrganizationMembersResponses = {
}; };
export type GetOrganizationMembersResponse = GetOrganizationMembersResponses[keyof GetOrganizationMembersResponses]; export type GetOrganizationMembersResponse = GetOrganizationMembersResponses[keyof GetOrganizationMembersResponses];
export type GetOauthServerMetadataData = {
body?: never;
path?: never;
query?: never;
url: '/.well-known/oauth-authorization-server';
};
export type GetOauthServerMetadataResponses = {
/**
* Successful Response
*/
200: OAuthServerMetadata;
};
export type GetOauthServerMetadataResponse = GetOauthServerMetadataResponses[keyof GetOauthServerMetadataResponses];

View File

@@ -8,7 +8,7 @@
* *
* For custom handler behavior, use src/mocks/handlers/overrides.ts * For custom handler behavior, use src/mocks/handlers/overrides.ts
* *
* Generated: 2025-11-25T00:22:46.981Z * Generated: 2025-11-26T12:21:51.098Z
*/ */
import { http, HttpResponse, delay } from 'msw'; import { http, HttpResponse, delay } from 'msw';