Open source Auth0/Clerk/Firebase alternative. Passkeys, SSO, MFA, passwordless, biometric login. Self-hosted or cloud. Enterprise-ready for SaaS & mobile apps
Authgear solves the "build your own auth" trap: every SaaS product needs signup, login, MFA, SSO, and session management, yet implementing these correctly is security-sensitive and time-consuming. Rolling your own means owning OIDC/OAuth 2.0/SAML compliance, passkey protocols, brute-force protection, and audit logging — work that distracts from the product.
What This Does
Authgear is an open-source, self-hostable authentication server with a pre-built login UI (authui/), a GraphQL admin API (pkg/lib/admin/), and a management portal (portal/). It supports passwordless login (magic links, OTP), passkeys, biometric login, TOTP/SMS/email 2FA, OIDC/OAuth 2.0/SAML SSO, and enterprise features like RBAC, audit logs, and rate limiting.
The repository is a monorepo of 13 self-contained projects. The core server (pkg/, 2,664 files) is Go; the portal and auth UI are React/TypeScript. The cmd/authgear/ directory contains the entry point, and e2e/ holds end-to-end tests. It was forked from the 1,999-star authgear/authgear-server project.
How It Is Wired
Execution starts at cmd/authgear/main.go, which loads configuration and wires dependencies via wire.go / wire_gen.go (compile-time dependency injection). The server exposes three APIs: the public auth API, the admin GraphQL API (cmd/authgear/adminapi/), and the portal API. The admin API routes through pkg/lib/admin/graphql/, which resolves mutations and queries against the database and Redis.
The portal/ frontend (React + Tailwind) calls the admin GraphQL API to manage users, authentication flows, and tenant settings. The authui/ directory contains the pre-built login pages, styled with Tailwind and served by the Go server. The pkg/lib/authn/ package handles the actual authentication logic — passkeys, OTP, SSO — and writes sessions to Redis and user records to the database. The pkg/lib/oauth/ package implements the OIDC/OAuth 2.0 endpoints.
The widest blast radius sits in pkg/lib/authn/ and pkg/lib/oauth/: every authentication flow routes through these packages, and a change there affects all 13 projects. The module graph shows a hub around pkg/lib/ with cycles between authn, oauth, and session — modifying one often requires touching the others.
How To Use It
Setup: Build the Docker image from cmd/authgear/Dockerfile and run via docker compose. The Makefile has make build and make test targets.
Configuration: Copy .env.example to .env and set database and Redis connection strings. Secrets go in authgear.secrets.yaml (see hack/custom-resources/ for a template).
A SaaS company self-hosts Authgear behind their API gateway. Users hit the pre-built login page (authui/), authenticate via passkey or OTP, and receive an OIDC token. The company's backend validates the token via the OIDC discovery endpoint, while the admin portal (portal/) manages users, enforces MFA, and reviews audit logs. The GraphQL admin API (pkg/lib/admin/graphql/) lets the company programmatically provision tenants and sync users with their CRM.
High – e2e/.env is tracked despite .gitignore excluding it; credentials may be live. Fix: git rm --cached, rotate, keep ignore rule.
High – GitHub Actions use oursky/action-gh-release@v2 (tag-pinned). Fix: pin to commit SHA.
High – Committed .env in e2e/. Fix: remove, rotate, add .env.example.
Medium – 3 workflows declare no permissions for GITHUB_TOKEN. Fix: add contents: read.
Medium – No Dependabot/Renovate despite 10 manifests. Fix: add .github/dependabot.yml.
Medium – Base images (node:20.19.5-bookworm, quay.io/theauthgear/golang:1.26.2-noble) not digest-pinned. Fix: use image@sha256:<digest>.
Medium – No pre-commit secret gate. Fix: add hook scanning staged content.
Medium – Large blobs tracked: cldr-common-44.0.zip (30.3MB), GeoLite2-Country.mmdb (9.1MB). Fix: use Git LFS.
Medium – persist-credentials: false not set on checkout in run-checks.yaml. Fix: add it.
Medium – No non-root USER in cmd/authgear/Dockerfile. Fix: add unprivileged user.
The Bottom Line
Authgear is a serious, production-grade authentication server with broad protocol support and a real admin portal — the Go core is well-structured and the test suite (961 files) is substantial. The committed secrets and unpinned CI actions are the main concerns; fix those before production. It's appropriate for teams needing a self-hosted Auth0/Clerk alternative with enterprise features and who can maintain a Go codebase.
What the analyser found
Deployment readiness
6/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✗No committed secrets
Composition
6,315 files
Go2888
HTML467
TypeScript432
TSX373
YAML288
CSS274
ReactTailwindDockerKubernetesGraphQLVector store
Findings
LowSecret-shaped paths present; the code-health audit confirms or clears them e2e/.env, e2e/var/authgear.secrets.yaml, hack/custom-resources/authgear.secrets.yaml
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
high3
medium7
low2
Untrack the file your own .gitignore says to ignorehigh
e2e/.env
tracked although .gitignore excludes it
The author already decided this file must never be committed, so its presence is an accident nobody noticed, which means the credentials in it are the ones actually in use.
Fix: git rm --cached the path and rotate; the ignore rule is already correct.
Pin third-party GitHub Actions to a commit SHAhigh
.github/workflows
oursky/action-gh-release@v2
A tag can be moved, so the action running with your token and secrets is whatever its owner last pushed; this is how tj-actions/changed-files leaked secrets from thousands of repos.
Fix: Replace each @vN with the 40-character commit SHA, keep # vN as a comment, and let Dependabot bump the SHAs.
Remove the committed .env and rotate what it holdshigh
e2e/.env
e2e/.env
A tracked .env is the most common route for a working key to reach a public clone, and it is the file the app actually loads, so the value is usually live.
Fix: git rm --cached the file, add it to .gitignore, rotate every credential it names, commit a .env.example with empty values.
Declare least-privilege permissions for GITHUB_TOKENmedium3 occurrences
.github/workflows/run-builds.yaml
3 workflow(s) declare no permissions, 1 of them reference secrets
With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI.
Fix: Add permissions: contents: read at the top of the workflow and widen per job only where needed.
Enable Dependabot or Renovatemedium
10 manifest(s), no update bot configured
Without a bot a published advisory sits unpatched until someone audits by hand, which across 1,322 repositories means never.
Fix: Commit .github/dependabot.yml covering the repo ecosystems plus github-actions.
Pin the container base image by digestmedium3 occurrences
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Add a pre-commit secret gatemedium
a secret-shaped file is tracked and no repo-level gate is visible
Without a gate the same class of leak recurs on the next commit, so a leak finding is the symptom and this is the cause.
Fix: Add a pre-commit hook scanning staged content, and enable push protection.
Move large binaries to Git LFS or out of the repositorymedium3 occurrences
cldr-common-44.0.zip
3 blobs over 5MB: cldr-common-44.0.zip 30.3MB, GeoLite2-Country.mmdb 9.1MB, material-symbols-outlined.ttf 8.4MB
One repository here carries twenty blobs over 5MB including a 10.9MB spreadsheet, so every clone and every CI checkout pays for data nobody diffs.
Fix: Track those extensions with LFS, or move datasets to object storage and fetch them in a setup step.
Set persist-credentials: false on checkoutmedium
.github/workflows/run-checks.yaml
checkout keeps the token, then dependencies are installed
The token stays in .git/config for every later step, so a malicious postinstall script reads a pushable credential without one ever being passed to it.
Fix: Add with: persist-credentials: false, and pass an explicit token only to the step that pushes.
Add a non-root USER to the imagemedium
cmd/authgear/Dockerfile
CMD or ENTRYPOINT with no USER directive
A process running as root in the container is root against every mounted volume, and it turns any container escape or writable-mount mistake from a contained problem into a host one.
Fix: Create an unprivileged user, chown what it needs, and end the Dockerfile with USER.
Set timeout-minutes on the workflow jobslow4 occurrences
.github/workflows/custom-build.yaml
4 workflow(s) declare no job timeout
A wedged step runs to the six-hour platform default, which on a two-hourly schedule means three runs overlap behind it.
Fix: Add timeout-minutes with a realistic bound to each job.
Add the repository convention files this project lackslow3 occurrences
missing .editorconfig, .gitattributes, a formatter config
Without them one contributor's editor writes tabs into a Python file, a shell script commits with CRLF and fails in the container, and a notebook diff is unreviewable.
Fix: Add .editorconfig, .gitattributes with text=auto eol=lf, and a formatter config.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.