summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-06-06 15:49:19 -0600
committermo khan <mo@mokhan.ca>2025-06-06 15:49:19 -0600
commit14c7a0e3ebf77451662bbbac1915facdec0bca3f (patch)
tree9473c21c06d425be2395398ec2a851c695c92a79
parent463c259bd41f20d5811b028e8045f3de3effe097 (diff)
refactor: try vibe coding with claude
-rw-r--r--.gitignore1
-rw-r--r--CLAUDE.md52
-rw-r--r--Cargo.lock875
-rw-r--r--Cargo.toml8
-rw-r--r--public/404.html11
-rw-r--r--public/metadata.json2
-rw-r--r--src/lib.rs451
-rw-r--r--src/main.rs52
8 files changed, 1417 insertions, 35 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..d58c33b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+.claude/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..7803849
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,52 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is an RFC-compliant OAuth2 Security Token Service (STS) implementation written in Rust. The project provides a complete OAuth2 authorization server with JWT token generation, supporting the authorization code flow.
+
+## Common Commands
+
+- **Build**: `cargo build`
+- **Run**: `cargo run`
+- **Test**: `cargo test`
+- **Check**: `cargo check`
+
+## Architecture
+
+The application implements a full OAuth2 authorization server with the following components:
+
+- **Main entry point** (`src/main.rs`): Reads `BIND_ADDR` environment variable (defaults to `127.0.0.1:7878`) and starts the server
+- **HTTP module** (`src/lib.rs`): Contains the core server and OAuth2 implementation:
+ - `Server` struct that handles TCP connections and HTTP routing
+ - `OAuthServer` struct that implements RFC-compliant OAuth2 flows
+ - JWT token generation and validation using HS256
+ - Authorization code storage and management
+ - RFC-compliant error responses
+
+## OAuth2 Endpoints
+
+- `GET /.well-known/oauth-authorization-server` → OAuth2 authorization server metadata (RFC 8414)
+- `GET /authorize` → Authorization endpoint for the authorization code flow
+- `POST /token` → Token endpoint to exchange authorization codes for access tokens
+- `GET /jwks` → JSON Web Key Set endpoint (currently returns empty set)
+
+## Supported OAuth2 Features
+
+- **Grant Types**: Authorization Code
+- **Response Types**: code
+- **Token Types**: JWT Bearer tokens
+- **Scopes**: openid, profile, email
+- **Client Authentication**: client_secret_basic, client_secret_post
+
+## Configuration
+
+- **BIND_ADDR**: Server bind address (default: `127.0.0.1:7878`)
+- **JWT Secret**: Hardcoded in development (should be configurable in production)
+
+## Security Notes
+
+- Authorization codes expire after 10 minutes
+- Access tokens expire after 1 hour
+- In production, replace hardcoded JWT secret with secure key management \ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index ba61b5b..a676533 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,5 +3,880 @@
version = 4
[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "bumpalo"
+version = "3.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
+
+[[package]]
+name = "cc"
+version = "1.2.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "jsonwebtoken"
+version = "9.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
+dependencies = [
+ "base64",
+ "js-sys",
+ "pem",
+ "ring",
+ "serde",
+ "serde_json",
+ "simple_asn1",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "pem"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
+dependencies = [
+ "base64",
+ "serde",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simple_asn1"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
name = "sts"
version = "0.1.0"
+dependencies = [
+ "base64",
+ "jsonwebtoken",
+ "rand",
+ "serde",
+ "serde_json",
+ "url",
+ "urlencoding",
+ "uuid",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6533885..e943299 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,3 +4,11 @@ version = "0.1.0"
edition = "2024"
[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+jsonwebtoken = "9.0"
+uuid = { version = "1.0", features = ["v4"] }
+url = "2.0"
+base64 = "0.22"
+rand = "0.8"
+urlencoding = "2.1"
diff --git a/public/404.html b/public/404.html
deleted file mode 100644
index d03f7ea..0000000
--- a/public/404.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE HTML>
-<html>
- <head>
- <meta charset="utf-8">
- <title>404 - Not Found</title>
-
- </head>
- <body>
- <h1>404 - Not Found</h1>
- </body>
-</html>
diff --git a/public/metadata.json b/public/metadata.json
deleted file mode 100644
index 2c63c08..0000000
--- a/public/metadata.json
+++ /dev/null
@@ -1,2 +0,0 @@
-{
-}
diff --git a/src/lib.rs b/src/lib.rs
index 947e852..f23c4a2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,42 +1,451 @@
+use base64::prelude::*;
+use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::fs;
+use std::io::BufReader;
+use std::io::prelude::*;
+use std::net::{TcpListener, TcpStream};
+use std::time::{SystemTime, UNIX_EPOCH};
+use url::Url;
+use uuid::Uuid;
+
+#[derive(Debug, Clone)]
+pub struct Config {
+ pub bind_addr: String,
+ pub issuer_url: String,
+ pub jwt_secret: String,
+}
+
+impl Config {
+ pub fn from_env() -> Self {
+ let bind_addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".to_string());
+ let issuer_url = format!("http://{}", bind_addr);
+ let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
+ "your-256-bit-secret-key-here-make-it-very-long-and-secure".to_string()
+ });
+
+ Self {
+ bind_addr,
+ issuer_url,
+ jwt_secret,
+ }
+ }
+}
+
pub mod http {
- use std::fs;
- use std::io::BufReader;
- use std::io::prelude::*;
- use std::net::TcpListener;
- use std::net::TcpStream;
+ use super::*;
+
pub struct Server {
- addr: String,
+ config: Config,
+ oauth_server: OAuthServer,
}
impl Server {
pub fn new(addr: String) -> Server {
- Server { addr }
+ let mut config = Config::from_env();
+ config.bind_addr = addr;
+ config.issuer_url = format!("http://{}", config.bind_addr);
+
+ Server {
+ oauth_server: OAuthServer::new(&config),
+ config,
+ }
}
pub fn start(&self) {
- let listener = TcpListener::bind(self.addr.clone()).unwrap();
- for next_stream in listener.incoming() {
- self.handle(next_stream.unwrap());
+ let listener = TcpListener::bind(self.config.bind_addr.clone()).unwrap();
+ println!("OAuth2 STS Server listening on {}", self.config.bind_addr);
+
+ for stream in listener.incoming() {
+ match stream {
+ Ok(stream) => self.handle(stream),
+ Err(e) => eprintln!("Error accepting connection: {}", e),
+ }
}
}
pub fn handle(&self, mut stream: TcpStream) {
- let io = BufReader::new(&stream);
- let request_line = io.lines().next().unwrap().unwrap();
+ let mut buffer = [0; 8192];
+ let bytes_read = stream.read(&mut buffer).unwrap_or(0);
+ let request = String::from_utf8_lossy(&buffer[..bytes_read]);
- let (status_line, filename) = match &request_line[..] {
- "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "./public/index.html"),
- "GET /.well-known/oauth-authorization-server HTTP/1.1" => {
- ("HTTP/1.1 200 OK", "./public/metadata.json")
+ let lines: Vec<&str> = request.lines().collect();
+ if lines.is_empty() {
+ self.send_error_response(&mut stream, 400, "Bad Request");
+ return;
+ }
+
+ let request_line = lines[0];
+ let parts: Vec<&str> = request_line.split_whitespace().collect();
+
+ if parts.len() != 3 {
+ self.send_error_response(&mut stream, 400, "Bad Request");
+ return;
+ }
+
+ let method = parts[0];
+ let path_and_query = parts[1];
+
+ // Parse URL and query parameters
+ let url = match Url::parse(&format!("http://localhost{}", path_and_query)) {
+ Ok(url) => url,
+ Err(_) => {
+ self.send_error_response(&mut stream, 400, "Bad Request");
+ return;
}
- _ => ("HTTP/1.1 404 NOT FOUND", "./public/404.html"),
};
- let contents = fs::read_to_string(filename).unwrap();
- let length = contents.len();
- let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
+ let path = url.path();
+ let query_params: HashMap<String, String> = url
+ .query_pairs()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+
+ match (method, path) {
+ ("GET", "/") => self.serve_static_file(&mut stream, "./public/index.html"),
+ ("GET", "/.well-known/oauth-authorization-server") => {
+ self.handle_metadata(&mut stream)
+ }
+ ("GET", "/jwks") => self.handle_jwks(&mut stream),
+ ("GET", "/authorize") => self.handle_authorize(&mut stream, &query_params),
+ ("POST", "/token") => self.handle_token(&mut stream, &request),
+ _ => self.send_error_response(&mut stream, 404, "Not Found"),
+ }
+ }
+
+ fn serve_static_file(&self, stream: &mut TcpStream, filename: &str) {
+ match fs::read_to_string(filename) {
+ Ok(contents) => {
+ let content_type = if filename.ends_with(".json") {
+ "application/json"
+ } else {
+ "text/html"
+ };
+
+ let response = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n{}",
+ content_type,
+ contents.len(),
+ contents
+ );
+ let _ = stream.write_all(response.as_bytes());
+ }
+ Err(_) => self.send_error_response(stream, 404, "Not Found"),
+ }
+ }
+
+ fn send_error_response(&self, stream: &mut TcpStream, status: u16, message: &str) {
+ let response = format!(
+ "HTTP/1.1 {} {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{}",
+ status,
+ message,
+ message.len(),
+ message
+ );
+ let _ = stream.write_all(response.as_bytes());
+ }
+
+ fn send_json_response(
+ &self,
+ stream: &mut TcpStream,
+ status: u16,
+ status_text: &str,
+ json: &str,
+ ) {
+ let response = format!(
+ "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
+ status,
+ status_text,
+ json.len(),
+ json
+ );
+ let _ = stream.write_all(response.as_bytes());
+ }
+
+ fn handle_metadata(&self, stream: &mut TcpStream) {
+ let metadata = serde_json::json!({
+ "issuer": self.config.issuer_url,
+ "authorization_endpoint": format!("{}/authorize", self.config.issuer_url),
+ "token_endpoint": format!("{}/token", self.config.issuer_url),
+ "jwks_uri": format!("{}/jwks", self.config.issuer_url),
+ "scopes_supported": ["openid", "profile", "email"],
+ "response_types_supported": ["code"],
+ "response_modes_supported": ["query"],
+ "grant_types_supported": ["authorization_code"],
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"],
+ "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"]
+ });
+ self.send_json_response(stream, 200, "OK", &metadata.to_string());
+ }
+
+ fn handle_jwks(&self, stream: &mut TcpStream) {
+ let jwks = self.oauth_server.get_jwks();
+ self.send_json_response(stream, 200, "OK", &jwks);
+ }
+
+ fn handle_authorize(&self, stream: &mut TcpStream, params: &HashMap<String, String>) {
+ match self.oauth_server.handle_authorize(params) {
+ Ok(redirect_url) => {
+ let response = format!(
+ "HTTP/1.1 302 Found\r\nLocation: {}\r\nContent-Length: 0\r\n\r\n",
+ redirect_url
+ );
+ let _ = stream.write_all(response.as_bytes());
+ }
+ Err(error_response) => {
+ self.send_json_response(stream, 400, "Bad Request", &error_response);
+ }
+ }
+ }
+
+ fn handle_token(&self, stream: &mut TcpStream, request: &str) {
+ let body = self.extract_body(request);
+ let form_params = self.parse_form_data(&body);
+
+ match self.oauth_server.handle_token(&form_params) {
+ Ok(token_response) => {
+ self.send_json_response(stream, 200, "OK", &token_response);
+ }
+ Err(error_response) => {
+ self.send_json_response(stream, 400, "Bad Request", &error_response);
+ }
+ }
+ }
+
+ fn extract_body(&self, request: &str) -> String {
+ if let Some(pos) = request.find("\r\n\r\n") {
+ request[pos + 4..].to_string()
+ } else {
+ String::new()
+ }
+ }
+
+ fn parse_form_data(&self, body: &str) -> HashMap<String, String> {
+ body.split('&')
+ .filter_map(|pair| {
+ let mut split = pair.splitn(2, '=');
+ if let (Some(key), Some(value)) = (split.next(), split.next()) {
+ Some((
+ urlencoding::decode(key).unwrap_or_default().to_string(),
+ urlencoding::decode(value).unwrap_or_default().to_string(),
+ ))
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct Claims {
+ sub: String,
+ iss: String,
+ aud: String,
+ exp: u64,
+ iat: u64,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ scope: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct TokenResponse {
+ access_token: String,
+ token_type: String,
+ expires_in: u64,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ refresh_token: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ scope: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct ErrorResponse {
+ error: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ error_description: Option<String>,
+}
+
+pub struct OAuthServer {
+ config: Config,
+ encoding_key: EncodingKey,
+ decoding_key: DecodingKey,
+ auth_codes: std::sync::Mutex<HashMap<String, AuthCode>>,
+}
+
+#[derive(Debug, Clone)]
+struct AuthCode {
+ client_id: String,
+ redirect_uri: String,
+ scope: Option<String>,
+ expires_at: u64,
+ user_id: String,
+}
- stream.write_all(response.as_bytes()).unwrap();
+impl OAuthServer {
+ pub fn new(config: &Config) -> Self {
+ Self {
+ encoding_key: EncodingKey::from_secret(config.jwt_secret.as_ref()),
+ decoding_key: DecodingKey::from_secret(config.jwt_secret.as_ref()),
+ auth_codes: std::sync::Mutex::new(HashMap::new()),
+ config: config.clone(),
}
}
+
+ fn get_jwks(&self) -> String {
+ // For simplicity, returning empty JWKS. In production, include public key
+ serde_json::json!({
+ "keys": []
+ })
+ .to_string()
+ }
+
+ pub fn handle_authorize(&self, params: &HashMap<String, String>) -> Result<String, String> {
+ // Validate required parameters
+ let client_id = params
+ .get("client_id")
+ .ok_or_else(|| self.error_response("invalid_request", "Missing client_id"))?;
+
+ let redirect_uri = params
+ .get("redirect_uri")
+ .ok_or_else(|| self.error_response("invalid_request", "Missing redirect_uri"))?;
+
+ let response_type = params
+ .get("response_type")
+ .ok_or_else(|| self.error_response("invalid_request", "Missing response_type"))?;
+
+ if response_type != "code" {
+ return Err(self.error_response(
+ "unsupported_response_type",
+ "Only code response type supported",
+ ));
+ }
+
+ // Generate authorization code
+ let code = Uuid::new_v4().to_string();
+ let expires_at = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs()
+ + 600; // 10 minutes
+
+ let auth_code = AuthCode {
+ client_id: client_id.clone(),
+ redirect_uri: redirect_uri.clone(),
+ scope: params.get("scope").cloned(),
+ expires_at,
+ user_id: "test_user".to_string(), // In production, get from authentication
+ };
+
+ {
+ let mut codes = self.auth_codes.lock().unwrap();
+ codes.insert(code.clone(), auth_code);
+ }
+
+ // Build redirect URL with authorization code
+ let mut redirect_url = Url::parse(redirect_uri)
+ .map_err(|_| self.error_response("invalid_request", "Invalid redirect_uri"))?;
+
+ redirect_url.query_pairs_mut().append_pair("code", &code);
+
+ if let Some(state) = params.get("state") {
+ redirect_url.query_pairs_mut().append_pair("state", state);
+ }
+
+ Ok(redirect_url.to_string())
+ }
+
+ fn handle_token(&self, params: &HashMap<String, String>) -> Result<String, String> {
+ let grant_type = params
+ .get("grant_type")
+ .ok_or_else(|| self.error_response("invalid_request", "Missing grant_type"))?;
+
+ if grant_type != "authorization_code" {
+ return Err(self.error_response(
+ "unsupported_grant_type",
+ "Only authorization_code grant type supported",
+ ));
+ }
+
+ let code = params
+ .get("code")
+ .ok_or_else(|| self.error_response("invalid_request", "Missing code"))?;
+
+ let client_id = params
+ .get("client_id")
+ .ok_or_else(|| self.error_response("invalid_request", "Missing client_id"))?;
+
+ // Validate authorization code
+ let auth_code = {
+ let mut codes = self.auth_codes.lock().unwrap();
+ codes.remove(code).ok_or_else(|| {
+ self.error_response("invalid_grant", "Invalid or expired authorization code")
+ })?
+ };
+
+ // Verify client_id matches
+ if auth_code.client_id != *client_id {
+ return Err(self.error_response("invalid_grant", "Client ID mismatch"));
+ }
+
+ // Check expiration
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ if now > auth_code.expires_at {
+ return Err(self.error_response("invalid_grant", "Authorization code expired"));
+ }
+
+ // Generate access token
+ let access_token =
+ self.generate_access_token(&auth_code.user_id, client_id, &auth_code.scope)?;
+
+ let token_response = TokenResponse {
+ access_token,
+ token_type: "Bearer".to_string(),
+ expires_in: 3600, // 1 hour
+ refresh_token: None,
+ scope: auth_code.scope,
+ };
+
+ serde_json::to_string(&token_response)
+ .map_err(|_| self.error_response("server_error", "Failed to serialize token response"))
+ }
+
+ fn generate_access_token(
+ &self,
+ user_id: &str,
+ client_id: &str,
+ scope: &Option<String>,
+ ) -> Result<String, String> {
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ let claims = Claims {
+ sub: user_id.to_string(),
+ iss: self.config.issuer_url.clone(),
+ aud: client_id.to_string(),
+ exp: now + 3600, // 1 hour
+ iat: now,
+ scope: scope.clone(),
+ };
+
+ encode(&Header::default(), &claims, &self.encoding_key)
+ .map_err(|_| self.error_response("server_error", "Failed to generate token"))
+ }
+
+ fn error_response(&self, error: &str, description: &str) -> String {
+ let error_resp = ErrorResponse {
+ error: error.to_string(),
+ error_description: Some(description.to_string()),
+ };
+ serde_json::to_string(&error_resp).unwrap_or_else(|_| "{}".to_string())
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 442bfe2..64f8fa3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,8 +12,58 @@ fn main() {
#[cfg(test)]
mod tests {
+ use super::*;
+ use std::collections::HashMap;
+
#[test]
- fn it_starts_a_server() {
+ fn test_oauth_server_creation() {
+ let server = sts::http::Server::new("127.0.0.1:0".to_string());
+ // If we get here without panicking, the server was created successfully
assert!(true);
}
+
+ #[test]
+ fn test_authorization_code_generation() {
+ let config = sts::Config::from_env();
+ let oauth_server = sts::OAuthServer::new(&config);
+ let mut params = HashMap::new();
+ params.insert("client_id".to_string(), "test_client".to_string());
+ params.insert("redirect_uri".to_string(), "http://localhost:3000/callback".to_string());
+ params.insert("response_type".to_string(), "code".to_string());
+ params.insert("state".to_string(), "test_state".to_string());
+
+ let result = oauth_server.handle_authorize(&params);
+ assert!(result.is_ok());
+
+ let redirect_url = result.unwrap();
+ assert!(redirect_url.contains("code="));
+ assert!(redirect_url.contains("state=test_state"));
+ }
+
+ #[test]
+ fn test_missing_client_id() {
+ let config = sts::Config::from_env();
+ let oauth_server = sts::OAuthServer::new(&config);
+ let mut params = HashMap::new();
+ params.insert("redirect_uri".to_string(), "http://localhost:3000/callback".to_string());
+ params.insert("response_type".to_string(), "code".to_string());
+
+ let result = oauth_server.handle_authorize(&params);
+ assert!(result.is_err());
+ assert!(result.unwrap_err().contains("invalid_request"));
+ }
+
+ #[test]
+ fn test_unsupported_response_type() {
+ let config = sts::Config::from_env();
+ let oauth_server = sts::OAuthServer::new(&config);
+ let mut params = HashMap::new();
+ params.insert("client_id".to_string(), "test_client".to_string());
+ params.insert("redirect_uri".to_string(), "http://localhost:3000/callback".to_string());
+ params.insert("response_type".to_string(), "token".to_string());
+
+ let result = oauth_server.handle_authorize(&params);
+ assert!(result.is_err());
+ assert!(result.unwrap_err().contains("unsupported_response_type"));
+ }
}