/jackson-remix-auth

Remix demo app showing Single sign-on (SSO) with Jackson

Primary LanguageTypeScript

Remix Demo App with Single Sign-On (SSO)

This demo shows how to use BoxyHQSSOStrategy to integrate Single Sign-On (SSO) into any remix application.

Two different SSO Service Provider setups are shown in this demo

  1. With a hosted SSO Service Provider.
  2. With the SSO Service Provider functionality embedded within the remix app using resource routes.

Getting started

Install dependencies

npm i

Set up the env

Create a .env file from .env.example

cp .env.example .env

NOTE: You may have to tweak .env based on the type of SSO Provider setup i.e. Hosted vs Embedded. Refer the SSO Provider Setup section below.

Start the app

npm run dev

SSO Provider Setup

We have two options when it comes to enabling SSO for a remix app. You can host the SSO feature (i.e. Jackson) as a standalone service or you could embed the SSO functionality inside the remix app with the help of our npm package.

Hosted SSO Service Provider

In this setup, you can use a hosted service endpoint for Jackson. See the deploy guide on how to run Jackson as a service. Once you have the service set up, simply point the env BOXYHQSSO_ISSUER to the service URL.

For a quick test, setup .env with BOXYHQSSO_ISSUER as below:

BOXYHQSSO_ISSUER=https://sso.eu.boxyhq.com
CLIENT_SECRET_VERIFIER=dummy

This uses a our SaaS instance of jackson as the SSO Service Provider. An SSO connection is preconfigured pointing to Mock SAML IdP.

The tenant and product are locked into boxyhq.com and 1eef7782-41d4-4a0a-b450-0857413b4f63 for the Mock SAML IdP Connection.

Embedded SSO Service Provider

This uses the jackson npm package to embed the Single Sign-On feature without depending on an external service. See JacksonProvider in auth.jackson.server.ts where the SSO controllers { connectionAPIController, oauthController } are exposed. The resource routes for SSO flow are added in app/routes/api.

For this setup, we will need some additional env for the following :

  db: {
    engine: process.env.DB_ENGINE,
    url: process.env.DB_URL,
    type: process.env.DB_TYPE,
    encryptionKey: process.env.DB_ENCRYPTION_KEY,
  } as DatabaseOption,
  clientSecretVerifier: process.env.CLIENT_SECRET_VERIFIER,
  openid: {
    jwsAlg: "RS256",
    jwtSigningKeys: {
      private: process.env.OPENID_RSA_PRIVATE_KEY!,
      public: process.env.OPENID_RSA_PUBLIC_KEY!,
    },
  },

Please refer to https://boxyhq.com/docs/jackson/deploy/env-variables for more info and set the variables in .env.

Once the app is running configure an SSO Connection as shown below. Here we are going with a SAML SSO Connection.

Below adds a SAML SSO connection for https://mocksaml.com
curl --location --request POST 'http://localhost:3366/api/v1/connections' \
--header 'Authorization: Api-Key ' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'encodedRawMetadata=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxFbnRpdHlEZXNjcmlwdG9yIHhtbG5zOm1kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bWV0YWRhdGEiIGVudGl0eUlEPSJodHRwczovL3NhbWwuZXhhbXBsZS5jb20vZW50aXR5aWQiIHZhbGlkVW50aWw9IjIwMjYtMDYtMjJUMTg6Mzk6NTMuMDAwWiI+CiAgPElEUFNTT0Rlc2NyaXB0b3IgV2FudEF1dGhuUmVxdWVzdHNTaWduZWQ9ImZhbHNlIiBwcm90b2NvbFN1cHBvcnRFbnVtZXJhdGlvbj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIj4KICAgIDxLZXlEZXNjcmlwdG9yIHVzZT0ic2lnbmluZyI+CiAgICAgIDxLZXlJbmZvIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgICAgICA8WDUwOURhdGE+CiAgICAgICAgICA8WDUwOUNlcnRpZmljYXRlPk1JSUM0akNDQWNvQ0NRQzMzd255YlQ1UVpEQU5CZ2txaGtpRzl3MEJBUXNGQURBeU1Rc3dDUVlEVlFRR0V3SlYKU3pFUE1BMEdBMVVFQ2d3R1FtOTRlVWhSTVJJd0VBWURWUVFEREFsTmIyTnJJRk5CVFV3d0lCY05Nakl3TWpJNApNakUwTmpNNFdoZ1BNekF5TVRBM01ERXlNVFEyTXpoYU1ESXhDekFKQmdOVkJBWVRBbFZMTVE4d0RRWURWUVFLCkRBWkNiM2g1U0ZFeEVqQVFCZ05WQkFNTUNVMXZZMnNnVTBGTlREQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dFUEFEQ0NBUW9DZ2dFQkFMR2ZZZXR0TXNjdDFUNnRWVXdUdWROSkg1UG5iOUdHbmtYaTlady9lNng0NUREMApSdVJPTmJGbEoyVDRSakFFL3VHK0FqWHhYUThvMlNaZmI5K0dnbUNIdVRKRk5nSG9aMW5GVlhDbWIvSGc4SHBkCjR2T0FHWG5kaXhhUmVPaXEzRUg1WHZwTWpNa0ozKzgrOVZZTXpNWk9qa2dRdEFxTzM2ZUFGRmZOS1g3ZFRqM1YKcHdMa3Z6Ni9LRkNxOE9Bd1krQVVpNGVabTVKNTdEMzFHempId2ZqSDlXVGVYME15bmRtbk5CMXFWNzVxUVIzYgoyL1c1c0dIUnYrOUFhcmdnSmtGK3B0VWtYb0x0VkE1MXdjZlltNmhJTHB0cGRlNUZRQzhSV1kxWXJzd0JXQUVaCk5meXJSNEplU3dlRWxOSGc0TlZPczRUd0dqT1B3V0dxelRmZ1RsRUNBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0YKQUFPQ0FRRUFBWVJsWWZsU1hBV29acEZmd05pQ1FWRTVkOXpaMERQek5kV2hBeWJYY1R5TWYwejVtRGY2RldCVwo1R3lvaTl1M0VNRURuekxjSk5rd0pBQWMzOUFwYTRJMi90bWwrSnkyOWRrOGJUeVg2bTkzbmdtQ2dkTGg1WmE0CmtodVUzQU0zTDYzZzdWZXhDdU83a3dramgvK0xxZGNJWHNWR082WERmdTJRT3MxWHBlOXpJekxwd20vUk5ZZVgKVWpiU2o1Y2UvamVrcEF3N3F5VlZMNHhPeWg4QXRVVzFlazN3SXcxTUp2RWdFUHQwZDE2b3NoV0pwb1MxT1Q4TApyLzIyU3ZZRW8zRW1TR2RUVkdnazN4M3MrQTBxV0FxVGN5anI3UTRzL0dLWVJGZm9tR3d6MFRaNEl3MVpOOTlNCm0wZW8yVVNsU1JUVmw3UUhSVHVpdVNUaEhwTEtRUT09CjwvWDUwOUNlcnRpZmljYXRlPgogICAgICAgIDwvWDUwOURhdGE+CiAgICAgIDwvS2V5SW5mbz4KICAgIDwvS2V5RGVzY3JpcHRvcj4KICAgIDxOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzPC9OYW1lSURGb3JtYXQ+CiAgICA8U2luZ2xlU2lnbk9uU2VydmljZSBCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1SZWRpcmVjdCIgTG9jYXRpb249Imh0dHBzOi8vbW9ja3NhbWwuY29tL2FwaS9zYW1sL3NzbyIvPgogICAgPFNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbW9ja3NhbWwuY29tL2FwaS9zYW1sL3NzbyIvPgogIDwvSURQU1NPRGVzY3JpcHRvcj4KPC9FbnRpdHlEZXNjcmlwdG9yPg==' \
--data-urlencode 'defaultRedirectUrl=http://localhost:3366' \
--data-urlencode 'redirectUrl=["http://localhost:3366/*"]' \
--data-urlencode 'tenant=boxyhq.com' \
--data-urlencode 'product=saml-demo.boxyhq.com' \
--data-urlencode 'name=demo-config' \
--data-urlencode 'description=Demo SAML config'

Routes

  1. / - Renders protected content if user is logged in.

  2. /login - Renders a form (action - /auth/sso) with an input box which can take in an email that can be used to switch tenant dynamically.

  3. /logout

  4. /auth/sso (hosted),/auth/sso/embed(embedded) - Action handlers for login initiating the OAuth 2.0 flow to the IdP.

  5. /auth/sso/callback (hosted),/auth/sso/embed/callback (embedded) - SSO Service Provider (Jackson) after parsing the SAML/OIDC response from IdP redirects back here with the authorization code. The SSO strategy uses the code to obtain the token and further the user profile and finally redirects back to successRedirect path.

Strategy Usage

auth.server.ts

// BASE_URL should be the hosting url of the app
// clientID and Secret set to 'dummy' here; they will be populated dynamically from the client side. The clientID is passed as a URL encoded string containing the tenant and product
export const auth = new Authenticator<BoxyHQSSOProfile>(sessionStorage);

// This strategy points to a hosted jackson instance
auth.use(
  new BoxyHQSSOStrategy(
    {
      issuer: process.env.BOXYHQSSO_ISSUER // "https://sso.eu.boxyhq.com",
      clientID: "dummy",
      clientSecret: "dummy",
      callbackURL: new URL("/auth/saml/callback", BASE_URL).toString(),
    },
    async ({ profile }) => {
      return profile;
    }
  )
);
// This strategy points to the same remix app host (resource routes are setup to handle SAML flow)
auth.use(
  new BoxyHQSSOStrategy(
    {
      issuer: process.env.BOXYHQSSO_ISSUER, //same as the APP URL
      clientID: "dummy",
      clientSecret: process.env.CLIENT_SECRET_VERIFIER, // this env will be used to perform authentication at token endpoint
      callbackURL: new URL("/auth/saml/embed/callback", BASE_URL).toString(),
    },
    async ({ profile }) => {
      return profile;
    }
  ),
  "boxyhq-saml-embed"
);

FAQ

How is the tenant/product passed from the client side?

In the login form action handlers app/routes/auth.sso.tsx, app/routes/auth.sso.embed.tsx, clientID is passed in the context param to authorize endpoint.

How can I test with a different "tenant/product"?

The demo uses email to detect the tenant. You can use an alternate mechanism such as directly asking for tenant information from the user. The same applies to "product". You might have to whitelist the product in validateProduct.