As companies grow, internal tools end up handling the most sensitive workflows. They need audit logs and alerting, tight integration with corporate Identity Providers (IdPs), group-aware permissions, and instant revocation when an employee leaves or a device is lost.
But, when you're building an internal analytics dashboard... you just want employees to log in with their company-provided account and stay logged in.
Instead you're knee-deep in the SCIM spec, trying to figure out if anyone ever really queries by arbitrary nested properties like the spec seems to allow (thankfully: no). Or worse, you're spending your time figuring out how to integrate with Keycloak, which somehow feels harder than integrating directly with your IdP.
Network controls help, but policy lives at the product layer. Let's look at how to make auth for internal tools easier to understand, audit, and extend.
A self-hosted auth sidecar for internal tools
What we want is a simple integration with our corporate IdP (Entra, Okta, OneLogin, etc.) that we can reuse across products. It should let us set per-app (and even per-role, per-group, or per-user) security policies, and give us escape hatches for the weird edge cases internal apps always have.
That's PropelAuth BYO: a self-hosted sidecar that handles SSO (the way employees login), SCIM provisioning (for automating access based on company directory), session management (with per-product security policies), and passkeys (for step-up security).

Why it works well for internal tools
PropelAuth BYO works well for internal tools thanks to a few key design decisions:
- Self-hosted - keep your data and logs fully in your control.
- Boring to operate - single container, one dependency (Postgres), built in Rust for performance.
- Ops tooling included - real-time session inspection/revocation, structured JSON audit logs for your SIEM, and a full API for automation.
- Deep IdP integrations - first-class support for OIDC SSO and SCIM provisioning with all major IdPs.
Here, for example, is a screenshot from built-in dashboard where we are drilling down into active sessions for a single IP across all our users and internal tools:

Here's another example where we are using the built-in dashboard to ensure that users sync'd over via SCIM have the right properties, meaning our internal tools can use a much simpler schema than the full SCIM spec:

Clean, developer-friendly SDKs
The dashboard makes it easy for your IT and security teams to audit and manage these integrations, but what about the developer experience of creating an internal tool?
And I know, I know, every devtool ever says they are easy to use. Instead of telling you, I'd like to show you a few code snippets, annotated with the important design decisions (we'll use TypeScript for these examples but the other SDKs have the same design principles).
This first one is your login function:
app.get("/auth/callback", async (req, res) => {
// Each BYO method is a single, focused action, scoped to a single feature.
const result = await auth.sso.completeOidcLogin({
stateFromCookie: req.cookies["stateFromCookie"],
// We parse what we need from the full
// URL (PKCE, CSRF, etc. handled under the hood)
callbackPathAndQueryParams: req.url,
});
// Every method returns a Rust-like Result type, so you get explicitly typed
// success and error states to handle.
if (result.ok) {
// result.data is well-typed and documented
} else if (result.error.type === "LoginBlockedByEmailAllowlist") {
// Each function has a set of error types that you can handle
// They are both in the docs and will auto-complete in your IDE
// Note: We do this differently for each language, to match conventions
}
});
This handles finishing the SSO flow, along with features like PKCE, CSRF protection, and blocking logins from unapproved email domains OR that have not been provisioned via SCIM. You can also see how difficult it would be to mess this up - we pass in the URL and the SDK handles all the tricky parts for you.
Here's another example, handling SCIM provisioning requests from your IdP. SCIM itself is a complicated protocol which handles user lifecycle events (provisioning, deprovisioning, updates), but here we've boiled it down to one wildcard route:
// You forward all SCIM requests to this endpoint, we return the appropriate response,
// but we also provide hooks to make sure you handle critical actions like provisioning/deprovisioning
app.use("/scim/*", async (req, res) => {
// Forward the request to BYO
const result = await auth.scim.scimRequest({
method: req.method,
pathAndQueryParams: req.url,
body: req.body,
scimApiKey: req.headers.authorization,
});
// Case 1: BYO gives you the exact response to return to the IdP
if (result.ok && result.data.status === "Completed") {
return res
.status(result.data.responseHttpCode)
.json(result.data.responseData);
}
// Case 2: Your IdP has sent a critical action (e.g. provision/deprovision user)
if (result.ok && result.data.status === "ActionRequired") {
// Your code to handle the action goes here
const response = await handleScimAction(result.data);
// Once handled, BYO provides the final response to return to the IdP
return res
.status(response.data.responseHttpCode)
.json(response.data.responseData);
}
// Case 3: An error occurred, in which case, BYO gives you the exact response to return to the IdP
res.status(result.error.statusToReturn).json(result.data.bodyToReturn);
});
This example is particularly powerful because it lets us easily add custom logic around critical actions like provisioning and deprovisioning users, while BYO handles all the SCIM protocol details for us.
Let's look at one more example, creating and validating a session after a successful SSO login:
const sessionResult = await auth.session.create({
userId: ssoUser.oidcUserId,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
tags: ["product:analytics-dashboard"],
});
// Later on...
const validateResult = await auth.session.validate({
sessionToken: req.cookies["sessionToken"],
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
});
This will automatically check for cases like a suspicious change in user agent, but possibly the most important part is the tags field. You can configure session policies based on these tags, allowing you to have different session lifetimes, inactivity timeouts, IP change rules, and more. Here's an example config where we've made our deploy-dashboard more strict than the default settings:
{
"defaults": {
"absolute_lifetime_secs": 1209600
},
"tags": [
{
// Our deploy-dashboard product needs stricter session policies
"tag": "product:deploy-dashboard",
"absolute_lifetime_secs": 14400, // 4 hours
"inactivity_timeout_secs": 900, // 15 minutes
"ip_allowlist": ["10.0.0.0/8"]
}
]
}
Sidecar model gives you full control
Internal apps are full of exceptions. With a managed, off-the-shelf auth provider, you end up wiring post-login hooks, fragile webhooks, or custom scripts to regain control.
If you look back on those BYO examples above, all of them can be easily extended with your own custom logic.
Let's say, as a precaution, you want to reject logins from anyone in the Contractor group to account for someone mistakenly being provisioned. Call the Get SCIM User function, check the groups, and block the login if needed.
Want to flag logins from new devices? Hook up our new device detection support and either send an alert or block the login entirely until it's approved.
With the sidecar model, nothing happens unless you say so. You have full control to add your own logic wherever you need it.
What's next?
Check out our getting started guide to see how easy it is to get up and running with PropelAuth BYO.
Summary
Internal tools end up touching your most sensitive workflows, so the right model is to centralize auth once and reuse it everywhere.
A self-hosted sidecar lets you integrate with your corporate IdP (Entra, Okta, OneLogin, etc.), handle OIDC SSO and SCIM, and enforce session policies without rewiring each app.
The SDKs keep the happy path simple and the sharp edges hard to reach, while the dashboard gives you real-time session visibility and SIEM-ready audit logs.
Most importantly, the sidecar keeps policy where it belongs - in your product - so you can add the exceptions and controls internal apps always need, ship faster, and keep security consistent across everything you run.


