Identity · Arbisoft
Native authentication for a DevLake based product
Authentication built into the product: an access directory with roles, encrypted multi provider OIDC and opt in local passwords, all kept in sync with Grafana.
Key decisions
- Replaced the OAuth proxy with native auth once customer admins needed to manage their own users at runtime.
- Gave interactive access its own directory instead of reusing DevLake's table of imported engineering users.
- Moved OIDC providers into the database with AES-GCM encrypted secrets, and only activate a change once Grafana has accepted it.
- Made local passwords opt in, with a generated one time admin password per deployment instead of a default login.
- Role
- Built the auth system across the Go backend, the React Config UI, Grafana config and database migrations, through to the production rollout.
- When
- Aug to Sep 2026
- Links
- Stack
- Go, Gin, GORM, React, TypeScript, MySQL, PostgreSQL, Grafana, OIDC, Argon2id, Playwright

Arbisoft is building a commercial engineering analytics product on top of Apache DevLake. DevLake ships a Config UI and Grafana but no real user model, so the first deployment sat behind an OAuth proxy with an email allowlist. That was fine for one internal team but not for a product where every customer brings their own identity provider and manages their own people.
Context
This is product work in Arbisoft's fork of DevLake, not an upstream contribution. The fork adds customer facing features on top of the open source project and has to keep rebasing onto new DevLake releases, so anything I added needed clean seams: its own packages, its own migrations and as little patching of upstream code as possible.
Two systems need to agree on who you are. The Config UI and API are ours. Grafana, where the dashboards live, has its own login, its own users and a fixed set of OAuth slots. Any change to identity has to land on both sides or neither.
My first recommendation was actually to leave the proxy alone and just make the UI aware of it: route logout through the proxy, add a signed out page and stop pointing people at a native login that was deliberately disabled. That held until the product needed customer admins to add and remove people at runtime. Access was an allowlist file on the staging VM and a ConfigMap in production, and neither could do that. So the proxy had to go.
An access directory
DevLake's own users table holds imported engineering data, people pulled from GitHub or Jira. Reusing it for logins would have mixed two very different ideas of a user, so interactive access got a separate directory: people, allowed email domains and two roles, customer admin and member.
The first admin comes from provisioning. A bootstrap claim row makes that step idempotent across replicas and stops a later config change from quietly creating a second admin. Admin actions go to an audit log that the access page shows directly.

Providers in the database
OIDC providers moved out of environment variables and into the database so a customer admin can add, edit, disable and retire them from the UI without a redeploy.
Client secrets are encrypted with AES-GCM behind a small keyring interface. Every ciphertext carries a key ID and is bound to its provider record as associated data, so a secret copied onto another row simply fails to decrypt. Keys can rotate with the previous key kept for reads, and the interface is shaped so a KMS adapter can replace the local keys later without touching auth flows.
Multiple providers and Grafana
A person can link more than one provider to the same account, and a local user can link an SSO identity from their own session. The fiddly part was Grafana. It only has fixed sign in slots like Google and generic OAuth, so each DevLake provider can claim at most one slot or be marked DevLake only, in which case Grafana falls back to its ordinary login.
Sharing one login between the two wasn't realistic. Grafana keeps its users and sessions in its own SQLite database inside its container, or on a volume in the Kubernetes deployment, so DevLake can't own that state. Keeping them separate had its own problem though: admins would have to set up the same identity provider twice. So DevLake sets up Grafana for them through Grafana's SSO settings API, authenticated with a Basic Auth admin credential. That works on Grafana OSS, Enterprise and Cloud alike. A service account token couldn't do it on OSS, because the fine grained permission for managing OAuth provider settings only exists on Enterprise and Cloud.
Provider changes are staged, pushed to Grafana with a machine credential and only then activated. If Grafana is unreachable the change stays saved and shows a retryable sync state instead of leaving the two systems half applied.
An admin saves a provider change, which is stored encrypted and staged. DevLake then syncs the change to Grafana with a machine credential. If Grafana accepts it, the provider becomes active. If Grafana is unreachable, the change stays saved in a retryable state and the admin can retry the sync.


Not an SSRF endpoint
An issuer URL is admin input that the server then fetches for discovery, JWKS and token exchange. Left alone, that's a classic SSRF hole.
Issuer and redirect URLs must be HTTPS and can't be literal private addresses. More importantly, the HTTP client used for every OIDC call resolves hostnames itself and refuses to dial loopback, private, link local, CGNAT or reserved ranges. Checking at dial time matters because a hostname can resolve to something public when you validate it and to an internal address a second later. Redirects are capped and go through the same check.
Local passwords
Some customers have no identity provider at all. Local auth is opt in, and new customer deployments are local first: provisioning generates a unique one time admin password per deployment instead of anything like admin/admin, and the admin can add OIDC later. Existing deployments are never silently converted.
- Passwords are hashed with Argon2id. Parameters live in the hash and are bounded on read, so a tampered hash or a bad config can't turn login into a denial of service. Concurrent hashing is capped as well.
- Failed attempts count against two HMAC keyed buckets, one for the login name and one for the client IP. The database stores counts and leases, never raw usernames or IPs.
- Five failures inside 15 minutes locks the bucket for 15 minutes. Attempts are reserved atomically so parallel requests can't sneak past the limit, which a regression test against a disposable MySQL instance checks.
- Unknown usernames still run a dummy hash, so response time doesn't reveal which accounts exist.
- Admin issued passwords are temporary and force a change at first sign in. Reset, disable and remove all revoke that person's sessions, and an admin can't remove someone's last way to sign in.

Testing it
Auth bugs tend to live between systems, so most of the confidence came from end to end tests against the real stack. 27 Playwright checks cover the login page, generic errors that don't enumerate accounts, throttling returning 429, the full first sign in and forced password change flow, session revocation on reset and disable, the member and admin privilege boundary, header spoofing and linking an OIDC identity from a local session.
Go unit and integration tests cover provider lifecycle transitions, Grafana sync failures, encryption and the throttle. A rebase matrix spec reruns the key flows after pulling in upstream DevLake changes.
How the work was run
The auth work shipped as a series of stacked pull requests over about a month, with a written trail for every stage.
- Each stage started from an implementation plan with explicit scope, non goals and the upstream coupling it was allowed to add.
- Each phase had an eval script that ran the Go tests and the Playwright suite against a real local stack. It was the gate for every change, whether I wrote it by hand or with an AI coding agent: make the change, run the eval and keep going until it passes. Only then did it go up for review.
- Every PR went through human review from teammates. I also ran AI review passes on top of that to catch edge cases early, but they were a supplement to human review, not a replacement for it.
- Findings from both were classified before touching code: fix it on the base PR, already superseded by a later PR in the stack, real plumbing a later PR depends on or a base bug the stack inherits. That kept stacked PRs from turning into a mess of duplicate fixes.
- Non blocking issues went into a technical debt register with why each was deferred and what would trigger a revisit.
- Some review suggestions were turned down on purpose and the reasoning is written down. An in memory repository fake, for example, can't verify MySQL unique key locking, which is exactly what the bootstrap and invitation flows rely on.
- Each stage ended with a test report against a rebuilt local stack before it went to staging.
Outcome
The access directory, database backed OIDC and local auth went through staging and into production in September 2026. Customer admins can now manage people, domains and identity providers themselves, and Grafana follows along.
Related work
Both were built for the same DevLake based product. Telemetry credentials are managed by the same admins the auth work introduced.
