Authenticate (Node.js Web Server)#

Inrupt provides the @inrupt/solid-client-authn-node library to authenticate in Node.js.

npm install @inrupt/solid-client-authn-node

For applications implementing Authorization Code Flow:

  1. The application starts the login process by sending the user to the user’s Solid Identity Provider.

  2. The user logs in to the Solid Identity Provider.

  3. The Solid Identity Provider sends the user back to your application, where the application handles the returned authentication information to complete the login process.

Login Flow: 1) Start Login by redirecting user to Solid Identity Provider. 2) User logs in.  3) Solid Identity Provider redirects the user back to the application to handle the returned authentication information.

Node.js Web Server: Multi-session Management#

A Node.js web server can use the @inrupt/solid-client-authn-node library to handle the user authentication flow and manage multiple sessions. In a multi-session context, the server maps requests to sessions. Typically, this is done attaching a cookie to the user’s browser.

From a session lifecycle perspective, there are two main types of requests:

  • those changing the session status (logging in or out),

  • and those performing an authenticated request from the session, without modifying its status.

By default, within the library code, all the session state is stored in memory, and lost on server restart. To persist the session state in external storage, you will need to register listeners for the authorizationRequest and newTokens events (see the dedicated section). These events allow you to capture the state needed to complete the login process and retrieve sessions in a clustered deployment where a sequence of requests may be directed to different nodes.

Starting the authentication flow:

1. Create a new Session for the user at the login endpoint. By default, the Session is periodically refreshed in the background using the refresh token. You should override this legacy behavior by specifying keepAlive: false as a Session option to the Session constructor.

At this point, you should associate the user’s browser to the Session identifier via a cookie, as the identifier is required in subsequent steps.

You should also ensure that you capture the authorizationRequest event and persist the object it returns in external storage. This event is emitted as the user is redirected to their OpenID Provider. The payload contains the state of the session at this stage of the login process so that the login can be completed upon redirect from the OpenID Provider back to the application.

2. Call the Session.login() function to start the login process. Pass in the following login options:

oidcIssuer

Set to the user’s Solid Identity Provider (where handleRedirect will send the user).

redirectUrl

Set to the location that the Solid Identity Provider will send the user back once logged in.

handleRedirect

Set to a callback function that sends users to their Solid Identity Provider.

clientName

(Optional) Set to the display name for the client during the login process. When logging in, the user has to approve the client’s access to the requested data. The clientName is the name displayed during the approval step. If clientName is not provided, a random identifier is generated and used for the name.

For other options available to the function, see ILoginInputOptions.

This process redirects the user from your application to the Solid Identity Provider. Once redirected to the Solid Identity Provider, the user logs in. Upon successful login, the Solid Identity Provider sends the user back to your application.

Completing the authentication flow:
  1. Retrieve the session using one of the methods above.

4. Set up a listener to capture the tokens created during the login process and persist them in external storage if you are using this approach (preferred).

5. To complete the login process, call Session.handleIncomingRedirect(), passing in the URL of the page handling the redirect. Session.handleIncomingRedirect() collects the session information provided by the Solid Identity Provider. Because this information is appended to the URL as query parameters, pass the function the full URL.

6. After Session.handleIncomingRedirect() returns, your session is logged in.

Making an authenticated requests:

7. Once logged in, the Session object provides a fetch() function that retrieves data using available login information.

You can pass this fetch() function as an option to the solid-client functions (e.g., getSolidDataset, saveSolidDatasetAt) to include the user’s credentials with a request.

Logging a session out:

By default, the application may log the user out by clearing the resources associated to the user session. This is an ad-hoc process, specific to the application session management mechanism.

8. In addition, the application may log the user out of their OpenID Provider (see the Session Lifecycle section) using the logout function exposed by @inrupt/solid-client-authn-node.

Getting a list of all the sessions currently in storage:

How session identifiers and tokens are managed by the external persistent storage is out of scope of the library. Listing these sessions is dependent of the specifics of the chosen storage.

Managing the Session tokens#

Warning

Tokens are very sensitive pieces of information because they allow access to user data. They must be stored securely: no third-party should have access to the tokens in storage.

Exchanging tokens with a Session#

Getting the tokens from the Session

When new tokens are issued (on login or on refresh), the newTokens event is emitted by the events emitter of the Session instance. This event is specific to the Node.js environment, it is available in addition to the common events described in the Session Events section of the Authentication documentation.

When listening for the newTokens event, your callback will receive a SessionTokenSet object containing information about the new tokens, including the access token, ID token, refresh token, and expiration information.

See Storing tokens for more information about storing tokens.

Injecting the tokens into a Session

Session.fromTokens is a static function that builds a Session instance from a SessionTokenSet object. If the tokens are not expired, the obtained Session instance is able to make authenticated requests: session.info.isLoggedIn is true. If the tokens have expired, first use the refreshTokens function to refresh the tokens, and then call Session.fromTokens with the new tokens. Do not forget to update persistent storage with the new tokens as well.

Storing tokens#

As part of its authentication lifecycle, the session makes use of two types of tokens:

Token Type

Description

Short-lived tokens

  • Access Token: Used to authenticate API requests to protected resources. Typically valid for a short period (minutes to hours).

  • ID Token: Contains user identity information, used for authentication purposes. Has a similar short lifespan to the Access Token.

Long-lived token

  • Refresh Token: Used to obtain new Access and ID tokens when they expire, without requiring the user to log in again. Usually valid for a longer period (days to weeks).

The Access Token is used directly by the Session to perform authenticated requests. For performance reasons, your application may cache the short-lived tokens to reuse them across multiple requests from an authenticated user.

The Refresh Token is used by the Session to refresh an expired Access Token. Refreshing a token requires a network round-trip with the Identity Provider. The Refresh Token is typically rotated when used: the Identity Provider issues a new Refresh Token when refreshing an Access Token, and the previous Refresh Token can no longer be used. In order to be able to perform authenticated operations without the user being present, the Refresh Token should be stored in a persistent storage.

Example#

The following Express server example uses the @inrupt/solid-client-authn-node library to log in to a Solid server.

Note

In the example, the cookie-session Express middleware is used to associate the session ID to the user’s browser through a cookie.

const express = require("express");
const cookieSession = require("cookie-session");

const {
  getSessionFromStorage,
  getSessionIdFromStorageAll,
  Session
} = require("@inrupt/solid-client-authn-node");

const app = express();
const port = 3000;

// The following snippet ensures that the server identifies each user's session
// with a cookie using an express-specific mechanism
app.use(
  cookieSession({
    keys: ["some secret used to sign cookies"],
  })
);

// For simplicity, all tokens and session state are stored in-memory. In a real case,
// persistent storage would be used for long-lived tokens.
const sessionCache = new Map();

app.get("/login", async (req, res) => {
  // 1. Create a new Session and ensure the request state is captured.
  const session = new Session({ keepAlive: false }); // Turn off periodic refresh of the Session in background
  req.session.sessionId = session.info.sessionId;
  session.events.on("authorizationRequest", (authorizationRequestState) => {
    sessionCache.set(req.session.sessionId, authorizationRequestState);
  });
  const redirectToSolidIdentityProvider = (url) => {
    // Since we use Express in this example, we can call `res.redirect` to send the user to the
    // given URL, but the specific method of redirection depends on your app's particular setup.
    // For example, if you are writing a command line app, this might simply display a prompt for
    // the user to visit the given URL in their browser.
    res.redirect(url);
  };
  // 2. Start the login process; the redirect handler will handle sending the user to their
  //    Solid Identity Provider.
  await session.login({
    // After login, the Solid Identity Provider will send the user back to the following
    // URL, with the data necessary to complete the authentication process
    // appended as query parameters:
    redirectUrl: `http://localhost:${port}/login/callback`,
    // Set to the user's Solid Identity Provider; e.g., "https://login.inrupt.com"
    oidcIssuer: "https://login.inrupt.com",
    // Set to you application's Client Identifier
    clientId: "https://example.org/client-id",
    handleRedirect: redirectToSolidIdentityProvider,
  });
});

app.get("/login/callback", async (req, res) => {
  // 3. If the user is sent back to the `redirectUrl` provided in step 2,
  //    it means that the login has been initiated and can be completed. In
  //    particular, initiating the login stores the session state in storage,
  //    which means it can be retrieved as follows.
  const authorizationRequestState = sessionCache[req.session.sessionId];
  const session = await Session.fromAuthorizationRequestState(
      authorizationRequestState,
      req.session.sessionId
  );

  // 4. Ensure the tokens are cached.
  session.events.on("newTokens", (tokenSet) => {
    sessionCache.set(req.session.sessionId, tokenSet);
  });

  // 5. With your session back from storage, you are now able to
  //    complete the login process using the data appended to it as query
  //    parameters in req.url by the Solid Identity Provider:
  await session.handleIncomingRedirect(`http://localhost:${port}${req.url}`);

  // 6. `session` now contains an authenticated Session instance.
  if (session.info.isLoggedIn) {
    return res.send(`<p>Logged in with the WebID ${session.info.webId}.</p>`)
  }
});

// 7. Once you are logged in, you can retrieve the tokens from the cache,
//    and perform authenticated fetches.
app.get("/fetch", async (req, res) => {
  if(typeof req.query["resource"] === "undefined") {
    res.send(
      "<p>Please pass the (encoded) URL of the Resource you want to fetch using `?resource=&lt;resource URL&gt;`.</p>"
    );
  }
  const sessionTokenSet = sessionCache.get(req.session.sessionId);
  const session = await Session.fromTokens(
    sessionTokenSet,
    req.session.sessionId,
  );
  console.log(
    await session.fetch(req.query["resource"])
      .then((response) => response.text())
  );
  res.send("<p>Performed authenticated fetch.</p>");
});

// 8. To log out a session, just retrieve the session, and
//    call the .logout method.
app.get("/logout", async (req, res) => {
  const sessionTokenSet = sessionCache.get(req.session.sessionId);
  const session = await Session.fromTokens(
      sessionTokenSet,
      req.session.sessionId,
  );
  session.logout();
  res.send(`<p>Logged out.</p>`);
});

app.listen(port, () => {
  console.log(
    `Server running on port [${port}]. ` +
    `Visit [http://localhost:${port}/login] to log in to [login.inrupt.com].`
  );
});