npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hipsquare/strapi-plugin-keycloak

v1.3.1

Published

Strapi plugin to support Keycloak authentication of end-users using a middleware.

Downloads

474

Readme

Strapi Keycloak Plugin

This is a Strapi plugin to support Keycloak (and more generally: OpenID Connect) authentication for end-users. It is not designed for admin users.

Quickstart

To configure Keycloak, see this guide. If you use another Identity Provider (IdP), see the documentation of your provider.

Install the plugin in your Strapi project:

yarn add @hipsquare/strapi-plugin-keycloak

Enable the plugin in config/plugins.js (create the file if it does not exist so far):

module.exports = {
  keycloak: {
    enabled: true,
  },
};

Create config/keycloak.js and configure Keycloak (or your other OIDC IdP) accordingly:

module.exports = {
  // client ID configured in Keycloak
  clientId: "strapi",

  // if the client access type is set to "confidential" in keycloak, add the client secret here. otherwise, don't set this value.
  clientSecret: "abcdefg",

  // auth endpoint, right value comes from Keycloak/your IdP
  authEndpoint:
    "http://localhost:8080/realms/strapi/protocol/openid-connect/auth",

  // token endpoint, right value comes from Keycloak/your IdP
  tokenEndpoint:
    "http://localhost:8080/realms/strapi/protocol/openid-connect/token",

  // user info endpoint, right value comes from Keycloak/your IdP.
  userinfoEndpoint:
    "http://localhost:8080/realms/strapi/protocol/openid-connect/userinfo",

  // logout endpoint, right value comes from Keycloak or your IdP.
  // if not set, the logout is performed locally only, so the session
  // will be ended by Strapi, but the user might still be logged in
  // with the IdP.
  //
  // this is not recommended behavior, but can be required for IdPs that
  // do not support ending sessions.
  logoutEndpoint:
    "http://localhost:8080/realms/strapi/protocol/openid-connect/logout",

  // redirect URI after Keycloak/IdP login, should be the full URL of the Strapi instance and always point to the `keycloak/callback` endpoint
  redirectUri: "http://localhost:1337/keycloak/callback",

  // default URL to redirect to when login process is finished. In normal cases, this would redirect you back to the application using Strapi data
  redirectToUrlAfterLogin: "http://localhost:1337/api/todos",

  // setting these allows the client to pass a `redirectTo` query parameter to the `login` endpoint. If the `redirectTo`
  // parameter is permitted by this array, after login, Strapi will redirect the user to it. Leave empty to disable
  // the functionality.
  permittedOverwriteRedirectUrls: [
    "http://localhost:1337",
    "http://localhost:1338",
  ],

  // URL to redirect to after logout
  redirectToUrlAfterLogout: "http://localhost:1337/",

  // enable debug messages in server log
  debug: true,
};

Using a Well Known URL to set the config

Most IdPs provide a Well Known file to retrieve configuration data, e.g. endpoints. You can supply the URL to it and save yourself some configuration work:

module.exports = {
  wellknownUrl: "https://example.com/oidc/.well-known/openid-configuration",

  // ...
};

The plugin then reads the following configuration items, which you can omit from the config:

  • Token endpoint,
  • auth endpoint,
  • userinfo endpoint,
  • logout endpoint,
  • JWKS URI.

Protecting Strapi routes

To protect a route, apply the middleware to that route in api/[content-type]/routes/[content-type].js (in our example todo).

const { createCoreRouter } = require("@strapi/strapi").factories;

module.exports = createCoreRouter("api::todo.todo", {
  config: {
    find: {
      middlewares: ["plugin::keycloak.keycloak"],
    },
  },
});

Restart Strapi.

Open http://localhost:1337/keycloak/login to start the login process.

Now open the find endpoint of your content type, in this example http://localhost:1337/api/todos.

Using Strapi API Tokens

Strapi introduced API Tokens in version 4, which are meant to allow bypassing other means of authorization when set. The middleware takes API tokens into account. If a valid API token is set, there will be no check for a valid Keycloak login.

Check if user is logged in

To check if the user is currently logged in with a valid access token, you can call the /keycloak/isLoggedIn endpoint. It will return true or false.

Refresh flow

The plugin supports refreshing the access token using refresh tokens:

Refresh using the refresh endpoint

You can manually call the /keycloak/refresh endpoint with a GET request. The plugin will use the refresh token stored in the user's session.

fetch("http://localhost:1337/keycloak/refresh", { credentials: "include" });

The plugin will then update the tokens in the user's session.

Auto refresh

You can enable auto refresh in the config:

module.exports = {
  // set the threshold for auto refresh: if a request comes in and the access token is about to
  // expire in less than this timespan (noted in milliseconds), it will be refreshed.
  autoRefreshMsBeforeExpiry: 30 * 60 * 1_000,
};

The refreshed access, ID and refresh tokens are automatically stored in the session.

Get user profile

Using the /keycloak/profile route, you can fetch the user's keycloak profile:

fetch("http://localhost:1337/keycloak/profile", { credentials: include })
  .then((res) => res.json())
  .then((profile) => console.log(profile));

// {"sub":"deab236b-db26-4b25-afa9-ce5132503afe","email_verified":true,"name":"John Doe","preferred_username":"john.doe","given_name":"John","family_name":"Doe"}

Get login status and user profile from access and ID tokens and avoid Keycloak roundtrip

By default, the plugin will check for the current login status by calling Keycloak's userinfo endpoint. For Strapi instances with many requests, this can become a performance bottleneck.

You can change this behavior by providing your IdP's public key or a JWKS URI, which allows the plugin to verify and decode the access and ID tokens provided by your IdP. Like that, the plugin will not contact Keycloak anymore to verify the user's login status, but rely on the verification status of the access and ID tokens.

Verifying tokens using a public key

To enable token verification using a public key, define the jwtPublicKey configuration property in config/keycloak.js:

module.exports = {
  jwtPublicKey: "Iadoghdsgh...",
};

You can find the public key in Keycloak under "Realm Settings" in the "Keys" tab. Look for the "RSA"-type public key with a "SIG" use, and click the "Public key" button to retrieve the public key.

If your IdP uses a non-default algorithm to sign tokens, you can define it with jwtAlgorithm:

module.exports = {
  jwtAlgorithm: "RS256",
};

If you don't set jwtAlgorithm, it defaults to RS256.

Verifying tokens using a JWKS URI

If your IdP provides a JWKS URI for token verification, you can set it in the config:

module.exports = {
  jwksUri: "https://my-jwks-uri.com/keys",
};

The plugin will fetch the keys from the given URL and use them to verify the tokens.

Access the user profile in Strapi code

When a user is logged in, the middleware will populate ctx.state.keycloak.profile with the current user's profile.

console.log("The current user is ", ctx.state?.keycloak?.profile);

The user profile is made up of:

  1. The payload of the access token,
  2. the payload of the ID token,
  3. the response of the "user info" endpoint.

Token & Session Handling & Access

The plugin takes care of retrieving tokens (access token, ID token, refresh token) from the IdP. These tokens then are stored in the user's session using koa-session, which Strapi uses as its underlying session management library.

You can access the tokens in the session:

 {
  accessToken,
  idToken,
  refreshToken,
} = ctx.session.keycloak;

Logout

To initiate a logout, redirect the user to /keycloak/logout.

You can append a redirectTo query parameter to forward the user to a custom URL:

http://localhost:1337/keycloak/logout?redirectTo=http://myfrontend/login

If none is specified, the user will be redirected to redirectToUrlAfterLogout defined in the configuration.

Lifecycle Hooks

You can optionally provide lifecycle hooks via the configuration:

onLoginSuccessful, onLoginFailed

module.exports = {
  onLoginSuccessful: (ctx) => console.log("Login was successful"),
  onLoginFailed: (ctx) => console.log("Login failed"),
};

These functions receive the full Koa context and can interact with it.

onRetrieveProfile

Additionally, you can use onRetrieveProfile to enrich the user profile returned by the /profile endpoint with custom information:

module.exports = {
  onRetrieveProfile: (ctx) => ({ customGreeting: "hello" }),
};

The returned object will be merged with the user profile retrieved from the IdP.

afterRetrieveProfile

You can use afterRetrieveProfile which will be called after the profile has been retrieved from the IdP and after onRetrieveProfile has been called. It gets both the context and the fetched user profile handed over as arguments.

module.exports = {
  afterRetrieveProfile: (ctx, userProfile) => {
    userProfile.randomField = "randomValue";
  },
};

canLogin

The canLogin hook is called after a successful OIDC authentication and when the user profile has been fetched. The function is provided with the user profile as an argument. If it returns true, a session is opened for the user. If it returns false, the login process is aborted.

This hook is useful to allow or deny users access to your entire application based on profile information (e.g. group membership).

If the hook isn't set, all users with a valid OIDC login can login and create sessions.

module.exports = {
  canLogin: async (userProfile) => {
    return userProfile.groups?.includes("members");
  },
};

Additional Scope Entries

If you require additional scope entries in token retrieval, you can define them using the additionalScopeEntries configuration property. By default, the scope consists of openid profile offline_access. The additional entries will be appended to this list:

module.exports = {
  additionalScopeEntries: ["email"],
};

Q&A

Does the plugin work with other identity providers than Keycloak?

The plugin implements the default OpenID Connect Authorization Code Flow. That's why it works with other Identity Providers than Keycloak, too. We have tested it with Auth0 and Azure Active Directory the login process works seamlessly.

The package name is somewhat misleading therefore -- we might change it to reflect the broader IdP support in the future.

I can login successfully, but isLoggedIn returns false/no session is created

One common reason for this is that the access and ID tokens supplied by your identity provider are very long. Strapi uses koa-session for session management, and koa-session stores all session information in a client-side browser cookie. Browser cookies have length limits, and if your tokens exceed that length limit, Strapi will fail to create a session.

As a solution, we recommend to use an external session store. See the koa-session documentation for details.

A primitive implementation in the Strapi session middleware config that we do not recommend for production could looks like this:

const sessionStore = new Map<string, { session: unknown; expires: number }>();

export default [
  {
    name: "strapi::session",
    config: {
      store: {
        async get(key: string) {
          const sessionInfo = sessionStore.get(key);

          if (!sessionInfo) {
            return;
          }

          if (sessionInfo.expires < +new Date()) {
            return;
          }

          return sessionInfo.session;
        },
        async set(key: string, session: unknown, maxAge: number) {
          sessionStore.set(key, {
            session,
            expires: +new Date() + maxAge * 1000,
          });
        },
        async destroy(key) {
          sessionStore.delete(key);
        },
      },
    },
  },
];

Related Projects

This plugin can be easily used to implement login flows in React using our React Auth Context library.