Building a Secure GraphQL API with Ory Kratos

no alter data available

Learn how to integrate Ory Kratos with a Flask-based GraphQL API for secure user authentication and session management.

Building a Secure GraphQL API with Ory Kratos

In this article, we'll walk through how to integrate Ory Kratos—a headless identity and authentication system—with a Flask-based GraphQL API. We'll cover user registration, login flows, session management, and discuss security best practices to ensure your application is both functional and secure.

A full working example is available here: wassimoo/ory-graphql

Prerequisites

Before we dive in, ensure you have the following:

  • A running instance of Ory Kratos (preferably version 0.8.0 or later).
  • Python 3.8+ installed.
  • Basic understanding of Flask and GraphQL.
  • Familiarity with Ory Kratos' self-service flows and session management.

What is Ory Kratos?

Ory Kratos is a cloud-native identity and user management system that provides a headless approach to authentication and authorization. Unlike traditional identity providers, Kratos doesn't come with a built-in UI—instead, it exposes APIs that allow you to build custom authentication flows that match your application's design.

Key features of Ory Kratos include:

  • Self-Service Flows: Users can register, login, recover passwords, and manage their accounts through API-driven flows
  • Session Management: Handles secure session creation, validation, and expiration
  • Multi-Factor Authentication: Supports various authentication methods including passwords, TOTP, and more
  • Identity Schemas: Flexible identity data models that you can customize for your needs
  • API-First: Designed to be consumed by any frontend or backend technology

In this tutorial, we'll use Kratos to handle user registration and authentication for our GraphQL API, demonstrating how to integrate it with a Flask application.

Basic Python GraphQL API

First, let's set up a basic Flask application with GraphQL support using Ariadne:

Install dependencies:

pip install Flask ariadne

Create server.py with a basic GraphQL query:

from flask import Flask, request, jsonify
from ariadne import QueryType, make_executable_schema, graphql_sync
from ariadne.explorer import ExplorerPlayground

app = Flask(__name__)
query = QueryType()

# Basic "Hello World" Query
@query.field("hello")
def resolve_hello(*_):
    return "Hello, world!"

schema = make_executable_schema("""
type Query {
    hello: String!
}
""", query)

@app.route("/graphql", methods=["GET", "POST"])
def graphql_endpoint():
    if request.method == "GET":
        return ExplorerPlayground().html(request)
    success, result = graphql_sync(schema, request.get_json(), context_value=request)
    return jsonify(result)

if __name__ == "__main__":
    app.run(port=8000)

Test it by visiting http://localhost:8000/graphql and running:

query {
  hello
}

GraphQL Playground - Hello World Query

Setting Up Ory Kratos

Download and run Kratos with Docker:

Create docker-compose.yml:

version: '3.8'
services:
  kratos:
    image: oryd/kratos:v1.2.0
    command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
    environment:
      - LOG_LEVEL=debug
      - DSN=memory
      - KRATOS_PUBLIC_URL=http://localhost:4433
      - KRATOS_ADMIN_URL=http://localhost:4434/admin
    ports:
      - '4433:4433'
      - '4434:4434'
    volumes:
      - ./kratos:/etc/config/kratos:ro

Create kratos/kratos.yml with minimal dev config:

log:
  level: debug

serve:
  public:
    base_url: http://localhost:4433/
  admin:
    base_url: http://localhost:4434/admin

selfservice:
  default_browser_return_url: http://localhost:4455/
  flows:
    login:
      ui_url: http://localhost:4455/login
    registration:
      ui_url: http://localhost:4455/registration
  methods:
    password:
      enabled: true

identity:
  default_schema_id: default
  schemas:
    - id: default
      url: file:///etc/config/kratos/identity.schema.json

secrets:
  default:
    - please-change-me-in-production

Some of what this configuration means

  • Public vs Admin URLs: serve.public.base_url is the client‑facing API (safe to expose). serve.admin.base_url is powerful and must stay private.
  • Self‑service UI endpoints: selfservice.flows.*.ui_url (and default_browser_return_url) point to your app’s pages that render login/registration and where users are redirected after.
  • Identity schema: identity.schemas defines your user model (traits). This example loads identity.schema.json and uses traits.email as the password identifier.
  • Storage: DSN=memory is for local/dev only. Use Postgres in real environments.

Now because the configuration points to an identity schema, we need to create it.

kratos/identity.schema.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://example.com/identity.schema.json",
  "title": "Default Identity Schema",
  "type": "object",
  "properties": {
    "traits": {
      "type": "object",
      "properties": {
        "email": {
          "type": "string",
          "format": "email",
          "ory.sh/kratos": {
            "credentials": {
              "password": {
                "identifier": true
              }
            }
          }
        }
      },
      "required": ["email"],
      "additionalProperties": false
    }
  },
  "required": ["traits"],
  "additionalProperties": false
}

This schema sets traits.email as the login identifier for the password method and disallows any extra, undeclared trait fields.

Test Kratos health endpoint:

curl http://localhost:4433/health/ready
# should return status: ok

Understanding Token Handling

In Ory Kratos, authentication is handled through session tokens. When a user successfully authenticates, Kratos returns a session_token that can be used to identify and authorize subsequent requests. This token-based approach provides several benefits:

  • Stateless Authentication: The token contains all necessary information to identify the user
  • Security: Tokens are cryptographically signed and can be validated without storing session data
  • Flexibility: Tokens can be passed in headers, cookies, or other mechanisms

The typical flow works like this:

  1. User provides credentials (email/password)
  2. Kratos validates the credentials
  3. If valid, Kratos creates a session and returns a session_token
  4. The client includes this token in subsequent requests via the X-Session-Token header
  5. Your API validates the token with Kratos to get user information

It's important to note that session_tokens are short-lived and should be refreshed periodically to maintain user sessions.

Adding User Registration to Our GraphQL API

Now that we understand how Ory Kratos handles authentication, let's integrate user registration into our GraphQL API. This will allow users to create accounts through our API, which will then be stored in Kratos.

First, we need to add HTTP request capabilities to communicate with Kratos:

pip install requests

Next, we'll extend our GraphQL server to include user registration functionality. We'll add a mutation that creates new user identities in Kratos:

mutation = MutationType()

KRATOS_ADMIN_URL = "http://localhost:4434/admin"

@mutation.field("registerUser")
def resolve_register_user(_, info, email, password):
    payload = {
        "schema_id": "default",
        "traits": {"email": email},
        "credentials": {"password": {"config": {"password": password}}},
    }
    resp = requests.post(f"{KRATOS_ADMIN_URL}/identities", json=payload)
    if resp.status_code in (200, 201):
        return f"User {email} registered successfully"
    return f"Registration failed: {resp.text}"

Now we need to update our GraphQL schema to include the new mutation. This allows clients to call the registration function:

schema = make_executable_schema("""
type Query {
    hello: String!
}
type Mutation {
    registerUser(email: String!, password: String!): String!
}
""", query, mutation)

Let's test the registration functionality:

mutation {
  registerUser(email: "alice@example.com", password: "secret")
}

GraphQL Playground - User Registration

Implementing User Login and Protected Queries

With user registration working, we now need to implement the login functionality and create protected queries that require authentication. This involves using Kratos' self-service login flows and implementing session validation.

Let's add the necessary imports and helper functions to handle authentication:

KRATOS_PUBLIC_URL = "http://localhost:4433"

def get_kratos_session(token):
    headers = {"X-Session-Token": token}
    resp = requests.get(f"{KRATOS_PUBLIC_URL}/sessions/whoami", headers=headers)
    if resp.status_code == 200:
        return resp.json()
    return None

Now we'll create a protected query that demonstrates how to access user information only for authenticated users. The me query will return the current user's email if they have a valid session:

@query.field("me")
def resolve_me(*_):
    token = request.headers.get("X-Session-Token")
    session = get_kratos_session(token)
    if not session:
        return "Unauthorized"
    return session["identity"]["traits"]["email"]

Next, we'll implement the login mutation that initiates a Kratos login flow and returns a session token for authenticated users:

@mutation.field("login")
def resolve_login(_, info, identifier, password):
    flow_resp = requests.get(f"{KRATOS_PUBLIC_URL}/self-service/login/api")
    flow_id = flow_resp.json().get("id")
    submit_resp = requests.post(
        f"{KRATOS_PUBLIC_URL}/self-service/login?flow={flow_id}",
        json={"method": "password", "identifier": identifier, "password": password},
    )
    payload = submit_resp.json()
    token = payload.get("session_token")
    return {"sessionToken": token, "identityEmail": payload.get("identity", {}).get("traits", {}).get("email", "")}

We need to update our GraphQL schema to include the new login mutation and the protected me query. We'll also define a custom response type for the login mutation:

schema = make_executable_schema("""
type Query {
    hello: String!
    me: String!
}
type Mutation {
    registerUser(email: String!, password: String!): String!
    login(identifier: String!, password: String!): LoginResponse!
}
type LoginResponse {
    sessionToken: String!
    identityEmail: String!
}
""", query, mutation)

Now you can test the login flow:

mutation {
  login(identifier: "alice@example.com", password: "secret") {
    sessionToken
    identityEmail
  }
}

GraphQL Playground - Login Flow

And access protected resources by including the session token in the X-Session-Token header:

query {
  me
}

Security Considerations

  • Always use HTTPS for public endpoints to encrypt data in transit
  • Treat session_token as sensitive; store in secure cookies or headers
  • Consider token expiration and session revocation to minimize security risks
  • Don't expose the Kratos admin URL in production; keep it internal
  • Implement secure cookie settings with Secure, HttpOnly, and SameSite attributes
  • Use strong secrets and rotate them regularly in production environments

Conclusion

Integrating Ory Kratos with a Flask-based GraphQL API provides a robust and secure authentication mechanism. By leveraging Kratos' self-service flows and session management, you can build scalable and secure user authentication systems.

Remember to follow security best practices, such as using secure cookies, enforcing HTTPS, and implementing token expiry and rotation, to protect your application and its users.

Feel free to explore the Ory Kratos documentation for more detailed information and advanced configurations.

For a complete, runnable example, see the repo: wassimoo/ory-graphql.