Share on:

OpenAM and Zero Trust: Confirming Critical Operations

Original article: https://github.com/OpenIdentityPlatform/OpenAM/wiki/OpenAM-and-Zero-Trust:-Confirming-Critical-Operations

Introduction

One of the principles of Zero Trust states: Never trust, always verify. In this article, we will explore how to implement this principle in an authentication system using open-source products OpenAM and OpenIG.

A practical example of this principle can be seen in banking applications. When confirming a payment, banks almost always want to ensure that it is you conducting the transaction, not a malicious actor. To verify this, they send a one-time code to a trusted device via push notification or SMS.

Alternatively, the user may be asked to confirm their biometric data, such as a fingerprint, use a hardware token, or rely on a specialized application like Microsoft Authenticator or Google Authenticator.

Solution Design

The solution consists of three components:

As a second authentication factor, one-time passwords generated using the TOTP algorithm will be used, along with mobile applications like Microsoft Authenticator or Google Authenticator.

Preparation

The complete solution code is available at this link: https://github.com/OpenIdentityPlatform/openam-openig-otp-example.

Preparing the Hosts File

Let’s assume the hostname for the authentication service will be openam.example.org and for the gateway openig.example.org. Before running the setup, add these hostnames and IP addresses to the hosts file, for example:
127.0.0.1 openam.example.org openig.example.org

Authenticator Application

Install the mobile application Microsoft Authenticator or Google Authenticator on your device.

Docker Compose

For simplicity, all services will be launched using docker compose.

Create an empty file named docker-compose.yml and add the services object:

services:

Demo Application

For demonstration purposes, we will use a simple Node.js application with two URLs:

The application code can be found at this link.

const express = require("express");
const app = express();
const port = 3000;

app.set("view engine", "ejs");

app.use((req, res, next) => {
    console.log(req.headers)
    const token = req.headers.authorization;
    if (!token) {
        return res.status(401).send("Unauthorized");
    }
    next()
})

app.get("/", (req, res) => {
    const token = req.headers.authorization;
    const jwtPayload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
    const user = { name: jwtPayload.sub };
    res.render("profile", { user });
});

app.get("/sensitive", (req, res) => {
    const sensitiveData = { bankAccount: "1234-5678-9012-3456", secretKey: "MY_SUPER_SECRET_KEY" };
    res.render("sensitive", { sensitiveData });
});

app.listen(port, () => console.log(`Server running at http://localhost:${port}`));

Add a demo application to the docker-compose.yml file in the services object

services:
  demo-app:
    build: ./demo-app
    container_name: demo-app

Run the application with the command docker compose up -d --build demo-app

OpenAM Authentication Service Configuration

Add the OpenAM seris to the docker-compose.yml file in the services object:

services:
...
  openam:
    image: openidentityplatform/openam:latest
    container_name: openam
    hostname: openam.example.org
    ports:
      - "8080:8080"

Start the OpenAM container with the command docker compose up openam. Wait for the container to start and perform the initial installation with the command:

docker exec -w '/usr/openam/ssoconfiguratortools' openam bash -c \
'echo "ACCEPT_LICENSES=true
SERVER_URL=http://openam.example.org:8080
DEPLOYMENT_URI=/$OPENAM_PATH
BASE_DIR=$OPENAM_DATA_DIR
locale=en_US
PLATFORM_LOCALE=en_US
AM_ENC_KEY=
ADMIN_PWD=passw0rd
AMLDAPUSERPASSWD=p@passw0rd
COOKIE_DOMAIN=example.org
ACCEPT_LICENSES=true
DATA_STORE=embedded
DIRECTORY_SSL=SIMPLE
DIRECTORY_SERVER=openam.example.org
DIRECTORY_PORT=50389
DIRECTORY_ADMIN_PORT=4444
DIRECTORY_JMX_PORT=1689
ROOT_SUFFIX=dc=openam,dc=example,dc=org
DS_DIRMGRDN=cn=Directory Manager
DS_DIRMGRPASSWD=passw0rd" > conf.file && java -jar openam-configurator-tool*.jar --file conf.file'

Wait until the installation is complete.

Configuring MFA in OpenAM

Open the OpenAM administrator console at http://openam.example.org:8080/openam.

Enter the administrator login and password. In this case, it is amadmin and passw0rd respectively.

In the console, select Top Level Realm. In the left menu, select AuthenticationModules and create a new module totp with type Authenticator (OATH).

New TOTP Module.png

In the settings, select OATH Algorithm to Use: TOTP, also specify Name of the Issuer, e.g. OpenAM. The rest of the settings can be left unchanged. Save the totp module settings.

TOTP module settings

Next, let’s configure the authentication chain

In the administrator console, open the Top Level Realm, then go to AuthenticationChains in the left menu and create a new authentication chain totp.

New TOTP authentication chain

Add the created totp module to the chain and save the changes.

TOTP authentication chain settings

Configuring OpenAM Authorization Policy

Now let’s move on to configuring the authorization policy in OpenAM for the /sensitive endpoint of the demo application. The policy will be configured so that the user is required to authenticate with a one-time code in the totp authentication chain, but the authentication will only be valid for 20 seconds.

Open the OpenAM administrator console. Open Top Level Realm. From the menu on the left, select AuthorizationPolicy Sets. Select the Default Policy Set. Create a new demo-sensitive policy.

Select URL as the resource type and specify the resource as shown in the example in the figure below. Click Add and then Create.

OpenAM new policy

For the created policy, on the Resources tab, allow GET and POST requests.

Policy Actions

On the Subjects tab, add the Authenticated Users type.

Policy Subjects

On the Environments tab, add the Authentication by Module Chain condition and add the totp chain.

Policy Environments

Save the changes. This policy will authorize requests authenticated by the totp chain.

Now configure the policy so that access is only valid for 20 seconds. OpenAM doesn’t have such a policy out of the box, so we’ll configure a policy script. But first let’s prepare OpenAM to work with the time from the script. In the top menu, go to Configure → Global Services. In the list that opens, select Scripting. Click the Secondary Configuration tab.

Configure Scripting

Open the POLICY_CONDITION configuration. On the Secondary Configurations tab, select EngineConfiguration.

Scripting Policy Condition

In the Java class whitelist, add java.time.* to allow Groovy scripts to work with time and date.

Save the changes. From the console’s top menu, select Realms → Top Level Realm and select Scripts from the menu on the left. Create a new script Auth Time Policy Condition.

New Script

Script type - POLICY_CONDITION. Language - Groovy.

Auth Time Policy Condition

import java.time.Instant;
import java.time.temporal.ChronoUnit;

logger.warning("Session: " + session) 
def authInstant = session.getProperty("authInstant")

logger.warning("Auth time expired at1: " + authInstant)

def instant = Instant.parse(authInstant)
def expired = instant.plus(20, ChronoUnit.SECONDS)
if (Instant.now().compareTo(expired) > 0) {
  logger.warning("Auth time expired at: " + expired)   
  authorized = false
} else {
  authorized = true                
}

Save the policy changes.

Now let’s configure the use of the script in the authorization policy. In the left menu, go to AuthorizationPolicy SetsDefault Policy Setdemo-sensitive.

On the Environments tab, add a condition with the Script type and the value Auth Time Policy Condition.

Policy Script

Save the changes.

Now let’s configure the use of MFA for the demo user. This account was created when OpenAM was installed.

OpenAM User Setup

Log out of the admin console, or open a browser in Incognito mode and go to http://openam.example.org:8080/openam/XUI/#login

In the login and password fields, enter demo and changeit respectively. This will open the user profile.

Now, start the authentication process on the totp chain. For this, open the link http://openam.example.org:8080/openam/XUI/#login/&service=totp&ForceAuth=true

A window will open asking you to register a new device

OpenAM Register Device

Click Register Device to register the device.

A page with a QR code will be displayed.

OpenAM QR Code

Open the authenticator app on your mobile device and scan the issued QR code in it. Click the Login Using Verification Code button.

Enter the code from the mobile app and click Submit.

OpenAM verification code

MFA for user demo is configured

Configuring Authentication Token Conversion to JWT

After successful authentication, OpenAM creates a session and writes the session ID, which is a random character set, in a cookie to the browser. We will configure OpenAM so that third-party applications, such as OpenIG, can exchange the authentication token to the JWT to make it easier for third-party applications to work with.

Open the OpenAM admin console as described earlier. Select Top Level Realm. From the menu on the left, select STS. In the list that opens, create a new Rest STS instance. Fill in the settings

Setting Value
Supported Token Transforms OPENAM->OPENIDCONNECT;don’t invalidate interim OpenAM session
Deployment Url Element jwt
The id of the OpenID Connect Token Provider https://openam.example.org/openam
Client secret changeme
Confirm client secret changeme
The audience for issued tokens https://openam.example.org/openam

Save the Rest STS instance conversion settings.

You can read more about installing and configuring OpenAM in the documentation: https://doc.openidentityplatform.org/openam/

Configure the OpenIG authorization gateway

Add the OpenIG service to the docker-compose.yml file

services:
...
  openig:
    image: openidentityplatform/openig:latest
    container_name: openig
    hostname: openig.example.org
    volumes:
      - ./openig:/usr/local/openig-config:ro
    environment:
      CATALINA_OPTS: -Dopenig.base=/usr/local/openig-config -Ddemo.app=http://demo-app:3000 -Dopenam=http://openam.example.org:8080/openam
    ports:
      - "8081:8080"

Note the arguments in the CATALINA_OPTS environment variable:

General Settings

Create a openig-config folder and in it another config folder. In the config folder, create an admin.json file with the following contents:

{
    "prefix": "openig",
    "mode": "PRODUCTION"
}

In the same folder, create a config.json file.

{
  "heap": [
    {
      "name": "EndpointHandler",
      "type": "DispatchHandler",
      "config": {
        "bindings": [
          {
            "handler": "ClientHandler",
            "capture": "all",
            "baseURI": "${system['demo.app']}"
          }
        ]
      }
    }
  ],
  "handler": {
    "type": "Chain",
    "config": {
      "filters": [
        {
          "name": "STSFilter",
          "type": "ConditionalFilter",
          "config": {
            "condition": "${empty contexts.sts.issuedToken and not empty request.cookies['iPlanetDirectoryPro'][0].value}",
            "delegate": {
              "type": "TokenTransformationFilter",
              "config": {
                "openamUri": "${system['openam']}",
                "realm": "/",
                "instance": "jwt",
                "from": "OPENAM",
                "to": "OPENIDCONNECT",
                "idToken": "${request.cookies['iPlanetDirectoryPro'][0].value}"
              }
            }
          }
        },
        {
          "name": "AuthorizationHeaderFilter",
          "type": "ConditionalFilter",
          "config": {
            "condition": "${not empty contexts.sts.issuedToken}",
            "delegate": {
              "type": "HeaderFilter",
              "config": {
                "messageType": "REQUEST",
                "remove": [
                  "Authorization",
                  "JWT"
                ],
                "add": {
                  "Authorization": [
                    "Bearer ${contexts.sts.issuedToken}"
                  ]
                }
              }
            }
          }
        },
        {
          "name": "AuthenticationRedirectionFilter",
          "type": "ConditionEnforcementFilter",
          "config": {
            "condition": "${not empty contexts.sts.issuedToken}",
            "failureHandler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 302,
                "reason": "Found",
                "headers": {
                  "Content-Type": [
                    "application/json"
                  ],
                  "Location": [
                    "${system['openam']}/XUI/#login&goto=${urlEncode(contexts.router.originalUri)}"
                  ]
                },
                "entity": "{ \"Redirect\": \"${system['openam']}/XUI/#login&goto=${urlEncode(contexts.router.originalUri)}\"}"
              }
            }
          }
        }
      ],
      "handler": {
        "type": "Router",
        "name": "_router",
        "capture": "all"
      }
    }
  }
}

The config.json file defines a filter chain for each request to the demo application:

An EndpointHandler handler is defined in the heap object that proxies requests in OpenIG to the demo application.

Configure Routes to the Demo Application

In the config folder, create a routes folder and add a 10-home.json route

{
  "name": "${matches(request.uri.path, '^/$')}",
  "condition": "${matches(request.uri.path, '^/$')}",
  "monitor": true,
  "timer": true,
  "handler": {
    "type": "Chain",
    "config": {
      "filters": [],
      "handler": "EndpointHandler"
    }
  },
  "heap": [
    
  ]
} 

The route simply proxies requests to the demo application using the EndpointHandler defined in the config.json configuration file.

We will add the route to a URL with sensitive information of the demo application. We will then configure a filter for the route so that it uses the authorization policy from OpenAM.

Add the 20-sensitive.json route to the routes folder

{
  "name": "${matches(request.uri.path, '^/sensitive')}",
  "condition": "${matches(request.uri.path, '^/sensitive')}",
  "monitor": true,
  "timer": true,
  "handler": {
    "type": "Chain",
    "config": {
      "filters": [
        {
          "name": "MFAPEPFilter",
          "type": "PolicyEnforcementFilter",
          "config": {
            "openamUrl": "${system['openam']}",
            "pepUsername": "amadmin",
            "pepPassword": "ampassword",
            "ssoTokenSubject": "${request.cookies['iPlanetDirectoryPro'][0].value}",
            "failureHandler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 403,
                "headers": {
                  "Content-Type": [
                    "application/json"
                  ]
                },
                "entity": "{ \"attributes\": \"${system['openam']}/XUI/#login&service=totp&ForceAuth=true&goto=${urlEncode(contexts.router.originalUri)}\"}"
              }
            }
          },
          "handler": "ClientHandler"
        }
      ],
      "handler": "EndpointHandler"
    }
  },
  "heap": []
}

The route uses MFAPEPFilter to get the result of the authorization policy from OpenAM. And, if the policy check fails, redirects to authentication with a one-time code.

You can read more about installing and configuring OpenIG in the documentation: https://doc.openidentityplatform.org/openig/

Test the Solution

Start OpenIG with the docker compose ui openig command.

Log out of OpenAM if you are still authenticated.

Open the URL of the OpenIG protected demo application: http://openig.example.org:8081/. You will be redirected to authenticate to OpenAM. Enter the test user login and password: demo and changeit.

After authentication, you will be redirected to the main screen of the demo application

Demo Profile Page

Click the Sensitive data link. You will be redirected to additional authentication with a one-time code in OpenAM.

TOTP Verification

Enter the code from the authenticator application and click Submit. If successful, you will be redirected back to the page with sensitive data

Demo Sensitive Page

Wait 30 seconds and reload the page. You will be redirected to authentication with a one-time code again.