{"id":33918754,"url":"https://github.com/rustunit/axum-turnstile","last_synced_at":"2026-03-11T01:01:55.487Z","repository":{"id":325731952,"uuid":"1102200114","full_name":"rustunit/axum-turnstile","owner":"rustunit","description":"Rust Cloudflare Turnstile verification middleware for Axum","archived":false,"fork":false,"pushed_at":"2025-11-23T02:37:33.000Z","size":49,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-09T11:59:11.032Z","etag":null,"topics":["axum","turnstile"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/axum-turnstile","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rustunit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"extrawurst"}},"created_at":"2025-11-23T02:09:57.000Z","updated_at":"2025-11-25T01:32:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rustunit/axum-turnstile","commit_stats":null,"previous_names":["rustunit/axum-turnstile"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/rustunit/axum-turnstile","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rustunit%2Faxum-turnstile","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rustunit%2Faxum-turnstile/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rustunit%2Faxum-turnstile/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rustunit%2Faxum-turnstile/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rustunit","download_url":"https://codeload.github.com/rustunit/axum-turnstile/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rustunit%2Faxum-turnstile/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30364607,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T21:41:54.280Z","status":"ssl_error","status_checked_at":"2026-03-10T21:40:59.357Z","response_time":106,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axum","turnstile"],"created_at":"2025-12-12T08:31:44.595Z","updated_at":"2026-03-11T01:01:55.462Z","avatar_url":"https://github.com/rustunit.png","language":"Rust","readme":"# axum-turnstile\n\n[![Crates.io](https://img.shields.io/crates/v/axum-turnstile.svg)](https://crates.io/crates/axum-turnstile)\n[![Documentation](https://docs.rs/axum-turnstile/badge.svg)](https://docs.rs/axum-turnstile)\n\n**Cloudflare Turnstile verification middleware for Axum**\n\nProtect your Axum web applications from bots and abuse with [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) - a privacy-first, user-friendly CAPTCHA alternative. This crate provides a seamless integration as Tower middleware.\n\n## Features\n\n- ✨ Drop-in middleware for Axum routes\n- 🎯 Type-safe verification with extractors\n- ⚙️ Customizable headers and endpoints\n- 🧪 Built-in support for test keys\n- 📦 Minimal dependencies\n\n## Installation\n\nAdd this to your `Cargo.toml`:\n\n```toml\n[dependencies]\naxum-turnstile = \"0.1\"\n```\n\n## Quick Start\n\n### 1. Get Your Turnstile Keys\n\nSign up at [Cloudflare Dashboard](https://dash.cloudflare.com/) and create a Turnstile site to get your:\n- **Site Key** (public, used in your frontend)\n- **Secret Key** (private, used in this middleware)\n\n### 2. Add the Middleware\n\n```rust\nuse axum::{routing::post, Router};\nuse axum_turnstile::{TurnstileLayer, VerifiedTurnstile};\n\n#[tokio::main]\nasync fn main() {\n    let app = Router::new()\n        .route(\"/api/submit\", post(submit_handler))\n        // Protect this route with Turnstile\n        .layer(TurnstileLayer::from_secret(\"your-secret-key\"));\n\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:3000\")\n        .await\n        .unwrap();\n    \n    axum::serve(listener, app).await.unwrap();\n}\n\n// This handler will only be called if Turnstile verification succeeds\nasync fn submit_handler(_verified: VerifiedTurnstile) -\u003e \u0026'static str {\n    \"Form submitted successfully!\"\n}\n```\n\n### 3. Frontend Integration\n\nInclude the Turnstile widget in your HTML and send the token with your request:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cscript src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cform id=\"myForm\"\u003e\n        \u003c!-- Turnstile widget --\u003e\n        \u003cdiv class=\"cf-turnstile\" data-sitekey=\"your-site-key\"\u003e\u003c/div\u003e\n        \u003cbutton type=\"submit\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n\n    \u003cscript\u003e\n        document.getElementById('myForm').addEventListener('submit', async (e) =\u003e {\n            e.preventDefault();\n            \n            // Get the Turnstile token\n            const token = document.querySelector('[name=\"cf-turnstile-response\"]').value;\n            \n            // Send it to your protected endpoint\n            const response = await fetch('/api/submit', {\n                method: 'POST',\n                headers: {\n                    'CF-Turnstile-Token': token,\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify({ /* your data */ })\n            });\n            \n            if (response.ok) {\n                alert('Success!');\n            } else {\n                alert('Verification failed');\n            }\n        });\n    \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Advanced Usage\n\n### Custom Configuration\n\n```rust\nuse axum_turnstile::{TurnstileConfig, TurnstileLayer};\n\nlet config = TurnstileConfig::new(\"your-secret-key\")\n    .with_header_name(\"X-Custom-Turnstile-Token\")\n    .with_verify_url(\"https://custom-endpoint.example.com/verify\");\n\nlet layer = TurnstileLayer::new(config);\n```\n\n### Selective Route Protection\n\nYou can apply the middleware to specific routes by using nested routers:\n\n```rust\nuse axum::{routing::{get, post}, Router};\nuse axum_turnstile::TurnstileLayer;\n\n// Create a router with protected routes\nlet protected = Router::new()\n    .route(\"/api/submit\", post(submit))\n    .route(\"/api/comment\", post(comment))\n    .layer(TurnstileLayer::from_secret(\"your-secret-key\"));\n\n// Merge with public routes\nlet app = Router::new()\n    .route(\"/\", get(home))\n    .route(\"/about\", get(about))\n    .merge(protected);\n```\n\nAlternatively, you can nest protected routes under a common path:\n\n```rust\nuse axum::{routing::{get, post}, Router};\nuse axum_turnstile::TurnstileLayer;\n\nlet app = Router::new()\n    // Public routes\n    .route(\"/\", get(home))\n    .route(\"/about\", get(about))\n    // Nest protected routes under /api\n    .nest(\"/api\", Router::new()\n        .route(\"/submit\", post(submit))\n        .route(\"/comment\", post(comment))\n        .layer(TurnstileLayer::from_secret(\"your-secret-key\"))\n    );\n```\n\n### Using the Extractor\n\nThe `VerifiedTurnstile` type can be used as an extractor in any handler:\n\n```rust\nuse axum::Json;\nuse axum_turnstile::VerifiedTurnstile;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Deserialize)]\nstruct FormData {\n    name: String,\n    email: String,\n}\n\n#[derive(Serialize)]\nstruct Response {\n    message: String,\n}\n\nasync fn submit_form(\n    _verified: VerifiedTurnstile,  // Ensures Turnstile was verified\n    Json(data): Json\u003cFormData\u003e,\n) -\u003e Json\u003cResponse\u003e {\n    // Process the form data\n    Json(Response {\n        message: format!(\"Thanks for submitting, {}!\", data.name)\n    })\n}\n```\n\n## Testing\n\nCloudflare provides test keys that always pass or fail verification:\n\n### Always Passes\n```rust\nuse axum_turnstile::TurnstileLayer;\n\n// Secret key that always passes\nlet layer = TurnstileLayer::from_secret(\"1x0000000000000000000000000000000AA\");\n```\n\n**Site key (frontend):** `1x00000000000000000000AA`\n\n### Always Fails\n```rust\n// Secret key that always fails\nlet layer = TurnstileLayer::from_secret(\"2x0000000000000000000000000000000AA\");\n```\n\n**Site key (frontend):** `2x00000000000000000000AA`\n\n### Writing Tests\n\n```rust\nuse axum::{\n    body::Body,\n    http::{Request, StatusCode},\n    routing::post,\n    Router,\n};\nuse axum_turnstile::TurnstileLayer;\nuse tower::ServiceExt;\n\n#[tokio::test]\nasync fn test_turnstile_verification() {\n    let app = Router::new()\n        .route(\"/submit\", post(|| async { \"OK\" }))\n        .layer(TurnstileLayer::from_secret(\"1x0000000000000000000000000000000AA\"));\n\n    let response = app\n        .oneshot(\n            Request::post(\"/submit\")\n                .header(\"CF-Turnstile-Token\", \"test-token\")\n                .body(Body::empty())\n                .unwrap(),\n        )\n        .await\n        .unwrap();\n\n    assert_eq!(response.status(), StatusCode::OK);\n}\n```\n\n## Response Status Codes\n\n| Status Code | Reason |\n|-------------|--------|\n| `400 Bad Request` | The `CF-Turnstile-Token` header is missing from the request |\n| `403 Forbidden` | The Turnstile token verification failed |\n| `500 Internal Server Error` | Error communicating with Cloudflare's verification API |\n\n## How It Works\n\n1. **Client Request**: The client includes the Turnstile token in the request header\n2. **Middleware Intercept**: The middleware extracts the token from the header\n3. **Verification**: The token is verified with Cloudflare's API\n4. **Success Path**: If valid, a `VerifiedTurnstile` marker is added to request extensions\n5. **Handler Execution**: Your handler can extract the marker to ensure verification\n6. **Failure Path**: If invalid or missing, an error response is returned immediately\n\n```\n┌─────────┐          ┌──────────────┐          ┌────────────┐          ┌─────────┐\n│ Client  │─────────▶│  Turnstile   │─────────▶│ Cloudflare │─────────▶│ Handler │\n│         │  Token   │  Middleware  │  Verify  │    API     │  Success │         │\n└─────────┘          └──────────────┘          └────────────┘          └─────────┘\n                            │\n                            │ Invalid/Missing\n                            ▼\n                     ┌───────────────┐\n                     │ Error Response│\n                     └───────────────┘\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nLicensed under either of:\n\n- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n\nat your option.\n\n## Resources\n\n- [Cloudflare Turnstile Documentation](https://developers.cloudflare.com/turnstile/)\n- [Axum Documentation](https://docs.rs/axum)\n- [API Documentation](https://docs.rs/axum-turnstile)\n","funding_links":["https://github.com/sponsors/extrawurst"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frustunit%2Faxum-turnstile","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frustunit%2Faxum-turnstile","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frustunit%2Faxum-turnstile/lists"}