0.10.5 โ€ข Published 3 years ago

sapper-oidc v0.10.5

Weekly downloads
4
License
Apache License 2....
Repository
github
Last release
3 years ago

logo of the package

Sapper OIDC Build Status CodeQL

This library is based on top of node-openid-client and allow you to quickly and effortlessly add OIDC to your sapper application. It is first meant to be used in a first-party scenario where you are the owner of the IDP (i.e you use Okta, Auth0, IdentityServer, Ory Hydra...); That being said, it works with anything that follows the open id connect specification. ๐Ÿงช Please note that this library is experimental and I wouldn't recommend you to use it in production for now. It has the following features

  • ๐Ÿ‘ฎโ€โ™€๏ธ Page protection (Will redirect the user to login, if on a page set to be protected)
  • ๐Ÿšดโ€โ™‚๏ธ Automatic token refresh on the frontend and backend (without using an iframe)
  • ๐Ÿ—„ Session management
  • โ†ช๏ธ Automatic redirection back to where the user was before the auth flow initiated.
  • ๐Ÿ”ฎ Silent login if the user already has a session at the IDP (without using an iframe)

Todo

  • Add a way to logout
  • Add a way to login programmatically (right now it logs you in only if you navigate on a protected path or if you enable silent login)
  • Support older versions of redis
  • Less boilerplate

Limitation

  • You can only have one identity provider.
  • You can only use Redis as the session store, and it must be >= v6.0.
  • Route with a "." will be ignored.

Installation

๐Ÿšง You must install https://www.npmjs.com/package/rollup-plugin-node-externals ๐Ÿšง And https://www.npmjs.com/package/@rollup/plugin-json rollup.config.js

import externals from "rollup-plugin-node-externals";
import json from "@rollup/plugin-json";

export default {
    ......
    server: {
        ......
        /* IMPORTANT, externals() needs to be at
        the top of the plugins array*/
        plugins: [externals(), json()]
        ......
    }
    ......
}
npm i --save-dev sapper-oidc

Create a confirguration file

in your /src create a file, for example named OIDCConfig.js and add the following:

export const authPath = "/auth"; // This route initiate the OIDC flow.
export const refreshPath = "/refresh"; // This is the route that will be called when tokens need to be refreshed
export const protectedPaths = [
  // This array stores all routes where the user MUST be logged in, if he is not he will be redirected to the identity provider.
  {
    path: "/private-info",
    recursive: true /* This means that all route starting with /private-info requires the user to be logged in*/,
  },
  {
    path: "/privateOnlyHere",
    recursive: false /* This means that only /privateOnlyHere requires the user to be logged in, /privateOnlyHere/1234569 doesn't require the user to be logged in*/,
  },
];

Server side configuration

You need to wrap all your code in an immediately invoked async function server.js

import { authPath, refreshPath, protectedPaths } from "./OIDCConfig"; // The file we just created
import { SapperOIDCClient } from "sapper-oidc/lib/server";

(async function () {
  const options = {
    issuerURL: "https://accounts.google.com/", // See your identity provider documentation
    clientID: "8db8f07d-547d-4e8b-8d8b-218fc08b7188",
    clientSecret: "3nxeS5K3mFe.5Hv7Gvjp6xUWq~",
    redirectURI: "http://127.0.0.1:3000/cb", // This is the URL the idp will redirect the user to. It must be the callback route that you will define bellow; you must add this url to your IDP authorized redirect URI.
    silentRedirectURI: "http://localhost:3001/silentcb", // (OPTIONAL) This is the callback URL if you want to silently login the user, you must add this URL to your IDP authorized redirect URI if you add this line.
    sessionMaxAge: 60 * 60 * 24 * 7, // How long does a user's session lives for (in seconds)
    authRequestMaxAge: 60 * 60, // How much time before an auth request is deemed invalid (in seconds).
    authPath,
    refreshPath,
    protectedPaths,
    /* Where do you want the user to be redirected to upon successful auth
      Except if you set at the callback route to redirect the user back to
      where he was before */
    authSuccessfulRedirectPath: "http://127.0.0.1:3000/",
    callbackPath: "/cb", // The route of the callback
    silentCallbackPath: "/silentcb", // (OPTIONAL) The route of the silent callback, adds this line only if you have added 'silentRedirectURI' and as I already said, the paths MUST match.
    scope: "openid profile offline_access", // You must have at least openid and offline_access
    redisURL: "", // The URL of the Redis server. Format: [redis[s]:]//[[user][:password@]][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]] (More info avaliable at IANA).
    // It default to: 127.0.0.1:6379 with no password
  };
  const client = new SapperOIDCClient(options);
  await client.init(); // Don't forget it ๐Ÿšฆ

  polka()
    .use(await client.middleware()) // Don't forget that ๐Ÿšฆ
    .use(
      compression({ threshold: 0 }),
      sirv("static", { dev }),
      sapper.middleware({
        session: (req, res) => ({
          // And finally ๐Ÿšฆ
          user: req.user,
        }),
      })
    )
    .listen(PORT, (err) => {
      if (err) console.log("error", err);
    });
})();

Client side configuration

Here we will set up the automatic token refresh, and I'll explain a little bit about how to get the user's data. Open your root _layout.svelte (or create one)

<script context="module">
  export async function preload(page, session) {
    return session;
  }
</script>

<script>
  import { onMount } from "svelte";
  import { stores } from "@sapper/app";
  import { silentRenew, pathGuard } from "sapper-oidc/lib/client";
  import { authPath, refreshPath, protectedPaths } from "../OIDCConfig";
  const { page } = stores();

  export let user;

  $: {
    if (user) {
      console.log(user); // You can see what data you get ๐Ÿ‘ฉโ€๐Ÿ”ฌ
    }
  }
  onMount(async () => {
    /* You can see the callback function assign "e" to "user",
        "e" is the data returned when a token is refreshed, it is
        the same structure as "user" returned before */
    await silentRenew(refreshPath, e => (user = e), user);
    page.subscribe(({ path }) => {
      /* If a user navigate client side to a route that you
            configured to be available only to logged in user,
            pathGuard will ensure that. */
      try {
        pathGuard(authPath, path, protectedPaths, user);
      }catch (error){
      // See the error section for more details
      }
    });
  });
</script>

I'd recommend that you create a Svelte store to store the data you get back from "user", and then you update it with the new data that you get from the callback function in "silentRenew". Now create a svelte file with the SAME path as your callbackPath set in the options. For example, if your path is "/cb" create a svelte file at the root of the routes folder named cb.svelte. cb.svelte

<script>
  import { onMount } from "svelte";
  import { callback } from "sapper-oidc/lib/client";

  onMount( () => {
    try {
      callback(true); // If true, the user will be redirected back to where he was before.
    }catch (error){
      // See the error section for more details
    }
  });
</script>

Finaly create a svelte file with the SAME path as your authPath set in the options. For example, if your path is "/auth" create a svelte file at the root of the routes folder named auth.svelte. auth.svelte

<script>
  import { onMount } from "svelte";
  import { auth } from "sapper-oidc/lib/client";
  import { authPath } from "../OIDCConfig";

  onMount(() => {
    try {
      auth(authPath);
    } catch (error) {
      console.log(error);
    }
  });
</script>

And done ๐Ÿ˜‡

OPTIONAL

If you've added silentRedirectURI and silentCallbackPath you must add one thing. Create a svelte file that has the same path as silentCallbackPath, example, if silentCallbackPath is set to /silentcb create a svelte file at the root of your routes folder like so silentcb.svelte. silentcb.svelte

<script>
  import { onMount } from "svelte";
  import { silentCallback, silentRenew } from "sapper-oidc/lib/client";
  import { refreshPath } from "../OIDCConfig";
  import { goto } from "@sapper/app";

  onMount(() => {
    try {
      silentCallback(goto, async (user) => {
        /* Do the same thing here as you where doing with _layout.svelte
          which is saving the data you get back to the same store.*/
          await silentRenew(
          refreshPath,
          (e) => {
            // Do it also here
          },
          user
        );
      });
    } catch (error) {
      // As this is supposed to be something 'silent' the only error that could be thrown is if the library failed to fetch the server.
    }
  });
</script>

Errors

From pathGuard

NameInfo
DB_ERRAn unexpected error from redis
AUTH_URL_ERRIt were not able to generate the authorization url
NO_STATEID_FOUND_IN_REQThere wasn't any stateID sent with the request

From callback

NameInfo
NO_STATE_FOUND_IN_REQThere wasn't any state sent with the request
NO_STATE_FOUND_IN_STRNo state found in storage (meaning localStorage is empty)
DB_ERRAn unexpected error from redis
CLAIMS_ERRIt were not able to claims the tokens (ie: get the user's info)
CALLBACK_ERRIt were not able to perform the callback for Authorization Server's authorization response
NO_STATE_FOUND_IN_DBThere wasn't any state corresponding to the stateID sent with the request in the DB
NO_PARAMS_FOUNDThe request didn't had any params
0.10.4

3 years ago

0.10.5

3 years ago

0.10.3

3 years ago

0.10.2

3 years ago

0.10.1

4 years ago

0.10.0

4 years ago

0.9.6

4 years ago

0.9.4

4 years ago

0.9.5

4 years ago

0.9.3

4 years ago

0.9.1

4 years ago

0.9.0

4 years ago

0.8.7

4 years ago

0.8.4

4 years ago

0.8.6

4 years ago

0.8.3

4 years ago

0.8.2

4 years ago

0.8.1

4 years ago

0.8.0

4 years ago

0.4.0

4 years ago

0.7.0

4 years ago

0.6.0

4 years ago

0.3.2

4 years ago

0.3.1

4 years ago

0.3.0

4 years ago