rust 114 lines · 3 tabs

Signing and Verifying Session Cookies with HMAC-SHA256 in Rust

Shared by codesnips Aug 2026
3 tabs
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    pub user_id: u64,
    pub issued_at: u64,
    pub expires_at: u64,
}

fn now() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock before epoch")
        .as_secs()
}

impl Session {
    pub fn new(user_id: u64, ttl_secs: u64) -> Self {
        let issued_at = now();
        Session {
            user_id,
            issued_at,
            expires_at: issued_at + ttl_secs,
        }
    }

    pub fn is_expired(&self) -> bool {
        now() >= self.expires_at
    }
}
3 files · rust Explain with highlit

This snippet shows a compact, self-contained approach to tamper-proof session cookies without any server-side session store. The idea is to serialize a small session payload, append an HMAC-SHA256 tag computed with a secret key, and hand the whole thing to the browser. Because the client never sees the key, it cannot forge a valid tag, and the server can verify authenticity on every request by recomputing the MAC over the received payload.

In session.rs the Session struct carries the claims that need to survive a round trip — a user_id, a monotonic issued_at, and an expires_at used for expiry checks. It derives Serialize/Deserialize so serde_json can turn it into a stable byte string; the JSON body is what gets signed, so any modification to it invalidates the tag.

cookie_codec.rs holds the actual crypto. CookieCodec::encode serializes the session, computes an HMAC over those bytes, and joins base64url(payload) and base64url(tag) with a . separator — the classic payload.signature shape also seen in JWTs. decode reverses this: it splits on the dot, base64-decodes both halves, then calls verify_slice from the hmac crate. That verification is the security-critical step. It is done with a constant-time comparison to avoid timing side channels; a naive == on the tag bytes could leak how many leading bytes matched and let an attacker recover a valid MAC byte by byte. Only after the signature checks out does the code parse the JSON and reject expired sessions, so untrusted input is never deserialized as trusted state before authentication.

The error type distinguishes BadFormat, BadSignature, and Expired so callers can respond appropriately — usually all three collapse to "log in again," but keeping them separate aids logging.

main.rs demonstrates the full loop and a forgery attempt: flipping a byte in the payload makes decode return BadSignature. The main trade-off of stateless cookies is revocation — a signed cookie stays valid until it expires, so short lifetimes and a rotatable key matter. Base64url keeps the token cookie-safe, and using Hmac<Sha256> keeps the dependency surface small and the verification fast.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs
go
package files

import (
  "context"
  "time"

Presigned S3 upload URLs (AWS SDK v2)

go aws s3
by Leah Thompson 1 tab
erb
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>

<section class="notifications">
  <h1>Notifications</h1>

Turbo Streams + authorization: signed per-user stream name

rails turbo hotwire
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Signing and Verifying Session Cookies with HMAC-SHA256 in Rust — share card
Link copied