Architecture
Big picture
Dex is one Go binary with three top-level parts. The server (server/) is the OIDC and OAuth2 authorization server: it registers the HTTP routes, issues tokens, manages signing keys, and serves discovery. The connectors (connector/) are the federation strategies, one per upstream identity provider. The storage (storage/) is a pluggable state layer that holds request state, issued codes and tokens, and client registrations.
A downstream application only ever talks to the server over OIDC. When a user needs to log in, the server hands off to a connector, the connector talks to the upstream provider, and the identity that comes back is normalised into a common shape. The sequence below is the authorization code flow with an upstream OIDC provider.
Components
Server (server/)
The authorization server owns the HTTP surface. Routes are registered in server/server.go, including /auth, /auth/{connector}, /callback, /callback/{connector}, /approval, /token, /keys, /userinfo, and /.well-known/openid-configuration (server/server.go:526-556). It also serves an administrative gRPC API. The bulk of the login and token logic lives in server/handlers.go and server/oauth2.go.
Connectors (connector/)
A connector encapsulates how to authenticate against one kind of upstream. The interfaces are defined in connector/connector.go. There are 16 implementations under connector/: ldap, github, gitlab, google, microsoft, oidc, oauth, saml, openshift, keystone, linkedin, bitbucketcloud, gitea, atlassiancrowd, authproxy, and mock. Whatever the upstream, a connector returns a normalised connector.Identity (connector/connector.go:38) carrying the user ID, username, email, and groups.
Storage (storage/)
Storage is an interface, storage.Storage (storage/storage.go:78), implemented four ways: memory, sql (via the ent ORM, backing SQLite, Postgres, and MySQL), etcd, and kubernetes (custom resources). It persists the flow state and issued artifacts: AuthRequest, AuthCode, RefreshToken, Client, and Connector configs.
How a request flows
The authorization code flow, from the downstream app's first request to the issued token:
GET /authlands inhandleAuthorization(server/handlers.go:167). It looks up the client byclient_id, lists connectors, and filters them by the client's allowed set. If exactly one connector applies, it redirects straight to/auth/{connector}(server/handlers.go:229).GET /auth/{connector}lands inhandleConnectorLogin(server/handlers.go:346). It parses the authorization request, verifies the connector is permitted, setsauthReq.ConnectorID, and saves theAuthRequestto storage. It then calls the connector'sLoginURL()and redirects the browser to the upstream provider.- The upstream login URL is built by the connector. For the OIDC connector,
LoginURL(connector/oidc/oidc.go:442) callsoauth2Config.AuthCodeURL(state, ...), putting Dex'sAuthRequestID intostate, and adds PKCE parameters (Proof Key for Code Exchange, RFC 7636, which binds the authorization code to the client that started the flow). GET /callback/{connector}lands inhandleConnectorCallback(server/handlers.go:701). It restores theAuthRequestfromstate, then type-switches on the connector kind and callsCallbackConnector.HandleCallback(server/handlers.go:760).HandleCallback(connector/oidc/oidc.go:499) exchanges the upstream code for tokens and callscreateIdentity(connector/oidc/oidc.go:559), which verifies the upstream ID Token and returns a normalisedconnector.Identity.finalizeLogin(server/handlers.go:814) copies the identity intostorage.Claimsand marks theAuthRequestlogged in. If offline access was requested and the connector can refresh, it creates or updates anOfflineSession(server/handlers.go:852).- Approval and code issuance.
handleApproval(server/handlers.go:960) shows the consent screen unless it is skipped, thensendCodeResponse(server/handlers.go:1054) issues astorage.AuthCode, deletes theAuthRequest, and redirects to the client with?code=...&state=.... POST /tokenlands inhandleToken(server/handlers.go:1261), which for the authorization-code grant callshandleAuthCode(server/handlers.go:1312). It loads and validates the code, runs PKCE verification, thenexchangeAuthCode(server/handlers.go:1372) mints the access token and ID Token and deletes the one-time code.
The Internals page walks the token endpoint, PKCE, and ID Token signing in detail.
Key design decisions
- Delegation over storage. Dex does not own user identities. It presents one OIDC face downstream and pushes the real authentication up to a connector, so an application implements OIDC once and Dex absorbs every upstream protocol.
- Capability by interface. A connector implements only the interfaces for what it can do:
PasswordConnectorfor direct username and password (connector/connector.go:58),CallbackConnectorfor OAuth2 redirect flows (connector/connector.go:65),SAMLConnectorfor SAML POST binding (connector/connector.go:91),RefreshConnectorfor refreshing claims (connector/connector.go:109). The server discovers each capability with a type assertion. - Storage is swappable, including Kubernetes. Because state hides behind
storage.Storage(storage/storage.go:78), Dex can run cluster-native on CRDs with no separate database, which is why embedded deployments favour it. - Upstream tokens never leak downstream. The
ConnectorDatafield on an identity holds upstream tokens and is kept inside storage; it is never handed to the end user or the downstream OAuth client (connector/connector.go:47-51).
Extension points
The connector interfaces in connector/connector.go are the primary extension surface: a new upstream is a new type that implements PasswordConnector or CallbackConnector. The storage layer is likewise pluggable through storage.Storage. Operational control is exposed through the gRPC admin API in api/api.proto, which lets an external system create and manage clients, passwords, and connectors at runtime.
Sources
- Source read at commit
17a54e9(v2.45.0 plus 248 commits): paths above are relative to the repository root. - Dex documentation