function signInWithIdp()

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 }) };
  }
}