Skip to content

Admin Authentication & SSO

Flow ID: AD-01 Module(s): auth, sso, advisable Complexity: Medium Last Updated: 2026-04-10

Business Overview

Admin authentication supports three providers: database auth (default), static config auth (fallback), and OAuth2/OIDC SSO via Keycloak. The system uses 9 roles mapped to dedicated admin panel home pages. Rate limiting (IP-based, 5 attempts / 10-minute window) protects against brute force. In addition to session-based auth for the admin panel, the REST API layer provides stateless JWT authentication for admin users with access + refresh token pairs.

The auth module also includes an internal task management system for admin users (create, assign, complete tasks), and an audit logging subsystem that records admin actions to a dedicated table.

API Reference

REST Endpoints (JWT-Based)

MethodPathControllerDescription
POST/rest/auth/admin/loginAdvisable\Rest\Auth\AdminAuth::loginAuthenticate admin user, returns JWT access + refresh tokens
POST/rest/auth/admin/refreshAdvisable\Rest\Auth\AdminAuth::refreshExchange refresh token for new access token

Admin Panel Routes (Session-Based)

RouteControllerDescription
advisable/loginAdv_login::loginLogin form + POST handler
advisable/login/logoutAdv_login::logoutDestroy session, redirect to login
advisable/sso/loginAdvSso::loginRedirect to Keycloak authorization URL
advisable/sso/callbackAdvSso::callbackOAuth2 code exchange callback
advisable/sso/logoutAdvSso::logoutKeycloak end-session + local session destroy

User management routes (auth, auth/add, auth/edit/{id}, auth/delete/{id}, auth/undelete/{id}, auth/changePassword/{id}, auth/changeMyPassword) are documented in (see AD-58 Admin User Management for the full legacy and REST route tables, RBAC policies, and endpoint details).

Task management routes (auth/tasks, auth/myTasks, auth/tasksTo, etc.) are documented in AD-55 Task Management.

Code Flow

Database Login Flow (Session-Based)

Adv_login::login()
  |-- Load Blocking library
  |-- POST: username + pwd
  |     |-- Advauth::login($username, $password)
  |     |     |-- Check $dbAuth flag (from config 'db_auth')
  |     |     |-- users_model->getByUsername($username)
  |     |     |-- Guard: $user->soft_delete must be 0
  |     |     |-- advAuthVerify($password, $user->password) [password_verify for bcrypt/argon2i]
  |     |     |-- user_role_model->getUserRolesIds($user->id) -> array of role IDs
  |     |     |-- session->set_userdata([uid, username, roles, loggedIn, uprovider=2])
  |     |     \-- return true
  |     |-- blocking->attempt(true) [clears IP record on success]
  |     |-- roles_model->getRole($roles[0]) -> get role home page
  |     \-- redirect($firstRole->home)
  |-- On failure:
  |     |-- blocking->attempt(false) [increments IP counter]
  |     \-- render['wrong_data'] = 1
  \-- blocking->allowAttempt() -> false if >=5 attempts within 10 min -> redirect to admin

Config Auth Flow (Fallback)

Advauth::login()
  |-- Check $configAuth flag (from config 'simple_auth')
  |-- Read config_auth array from auth.php
  |-- Plain-text comparison: username == $user['username'] && password == $user['password']
  |-- session->set_userdata([uid=null, username, roles, loggedIn, uprovider=1])
  \-- return true

Config users are defined in application/config/auth.php via environment variables:

  • APP_ADMIN_USERNAME / APP_ADMIN_PWD with AUTH_ROLE_ADMIN
  • APP_ADMIN_ADVISABLE_USERNAME / APP_ADMIN_ADVISABLE_PWD with AUTH_ROLE_ADVISABLE

SSO Login Flow (Keycloak OIDC)

AdvSso::login()
  |-- SingleSignOn::getLoginUrl()
  |     |-- OIDC discovery: GET {keycloak}/realms/{realm}/.well-known/openid-configuration [cached via PSCache]
  |     |-- Build URL: authorization_endpoint + client_id + response_type=code + scope=openid roles
  |     \-- redirect to Keycloak
  |
AdvSso::callback()
  |-- Verify ?iss matches expected issuer ({server}/realms/{realm})
  |-- failOnError(?error)
  |-- expectAuthorizationCode(?code)
  |-- exchangeAuthorizationCodeForTokens($code)
  |     |-- POST to token_endpoint with grant_type=authorization_code
  |     |-- client_id + client_secret from env
  |     \-- returns [access_token, refresh_token, full_response]
  |-- decodeAccessToken($accessToken)
  |     |-- Extract kid from JWT header
  |     |-- Match kid against JWKs (cached from jwks_uri)
  |     |-- Extract x5c certificate, build PEM
  |     \-- JWT::decode($accessToken, new Key($pem, $alg))
  |-- determineRoleFromAccessToken($decoded)
  |     |-- Read groups claim from token
  |     |-- Build group paths to check (org+site combos with defaults):
  |     |     /group-org-{orgId}/group-site-{siteId}/role-{roleName}
  |     |-- Cross-reference realm_access.roles (prefixed 'ecommercen-v4-{role}')
  |     |-- Match group path against ROLE_MAPPING
  |     \-- Return AUTH_ROLE_* constant
  |-- session->set_userdata([sso_access_token, sso_refresh_token, uid=null, username=$name, roles=[$role], loggedIn, uprovider=3])
  \-- redirect to role home page

SSO Session Validation (Every Admin Page Load)

The Adv_admin_controller constructor calls validateSSOSession() on every request:

validateSSOSession() [ecommercen/helpers/auth_helper.php]
  |-- Skip if uprovider != AUTH_PROVIDER_SSO
  |-- Try to decode current sso_access_token
  |-- On DecodeTokenException (expired):
  |     |-- refreshAccessToken(sso_refresh_token)
  |     |     |-- POST to token_endpoint with grant_type=refresh_token
  |     |     \-- returns new [access_token, refresh_token]
  |     |-- Decode new access token
  |     |-- Determine new role
  |     \-- Update session with new tokens + roles
  |-- On any other SSO exception:
  |     |-- Destroy session data
  |     \-- Re-throw (controller catches and redirects)

REST API JWT Auth Flow

AdminAuth::login()
  |-- Read username + password from JSON body or POST
  |-- Advauth::statelessAdminLogin($username, $password)
  |     |-- Check DB auth: users_model->getByUsername, password_verify
  |     |-- Check config auth: plain-text match
  |     \-- Return user ID or null
  |-- getUserRoles($userId)
  |     |-- Check config_auth users first (match by username)
  |     \-- Fall back to user_role_model->getUserRolesIds
  |-- Tokens::generateAccessToken($userId, REST_BACKEND_USER, $roles)
  |     |-- JWT payload: iss, aud, iat, nbf, exp, data{userId, type, roles}
  |     \-- Signed with HS256 (configurable)
  |-- Tokens::generateRefreshToken($userId, REST_BACKEND_USER)
  |     |-- Delete existing refresh tokens for user
  |     |-- Generate 64-char random hex token
  |     \-- Store in refresh_tokens table with expiry
  \-- Return {access_token, refresh_token}

AdminAuth::refresh()
  |-- Validate refresh token from refresh_tokens table
  |-- Check expiry and user_type == REST_BACKEND_USER
  \-- Return new access_token with current roles

Domain Layer

Legacy Auth Module (ecommercen/auth/)

FileDescription
controllers/Adv_auth.phpUser CRUD + task management (598 lines). Role-gated: ADVISABLE+ADMIN for user management. ADVISABLE-only users cannot see Advisable/Developer roles in dropdown. Extensible via afterAdd/afterEdit/afterDelete hooks.
models/Adv_users_model.phpUsers table CRUD + audit log queries. Soft-delete pattern. guardUserEdit() prevents password/soft_delete modification via edit.
models/Adv_roles_model.phpDB-based roles. Constructor reads the roles table. Provides dropdown, getRoles, getRole, getRolesByIds, getRoleByName, getRolesByName.
models/Adv_user_role_model.phpJunction table user_role management. Diff-based update: calculates roles to add/remove.
models/Adv_tasks_model.phpTasks CRUD — see AD-55 Task Management for full model reference.

SSO Module (ecommercen/sso/)

FileDescription
controllers/AdvSso.phpLogin, callback, logout endpoints. Extends Base_c (not Admin_c -- unauthenticated entry point). Uses guard() wrapper for exception handling.

Modern SSO Layer (src/Auth/SingleSignOn/)

FileDescription
SingleSignOn.phpComplete OIDC client (472 lines). Handles discovery, JWK loading, token exchange, token refresh, JWT decoding, group-based role determination.
Exception/BaseException.phpBase with HTTP status code support
Exception/NotEnabledException.phpSSO feature flag disabled
Exception/AuthorizationCodeMissingException.phpMissing ?code parameter
Exception/AuthorizationExchangeFailedException.phpToken exchange HTTP error
Exception/DecodeTokenException.phpJWT decode failure (expired, malformed, no matching key)
Exception/JWKsIrretrievableException.phpCannot fetch JWK set
Exception/MissingGroupsClaimException.phpJWT has no groups claim
Exception/OpenIDConfigurationIrretrievableException.phpCannot fetch OIDC discovery
Exception/RoleMismatchException.phpGroup matched but role not in ROLE_MAPPING
Exception/RoleMissingException.phpNo matching group path found
Exception/TokenRefreshFailedException.phpRefresh token exchange failed
Exception/UnexpectedIssuerException.php?iss does not match expected realm

REST Auth Layer (src/Rest/Auth/)

FileDescription
AdminAuth.phpAdmin JWT login + refresh (151 lines). OpenAPI annotated.
CustomerAuth.phpCustomer JWT login + refresh (separate system).
Tokens.phpJWT generation (access) and refresh token generation/validation. Uses firebase/php-jwt.
Config.phpConfig DTO: key, algorithm, accessTokenExpire, refreshTokenExpire.
RefreshTokenModel.phpCRUD for refresh_tokens table. Extends CI_Model.
TokenProtect.phpMiddleware-style JWT validation for protected endpoints.
BackendGuard.php / FrontendGuard.php / AnyGuard.phpRoute guards by user type.
RemoveExpiredRefreshTokens.phpCleanup job for expired refresh tokens.

Auth Helper (ecommercen/helpers/auth_helper.php)

Global functions loaded on every admin request:

  • isLoggedIn() -- checks session loggedIn
  • validateSSOSession() -- auto-refreshes SSO tokens
  • allowRole($roles, $roleIds) -- checks if any session role matches allowed list
  • advAuthHash($val, $token) -- delegates to Advauth::advAuthHash()
  • isAdvisableUser(), isDeveloper(), isAdmin(), isCmsUser(), isProductsUser(), isReportingUser(), isOrdersUser(), isMarketingUser(), isMediaUser() -- individual role checks
  • mergeUsersRoles($users, $usersRoles) -- decorates user objects with roles
  • allowedUsers($users, $roles) -- filters users by roles

Architecture

Authentication Providers

IDConstantSourcePassword HandlingSession uid
1AUTH_PROVIDER_CONFIGapplication/config/auth.php (config_auth)Plain-text comparisonnull
2AUTH_PROVIDER_DBusers tablepassword_verify() (bcrypt/argon2i)User ID
3AUTH_PROVIDER_SSOKeycloak OIDCJWT signature verificationnull

Provider priority in Advauth::login(): DB auth checked first, then config auth. Both can be enabled simultaneously via config flags db_auth and simple_auth.

9 Admin Roles

ConstantIDNameHome RouteDescription
AUTH_ROLE_ADVISABLE1AdvisableadvisableSuperuser -- platform developers
AUTH_ROLE_DEVELOPER2DeveloperadvisableDevelopment access
AUTH_ROLE_ADMIN3AdminadvisableClient admin
AUTH_ROLE_CMS4CMScategory/category_adminContent management
AUTH_ROLE_PRODUCTS5Productsproducts_adminProduct catalog
AUTH_ROLE_ORDERS6Ordersorders_adminOrder management
AUTH_ROLE_REPORTING7Reportingeshop/reportingReports & analytics
AUTH_ROLE_MARKETING8Marketingpromo_adminCampaigns & promotions
AUTH_ROLE_MEDIA9Mediasliders/sliders_adminSlider/media management

Defined in application/config/constants.php (lines 99-107). Role metadata (name, home) stored in the roles database table (seeded by migration 20260409131959_create_roles_table.php).

Keycloak Group Path Resolution

SSO role determination follows a priority-ordered group path search:

  1. /group-org-{orgId}/group-site-{siteId}/role-{roleName} (exact match)
  2. /group-org-{orgId}/group-site-default/role-{roleName} (default site)
  3. /group-org-default/group-site-{siteId}/role-{roleName} (default org)
  4. /group-org-default/group-site-default/role-{roleName} (full default)

The role name is extracted from realm_access.roles with prefix ecommercen-v4- stripped, then matched against ROLE_MAPPING (advisable, developer, admin, cms, products, orders, reporting, marketing, media).

Password Hashing

Configured in application/config/auth.php via auth_hash:

Config ValueAlgorithmNotes
password-hash-mcrypt (default)PASSWORD_BCRYPT via password_hash()Recommended
password-hash-argon2iPASSWORD_ARGON2I via password_hash()PHP 7.2+
md5md5()Deprecated -- do not use
md5-tokenmd5($val . $token)Deprecated -- salt-based MD5
wecare-cryptsha1()Legacy -- verify accepts md5 OR sha1

JWT Token Structure (REST API)

Access token payload:

json
{
  "iss": "https://site-url.com/",
  "aud": "https://site-url.com/",
  "iat": 1712345678,
  "nbf": 1712345678,
  "exp": 1712349278,
  "data": {
    "userId": "42",
    "type": "backend",
    "roles": [1, 3]
  }
}

Refresh tokens are opaque 64-character hex strings stored in the refresh_tokens table.

Session Data Structure

php
// DB Auth
['uid' => 42, 'username' => 'admin', 'roles' => [1, 3], 'loggedIn' => true, 'uprovider' => 2]

// Config Auth
['uid' => null, 'username' => 'admin', 'roles' => [3], 'loggedIn' => true, 'uprovider' => 1]

// SSO Auth
['uid' => null, 'username' => 'John Doe', 'roles' => [1], 'loggedIn' => true, 'uprovider' => 3,
 'sso_access_token' => '...jwt...', 'sso_refresh_token' => '...']

Data Model

users and user_role Tables

Full column-level schema, indexes, and FK notes for users and user_role are documented in (see AD-58 Admin User Management for canonical schema of both tables). Key auth-relevant facts: soft_delete = 1 blocks login; user_role.role_id has a FK to roles(id) via fk_user_role_role_id.

block_access Table

ColumnTypeDescription
idINT(11) PK AIRecord ID
ipVARCHAR(15)Client IP address
attemptsTINYINT(1)Failed attempt count
last_attemptDATETIMETimestamp of last attempt

Index: ip. Records auto-deleted when block window expires (10 minutes).

user_audit_logs Table

Full schema documented in (see AD-32 Audit Logging for the canonical user_audit_logs column definitions, indexes, and migration reference). Auth-relevant fact: SSO users produce rows with user_id = null; provider records DATABASE or CONFIG.

tasks Table

See AD-55 Task Management for the canonical schema. Key points: title is TEXT (not VARCHAR), there is no updated_at column, and the table has no Phinx migration (defined only in database/initial/initial.sql).

refresh_tokens Table

ColumnTypeDescription
user_idVARCHARUser ID or username
user_typeVARCHARbackend or frontend
tokenVARCHAR64-char hex refresh token
expires_atDATETIMEToken expiry timestamp

Configuration

Environment Variables (SSO)

VariableDescription
APP_ADMIN_SSO_ENABLEDEnable/disable SSO (boolean)
APP_ADMIN_SSO_KEYCLOAK_SERVER_URLKeycloak base URL
APP_ADMIN_SSO_KEYCLOAK_REALMKeycloak realm name
APP_ADMIN_SSO_KEYCLOAK_CLIENT_IDOAuth2 client ID
APP_ADMIN_SSO_KEYCLOAK_CLIENT_SECRETOAuth2 client secret
APP_SAAS_SITE_IDSite identifier for group matching
APP_SAAS_ORG_IDOrganization identifier for group matching

Environment Variables (Static Auth)

VariableDescription
APP_ADMIN_USERNAMEConfig auth admin username
APP_ADMIN_PWDConfig auth admin password
APP_ADMIN_ADVISABLE_USERNAMEConfig auth advisable username
APP_ADMIN_ADVISABLE_PWDConfig auth advisable password

Config Files

FileContents
application/config/auth.phpAuth flags (simple_auth, db_auth), hash type, static users
application/config/constants.phpAUTH_ROLE_* and AUTH_PROVIDER_* constants
src/Rest/Auth/container.phpDI registration for REST auth (Tokens, Config, AdminAuth, CustomerAuth), API versioning (VersionResolver), and the full middleware pipeline (PolicyResolver, Authentication, Authorization, ResourceContext, RelationFilter, Audit)

Client Extension Points

  • Adv_auth ships eight empty extension hooks for client repos to override — user-lifecycle hooks (afterAdd, afterEdit, afterDelete, afterUndelete, afterChangePassword) and task-lifecycle hooks (afterAddTask, afterEditTask, afterTaskDelete). Full hook table and override pattern: (see AD-58 Admin User Management for the complete client extension hook reference).
  • Client repos can add static config users via environment variables in .env.
  • The Adv_login controller extends Base_c (not Admin_c) so it can be overridden in application/controllers/ in client repos for custom login page behavior.

Business Rules

  • Provider priority: DB auth is checked first, then config auth. Both can be enabled simultaneously.
  • Soft-delete: soft_delete = 1 prevents login. Users are never hard-deleted.
  • SSO uid is null: SSO users have no local users table record. The uid session key is null.
  • Rate limiting is IP-based: The block_access table tracks per-IP failed attempts. After 5 failures within 10 minutes, the IP is blocked. On successful login, the record is deleted. Block records auto-expire after 10 minutes.
  • No 2FA: The system does not implement two-factor authentication.
  • Role gating: allowRole($roles, $sessionRoles) is called in every admin controller constructor. Non-ADVISABLE users cannot see/assign ADVISABLE or Developer roles.
  • Password change: Only ADVISABLE + ADMIN roles can change other users' passwords. Any logged-in user can change their own password via changeMyPassword().
  • Task management: See AD-55 Task Management for all task business rules, security gaps, and lifecycle details.
  • SSO caching: OIDC configuration and JWKs are cached via PSCache to avoid hitting Keycloak on every request. TTL matches the L2 cache default.
  • SSO session refresh: If the access token is expired but the refresh token is valid, validateSSOSession() transparently refreshes both tokens and updates session roles.

Live Browser Testing Findings

Login Page (/advisable/login.htm)

  • URL behavior: Root /advisable redirects with HTTP 307 to /advisable/login.htm.
  • Form fields: username (text input), pwd (password input), submit button labeled "Σύνδεση".
  • Visitor IP display: The login page displays the visitor's IP address on the page.
  • Session cookie: Standard PHPSESSID cookie (HttpOnly).

REST JWT Auth

  • Endpoint: POST /rest/auth/admin/login returns access_token and refresh_token at the root level of the JSON response (not nested under a data key).

SSO Configuration

  • SSO is enabled via Keycloak at auth.ecommercen.com, realm ecommercen.

Admin Panel Sidebar

After login, the admin panel sidebar contains 60+ menu items organized across 10 major sections:

SectionKey Items
DashboardHome, Quick Stats
CustomersCustomer list, Loyalty, Reviews
OrdersOrder list, Returns, Payments
ProductsCatalog, Categories, Brands, Variations, Bundles
SEORedirects, Sitemap, Meta tags
MarketingCampaigns, Coupons, Newsletters, Promotions
ContentPages, Blog, Sliders, Menus, Builder
ReportingSales, Traffic, Analytics
ECOMMERCEN PlusAI features, Marketplace, Integrations
CacheCache management, Rebuild
SettingsGeneral, Shipping, Payments, Taxes, Emails