in src/emulator/auth/operations.ts [1416:1581]
function signInWithIdp(
state: ProjectState,
reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithIdpRequest"]
): SignInWithIdpResponse {
assert(!state.disableAuth, "PROJECT_DISABLED");
assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION");
if (reqBody.returnRefreshToken) {
throw new NotImplementedError("returnRefreshToken is not implemented yet.");
}
if (reqBody.pendingIdToken) {
throw new NotImplementedError("pendingIdToken is not implemented yet.");
}
const normalizedUri = getNormalizedUri(reqBody);
const providerId = normalizedUri.searchParams.get("providerId")?.toLowerCase();
assert(
providerId,
`INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential: ${normalizedUri.toString()}`
);
const oauthIdToken = normalizedUri.searchParams.get("id_token") || undefined;
const oauthAccessToken = normalizedUri.searchParams.get("access_token") || undefined;
const claims = parseClaims(oauthIdToken) || parseClaims(oauthAccessToken);
if (!claims) {
// Try to give the most helpful error message, depending on input.
if (oauthIdToken) {
throw new BadRequestError(
`INVALID_IDP_RESPONSE : Unable to parse id_token: ${oauthIdToken} ((Auth Emulator only accepts strict JSON or JWTs as fake id_tokens.))`
);
} else if (oauthAccessToken) {
if (providerId === "google.com" || providerId === "apple.com") {
throw new NotImplementedError(
`The Auth Emulator only support sign-in with ${providerId} using id_token, not access_token. Please update your code to use id_token.`
);
} else {
throw new NotImplementedError(
`The Auth Emulator does not support ${providerId} sign-in with credentials.`
);
}
} else {
throw new NotImplementedError(
"The Auth Emulator only supports sign-in with credentials (id_token required)."
);
}
}
// Generic SAML flow
let samlResponse: SamlResponse | undefined;
let signInAttributes = undefined;
if (normalizedUri.searchParams.get("SAMLResponse")) {
// Auth emulator purposefully does not parse SAML and expects SAML-related
// fields to be JSON objects.
samlResponse = JSON.parse(normalizedUri.searchParams.get("SAMLResponse")!) as SamlResponse;
signInAttributes = samlResponse.assertion?.attributeStatements;
assert(samlResponse.assertion, "INVALID_IDP_RESPONSE ((Missing assertion in SAMLResponse.))");
assert(
samlResponse.assertion.subject,
"INVALID_IDP_RESPONSE ((Missing assertion.subject in SAMLResponse.))"
);
assert(
samlResponse.assertion.subject.nameId,
"INVALID_IDP_RESPONSE ((Missing assertion.subject.nameId in SAMLResponse.))"
);
}
let { response, rawId } = fakeFetchUserInfoFromIdp(providerId, claims, samlResponse);
// Always return an access token, so that clients depending on it sorta work.
// e.g. JS SDK creates credentials from accessTokens for most providers:
// https://github.com/firebase/firebase-js-sdk/blob/6d640284ef6fd228bd7defdcb2d85a9f88239ad8/packages/auth/src/authcredential.js#L1515
response.oauthAccessToken =
oauthAccessToken || `FirebaseAuthEmulatorFakeAccessToken_${providerId}`;
response.oauthIdToken = oauthIdToken;
// What about response.refreshToken?
const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined;
const userMatchingProvider = state.getUserByProviderRawId(providerId, rawId);
let accountUpdates: AccountUpdates;
try {
if (userFromIdToken) {
assert(!userMatchingProvider, "FEDERATED_USER_ID_ALREADY_LINKED");
({ accountUpdates, response } = handleLinkIdp(state, response, userFromIdToken));
} else if (state.oneAccountPerEmail) {
const userMatchingEmail = response.email ? state.getUserByEmail(response.email) : undefined;
({ accountUpdates, response } = handleIdpSigninEmailRequired(
response,
rawId,
userMatchingProvider,
userMatchingEmail
));
} else {
({ accountUpdates, response } = handleIdpSigninEmailNotRequired(
response,
userMatchingProvider
));
}
} catch (err: any) {
if (reqBody.returnIdpCredential && err instanceof BadRequestError) {
response.errorMessage = err.message;
return response;
} else {
throw err;
}
}
if (response.needConfirmation) {
return response;
}
const providerUserInfo: ProviderUserInfo = {
providerId,
rawId,
// For some reason, production API responses sets federatedId to be same as
// rawId, instead of the prefixed ID. TODO: Create internal bug?
federatedId: rawId,
displayName: response.displayName,
photoUrl: response.photoUrl,
email: response.email,
screenName: response.screenName,
};
let user: UserInfo;
if (response.isNewUser) {
user = state.createUser({
...accountUpdates.fields,
lastLoginAt: Date.now().toString(),
providerUserInfo: [providerUserInfo],
tenantId: state instanceof TenantProjectState ? state.tenantId : undefined,
});
response.localId = user.localId;
} else {
if (!response.localId) {
throw new Error("Internal assertion error: localId not set for exising user.");
}
user = state.updateUserByLocalId(
response.localId,
{
...accountUpdates.fields,
},
{
upsertProviders: [providerUserInfo],
}
);
}
if (user.email === response.email) {
response.emailVerified = user.emailVerified;
}
if (state instanceof TenantProjectState) {
response.tenantId = state.tenantId;
}
if (
(state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") &&
user.mfaInfo?.length
) {
return { ...response, ...mfaPending(state, user, providerId) };
} else {
user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() });
return { ...response, ...issueTokens(state, user, providerId, { signInAttributes }) };
}
}