Appearance
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)
| Method | Path | Controller | Description |
|---|---|---|---|
| POST | /rest/auth/admin/login | Advisable\Rest\Auth\AdminAuth::login | Authenticate admin user, returns JWT access + refresh tokens |
| POST | /rest/auth/admin/refresh | Advisable\Rest\Auth\AdminAuth::refresh | Exchange refresh token for new access token |
Admin Panel Routes (Session-Based)
| Route | Controller | Description |
|---|---|---|
advisable/login | Adv_login::login | Login form + POST handler |
advisable/login/logout | Adv_login::logout | Destroy session, redirect to login |
advisable/sso/login | AdvSso::login | Redirect to Keycloak authorization URL |
advisable/sso/callback | AdvSso::callback | OAuth2 code exchange callback |
advisable/sso/logout | AdvSso::logout | Keycloak 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 adminConfig 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 trueConfig users are defined in application/config/auth.php via environment variables:
APP_ADMIN_USERNAME/APP_ADMIN_PWDwithAUTH_ROLE_ADMINAPP_ADMIN_ADVISABLE_USERNAME/APP_ADMIN_ADVISABLE_PWDwithAUTH_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 pageSSO 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 rolesDomain Layer
Legacy Auth Module (ecommercen/auth/)
| File | Description |
|---|---|
controllers/Adv_auth.php | User 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.php | Users table CRUD + audit log queries. Soft-delete pattern. guardUserEdit() prevents password/soft_delete modification via edit. |
models/Adv_roles_model.php | DB-based roles. Constructor reads the roles table. Provides dropdown, getRoles, getRole, getRolesByIds, getRoleByName, getRolesByName. |
models/Adv_user_role_model.php | Junction table user_role management. Diff-based update: calculates roles to add/remove. |
models/Adv_tasks_model.php | Tasks CRUD — see AD-55 Task Management for full model reference. |
SSO Module (ecommercen/sso/)
| File | Description |
|---|---|
controllers/AdvSso.php | Login, callback, logout endpoints. Extends Base_c (not Admin_c -- unauthenticated entry point). Uses guard() wrapper for exception handling. |
Modern SSO Layer (src/Auth/SingleSignOn/)
| File | Description |
|---|---|
SingleSignOn.php | Complete OIDC client (472 lines). Handles discovery, JWK loading, token exchange, token refresh, JWT decoding, group-based role determination. |
Exception/BaseException.php | Base with HTTP status code support |
Exception/NotEnabledException.php | SSO feature flag disabled |
Exception/AuthorizationCodeMissingException.php | Missing ?code parameter |
Exception/AuthorizationExchangeFailedException.php | Token exchange HTTP error |
Exception/DecodeTokenException.php | JWT decode failure (expired, malformed, no matching key) |
Exception/JWKsIrretrievableException.php | Cannot fetch JWK set |
Exception/MissingGroupsClaimException.php | JWT has no groups claim |
Exception/OpenIDConfigurationIrretrievableException.php | Cannot fetch OIDC discovery |
Exception/RoleMismatchException.php | Group matched but role not in ROLE_MAPPING |
Exception/RoleMissingException.php | No matching group path found |
Exception/TokenRefreshFailedException.php | Refresh token exchange failed |
Exception/UnexpectedIssuerException.php | ?iss does not match expected realm |
REST Auth Layer (src/Rest/Auth/)
| File | Description |
|---|---|
AdminAuth.php | Admin JWT login + refresh (151 lines). OpenAPI annotated. |
CustomerAuth.php | Customer JWT login + refresh (separate system). |
Tokens.php | JWT generation (access) and refresh token generation/validation. Uses firebase/php-jwt. |
Config.php | Config DTO: key, algorithm, accessTokenExpire, refreshTokenExpire. |
RefreshTokenModel.php | CRUD for refresh_tokens table. Extends CI_Model. |
TokenProtect.php | Middleware-style JWT validation for protected endpoints. |
BackendGuard.php / FrontendGuard.php / AnyGuard.php | Route guards by user type. |
RemoveExpiredRefreshTokens.php | Cleanup job for expired refresh tokens. |
Auth Helper (ecommercen/helpers/auth_helper.php)
Global functions loaded on every admin request:
isLoggedIn()-- checks sessionloggedInvalidateSSOSession()-- auto-refreshes SSO tokensallowRole($roles, $roleIds)-- checks if any session role matches allowed listadvAuthHash($val, $token)-- delegates toAdvauth::advAuthHash()isAdvisableUser(),isDeveloper(),isAdmin(),isCmsUser(),isProductsUser(),isReportingUser(),isOrdersUser(),isMarketingUser(),isMediaUser()-- individual role checksmergeUsersRoles($users, $usersRoles)-- decorates user objects with rolesallowedUsers($users, $roles)-- filters users by roles
Architecture
Authentication Providers
| ID | Constant | Source | Password Handling | Session uid |
|---|---|---|---|---|
| 1 | AUTH_PROVIDER_CONFIG | application/config/auth.php (config_auth) | Plain-text comparison | null |
| 2 | AUTH_PROVIDER_DB | users table | password_verify() (bcrypt/argon2i) | User ID |
| 3 | AUTH_PROVIDER_SSO | Keycloak OIDC | JWT signature verification | null |
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
| Constant | ID | Name | Home Route | Description |
|---|---|---|---|---|
AUTH_ROLE_ADVISABLE | 1 | Advisable | advisable | Superuser -- platform developers |
AUTH_ROLE_DEVELOPER | 2 | Developer | advisable | Development access |
AUTH_ROLE_ADMIN | 3 | Admin | advisable | Client admin |
AUTH_ROLE_CMS | 4 | CMS | category/category_admin | Content management |
AUTH_ROLE_PRODUCTS | 5 | Products | products_admin | Product catalog |
AUTH_ROLE_ORDERS | 6 | Orders | orders_admin | Order management |
AUTH_ROLE_REPORTING | 7 | Reporting | eshop/reporting | Reports & analytics |
AUTH_ROLE_MARKETING | 8 | Marketing | promo_admin | Campaigns & promotions |
AUTH_ROLE_MEDIA | 9 | Media | sliders/sliders_admin | Slider/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:
/group-org-{orgId}/group-site-{siteId}/role-{roleName}(exact match)/group-org-{orgId}/group-site-default/role-{roleName}(default site)/group-org-default/group-site-{siteId}/role-{roleName}(default org)/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 Value | Algorithm | Notes |
|---|---|---|
password-hash-mcrypt (default) | PASSWORD_BCRYPT via password_hash() | Recommended |
password-hash-argon2i | PASSWORD_ARGON2I via password_hash() | PHP 7.2+ |
md5 | md5() | Deprecated -- do not use |
md5-token | md5($val . $token) | Deprecated -- salt-based MD5 |
wecare-crypt | sha1() | 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
| Column | Type | Description |
|---|---|---|
id | INT(11) PK AI | Record ID |
ip | VARCHAR(15) | Client IP address |
attempts | TINYINT(1) | Failed attempt count |
last_attempt | DATETIME | Timestamp 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
| Column | Type | Description |
|---|---|---|
user_id | VARCHAR | User ID or username |
user_type | VARCHAR | backend or frontend |
token | VARCHAR | 64-char hex refresh token |
expires_at | DATETIME | Token expiry timestamp |
Configuration
Environment Variables (SSO)
| Variable | Description |
|---|---|
APP_ADMIN_SSO_ENABLED | Enable/disable SSO (boolean) |
APP_ADMIN_SSO_KEYCLOAK_SERVER_URL | Keycloak base URL |
APP_ADMIN_SSO_KEYCLOAK_REALM | Keycloak realm name |
APP_ADMIN_SSO_KEYCLOAK_CLIENT_ID | OAuth2 client ID |
APP_ADMIN_SSO_KEYCLOAK_CLIENT_SECRET | OAuth2 client secret |
APP_SAAS_SITE_ID | Site identifier for group matching |
APP_SAAS_ORG_ID | Organization identifier for group matching |
Environment Variables (Static Auth)
| Variable | Description |
|---|---|
APP_ADMIN_USERNAME | Config auth admin username |
APP_ADMIN_PWD | Config auth admin password |
APP_ADMIN_ADVISABLE_USERNAME | Config auth advisable username |
APP_ADMIN_ADVISABLE_PWD | Config auth advisable password |
Config Files
| File | Contents |
|---|---|
application/config/auth.php | Auth flags (simple_auth, db_auth), hash type, static users |
application/config/constants.php | AUTH_ROLE_* and AUTH_PROVIDER_* constants |
src/Rest/Auth/container.php | DI 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_authships 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_logincontroller extendsBase_c(notAdmin_c) so it can be overridden inapplication/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 = 1prevents login. Users are never hard-deleted. - SSO uid is null: SSO users have no local
userstable record. Theuidsession key is null. - Rate limiting is IP-based: The
block_accesstable 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
/advisableredirects 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
PHPSESSIDcookie (HttpOnly).
REST JWT Auth
- Endpoint:
POST /rest/auth/admin/loginreturnsaccess_tokenandrefresh_tokenat the root level of the JSON response (not nested under adatakey).
SSO Configuration
- SSO is enabled via Keycloak at
auth.ecommercen.com, realmecommercen.
Admin Panel Sidebar
After login, the admin panel sidebar contains 60+ menu items organized across 10 major sections:
| Section | Key Items |
|---|---|
| Dashboard | Home, Quick Stats |
| Customers | Customer list, Loyalty, Reviews |
| Orders | Order list, Returns, Payments |
| Products | Catalog, Categories, Brands, Variations, Bundles |
| SEO | Redirects, Sitemap, Meta tags |
| Marketing | Campaigns, Coupons, Newsletters, Promotions |
| Content | Pages, Blog, Sliders, Menus, Builder |
| Reporting | Sales, Traffic, Analytics |
| ECOMMERCEN Plus | AI features, Marketplace, Integrations |
| Cache | Cache management, Rebuild |
| Settings | General, Shipping, Payments, Taxes, Emails |
Related Flows
- CF-10 Customer Auth -- separate customer authentication system (triple-SHA1 passwords, different session structure)
- AD-32 Admin Audit Logging -- tracks admin actions via
user_audit_logstable - AD-13 Settings & Configuration -- registry-based platform configuration
- AD-44 Social Auth Settings -- Google/Facebook OAuth credentials for customer social login (managed under settings, not admin auth)
- AD-55 Task Management -- admin task assignments referencing admin user accounts