Vipps as custom OAuth provider with Parse Server hosted with Back4App, and React Native

Tags: Vipps, OAuth, Open ID Connect, Back4App, Parse Server, React Native, TypeScript

lock on a wire

Published: 2021-02-20

In this blog post I try to explain how I used Vipps as a custom OAuth provider for a Back4App hosted Parse Server, and utilizing React Native as the client.

Vipps is a Norwegian payment and authentication provider, but the process of connecting with a 3rd party OAuth provider that are not out-of-the-box supported by the Parse Server should be similar.

This post is separated into five parts:

  1. Setting up the authentication provider on the Parse Server.
  2. Configuring the authentication provider in Back4App.
  3. Authenticating from the React Native client
  4. Connecting the Vipps deep linking
  5. Creating the cloud code for signing in.

Setting up the authentication provider on the Parse Server in Back4App

First off, you need an authentication setup in the cloud code on the Parse Server. Add a customAuthenticationProvider.js in your cloud folder. The name here isn't important, you can call it what you want.

In Back4App you can either upload your cloud code in the Parse Dashboard or you can use their CLI.

Two methods are required for implementing a custom auth provider and those are validateAuthData(authData, options) and validateAppId(appIds, authData, options).

In most cases the validateAppId is not necessary as there's no app id to validate. If I understood this correctly, for instance Facebook requires you to create an app in the Facebook developer pages for enabling OAuth login, and it's this id that should be validated in this method. Here's the implementation for the Facebook login.

In our case an empty promise will suffice.

const validateAppId = (appIds, authData, options) => {
  return Promise.resolve();
};

For the validateAuthData part we need to check that the authenticated user actually is the user it claims to be. For this we call the user info api to validate that ID's match. The getUserData is just a helper method for making a http request to the user info endpoint.

const validateAuthData = (authData, options) => {
  const token = "Bearer " + authData.access_token;
  return getUserData(token).then(({ status, data }) => {
    if (data && data.sub === authData.id) return;
    throw new Parse.Error(
      Parse.Error.OBJECT_NOT_FOUND,
      "Vipps auth is invalid for this user."
    );
  });
};

The whole file will then look like this:

const https = require("https");
const Parse = require("parse/node").Parse;

const getJson = (token) => {
  const options = {
    port: 443,
    method: "GET",
    host: "api.vipps.no",
    path: "/vipps-userinfo-api/userinfo",
    headers: {
      Authorization: token,
      "User-Agent": "parse-server",
      "Content-Type": "application/json",
    },
  };

  let reqHandler = https;

  return new Promise((resolve, reject) => {
    let req = reqHandler.request(options, (res) => {
      let output = "";
      res.setEncoding("utf8");

      res.on("data", function (chunk) {
        output += chunk;
      });

      res.on("end", () => {
        try {
          let obj = JSON.parse(output);
          resolve({
            statusCode: res.statusCode,
            data: obj,
          });
        } catch (err) {
          console.error("rest::end", err);
          reject(err);
        }
      });
    });

    req.on("error", (err) => {
      console.error("rest::request", err);
      reject(err);
    });

    req.end();
  });
};

const validateAuthData = (authData, options) => {
  const token = "Bearer " + authData.access_token;
  return getJson(token).then(({ status, data }) => {
    if (data && data.sub === authData.id) return;
    throw new Parse.Error(
      Parse.Error.OBJECT_NOT_FOUND,
      "Vipps auth is invalid for this user."
    );
  });
};

const validateAppId = (appIds, authData, options) => {
  return Promise.resolve();
};

module.exports = {
  validateAppId,
  validateAuthData,
};

Configuring the authentication provider in Back4App.

Now that the custom authentication provider is in place in our cloud code, we just need to configure the Parse Server instance to pickup this custom configuration.

Lets head to the [Server Settings -> Custom Parse Options] and tell the Parse Server to use the file from the previous section as a custom auth provider. Add the following JSON to the Custom Parse Server Options

{
  "auth": {
    "vippsAuthentication": {
      "module": "/usr/src/app/data/cloud/customAuthenticationProvider.js"
    }
  }
}

There's two things to notice here; the name of authentication module, in this case customAuthenticationProvider, and the path to the cloud code location at Back4App. The name because we are using it later and the path to make sure it's typed correctly so the config finds the file.

Authenticating from the React Native client

For authenticating, an OAuth library is chosen, in this case react-native-app-auth. Setup instructions for this library is well explained on their GitHub page so I'm not going go through that here.

First off, the following needs to be imported:

import Parse, { AuthData } from "parse/react-native";
import { AuthConfiguration, authorize } from "react-native-app-auth";

Following is the complete code for the login process. In short, this is what is happening:

  1. Calling authorize to open the Vipps OAuth endpoint.
  2. Fetching the user data
  3. Login to Parse with the custom cloud code login.
const vippsLogin = async (): Promise<IUser> => {
  // Setup
  const config: AuthConfiguration = {
    issuer: "https://api.vipps.no/access-management-1.0/access",
    clientId: "YOUR-VIPPS-CLIENT-ID",
    clientSecret: "YOUR-VIPPS-CLIENT-SECRET",
    redirectUrl: "yourapp://deep/link/url/scheme",
    scopes: ["openid", "name", "email", "phoneNumber", "api_version_2"],
  };
  try {
    // The actual authentication handled by react-native-app-auth
    const authResult = await authorize(config);

    // Get the user info from the user info api
    const userInfoResponse = await fetch(
      "https://api.vipps.no/vipps-userinfo-api/userinfo/",
      {
        method: "GET",
        headers: {
          Authorization: "Bearer " + authResult.accessToken,
        },
      }
    );
    const userInfo: IVippsUserInfo = await userInfoResponse.json();

    // Create the Parse auth data
    const authData: AuthData = {
      id: userInfo.sub,
      access_token: authResult.accessToken,
      expiration_date: authResult.accessTokenExpirationDate,
    };

    try {
      // Run the custom linking code with the user info and auth data
      // which is defined in the last section.
      const mergedUser = await Parse.Cloud.run("linkUserWithVipps", {
        vippsUser: userInfo,
        authData,
      });

      // Login the authenticated user and storing session on device
      const loggedInUser = await Parse.User.logInWith(
        "vippsAuthentication", // This is the name from the config JSON
        {
          authData,
        }
      );
      return loggedInUser;
    } catch (e) {
      console.log("Error logging in with Vipps: ", e);
      throw e;
    }
  } catch (e) {
    throw e;
  }
};

Connecting the Vipps deep linking

To be able to use Vipps as authentication you have to enable it in their developer pages and add allowed callback URL's for your project, as shown in the following image:

Setup deep link in Vipps

Creating the cloud code for signing in.

Now we need some back-end code for the client to call when signing in. Create a main.js in your cloud code folder if you haven't already, and define a cloud code function.

Parse.Cloud.define("linkUserWithVipps", async (request) => {
  try {
    const { vippsUser, authData } = request.params;
    const phoneNumber = vippsUser.phone_number.substr(2); // Remove the country code

    // Check if user already exist
    const userQuery = new Parse.Query(Parse.User);
    userQuery.equalTo("username", vippsUser.email);
    let targetUser = await userQuery.first({ useMasterKey: true });

    if (!targetUser) {
      targetUser = new Parse.User();
    }

    // Update user with the data from the user info api
    const savedUser = await targetUser.save(
      {
        firstName: vippsUser.given_name,
        lastName: vippsUser.family_name,
        email: vippsUser.email,
        username: vippsUser.email,
        phone: phoneNumber,
        password: Math.random().toString(36).substr(2, 8),
      },
      { useMasterKey: true }
    );

    // Link user with the custom authentication provider
    return await savedUser.linkWith(
      "vippsAuthentication", // the name chosen in the config section above
      { authData },
      { useMasterKey: true }
    );
  } catch (e) {
    throw e;
  }
});

And that should be it for authenticating and linking a custom OAuth provider with Parse Server on Back4App.

If you have questions or comments, you can reach out to me on Twitter @kentrh.

Thank you!