Sign-in and authorization
How Kratos, Hydra, the identity service, jwtauth and authz fit together, and what exists today.
Every request to a Flow service answers two questions. Who is calling? is answered by a signed access token. May they do this? is answered by Cedar policies that the service evaluates itself. This page explains which piece does what. The guides after it show how to build on them:
Web apps
Sign a person in, get an access token, call services with it, and sign out.
Services
Verify tokens, check permissions with Cedar, and call other services as yourself.
The binding rules are in docs/architecture.md and the decisions docs/decisions/0013-jwt-validated-in-each-service.md and docs/decisions/0047-ory-for-authentication-identity-stays-first-party.md.
The pieces
| Piece | Where | What it does |
|---|---|---|
| Kratos | services/authn | Holds people's accounts: passwords, Google sign-in, the browser session. It proves who a person is. |
| Hydra | services/authn | The OAuth2 and OpenID Connect server. It issues short-lived JWT access tokens and publishes the keys that sign them. |
| Sign-in app (authn-app) | frontend/authn-app | The screens people see: sign in, register, and the login, consent and logout steps Hydra hands off to it. |
| identity service | services/identity | Holds every Principal: A person or a service that the identity service knows about, identified by its FID, with a kind (person or system) and a status (active or disabled). Having a principal grants nothing on its own; access comes from grants. and its Grant: A role held by a principal at a scope: global, one building, or one unit. Grants live in the identity service and are the only input to authorization besides the endpoint_access data that says which roles an action allows.s: which role it has, at which scope. Stores no personal data. |
| profile service | services/profile | Gives an app the signed-in person's own email and name, read from their Kratos identity when the app asks. Stores nothing. |
| User admin app (users-app) | frontend/users-app | Where administrators find people, manage roles and grants, and create and manage service accounts, under Clients. Served at /admin on the sign-in host. Signs a person in through Hydra as the public client flow-users-app, and calls identity with that person's own access token. |
common/jwtauth | common/jwtauth | Go middleware. Verifies a Hydra access token and puts the caller's FID: The id of a principal. For a person it is their Kratos identity id, a lowercase UUID; for a service it is its OAuth2 client_id. It arrives as the verified sub claim of a Hydra access token and becomes the actor on the request. on the request context. |
common/authz | common/authz | Go package. Evaluates the service's Cedar policies against the caller's grants. |
Two things are deliberately kept apart. Authentication, proving who someone is, belongs to Ory (Kratos and Hydra). Authorization data, the roles people hold, belongs to the identity service. A token never carries roles. A service reads roles from identity's data, so a revoked grant takes effect without waiting for a token to expire.
A token carries no personal data either: it names the person only by their
FID: The id of a principal. For a person it is their Kratos identity id, a lowercase UUID; for a service it is its OAuth2 client_id. It arrives as the verified sub claim of a Hydra access token and becomes the actor on the request.. Their email and name stay in Kratos, and an app that needs
them asks the profile service, GET /v1/me/profile, with the person's token.
The profile service reads them from Kratos on each request and keeps no copy.
How a request is authorized
A person signs in once, through the sign-in app. Their web app then holds an access token and sends it with every call:
Diagram source
sequenceDiagram
participant B as Browser (web app)
participant H as Hydra
participant A as Sign-in app
participant K as Kratos
participant S as Service
B->>H: /oauth2/auth (client_id, PKCE challenge, audience)
H->>A: /login?login_challenge=...
A->>K: sign in (password or Google)
K->>H: accept login, subject = Kratos identity id
H->>A: /consent?consent_challenge=...
A->>H: accept (first-party clients only)
H->>B: redirect with code
B->>H: /oauth2/token (code + PKCE verifier)
H->>B: access token (JWT) and ID token
B->>S: request with Authorization: Bearer <access token>
S->>S: jwtauth verifies the token, sub becomes the actor
S->>S: authz evaluates Cedar with the actor's grantsInside the service there are always two steps, in this order:
- Verify the token with
common/jwtauth. A missing, expired or forged token is a 401, and no handler runs. - Check the permission with
common/authz. A refusal is a 403.
A service never trusts a header that names the caller, and there is no development bypass. It trusts only a token whose signature it checked itself.
People and services are both principals
| Kind | FID | How it signs in |
|---|---|---|
person | The Kratos identity id, a lowercase UUID | Through a web or mobile app, with the authorization code flow |
system | The OAuth2 client_id: flow-sample-service for a client in the repository, svc- and a name for a service account made in users-app | With its own client secret, using the client_credentials grant |
A person's principal is created automatically when they register: Kratos calls a
webhook on the identity service. Having a principal grants nothing. Access comes
from grants such as staff@building:<uuid> or platform-admin@global:*, added
in the user admin app.
What exists today and what does not
The auth system is deployed to dev only. No existing service uses it yet, and each service adopts it as its own change.
| Built and on dev | Still to come |
|---|---|
| Kratos, Hydra and the sign-in app, including OAuth2 login, consent and logout | Production deployment |
The identity service: principals and grants, disabling a principal, the role registry with roles for people and roles for service accounts, the admin audit record, and GET /v1/authz/me, which tells an app what the caller may do | Hydra's admin port restricted to identity and the client bootstrap, which production needs first |
The profile service, GET /v1/me/profile | Renaming and deleting roles |
| The user admin app: people, grants, roles and the actions each role may do, and Clients, where service accounts are created, given roles, rotated, disabled and deleted | Existing services adopting common/jwtauth and common/authz |
| identity checking its own admin requests with Cedar and endpoint access, instead of trusting the user admin app's key alone | |
| Service accounts: identity creates them in Hydra, each with a system principal, and shows the secret once | |
common/jwtauth, common/authz, and common/principalcopy, the local copy of identity's principals and grants that is the database Store for common/authz | The legacy deepsky backend accepting Hydra tokens |
OAuth2 clients defined in the repository and applied on every deploy. The deploy changes only clients tagged managed_by: repo and prints a report of every other difference | Cookie names that say Flow rather than Ory |
The sample service, services/sample, is built and runs locally only. It uses all three packages, and the services guide follows its code. It is a reference to copy and is never deployed.
The sample web app, frontend/sample-app, is also built and runs locally only. It signs a person in through Hydra, shows who is signed in from the profile service, calls the sample service with the person's access token, and signs them out again, and the web apps guide follows its code.
The profile service, services/profile, is built and on dev.
The two sample clients, flow-sample-web and flow-sample-service, are
registered in Hydra on dev and locally. The sample web app signs people in
with the first. The second is a machine client that calls the sample service
with its own token.
A client that signs people in is always defined in the repository, where every change is reviewed. A machine client can be defined there too, or created as a service account in the user admin app, under Clients: see Building a service.