DEV Community

Meat Boy
Meat Boy

Posted on

How to add Limited Login Facebook on iOS and Android on server-side and client-side

Meta recently introduced the concept of Limited Login for iOS authentication with Facebook. This tutorial provides a step-by-step guide on integrating Facebook Login into a React Native application, including server-side implementation, with a focus on supporting both standard and limited login flows in 2024.

1. Project Initialisation

If you have an existing application, you may bypass this section. For those starting anew, follow these instructions to create a new application:
Initialise a new project using Expo
Initialise a new project with React Native CLI

2. Installation of react-native-fbsdk-next

Follow the official documentation for installation instructions:
FBSDK Next - Expo Setup
or
FBSDK Next - React Native CLI Setup

3. Implementation of Limited Login on the Client Side

Integrate the following code snippet to implement Limited Login in your React Native project:

const loginWithFacebook = async () => {
    try {
      const result = await LoginManager.logInWithPermissions(
        ['public_profile', 'email'],
        'limited' // This parameter ensures consistent limited login behaviour on iOS
      );
      if (result.isCancelled) {
        return;
      }

      let token = await (Platform.OS === 'ios'
        ? AuthenticationToken.getAuthenticationTokenIOS().then(
            data => data?.authenticationToken || ''
          )
        : AccessToken.getCurrentAccessToken().then(
            data => data?.accessToken || ''
          ));

      // Utilise the token for further operations, such as server-side authentication
    } catch (e) {
      console.error(e);
    }
  };
Enter fullscreen mode Exit fullscreen mode

4. Server-Side Token Validation

The server-side implementation requires handling both authorisation codes and authentication codes to support standard and limited login flows.

Let's create a new endpoint using express. Note that this approach can be adapted to other web server frameworks with minimal modifications.

router.post('/auth/facebook', async (req, res, next) => {
  try {
    const { facebookToken } = req.body;
    const { facebookUserId, facebookUserName } = await getFacebookUser({ token: facebookToken, appId: facebookAppId });

    // Utilise facebookUserId and facebookUserName for user creation or authentication
  } catch (err) {
    // Token exchange or validation failure; handle the error or pass it to the next middleware
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

Now, let's implement the getFacebookUser function to validate and exchange tokens for user data. This function handles both authentication tokens and OIDC Authorisation Tokens.

interface FacebookUser {
  facebookUserId: string;
  facebookUserName: string;
}

interface GetFacebookUserParams {
  token: string;
  appId: string;
}

export async function getFacebookUser({ token, appId }: GetFacebookUserParams): Promise<FacebookUser> {
  try {
    return await getStandardFacebookUser(token);
  } catch (error) {
    console.warn('Failed to get standard Facebook user, trying limited user');
    return getLimitedFacebookUser({ token, appId });
  }
}
Enter fullscreen mode Exit fullscreen mode

First, let's handle the standard flow Authorisation Code. You can simply use it to exchange data by calling API. We will use axios for REST API calls. You may also need jwks-rsa for extracting the proper key from jwks endpoint (you may also use axios, but it requires a little more effort) and jsonwebtoken library for validation and decoding of JWT. Of course, you can use any other tech stack. The general idea will be the same in Go, Python or Java.

const FACEBOOK_GRAPH_API = 'https://graph.facebook.com/me';

async function getStandardFacebookUser(token: string): Promise<FacebookUser> {
  try {
    const { data } = await axios.get(`${FACEBOOK_GRAPH_API}?fields=id,name&access_token=${token}`);
    if (!data.id || !data.name) {
      throw new Error('Invalid token (missing id or name)');
    }
    return { facebookUserId: data.id, facebookUserName: data.name };
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(`Failed to fetch Facebook user: ${error.message}`);
    }
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the response, you will get both the id and the name of the user scoped into your application. If any data is missing, we will throw an error. No matter what the issue is, we still want to give the user a chance to sign in. Maybe the user signed in using iOS with ATT turned off and the token is OIDC Authentication Token. Then we need to:

const FACEBOOK_JWKS_URI = 'https://www.facebook.com/.well-known/oauth/openid/jwks';
const FACEBOOK_ISSUER = 'https://www.facebook.com';

async function getLimitedFacebookUser({ token, appId }: GetFacebookUserParams): Promise<FacebookUser> {
  const client = jwksClient({ jwksUri: FACEBOOK_JWKS_URI });

  return new Promise((resolve, reject) => {
    jwt.verify(
      token,
      async (header, callback) => {
        try {
          const key = await client.getSigningKey(header.kid);
          callback(null, key.getPublicKey());
        } catch (error) {
          callback(error as Error, undefined);
        }
      },
      {
        algorithms: ['RS256'],
        audience: appId,
        issuer: FACEBOOK_ISSUER,
      },
      (err, decoded) => {
        if (err) {
          reject(new Error(`JWT verification failed: ${err.message}`));
        } else {
          const { sub, name } = decoded as JwtPayload;
          if (!sub || !name) {
            reject(new Error('Invalid token (missing sub or name)'));
          } else {
            resolve({ facebookUserId: sub, facebookUserName: name });
          }
        }
      }
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

In the code above, first, you create a new JWKS client looking for a list of public keys of Facebook. Then you will extract key id (kid) from JWT to get the proper public key. At this moment, all JWT signatures are signed using RS256 algorithm, so you have to specify that. The audience of the JWT is your application, so it must be set to appId from the Facebook app developer dashboard. The issuer is constant and always https://www.facebook.com.

In the next step, JWT will be either validated or not. If it's validated, you can safely decode it and get both sub and name. The sub property is the user id in your Facebook application scope, while name is the public display name of the user.

The whole code looks like this:

import jwksLocalClient from 'jwks-rsa';
import jwt, { JwtPayload } from 'jsonwebtoken';
import axios, { isAxiosError } from 'axios';

export async function getStandardFacebookUser(token: string) {
  const { data } = await axios.get(`https://graph.facebook.com/me?fields=id,name&access_token=${token}`);
  if (!data.id) {
    throw new Error('Invalid token (missing id or name)');
  }
  return { facebookUserId: data.id, facebookUserName: data.name };
}

export async function getLimitedFacebookUser({
  token,
  appId,
}: {
  token: string;
  appId: string;
}) {
  const jwksClient = jwksLocalClient({
    jwksUri: 'https://www.facebook.com/.well-known/oauth/openid/jwks',
  });

  return new Promise<{
    facebookUserId: string;
    facebookUserName: string;
  }>((resolve, reject) => {
    jwt.verify(
      token,
      async (header, callback) => {
        const key = await jwksClient.getSigningKey(header.kid);
        const signingKey = key.getPublicKey();
        callback(null, signingKey);
      },
      {
        algorithms: ['RS256'],
        audience: appId,
        issuer: 'https://www.facebook.com',
      },
      (err, decoded) => {
        if (err) reject(err);
        const decodedData = decoded as JwtPayload;
        if (!decodedData.sub) {
          reject(new Error('Invalid token (missing sub)'));
        } else {
          resolve({ facebookUserId: decodedData.sub, facebookUserName: decodedData.name });
        }
      },
    );
  });
}

export async function getFacebookUser({
  token,
  appId,
}: {
  token: string;
  appId: string;
}) {
  try {
    return await getStandardFacebookUser(token);
  } catch (error) {
    if (isAxiosError(error)) {
      console.warn('Failed to get standard Facebook user, trying limited user');
    }
    return getLimitedFacebookUser({
      token,
      appId,
    });
  }
}

router.post('/auth/facebook', async (req, res, next) => {
  try {
    const { facebookToken } = req.body;
    const { facebookUserId, facebookUserName } = await getFacebookUser({ token: facebookToken, appId: facebookAppId });

    // Utilise facebookUserId and facebookUserName for user creation or authentication
  } catch (err) {
    // Token exchange or validation failure; handle the error or pass it to the next middleware
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

This implementation provides a quick solution for authenticating users regardless of their chosen login flow. If you require further assistance, please leave a comment, and I will do my best to help :)

Top comments (0)