# Login With Oasis

# Overview

In addition to storing sensitive data directly in your app or your Oasis Parcel account, you can also store data in your users' Oasis accounts, giving them full ownership, control, and visibility over their data.

To store data in user accounts, your users need to sign up for Oasis Steward (opens new window), after which you will need to ask them to authenticate themselves to your application. Behind the scenes, users are authenticated via the Oasis Auth service which implements OpenID Connect (opens new window) (OIDC), an identity layer on top of the OAuth 2.0 (opens new window) standard.

# How It Works

Each user is represented by a Parcel identity on the Oasis platform. We will use Oasis Auth to retrieve a unique ID associated with this identity. Then, you can either:

  • use Oasis Auth as a single method of authentication for your application, or
  • combine Oasis Auth with your application's own authentication method by linking the user's Oasis account with their account in your existing system.

In the example below we will guide our users through the following authentication procedure:

  1. (In the case of account linking) The user first logs in to your application using the application-specific authentication method. Otherwise, this step is skipped.
  2. User wants to manage their data through Oasis Steward.
  3. User is redirected to Oasis Auth.
  4. User logs in to Oasis Auth (or signs up for the first time).
  5. On successful login and allowing your app/client to access the user's data, they are redirected back to your application. The callback URL contains an authorization code.
  6. Your application exchanges the authorization code with Oasis Auth for an ID token.
  7. Your application extracts a Parcel identity from the ID token.
  8. (In the case of account linking) A Parcel identity can be linked to your internal user ID.

The steps described above implement a so called explicit authentication flow. If this is the first time you met with the OAuth 2.0 standard and OIDC, we suggest you to read these tutorials (opens new window) up to Chapter 5 (Single-Page Apps) to learn about the authentication basics and other flows.

# Oasis Auth for Backend Web Applications

Let's create a simple Node.js application which executes all application business logic on the server. We will use openid-client library to perform most of the OIDC heavy lifting.

EXAMPLE

A complete example can be found in the Parcel Examples repository. We recommend that you use it as a reference while reading this tutorial.

To run the example set your backend client credentials in src/server.ts, ACME_BACKEND_CLIENT_ID environment variable to your backend client ID, spin up the example with npm i && npm start and then visit http://localhost:4050 with your browser.

The first thing we need to do in our backend application is to configure the openid-client:

// Configure OpenID Connect client.
const PARCEL_AUTH_URL = process.env.PARCEL_AUTH_URL ?? 'https://auth.oasislabs.com';

const issuer = await Issuer.discover(PARCEL_AUTH_URL);

const client = new issuer.Client(
  {
    // Replace with your app's back-end client ID.
    client_id: process.env.ACME_BACKEND_CLIENT_ID!,
    redirect_uris: ['http://localhost:4050/callback'],
    response_types: ['code'],
    token_endpoint_auth_method: 'private_key_jwt',
    token_endpoint_auth_signing_alg: 'ES256',
    id_token_signed_response_alg: 'ES256',
  },
  {
    keys: [
      {
        // Back-end client private key.
        // Note: Make sure kid matches the one you added in portal.
        kid: 'acme-backend-client',
        use: 'sig',
        kty: 'EC',
        crv: 'P-256',
        alg: 'ES256',
        x: 'mqlepd4Gr5L4zEauL2V-3x46cvXFTP10LY4AfOyCjd4',
        y: 'iTMKFMDJVqDDf-Tbt3fVxVs4F84_6nSpMji9uDCE3hY',
        d: 'SjSlVeiDxJ9wFBLIky2WSoUTI3NBJgm2YpbxBpfPQr0',
      },
    ],
  },
);

In the beginning of the snippet above we discover Oasis Auth configuration by calling Issuer.discover(). Feel free to simply replace the PARCEL_AUTH_URL constant with the https://auth.oasislabs.com literal.

Then, we create a client with some metadata and a list of private JSON Web Keys. Adapt the following options:

  • client_id is the client ID of your app's backend client. If you have not generated a backend client in the Parcel developer portal yet, check the Registering Credentials section. Then, export the obtained client ID to the ACME_BACKEND_CLIENT_ID environment variable.

  • redirect_uris must contain one URL to which the user's browser will be redirected to after the authentication. We will use http://localhost:4050/callback since we will be locally running Node.js on port 4050 and answering /callback GET request.

    WARNING

    The redirect URL must be listed in your client's redirect URLs list in the Parcel developer portal! Also, due to security considerations the callback must either use a secure https protocol or be deployed at localhost.

  • Add at least one private JSON Web Key to the list which matches the public key you registered the client with.

We configure specific primitives for verifying the JSON Web Token next. In the snippet below we generate a unique, 10 bytes long state and nonce:

const state = generators.state(10);
const nonce = generators.nonce(10);
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);

Now we can implement our index page. Since we are developing a backend web application we will use a simple express framework together with the pug templating engine for Node.js. We generate the authorization URL by using the client object we defined above and then inject this URL to the index template:

app.get('/', (req: express.Request, res: express.Response) => {
  // Obtain authorization URL.
  const authorizationUrl = client.authorizationUrl({
    scope: 'openid profile email',
    audience: 'https://api.oasislabs.com/parcel',
    state,
    nonce,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });
  res.render('index', {
    authUrl: authorizationUrl,
  });
});

Let's look more closely at some parameters we provided while generating the authorization URL above:

  • scope is a list of user data which your application would like to obtain. openid, profile, and email are all standard OpenID claims (opens new window).
  • audience for the Parcel SDK is always https://api.oasislabs.com/parcel,
  • state, nonce, and code_challenge values were initially generated above and will be required in the last step of authorization flow,

The index page is now implemented, so you should be able to see a nice food recipe landing page, if you are running the example code. Notice the following message:

h2 Data Privacy
p
    | Here at Acme ltd., your privacy is very important to us. We've
    | partnered with Oasis Labs so you can own your recipe data from our
    | app.

It is a good idea to motivate users a bit by giving them a context around what Oasis is and why they should take control of their data!

Now, when a user clicks on the authorization URL link, he will be redirected to Oasis Auth. Once logged in, your user will be prompted to share his newly minted Parcel identity address, profile, and email address:

oasis_auth_share_openid_profile_email

The user's browser will then redirect back to the callback URL you provided in a redirect_uris client parameter. If the user agreed to sharing their information, an authorization code and some other callback parameters will be passed along in the URL.

Finally, we need to implement the callback page. Obtain and verify the authorization token set from the URL callback parameters using the same state and nonce we used to generate the authorization URL:

const callbackParams = client.callbackParams(req.url);

// Exchange code for tokens.
const tokenSet = await client.callback(
  'http://localhost:4050/callback',
  callbackParams,
  {
    code_verifier: codeVerifier,
    state,
    nonce,
    response_type: 'code',
  },
  {
    exchangeBody: {
      audience: 'https://api.oasislabs.com/parcel',
    },
  },
);

Then extract the ID token from the authorization token set and, as a proof of concept, print the Parcel identity located inside the sub field:

const idToken = tokenSet.claims();

console.log(`ID token:\n${JSON.stringify(idToken)}`);

res.render('callback', {
  parcelId: idToken.sub,
});

Go ahead and try it! If everything goes well, the Parcel identity shown should match the one from your profile in Oasis Steward (opens new window).

From here on you can, instead of printing this ID, link it with existing user IDs in your application.

# Oasis Auth for Static Single-Page Applications

Alternatively, let's look at how to implement the authentication flow in a static, client-side, feature-focused single-page application. In essence, just two implementation details are different:

  • In the Parcel developer portal you will need to register a frontend client instead of the backend one.
  • You will need to use a JavaScript library for OIDC which works well in the browser. In our case that will be oidc-client.

EXAMPLE

Go ahead and check out the frontend example code from the Parcel Examples repository first. You can start the example by typing npm i && npm start after setting the ACME_FRONTEND_CLIENT_ID environment variable.

In contrast to the backend example, all of the business logic will be implemented on the frontend side, namely in two HTML files: index.html and callback/index.html. However, for easier development and testing the example will still use Node.js and express to serve static content.

First, let's look at the index page. We start by creating the configuration for oidc-client:

const PARCEL_AUTH_URL = process.env.PARCEL_AUTH_URL ?? 'https://auth.oasislabs.com';

const oidcConfig = {
  authority: PARCEL_AUTH_URL,
  // Replace with your app's frontend client ID.
  client_id: process.env.ACME_FRONTEND_CLIENT_ID!,
  redirect_uri: `http://localhost:${port}/callback`,
  response_type: 'code',
  scope: 'openid profile email',
  filterProtocolClaims: false,
  loadUserInfo: false,
  extraQueryParams: {
    audience: 'https://api.oasislabs.com/parcel',
  },
  extraTokenParams: {
    audience: 'https://api.oasislabs.com/parcel',
  },
};

It is very similar to the one we used in the backend example for creating the client instance and generating the authorization URL with two modifications:

  • client_id now points to the frontend client of your app.
  • Since the frontend clients don't use a keypair, there is no list of private JSON Web Keys.

Now let's initialize oidc-client with the above configuration:

<script src="/getOidcConfig" type="text/javascript"></script>
<script>
  Oidc.Log.logger = console;
  Oidc.Log.level = Oidc.Log.DEBUG;
  console.log(oidcConfig)
  const oidcClient = new Oidc.UserManager(oidcConfig);
</script>

TIP

We actually created a configuration above in the backend dynamically to make use of environment variables and then passed it to the frontend via the /getOidcConfig action. When the application is deployed the configuration should reside in a static, publicly accessible asset instead.

The index page is now complete. You can go ahead and try it. When the user clicks the Get started with Oasis button, oidc-client will obtain the authorization URL and redirect the user to the Oasis Auth. The authentication steps which follow are the same as in the backend example.

Based on the configuration we supplied to oidc-client, we need to be ready to receive the authorization code at https://localhost:4050/callback. We will do this solely on the frontend side by serving the callback/index.html file. Inside, we use the following JavaScript snippet:

<script src="/getOidcConfig" type="text/javascript"></script>
<script>
  Oidc.Log.logger = console;
  Oidc.Log.level = Oidc.Log.DEBUG;
  const oidcClient = new Oidc.OidcClient(oidcConfig);
  (async function () {
    try {
      const response = await oidcClient.processSigninResponse(location.href)
      const idToken = response.profile;
      console.log(`ID token:\n${JSON.stringify(idToken)}`);

      document.getElementById('parcel-id').innerText = `${idToken.sub}`;
      document.getElementById('result').hidden = false
    } catch (error) {
      document.getElementById('result').hidden = true;
      document.getElementById('error').innerText = `${error}`;
    }
  })()
</script>

First, we initialize the oidc-client the same way as we did in the index page. Then, we exchange the authorization code for the ID token using the oidcClient.processSigninResponse and passing it the URL-encoded callback parameters. The resulting object is equal to the ID token used in the backend tutorial with the sub field containing the user's Parcel identity.

Now that your users are onboard, you can start managing their data in the next tutorial!