Skip to content
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions src/server/oauth/convexAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ export const defaultCookiesOptions: (
};
};

// Microsoft multi-tenant: aliases like "common", "organizations", "consumers"
// discover issuer .../{alias}/v2.0 but tokens carry tenant-specific
// iss claims (.../{tenantId}/v2.0). Use oauth4webapi's
// _expectedIssuer extension to accept the token's own iss,
// matching the approach used by @auth/core for Microsoft providers.
// Only needed for multi-tenant aliases; tenant-specific endpoints
// return exact issuer matches.
function applyMultiTenantEntraFix(
as: o.AuthorizationServer,
config: OAuthConfig<any>,
) {
const issuer = (as as any).issuer ?? as.issuer ?? "";
const isMultiTenantEntra =
config.type === "oidc" &&
config.id === "microsoft-entra-id" &&
/\/(common|organizations|consumers)\/v2\.0\/?$/.test(issuer);
if (isMultiTenantEntra) {
// _expectedIssuer is a runtime-exported Symbol from oauth4webapi,
// not yet in the type declarations.
const expectedIssuer = (o as any)._expectedIssuer as symbol;
(as as any)[expectedIssuer] = (result: any) => result.claims.iss;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does oauth4webapi version 3.1.2 export _expectedIssuer symbol for custom issuer validation?

💡 Result:

No, oauth4webapi version 3.1.2 does not export a symbol named _expectedIssuer for custom issuer validation. The package exports tree-shakeable ESM with documented public APIs such as processDiscoveryResponse(expectedIssuerIdentifier: URL, response: Response), which handles issuer validation using the provided expectedIssuerIdentifier parameter passed by the caller. No sources mention an exported _expectedIssuer symbol; the underscore prefix suggests it would be private if it exists internally. Official docs, API reference, changelog for 3.1.2 (refactor: shake supported functions, types update), and examples confirm custom validation is done via public functions like processDiscoveryResponse, not a special exported symbol.

Citations:


Replace reliance on undocumented internal _expectedIssuer symbol with oauth4webapi's public API.

The code accesses _expectedIssuer from oauth4webapi 3.1.2, which is not documented or exported as part of the library's public API. The underscore prefix and absence of any mention in oauth4webapi's official documentation or type declarations confirms this is internal implementation detail. oauth4webapi instead provides documented public APIs like processDiscoveryResponse(expectedIssuerIdentifier, response) for handling issuer validation. Using undocumented internal symbols creates a fragile dependency that can silently break with any library update, and should be replaced with the documented public functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/oauth/convexAuth.ts` around lines 107 - 110, The code currently
reads the internal symbol _expectedIssuer from oauth4webapi (const
expectedIssuer = (o as any)._expectedIssuer) and assigns a function to (as as
any)[expectedIssuer] to pull result.claims.iss; replace this fragile
internal-symbol approach by using oauth4webapi's documented
processDiscoveryResponse API: call processDiscoveryResponse with the expected
issuer identifier and the discovery response (instead of touching
_expectedIssuer) and use the returned/validated issuer value for issuer
validation and any assignment previously done via (as as any)[expectedIssuer];
update references involving o, as, expectedIssuer, and result.claims.iss to use
the processDiscoveryResponse call and its output for issuer validation and
assignment.

}
}

export async function oAuthConfigToInternalProvider(config: OAuthConfig<any>): Promise<InternalProvider<"oauth" | "oidc">> {
// Only do service discovery if the provider does not have the required configuration
if (!config.authorization || !config.token || !config.userinfo) {
Expand All @@ -113,6 +137,8 @@ export async function oAuthConfigToInternalProvider(config: OAuthConfig<any>): P
"TODO: Authorization server did not provide a token endpoint.",
);

applyMultiTenantEntraFix(discoveredAs, config);

const as: o.AuthorizationServer = discoveredAs;
return {
...config,
Expand Down Expand Up @@ -147,6 +173,13 @@ export async function oAuthConfigToInternalProvider(config: OAuthConfig<any>): P
const userinfo = config.userinfo
? normalizeEndpoint(config.userinfo)
: undefined;
const as: o.AuthorizationServer = {
issuer: config.issuer ?? "theremustbeastringhere.dev",
authorization_endpoint: authorization?.url.toString(),
token_endpoint: token?.url.toString(),
userinfo_endpoint: userinfo?.url.toString(),
};
applyMultiTenantEntraFix(as, config);
return {
...config,
checks: config.checks!,
Expand All @@ -157,12 +190,7 @@ export async function oAuthConfigToInternalProvider(config: OAuthConfig<any>): P
authorization,
token,
userinfo,
as: {
issuer: config.issuer ?? "theremustbeastringhere.dev",
authorization_endpoint: authorization?.url.toString(),
token_endpoint: token?.url.toString(),
userinfo_endpoint: userinfo?.url.toString(),
},
as,
configSource: "provided",
};
}