simpliSSO is a centralized identity portal built on Authentik and federated with your Microsoft 365 / Azure AD tenant. Staff sign in once with their existing company email — and reach every internal application: web apps over OIDC, legacy ERPs over SAML2, hardware over LDAP.
Azure AD is the government that issued the passport — it owns the identity. Authentik is the border officer that checks it and grants access to each specific room (application). Same passport, every door.
The system that knows who you are. In simpliSSO, Authentik is the IdP, backed by Azure AD as the upstream source.
Any app that trusts the IdP to verify users. ERPs, A/R aging, document control, every internal tool.
Modern token-based login protocol. Used by all Tier 1 web apps. Tokens are JSON (JWT), signed by Authentik.
Older XML-based SSO protocol. Required for legacy ERPs like Infor M3 — Authentik speaks both at the same time.
A proxy Authentik deploys for systems that can't speak OIDC or SAML2 natively — printers, label hardware, legacy boxes.
A flow is a sequence of stages Authentik runs on login: enter password, verify OTP, link WhatsApp. Each step is configurable.
A Python rule that allows, denies, or modifies access. "Only Finance group can reach A/R Aging." Evaluated inside flows.
A Python expression that shapes the token. Used to embed ERP claims — M3 company, division, roles — directly into the JWT.
Azure AD is shown in the architecture below — but Authentik federates with any standards-compliant identity source. Swap or stack providers without changing your apps.
The setup shown in this architecture. Staff use existing MS365 credentials and MFA flows from Conditional Access pass straight through.
Same pattern, swap the upstream. Staff sign in with their workspace.google.com account and Authentik issues the downstream tokens.
Common when consolidating off another SSO. Authentik can ride on top while you migrate apps one at a time — no big-bang cutover.
For air-gapped or strictly on-prem sites. Authentik binds to your existing AD or OpenLDAP directory — no cloud trip required.
Any provider that publishes a .well-known/openid-configuration URL — Keycloak, Auth0, Cognito, GitLab, your own.
Any IdP that publishes a SAML metadata XML — ADFS, Shibboleth, OneLogin, internal IdPs. Authentik consumes the metadata and federates.
Same pattern for all of them — Authentik handles the federation, your apps never see the upstream change. You can also stack multiple sources in one flow (e.g., MS365 for staff + LDAP for contractors).
Azure AD is the source of truth. Authentik is the single SSO hub. Every internal app — modern, legacy, or hardware — connects through one of three protocols.
The single SSO gatekeeper. Self-hosted on your infrastructure, never stores passwords — it federates upstream to Azure AD and issues tokens downstream to every app.
Your existing Microsoft tenant remains the authoritative directory. Staff sign in with the same company email and password they already use for Office 365 — no new accounts to manage.
All custom logic runs inside Authentik as expression policies, property mappings, and Django stages. No external services. All Python code becomes your IP on final payment.
All modern internal apps over OIDC. Legacy ERPs over SAML2. Tokens carry custom claims so apps read roles directly — no separate permission database.
Per-user account linking stages bind each staff member's WhatsApp number and 3CX extension to their Azure AD account on first login. Portal tiles deep-link to the right conversation.
Printers, label hardware, and legacy boxes that cannot speak OIDC or SAML2 authenticate against the LDAP outpost. Robot systems connect via portal deep link only.
Staff hit the portal · Authentik checks session and federates to Azure AD · tokens flow downstream to every tier via the right protocol.
Three flows cover 99% of daily use: the first login of the day, returning to another app, and the additional MFA challenge on sensitive systems.
Scenario A — first login of the day. Staff opens the portal, signs in once via Azure AD, and the portal personalizes the app tile grid based on their group memberships.
Scenario B — returning user. Same staff member, same browser, different app. The session cookie carries them straight through — zero prompts, zero passwords re-entered.
Scenario C — step-up MFA. When the same user opens a sensitive system (IT Services admin, finance apps), Authentik triggers an extra MFA challenge if the last verification was more than 30 minutes ago.
Scenario D — SCIM provisioning. IT creates or deactivates a staff account once in Azure AD. SCIM pushes the change to Authentik and every connected app reflects it instantly — no per-app setup, no manual cleanup on resignation.
Each module is priced and delivered independently. Add or remove before signing with proportional price adjustment. Infrastructure (F-01, F-02) and Core SSO (F-03) are mandatory — the rest can be phased.
Azure as upstream IdP. All 24 services inherit MS365 login, MFA, and conditional access. Staff use existing company credentials.
Self-hosted Docker Compose production stack: PostgreSQL, Redis, Nginx SSL, backups, admin portal, branding.
Register all 24 services as OIDC or SAML2 Applications. Group-based access policies. Blueprint YAML config-as-code.
Python property mappings embed company code, division, and M3 roles into the token. Apps read roles directly — no separate permission DB.
Expression policy enforces an extra MFA challenge when accessing designated sensitive systems. ~20 lines of Python.
FastAPI endpoint renders only the apps each user is authorized to see. Tile icons + Thai/English labels + deep links.
Custom Django stage links each staff member's WhatsApp number to their AD account on first login. Portal tile deep-links to the right conversation.
Maps each staff member's 3CX extension to their AD account. Portal tile launches the web client pre-authenticated to the right extension.
Custom property mappings bridge Authentik's modern SAML2 output to M3's non-standard attribute names and NameID requirements.
CSS + JS injection localizes login, MFA prompts, error messages, and password reset into Thai. Includes branding on every auth screen.
End-to-end testing across all 24 services. Edge cases: token expiry, MFA failures, session conflicts, SAML mismatches, LDAP connectivity. 2-hour UAT.
Architecture diagrams, admin runbook, onboarding/offboarding guides, troubleshooting — Thai and English. 2-hour live handover session.
Hardware (Kyocera Print, Print Label, WCS Robot) is client-provided and excluded from scope. Portal deep links to hardware systems are included where technically possible.
Every app needs three things: redirect to Authentik, handle the callback, validate the session. The pattern is identical across frameworks — only the library differs. Four env vars per app, no homegrown auth code.
Every Authentik OIDC provider auto-publishes a discovery document. New apps only need a client_id and client_secret — URLs, signing algorithms, and supported scopes are fetched from this single endpoint.
GET https://sso.example.com/application/o/<app-slug>/.well-known/openid-configuration
# pip install authlib fastapi itsdangerous
from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.responses import RedirectResponse
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
import os
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"])
oauth = OAuth()
oauth.register(
name="authentik",
client_id=os.environ["OIDC_CLIENT_ID"],
client_secret=os.environ["OIDC_CLIENT_SECRET"],
server_metadata_url=os.environ["OIDC_ISSUER_URL"] + ".well-known/openid-configuration",
client_kwargs={"scope": "openid email profile groups"},
)
@app.get("/login")
async def login(request: Request):
return await oauth.authentik.authorize_redirect(
request, request.url_for("auth_callback"))
@app.get("/auth/callback")
async def auth_callback(request: Request):
token = await oauth.authentik.authorize_access_token(request)
user = token["userinfo"]
request.session["user"] = {
"sub": user["sub"],
"email": user["email"],
"groups": user.get("groups", []),
# Custom ERP claims from F-05
"m3_roles": user.get("m3_roles", []),
"ar_access_level": user.get("ar_access_level", "none"),
}
return RedirectResponse(url="/")
async def require_auth(request: Request) -> dict:
user = request.session.get("user")
if not user:
raise HTTPException(status_code=401)
return user
@app.get("/dashboard")
async def dashboard(user: dict = Depends(require_auth)):
return {"hello": user["email"], "groups": user["groups"]}
# pip install mozilla-django-oidc
# settings.py
INSTALLED_APPS += ["mozilla_django_oidc"]
AUTHENTICATION_BACKENDS = ["myapp.auth.AuthtikBackend"]
OIDC_RP_CLIENT_ID = os.environ["OIDC_CLIENT_ID"]
OIDC_RP_CLIENT_SECRET = os.environ["OIDC_CLIENT_SECRET"]
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_OP_JWKS_ENDPOINT = ISSUER + "jwks/"
OIDC_OP_AUTHORIZATION_ENDPOINT = ISSUER + "authorize/"
OIDC_OP_TOKEN_ENDPOINT = ISSUER + "token/"
OIDC_OP_USER_ENDPOINT = ISSUER + "userinfo/"
OIDC_RP_SCOPES = "openid email profile groups"
LOGIN_URL = "/oidc/authenticate/"
LOGIN_REDIRECT_URL = "/"
# urls.py
urlpatterns = [path("oidc/", include("mozilla_django_oidc.urls")), ...]
# auth.py — sync AD groups + custom ERP claims to local user
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from django.contrib.auth.models import Group
class AuthtikBackend(OIDCAuthenticationBackend):
def update_user(self, user, claims):
user.groups.clear()
for name in claims.get("groups", []):
group, _ = Group.objects.get_or_create(name=name)
user.groups.add(group)
profile = user.profile
profile.m3_roles = claims.get("m3_roles", [])
profile.ar_access_level = claims.get("ar_access_level", "none")
profile.save()
return super().update_user(user, claims)
# views.py
from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
return render(request, "dashboard.html", {"user": request.user})
// npm install openid-client express-session
const { Issuer, generators } = require("openid-client");
const session = require("express-session");
let client;
async function initOIDC() {
const issuer = await Issuer.discover(process.env.OIDC_ISSUER_URL);
client = new issuer.Client({
client_id: process.env.OIDC_CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET,
redirect_uris: ["https://yourapp.example.com/auth/callback"],
response_types: ["code"],
});
}
function requireAuth(req, res, next) {
if (!req.session.user) return res.redirect("/login");
next();
}
app.get("/login", (req, res) => {
req.session.state = generators.state();
req.session.nonce = generators.nonce();
res.redirect(client.authorizationUrl({
scope: "openid email profile groups",
state: req.session.state,
nonce: req.session.nonce,
}));
});
app.get("/auth/callback", async (req, res) => {
const params = client.callbackParams(req);
const tokenSet = await client.callback(
"https://yourapp.example.com/auth/callback", params,
{ state: req.session.state, nonce: req.session.nonce }
);
const claims = tokenSet.claims();
req.session.user = {
sub: claims.sub,
email: claims.email,
groups: claims.groups || [],
// Custom ERP claims from F-05
m3Roles: claims.m3_roles || [],
arAccessLevel: claims.ar_access_level || "none",
};
res.redirect("/");
});
app.get("/dashboard", requireAuth, (req, res) => res.json(req.session.user));
// composer require league/oauth2-client
<?php
use League\OAuth2\Client\Provider\GenericProvider;
session_start();
$issuer = getenv("OIDC_ISSUER_URL");
$provider = new GenericProvider([
"clientId" => getenv("OIDC_CLIENT_ID"),
"clientSecret" => getenv("OIDC_CLIENT_SECRET"),
"redirectUri" => "https://yourapp.example.com/callback.php",
"urlAuthorize" => $issuer . "authorize/",
"urlAccessToken" => $issuer . "token/",
"urlResourceOwnerDetails" => $issuer . "userinfo/",
"scopes" => "openid email profile groups",
]);
// login.php — start the dance
if (!isset($_GET["code"])) {
$_SESSION["oauth2state"] = $provider->getState();
header("Location: " . $provider->getAuthorizationUrl());
exit;
}
// callback.php — exchange code for token
if ($_GET["state"] !== $_SESSION["oauth2state"]) exit("CSRF");
$token = $provider->getAccessToken("authorization_code", ["code" => $_GET["code"]]);
$user = $provider->getResourceOwner($token)->toArray();
$_SESSION["user"] = [
"sub" => $user["sub"],
"email" => $user["email"],
"groups" => $user["groups"] ?? [],
// Custom ERP claims from F-05
"m3_roles" => $user["m3_roles"] ?? [],
"ar_access_level" => $user["ar_access_level"] ?? "none",
];
header("Location: /index.php");
That's the whole contract between your app and simpliSSO. Vendor provides the client_id / client_secret pair after Authentik setup; you generate a random session secret; the issuer URL stays constant across all apps.
# .env — never commit to source control
OIDC_CLIENT_ID = "your-app-client-id"
OIDC_CLIENT_SECRET = "your-app-client-secret"
SESSION_SECRET = "random-256-bit-string"
OIDC_ISSUER_URL = "https://sso.example.com/application/o/<app-slug>/"
12 independently scoped modules. Add or remove any before signing for a proportional adjustment. Mandatory baseline is Infrastructure + Core SSO (F-01 · F-02 · F-03). Pricing is quoted per deployment — talk to us for a fixed-price proposal.
Payment is tied to verified deliverables, not calendar dates.
Phased rollout — federation and core SSO first, then custom integrations, with testing and handover compressed at the end.
Authentik install, MS365 federation, SSL termination, admin portal configuration.
All 24 apps configured as OIDC or SAML2 Applications in Authentik. Group-based access policies.
Custom JWT claims, step-up MFA policy, Infor M3 SAML2 workaround.
Thai language CSS, company branding on all auth screens, mobile responsive verification.
App portal tile API, WhatsApp account linking, 3CX extension linking.
End-to-end QA, user acceptance session, documentation, go-live.
Optional after go-live. 30-day written notice to cancel. Pick the tier that matches your operational tempo.
The identity debt, audit pain, and access-sprawl risks that drove this product — written up on Simplico's blog.
Most engineering orgs lack centralized identity management, leaving them exposed to access sprawl and audit failures. A platform-grade SSO layer kills this identity debt — and lifts security posture, dev productivity, and enterprise sales velocity along with it.
Authentication scattered across dozens of fragmented systems means ghost accounts, offboarding gaps, and silent breach paths. SSO with a centralized IdP collapses 24 attack surfaces into one — and gives you a single switch to flip when someone leaves.
Tell us how many apps, which protocols, and whether you're on MS365 or another upstream IdP. We'll scope a fixed-price proposal in a single call.