Skip to content

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

CommandDescription
hydra serve allStart both public and admin servers
hydra serve publicStart only the public server
hydra serve adminStart only the admin server
hydra migrate sqlRun database migrations
hydra create clientRegister an OAuth2 client
hydra delete clientDelete an OAuth2 client
hydra list clientsList all registered clients
hydra get clientGet client details
hydra introspect tokenIntrospect an access token
hydra revoke tokenRevoke an access or refresh token
hydra perform authorization-codeTest authorization code flow
hydra perform client-credentialsTest 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 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

IssueSolution
Unable to connect to databaseVerify DSN format and database accessibility
Login loopEnsure login provider accepts and redirects the challenge correctly
CORS errorsEnable CORS in serve.public.cors configuration
Token not validCheck urls.self.issuer matches the token issuer
PKCE required errorUse PKCE with public clients; set oauth2.pkce.enforced
Consent screen loopEnsure consent provider handles skip parameter
Admin API 401Admin API is not authenticated by default; secure it via network policies
Key rotation issuesEnsure new keys are created before revoking old ones