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.
Match your OAuth error to the failing step
These errors are spec-defined, so every provider returns the same handful of strings for very different underlying bugs. The string tells you which of the five steps below failed; it does not tell you why. Start here, then read the matching step.
invalid_grant — one string, two unrelated bugs
RFC 6749 §5.2 defines invalid_grant as "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the initial request, or was issued to another client." RFC 7636 then reuses the same code for something completely different: when PKCE is in play and the challenge the provider stored does not match the verifier you present at the token endpoint, §4.6 requires the server to answer with invalid_grant as well. So the same five words mean either "this authorization code is spent, stale, or belongs to another client" or "your PKCE verifier did not survive the round-trip," and no amount of re-reading the response body separates them.
PKCE is the half that bites on Base44, because it is on unless you turned it off. The code_verifier is, in RFC 7636's words, "a cryptographically random string that is used to correlate the authorization request to the token request" — it is generated before you redirect the user out, and it has to still be there, unchanged, when your callback runs minutes later. A Base44 callback function that keeps the verifier in module scope or in an in-process map loses it on any cold start between the two legs, which is why this failure is intermittent and why it gets worse on a low-traffic app whose functions sleep between users. Persist the verifier against the state value — a session cookie or an entity row — and read it back from there.
You can separate the two causes in one run rather than guessing. Base44's Advanced / Manual SSO configuration exposes a Use PKCE toggle, on by default, and Base44's own guidance is to turn it off only for providers that reject unknown OAuth parameters — it names ClassLink and older Shibboleth and ADFS deployments. Turn it off, re-run the flow once, and read the result: if invalid_grant disappears, the verifier was never reaching the token exchange; if it persists, the code itself was reused or expired and you should turn PKCE straight back on. Then go to If step 4 fails.
invalid_request — the authorization URL never reached a consent screen
Per the same section, invalid_request means "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed." Because it fires before the user consents, you will see a provider-hosted error page and nothing at all in your Base44 function logs — which is the fastest way to distinguish it from every other error here. Go to If step 1 fails.
AADSTS50011 (Microsoft Entra ID)
Microsoft's error reference reads: "InvalidReplyTo - The reply address is missing, misconfigured, or doesn't match reply addresses configured for the app." The reply address a Base44 app sends is https://app.base44.com/api/apps/{{APP_ID}}/auth/sso/callback, with your app ID taken from the editor URL between /apps/ and /editor/ — not your own domain, which is what most Entra app registrations get pointed at. If you enabled Use this app's custom domain as the SSO callback (off by default, and it requires a verified custom domain on the Domains tab first), Base44 sends the custom-domain callback instead and that URL has to be registered in Entra as well. Base44's docs specifically name strict Azure and Entra policies as a reason to turn that setting on. Go to If step 3 fails.
unauthorized_client and invalid_client — credentials, not URLs
Two different failures that read almost alike. invalid_client is "Client authentication failed (e.g., unknown client, no authentication method supported, or unsupported authentication method)" — the ID or secret is wrong, or you are sending it the wrong way. unauthorized_client is "The authenticated client is not authorized to use this authorization grant type" — the credentials are fine and the grant type is not permitted for that client. Chasing a redirect URI when you have either of these wastes the afternoon. Go to If step 4 fails.
invalid_scope, and Okta's "Email not returned by OAuth provider"
invalid_scope is "The requested scope is invalid, unknown, malformed, or exceeds the scope previously granted by the resource owner." Base44 expects openid email profile for Google Workspace, Microsoft and generic OIDC, and user:email for GitHub. The related failure that is not an invalid_scope at all is Base44's own message Email not returned by OAuth provider, which some Okta orgs produce because their default authorization server never returns the email claim — Base44's fix is to include the authorization-server path in the Okta Domain field, for example https://your-domain.okta.com/oauth2/default, and to map the claim in the Okta app profile if your org has no custom authorization server. Go to If step 5 fails.
The callback fires, the state does not match, and no session is created
You have a code in the query string and still no session, with no provider-side error anywhere. That is your own state check rejecting the round-trip, and it fails for the same reason the PKCE verifier does: the value was stored somewhere that did not survive to the callback. Go to If step 3 fails.
Two things this table deliberately does not cover. redirect_uri_mismatch on a Google sign-in belongs on the Google auth fix page; the identical string from GitHub is just step 3 here, with GitHub's Authorization callback URL as the field to correct. And every provider in this guide except a hand-rolled OIDC client reaches Base44 through Single Sign-On, which is an Elite-plan feature — on a lower plan the GitHub, Okta and Entra options are not broken, they are not available.
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 — see what the fixed-price Base44 fix sprint covers before you spend a half-day on the diagnostic loop yourself.
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.