# Ankh
A Rust library for passwordless email-based authentication and optional OAuth
integration.
Ankh is a simple, secure, production-ready authentication library with very few
moving parts. It uses magic link email authentication, OAuth, and **stateless
sessions** to avoid the complexities of account management and database
overhead.
- Passwordless Email Verification
- OAuth Integrations (Google, GitHub, etc.)
- Stateless Sessions via encrypted cookies
- Rate Limiting to prevent abuse
- Key Rotation Support for rotating out old keys
- Customizable Redirects post-login
- Axum-Native endpoints and traits
Session data is stored in **encrypted cookies** (AES-256-GCM). The same key is
also used to generate an HMAC-SHA256 signature to detect tampering. To support
key rotation, previously used keys are also kept around (for decryption and
verification only).
---
## Introduction
Ankh integrates seamlessly with the Axum web framework (though it can be
adapted to others). Because sessions are **stateless** (only an encrypted
cookie is needed), there is no need for server-side session storage. The single
`master_key` is used for both encryption and signing, simplifying key
management. However, keys inevitably need to be rotated. Therefore, **Ankh**
supports multiple “rotated” keys for decrypting and verifying any data produced
with older keys.
---
## Configuration
```rust
use std::time::Duration;
/// SameSite cookie attribute options
/// (Used internally by the library; devmode may override some usage)
#[derive(Clone, Debug)]
pub enum SameSite {
Strict,
Lax,
None,
}
/// Configuration for the Ankh authentication system
#[derive(Clone, Debug)]
pub struct AnkhConfig {
/// A 32-byte key used for both AES-256-GCM encryption and HMAC-SHA256 signing
/// (always used for newly encrypted/signed data).
pub master_key: [u8; 32],
/// Previously used keys that are still valid for decryption and HMAC verification,
/// but are **never** used for new encryption or signing.
pub rotated_keys: Vec<[u8; 32]>,
/// Maximum email verification requests per email address per hour
pub max_email_attempts_per_hour: u32,
/// Maximum invalid token attempts per IP address per hour
pub max_token_attempts_per_hour: u32,
/// Maximum total requests per IP address per hour
pub max_requests_per_hour: u32,
/// Name of the session cookie
pub cookie_name: String,
/// Duration until session expires
pub session_duration: Duration,
/// Domain for the cookie (None implies default same-origin)
pub cookie_domain: Option<String>,
/// Path attribute for the cookie
pub cookie_path: String,
/// Base URL for verification links (for passwordless email flows)
pub verification_url_base: String,
/// Duration until email verification tokens expire
pub token_duration: Duration,
/// OAuth client ID (for e.g., Google, GitHub)
pub oauth_client_id: Option<String>,
/// OAuth client secret
pub oauth_client_secret: Option<String>,
/// OAuth callback URL
pub oauth_callback_url: Option<String>,
/// Whether to run in developer mode.
///
/// When `true`, the library relaxes certain security features to make
/// local development easier. **Never set this to `true` in production.**
pub devmode: bool,
}
```
When `devmode` is `false`, stricter security is enforced—cookies only sent over
TLS, stricter SameSite policies, etc. When `devmode` is `true`, Ankh relaxes
these checks to simplify local development (e.g., allows plain HTTP).
---
## Email Normalization
Before storing or verifying any email address, Ankh normalizes it:
- **Case Normalization** All email addresses are converted to lowercase. For
example, `"UsEr@Example.COM"` becomes `"user@example.com"`.
- **Subaddress (Plus) Removal** If the local part includes a `+` (plus) and
anything after it, that suffix is removed. For example,
`"user+newsletter@example.com"` becomes `"user@example.com"`.
- **When It Happens** Normalization occurs as soon as the email is received in
login or OAuth flows.
- **Scope** Ankh does not handle domain-specific aliases (e.g., Gmail’s
optional dot handling). Only case changes and `+` subaddress removal are
performed by default.
---
## Session Cookie & IDs
```rust
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionCookie {
/// User's email address (or derived from OAuth profile)
pub email: String,
/// Unique session identifier
pub sid: String,
/// Unix timestamp when cookie was issued
pub iat: i64,
/// Unix timestamp when cookie expires
pub exp: i64,
}
```
- **email**: Identifies the authenticated user
- **sid**: 256-bit random session ID
- **iat/exp**: Timestamps for issuing and expiration
- **Encryption**: AES-256-GCM with a random IV
- **Signing**: HMAC-SHA256 with the main `master_key`
- **HttpOnly**: Always `true` to mitigate XSS
- **Secure**: Only sent over TLS unless `devmode` is `true`
- **SameSite**: By default, a strict policy unless `devmode` is `true`
- **Max-Age**: Based on `session_duration`
- **Domain & Path**: Specified in `AnkhConfig`
- **Key Rotation**: If decryption or verification with `master_key` fails, Ankh
tries the keys in `rotated_keys`
---
## Errors
Because Ankh calls your implementation methods for rate limiting, email
sending, etc., it’s important to propagate typed errors. Ankh provides two
lower-level errors (`StorageError` and `EmailError`) plus a top-level error
(`AnkhError`) you can return from your trait methods.
```rust
/// A timestamp type representing a point in time (e.g., Unix time).
pub type Timestamp = i64;
#[derive(Debug)]
pub enum StorageError {
/// A catch-all error for various internal issues (database failures, etc.).
Internal(String),
}
#[derive(Debug)]
pub enum EmailError {
/// A general error, potentially with a message from the underlying email provider.
DeliveryFailed(String),
}
/// The top-level error type for Ankh methods.
/// Implementors can wrap `StorageError` or `EmailError` inside `AnkhError`
/// or use other variants for domain-specific errors.
#[derive(Debug)]
pub enum AnkhError {
/// Wraps an internal storage-related error.
Storage(StorageError),
/// Wraps an internal email-delivery-related error.
Email(EmailError),
/// A token was invalid or missing.
InvalidToken,
/// A rate limit was exceeded.
RateLimitExceeded,
/// The user is unauthorized for some reason (e.g., bad credentials).
Unauthorized,
/// A generic or unexpected error occurred.
Other(String),
}
```
---
## Single "Ankh" Trait
Below is the one trait you implement for your application. It combines what was
previously broken out into separate traits for rate limiting, email delivery,
and redirect policy. Your application must provide one type that implements
**all** these methods. Note the `.config()` method, which returns your
`AnkhConfig`.
```rust
use async_trait::async_trait;
#[async_trait]
pub trait Ankh: Send + Sync + Clone {
/// Access the user's AnkhConfig.
fn config(&self) -> &AnkhConfig;
/// Records a new login attempt linked to a specific `email` at the given `timestamp`.
async fn record_email_attempt(
&mut self,
email: &str,
timestamp: Timestamp,
) -> Result<(), AnkhError>;
/// Retrieves all recorded email login attempts for the given `email`
/// since the specified `timestamp`.
async fn get_email_attempts(
&self,
email: &str,
since: Timestamp,
) -> Result<Vec<Timestamp>, AnkhError>;
/// Records a new login attempt from a specific `ip` address at the
/// given `timestamp`.
async fn record_ip_attempt(
&mut self,
ip: &str,
timestamp: Timestamp,
) -> Result<(), AnkhError>;
/// Retrieves all recorded login attempts from the given `ip` address
/// since the specified `timestamp`.
async fn get_ip_attempts(
&self,
ip: &str,
since: Timestamp,
) -> Result<Vec<Timestamp>, AnkhError>;
/// Sends a verification email to the recipient `to`, with a link that
/// includes a `verification_url`.
async fn send_verification_email(
&self,
to: &str,
verification_url: &str,
) -> Result<(), AnkhError>;
/// Returns the absolute or relative URL where the user should be redirected
/// upon successful authentication.
fn redirect_url(&self) -> &str;
}
```
---
## AnkhCore
`AnkhCore` is a library-provided struct that encapsulates the **common** Ankh
logic (endpoint creation, cookie encryption/decryption, token verification,
etc.) while delegating storage, email sending, and redirect decisions to any
user-provided type that implements the `Ankh` trait.
```rust
use std::sync::Arc;
use async_trait::async_trait;
use crate::{Ankh, AnkhConfig, AnkhError, SessionCookie, Timestamp};
/// `AnkhCore` is generic over `A`, which must implement `Ankh` (and be `Send+Sync+Clone`).
#[derive(Clone)]
pub struct AnkhCore<A>
where
A: Ankh + Send + Sync + Clone,
{
/// Internal reference to the user's implementation of `Ankh`.
inner: Arc<A>,
}
impl<A> AnkhCore<A>
where
A: Ankh + Send + Sync + Clone,
{
/// Creates a new `AnkhCore` by wrapping the user-provided `Ankh` implementation in an `Arc`.
pub fn new(ankh_impl: A) -> Self {
// ...
# unimplemented!()
}
/// Returns a reference to the Ankh configuration (`AnkhConfig`).
pub fn config(&self) -> &AnkhConfig {
// ...
# unimplemented!()
}
/// A method to decrypt and validate a session cookie value.
pub fn decrypt_and_verify_cookie(
&self,
cookie_value: &str,
) -> Result<SessionCookie, AnkhError> {
// ...
# unimplemented!()
}
/// A method to generate an encrypted & signed cookie value from a `SessionCookie`.
pub fn encrypt_and_sign_cookie(
&self,
session: &SessionCookie,
) -> Result<String, AnkhError> {
// ...
# unimplemented!()
}
/// A method for verifying a magic link token (passwordless flow).
pub fn verify_magic_token(&self, token: &str) -> Result<String, AnkhError> {
// ...
# unimplemented!()
}
/// Example Axum handler (or any usage) that demonstrates using the
/// underlying `Ankh` trait for rate-limiting, storage, etc.
pub async fn example_handler(&self) -> String {
// ...
# unimplemented!()
}
/// Add more library-endpoint methods, OAuth callback handlers, etc.
}
```
All actual implementation details (AES-GCM encryption, HMAC verification, etc.)
are omitted here to show the **specification**. In practice, `AnkhCore` would
contain the logic for:
- Checking the user’s `master_key` first, then the `rotated_keys` on decryption errors.
- Generating new cookies/tokens exclusively with `master_key`.
- Handling token timestamps and expiry.
---
## Email Verification Flow
In magic link flows, Ankh sends an email containing a short-lived verification
token. Once the user clicks the link, Ankh validates the token and sets a
stateless session cookie.
1. User requests login by email.
2. Server normalizes the email (case + subaddress removal).
3. Server generates a short-lived token and calls `send_verification_email`.
4. User clicks the link; the server verifies the token, sets a cookie, and redirects to `redirect_url()`.
---
## OAuth Integration Flow
Users can log in via Google, GitHub, etc. The typical flow:
1. **GET** `/auth/oauth/<provider>`
- Redirects the user to the OAuth provider’s consent page.
2. **GET** `/auth/oauth/<provider>/callback`
- Receives an authorization code.
- Exchanges it for user info (e.g., email).
- Normalizes the email.
- Sets a session cookie.
- Redirects to `redirect_url()`.
For OAuth to work, `oauth_client_id`, `oauth_client_secret`, and
`oauth_callback_url` must be set in `AnkhConfig`. Use a standard crate like
[oauth2](https://crates.io/crates/oauth2) to handle provider details.
---
## API Endpoints & Workflow
Typical Axum-based routes might look like:
1. **POST** `/auth/login` (Email)
Request body:
```json
{
"email": "user@example.com"
}
```
- Normalizes the email, generates and stores a token, calls `send_verification_email`, and applies rate limits.
- Responds with a success message and token expiration info.
2. **GET** `/auth/verify` (Email)
Query parameters: `?token=...`
- Validates the token.
- Sets the stateless session cookie.
- Redirects to `redirect_url()`.
3. **GET** `/auth/oauth/<provider>`
- Redirects to the provider’s OAuth page with your client ID/state.
4. **GET** `/auth/oauth/<provider>/callback`
- Exchanges the code for user info.
- Normalizes the email.
- Sets the session cookie.
- Redirects to `redirect_url()`.
5. **GET** `/auth/logout`
- Clears the auth cookie.
- Redirects to a login page or home.
---
## Testing & Tooling
- **Integration Tests**
- Email login: confirm token generation, redemption, and cookie.
- OAuth login: confirm the code exchange, user info retrieval, cookie
setting.
- Logout: verify the cookie is cleared.
- Normalization: ensure case-lowering and subaddress removal.
- **Typed Errors**
- Differentiate between rate-limit breaches, invalid tokens, OAuth failures,
or storage issues using `AnkhError`.
- **Example Implementations**
- For `record_email_attempt`, `get_email_attempts`, etc., use Redis or a SQL
DB
- For `send_verification_email`, integrate with SendGrid, SES, Mailgun, etc.
- For OAuth, show a Google or GitHub example using
[oauth2](https://crates.io/crates/oauth2).
---
## Roadmap & Next Steps
- **Revocation Mechanism**
- Provide an optional store for invalidating session IDs before they expire.
- **Key Rotation**
- With the `rotated_keys` attribute, older keys remain valid for decryption
and verification.
- The library uses `master_key` for newly generated cookies, while older
cookies/tokens are still honored if they match any key in `rotated_keys`.
- **Advanced CSRF**
- For multi-form flows beyond login, add explicit CSRF tokens.
- **Extended Use Cases**
- Multi-tenant apps, partial sign-ups, advanced user data retrieval.
With the **`Ankh`** trait plus the **`AnkhCore`** struct, your application can
integrate seamlessly with Ankh’s stateless authentication. `AnkhCore` handles
the common authentication logic (decryption, encryption, verifying tokens), and
your `Ankh` implementation provides domain-specific behavior (rate limiting,
email delivery, and redirect UObserveFunctionHow was your dayDrilling nextAre you going toSend itYesCan you tell me what's wrongLook upStringYou're very close to beingSo you can modify that string to strip thingsRLs).