PolicyLogic Examples

All examples should be used as references. Depending on the NAS, you may need to add or remove AVPs in your implementation. 

Disconnect-Message OR Change-Of-Authorization

function getExistingSession() {
  // Query for existing active entries (assuming that Accounting records stored on the "sessions" colllection)
  var session = queryOne("Calling-Station-Id", radius.request['Calling-Station-Id'], "sessions");
  // Check for result
  if (session){
    // Store NAS-IP-Address for Disconnect or Change-Of-Authorization requests
    var nas_ip = session['NAS-IP-Address'];
    if (nas_ip) {
      sendDM(nas_ip);
      // sendCoA(coa_avps);
    }
  // Entry is not found
  } else {
  // Create Reply-Message
  radius.reply['Reply-Message'] = "No active sessions found";
  // Return fail response
  fail();
  }

}

function sendDM(nas_ip) {
  // Before building DM or CoA request, save existing AVP's
  // into "old_request" group
  radius.old_request = radius.request 
  // Clean "request" group
  radius.request = {};
  // Add Calling-Station-Id to "request" group
  radius.request['Calling-Station-Id'] = radius.old_request['Calling-Station-Id'];
  // Send Disconnect-Request
    var r = RadiusRequest("DM-REQUEST", nas_ip, 3799, "3000");
  // Return result of request
  return r;
}

function sendCoA(avps) {
  // Before building DM or CoA request, save existing AVP's
  // into "old_request" group
  radius.old_request = radius.request 
  // Clean "request" group
  radius.request = {};
  // Add Calling-Station-Id to "request" group
  radius.request['Calling-Station-Id'] = radius.old_request['Calling-Station-Id'];
  // Send Disconnect-Request
    var r = RadiusRequest("COA-REQUEST", nas_ip, 3799, "3000");
  // Return result of request
  return r;
}

Send PPSK (DPSK) response with dynamic vlan.

// Check for PPSK Auth
    //
    function checkPPSK() {

      if (radius.request['User-Name'] == radius.request['User-Password']) {
        var dbResult = queryMany("Called-Station-Id", radius.request['Called-Station-Id'], "ppsk");
        if (dbResult) {
          for (i = 0; i <= dbResult.length - 1; i++) {
            radius.reply['tag'][i] = {};
            radius.reply['tag'][i]['Tunnel-Password'] = dbResult[i]['Tunnel-Password'];
            radius.reply['tag'][i]['Tunnel-Private-Group-Id'] = dbResult[i]['Tunnel-Private-Group-Id'];
            radius.reply['tag'][i]['Tunnel-Type'] = dbResult[i]['Tunnel-Type'];
            radius.reply['tag'][i]['Tunnel-Medium-Type'] = dbResult[i]['Tunnel-Medium-Type'];
          }

          radius.reply['Reply-Message'] = "Success Auth";

          //
          // Store request in the "Session" collection with TTL 1hr
          //
          // function: upsertEntry(collectionName, object, TTL, indexName, indexValue);
          //      
          upsertEntry("sessions", radius.request, 3600, "Calling-Station-Id", radius.request['Calling-Station-Id']);

          success();
          return true;
        }
        return false;
      }
    }

Send Radius request to different AAA (proxy the request)

//
// Determine whether the request has to be proxied to a partner network.
// getProxySettings(realm_or_identity) is internal function.
//
function proxyCheck() {
  const proxySettings = getProxySettings(radius.user['Realm']);
    if (proxySettings) {
        // Parameters for this realm is exists on the "config" collections. 
        const proxyInstance = new RadiusProxy();

        // Or set parameters manually.
        // proxyInstance.code = radius.code;
        // proxyInstance.dst = "1.2.3.4"; //radius.proxy['Proxy-Address'] = "1.2.3.4";
        // proxyInstance.dst_port = "1812"; // radius.proxy['Proxy-Address-Port'] = "1812";
        // proxyInstance.timeout = "2000";
        // proxyInstance.name = "MY_PROXY_PROVIDER";
        // proxyInstance.timeout_unreachable = "10000";
        // proxyInstance.balance_mode = "round-robin";
        // proxyInstance.dpd = "false";

        var  proxyResult = proxyInstance.send();
        if (proxyResult)
          return proxyResult;
    }
    return false;
}

RFC5580

    radius.request['Location-Capable'] = "NAS-Location";
    radius.request['Location-Data'] = {
      'Index': '1',
      'Location': {
        'city': 'Chicago',
        'state': 'IL',
        'zip': '60603',
        'nam': 'NAM_Value',
        'plc': 'PLC_Value',
        'loc': 'LOC_Value',
        'lmk': 'LMK_Value'
      },
      'Country': 'US'
    };

    radius.request['Location-Information'] = {
      'Index': '1',
      'Code': '0',
      'Entity': '1',
      'Sighting-Time': convertToNTP64(Date.now()),
      'Time-To-Live': convertToNTP64(Date.now() + (43200*1000)),
      'Method': 'Manual'
    }

Call function in the new thread or parallel

function acct_handler() {

    //
    // To avoid delaying the Accounting-Response, execute the Session update function in parallel.
    // So first return Acct-Response
    success();
    //
    // and then insert / update "sessions" collection
    //
    runParallel('acct()', 500);
  }

  //
  // Update sessions collection.
  //
  function acct() {
    // function: upsertEntry(collectionName, object, TTL, indexName, indexValue);
    upsertEntry("sessions", radius.request, 3600, "Calling-Station-Id", radius.request['Calling-Station-Id']);
  }

Issue EAP-TLS certificate by calling SpherAAA API

function issue_cert() {
  var ca_id = "6461706ef4befabdc81e6d19";
  var cert_type = "pem";
  var url = "https://aaa.spheralogic.com/api/pki/gen/?ca_id="+ca_id+"&cert_type="+cert_type

  var headers = {}
  headers["accept"] = "application/json"
  headers["Content-Type"] = "application/json"
  headers["Authorization"] = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."

  var httpclient = new HTTPClient();
  httpclient.url = url;
  httpclient.headers = headers;
  httpclient.method = "POST";

  var data = {
    "ct": "US",
    "st": "NY",
    "city": "NewYork",
    "o": "Company",
    "ou": "USER_NAME/ID/Department_Name",
    "cn": "anonymous@company.com",
    "days": "365",
    "passphrase": "nopass",
    "comment": "EAP-TLS Client Cert",
    "email_send_as_file": false,
    "email_send_as_url": false,
    "ssid": "nossid"
  };
  httpclient.data = JSON.stringify(data);

  try {
    var response = httpclient.send();
    log("httpclient result: " + JSON.stringify(response));
  } catch (e) {
    log("Recived error: " + e.toString());
  }
}

Insert data to the Elasticsearch or OpenSearch (HTTP Basic Auth)

function ops_insert() {
  var url = "https://opensearch.domain.com:9200/my_index/_doc"
  var headers = {}
  var auth = Base64.encode("user:password")
  headers["Content-Type"] = "application/json"
  headers["Authorization"] = "Basic "+ auth
  var httpclient = new HTTPClient();
  httpclient.url = url;
  httpclient.headers = headers;
  httpclient.method = "POST";
  httpclient.disableSslVerification = false; 
  var data = {"sourcetype":"auth", "data":radius}
  //var data = {"sourcetype":"acct", "data":radius}
  httpclient.data = JSON.stringify(data);

  try {
    var response = httpclient.send();
    log("httpclient result: " + JSON.stringify(response));
  } catch (e) {
    log("Recived error: " + e.toString());
  }
}

EAP-TLS/TTLS/PEAP Authentication with Microsoft EntraID

Overview

This section shows a concise, practical approach to validate EAP‑TLS client certificates against Microsoft Entra ID (Azure AD) during a RADIUS EAP‑TLS exchange. It is based on SpherAAA extracting the client certificate field into an object and calling the EntraIDAuth.checkUser() method against EntraID.

Before using certificate-based EAP‑TLS for ongoing authentications, users must first be onboarded. Onboarding is an initial authentication that requires the user's real password (for example: EAP‑TTLS with PAP or GTC, or EAP‑PEAP with GTC). During onboarding SpherAAA authenticates the supplied credentials against EntraID (e.g., EntraIDAuth.checkCredentials(username, password)). On successful verification SpherAAA will:

  • Generate an EAP‑TLS client certificate or configuration package (PEM, PKCS#12, or Apple mobileconfig).
  • Embed the chosen identifier/OID (used later as the certificate OU or extension).
  • Deliver the certificate/configuration to the user via email (or another agreed delivery mechanism).

After onboarding completes and the client installs the certificate/configuration, subsequent authentications use EAP‑TLS for configured SSID. During those EAP‑TLS exchanges SpherAAA extracts the certificate identifier (OU/OID) from the presented client certificate and validates the account state by calling EntraIDAuth.checkUser(oid). If EntraID reports the user as active and the certificate mapping is valid (optinally), access is allowed; otherwise the request is rejected.

Implementers should ensure secure handling of secrets and certificates, record the mapping used during onboarding for later validation, and test the onboarding and EAP‑TLS flows in a staging tenant before production rollout.

Prerequisites

  • An registered application in Entra ID with Graph API permissions to read users (client credentials flow).
  • CA certificate that was imported or generated in SpherAAA PKI (the CA only needs to be trusted by SpherAAA PKI for interoperability; it does not need to be trusted by EntraID).

High-level flow

  1. Client performs EAP-TTLS with GTC or PAP (or EAP-PEAP with GTC)
  2. PolicyLogic verifies that authentication was done using credentials (username and password exists) and using EntraIDAuth.checkCredentials(username,password) method, sends request to EntraID.
  3. Upon success result from EntraIDAuth.checkCredentials, SpherAAA responds with Access-Accept to Access-Point.
  4. At the same time, SpherAAA uses the OID from EntraID response and calls pkiCertGen(oid), which creates an EAP-TLS certificate, uses the OID value as the OU field on the certificate, and sends that certificate configuration or payload to the client's email. The email is already known from the username or EntraID response.
  5. Configuration could be: Full chain PEM, PKCS#12 (for Android, Linux, etc.), or MobileConfig file for Apple products.
  6. After installing the configuration, Client performs EAP‑TLS and presents the client certificate to the RADIUS server.
  7. RADIUS script extracts the certificate OU field from the subject and PolicyLogic queries Microsoft Graph using EntraIDAuth.checkUser(oid) function to verify that this user is still active.
  8. If the response is success, SpherAAA allows access and, based on the response field, SpherAAA could set different VLANs or PolicyGroups.
  9. Otherwise, reject the access request.

Entra ID registration

  1. Sign in to the Azure portal (https://portal.azure.com) with an account that has at least Application Administrator + consent rights. Open Microsoft Entra ID.
  2. Go to: App registrations → New registration. Enter:

  3. Name: spheraaa
    Supported account types: Accounts in this organizational directory only (Single tenant)
    Redirect URI: leave empty
    Click Register

  4. On the Overview page copy and securely store:
    Application (client) ID
    Directory (tenant) ID

  5. Create a client secret: Certificates & secrets → Client secrets → New client secret.
    Description: spheraaa-client
    Expiration: choose shortest that fits rotation policy
    After creation copy the Secret Value immediately (cannot be retrieved later)

  6. Add Microsoft Graph application permission:
    API permissions → Add a permission → Microsoft Graph → Application permissions
    Select: Directory.Read.All
    Add permissions

  7. Grant admin consent:
    API permissions → Grant admin consent for → Confirm
    Verify Status = Granted for Directory.Read.All

Security notes:
* Store client_id, tenant_id, and client_secret only in Vault entries (never in source).
* Enforce secret rotation before expiry (calendar reminder + automation if possible).
* Keep permissions minimal (only Directory.Read.All).
* Review Entra audit logs after granting consent.
* Remove unused app registrations periodically.

SpherAAA Vault

After registering the app, add the credentials to the SpherAAA Vault so PolicyLogic can call Microsoft Graph.

  1. In SpherAAA UI: Collections → Vault → Create Entry.
  2. Create these vault entries (keys are the default names used by PolicyLogic examples):

  3. Key name: client_id
    Key value: <your-client-id>
    Expires: optional
    Note: Azure Application (client) ID

  4. Key name: tenant_id
    Key value: <your-tenant-id>
    Expires: optional
    Note: Azure Directory (tenant) ID

  5. Key name: client_secret
    Key value: <your-client-secret>
    Expires: recommended (set an expiration)
    Note: Application secret — keep this encrypted and rotate regularly

  6. Save each entry and verify PolicyLogic can read them in a test run.

Example script for PolicyLogic.

// Override this function on your main file. 
function verifyEapTls() {
  if (radius.request['Inner-Auth-Method'] === "X509") {
    // Check whether to perform OID-based authentication using EntraID.
    // Verify that the EAP identity is "anonymous@company.com",
    // which indicates the EAP-TLS certificate was issued during onboarding
    // (for example, via EAP-TTLS).
    if (radius.request['User-Name'] === "anonymous@company.com") {
      const subjectObjects = parseDN(radius.user.X509.SubjectDN);
      const entraResult = verifyEntraId(subjectObjects.OU);
      if (entraResult) {
        return true;
      }
    } else if (checkTLSCert()) {
      return true;
    } else {
      radius.reply['Reply-Message'] = "Certificate not found";
      return false;
    }
  } else {
    return false;
  }
}

// Verify credentials against EntraID
// more info https://spheralogic.com/wiki/policy/#entraidauth
function verifyEntraId(username, password) {

  // Read credentials from Vault
  const clientId = readFromVault("entraid-clientid");
  const tenantId = readFromVault("entraid-tenantid");
  const clientSecret = readFromVault("entraid-key");

  // Flag for cert generation
  var genCert = false;

  // Validate required parameters
  if (!clientId || !tenantId || !clientSecret) {
    throw new Error("Missing EntraID credentials in Vault");
  }

  // Initialize EntraIDAuth object
  const authClient = new EntraIDAuth();

  // Set parameters for the auth client
  authClient.client_id = clientId;
  authClient.tenant_id = tenantId;
  authClient.client_secret = clientSecret;

  // Attempt authentication
  var response;
  if (isEmail(username) && password !== undefined) {
    response = authClient.checkCredentials(username, password);
    genCert = true;
  } else
    response = authClient.checkUser(username);

  // Log for debug
  //log(response);

  const result = !!(response && response.exists);
  if (result) {
    if (genCert) {
      radius.reply['Reply-Message'] = ['UP Auth OK'];
      generateCert(response);
    } else
      radius.reply['Reply-Message'] = ['OID Auth OK'];
  }

  // Return true if authentication succeeded (user exists and access_token decoded)
  return result;
}

function generateCert(result) {
  log(result)
  var cert_request = {
    "ct": "US",
    "st": "NY",
    "city": "NewYork",
    "o": "Company",
    "ou": result.oid,
    "cn": "anonymous@company.com",
    "days": "365",
    "passphrase": "nopass",
    "comment": "EAP-TLS Client Cert",
    "email_send_as_file": result.upn,
    "email_send_as_url": false,
    "ssid": "MySecureWiFi", // Replace with your 802.1x (WPA-Ent) SSID.
    "ca_id": "687aa36926d95d8920665d72", // CA certificate ID.
    "cert_type": "pem" //cert_type: Supported formats: "pem","p12", "apple_mobileconfig".
  };

  // Log for debug
  // log(cert_request) 

  pkiGenCert(cert_request);
}