Skip to content

Okta integration

Credit to @fas3r for the original walkthrough.

TIP

This chapter walks through a sample application that implements SP-initiated SSO with Okta.

Prerequisites

  • samlify
  • Express (or another Node.js web framework)
  • body-parser

Step-by-step tutorial

1. Create a new SAML 2.0 web app in Okta

2. Configure the SAML integration

General settings:

Configure SAML:

WARNING

Never upload your private key online.

  • Single Sign-On URL — the endpoint that receives the POSTed SAML response.
  • Audience URI — the URL that serves the SP metadata. Not required if you prefer not to publish metadata; see Metadata distribution.
  • Assertion Encryption — set to Encrypted to encrypt the SAML assertion.
  • Encryption Certificate — upload the *.cer used by samlify to encrypt the assertion.

  • Add the attribute statements / groups to return in the assertion.

Feedback:

  • Choose the option that matches your deployment.

3. Review the Sign On tab

  • Red arrow: the SAML 2.0 signing certificate.
  • Green arrow: download the IdP XML metadata for the application.
  • Blue arrow: direct link to the application's metadata URL.
  • The General tab should look similar to:

4. Example code

js
const express = require('express');
const fs = require('fs');
const saml = require('samlify');
const axios = require('axios');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(serveStatic(path.resolve(__dirname, 'public')));

// URL of the Okta metadata document.
const uri_okta_metadata = 'https://dev-xxxxxxx.oktapreview.com/app/APP_ID/sso/saml/metadata';

axios.get(uri_okta_metadata).then(response => {

  const idp = saml.IdentityProvider({
    metadata: response.data,
    isAssertionEncrypted: true,
    messageSigningOrder: 'encrypt-then-sign',
    wantLogoutRequestSigned: true
  });

  const sp = saml.ServiceProvider({
    entityID: 'http://localhost:8080/sp/metadata?encrypted=true',
    authnRequestsSigned: false,
    wantAssertionsSigned: true,
    wantMessageSigned: true,
    wantLogoutResponseSigned: true,
    wantLogoutRequestSigned: true,
    // Private key (PEM) used to sign the assertion.
    privateKey: fs.readFileSync(__dirname + '/ssl/sign/privkey.pem'),
    // Passphrase for the signing private key.
    privateKeyPass: 'VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px',
    // Private key (PEM) used to decrypt the assertion.
    encPrivateKey: fs.readFileSync(__dirname + '/ssl/encrypt/privkey.pem'),
    isAssertionEncrypted: true,
    assertionConsumerService: [{
      Binding: saml.Constants.namespace.binding.post,
      Location: 'http://localhost:8080/sp/acs?encrypted=true',
    }]
  });

  app.post('/sp/acs', async (req, res) => {
    try {
      const { extract } = await sp.parseLoginResponse(idp, 'post', req);
      console.log(extract.attributes);
      /**
       * Application logic goes here.
       * `extract.attributes` typically contains firstName, lastName, email, uid, and groups.
       */
    } catch (e) {
      console.error('[FATAL] failed to parse the login response from Okta', e);
      return res.redirect('/');
    }
  });

  app.get('/login', async (req, res) => {
    const { id, context } = await sp.createLoginRequest(idp, 'redirect');
    console.log(context);
    return res.redirect(context);
  });

  app.get('/sp/metadata', (req, res) => {
    res.header('Content-Type', 'text/xml').send(idp.getMetadata());
  });

});