Skip to content

Commit 047ce22

Browse files
committed
feat: add OAuth proxy endpoints to eliminate CORS issues
- Add GET /oauth/metadata for authorization server metadata discovery - Add GET /oauth/resource-metadata for protected resource metadata - Add POST /oauth/register for Dynamic Client Registration (DCR) - Add POST /oauth/token for token exchange - All endpoints use existing auth middleware and origin validation - Properly handle URL construction for different OAuth server types
1 parent 82067f3 commit 047ce22

1 file changed

Lines changed: 207 additions & 0 deletions

File tree

server/src/index.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,213 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
787787
}
788788
});
789789

790+
// OAuth Proxy Endpoints - for routing OAuth requests through the proxy to avoid CORS issues
791+
app.use(express.json()); // Ensure JSON body parsing is enabled
792+
793+
/**
794+
* Proxy endpoint for OAuth Authorization Server Metadata Discovery
795+
* GET /oauth/metadata?authServerUrl=<url>
796+
*/
797+
app.get(
798+
"/oauth/metadata",
799+
originValidationMiddleware,
800+
authMiddleware,
801+
async (req, res) => {
802+
try {
803+
const authServerUrl = req.query.authServerUrl as string;
804+
if (!authServerUrl) {
805+
res
806+
.status(400)
807+
.json({ error: "authServerUrl query parameter is required" });
808+
return;
809+
}
810+
811+
console.log(`OAuth metadata discovery for: ${authServerUrl}`);
812+
813+
// Append the well-known path to the authServerUrl
814+
// Remove trailing slash if present, then append the well-known path
815+
const baseUrl = authServerUrl.endsWith("/")
816+
? authServerUrl.slice(0, -1)
817+
: authServerUrl;
818+
const metadataUrl = `${baseUrl}/.well-known/oauth-authorization-server`;
819+
console.log(`Fetching metadata from: ${metadataUrl}`);
820+
821+
const response = await fetch(metadataUrl);
822+
823+
if (!response.ok) {
824+
const errorText = await response.text();
825+
res.status(response.status).json({
826+
error: `Failed to fetch OAuth metadata: ${response.statusText}`,
827+
details: errorText,
828+
});
829+
return;
830+
}
831+
832+
const metadata = await response.json();
833+
res.json(metadata);
834+
} catch (error) {
835+
console.error("Error in /oauth/metadata route:", error);
836+
res.status(500).json({
837+
error: error instanceof Error ? error.message : String(error),
838+
});
839+
}
840+
},
841+
);
842+
843+
/**
844+
* Proxy endpoint for OAuth Protected Resource Metadata Discovery
845+
* GET /oauth/resource-metadata?serverUrl=<url>
846+
*/
847+
app.get(
848+
"/oauth/resource-metadata",
849+
originValidationMiddleware,
850+
authMiddleware,
851+
async (req, res) => {
852+
try {
853+
const serverUrl = req.query.serverUrl as string;
854+
if (!serverUrl) {
855+
res
856+
.status(400)
857+
.json({ error: "serverUrl query parameter is required" });
858+
return;
859+
}
860+
861+
console.log(`OAuth resource metadata discovery for: ${serverUrl}`);
862+
863+
// For resource metadata, use the origin (protocol + host + port) only
864+
// This is per RFC 8414 - resource metadata is at the origin root
865+
const url = new URL(serverUrl);
866+
const metadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
867+
console.log(`Fetching resource metadata from: ${metadataUrl}`);
868+
869+
const response = await fetch(metadataUrl);
870+
871+
if (!response.ok) {
872+
const errorText = await response.text();
873+
res.status(response.status).json({
874+
error: `Failed to fetch resource metadata: ${response.statusText}`,
875+
details: errorText,
876+
});
877+
return;
878+
}
879+
880+
const metadata = await response.json();
881+
res.json(metadata);
882+
} catch (error) {
883+
console.error("Error in /oauth/resource-metadata route:", error);
884+
res.status(500).json({
885+
error: error instanceof Error ? error.message : String(error),
886+
});
887+
}
888+
},
889+
);
890+
891+
/**
892+
* Proxy endpoint for OAuth Dynamic Client Registration (DCR)
893+
* POST /oauth/register
894+
* Body: { registrationEndpoint: string, clientMetadata: object }
895+
*/
896+
app.post(
897+
"/oauth/register",
898+
originValidationMiddleware,
899+
authMiddleware,
900+
async (req, res) => {
901+
try {
902+
const { registrationEndpoint, clientMetadata } = req.body;
903+
904+
if (!registrationEndpoint || !clientMetadata) {
905+
res.status(400).json({
906+
error:
907+
"registrationEndpoint and clientMetadata are required in request body",
908+
});
909+
return;
910+
}
911+
912+
console.log(`OAuth client registration at: ${registrationEndpoint}`);
913+
914+
const response = await fetch(registrationEndpoint, {
915+
method: "POST",
916+
headers: {
917+
"Content-Type": "application/json",
918+
Accept: "application/json",
919+
},
920+
body: JSON.stringify(clientMetadata),
921+
});
922+
923+
if (!response.ok) {
924+
const errorText = await response.text();
925+
res.status(response.status).json({
926+
error: `Failed to register client: ${response.statusText}`,
927+
details: errorText,
928+
});
929+
return;
930+
}
931+
932+
const clientInformation = await response.json();
933+
res.json(clientInformation);
934+
} catch (error) {
935+
console.error("Error in /oauth/register route:", error);
936+
res.status(500).json({
937+
error: error instanceof Error ? error.message : String(error),
938+
});
939+
}
940+
},
941+
);
942+
943+
/**
944+
* Proxy endpoint for OAuth Token Exchange
945+
* POST /oauth/token
946+
* Body: { tokenEndpoint: string, params: object }
947+
*/
948+
app.post(
949+
"/oauth/token",
950+
originValidationMiddleware,
951+
authMiddleware,
952+
async (req, res) => {
953+
try {
954+
const { tokenEndpoint, params } = req.body;
955+
956+
if (!tokenEndpoint || !params) {
957+
res.status(400).json({
958+
error: "tokenEndpoint and params are required in request body",
959+
});
960+
return;
961+
}
962+
963+
console.log(`OAuth token exchange at: ${tokenEndpoint}`);
964+
965+
// Convert params object to URLSearchParams for form encoding
966+
const formBody = new URLSearchParams(params as Record<string, string>);
967+
968+
const response = await fetch(tokenEndpoint, {
969+
method: "POST",
970+
headers: {
971+
"Content-Type": "application/x-www-form-urlencoded",
972+
Accept: "application/json",
973+
},
974+
body: formBody.toString(),
975+
});
976+
977+
if (!response.ok) {
978+
const errorText = await response.text();
979+
res.status(response.status).json({
980+
error: `Failed to exchange token: ${response.statusText}`,
981+
details: errorText,
982+
});
983+
return;
984+
}
985+
986+
const tokens = await response.json();
987+
res.json(tokens);
988+
} catch (error) {
989+
console.error("Error in /oauth/token route:", error);
990+
res.status(500).json({
991+
error: error instanceof Error ? error.message : String(error),
992+
});
993+
}
994+
},
995+
);
996+
790997
const PORT = parseInt(
791998
process.env.SERVER_PORT || DEFAULT_MCP_PROXY_LISTEN_PORT,
792999
10,

0 commit comments

Comments
 (0)