Why your base44 third-party OAuth is broken
Base44 third-party OAuth fails because one of five flow steps is wrong: the authorization URL is missing scopes or the tenant ID, the redirect URI is not registered exactly as Base44 sends it, the callback handler does not validate the state parameter, the token-exchange POST omits the client secret or uses the wrong content type, or the profile fetch hits the wrong endpoint for that provider. The Google-specific failure has its own dedicated fix page. For GitHub, Microsoft Entra ID, Slack, generic OIDC, and custom providers, the diagnostic is identical: capture the callback URL parameters in your browser, inspect the token-exchange request and response in your function logs, confirm the registered redirect URI byte-for-byte matches Base44's outgoing redirect, and verify your requested scopes cover every field your profile fetch reads.
You wired up GitHub login. You tested it. It worked in the editor preview. You shipped to production and the first real user gets bounced back to your homepage with no session. Or worse, the provider's consent screen never appears and the user lands on an error page from accounts.github.com or login.microsoftonline.com with a cryptic invalid_request message.
Base44's authentication layer handles email-and-password and Google sign-in cleanly enough for most teams, but third-party OAuth (GitHub, Microsoft Entra ID, Slack, generic OIDC, and any custom-built provider) involves five distinct steps and any one of them can break in a way that produces a silent failure or a misleading error. The platform-provided wiring is thin; you usually own the callback handler, the token exchange, and the profile-to-session mapping inside a Base44 function. Most of the bugs live in those three places.
This guide covers the non-Google providers. The Google-specific fix is on its own page because Google is the single most common case and has its own consent-screen approval flow that no other provider shares.
What causes base44 third-party OAuth integration to fail
There are five steps in the OAuth 2.0 Authorization Code flow as Base44 implements it, and each one has its own failure modes.
Step 1 — Build the authorization URL. Your app constructs a URL pointing at the provider's /authorize endpoint with query parameters: client_id, redirect_uri, response_type=code, scope, state, and (for PKCE) code_challenge + code_challenge_method. Mistakes here produce a provider-side error page before the user ever sees a consent screen. The most common bugs are a wrong tenant in the URL path (Microsoft), a missing or unregistered scope, a redirect_uri the provider does not recognize, and an empty state parameter.
Step 2 — User authenticates and consents. The provider shows its login page and a consent screen listing the scopes you requested. The user accepts or declines. There is almost nothing your code can do here — failures at this step are either configuration (your app is in test mode and the user is not a tester) or user choice (they hit cancel).
Step 3 — Provider redirects to your callback URL with a code. The provider 302s back to the redirect_uri you registered, appending code and state (and sometimes scope) as query parameters. If anything was wrong upstream, you get error and error_description instead. Two things go wrong at this step: the registered redirect URI does not exactly match the URL Base44 served (different host, missing trailing slash, http vs https), or your callback handler does not read the query parameters correctly because it expects them on a different HTTP method or path.
Step 4 — Token exchange. Your callback handler POSTs to the provider's /token endpoint with grant_type=authorization_code, code, redirect_uri, client_id, and either client_secret or code_verifier (PKCE). The provider returns an access_token, a token_type, an expires_in, and optionally an id_token (OIDC) and a refresh_token. This step fails in three ways: wrong content type (sending JSON when the provider requires form-encoded), wrong authentication (secret in body vs HTTP Basic), and a mismatched redirect_uri (it must be identical to step 1).
Step 5 — Profile fetch and session create. Your code uses the access token to fetch the user's profile from the provider's userinfo endpoint and creates a session in your app. This step fails when the provider's response shape does not match your assumptions — email vs mail, id vs sub vs oid, missing fields because you did not request the right scope, or a different endpoint for the same product (GitHub's /user/emails vs /user).
A working OAuth integration handles all five steps correctly. A broken one is broken at exactly one of them — find which one and the fix is usually a one-line change.
Source: RFC 6749 (OAuth 2.0), RFC 7636 (PKCE), OpenID Connect Core 1.0, and the provider-specific docs cited per section below.
Provider-specific gotchas
GitHub OAuth Apps
GitHub has two products that both let users sign in with their GitHub account: OAuth Apps and GitHub Apps. They are not interchangeable. OAuth Apps use the classic Authorization Code flow with user-to-server tokens. GitHub Apps use installation tokens scoped to specific repositories and a slightly different flow. For "Sign in with GitHub" in a SaaS app, you want OAuth Apps.
GitHub requires explicit scopes for email and profile data. Without read:user, your call to https://api.github.com/user returns the public profile only and the email field is null for any user who has not made their primary email public. The fix is to request read:user user:email and then separately call https://api.github.com/user/emails to get the verified primary email:
// Inside a Base44 function — GitHub OAuth callback handler
export async function handler(req) {
const { code, state } = req.query;
if (!validateState(state)) return error(403, "state mismatch");
// Step 4 — token exchange. GitHub accepts JSON body if you set Accept.
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
redirect_uri: process.env.GITHUB_REDIRECT_URI,
}),
});
const { access_token, error: tokenError } = await tokenRes.json();
if (tokenError) return error(400, tokenError);
// Step 5 — profile fetch. Need TWO calls for email.
const auth = { Authorization: `Bearer ${access_token}` };
const [profile, emails] = await Promise.all([
fetch("https://api.github.com/user", { headers: auth }).then((r) => r.json()),
fetch("https://api.github.com/user/emails", { headers: auth }).then((r) => r.json()),
]);
const primaryEmail = emails.find((e) => e.primary && e.verified)?.email;
if (!primaryEmail) return error(400, "no verified primary email");
return createSession({ provider: "github", externalId: profile.id, email: primaryEmail, name: profile.name });
}
The other GitHub-specific trap is the homepage URL. GitHub requires Homepage URL and Authorization callback URL to be set on the OAuth App, and the callback URL is matched exactly. https://app.example.com/auth/github/callback does not match https://app.example.com/auth/github/callback/ — note the trailing slash.
Microsoft Entra ID (formerly Azure AD)
Microsoft is the OAuth provider with the most ways to get the URL wrong. The authorization endpoint is https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize where {tenant} is one of:
- A specific tenant directory ID (a GUID) — restricts sign-in to that organization
common— accepts any work, school, or personal Microsoft accountorganizations— accepts work and school accounts but not personalconsumers— accepts personal Microsoft accounts only
Pick the wrong one and either the wrong users can sign in or the right users cannot. For a B2B SaaS where any organization should be able to sign in, use organizations. For consumer-facing apps, use consumers or common.
The v1.0 vs v2.0 split is the other Microsoft trap. The v1.0 endpoints (/oauth2/authorize, /oauth2/token) are the old Azure AD flow that returned a different ID token format and used different scope syntax (resource URIs instead of named scopes). The v2.0 endpoints (/oauth2/v2.0/authorize, /oauth2/v2.0/token) are the unified Microsoft identity platform and use OIDC-style scopes (openid profile email User.Read). Mixing them produces obscure errors at the token exchange step.
// Microsoft Entra ID v2.0 — token exchange
const tokenRes = await fetch(
`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.MS_CLIENT_ID,
client_secret: process.env.MS_CLIENT_SECRET,
code,
redirect_uri: process.env.MS_REDIRECT_URI,
grant_type: "authorization_code",
scope: "openid profile email User.Read",
}),
}
);
Microsoft also requires the redirect URI to be registered under the Web platform in the Entra ID app registration, not Single-page application or Mobile and desktop applications. Base44 functions are server-side; SPA platform configurations explicitly disable the client_secret and the token exchange will fail.
Slack OAuth v2
Slack's v2 OAuth (/oauth.v2.access) is the current API and the only one you should use for new integrations. The v1 endpoint (/oauth.access) still works but returns a different response shape and uses different scope syntax. If your token-exchange code was copied from a tutorial written before 2020, it likely uses v1.
The v2 response shape separates bot tokens from user tokens:
{
"ok": true,
"access_token": "xoxb-bot-token",
"scope": "chat:write,channels:read",
"bot_user_id": "U0KRQLJ9H",
"team": { "id": "T9TK3CUKW", "name": "Slack Pickleball Club" },
"authed_user": {
"id": "U1234",
"access_token": "xoxp-user-token",
"scope": "identity.basic,identity.email"
}
}
If you want a bot token (for posting to channels), read access_token at the top level. If you want a user token (for reading the user's profile), read authed_user.access_token. The two are governed by separate scope parameters in the authorization URL — scope for bot and user_scope for user — and Slack will silently return a token in only the field corresponding to the scopes you requested.
For pure sign-in flows, you usually want the user scopes identity.basic identity.email identity.avatar and you read authed_user.access_token followed by https://slack.com/api/users.identity.
Generic OIDC providers
For any provider that implements OpenID Connect (Okta, Auth0, Keycloak, Authelia, custom Hydra deployments), the flow is standardized and the discovery document tells you the endpoints:
// Discover OIDC configuration
const discovery = await fetch(
`${issuer}/.well-known/openid-configuration`
).then((r) => r.json());
// discovery.authorization_endpoint
// discovery.token_endpoint
// discovery.userinfo_endpoint
// discovery.jwks_uri
Use the discovery document. Do not hardcode endpoints. Providers move them.
// Generic OIDC token exchange with PKCE
const tokenRes = await fetch(discovery.token_endpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: process.env.OIDC_REDIRECT_URI,
client_id: process.env.OIDC_CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET,
code_verifier: storedVerifierForState(state),
}),
});
const { access_token, id_token } = await tokenRes.json();
The two requirements that almost-but-not-quite-standard providers miss: the code_verifier is a string, not the SHA-256 hash, and the redirect_uri must be byte-identical to the one in the authorization URL.
Custom OAuth providers you built yourself
If your provider is something your own team built (a partner SSO, a legacy auth system you wrapped in an OAuth layer), the failure mode is usually the state parameter or the nonce. The OAuth 2.0 spec says state is recommended; OIDC says nonce is required for the implicit and hybrid flows. Many home-grown providers do not enforce these and many home-grown clients do not send them. The result works in development and fails when you turn on strict mode in production.
Always: generate a cryptographically random state per authorization request, store it server-side keyed to the user's session, validate the returned state matches, and reject the callback if it does not.
How to confirm base44 third-party OAuth is broken (reproduction)
- Open your Base44 app in a fresh incognito window so there is no cached session.
- Open browser DevTools, switch to the Network tab, and check "Preserve log."
- Click the sign-in-with-provider button. Watch the redirect to the provider.
- If you land on a provider error page (GitHub: an
invalid_requestpage; Microsoft: alogin.microsoftonline.comerror page; Slack: aslack.comconsent failure), step 1 is wrong. Copy the full URL of the error page and theerror_descriptionquery parameter. The provider tells you exactly what is wrong with the authorization URL. - If you complete the consent screen and land back on your Base44 app with a query parameter like
?error=access_denied&error_description=..., the user denied consent or your client is misconfigured. Read the description. - If you land back on your Base44 app with
?code=abc&state=xyzbut the page shows an error or redirects to login, step 4 or 5 is wrong. Open the Base44 function logs for your OAuth callback handler. Find the request and read the full log line — the token-exchange response and any thrown exception should be there. - Capture the registered redirect URI from the provider's app settings. Capture the actual callback URL from the failed request in step 6. Diff them character by character. If they differ at all, the provider rejected the token exchange.
How to fix base44 third-party OAuth — by failing step
If step 1 fails (provider error page before consent)
Open the provider's app registration and verify the redirect URI is registered exactly as Base44 is sending it. Open your authorization URL builder and confirm every required parameter is present. For Microsoft, double-check the tenant in the URL path. For Slack, confirm the scopes are spelled exactly as in the v2 scope reference. Rebuild and retry.
If step 3 fails (callback never fires or fires with error)
Provider audit logs are your friend here. GitHub: Settings -> Developer settings -> OAuth Apps -> your app -> Advanced -> Recent revocations and authorizations. Microsoft: Entra ID -> Sign-in logs. Slack: api.slack.com/apps -> your app -> Activity logs. The provider records why it rejected the flow.
If step 4 fails (token exchange returns an error)
The token-exchange response always includes an error and error_description field. Common values: invalid_grant (the code is wrong, used, or expired — codes are single-use and expire in ~10 minutes), invalid_client (client_id or client_secret is wrong), redirect_uri_mismatch (the redirect_uri in the POST does not match the one in the authorization URL or the registered URI), unsupported_grant_type (you sent grant_type=code instead of grant_type=authorization_code).
If step 5 fails (profile fetch succeeds but session create errors)
Log the full profile response. Map every field your session create reads to a field the provider actually returns. Most often, you assumed email and the provider returned mail (Microsoft Graph), or you assumed id and OIDC returned sub. Add an explicit mapping layer between the provider response and your user model.
In all cases: enable verbose logging on the callback handler
console.log("oauth callback", {
provider: "github",
code: code ? "present" : "missing",
state: state ? "present" : "missing",
query_keys: Object.keys(req.query),
});
Log enough to diagnose; never log the code itself, the access token, or the client secret. These end up in your function logs and become a credential leak.
How long does it take to fix base44 third-party OAuth?
For a single-provider integration where you have access to the provider's app settings and your Base44 function code, plan 30-60 minutes once you have isolated the failing step. The longest part is usually waiting for the provider's audit log to update or for a propagation delay after you change the registered redirect URI.
For a multi-provider integration where several providers are partially broken, plan 2-4 hours. Fix one provider end-to-end before moving to the next; trying to fix three at once produces a soup of half-applied configurations and you cannot tell which change helped.
Migrating from a custom OAuth provider to a standard one (replacing a home-grown OAuth wrapper with Auth0 or Clerk) is a 1-3 day project depending on how deeply the custom provider's token format is wired into your code.
DIY vs hire decision
DIY this if: You have access to both the Base44 function code and the provider's app settings, you can read function logs, and the failure is on a single provider. Most third-party OAuth fixes are a 5-line change once you know which line is wrong.
Hire help if: The integration touches a partner SSO where you do not own the provider, the failure is intermittent (works for some users and not others, which usually means a tenant or scope edge case), or you need the fix shipped before a launch and you cannot afford a half-day diagnostic loop. Our fix-sprint handles a single broken OAuth integration end-to-end including the audit log review and the production verification.
Need this fixed before your next launch?
Our fix-sprint ships a working OAuth integration for any single provider including the provider-side app configuration review, the callback handler rewrite, PKCE adoption if missing, structured logging instrumentation, and an end-to-end verification in production.
Start a fix-sprint for a broken OAuth integration
Related problems
- Base44 Google authentication not working — the Google-specific OAuth fix with its own consent-screen quirks.
- Base44 white screen / 405 after login — when the OAuth flow completes but the post-login page breaks.
- Auth bypass and SSO vulnerability — the security review you should run after any OAuth change.