Ory Hydra Cheat Sheet
Overview
Ory Hydra is an open-source, OpenID Connect Certified OAuth 2.0 server and OpenID Connect provider built in Go. Unlike full identity platforms, Hydra focuses exclusively on the OAuth 2.0 and OIDC protocol layer, delegating user authentication to your existing login system through a consent and login flow architecture. This separation allows maximum flexibility in how you manage user identities.
Hydra implements OAuth 2.0 Authorization Code, Implicit, Client Credentials, and Refresh Token grants, as well as OpenID Connect Core 1.0. It is designed for cloud-native deployments with support for PostgreSQL, MySQL, and CockroachDB, horizontal scaling, and Kubernetes-native operation via the Ory Network or self-hosting.
Installation
# macOS
brew install ory/tap/hydra
# Linux
bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -b /usr/local/bin hydra
# Docker
docker pull oryd/hydra
# Docker Compose quickstart
git clone https://github.com/ory/hydra.git
cd hydra
docker compose -f quickstart.yml up -d
# Go install
go install github.com/ory/hydra/v2@latest
# Verify
hydra version
Core Commands
| Command | Description |
|---|---|
hydra serve all | Start both public and admin servers |
hydra serve public | Start only the public server |
hydra serve admin | Start only the admin server |
hydra migrate sql | Run database migrations |
hydra create client | Register an OAuth2 client |
hydra delete client | Delete an OAuth2 client |
hydra list clients | List all registered clients |
hydra get client | Get client details |
hydra introspect token | Introspect an access token |
hydra revoke token | Revoke an access or refresh token |
hydra perform authorization-code | Test authorization code flow |
hydra perform client-credentials | Test client credentials flow |
Quick Start
# Start Hydra with in-memory database (development only)
hydra serve all --dev
# Start with PostgreSQL
export DSN="postgres://hydra:password@localhost:5432/hydra?sslmode=disable"
hydra migrate sql -e --yes
hydra serve all
# Using Docker
docker run -d \
--name hydra \
-p 4444:4444 \
-p 4445:4445 \
-e DSN="postgres://hydra:password@db:5432/hydra?sslmode=disable" \
-e URLS_SELF_ISSUER=https://auth.example.com \
-e URLS_LOGIN=https://login.example.com/login \
-e URLS_CONSENT=https://login.example.com/consent \
oryd/hydra serve all
Client Management
# Create a confidential client (server-side apps)
hydra create client \
--endpoint http://localhost:4445 \
--name "My Web App" \
--grant-type authorization_code,refresh_token \
--response-type code \
--scope openid,offline_access,email,profile \
--redirect-uri http://localhost:3000/callback \
--token-endpoint-auth-method client_secret_post
# Create a public client (SPA, mobile)
hydra create client \
--endpoint http://localhost:4445 \
--name "My SPA" \
--grant-type authorization_code,refresh_token \
--response-type code \
--scope openid,offline_access \
--redirect-uri http://localhost:3000/callback \
--token-endpoint-auth-method none
# Create a machine-to-machine client
hydra create client \
--endpoint http://localhost:4445 \
--name "API Service" \
--grant-type client_credentials \
--scope api.read,api.write \
--audience https://api.example.com
# List clients
hydra list clients --endpoint http://localhost:4445
# Delete client
hydra delete client CLIENT_ID --endpoint http://localhost:4445
Testing Flows
# Test Authorization Code flow
hydra perform authorization-code \
--endpoint http://localhost:4444 \
--client-id CLIENT_ID \
--client-secret CLIENT_SECRET \
--scope openid,offline_access \
--redirect http://localhost:5555/callback
# Test Client Credentials flow
hydra perform client-credentials \
--endpoint http://localhost:4444 \
--client-id CLIENT_ID \
--client-secret CLIENT_SECRET \
--scope api.read
# Introspect a token
hydra introspect token \
--endpoint http://localhost:4445 \
ACCESS_TOKEN
# Revoke a token
hydra revoke token \
--endpoint http://localhost:4444 \
--client-id CLIENT_ID \
--client-secret CLIENT_SECRET \
ACCESS_TOKEN
Login and Consent Flow
Login Provider (Node.js)
const express = require("express");
const axios = require("axios");
const app = express();
const HYDRA_ADMIN = "http://localhost:4445";
// Login endpoint - Hydra redirects here
app.get("/login", async (req, res) => {
const challenge = req.query.login_challenge;
// Get login request from Hydra
const { data } = await axios.get(
`${HYDRA_ADMIN}/admin/oauth2/auth/requests/login?login_challenge=${challenge}`
);
// Skip if already authenticated
if (data.skip) {
const { data: accept } = await axios.put(
`${HYDRA_ADMIN}/admin/oauth2/auth/requests/login/accept?login_challenge=${challenge}`,
{ subject: data.subject }
);
return res.redirect(accept.redirect_to);
}
// Show login form
res.render("login", { challenge });
});
// Handle login form submission
app.post("/login", async (req, res) => {
const { challenge, email, password } = req.body;
// Authenticate user against your database
const user = await authenticateUser(email, password);
if (!user) {
return res.render("login", { error: "Invalid credentials", challenge });
}
// Accept login request
const { data } = await axios.put(
`${HYDRA_ADMIN}/admin/oauth2/auth/requests/login/accept?login_challenge=${challenge}`,
{
subject: user.id,
remember: true,
remember_for: 3600,
}
);
res.redirect(data.redirect_to);
});
// Consent endpoint
app.get("/consent", async (req, res) => {
const challenge = req.query.consent_challenge;
const { data } = await axios.get(
`${HYDRA_ADMIN}/admin/oauth2/auth/requests/consent?consent_challenge=${challenge}`
);
// Auto-accept for trusted first-party apps
if (data.skip || data.client.metadata?.trusted) {
const { data: accept } = await axios.put(
`${HYDRA_ADMIN}/admin/oauth2/auth/requests/consent/accept?consent_challenge=${challenge}`,
{
grant_scope: data.requested_scope,
grant_access_token_audience: data.requested_access_token_audience,
session: {
id_token: { email: data.subject },
},
}
);
return res.redirect(accept.redirect_to);
}
res.render("consent", { challenge, scopes: data.requested_scope });
});
Admin API
# List OAuth2 clients
curl http://localhost:4445/admin/clients
# Get login request
curl "http://localhost:4445/admin/oauth2/auth/requests/login?login_challenge=CHALLENGE"
# Accept login request
curl -X PUT "http://localhost:4445/admin/oauth2/auth/requests/login/accept?login_challenge=CHALLENGE" \
-H "Content-Type: application/json" \
-d '{"subject": "user-123", "remember": true}'
# Flush expired tokens
curl -X POST http://localhost:4445/admin/oauth2/flush \
-H "Content-Type: application/json" \
-d '{"notAfter": "2024-01-01T00:00:00Z"}'
Configuration
# hydra.yml
serve:
public:
port: 4444
cors:
enabled: true
allowed_origins: ["https://app.example.com"]
admin:
port: 4445
urls:
self:
issuer: https://auth.example.com
login: https://login.example.com/login
consent: https://login.example.com/consent
logout: https://login.example.com/logout
dsn: postgres://hydra:password@db:5432/hydra?sslmode=require
ttl:
access_token: 1h
refresh_token: 720h
id_token: 1h
auth_code: 10m
login_consent_request: 30m
oauth2:
pkce:
enforced: true
session:
encrypt_at_rest: true
secrets:
system:
- "your-system-secret-minimum-32-chars"
# Environment variables
export DSN="postgres://hydra:password@localhost:5432/hydra"
export URLS_SELF_ISSUER="https://auth.example.com"
export URLS_LOGIN="https://login.example.com/login"
export URLS_CONSENT="https://login.example.com/consent"
export SECRETS_SYSTEM="your-system-secret-minimum-32-chars"
export SERVE_PUBLIC_PORT=4444
export SERVE_ADMIN_PORT=4445
Advanced Usage
JWKS Key Rotation
# Create a new JSON Web Key Set
hydra create jwks hydra.openid.id-token \
--endpoint http://localhost:4445 \
--alg RS256 \
--use sig
Token Hook
# hydra.yml - webhook for token customization
oauth2:
token_hook:
url: https://api.example.com/token-hook
Troubleshooting
| Issue | Solution |
|---|---|
Unable to connect to database | Verify DSN format and database accessibility |
| Login loop | Ensure login provider accepts and redirects the challenge correctly |
| CORS errors | Enable CORS in serve.public.cors configuration |
| Token not valid | Check urls.self.issuer matches the token issuer |
| PKCE required error | Use PKCE with public clients; set oauth2.pkce.enforced |
| Consent screen loop | Ensure consent provider handles skip parameter |
| Admin API 401 | Admin API is not authenticated by default; secure it via network policies |
| Key rotation issues | Ensure new keys are created before revoking old ones |