# OpenID Connect

OpenID Connect (opens new window) is a security protocol for authenticating in distributed applications as a centrally managed user without exposing its credentials to either application.

OpenID Connect supports different flows. This example is about Authorization Code Flow (opens new window).

# Prerequisites

This protocol depends on a remotely set up identity provider (IdP). This tutorial is illustrating how to enable a Hitchy-based application to support authentication against such an IdP via OpenID Connect. Setting up an IdP is beyond its scope, though.

There are plenty of solutions available for running your own IdP. There are multiple options including commercial and open-source software. Keycloak (opens new window) and authentik (opens new window) are examples for the latter. See our rough step-by-step tutorial for setting up Keycloak on a server using a stack of Docker containers (opens new window). This tutorial is based on Keycloak.

# Create custom strategy

This plugin relies on passport.js (opens new window) which in turn supports so called strategies to support different kinds of authentication services and security protocols. The strategy used in this example is included with 3rd-party package openid-client (opens new window) which you need to install in context of your application:

npm i openid-client

@hitchy/plugin-auth is picking up all strategies configured in its runtime configuration on application start. It includes a factory service offering methods for generating strategies from a set of configuration options. That's what we use here.

Open file config/auth.js of your project and add the oidc property to the list of strategies as illustrated in this example:

"use strict";

module.exports = async function() {
	const { service } = this.runtime;

	return {
		auth: {
			strategies: {
				oidc: await service.AuthenticationStrategies.generateOpenIdConnect( "oidc", {
					// Provides URL of IdP for discovering settings for this
					// application's authentication realm.
					discovery_url: "https://idp.cepharum.de/auth/realms/hitchy-plugin-auth-ci-test",

					// Provides this application's name in context of discovered
					// IdP realm. Think of it as the application's username for
					// authenticating itself at the IdP.
					client_id: "ci-openid-test",

					// Provides IdP-specific secret used to authenticate this
					// application as a valid client of IdP. Think of it as the
					// application's password for authenticating itself.
					client_secret: "some-random-secret",

					// Lists valid URLs user may be redirected to after logging
					// in successfully at IdP.
					redirect_uris: ["http://localhost:3000/api/auth/login/oidc"],

					// Lists metadata/attributes of authenticated user to be
					// delivered by IdP in redirect after successful login.
					response_types: ["code"],

					// Lists valid URLs user may be redirected to after logging
					// out from IdP.
					post_logout_redirect_uris: ["http://localhost:3000/api/auth/logout"],
				} ),
			},
		}
	};
};

Multiple strategies?

It's fine to have multiple strategies of different name being set up like that. Just make sure name of property is different, URL-safe and provided as first argument to the generator method.

The strategy in this example is named oidc. Thus, the same name is given as first argument to the generator function, too. Its second argument provides configuration options for used to set up the strategy in detail.

Adapt to your IdP

The configuration in code example is for illustration, only. You need to adapt the settings to work with your IdP.

  • The discovery_url is addressing your IdP's meta settings which is a computable set of configuration parameters the client will pick up and adapt to your IdP properly. In our case, which is based on Keycloak, it is https://idp.cepharum.de/auth/realms/hitchy-plugin-auth-ci-test (opens new window). Click the link to see all meta settings related to a realm named hitchy-plugin-auth-ci-test.
  • client_id and client_secret are just like a user's name and password, but this time they are suitable for authenticating your application at your IdP.
  • A list of redirect_uris can be provided, but usually it consists of a single URL, only. In Authorization Code Flow, users are redirected to that URL right after successful authentication. Additional data on authenticated user is passed in query parameters there. The strategy is required to process that request, too. Thus, in case of hitchy/plugin-auth, this is usually referring to the same login endpoint used to trigger the authentication in the first place, e.g. http://localhost:3000/api/auth/login/oidc (opens new window). This support is based on some routing defaults.
  • Similar to that, another list of possible redirect URIs is given in post_logout_redirect_uris, this time for redirecting a user's browser after successful logout at IdP. The target is meant to finish the logout process on behalf of your application, thus you should point it to the logout URL http://localhost:3000/api/auth/logout (opens new window) accordingly.
  • A list of required meta attributes to deliver on an authenticated user can be configured, too. Requesting code response type is essential to Authorization Code Flow. That's referring to the user's name. You might query for additional data such as the user's mail address or similar.

Additional options

All but the discovery_url are metadata options supported by the underlying openid-client package, thus you should look into its documentation (opens new window) for additional options available. Some of them might be failing to work because of the way the client is integrated with Hitchy.

# Restart and test

Restart your hitchy instance. Open browser at http://localhot:3000/api/auth/current (opens new window) and get a response like this one:

{"success":true,"authenticated":false}

The request has succeeded, but no user is authenticated currently.

Trigger authentication by opening URL http://localhost:3000/api/auth/login/oidc (opens new window) next. Last segment in this URL is referring to the strategy name you've picked when integrating it with the list of configured strategies.

This will redirect the browser to your IdP for prompting to log in:

screenshot of IdP login

After logging in there, your browser is instantly redirected back to the URL http://localhost:3000/api/auth/login/oidc (opens new window) as given in configuration above. This time it is succeeding with a response:

{"success":true,"authenticated":true}

You are authenticated!

Return to the first URL requested above. It's providing a different set of information this time:

{"success":true,"authenticated":{"uuid":"d778afae-1234-4a58-a254-b56b1f36e914","name":"your-user","roles":[]}}

Try repeating this request if you like. Unless closing your browser, you stay authenticated.

If you happen to close the browser, just return to the login URL and - based on your IdP configuration - you will be re-authenticated instantly without being prompted for entering username and password again.

Next send a request to http://localhost:3000/api/auth/logout (opens new window). It gets approved:

{"success":true}

Fetch current state of authentication at https://localhost:3000/api/auth/current (opens new window) once again.

{"success":true,"authenticated":false}

Try to re-authenticate instantly at http://localhost:3000/api/auth/login/oidc (opens new window). It's going to prompt for username and password at your IdP again.

# How to welcome users?

Well, if you don't want your application's users to see the approval of logging in as raw JSON data, you simply have to replace the controller for route GET /api/auth/login/oidc and make it provide any other response, e.g. some redirection to a welcome page of your application.

Adjust your application's file config/routes.js accordingly:

exports.routes = {
    "GET /api/auth/login/oidc": ( _, res ) => res.redirect( 301, "/welcome" ),
};

This is possible in a custom policy processed after this plugin's policy (opens new window), either. This could be your config/policies.js file:

exports.policies = {
    "GET /api/auth/login/oidc"( req, res, next ) {
        if ( req.user ) {
            res.redirect( 301, "/welcome" );
        } else {
            next();
        }
    },
};