oauth2-4.4.1/.cargo_vcs_info.json0000644000000001360000000000100122440ustar { "git": { "sha1": "1ba2862ce6ebcf58156f02acd58038f482f893d5" }, "path_in_vcs": "" }oauth2-4.4.1/.github/FUNDING.yml000064400000000000000000000000241046102023000142050ustar 00000000000000github: [ramosbugs] oauth2-4.4.1/.github/workflows/main.yml000064400000000000000000000053511046102023000161040ustar 00000000000000name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events. push: {} pull_request: {} schedule: # Run daily to catch breakages in new Rust versions as well as new cargo audit findings. - cron: '0 16 * * *' # Allows you to run this workflow manually from the Actions tab workflow_dispatch: env: CARGO_TERM_COLOR: always # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" test: # The type of runner that the job will run on runs-on: ${{ matrix.rust_os.os }} strategy: fail-fast: false matrix: rust_os: - { rust: 1.45.0, os: ubuntu-20.04 } - { rust: stable, os: ubuntu-latest } - { rust: beta, os: ubuntu-latest } - { rust: nightly, os: ubuntu-latest } env: CARGO_NET_GIT_FETCH_WITH_CLI: "true" # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust_os.rust }} override: true components: clippy, rustfmt target: wasm32-unknown-unknown # Newer dependency versions don't support rustc 1.45, so we use a Cargo.lock file for those # builds. - name: Use Rust 1.45 lockfile if: ${{ matrix.rust_os.rust == '1.45.0' }} run: cp Cargo-1.45.lock Cargo.lock - name: Run tests run: cargo test --tests --examples - name: Doc tests run: cargo test --doc - name: Test with all features enabled run: cargo test --all-features # Curl without reqwest (examples will not build) - name: Test with curl (w/o reqwest) run: cargo test --tests --features curl --no-default-features - name: Check fmt if: ${{ matrix.rust_os.rust == '1.45.0' }} run: cargo fmt --all -- --check - name: Clippy if: ${{ matrix.rust_os.rust == '1.45.0' }} run: cargo clippy --all --all-features -- --deny warnings - name: Audit if: ${{ matrix.rust_os.rust == 'stable' }} run: | cargo install --force cargo-audit # The chrono thread safety issue doesn't affect this crate since the crate does not rely # on the system's local time zone, only UTC. See: # https://github.com/chronotope/chrono/issues/499#issuecomment-946388161 cargo audit --ignore RUSTSEC-2020-0159 - name: Check WASM build run: cargo check --target wasm32-unknown-unknown oauth2-4.4.1/.gitignore000064400000000000000000000000461046102023000130240ustar 00000000000000.idea/** *.iml /target /Cargo.lock *~ oauth2-4.4.1/Cargo-1.45.lock000064400000000000000000001243671046102023000133430ustar 00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "anyhow" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "async-channel" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" dependencies = [ "concurrent-queue", "event-listener", "futures-core", ] [[package]] name = "async-executor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "once_cell", "slab", ] [[package]] name = "async-global-executor" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" dependencies = [ "async-channel", "async-executor", "async-io", "async-lock", "blocking", "futures-lite", "num_cpus", "once_cell", ] [[package]] name = "async-io" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd" dependencies = [ "concurrent-queue", "fastrand", "futures-lite", "libc", "log", "nb-connect", "once_cell", "parking", "polling", "vec-arena", "waker-fn", "winapi", ] [[package]] name = "async-lock" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" dependencies = [ "event-listener", ] [[package]] name = "async-std" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-channel", "async-global-executor", "async-io", "async-lock", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", "futures-lite", "gloo-timers", "kv-log-macro", "log", "memchr", "once_cell", "pin-project-lite", "pin-utils", "slab", "wasm-bindgen-futures", ] [[package]] name = "async-task" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" [[package]] name = "atomic-waker" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "block-buffer" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" dependencies = [ "async-channel", "async-task", "atomic-waker", "fastrand", "futures-lite", "once_cell", ] [[package]] name = "bumpalo" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cache-padded" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", "num-integer", "num-traits", "serde", "winapi", ] [[package]] name = "chunked_transfer" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "concurrent-queue" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" dependencies = [ "cache-padded", ] [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "crypto-common" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", "typenum", ] [[package]] name = "ctor" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" dependencies = [ "quote", "syn", ] [[package]] name = "curl" version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a872858e9cb9e3b96c80dd78774ad9e32e44d3b05dc31e142b858d14aebc82c" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2", "winapi", ] [[package]] name = "curl-sys" version = "0.4.55+curl-7.83.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "winapi", ] [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "encoding_rs" version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if", ] [[package]] name = "event-listener" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" [[package]] name = "fastrand" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-lite" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" dependencies = [ "fastrand", "futures-core", "futures-io", "memchr", "parking", "pin-project-lite", "waker-fn", ] [[package]] name = "futures-macro" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-core", "futures-io", "futures-macro", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gloo-timers" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "h2" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "http" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes", "fnv", "itoa 0.4.8", ] [[package]] name = "http-body" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", "pin-project-lite", ] [[package]] name = "httparse" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] name = "httpdate" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" [[package]] name = "hyper" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa 0.4.8", "pin-project", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper-rustls" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" dependencies = [ "futures-util", "hyper", "log", "rustls", "tokio", "tokio-rustls", "webpki", ] [[package]] name = "hyper-tls" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", "hyper", "native-tls", "tokio", "tokio-native-tls", ] [[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "ipnet" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "js-sys" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" dependencies = [ "wasm-bindgen", ] [[package]] name = "kv-log-macro" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" dependencies = [ "log", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libz-sys" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "lock_api" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", "value-bag", ] [[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", "miow", "ntapi", "winapi", ] [[package]] name = "miow" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ "winapi", ] [[package]] name = "native-tls" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nb-connect" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670361df1bc2399ee1ff50406a0d422587dd3bb0da596e1978fe8e05dabddf4f" dependencies = [ "libc", "socket2", ] [[package]] name = "ntapi" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "oauth2" version = "4.4.1" dependencies = [ "anyhow", "async-std", "base64", "chrono", "curl", "getrandom", "hex", "hmac", "http", "rand", "reqwest", "serde", "serde_json", "serde_path_to_error", "sha2", "thiserror", "tokio", "ureq", "url", "uuid", ] [[package]] name = "once_cell" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac8b1a9b2518dc799a2271eff1688707eb315f0d4697aa6b0871369ca4c4da55" [[package]] name = "openssl" version = "0.10.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" dependencies = [ "bitflags", "cfg-if", "foreign-types", "lazy_static", "libc", "openssl-sys", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" dependencies = [ "autocfg", "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", "libc", "redox_syscall", "smallvec", "winapi", ] [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "polling" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" dependencies = [ "cfg-if", "libc", "log", "wepoll-ffi", "winapi", ] [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "reqwest" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd281b1030aa675fb90aa994d07187645bb3c8fc756ca766e7c3070b439de9de" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", "http", "http-body", "hyper", "hyper-rustls", "hyper-tls", "ipnet", "js-sys", "lazy_static", "log", "mime", "native-tls", "percent-encoding", "pin-project-lite", "rustls", "serde", "serde_urlencoded", "tokio", "tokio-native-tls", "tokio-rustls", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", "winreg", ] [[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", "once_cell", "spin", "untrusted", "web-sys", "winapi", ] [[package]] name = "rustls" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64", "log", "ring", "sct", "webpki", ] [[package]] name = "ryu" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "schannel" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", "windows-sys", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", ] [[package]] name = "security-framework" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "itoa 1.0.2", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7868ad3b8196a8a0aea99a8220b124278ee5320a55e4fde97794b6f85b1a377" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa 1.0.2", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if", "libc", "winapi", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", "fastrand", "libc", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "thiserror" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "tokio-macros", "winapi", ] [[package]] name = "tokio-macros" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls", "tokio", "webpki", ] [[package]] name = "tokio-util" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-bidi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3131cd6cb18488da91da1d10ed31e966f453c06b65bf010d35638456976a3fd7" dependencies = [ "base64", "chunked_transfer", "log", "once_cell", "rustls", "url", "webpki", "webpki-roots", ] [[package]] name = "url" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", "matches", "percent-encoding", "serde", ] [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom", ] [[package]] name = "value-bag" version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" dependencies = [ "ctor", "version_check", ] [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vec-arena" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae23c56872cdb2d1b1ddb90112da26615654fa4d4e3ee84e2d3b3e9c9853145" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ "log", "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if", "serde", "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" dependencies = [ "bumpalo", "lazy_static", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" [[package]] name = "web-sys" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", "untrusted", ] [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ "webpki", ] [[package]] name = "wepoll-ffi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" dependencies = [ "cc", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "winreg" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] oauth2-4.4.1/Cargo.lock0000644000001243670000000000100102340ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "anyhow" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "async-channel" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" dependencies = [ "concurrent-queue", "event-listener", "futures-core", ] [[package]] name = "async-executor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "once_cell", "slab", ] [[package]] name = "async-global-executor" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" dependencies = [ "async-channel", "async-executor", "async-io", "async-lock", "blocking", "futures-lite", "num_cpus", "once_cell", ] [[package]] name = "async-io" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd" dependencies = [ "concurrent-queue", "fastrand", "futures-lite", "libc", "log", "nb-connect", "once_cell", "parking", "polling", "vec-arena", "waker-fn", "winapi", ] [[package]] name = "async-lock" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" dependencies = [ "event-listener", ] [[package]] name = "async-std" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-channel", "async-global-executor", "async-io", "async-lock", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", "futures-lite", "gloo-timers", "kv-log-macro", "log", "memchr", "once_cell", "pin-project-lite", "pin-utils", "slab", "wasm-bindgen-futures", ] [[package]] name = "async-task" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" [[package]] name = "atomic-waker" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "block-buffer" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" dependencies = [ "async-channel", "async-task", "atomic-waker", "fastrand", "futures-lite", "once_cell", ] [[package]] name = "bumpalo" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cache-padded" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", "num-integer", "num-traits", "serde", "winapi", ] [[package]] name = "chunked_transfer" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "concurrent-queue" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" dependencies = [ "cache-padded", ] [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "crypto-common" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", "typenum", ] [[package]] name = "ctor" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" dependencies = [ "quote", "syn", ] [[package]] name = "curl" version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a872858e9cb9e3b96c80dd78774ad9e32e44d3b05dc31e142b858d14aebc82c" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2", "winapi", ] [[package]] name = "curl-sys" version = "0.4.55+curl-7.83.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "winapi", ] [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "encoding_rs" version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if", ] [[package]] name = "event-listener" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" [[package]] name = "fastrand" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-lite" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" dependencies = [ "fastrand", "futures-core", "futures-io", "memchr", "parking", "pin-project-lite", "waker-fn", ] [[package]] name = "futures-macro" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-core", "futures-io", "futures-macro", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gloo-timers" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "h2" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "http" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes", "fnv", "itoa 0.4.8", ] [[package]] name = "http-body" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", "pin-project-lite", ] [[package]] name = "httparse" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] name = "httpdate" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" [[package]] name = "hyper" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa 0.4.8", "pin-project", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper-rustls" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" dependencies = [ "futures-util", "hyper", "log", "rustls", "tokio", "tokio-rustls", "webpki", ] [[package]] name = "hyper-tls" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", "hyper", "native-tls", "tokio", "tokio-native-tls", ] [[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "ipnet" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "js-sys" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" dependencies = [ "wasm-bindgen", ] [[package]] name = "kv-log-macro" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" dependencies = [ "log", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libz-sys" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "lock_api" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", "value-bag", ] [[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", "miow", "ntapi", "winapi", ] [[package]] name = "miow" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ "winapi", ] [[package]] name = "native-tls" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nb-connect" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670361df1bc2399ee1ff50406a0d422587dd3bb0da596e1978fe8e05dabddf4f" dependencies = [ "libc", "socket2", ] [[package]] name = "ntapi" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "oauth2" version = "4.4.1" dependencies = [ "anyhow", "async-std", "base64", "chrono", "curl", "getrandom", "hex", "hmac", "http", "rand", "reqwest", "serde", "serde_json", "serde_path_to_error", "sha2", "thiserror", "tokio", "ureq", "url", "uuid", ] [[package]] name = "once_cell" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac8b1a9b2518dc799a2271eff1688707eb315f0d4697aa6b0871369ca4c4da55" [[package]] name = "openssl" version = "0.10.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" dependencies = [ "bitflags", "cfg-if", "foreign-types", "lazy_static", "libc", "openssl-sys", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" dependencies = [ "autocfg", "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", "libc", "redox_syscall", "smallvec", "winapi", ] [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "polling" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" dependencies = [ "cfg-if", "libc", "log", "wepoll-ffi", "winapi", ] [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "reqwest" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd281b1030aa675fb90aa994d07187645bb3c8fc756ca766e7c3070b439de9de" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", "http", "http-body", "hyper", "hyper-rustls", "hyper-tls", "ipnet", "js-sys", "lazy_static", "log", "mime", "native-tls", "percent-encoding", "pin-project-lite", "rustls", "serde", "serde_urlencoded", "tokio", "tokio-native-tls", "tokio-rustls", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", "winreg", ] [[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", "once_cell", "spin", "untrusted", "web-sys", "winapi", ] [[package]] name = "rustls" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64", "log", "ring", "sct", "webpki", ] [[package]] name = "ryu" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "schannel" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", "windows-sys", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", ] [[package]] name = "security-framework" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "itoa 1.0.2", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7868ad3b8196a8a0aea99a8220b124278ee5320a55e4fde97794b6f85b1a377" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa 1.0.2", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if", "libc", "winapi", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", "fastrand", "libc", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "thiserror" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "tokio-macros", "winapi", ] [[package]] name = "tokio-macros" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls", "tokio", "webpki", ] [[package]] name = "tokio-util" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-bidi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3131cd6cb18488da91da1d10ed31e966f453c06b65bf010d35638456976a3fd7" dependencies = [ "base64", "chunked_transfer", "log", "once_cell", "rustls", "url", "webpki", "webpki-roots", ] [[package]] name = "url" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", "matches", "percent-encoding", "serde", ] [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom", ] [[package]] name = "value-bag" version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" dependencies = [ "ctor", "version_check", ] [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vec-arena" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae23c56872cdb2d1b1ddb90112da26615654fa4d4e3ee84e2d3b3e9c9853145" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ "log", "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if", "serde", "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" dependencies = [ "bumpalo", "lazy_static", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" [[package]] name = "web-sys" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", "untrusted", ] [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ "webpki", ] [[package]] name = "wepoll-ffi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" dependencies = [ "cc", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "winreg" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] oauth2-4.4.1/Cargo.toml0000644000000043410000000000100102440ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "oauth2" version = "4.4.1" authors = [ "Alex Crichton ", "Florin Lipan ", "David A. Ramos ", ] description = "An extensible, strongly-typed implementation of OAuth2" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/ramosbugs/oauth2-rs" [package.metadata.docs.rs] all-features = true [dependencies.base64] version = "0.13" [dependencies.chrono] version = "0.4" features = [ "clock", "serde", "std", ] default-features = false [dependencies.http] version = "0.2" [dependencies.rand] version = "0.8" [dependencies.reqwest] version = "0.11" features = ["blocking"] optional = true default-features = false [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" [dependencies.serde_path_to_error] version = "0.1.2" [dependencies.sha2] version = "0.10" [dependencies.thiserror] version = "1.0" [dependencies.ureq] version = "2" optional = true [dependencies.url] version = "2.1" features = ["serde"] [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.async-std] version = "1.6.3" [dev-dependencies.hex] version = "0.4" [dev-dependencies.hmac] version = "0.12" [dev-dependencies.tokio] version = "1.0" features = ["full"] [dev-dependencies.uuid] version = "0.8" features = ["v4"] [features] default = [ "reqwest", "rustls-tls", ] native-tls = ["reqwest/native-tls"] pkce-plain = [] rustls-tls = ["reqwest/rustls-tls"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.curl] version = "0.4.0" optional = true [target."cfg(target_arch = \"wasm32\")".dependencies.getrandom] version = "0.2" features = ["js"] [badges.maintenance] status = "actively-developed" oauth2-4.4.1/Cargo.toml.orig000064400000000000000000000026751046102023000137350ustar 00000000000000[package] name = "oauth2" authors = ["Alex Crichton ", "Florin Lipan ", "David A. Ramos "] version = "4.4.1" license = "MIT OR Apache-2.0" description = "An extensible, strongly-typed implementation of OAuth2" repository = "https://github.com/ramosbugs/oauth2-rs" edition = "2018" readme = "README.md" [package.metadata.docs.rs] all-features = true [badges] maintenance = { status = "actively-developed" } [features] default = ["reqwest", "rustls-tls"] pkce-plain = [] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] [dependencies] base64 = "0.13" thiserror = "1.0" http = "0.2" rand = "0.8" reqwest = { version = "0.11", optional = true, default-features = false, features = ["blocking"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10" ureq = { version = "2", optional = true } url = { version = "2.1", features = ["serde"] } chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] } serde_path_to_error = "0.1.2" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] curl = { version = "0.4.0", optional = true } [dev-dependencies] hex = "0.4" hmac = "0.12" uuid = { version = "0.8", features = ["v4"] } anyhow = "1.0" tokio = { version = "1.0", features = ["full"] } async-std = "1.6.3" oauth2-4.4.1/LICENSE-APACHE000064400000000000000000000251371046102023000127700ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. oauth2-4.4.1/LICENSE-MIT000064400000000000000000000020411046102023000124650ustar 00000000000000Copyright (c) 2014 Alex Crichton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. oauth2-4.4.1/README.md000064400000000000000000000012411046102023000123110ustar 00000000000000# OAuth2 [![Build Status](https://github.com/ramosbugs/oauth2-rs/actions/workflows/main.yml/badge.svg)](https://github.com/ramosbugs/oauth2-rs/actions/workflows/main.yml) An extensible, strongly-typed implementation of OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749)). Documentation is available on [docs.rs](https://docs.rs/oauth2). Release notes are available on [GitHub](https://github.com/ramosbugs/oauth2-rs/releases). ## Sponsorship This project is sponsored by [Unflakable](https://unflakable.com), a service for tracking and quarantining flaky tests. oauth2-4.4.1/examples/github.rs000064400000000000000000000125201046102023000145020ustar 00000000000000//! //! This example showcases the Github OAuth2 process for requesting access to the user's public repos and //! email address. //! //! Before running it, you'll need to generate your own Github OAuth2 credentials. //! //! In order to run the example call: //! //! ```sh //! GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy cargo run --example github //! ``` //! //! ...and follow the instructions. //! use oauth2::basic::BasicClient; // Alternatively, this can be `oauth2::curl::http_client` or a custom client. use oauth2::reqwest::http_client; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use url::Url; fn main() { let github_client_id = ClientId::new( env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable."), ); let github_client_secret = ClientSecret::new( env::var("GITHUB_CLIENT_SECRET") .expect("Missing the GITHUB_CLIENT_SECRET environment variable."), ); let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string()) .expect("Invalid token endpoint URL"); // Set up the config for the Github OAuth2 process. let client = BasicClient::new( github_client_id, Some(github_client_secret), auth_url, Some(token_url), ) // This example will be running its own server at localhost:8080. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client .authorize_url(CsrfToken::new_random) // This example is requesting access to the user's public repos and email. .add_scope(Scope::new("public_repo".to_string())) .add_scope(Scope::new("user:email".to_string())) .url(); println!( "Open this URL in your browser:\n{}\n", authorize_url.to_string() ); // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { if let Ok(mut stream) = stream { let code; let state; { let mut reader = BufReader::new(&stream); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "code" }) .unwrap(); let (_, value) = code_pair; code = AuthorizationCode::new(value.into_owned()); let state_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "state" }) .unwrap(); let (_, value) = state_pair; state = CsrfToken::new(value.into_owned()); } let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).unwrap(); println!("Github returned the following code:\n{}\n", code.secret()); println!( "Github returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token_res = client.exchange_code(code).request(http_client); println!("Github returned the following token:\n{:?}\n", token_res); if let Ok(token) = token_res { // NB: Github returns a single comma-separated "scope" parameter instead of multiple // space-separated scopes. Github-specific clients can parse this scope into // multiple scopes by splitting at the commas. Note that it's not safe for the // library to do this by default because RFC 6749 allows scopes to contain commas. let scopes = if let Some(scopes_vec) = token.scopes() { scopes_vec .iter() .map(|comma_separated| comma_separated.split(',')) .flatten() .collect::>() } else { Vec::new() }; println!("Github returned the following scopes:\n{:?}\n", scopes); } // The server will terminate itself after collecting the first code. break; } } } oauth2-4.4.1/examples/github_async.rs000064400000000000000000000127231046102023000157040ustar 00000000000000//! //! This example showcases the Github OAuth2 process for requesting access to the user's public repos and //! email address. //! //! Before running it, you'll need to generate your own Github OAuth2 credentials. //! //! In order to run the example call: //! //! ```sh //! GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy cargo run --example github //! ``` //! //! ...and follow the instructions. //! use oauth2::basic::BasicClient; // Alternatively, this can be `oauth2::curl::http_client` or a custom client. use oauth2::reqwest::async_http_client; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use std::env; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpListener; use url::Url; #[tokio::main] async fn main() { let github_client_id = ClientId::new( env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable."), ); let github_client_secret = ClientSecret::new( env::var("GITHUB_CLIENT_SECRET") .expect("Missing the GITHUB_CLIENT_SECRET environment variable."), ); let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string()) .expect("Invalid token endpoint URL"); // Set up the config for the Github OAuth2 process. let client = BasicClient::new( github_client_id, Some(github_client_secret), auth_url, Some(token_url), ) // This example will be running its own server at localhost:8080. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client .authorize_url(CsrfToken::new_random) // This example is requesting access to the user's public repos and email. .add_scope(Scope::new("public_repo".to_string())) .add_scope(Scope::new("user:email".to_string())) .url(); println!( "Open this URL in your browser:\n{}\n", authorize_url.to_string() ); // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); loop { if let Ok((mut stream, _)) = listener.accept().await { let code; let state; { let mut reader = BufReader::new(&mut stream); let mut request_line = String::new(); reader.read_line(&mut request_line).await.unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "code" }) .unwrap(); let (_, value) = code_pair; code = AuthorizationCode::new(value.into_owned()); let state_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "state" }) .unwrap(); let (_, value) = state_pair; state = CsrfToken::new(value.into_owned()); } let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).await.unwrap(); println!("Github returned the following code:\n{}\n", code.secret()); println!( "Github returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token_res = client .exchange_code(code) .request_async(async_http_client) .await; println!("Github returned the following token:\n{:?}\n", token_res); if let Ok(token) = token_res { // NB: Github returns a single comma-separated "scope" parameter instead of multiple // space-separated scopes. Github-specific clients can parse this scope into // multiple scopes by splitting at the commas. Note that it's not safe for the // library to do this by default because RFC 6749 allows scopes to contain commas. let scopes = if let Some(scopes_vec) = token.scopes() { scopes_vec .iter() .map(|comma_separated| comma_separated.split(',')) .flatten() .collect::>() } else { Vec::new() }; println!("Github returned the following scopes:\n{:?}\n", scopes); } // The server will terminate itself after collecting the first code. break; } } } oauth2-4.4.1/examples/google.rs000064400000000000000000000134471046102023000145050ustar 00000000000000//! //! This example showcases the Google OAuth2 process for requesting access to the Google Calendar features //! and the user's profile. //! //! Before running it, you'll need to generate your own Google OAuth2 credentials. //! //! In order to run the example call: //! //! ```sh //! GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy cargo run --example google //! ``` //! //! ...and follow the instructions. //! use oauth2::{basic::BasicClient, revocation::StandardRevocableToken, TokenResponse}; // Alternatively, this can be oauth2::curl::http_client or a custom. use oauth2::reqwest::http_client; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, RevocationUrl, Scope, TokenUrl, }; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use url::Url; fn main() { let google_client_id = ClientId::new( env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."), ); let google_client_secret = ClientSecret::new( env::var("GOOGLE_CLIENT_SECRET") .expect("Missing the GOOGLE_CLIENT_SECRET environment variable."), ); let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://www.googleapis.com/oauth2/v3/token".to_string()) .expect("Invalid token endpoint URL"); // Set up the config for the Google OAuth2 process. let client = BasicClient::new( google_client_id, Some(google_client_secret), auth_url, Some(token_url), ) // This example will be running its own server at localhost:8080. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), ) // Google supports OAuth 2.0 Token Revocation (RFC-7009) .set_revocation_uri( RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string()) .expect("Invalid revocation endpoint URL"), ); // Google supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/). // Create a PKCE code verifier and SHA-256 encode it as a code challenge. let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client .authorize_url(CsrfToken::new_random) // This example is requesting access to the "calendar" features and the user's profile. .add_scope(Scope::new( "https://www.googleapis.com/auth/calendar".to_string(), )) .add_scope(Scope::new( "https://www.googleapis.com/auth/plus.me".to_string(), )) .set_pkce_challenge(pkce_code_challenge) .url(); println!( "Open this URL in your browser:\n{}\n", authorize_url.to_string() ); // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { if let Ok(mut stream) = stream { let code; let state; { let mut reader = BufReader::new(&stream); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "code" }) .unwrap(); let (_, value) = code_pair; code = AuthorizationCode::new(value.into_owned()); let state_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "state" }) .unwrap(); let (_, value) = state_pair; state = CsrfToken::new(value.into_owned()); } let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).unwrap(); println!("Google returned the following code:\n{}\n", code.secret()); println!( "Google returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token_response = client .exchange_code(code) .set_pkce_verifier(pkce_code_verifier) .request(http_client); println!( "Google returned the following token:\n{:?}\n", token_response ); // Revoke the obtained token let token_response = token_response.unwrap(); let token_to_revoke: StandardRevocableToken = match token_response.refresh_token() { Some(token) => token.into(), None => token_response.access_token().into(), }; client .revoke_token(token_to_revoke) .unwrap() .request(http_client) .expect("Failed to revoke token"); // The server will terminate itself after revoking the token. break; } } } oauth2-4.4.1/examples/google_devicecode.rs000064400000000000000000000061101046102023000166440ustar 00000000000000//! //! This example showcases the Google OAuth2 process for requesting access to the Google Calendar features //! and the user's profile. //! //! Before running it, you'll need to generate your own Google OAuth2 credentials. //! //! In order to run the example call: //! //! ```sh //! GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy cargo run --example google //! ``` //! //! ...and follow the instructions. //! use oauth2::basic::BasicClient; // Alternatively, this can be oauth2::curl::http_client or a custom. use oauth2::devicecode::{DeviceAuthorizationResponse, ExtraDeviceAuthorizationFields}; use oauth2::reqwest::http_client; use oauth2::{AuthType, AuthUrl, ClientId, ClientSecret, DeviceAuthorizationUrl, Scope, TokenUrl}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env; #[derive(Debug, Serialize, Deserialize)] struct StoringFields(HashMap); impl ExtraDeviceAuthorizationFields for StoringFields {} type StoringDeviceAuthorizationResponse = DeviceAuthorizationResponse; fn main() { let google_client_id = ClientId::new( env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."), ); let google_client_secret = ClientSecret::new( env::var("GOOGLE_CLIENT_SECRET") .expect("Missing the GOOGLE_CLIENT_SECRET environment variable."), ); let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://www.googleapis.com/oauth2/v3/token".to_string()) .expect("Invalid token endpoint URL"); let device_auth_url = DeviceAuthorizationUrl::new("https://oauth2.googleapis.com/device/code".to_string()) .expect("Invalid device authorization endpoint URL"); // Set up the config for the Google OAuth2 process. // // Google's OAuth endpoint expects the client_id to be in the request body, // so ensure that option is set. let device_client = BasicClient::new( google_client_id, Some(google_client_secret), auth_url, Some(token_url), ) .set_device_authorization_url(device_auth_url) .set_auth_type(AuthType::RequestBody); // Request the set of codes from the Device Authorization endpoint. let details: StoringDeviceAuthorizationResponse = device_client .exchange_device_code() .unwrap() .add_scope(Scope::new("profile".to_string())) .request(http_client) .expect("Failed to request codes from device auth endpoint"); // Display the URL and user-code. println!( "Open this URL in your browser:\n{}\nand enter the code: {}", details.verification_uri().to_string(), details.user_code().secret().to_string() ); // Now poll for the token let token = device_client .exchange_device_access_token(&details) .request(http_client, std::thread::sleep, None) .expect("Failed to get token"); println!("Google returned the following token:\n{:?}\n", token); } oauth2-4.4.1/examples/letterboxd.rs000064400000000000000000000115201046102023000153730ustar 00000000000000//! //! This example showcases the Letterboxd OAuth2 process for requesting access //! to the API features restricted by authentication. Letterboxd requires all //! requests being signed as described in http://api-docs.letterboxd.com/#signing. //! So this serves as an example how to implement a custom client, which signs //! requests and appends the signature to the url query. //! //! Before running it, you'll need to get access to the API. //! //! In order to run the example call: //! //! ```sh //! LETTERBOXD_CLIENT_ID=xxx LETTERBOXD_CLIENT_SECRET=yyy LETTERBOXD_USERNAME=www LETTERBOXD_PASSWORD=zzz cargo run --example letterboxd //! ``` use hex::ToHex; use hmac::{Hmac, Mac}; use oauth2::{ basic::BasicClient, AuthType, AuthUrl, ClientId, ClientSecret, HttpRequest, HttpResponse, ResourceOwnerPassword, ResourceOwnerUsername, TokenUrl, }; use sha2::Sha256; use url::Url; use std::env; use std::time; fn main() -> Result<(), anyhow::Error> { // a.k.a api key in Letterboxd API documentation let letterboxd_client_id = ClientId::new( env::var("LETTERBOXD_CLIENT_ID") .expect("Missing the LETTERBOXD_CLIENT_ID environment variable."), ); // a.k.a api secret in Letterboxd API documentation let letterboxd_client_secret = ClientSecret::new( env::var("LETTERBOXD_CLIENT_SECRET") .expect("Missing the LETTERBOXD_CLIENT_SECRET environment variable."), ); // Letterboxd uses the Resource Owner flow and does not have an auth url let auth_url = AuthUrl::new("https://api.letterboxd.com/api/v0/auth/404".to_string())?; let token_url = TokenUrl::new("https://api.letterboxd.com/api/v0/auth/token".to_string())?; // Set up the config for the Letterboxd OAuth2 process. let client = BasicClient::new( letterboxd_client_id.clone(), Some(letterboxd_client_secret.clone()), auth_url, Some(token_url), ); // Resource Owner flow uses username and password for authentication let letterboxd_username = ResourceOwnerUsername::new( env::var("LETTERBOXD_USERNAME") .expect("Missing the LETTERBOXD_USERNAME environment variable."), ); let letterboxd_password = ResourceOwnerPassword::new( env::var("LETTERBOXD_PASSWORD") .expect("Missing the LETTERBOXD_PASSWORD environment variable."), ); // All API requests must be signed as described at http://api-docs.letterboxd.com/#signing; // for that, we use a custom http client. let http_client = SigningHttpClient::new(letterboxd_client_id, letterboxd_client_secret); let token_result = client .set_auth_type(AuthType::RequestBody) .exchange_password(&letterboxd_username, &letterboxd_password) .request(|request| http_client.execute(request))?; println!("{:?}", token_result); Ok(()) } /// Custom HTTP client which signs requests. /// /// See http://api-docs.letterboxd.com/#signing. #[derive(Debug, Clone)] struct SigningHttpClient { client_id: ClientId, client_secret: ClientSecret, } impl SigningHttpClient { fn new(client_id: ClientId, client_secret: ClientSecret) -> Self { Self { client_id, client_secret, } } /// Signs the request before calling `oauth2::reqwest::http_client`. fn execute(&self, mut request: HttpRequest) -> Result { let signed_url = self.sign_url(request.url, &request.method, &request.body); request.url = signed_url; oauth2::reqwest::http_client(request) } /// Signs the request based on a random and unique nonce, timestamp, and /// client id and secret. /// /// The client id, nonce, timestamp and signature are added to the url's /// query. /// /// See http://api-docs.letterboxd.com/#signing. fn sign_url(&self, mut url: Url, method: &http::method::Method, body: &[u8]) -> Url { let nonce = uuid::Uuid::new_v4(); // use UUID as random and unique nonce let timestamp = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .expect("SystemTime::duration_since failed") .as_secs(); url.query_pairs_mut() .append_pair("apikey", &self.client_id) .append_pair("nonce", &format!("{}", nonce)) .append_pair("timestamp", &format!("{}", timestamp)); // create signature let mut hmac = Hmac::::new_from_slice(&self.client_secret.secret().as_bytes()) .expect("HMAC can take key of any size"); hmac.update(method.as_str().as_bytes()); hmac.update(&[b'\0']); hmac.update(url.as_str().as_bytes()); hmac.update(&[b'\0']); hmac.update(body); let signature: String = hmac.finalize().into_bytes().encode_hex(); url.query_pairs_mut().append_pair("signature", &signature); url } } oauth2-4.4.1/examples/microsoft_devicecode.rs000064400000000000000000000026441046102023000174050ustar 00000000000000use oauth2::basic::BasicClient; use oauth2::devicecode::StandardDeviceAuthorizationResponse; use oauth2::reqwest::async_http_client; use oauth2::{AuthUrl, ClientId, DeviceAuthorizationUrl, Scope, TokenUrl}; use std::error::Error; #[tokio::main] async fn main() -> Result<(), Box> { let device_auth_url = DeviceAuthorizationUrl::new( "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(), )?; let client = BasicClient::new( ClientId::new("client_id".to_string()), None, AuthUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string())?, Some(TokenUrl::new( "https://login.microsoftonline.com/common/v2.0/oauth2/token".to_string(), )?), ) .set_device_authorization_url(device_auth_url); let details: StandardDeviceAuthorizationResponse = client .exchange_device_code()? .add_scope(Scope::new("read".to_string())) .request_async(async_http_client) .await?; eprintln!( "Open this URL in your browser:\n{}\nand enter the code: {}", details.verification_uri().to_string(), details.user_code().secret().to_string() ); let token_result = client .exchange_device_access_token(&details) .request_async(async_http_client, tokio::time::sleep, None) .await; eprintln!("Token:{:?}", token_result); Ok(()) } oauth2-4.4.1/examples/msgraph.rs000064400000000000000000000133331046102023000146640ustar 00000000000000//! //! This example showcases the Microsoft Graph OAuth2 process for requesting access to Microsoft //! services using PKCE. //! //! Before running it, you'll need to generate your own Microsoft OAuth2 credentials. See //! https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app //! * Register a `Web` application with a `Redirect URI` of `http://localhost:3003/redirect`. //! * In the left menu select `Overview`. Copy the `Application (client) ID` as the MSGRAPH_CLIENT_ID. //! * In the left menu select `Certificates & secrets` and add a new client secret. Copy the secret value //! as MSGRAPH_CLIENT_SECRET. //! * In the left menu select `API permissions` and add a permission. Select Microsoft Graph and //! `Delegated permissions`. Add the `Files.Read` permission. //! //! In order to run the example call: //! //! ```sh //! MSGRAPH_CLIENT_ID=xxx MSGRAPH_CLIENT_SECRET=yyy cargo run --example msgraph //! ``` //! //! ...and follow the instructions. //! use oauth2::basic::BasicClient; // Alternatively, this can be `oauth2::curl::http_client` or a custom client. use oauth2::reqwest::http_client; use oauth2::{ AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, TokenUrl, }; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use url::Url; fn main() { let graph_client_id = ClientId::new( env::var("MSGRAPH_CLIENT_ID").expect("Missing the MSGRAPH_CLIENT_ID environment variable."), ); let graph_client_secret = ClientSecret::new( env::var("MSGRAPH_CLIENT_SECRET") .expect("Missing the MSGRAPH_CLIENT_SECRET environment variable."), ); let auth_url = AuthUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string()) .expect("Invalid token endpoint URL"); // Set up the config for the Microsoft Graph OAuth2 process. let client = BasicClient::new( graph_client_id, Some(graph_client_secret), auth_url, Some(token_url), ) // Microsoft Graph requires client_id and client_secret in URL rather than // using Basic authentication. .set_auth_type(AuthType::RequestBody) // This example will be running its own server at localhost:3003. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:3003/redirect".to_string()) .expect("Invalid redirect URL"), ); // Microsoft Graph supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/). // Create a PKCE code verifier and SHA-256 encode it as a code challenge. let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client .authorize_url(CsrfToken::new_random) // This example requests read access to OneDrive. .add_scope(Scope::new( "https://graph.microsoft.com/Files.Read".to_string(), )) .set_pkce_challenge(pkce_code_challenge) .url(); println!( "Open this URL in your browser:\n{}\n", authorize_url.to_string() ); // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:3003").unwrap(); for stream in listener.incoming() { if let Ok(mut stream) = stream { let code; let state; { let mut reader = BufReader::new(&stream); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "code" }) .unwrap(); let (_, value) = code_pair; code = AuthorizationCode::new(value.into_owned()); let state_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "state" }) .unwrap(); let (_, value) = state_pair; state = CsrfToken::new(value.into_owned()); } let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).unwrap(); println!("MS Graph returned the following code:\n{}\n", code.secret()); println!( "MS Graph returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token = client .exchange_code(code) // Send the PKCE code verifier in the token request .set_pkce_verifier(pkce_code_verifier) .request(http_client); println!("MS Graph returned the following token:\n{:?}\n", token); // The server will terminate itself after collecting the first code. break; } } } oauth2-4.4.1/examples/wunderlist.rs000064400000000000000000000211661046102023000154260ustar 00000000000000//! //! This example showcases the Wunderlist OAuth2 process for requesting access to the user's todo lists. //! Wunderlist does not implement the correct token response, so this serves as an example of how to //! implement a custom client. //! //! Before running it, you'll need to create your own wunderlist app. //! //! In order to run the example call: //! //! ```sh //! WUNDER_CLIENT_ID=xxx WUNDER_CLIENT_SECRET=yyy cargo run --example wunderlist //! ``` //! //! ...and follow the instructions. //! use oauth2::TokenType; use oauth2::{ basic::{ BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenType, }, revocation::StandardRevocableToken, }; // Alternatively, this can be `oauth2::curl::http_client` or a custom client. use oauth2::helpers; use oauth2::reqwest::http_client; use oauth2::{ AccessToken, AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, ExtraTokenFields, RedirectUrl, RefreshToken, Scope, TokenResponse, TokenUrl, }; use serde::{Deserialize, Serialize}; use std::time::Duration; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use url::Url; type SpecialTokenResponse = NonStandardTokenResponse; type SpecialClient = Client< BasicErrorResponse, SpecialTokenResponse, BasicTokenType, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, >; fn default_token_type() -> Option { Some(BasicTokenType::Bearer) } /// /// Non Standard OAuth2 token response. /// /// This struct includes the fields defined in /// [Section 5.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.1), as well as /// extensions defined by the `EF` type parameter. /// In this particular example token_type is optional to showcase how to deal with a non /// compliant provider. /// #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NonStandardTokenResponse { access_token: AccessToken, // In this example wunderlist does not follow the RFC specs and don't return the // token_type. `NonStandardTokenResponse` makes the `token_type` optional. #[serde(default = "default_token_type")] token_type: Option, #[serde(skip_serializing_if = "Option::is_none")] expires_in: Option, #[serde(skip_serializing_if = "Option::is_none")] refresh_token: Option, #[serde(rename = "scope")] #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] scopes: Option>, #[serde(bound = "EF: ExtraTokenFields")] #[serde(flatten)] extra_fields: EF, } impl TokenResponse for NonStandardTokenResponse where EF: ExtraTokenFields, BasicTokenType: TokenType, { /// /// REQUIRED. The access token issued by the authorization server. /// fn access_token(&self) -> &AccessToken { &self.access_token } /// /// REQUIRED. The type of the token issued as described in /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). /// Value is case insensitive and deserialized to the generic `TokenType` parameter. /// But in this particular case as the service is non compliant, it has a default value /// fn token_type(&self) -> &BasicTokenType { match &self.token_type { Some(t) => t, None => &BasicTokenType::Bearer, } } /// /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600 /// denotes that the access token will expire in one hour from the time the response was /// generated. If omitted, the authorization server SHOULD provide the expiration time via /// other means or document the default value. /// fn expires_in(&self) -> Option { self.expires_in.map(Duration::from_secs) } /// /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same /// authorization grant as described in /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). /// fn refresh_token(&self) -> Option<&RefreshToken> { self.refresh_token.as_ref() } /// /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The /// scipe of the access token as described by /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response, /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from /// the response, this field is `None`. /// fn scopes(&self) -> Option<&Vec> { self.scopes.as_ref() } } fn main() { let client_id_str = env::var("WUNDERLIST_CLIENT_ID") .expect("Missing the WUNDERLIST_CLIENT_ID environment variable."); let client_secret_str = env::var("WUNDERLIST_CLIENT_SECRET") .expect("Missing the WUNDERLIST_CLIENT_SECRET environment variable."); let wunder_client_id = ClientId::new(client_id_str.clone()); let wunderlist_client_secret = ClientSecret::new(client_secret_str.clone()); let auth_url = AuthUrl::new("https://www.wunderlist.com/oauth/authorize".to_string()) .expect("Invalid authorization endpoint URL"); let token_url = TokenUrl::new("https://www.wunderlist.com/oauth/access_token".to_string()) .expect("Invalid token endpoint URL"); // Set up the config for the Wunderlist OAuth2 process. let client = SpecialClient::new( wunder_client_id, Some(wunderlist_client_secret), auth_url, Some(token_url), ) // This example will be running its own server at localhost:8080. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"), ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random).url(); println!( "Open this URL in your browser:\n{}\n", authorize_url.to_string() ); // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { if let Ok(mut stream) = stream { let code; let state; { let mut reader = BufReader::new(&stream); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "code" }) .unwrap(); let (_, value) = code_pair; code = AuthorizationCode::new(value.into_owned()); let state_pair = url .query_pairs() .find(|pair| { let &(ref key, _) = pair; key == "state" }) .unwrap(); let (_, value) = state_pair; state = CsrfToken::new(value.into_owned()); } let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).unwrap(); println!( "Wunderlist returned the following code:\n{}\n", code.secret() ); println!( "Wunderlist returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token_res = client .exchange_code(code) .add_extra_param("client_id", client_id_str) .add_extra_param("client_secret", client_secret_str) .request(http_client); println!( "Wunderlist returned the following token:\n{:?}\n", token_res ); break; } } } oauth2-4.4.1/src/basic.rs000064400000000000000000000150031046102023000132510ustar 00000000000000use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; use super::{ Client, EmptyExtraTokenFields, ErrorResponseType, RequestTokenError, StandardErrorResponse, StandardTokenResponse, TokenType, }; use crate::{ revocation::{RevocationErrorResponseType, StandardRevocableToken}, StandardTokenIntrospectionResponse, }; /// /// Basic OAuth2 client specialization, suitable for most applications. /// pub type BasicClient = Client< BasicErrorResponse, BasicTokenResponse, BasicTokenType, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, >; /// /// Basic OAuth2 authorization token types. /// #[derive(Clone, Debug, PartialEq)] pub enum BasicTokenType { /// /// Bearer token /// ([OAuth 2.0 Bearer Tokens - RFC 6750](https://tools.ietf.org/html/rfc6750)). /// Bearer, /// /// MAC ([OAuth 2.0 Message Authentication Code (MAC) /// Tokens](https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-05)). /// Mac, /// /// An extension not defined by RFC 6749. /// Extension(String), } impl BasicTokenType { fn from_str(s: &str) -> Self { match s { "bearer" => BasicTokenType::Bearer, "mac" => BasicTokenType::Mac, ext => BasicTokenType::Extension(ext.to_string()), } } } impl AsRef for BasicTokenType { fn as_ref(&self) -> &str { match *self { BasicTokenType::Bearer => "bearer", BasicTokenType::Mac => "mac", BasicTokenType::Extension(ref ext) => ext.as_str(), } } } impl<'de> serde::Deserialize<'de> for BasicTokenType { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let variant_str = String::deserialize(deserializer)?; Ok(Self::from_str(&variant_str)) } } impl serde::ser::Serialize for BasicTokenType { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(self.as_ref()) } } impl TokenType for BasicTokenType {} /// /// Basic OAuth2 token response. /// pub type BasicTokenResponse = StandardTokenResponse; /// /// Basic OAuth2 token introspection response. /// pub type BasicTokenIntrospectionResponse = StandardTokenIntrospectionResponse; /// /// Basic access token error types. /// /// These error types are defined in /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2). /// #[derive(Clone, PartialEq)] pub enum BasicErrorResponseType { /// /// Client authentication failed (e.g., unknown client, no client authentication included, /// or unsupported authentication method). /// InvalidClient, /// /// The provided authorization grant (e.g., authorization code, resource owner credentials) /// or refresh token is invalid, expired, revoked, does not match the redirection URI used /// in the authorization request, or was issued to another client. /// InvalidGrant, /// /// The request is missing a required parameter, includes an unsupported parameter value /// (other than grant type), repeats a parameter, includes multiple credentials, utilizes /// more than one mechanism for authenticating the client, or is otherwise malformed. /// InvalidRequest, /// /// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the /// resource owner. /// InvalidScope, /// /// The authenticated client is not authorized to use this authorization grant type. /// UnauthorizedClient, /// /// The authorization grant type is not supported by the authorization server. /// UnsupportedGrantType, /// /// An extension not defined by RFC 6749. /// Extension(String), } impl BasicErrorResponseType { pub(crate) fn from_str(s: &str) -> Self { match s { "invalid_client" => BasicErrorResponseType::InvalidClient, "invalid_grant" => BasicErrorResponseType::InvalidGrant, "invalid_request" => BasicErrorResponseType::InvalidRequest, "invalid_scope" => BasicErrorResponseType::InvalidScope, "unauthorized_client" => BasicErrorResponseType::UnauthorizedClient, "unsupported_grant_type" => BasicErrorResponseType::UnsupportedGrantType, ext => BasicErrorResponseType::Extension(ext.to_string()), } } } impl AsRef for BasicErrorResponseType { fn as_ref(&self) -> &str { match *self { BasicErrorResponseType::InvalidClient => "invalid_client", BasicErrorResponseType::InvalidGrant => "invalid_grant", BasicErrorResponseType::InvalidRequest => "invalid_request", BasicErrorResponseType::InvalidScope => "invalid_scope", BasicErrorResponseType::UnauthorizedClient => "unauthorized_client", BasicErrorResponseType::UnsupportedGrantType => "unsupported_grant_type", BasicErrorResponseType::Extension(ref ext) => ext.as_str(), } } } impl<'de> serde::Deserialize<'de> for BasicErrorResponseType { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let variant_str = String::deserialize(deserializer)?; Ok(Self::from_str(&variant_str)) } } impl serde::ser::Serialize for BasicErrorResponseType { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(self.as_ref()) } } impl ErrorResponseType for BasicErrorResponseType {} impl Debug for BasicErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { Display::fmt(self, f) } } impl Display for BasicErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { write!(f, "{}", self.as_ref()) } } /// /// Error response specialization for basic OAuth2 implementation. /// pub type BasicErrorResponse = StandardErrorResponse; /// /// Token error specialization for basic OAuth2 implementation. /// pub type BasicRequestTokenError = RequestTokenError; /// /// Revocation error response specialization for basic OAuth2 implementation. /// pub type BasicRevocationErrorResponse = StandardErrorResponse; oauth2-4.4.1/src/curl.rs000064400000000000000000000054401046102023000131410ustar 00000000000000use std::io::Read; use curl::easy::Easy; use http::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use http::method::Method; use http::status::StatusCode; use super::{HttpRequest, HttpResponse}; /// /// Error type returned by failed curl HTTP requests. /// #[derive(Debug, thiserror::Error)] pub enum Error { /// Error returned by curl crate. #[error("curl request failed")] Curl(#[source] curl::Error), /// Non-curl HTTP error. #[error("HTTP error")] Http(#[source] http::Error), /// Other error. #[error("Other error: {}", _0)] Other(String), } /// /// Synchronous HTTP client. /// pub fn http_client(request: HttpRequest) -> Result { let mut easy = Easy::new(); easy.url(&request.url.to_string()[..]) .map_err(Error::Curl)?; let mut headers = curl::easy::List::new(); request .headers .iter() .map(|(name, value)| { headers .append(&format!( "{}: {}", name, value.to_str().map_err(|_| Error::Other(format!( "invalid {} header value {:?}", name, value.as_bytes() )))? )) .map_err(Error::Curl) }) .collect::>()?; easy.http_headers(headers).map_err(Error::Curl)?; if let Method::POST = request.method { easy.post(true).map_err(Error::Curl)?; easy.post_field_size(request.body.len() as u64) .map_err(Error::Curl)?; } else { assert_eq!(request.method, Method::GET); } let mut form_slice = &request.body[..]; let mut data = Vec::new(); { let mut transfer = easy.transfer(); transfer .read_function(|buf| Ok(form_slice.read(buf).unwrap_or(0))) .map_err(Error::Curl)?; transfer .write_function(|new_data| { data.extend_from_slice(new_data); Ok(new_data.len()) }) .map_err(Error::Curl)?; transfer.perform().map_err(Error::Curl)?; } let status_code = easy.response_code().map_err(Error::Curl)? as u16; Ok(HttpResponse { status_code: StatusCode::from_u16(status_code).map_err(|err| Error::Http(err.into()))?, headers: easy .content_type() .map_err(Error::Curl)? .map(|content_type| { Ok(vec![( CONTENT_TYPE, HeaderValue::from_str(content_type).map_err(|err| Error::Http(err.into()))?, )] .into_iter() .collect::()) }) .transpose()? .unwrap_or_else(HeaderMap::new), body: data, }) } oauth2-4.4.1/src/devicecode.rs000064400000000000000000000205141046102023000142650ustar 00000000000000use std::error::Error; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; use std::marker::PhantomData; use std::time::Duration; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use super::{ DeviceCode, EndUserVerificationUrl, ErrorResponse, ErrorResponseType, RequestTokenError, StandardErrorResponse, TokenResponse, TokenType, UserCode, }; use crate::basic::BasicErrorResponseType; use crate::types::VerificationUriComplete; /// The minimum amount of time in seconds that the client SHOULD wait /// between polling requests to the token endpoint. If no value is /// provided, clients MUST use 5 as the default. fn default_devicecode_interval() -> u64 { 5 } /// /// Trait for adding extra fields to the `DeviceAuthorizationResponse`. /// pub trait ExtraDeviceAuthorizationFields: DeserializeOwned + Debug + Serialize {} #[derive(Clone, Debug, Deserialize, Serialize)] /// /// Empty (default) extra token fields. /// pub struct EmptyExtraDeviceAuthorizationFields {} impl ExtraDeviceAuthorizationFields for EmptyExtraDeviceAuthorizationFields {} /// /// Standard OAuth2 device authorization response. /// #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DeviceAuthorizationResponse where EF: ExtraDeviceAuthorizationFields, { /// The device verification code. device_code: DeviceCode, /// The end-user verification code. user_code: UserCode, /// The end-user verification URI on the authorization The URI should be /// short and easy to remember as end users will be asked to manually type /// it into their user agent. /// /// The `verification_url` alias here is a deviation from the RFC, as /// implementations of device code flow predate RFC 8628. #[serde(alias = "verification_url")] verification_uri: EndUserVerificationUrl, /// A verification URI that includes the "user_code" (or other information /// with the same function as the "user_code"), which is designed for /// non-textual transmission. #[serde(skip_serializing_if = "Option::is_none")] verification_uri_complete: Option, /// The lifetime in seconds of the "device_code" and "user_code". expires_in: u64, /// The minimum amount of time in seconds that the client SHOULD wait /// between polling requests to the token endpoint. If no value is /// provided, clients MUST use 5 as the default. #[serde(default = "default_devicecode_interval")] interval: u64, #[serde(bound = "EF: ExtraDeviceAuthorizationFields", flatten)] extra_fields: EF, } impl DeviceAuthorizationResponse where EF: ExtraDeviceAuthorizationFields, { /// The device verification code. pub fn device_code(&self) -> &DeviceCode { &self.device_code } /// The end-user verification code. pub fn user_code(&self) -> &UserCode { &self.user_code } /// The end-user verification URI on the authorization The URI should be /// short and easy to remember as end users will be asked to manually type /// it into their user agent. pub fn verification_uri(&self) -> &EndUserVerificationUrl { &self.verification_uri } /// A verification URI that includes the "user_code" (or other information /// with the same function as the "user_code"), which is designed for /// non-textual transmission. pub fn verification_uri_complete(&self) -> Option<&VerificationUriComplete> { self.verification_uri_complete.as_ref() } /// The lifetime in seconds of the "device_code" and "user_code". pub fn expires_in(&self) -> Duration { Duration::from_secs(self.expires_in) } /// The minimum amount of time in seconds that the client SHOULD wait /// between polling requests to the token endpoint. If no value is /// provided, clients MUST use 5 as the default. pub fn interval(&self) -> Duration { Duration::from_secs(self.interval) } /// Any extra fields returned on the response. pub fn extra_fields(&self) -> &EF { &self.extra_fields } } /// /// Standard implementation of DeviceAuthorizationResponse which throws away /// extra received response fields. /// pub type StandardDeviceAuthorizationResponse = DeviceAuthorizationResponse; /// /// Basic access token error types. /// /// These error types are defined in /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2) and /// [Section 3.5 of RFC 6749](https://tools.ietf.org/html/rfc8628#section-3.5) /// #[derive(Clone, PartialEq)] pub enum DeviceCodeErrorResponseType { /// /// The authorization request is still pending as the end user hasn't /// yet completed the user-interaction steps. The client SHOULD repeat the /// access token request to the token endpoint. Before each new request, /// the client MUST wait at least the number of seconds specified by the /// "interval" parameter of the device authorization response, or 5 seconds /// if none was provided, and respect any increase in the polling interval /// required by the "slow_down" error. /// AuthorizationPending, /// /// A variant of "authorization_pending", the authorization request is /// still pending and polling should continue, but the interval MUST be /// increased by 5 seconds for this and all subsequent requests. SlowDown, /// /// The authorization request was denied. /// AccessDenied, /// /// The "device_code" has expired, and the device authorization session has /// concluded. The client MAY commence a new device authorization request /// but SHOULD wait for user interaction before restarting to avoid /// unnecessary polling. ExpiredToken, /// /// A Basic response type /// Basic(BasicErrorResponseType), } impl DeviceCodeErrorResponseType { fn from_str(s: &str) -> Self { match BasicErrorResponseType::from_str(s) { BasicErrorResponseType::Extension(ext) => match ext.as_str() { "authorization_pending" => DeviceCodeErrorResponseType::AuthorizationPending, "slow_down" => DeviceCodeErrorResponseType::SlowDown, "access_denied" => DeviceCodeErrorResponseType::AccessDenied, "expired_token" => DeviceCodeErrorResponseType::ExpiredToken, _ => DeviceCodeErrorResponseType::Basic(BasicErrorResponseType::Extension(ext)), }, basic => DeviceCodeErrorResponseType::Basic(basic), } } } impl AsRef for DeviceCodeErrorResponseType { fn as_ref(&self) -> &str { match self { DeviceCodeErrorResponseType::AuthorizationPending => "authorization_pending", DeviceCodeErrorResponseType::SlowDown => "slow_down", DeviceCodeErrorResponseType::AccessDenied => "access_denied", DeviceCodeErrorResponseType::ExpiredToken => "expired_token", DeviceCodeErrorResponseType::Basic(basic) => basic.as_ref(), } } } impl<'de> serde::Deserialize<'de> for DeviceCodeErrorResponseType { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let variant_str = String::deserialize(deserializer)?; Ok(Self::from_str(&variant_str)) } } impl serde::ser::Serialize for DeviceCodeErrorResponseType { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(self.as_ref()) } } impl ErrorResponseType for DeviceCodeErrorResponseType {} impl Debug for DeviceCodeErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { Display::fmt(self, f) } } impl Display for DeviceCodeErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { write!(f, "{}", self.as_ref()) } } /// /// Error response specialization for device code OAuth2 implementation. /// pub type DeviceCodeErrorResponse = StandardErrorResponse; pub(crate) enum DeviceAccessTokenPollResult where TE: ErrorResponse + 'static, TR: TokenResponse, TT: TokenType, RE: Error + 'static, { ContinueWithNewPollInterval(Duration), Done(Result>, PhantomData), } oauth2-4.4.1/src/helpers.rs000064400000000000000000000250411046102023000136350ustar 00000000000000use serde::ser::{Impossible, SerializeStructVariant, SerializeTupleVariant}; use serde::{de, ser}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; use std::marker::PhantomData; /// /// Serde case-insensitive deserializer for an untagged `enum`. /// /// This function converts values to lowercase before deserializing as the `enum`. Requires the /// `#[serde(rename_all = "lowercase")]` attribute to be set on the `enum`. /// /// # Example /// /// In example below, the following JSON values all deserialize to /// `GroceryBasket { fruit_item: Fruit::Banana }`: /// /// * `{"fruit_item": "banana"}` /// * `{"fruit_item": "BANANA"}` /// * `{"fruit_item": "Banana"}` /// /// Note: this example does not compile automatically due to /// [Rust issue #29286](https://github.com/rust-lang/rust/issues/29286). /// /// ``` /// # /* /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// #[serde(rename_all = "lowercase")] /// enum Fruit { /// Apple, /// Banana, /// Orange, /// } /// /// #[derive(Deserialize)] /// struct GroceryBasket { /// #[serde(deserialize_with = "helpers::deserialize_untagged_enum_case_insensitive")] /// fruit_item: Fruit, /// } /// # */ /// ``` /// pub fn deserialize_untagged_enum_case_insensitive<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de>, D: Deserializer<'de>, { use serde::de::Error; use serde_json::Value; T::deserialize(Value::String( String::deserialize(deserializer)?.to_lowercase(), )) .map_err(Error::custom) } /// /// Serde space-delimited string deserializer for a `Vec`. /// /// This function splits a JSON string at each space character into a `Vec` . /// /// # Example /// /// In example below, the JSON value `{"items": "foo bar baz"}` would deserialize to: /// /// ``` /// # struct GroceryBasket { /// # items: Vec, /// # } /// GroceryBasket { /// items: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()] /// }; /// ``` /// /// Note: this example does not compile automatically due to /// [Rust issue #29286](https://github.com/rust-lang/rust/issues/29286). /// /// ``` /// # /* /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct GroceryBasket { /// #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] /// items: Vec, /// } /// # */ /// ``` /// pub fn deserialize_space_delimited_vec<'de, T, D>(deserializer: D) -> Result where T: Default + Deserialize<'de>, D: Deserializer<'de>, { use serde::de::Error; use serde_json::Value; if let Some(space_delimited) = Option::::deserialize(deserializer)? { let entries = space_delimited .split(' ') .map(|s| Value::String(s.to_string())) .collect(); T::deserialize(Value::Array(entries)).map_err(Error::custom) } else { // If the JSON value is null, use the default value. Ok(T::default()) } } /// /// Deserializes a string or array of strings into an array of strings /// pub fn deserialize_optional_string_or_vec_string<'de, D>( deserializer: D, ) -> Result>, D::Error> where D: Deserializer<'de>, { struct StringOrVec(PhantomData>); impl<'de> de::Visitor<'de> for StringOrVec { type Value = Option>; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or list of strings") } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(Some(vec![value.to_owned()])) } fn visit_none(self) -> Result where E: de::Error, { Ok(None) } fn visit_unit(self) -> Result where E: de::Error, { Ok(None) } fn visit_seq(self, visitor: S) -> Result where S: de::SeqAccess<'de>, { Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)).map(Some) } } deserializer.deserialize_any(StringOrVec(PhantomData)) } /// /// Serde space-delimited string serializer for an `Option>`. /// /// This function serializes a string vector into a single space-delimited string. /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` /// in the case of JSON serialization). /// pub fn serialize_space_delimited_vec( vec_opt: &Option>, serializer: S, ) -> Result where T: AsRef, S: Serializer, { if let Some(ref vec) = *vec_opt { let space_delimited = vec.iter().map(|s| s.as_ref()).collect::>().join(" "); serializer.serialize_str(&space_delimited) } else { serializer.serialize_none() } } /// /// Serde string serializer for an enum. ///< /// Source: /// [https://github.com/serde-rs/serde/issues/553](https://github.com/serde-rs/serde/issues/553) /// pub fn variant_name(t: &T) -> &'static str { #[derive(Debug)] struct NotEnum; type Result = std::result::Result; impl std::error::Error for NotEnum { fn description(&self) -> &str { "not struct" } } impl std::fmt::Display for NotEnum { fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result { unimplemented!() } } impl ser::Error for NotEnum { fn custom(_msg: T) -> Self { NotEnum } } struct VariantName; impl Serializer for VariantName { type Ok = &'static str; type Error = NotEnum; type SerializeSeq = Impossible; type SerializeTuple = Impossible; type SerializeTupleStruct = Impossible; type SerializeTupleVariant = Enum; type SerializeMap = Impossible; type SerializeStruct = Impossible; type SerializeStructVariant = Enum; fn serialize_bool(self, _v: bool) -> Result { Err(NotEnum) } fn serialize_i8(self, _v: i8) -> Result { Err(NotEnum) } fn serialize_i16(self, _v: i16) -> Result { Err(NotEnum) } fn serialize_i32(self, _v: i32) -> Result { Err(NotEnum) } fn serialize_i64(self, _v: i64) -> Result { Err(NotEnum) } fn serialize_u8(self, _v: u8) -> Result { Err(NotEnum) } fn serialize_u16(self, _v: u16) -> Result { Err(NotEnum) } fn serialize_u32(self, _v: u32) -> Result { Err(NotEnum) } fn serialize_u64(self, _v: u64) -> Result { Err(NotEnum) } fn serialize_f32(self, _v: f32) -> Result { Err(NotEnum) } fn serialize_f64(self, _v: f64) -> Result { Err(NotEnum) } fn serialize_char(self, _v: char) -> Result { Err(NotEnum) } fn serialize_str(self, _v: &str) -> Result { Err(NotEnum) } fn serialize_bytes(self, _v: &[u8]) -> Result { Err(NotEnum) } fn serialize_none(self) -> Result { Err(NotEnum) } fn serialize_some(self, _value: &T) -> Result { Err(NotEnum) } fn serialize_unit(self) -> Result { Err(NotEnum) } fn serialize_unit_struct(self, _name: &'static str) -> Result { Err(NotEnum) } fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, ) -> Result { Ok(variant) } fn serialize_newtype_struct( self, _name: &'static str, _value: &T, ) -> Result { Err(NotEnum) } fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _value: &T, ) -> Result { Ok(variant) } fn serialize_seq(self, _len: Option) -> Result { Err(NotEnum) } fn serialize_tuple(self, _len: usize) -> Result { Err(NotEnum) } fn serialize_tuple_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(NotEnum) } fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _len: usize, ) -> Result { Ok(Enum(variant)) } fn serialize_map(self, _len: Option) -> Result { Err(NotEnum) } fn serialize_struct( self, _name: &'static str, _len: usize, ) -> Result { Err(NotEnum) } fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _len: usize, ) -> Result { Ok(Enum(variant)) } } struct Enum(&'static str); impl SerializeStructVariant for Enum { type Ok = &'static str; type Error = NotEnum; fn serialize_field( &mut self, _key: &'static str, _value: &T, ) -> Result<()> { Ok(()) } fn end(self) -> Result { Ok(self.0) } } impl SerializeTupleVariant for Enum { type Ok = &'static str; type Error = NotEnum; fn serialize_field(&mut self, _value: &T) -> Result<()> { Ok(()) } fn end(self) -> Result { Ok(self.0) } } t.serialize(VariantName).unwrap() } oauth2-4.4.1/src/lib.rs000064400000000000000000003173031046102023000127460ustar 00000000000000#![warn(missing_docs)] //! //! An extensible, strongly-typed implementation of OAuth2 //! ([RFC 6749](https://tools.ietf.org/html/rfc6749)) including token introspection ([RFC 7662](https://tools.ietf.org/html/rfc7662)) //! and token revocation ([RFC 7009](https://tools.ietf.org/html/rfc7009)). //! //! # Contents //! * [Importing `oauth2`: selecting an HTTP client interface](#importing-oauth2-selecting-an-http-client-interface) //! * [Getting started: Authorization Code Grant w/ PKCE](#getting-started-authorization-code-grant-w-pkce) //! * [Example: Synchronous (blocking) API](#example-synchronous-blocking-api) //! * [Example: Asynchronous API](#example-asynchronous-api) //! * [Implicit Grant](#implicit-grant) //! * [Resource Owner Password Credentials Grant](#resource-owner-password-credentials-grant) //! * [Client Credentials Grant](#client-credentials-grant) //! * [Device Code Flow](#device-code-flow) //! * [Other examples](#other-examples) //! * [Contributed Examples](#contributed-examples) //! //! # Importing `oauth2`: selecting an HTTP client interface //! //! This library offers a flexible HTTP client interface with two modes: //! * **Synchronous (blocking)** //! * **Asynchronous** //! //! For the HTTP client modes described above, the following HTTP client implementations can be //! used: //! * **[`reqwest`]** //! //! The `reqwest` HTTP client supports both the synchronous and asynchronous modes and is enabled //! by default. //! //! Synchronous client: [`reqwest::http_client`] //! //! Asynchronous client: [`reqwest::async_http_client`] //! //! * **[`curl`]** //! //! The `curl` HTTP client only supports the synchronous HTTP client mode and can be enabled in //! `Cargo.toml` via the `curl` feature flag. //! //! Synchronous client: [`curl::http_client`] //! //! * **[`ureq`]** //! //! The `ureq` HTTP client is a simple HTTP client with minimal dependencies. It only supports //! the synchronous HTTP client mode and can be enabled in `Cargo.toml` via the `ureq` feature //! flag. //! //! * **Custom** //! //! In addition to the clients above, users may define their own HTTP clients, which must accept //! an [`HttpRequest`] and return an [`HttpResponse`] or error. Users writing their own clients //! may wish to disable the default `reqwest` dependency by specifying //! `default-features = false` in `Cargo.toml` (replacing `...` with the desired version of this //! crate): //! ```toml //! oauth2 = { version = "...", default-features = false } //! ``` //! //! Synchronous HTTP clients should implement the following trait: //! ```rust,ignore //! FnOnce(HttpRequest) -> Result //! where RE: std::error::Error + 'static //! ``` //! //! Asynchronous HTTP clients should implement the following trait: //! ```rust,ignore //! FnOnce(HttpRequest) -> F //! where //! F: Future>, //! RE: std::error::Error + 'static //! ``` //! //! # Getting started: Authorization Code Grant w/ PKCE //! //! This is the most common OAuth2 flow. PKCE is recommended whenever the OAuth2 client has no //! client secret or has a client secret that cannot remain confidential (e.g., native, mobile, or //! client-side web applications). //! //! ## Example: Synchronous (blocking) API //! //! This example works with `oauth2`'s default feature flags, which include `reqwest`. //! //! ```rust,no_run //! use anyhow; //! use oauth2::{ //! AuthorizationCode, //! AuthUrl, //! ClientId, //! ClientSecret, //! CsrfToken, //! PkceCodeChallenge, //! RedirectUrl, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! use oauth2::reqwest::http_client; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! // Create an OAuth2 client by specifying the client ID, client secret, authorization URL and //! // token URL. //! let client = //! BasicClient::new( //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! AuthUrl::new("http://authorize".to_string())?, //! Some(TokenUrl::new("http://token".to_string())?) //! ) //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?); //! //! // Generate a PKCE challenge. //! let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); //! //! // Generate the full authorization URL. //! let (auth_url, csrf_token) = client //! .authorize_url(CsrfToken::new_random) //! // Set the desired scopes. //! .add_scope(Scope::new("read".to_string())) //! .add_scope(Scope::new("write".to_string())) //! // Set the PKCE code challenge. //! .set_pkce_challenge(pkce_challenge) //! .url(); //! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. //! println!("Browse to: {}", auth_url); //! //! // Once the user has been redirected to the redirect URL, you'll have access to the //! // authorization code. For security reasons, your code should verify that the `state` //! // parameter returned by the server matches `csrf_state`. //! //! // Now you can trade it for an access token. //! let token_result = //! client //! .exchange_code(AuthorizationCode::new("some authorization code".to_string())) //! // Set the PKCE code verifier. //! .set_pkce_verifier(pkce_verifier) //! .request(http_client)?; //! //! // Unwrapping token_result will either produce a Token or a RequestTokenError. //! # Ok(()) //! # } //! ``` //! //! ## Example: Asynchronous API //! //! The example below uses async/await: //! //! ```rust,no_run //! use anyhow; //! use oauth2::{ //! AuthorizationCode, //! AuthUrl, //! ClientId, //! ClientSecret, //! CsrfToken, //! PkceCodeChallenge, //! RedirectUrl, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest")] //! use oauth2::reqwest::async_http_client; //! use url::Url; //! //! # #[cfg(feature = "reqwest")] //! # async fn err_wrapper() -> Result<(), anyhow::Error> { //! // Create an OAuth2 client by specifying the client ID, client secret, authorization URL and //! // token URL. //! let client = //! BasicClient::new( //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! AuthUrl::new("http://authorize".to_string())?, //! Some(TokenUrl::new("http://token".to_string())?) //! ) //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?); //! //! // Generate a PKCE challenge. //! let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); //! //! // Generate the full authorization URL. //! let (auth_url, csrf_token) = client //! .authorize_url(CsrfToken::new_random) //! // Set the desired scopes. //! .add_scope(Scope::new("read".to_string())) //! .add_scope(Scope::new("write".to_string())) //! // Set the PKCE code challenge. //! .set_pkce_challenge(pkce_challenge) //! .url(); //! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. //! println!("Browse to: {}", auth_url); //! //! // Once the user has been redirected to the redirect URL, you'll have access to the //! // authorization code. For security reasons, your code should verify that the `state` //! // parameter returned by the server matches `csrf_state`. //! //! // Now you can trade it for an access token. //! let token_result = client //! .exchange_code(AuthorizationCode::new("some authorization code".to_string())) //! // Set the PKCE code verifier. //! .set_pkce_verifier(pkce_verifier) //! .request_async(async_http_client) //! .await?; //! //! // Unwrapping token_result will either produce a Token or a RequestTokenError. //! # Ok(()) //! # } //! ``` //! //! # Implicit Grant //! //! This flow fetches an access token directly from the authorization endpoint. Be sure to //! understand the security implications of this flow before using it. In most cases, the //! Authorization Code Grant flow is preferable to the Implicit Grant flow. //! //! ## Example //! //! ```rust,no_run //! use anyhow; //! use oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! CsrfToken, //! RedirectUrl, //! Scope //! }; //! use oauth2::basic::BasicClient; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let client = //! BasicClient::new( //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! AuthUrl::new("http://authorize".to_string())?, //! None //! ); //! //! // Generate the full authorization URL. //! let (auth_url, csrf_token) = client //! .authorize_url(CsrfToken::new_random) //! .use_implicit_flow() //! .url(); //! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. //! println!("Browse to: {}", auth_url); //! //! // Once the user has been redirected to the redirect URL, you'll have the access code. //! // For security reasons, your code should verify that the `state` parameter returned by the //! // server matches `csrf_state`. //! //! # Ok(()) //! # } //! ``` //! //! # Resource Owner Password Credentials Grant //! //! You can ask for a *password* access token by calling the `Client::exchange_password` method, //! while including the username and password. //! //! ## Example //! //! ```rust,no_run //! use anyhow; //! use oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! ResourceOwnerPassword, //! ResourceOwnerUsername, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! use oauth2::reqwest::http_client; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let client = //! BasicClient::new( //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! AuthUrl::new("http://authorize".to_string())?, //! Some(TokenUrl::new("http://token".to_string())?) //! ); //! //! let token_result = //! client //! .exchange_password( //! &ResourceOwnerUsername::new("user".to_string()), //! &ResourceOwnerPassword::new("pass".to_string()) //! ) //! .add_scope(Scope::new("read".to_string())) //! .request(http_client)?; //! # Ok(()) //! # } //! ``` //! //! # Client Credentials Grant //! //! You can ask for a *client credentials* access token by calling the //! `Client::exchange_client_credentials` method. //! //! ## Example //! //! ```rust,no_run //! use anyhow; //! use oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! use oauth2::reqwest::http_client; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let client = //! BasicClient::new( //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! AuthUrl::new("http://authorize".to_string())?, //! Some(TokenUrl::new("http://token".to_string())?), //! ); //! //! let token_result = client //! .exchange_client_credentials() //! .add_scope(Scope::new("read".to_string())) //! .request(http_client)?; //! # Ok(()) //! # } //! ``` //! //! # Device Code Flow //! //! Device Code Flow allows users to sign in on browserless or input-constrained //! devices. This is a two-stage process; first a user-code and verification //! URL are obtained by using the `Client::exchange_client_credentials` //! method. Those are displayed to the user, then are used in a second client //! to poll the token endpoint for a token. //! //! ## Example //! //! ```rust,no_run //! use anyhow; //! use oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! DeviceAuthorizationUrl, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! use oauth2::devicecode::StandardDeviceAuthorizationResponse; //! use oauth2::reqwest::http_client; //! use url::Url; //! //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let device_auth_url = DeviceAuthorizationUrl::new("http://deviceauth".to_string())?; //! let client = //! BasicClient::new( //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! AuthUrl::new("http://authorize".to_string())?, //! Some(TokenUrl::new("http://token".to_string())?), //! ) //! .set_device_authorization_url(device_auth_url); //! //! let details: StandardDeviceAuthorizationResponse = client //! .exchange_device_code()? //! .add_scope(Scope::new("read".to_string())) //! .request(http_client)?; //! //! println!( //! "Open this URL in your browser:\n{}\nand enter the code: {}", //! details.verification_uri().to_string(), //! details.user_code().secret().to_string() //! ); //! //! let token_result = //! client //! .exchange_device_access_token(&details) //! .request(http_client, std::thread::sleep, None)?; //! //! # Ok(()) //! # } //! ``` //! //! # Other examples //! //! More specific implementations are available as part of the examples: //! //! - [Google](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/google.rs) (includes token revocation) //! - [Github](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/github.rs) //! - [Microsoft Device Code Flow (async)](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/microsoft_devicecode.rs) //! - [Microsoft Graph](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/msgraph.rs) //! - [Wunderlist](https://github.com/ramosbugs/oauth2-rs/blob/main/examples/wunderlist.rs) //! //! ## Contributed Examples //! //! - [`actix-web-oauth2`](https://github.com/pka/actix-web-oauth2) (version 2.x of this crate) //! use std::borrow::Cow; use std::error::Error; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; use std::future::Future; use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use chrono::serde::ts_seconds_option; use chrono::{DateTime, Utc}; use http::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::status::StatusCode; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use url::{form_urlencoded, Url}; use crate::devicecode::DeviceAccessTokenPollResult; /// /// Basic OAuth2 implementation with no extensions /// ([RFC 6749](https://tools.ietf.org/html/rfc6749)). /// pub mod basic; /// /// HTTP client backed by the [curl](https://crates.io/crates/curl) crate. /// Requires "curl" feature. /// #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] pub mod curl; #[cfg(all(feature = "curl", target_arch = "wasm32"))] compile_error!("wasm32 is not supported with the `curl` feature. Use the `reqwest` backend or a custom backend for wasm32 support"); /// /// Device Code Flow OAuth2 implementation /// ([RFC 8628](https://tools.ietf.org/html/rfc8628)). /// pub mod devicecode; /// /// OAuth 2.0 Token Revocation implementation /// ([RFC 7009](https://tools.ietf.org/html/rfc7009)). /// pub mod revocation; /// /// Helper methods used by OAuth2 implementations/extensions. /// pub mod helpers; /// /// HTTP client backed by the [reqwest](https://crates.io/crates/reqwest) crate. /// Requires "reqwest" feature. /// #[cfg(feature = "reqwest")] pub mod reqwest; #[cfg(test)] mod tests; mod types; /// /// HTTP client backed by the [ureq](https://crates.io/crates/ureq) crate. /// Requires "ureq" feature. /// #[cfg(feature = "ureq")] pub mod ureq; /// /// Public re-exports of types used for HTTP client interfaces. /// pub use http; pub use url; pub use devicecode::{ DeviceAuthorizationResponse, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, ExtraDeviceAuthorizationFields, StandardDeviceAuthorizationResponse, }; pub use types::{ AccessToken, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, DeviceAuthorizationUrl, DeviceCode, EndUserVerificationUrl, IntrospectionUrl, PkceCodeChallenge, PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, ResourceOwnerPassword, ResourceOwnerUsername, ResponseType, RevocationUrl, Scope, TokenUrl, UserCode, VerificationUriComplete, }; pub use revocation::{RevocableToken, RevocationErrorResponseType, StandardRevocableToken}; const CONTENT_TYPE_JSON: &str = "application/json"; const CONTENT_TYPE_FORMENCODED: &str = "application/x-www-form-urlencoded"; /// /// There was a problem configuring the request. /// #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum ConfigurationError { /// /// The endpoint URL tp be contacted is missing. /// #[error("No {0} endpoint URL specified")] MissingUrl(&'static str), /// /// The endpoint URL to be contacted MUST be HTTPS. /// #[error("Scheme for {0} endpoint URL must be HTTPS")] InsecureUrl(&'static str), } /// /// Indicates whether requests to the authorization server should use basic authentication or /// include the parameters in the request body for requests in which either is valid. /// /// The default AuthType is *BasicAuth*, following the recommendation of /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). /// #[derive(Clone, Debug)] #[non_exhaustive] pub enum AuthType { /// The client_id and client_secret (if set) will be included as part of the request body. RequestBody, /// The client_id and client_secret will be included using the basic auth authentication scheme. BasicAuth, } /// /// Stores the configuration for an OAuth2 client. /// /// # Error Types /// /// To enable compile time verification that only the correct and complete set of errors for the `Client` function being /// invoked are exposed to the caller, the `Client` type is specialized on multiple implementations of the /// [`ErrorResponse`] trait. The exact [`ErrorResponse`] implementation returned varies by the RFC that the invoked /// `Client` function implements: /// /// - Generic type `TE` (aka Token Error) for errors defined by [RFC 6749 OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749). /// - Generic type `TRE` (aka Token Revocation Error) for errors defined by [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009). /// /// For example when revoking a token, error code `unsupported_token_type` (from RFC 7009) may be returned: /// ```rust /// # use thiserror::Error; /// # use http::status::StatusCode; /// # use http::header::{HeaderValue, CONTENT_TYPE}; /// # use oauth2::{*, basic::*}; /// # let client = BasicClient::new( /// # ClientId::new("aaa".to_string()), /// # Some(ClientSecret::new("bbb".to_string())), /// # AuthUrl::new("https://example.com/auth".to_string()).unwrap(), /// # Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), /// # ) /// # .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); /// # /// # #[derive(Debug, Error)] /// # enum FakeError { /// # #[error("error")] /// # Err, /// # } /// # /// # let http_client = |_| -> Result { /// # Ok(HttpResponse { /// # status_code: StatusCode::BAD_REQUEST, /// # headers: vec![( /// # CONTENT_TYPE, /// # HeaderValue::from_str("application/json").unwrap(), /// # )] /// # .into_iter() /// # .collect(), /// # body: "{\"error\": \"unsupported_token_type\", \"error_description\": \"stuff happened\", \ /// # \"error_uri\": \"https://errors\"}" /// # .to_string() /// # .into_bytes(), /// # }) /// # }; /// # /// let res = client /// .revoke_token(AccessToken::new("some token".to_string()).into()) /// .unwrap() /// .request(http_client); /// /// assert!(matches!(res, Err( /// RequestTokenError::ServerResponse(err)) if matches!(err.error(), /// RevocationErrorResponseType::UnsupportedTokenType))); /// ``` /// #[derive(Clone, Debug)] pub struct Client where TE: ErrorResponse, TR: TokenResponse, TT: TokenType, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse, { client_id: ClientId, client_secret: Option, auth_url: AuthUrl, auth_type: AuthType, token_url: Option, redirect_url: Option, introspection_url: Option, revocation_url: Option, device_authorization_url: Option, phantom: PhantomData<(TE, TR, TT, TIR, RT, TRE)>, } impl Client where TE: ErrorResponse + 'static, TR: TokenResponse, TT: TokenType, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, { /// /// Initializes an OAuth2 client with the fields common to most OAuth2 flows. /// /// # Arguments /// /// * `client_id` - Client ID /// * `client_secret` - Optional client secret. A client secret is generally used for private /// (server-side) OAuth2 clients and omitted from public (client-side or native app) OAuth2 /// clients (see [RFC 8252](https://tools.ietf.org/html/rfc8252)). /// * `auth_url` - Authorization endpoint: used by the client to obtain authorization from /// the resource owner via user-agent redirection. This URL is used in all standard OAuth2 /// flows except the [Resource Owner Password Credentials /// Grant](https://tools.ietf.org/html/rfc6749#section-4.3) and the /// [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). /// * `token_url` - Token endpoint: used by the client to exchange an authorization grant /// (code) for an access token, typically with client authentication. This URL is used in /// all standard OAuth2 flows except the /// [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2). If this value is set /// to `None`, the `exchange_*` methods will return `Err(RequestTokenError::Other(_))`. /// pub fn new( client_id: ClientId, client_secret: Option, auth_url: AuthUrl, token_url: Option, ) -> Self { Client { client_id, client_secret, auth_url, auth_type: AuthType::BasicAuth, token_url, redirect_url: None, introspection_url: None, revocation_url: None, device_authorization_url: None, phantom: PhantomData, } } /// /// Configures the type of client authentication used for communicating with the authorization /// server. /// /// The default is to use HTTP Basic authentication, as recommended in /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). Note that /// if a client secret is omitted (i.e., `client_secret` is set to `None` when calling /// [`Client::new`]), [`AuthType::RequestBody`] is used regardless of the `auth_type` passed to /// this function. /// pub fn set_auth_type(mut self, auth_type: AuthType) -> Self { self.auth_type = auth_type; self } /// /// Sets the redirect URL used by the authorization endpoint. /// pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self { self.redirect_url = Some(redirect_url); self } /// /// Sets the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) /// introspection endpoint. /// pub fn set_introspection_uri(mut self, introspection_url: IntrospectionUrl) -> Self { self.introspection_url = Some(introspection_url); self } /// /// Sets the revocation URL for contacting the revocation endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)). /// /// See: [`revoke_token()`](Self::revoke_token()) /// pub fn set_revocation_uri(mut self, revocation_url: RevocationUrl) -> Self { self.revocation_url = Some(revocation_url); self } /// /// Sets the the device authorization URL used by the device authorization endpoint. /// Used for Device Code Flow, as per [RFC 8628](https://tools.ietf.org/html/rfc8628). /// pub fn set_device_authorization_url( mut self, device_authorization_url: DeviceAuthorizationUrl, ) -> Self { self.device_authorization_url = Some(device_authorization_url); self } /// /// Generates an authorization URL for a new authorization request. /// /// # Arguments /// /// * `state_fn` - A function that returns an opaque value used by the client to maintain state /// between the request and callback. The authorization server includes this value when /// redirecting the user-agent back to the client. /// /// # Security Warning /// /// Callers should use a fresh, unpredictable `state` for each authorization request and verify /// that this value matches the `state` parameter passed by the authorization server to the /// redirect URI. Doing so mitigates /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) /// attacks. To disable CSRF protections (NOT recommended), use `insecure::authorize_url` /// instead. /// pub fn authorize_url(&self, state_fn: S) -> AuthorizationRequest where S: FnOnce() -> CsrfToken, { AuthorizationRequest { auth_url: &self.auth_url, client_id: &self.client_id, extra_params: Vec::new(), pkce_challenge: None, redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), response_type: "code".into(), scopes: Vec::new(), state: state_fn(), } } /// /// Exchanges a code produced by a successful authorization process with an access token. /// /// Acquires ownership of the `code` because authorization codes may only be used once to /// retrieve an access token from the authorization server. /// /// See . /// pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest { CodeTokenRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), code, extra_params: Vec::new(), pkce_verifier: None, token_url: self.token_url.as_ref(), redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), _phantom: PhantomData, } } /// /// Requests an access token for the *password* grant type. /// /// See . /// pub fn exchange_password<'a, 'b>( &'a self, username: &'b ResourceOwnerUsername, password: &'b ResourceOwnerPassword, ) -> PasswordTokenRequest<'b, TE, TR, TT> where 'a: 'b, { PasswordTokenRequest::<'b> { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), username, password, extra_params: Vec::new(), scopes: Vec::new(), token_url: self.token_url.as_ref(), _phantom: PhantomData, } } /// /// Requests an access token for the *client credentials* grant type. /// /// See . /// pub fn exchange_client_credentials(&self) -> ClientCredentialsTokenRequest { ClientCredentialsTokenRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), scopes: Vec::new(), token_url: self.token_url.as_ref(), _phantom: PhantomData, } } /// /// Exchanges a refresh token for an access token /// /// See . /// pub fn exchange_refresh_token<'a, 'b>( &'a self, refresh_token: &'b RefreshToken, ) -> RefreshTokenRequest<'b, TE, TR, TT> where 'a: 'b, { RefreshTokenRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), refresh_token, scopes: Vec::new(), token_url: self.token_url.as_ref(), _phantom: PhantomData, } } /// /// Perform a device authorization request as per /// . /// pub fn exchange_device_code( &self, ) -> Result, ConfigurationError> { Ok(DeviceAuthorizationRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), scopes: Vec::new(), device_authorization_url: self .device_authorization_url .as_ref() .ok_or(ConfigurationError::MissingUrl("device authorization_url"))?, _phantom: PhantomData, }) } /// /// Perform a device access token request as per /// . /// pub fn exchange_device_access_token<'a, 'b, 'c, EF>( &'a self, auth_response: &'b DeviceAuthorizationResponse, ) -> DeviceAccessTokenRequest<'b, 'c, TR, TT, EF> where 'a: 'b, EF: ExtraDeviceAuthorizationFields, { DeviceAccessTokenRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), token_url: self.token_url.as_ref(), dev_auth_resp: auth_response, time_fn: Arc::new(Utc::now), max_backoff_interval: None, _phantom: PhantomData, } } /// /// Query the authorization server [`RFC 7662 compatible`](https://tools.ietf.org/html/rfc7662) introspection /// endpoint to determine the set of metadata for a previously received token. /// /// Requires that [`set_introspection_uri()`](Self::set_introspection_uri()) have already been called to set the /// introspection endpoint URL. /// /// Attempting to submit the generated request without calling [`set_introspection_uri()`](Self::set_introspection_uri()) /// first will result in an error. /// pub fn introspect<'a>( &'a self, token: &'a AccessToken, ) -> Result, ConfigurationError> { Ok(IntrospectionRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), introspection_url: self .introspection_url .as_ref() .ok_or(ConfigurationError::MissingUrl("introspection"))?, token, token_type_hint: None, _phantom: PhantomData, }) } /// /// Attempts to revoke the given previously received token using an [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009) /// compatible endpoint. /// /// Requires that [`set_revocation_uri()`](Self::set_revocation_uri()) have already been called to set the /// revocation endpoint URL. /// /// Attempting to submit the generated request without calling [`set_revocation_uri()`](Self::set_revocation_uri()) /// first will result in an error. /// pub fn revoke_token( &self, token: RT, ) -> Result, ConfigurationError> { // https://tools.ietf.org/html/rfc7009#section-2 states: // "The client requests the revocation of a particular token by making an // HTTP POST request to the token revocation endpoint URL. This URL // MUST conform to the rules given in [RFC6749], Section 3.1. Clients // MUST verify that the URL is an HTTPS URL." let revocation_url = match self.revocation_url.as_ref() { Some(url) if url.url().scheme() == "https" => Ok(url), Some(_) => Err(ConfigurationError::InsecureUrl("revocation")), None => Err(ConfigurationError::MissingUrl("revocation")), }?; Ok(RevocationRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), revocation_url, token, _phantom: PhantomData, }) } /// /// Returns the Client ID. /// pub fn client_id(&self) -> &ClientId { &self.client_id } /// /// Returns the authorization endpoint. /// pub fn auth_url(&self) -> &AuthUrl { &self.auth_url } /// /// Returns the type of client authentication used for communicating with the authorization /// server. /// pub fn auth_type(&self) -> &AuthType { &self.auth_type } /// /// Returns the token endpoint. /// pub fn token_url(&self) -> Option<&TokenUrl> { self.token_url.as_ref() } /// /// Returns the redirect URL used by the authorization endpoint. /// pub fn redirect_url(&self) -> Option<&RedirectUrl> { self.redirect_url.as_ref() } /// /// Returns the introspection URL for contacting the ([RFC 7662](https://tools.ietf.org/html/rfc7662)) /// introspection endpoint. /// pub fn introspection_url(&self) -> Option<&IntrospectionUrl> { self.introspection_url.as_ref() } /// /// Returns the revocation URL for contacting the revocation endpoint ([RFC 7009](https://tools.ietf.org/html/rfc7009)). /// /// See: [`revoke_token()`](Self::revoke_token()) /// pub fn revocation_url(&self) -> Option<&RevocationUrl> { self.revocation_url.as_ref() } /// /// Returns the the device authorization URL used by the device authorization endpoint. /// pub fn device_authorization_url(&self) -> Option<&DeviceAuthorizationUrl> { self.device_authorization_url.as_ref() } } /// /// A request to the authorization endpoint /// #[derive(Debug)] pub struct AuthorizationRequest<'a> { auth_url: &'a AuthUrl, client_id: &'a ClientId, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pkce_challenge: Option, redirect_url: Option>, response_type: Cow<'a, str>, scopes: Vec>, state: CsrfToken, } impl<'a> AuthorizationRequest<'a> { /// /// Appends a new scope to the authorization URL. /// pub fn add_scope(mut self, scope: Scope) -> Self { self.scopes.push(Cow::Owned(scope)); self } /// /// Appends a collection of scopes to the token request. /// pub fn add_scopes(mut self, scopes: I) -> Self where I: IntoIterator, { self.scopes.extend(scopes.into_iter().map(Cow::Owned)); self } /// /// Appends an extra param to the authorization URL. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Enables the [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2) flow. /// pub fn use_implicit_flow(mut self) -> Self { self.response_type = "token".into(); self } /// /// Enables custom flows other than the `code` and `token` (implicit flow) grant. /// pub fn set_response_type(mut self, response_type: &ResponseType) -> Self { self.response_type = (&**response_type).to_owned().into(); self } /// /// Enables the use of [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) /// (PKCE). /// /// PKCE is *highly recommended* for all public clients (i.e., those for which there /// is no client secret or for which the client secret is distributed with the client, /// such as in a native, mobile app, or browser app). /// pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self { self.pkce_challenge = Some(pkce_code_challenge); self } /// /// Overrides the `redirect_url` to the one specified. /// pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self { self.redirect_url = Some(redirect_url); self } /// /// Returns the full authorization URL and CSRF state for this authorization /// request. /// pub fn url(self) -> (Url, CsrfToken) { let scopes = self .scopes .iter() .map(|s| s.to_string()) .collect::>() .join(" "); let url = { let mut pairs: Vec<(&str, &str)> = vec![ ("response_type", self.response_type.as_ref()), ("client_id", &self.client_id), ("state", self.state.secret()), ]; if let Some(ref pkce_challenge) = self.pkce_challenge { pairs.push(("code_challenge", &pkce_challenge.as_str())); pairs.push(("code_challenge_method", &pkce_challenge.method().as_str())); } if let Some(ref redirect_url) = self.redirect_url { pairs.push(("redirect_uri", redirect_url.as_str())); } if !scopes.is_empty() { pairs.push(("scope", &scopes)); } let mut url: Url = self.auth_url.url().to_owned(); url.query_pairs_mut() .extend_pairs(pairs.iter().map(|&(k, v)| (k, &v[..]))); url.query_pairs_mut() .extend_pairs(self.extra_params.iter().cloned()); url }; (url, self.state) } } /// /// An HTTP request. /// #[derive(Clone, Debug)] pub struct HttpRequest { // These are all owned values so that the request can safely be passed between // threads. /// URL to which the HTTP request is being made. pub url: Url, /// HTTP request method for this request. pub method: http::method::Method, /// HTTP request headers to send. pub headers: HeaderMap, /// HTTP request body (typically for POST requests only). pub body: Vec, } /// /// An HTTP response. /// #[derive(Clone, Debug)] pub struct HttpResponse { /// HTTP status code returned by the server. pub status_code: http::status::StatusCode, /// HTTP response headers returned by the server. pub headers: HeaderMap, /// HTTP response body returned by the server. pub body: Vec, } /// /// A request to exchange an authorization code for an access token. /// /// See . /// #[derive(Debug)] pub struct CodeTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse, TR: TokenResponse, TT: TokenType, { auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, code: AuthorizationCode, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pkce_verifier: Option, token_url: Option<&'a TokenUrl>, redirect_url: Option>, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> CodeTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse + 'static, TR: TokenResponse, TT: TokenType, { /// /// Appends an extra param to the token request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Completes the [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) /// (PKCE) protocol flow. /// /// This method must be called if `set_pkce_challenge` was used during the authorization /// request. /// pub fn set_pkce_verifier(mut self, pkce_verifier: PkceCodeVerifier) -> Self { self.pkce_verifier = Some(pkce_verifier); self } /// /// Overrides the `redirect_url` to the one specified. /// pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self { self.redirect_url = Some(redirect_url); self } fn prepare_request(self) -> Result> where RE: Error + 'static, { let mut params = vec![ ("grant_type", "authorization_code"), ("code", self.code.secret()), ]; if let Some(ref pkce_verifier) = self.pkce_verifier { params.push(("code_verifier", pkce_verifier.secret())); } Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, self.redirect_url, None, self.token_url .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? .url(), params, )) } /// /// Synchronously sends the request to the authorization server and awaits a response. /// pub fn request(self, http_client: F) -> Result> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response) } /// /// Asynchronously sends the request to the authorization server and returns a Future. /// pub async fn request_async( self, http_client: C, ) -> Result> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response(http_response) } } /// /// A request to exchange a refresh token for an access token. /// /// See . /// #[derive(Debug)] pub struct RefreshTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse, TR: TokenResponse, TT: TokenType, { auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, refresh_token: &'a RefreshToken, scopes: Vec>, token_url: Option<&'a TokenUrl>, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> RefreshTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse + 'static, TR: TokenResponse, TT: TokenType, { /// /// Appends an extra param to the token request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Appends a new scope to the token request. /// pub fn add_scope(mut self, scope: Scope) -> Self { self.scopes.push(Cow::Owned(scope)); self } /// /// Appends a collection of scopes to the token request. /// pub fn add_scopes(mut self, scopes: I) -> Self where I: IntoIterator, { self.scopes.extend(scopes.into_iter().map(Cow::Owned)); self } /// /// Synchronously sends the request to the authorization server and awaits a response. /// pub fn request(self, http_client: F) -> Result> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response) } /// /// Asynchronously sends the request to the authorization server and awaits a response. /// pub async fn request_async( self, http_client: C, ) -> Result> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response(http_response) } fn prepare_request(&self) -> Result> where RE: Error + 'static, { Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.token_url .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? .url(), vec![ ("grant_type", "refresh_token"), ("refresh_token", self.refresh_token.secret()), ], )) } } /// /// A request to exchange resource owner credentials for an access token. /// /// See . /// #[derive(Debug)] pub struct PasswordTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse, TR: TokenResponse, TT: TokenType, { auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, scopes: Vec>, token_url: Option<&'a TokenUrl>, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> PasswordTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse + 'static, TR: TokenResponse, TT: TokenType, { /// /// Appends an extra param to the token request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Appends a new scope to the token request. /// pub fn add_scope(mut self, scope: Scope) -> Self { self.scopes.push(Cow::Owned(scope)); self } /// /// Appends a collection of scopes to the token request. /// pub fn add_scopes(mut self, scopes: I) -> Self where I: IntoIterator, { self.scopes.extend(scopes.into_iter().map(Cow::Owned)); self } /// /// Synchronously sends the request to the authorization server and awaits a response. /// pub fn request(self, http_client: F) -> Result> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response) } /// /// Asynchronously sends the request to the authorization server and awaits a response. /// pub async fn request_async( self, http_client: C, ) -> Result> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response(http_response) } fn prepare_request(&self) -> Result> where RE: Error + 'static, { Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.token_url .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? .url(), vec![ ("grant_type", "password"), ("username", self.username), ("password", self.password.secret()), ], )) } } /// /// A request to exchange client credentials for an access token. /// /// See . /// #[derive(Debug)] pub struct ClientCredentialsTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse, TR: TokenResponse, TT: TokenType, { auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, scopes: Vec>, token_url: Option<&'a TokenUrl>, _phantom: PhantomData<(TE, TR, TT)>, } impl<'a, TE, TR, TT> ClientCredentialsTokenRequest<'a, TE, TR, TT> where TE: ErrorResponse + 'static, TR: TokenResponse, TT: TokenType, { /// /// Appends an extra param to the token request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Appends a new scope to the token request. /// pub fn add_scope(mut self, scope: Scope) -> Self { self.scopes.push(Cow::Owned(scope)); self } /// /// Appends a collection of scopes to the token request. /// pub fn add_scopes(mut self, scopes: I) -> Self where I: IntoIterator, { self.scopes.extend(scopes.into_iter().map(Cow::Owned)); self } /// /// Synchronously sends the request to the authorization server and awaits a response. /// pub fn request(self, http_client: F) -> Result> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response) } /// /// Asynchronously sends the request to the authorization server and awaits a response. /// pub async fn request_async( self, http_client: C, ) -> Result> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response(http_response) } fn prepare_request(&self) -> Result> where RE: Error + 'static, { Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.token_url .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? .url(), vec![("grant_type", "client_credentials")], )) } } /// /// A request to introspect an access token. /// /// See . /// #[derive(Debug)] pub struct IntrospectionRequest<'a, TE, TIR, TT> where TE: ErrorResponse, TIR: TokenIntrospectionResponse, TT: TokenType, { token: &'a AccessToken, token_type_hint: Option>, auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, introspection_url: &'a IntrospectionUrl, _phantom: PhantomData<(TE, TIR, TT)>, } impl<'a, TE, TIR, TT> IntrospectionRequest<'a, TE, TIR, TT> where TE: ErrorResponse + 'static, TIR: TokenIntrospectionResponse, TT: TokenType, { /// /// Sets the optional token_type_hint parameter. /// /// See . /// /// OPTIONAL. A hint about the type of the token submitted for /// introspection. The protected resource MAY pass this parameter to /// help the authorization server optimize the token lookup. If the /// server is unable to locate the token using the given hint, it MUST /// extend its search across all of its supported token types. An /// authorization server MAY ignore this parameter, particularly if it /// is able to detect the token type automatically. Values for this /// field are defined in the "OAuth Token Type Hints" registry defined /// in OAuth Token Revocation [RFC7009](https://tools.ietf.org/html/rfc7009). /// pub fn set_token_type_hint(mut self, value: V) -> Self where V: Into>, { self.token_type_hint = Some(value.into()); self } /// /// Appends an extra param to the token introspection request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7662](https://tools.ietf.org/html/rfc7662). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } fn prepare_request(self) -> Result> where RE: Error + 'static, { let mut params: Vec<(&str, &str)> = vec![("token", self.token.secret())]; if let Some(ref token_type_hint) = self.token_type_hint { params.push(("token_type_hint", token_type_hint)); } Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, self.introspection_url.url(), params, )) } /// /// Synchronously sends the request to the authorization server and awaits a response. /// pub fn request(self, http_client: F) -> Result> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response) } /// /// Asynchronously sends the request to the authorization server and returns a Future. /// pub async fn request_async( self, http_client: C, ) -> Result> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response(http_response) } } /// /// A request to revoke a token via an [`RFC 7009`](https://tools.ietf.org/html/rfc7009#section-2.1) compatible /// endpoint. /// #[derive(Debug)] pub struct RevocationRequest<'a, RT, TE> where RT: RevocableToken, TE: ErrorResponse, { token: RT, auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, revocation_url: &'a RevocationUrl, _phantom: PhantomData<(RT, TE)>, } impl<'a, RT, TE> RevocationRequest<'a, RT, TE> where RT: RevocableToken, TE: ErrorResponse + 'static, { /// /// Appends an extra param to the token revocation request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7662](https://tools.ietf.org/html/rfc7662). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } fn prepare_request(self) -> Result> where RE: Error + 'static, { let mut params: Vec<(&str, &str)> = vec![("token", self.token.secret())]; if let Some(type_hint) = self.token.type_hint() { params.push(("token_type_hint", type_hint)); } Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, self.revocation_url.url(), params, )) } /// /// Synchronously sends the request to the authorization server and awaits a response. /// /// A successful response indicates that the server either revoked the token or the token was not known to the /// server. /// /// Error [`UnsupportedTokenType`](crate::revocation::RevocationErrorResponseType::UnsupportedTokenType) will be returned if the /// type of token type given is not supported by the server. /// pub fn request(self, http_client: F) -> Result<(), RequestTokenError> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, { // From https://tools.ietf.org/html/rfc7009#section-2.2: // "The content of the response body is ignored by the client as all // necessary information is conveyed in the response code." http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response_status_only) } /// /// Asynchronously sends the request to the authorization server and returns a Future. /// pub async fn request_async( self, http_client: C, ) -> Result<(), RequestTokenError> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response_status_only(http_response) } } #[allow(clippy::too_many_arguments)] fn endpoint_request<'a>( auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: &'a [(Cow<'a, str>, Cow<'a, str>)], redirect_url: Option>, scopes: Option<&'a Vec>>, url: &'a Url, params: Vec<(&'a str, &'a str)>, ) -> HttpRequest { let mut headers = HeaderMap::new(); headers.append(ACCEPT, HeaderValue::from_static(CONTENT_TYPE_JSON)); headers.append( CONTENT_TYPE, HeaderValue::from_static(CONTENT_TYPE_FORMENCODED), ); let scopes_opt = scopes.and_then(|scopes| { if !scopes.is_empty() { Some( scopes .iter() .map(|s| s.to_string()) .collect::>() .join(" "), ) } else { None } }); let mut params: Vec<(&str, &str)> = params; if let Some(ref scopes) = scopes_opt { params.push(("scope", scopes)); } // FIXME: add support for auth extensions? e.g., client_secret_jwt and private_key_jwt match (auth_type, client_secret) { // Basic auth only makes sense when a client secret is provided. Otherwise, always pass the // client ID in the request body. (AuthType::BasicAuth, Some(secret)) => { // Section 2.3.1 of RFC 6749 requires separately url-encoding the id and secret // before using them as HTTP Basic auth username and password. Note that this is // not standard for ordinary Basic auth, so curl won't do it for us. let urlencoded_id: String = form_urlencoded::byte_serialize(&client_id.as_bytes()).collect(); let urlencoded_secret: String = form_urlencoded::byte_serialize(secret.secret().as_bytes()).collect(); let b64_credential = base64::encode(&format!("{}:{}", &urlencoded_id, urlencoded_secret)); headers.append( AUTHORIZATION, HeaderValue::from_str(&format!("Basic {}", &b64_credential)).unwrap(), ); } (AuthType::RequestBody, _) | (AuthType::BasicAuth, None) => { params.push(("client_id", client_id)); if let Some(ref client_secret) = client_secret { params.push(("client_secret", client_secret.secret())); } } } if let Some(ref redirect_url) = redirect_url { params.push(("redirect_uri", redirect_url.as_str())); } params.extend_from_slice( extra_params .iter() .map(|&(ref k, ref v)| (k.as_ref(), v.as_ref())) .collect::>() .as_slice(), ); let body = url::form_urlencoded::Serializer::new(String::new()) .extend_pairs(params) .finish() .into_bytes(); HttpRequest { url: url.to_owned(), method: http::method::Method::POST, headers, body, } } fn endpoint_response( http_response: HttpResponse, ) -> Result> where RE: Error + 'static, TE: ErrorResponse, DO: DeserializeOwned, { check_response_status(&http_response)?; check_response_body(&http_response)?; let response_body = http_response.body.as_slice(); serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(response_body)) .map_err(|e| RequestTokenError::Parse(e, response_body.to_vec())) } fn endpoint_response_status_only( http_response: HttpResponse, ) -> Result<(), RequestTokenError> where RE: Error + 'static, TE: ErrorResponse, { check_response_status(&http_response) } fn check_response_status( http_response: &HttpResponse, ) -> Result<(), RequestTokenError> where RE: Error + 'static, TE: ErrorResponse, { if http_response.status_code != StatusCode::OK { let reason = http_response.body.as_slice(); if reason.is_empty() { return Err(RequestTokenError::Other( "Server returned empty error response".to_string(), )); } else { let error = match serde_path_to_error::deserialize::<_, TE>( &mut serde_json::Deserializer::from_slice(reason), ) { Ok(error) => RequestTokenError::ServerResponse(error), Err(error) => RequestTokenError::Parse(error, reason.to_vec()), }; return Err(error); } } Ok(()) } fn check_response_body( http_response: &HttpResponse, ) -> Result<(), RequestTokenError> where RE: Error + 'static, TE: ErrorResponse, { // Validate that the response Content-Type is JSON. http_response .headers .get(CONTENT_TYPE) .map_or(Ok(()), |content_type| // Section 3.1.1.1 of RFC 7231 indicates that media types are case insensitive and // may be followed by optional whitespace and/or a parameter (e.g., charset). // See https://tools.ietf.org/html/rfc7231#section-3.1.1.1. if content_type.to_str().ok().filter(|ct| ct.to_lowercase().starts_with(CONTENT_TYPE_JSON)).is_none() { Err( RequestTokenError::Other( format!( "Unexpected response Content-Type: {:?}, should be `{}`", content_type, CONTENT_TYPE_JSON ) ) ) } else { Ok(()) } )?; if http_response.body.is_empty() { return Err(RequestTokenError::Other( "Server returned empty response body".to_string(), )); } Ok(()) } /// /// The request for a set of verification codes from the authorization server. /// /// See . /// #[derive(Debug)] pub struct DeviceAuthorizationRequest<'a, TE> where TE: ErrorResponse, { auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, scopes: Vec>, device_authorization_url: &'a DeviceAuthorizationUrl, _phantom: PhantomData, } impl<'a, TE> DeviceAuthorizationRequest<'a, TE> where TE: ErrorResponse + 'static, { /// /// Appends an extra param to the token request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Appends a new scope to the token request. /// pub fn add_scope(mut self, scope: Scope) -> Self { self.scopes.push(Cow::Owned(scope)); self } /// /// Appends a collection of scopes to the token request. /// pub fn add_scopes(mut self, scopes: I) -> Self where I: IntoIterator, { self.scopes.extend(scopes.into_iter().map(Cow::Owned)); self } fn prepare_request(self) -> Result> where RE: Error + 'static, { Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.device_authorization_url.url(), vec![], )) } /// /// Synchronously sends the request to the authorization server and awaits a response. /// pub fn request( self, http_client: F, ) -> Result, RequestTokenError> where F: FnOnce(HttpRequest) -> Result, RE: Error + 'static, EF: ExtraDeviceAuthorizationFields, { http_client(self.prepare_request()?) .map_err(RequestTokenError::Request) .and_then(endpoint_response) } /// /// Asynchronously sends the request to the authorization server and returns a Future. /// pub async fn request_async( self, http_client: C, ) -> Result, RequestTokenError> where C: FnOnce(HttpRequest) -> F, F: Future>, RE: Error + 'static, EF: ExtraDeviceAuthorizationFields, { let http_request = self.prepare_request()?; let http_response = http_client(http_request) .await .map_err(RequestTokenError::Request)?; endpoint_response(http_response) } } /// /// The request for an device access token from the authorization server. /// /// See . /// #[derive(Clone)] pub struct DeviceAccessTokenRequest<'a, 'b, TR, TT, EF> where TR: TokenResponse, TT: TokenType, EF: ExtraDeviceAuthorizationFields, { auth_type: &'a AuthType, client_id: &'a ClientId, client_secret: Option<&'a ClientSecret>, extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, token_url: Option<&'a TokenUrl>, dev_auth_resp: &'a DeviceAuthorizationResponse, time_fn: Arc DateTime + 'b + Send + Sync>, max_backoff_interval: Option, _phantom: PhantomData<(TR, TT, EF)>, } impl<'a, 'b, TR, TT, EF> DeviceAccessTokenRequest<'a, 'b, TR, TT, EF> where TR: TokenResponse, TT: TokenType, EF: ExtraDeviceAuthorizationFields, { /// /// Appends an extra param to the token request. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). /// pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.extra_params.push((name.into(), value.into())); self } /// /// Specifies a function for returning the current time. /// /// This function is used while polling the authorization server. /// pub fn set_time_fn(mut self, time_fn: T) -> Self where T: Fn() -> DateTime + 'b + Send + Sync, { self.time_fn = Arc::new(time_fn); self } /// /// Sets the upper limit of the sleep interval to use for polling the token endpoint when the /// HTTP client returns an error (e.g., in case of connection timeout). /// pub fn set_max_backoff_interval(mut self, interval: Duration) -> Self { self.max_backoff_interval = Some(interval); self } /// /// Synchronously polls the authorization server for a response, waiting /// using a user defined sleep function. /// pub fn request( self, http_client: F, sleep_fn: S, timeout: Option, ) -> Result> where F: Fn(HttpRequest) -> Result, S: Fn(Duration), RE: Error + 'static, { // Get the request timeout and starting interval let timeout_dt = self.compute_timeout(timeout)?; let mut interval = self.dev_auth_resp.interval(); // Loop while requesting a token. loop { let now = (*self.time_fn)(); if now > timeout_dt { break Err(RequestTokenError::ServerResponse( DeviceCodeErrorResponse::new( DeviceCodeErrorResponseType::ExpiredToken, Some(String::from("This device code has expired.")), None, ), )); } match self.process_response(http_client(self.prepare_request()?), interval) { DeviceAccessTokenPollResult::ContinueWithNewPollInterval(new_interval) => { interval = new_interval } DeviceAccessTokenPollResult::Done(res, _) => break res, } // Sleep here using the provided sleep function. sleep_fn(interval); } } /// /// Asynchronously sends the request to the authorization server and awaits a response. /// pub async fn request_async( self, http_client: C, sleep_fn: S, timeout: Option, ) -> Result> where C: Fn(HttpRequest) -> F, F: Future>, S: Fn(Duration) -> SF, SF: Future, RE: Error + 'static, { // Get the request timeout and starting interval let timeout_dt = self.compute_timeout(timeout)?; let mut interval = self.dev_auth_resp.interval(); // Loop while requesting a token. loop { let now = (*self.time_fn)(); if now > timeout_dt { break Err(RequestTokenError::ServerResponse( DeviceCodeErrorResponse::new( DeviceCodeErrorResponseType::ExpiredToken, Some(String::from("This device code has expired.")), None, ), )); } match self.process_response(http_client(self.prepare_request()?).await, interval) { DeviceAccessTokenPollResult::ContinueWithNewPollInterval(new_interval) => { interval = new_interval } DeviceAccessTokenPollResult::Done(res, _) => break res, } // Sleep here using the provided sleep function. sleep_fn(interval).await; } } fn prepare_request( &self, ) -> Result> where RE: Error + 'static, { Ok(endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, self.token_url .ok_or_else(|| RequestTokenError::Other("no token_url provided".to_string()))? .url(), vec![ ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), ("device_code", self.dev_auth_resp.device_code().secret()), ], )) } fn process_response( &self, res: Result, current_interval: Duration, ) -> DeviceAccessTokenPollResult where RE: Error + 'static, { let http_response = match res { Ok(inner) => inner, Err(_) => { // RFC 8628 requires a backoff in cases of connection timeout, but we can't // distinguish between connection timeouts and other HTTP client request errors // here. Set a maximum backoff so that the client doesn't effectively backoff // infinitely when there are network issues unrelated to server load. const DEFAULT_MAX_BACKOFF_INTERVAL: Duration = Duration::from_secs(10); let new_interval = std::cmp::min( current_interval.checked_mul(2).unwrap_or(current_interval), self.max_backoff_interval .unwrap_or(DEFAULT_MAX_BACKOFF_INTERVAL), ); return DeviceAccessTokenPollResult::ContinueWithNewPollInterval(new_interval); } }; // Explicitly process the response with a DeviceCodeErrorResponse let res = endpoint_response::(http_response); match res { // On a ServerResponse error, the error needs inspecting as a DeviceCodeErrorResponse // to work out whether a retry needs to happen. Err(RequestTokenError::ServerResponse(dcer)) => { match dcer.error() { // On AuthorizationPending, a retry needs to happen with the same poll interval. DeviceCodeErrorResponseType::AuthorizationPending => { DeviceAccessTokenPollResult::ContinueWithNewPollInterval(current_interval) } // On SlowDown, a retry needs to happen with a larger poll interval. DeviceCodeErrorResponseType::SlowDown => { DeviceAccessTokenPollResult::ContinueWithNewPollInterval( current_interval + Duration::from_secs(5), ) } // On any other error, just return the error. _ => DeviceAccessTokenPollResult::Done( Err(RequestTokenError::ServerResponse(dcer)), PhantomData, ), } } // On any other success or failure, return the failure. res => DeviceAccessTokenPollResult::Done(res, PhantomData), } } fn compute_timeout( &self, timeout: Option, ) -> Result, RequestTokenError> where RE: Error + 'static, { // Calculate the request timeout - if the user specified a timeout, // use that, otherwise use the value given by the device authorization // response. let timeout_dur = timeout.unwrap_or_else(|| self.dev_auth_resp.expires_in()); let chrono_timeout = chrono::Duration::from_std(timeout_dur) .map_err(|_| RequestTokenError::Other("Failed to convert duration".to_string()))?; // Calculate the DateTime at which the request times out. let timeout_dt = (*self.time_fn)() .checked_add_signed(chrono_timeout) .ok_or_else(|| RequestTokenError::Other("Failed to calculate timeout".to_string()))?; Ok(timeout_dt) } } /// /// Trait for OAuth2 access tokens. /// pub trait TokenType: Clone + DeserializeOwned + Debug + PartialEq + Serialize {} /// /// Trait for adding extra fields to the `TokenResponse`. /// pub trait ExtraTokenFields: DeserializeOwned + Debug + Serialize {} /// /// Empty (default) extra token fields. /// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct EmptyExtraTokenFields {} impl ExtraTokenFields for EmptyExtraTokenFields {} /// /// Common methods shared by all OAuth2 token implementations. /// /// The methods in this trait are defined in /// [Section 5.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.1). This trait exists /// separately from the `StandardTokenResponse` struct to support customization by clients, /// such as supporting interoperability with non-standards-complaint OAuth2 providers. /// pub trait TokenResponse: Debug + DeserializeOwned + Serialize where TT: TokenType, { /// /// REQUIRED. The access token issued by the authorization server. /// fn access_token(&self) -> &AccessToken; /// /// REQUIRED. The type of the token issued as described in /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). /// Value is case insensitive and deserialized to the generic `TokenType` parameter. /// fn token_type(&self) -> &TT; /// /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600 /// denotes that the access token will expire in one hour from the time the response was /// generated. If omitted, the authorization server SHOULD provide the expiration time via /// other means or document the default value. /// fn expires_in(&self) -> Option; /// /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same /// authorization grant as described in /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). /// fn refresh_token(&self) -> Option<&RefreshToken>; /// /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The /// scope of the access token as described by /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response, /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from /// the response, this field is `None`. /// fn scopes(&self) -> Option<&Vec>; } /// /// Standard OAuth2 token response. /// /// This struct includes the fields defined in /// [Section 5.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.1), as well as /// extensions defined by the `EF` type parameter. /// #[derive(Clone, Debug, Deserialize, Serialize)] pub struct StandardTokenResponse where EF: ExtraTokenFields, TT: TokenType, { access_token: AccessToken, #[serde(bound = "TT: TokenType")] #[serde(deserialize_with = "helpers::deserialize_untagged_enum_case_insensitive")] token_type: TT, #[serde(skip_serializing_if = "Option::is_none")] expires_in: Option, #[serde(skip_serializing_if = "Option::is_none")] refresh_token: Option, #[serde(rename = "scope")] #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] scopes: Option>, #[serde(bound = "EF: ExtraTokenFields")] #[serde(flatten)] extra_fields: EF, } impl StandardTokenResponse where EF: ExtraTokenFields, TT: TokenType, { /// /// Instantiate a new OAuth2 token response. /// pub fn new(access_token: AccessToken, token_type: TT, extra_fields: EF) -> Self { Self { access_token, token_type, expires_in: None, refresh_token: None, scopes: None, extra_fields, } } /// /// Set the `access_token` field. /// pub fn set_access_token(&mut self, access_token: AccessToken) { self.access_token = access_token; } /// /// Set the `token_type` field. /// pub fn set_token_type(&mut self, token_type: TT) { self.token_type = token_type; } /// /// Set the `expires_in` field. /// pub fn set_expires_in(&mut self, expires_in: Option<&Duration>) { self.expires_in = expires_in.map(Duration::as_secs); } /// /// Set the `refresh_token` field. /// pub fn set_refresh_token(&mut self, refresh_token: Option) { self.refresh_token = refresh_token; } /// /// Set the `scopes` field. /// pub fn set_scopes(&mut self, scopes: Option>) { self.scopes = scopes; } /// /// Extra fields defined by the client application. /// pub fn extra_fields(&self) -> &EF { &self.extra_fields } /// /// Set the extra fields defined by the client application. /// pub fn set_extra_fields(&mut self, extra_fields: EF) { self.extra_fields = extra_fields; } } impl TokenResponse for StandardTokenResponse where EF: ExtraTokenFields, TT: TokenType, { /// /// REQUIRED. The access token issued by the authorization server. /// fn access_token(&self) -> &AccessToken { &self.access_token } /// /// REQUIRED. The type of the token issued as described in /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). /// Value is case insensitive and deserialized to the generic `TokenType` parameter. /// fn token_type(&self) -> &TT { &self.token_type } /// /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600 /// denotes that the access token will expire in one hour from the time the response was /// generated. If omitted, the authorization server SHOULD provide the expiration time via /// other means or document the default value. /// fn expires_in(&self) -> Option { self.expires_in.map(Duration::from_secs) } /// /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same /// authorization grant as described in /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). /// fn refresh_token(&self) -> Option<&RefreshToken> { self.refresh_token.as_ref() } /// /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The /// scope of the access token as described by /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response, /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from /// the response, this field is `None`. /// fn scopes(&self) -> Option<&Vec> { self.scopes.as_ref() } } /// /// Common methods shared by all OAuth2 token introspection implementations. /// /// The methods in this trait are defined in /// [Section 2.2 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-2.2). This trait exists /// separately from the `StandardTokenIntrospectionResponse` struct to support customization by /// clients, such as supporting interoperability with non-standards-complaint OAuth2 providers. /// pub trait TokenIntrospectionResponse: Debug + DeserializeOwned + Serialize where TT: TokenType, { /// /// REQUIRED. Boolean indicator of whether or not the presented token /// is currently active. The specifics of a token's "active" state /// will vary depending on the implementation of the authorization /// server and the information it keeps about its tokens, but a "true" /// value return for the "active" property will generally indicate /// that a given token has been issued by this authorization server, /// has not been revoked by the resource owner, and is within its /// given time window of validity (e.g., after its issuance time and /// before its expiration time). /// fn active(&self) -> bool; /// /// /// OPTIONAL. A JSON string containing a space-separated list of /// scopes associated with this token, in the format described in /// [Section 3.3 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-3.3). /// If included in the response, /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from /// the response, this field is `None`. /// fn scopes(&self) -> Option<&Vec>; /// /// OPTIONAL. Client identifier for the OAuth 2.0 client that /// requested this token. /// fn client_id(&self) -> Option<&ClientId>; /// /// OPTIONAL. Human-readable identifier for the resource owner who /// authorized this token. /// fn username(&self) -> Option<&str>; /// /// OPTIONAL. Type of the token as defined in /// [Section 5.1 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-5.1). /// Value is case insensitive and deserialized to the generic `TokenType` parameter. /// fn token_type(&self) -> Option<&TT>; /// /// OPTIONAL. Integer timestamp, measured in the number of seconds /// since January 1 1970 UTC, indicating when this token will expire, /// as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519). /// fn exp(&self) -> Option>; /// /// OPTIONAL. Integer timestamp, measured in the number of seconds /// since January 1 1970 UTC, indicating when this token was /// originally issued, as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519). /// fn iat(&self) -> Option>; /// /// OPTIONAL. Integer timestamp, measured in the number of seconds /// since January 1 1970 UTC, indicating when this token is not to be /// used before, as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519). /// fn nbf(&self) -> Option>; /// /// OPTIONAL. Subject of the token, as defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519). /// Usually a machine-readable identifier of the resource owner who /// authorized this token. /// fn sub(&self) -> Option<&str>; /// /// OPTIONAL. Service-specific string identifier or list of string /// identifiers representing the intended audience for this token, as /// defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519). /// fn aud(&self) -> Option<&Vec>; /// /// OPTIONAL. String representing the issuer of this token, as /// defined in JWT [RFC7519](https://tools.ietf.org/html/rfc7519). /// fn iss(&self) -> Option<&str>; /// /// OPTIONAL. String identifier for the token, as defined in JWT /// [RFC7519](https://tools.ietf.org/html/rfc7519). /// fn jti(&self) -> Option<&str>; } /// /// Standard OAuth2 token introspection response. /// /// This struct includes the fields defined in /// [Section 2.2 of RFC 7662](https://tools.ietf.org/html/rfc7662#section-2.2), as well as /// extensions defined by the `EF` type parameter. /// #[derive(Clone, Debug, Deserialize, Serialize)] pub struct StandardTokenIntrospectionResponse where EF: ExtraTokenFields, TT: TokenType + 'static, { active: bool, #[serde(rename = "scope")] #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] scopes: Option>, #[serde(skip_serializing_if = "Option::is_none")] client_id: Option, #[serde(skip_serializing_if = "Option::is_none")] username: Option, #[serde( bound = "TT: TokenType", skip_serializing_if = "Option::is_none", deserialize_with = "helpers::deserialize_untagged_enum_case_insensitive", default = "none_field" )] token_type: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(with = "ts_seconds_option")] #[serde(default)] exp: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(with = "ts_seconds_option")] #[serde(default)] iat: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(with = "ts_seconds_option")] #[serde(default)] nbf: Option>, #[serde(skip_serializing_if = "Option::is_none")] sub: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] #[serde(deserialize_with = "helpers::deserialize_optional_string_or_vec_string")] aud: Option>, #[serde(skip_serializing_if = "Option::is_none")] iss: Option, #[serde(skip_serializing_if = "Option::is_none")] jti: Option, #[serde(bound = "EF: ExtraTokenFields")] #[serde(flatten)] extra_fields: EF, } fn none_field() -> Option { None } impl StandardTokenIntrospectionResponse where EF: ExtraTokenFields, TT: TokenType, { /// /// Instantiate a new OAuth2 token introspection response. /// pub fn new(active: bool, extra_fields: EF) -> Self { Self { active, scopes: None, client_id: None, username: None, token_type: None, exp: None, iat: None, nbf: None, sub: None, aud: None, iss: None, jti: None, extra_fields, } } /// /// Sets the `set_active` field. /// pub fn set_active(&mut self, active: bool) { self.active = active; } /// /// Sets the `set_scopes` field. /// pub fn set_scopes(&mut self, scopes: Option>) { self.scopes = scopes; } /// /// Sets the `set_client_id` field. /// pub fn set_client_id(&mut self, client_id: Option) { self.client_id = client_id; } /// /// Sets the `set_username` field. /// pub fn set_username(&mut self, username: Option) { self.username = username; } /// /// Sets the `set_token_type` field. /// pub fn set_token_type(&mut self, token_type: Option) { self.token_type = token_type; } /// /// Sets the `set_exp` field. /// pub fn set_exp(&mut self, exp: Option>) { self.exp = exp; } /// /// Sets the `set_iat` field. /// pub fn set_iat(&mut self, iat: Option>) { self.iat = iat; } /// /// Sets the `set_nbf` field. /// pub fn set_nbf(&mut self, nbf: Option>) { self.nbf = nbf; } /// /// Sets the `set_sub` field. /// pub fn set_sub(&mut self, sub: Option) { self.sub = sub; } /// /// Sets the `set_aud` field. /// pub fn set_aud(&mut self, aud: Option>) { self.aud = aud; } /// /// Sets the `set_iss` field. /// pub fn set_iss(&mut self, iss: Option) { self.iss = iss; } /// /// Sets the `set_jti` field. /// pub fn set_jti(&mut self, jti: Option) { self.jti = jti; } /// /// Extra fields defined by the client application. /// pub fn extra_fields(&self) -> &EF { &self.extra_fields } /// /// Sets the `set_extra_fields` field. /// pub fn set_extra_fields(&mut self, extra_fields: EF) { self.extra_fields = extra_fields; } } impl TokenIntrospectionResponse for StandardTokenIntrospectionResponse where EF: ExtraTokenFields, TT: TokenType, { fn active(&self) -> bool { self.active } fn scopes(&self) -> Option<&Vec> { self.scopes.as_ref() } fn client_id(&self) -> Option<&ClientId> { self.client_id.as_ref() } fn username(&self) -> Option<&str> { self.username.as_deref() } fn token_type(&self) -> Option<&TT> { self.token_type.as_ref() } fn exp(&self) -> Option> { self.exp } fn iat(&self) -> Option> { self.iat } fn nbf(&self) -> Option> { self.nbf } fn sub(&self) -> Option<&str> { self.sub.as_deref() } fn aud(&self) -> Option<&Vec> { self.aud.as_ref() } fn iss(&self) -> Option<&str> { self.iss.as_deref() } fn jti(&self) -> Option<&str> { self.jti.as_deref() } } /// /// Server Error Response /// /// This trait exists separately from the `StandardErrorResponse` struct /// to support customization by clients, such as supporting interoperability with /// non-standards-complaint OAuth2 providers /// pub trait ErrorResponse: Debug + DeserializeOwned + Serialize {} /// /// Error types enum. /// /// NOTE: The serialization must return the `snake_case` representation of /// this error type. This value must match the error type from the relevant OAuth 2.0 standards /// (RFC 6749 or an extension). /// pub trait ErrorResponseType: Debug + DeserializeOwned + Serialize {} /// /// Error response returned by server after requesting an access token. /// /// The fields in this structure are defined in /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2). This /// trait is parameterized by a `ErrorResponseType` to support error types specific to future OAuth2 /// authentication schemes and extensions. /// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct StandardErrorResponse { #[serde(bound = "T: ErrorResponseType")] error: T, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] error_description: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] error_uri: Option, } impl StandardErrorResponse { /// /// Instantiate a new `ErrorResponse`. /// /// # Arguments /// /// * `error` - REQUIRED. A single ASCII error code deserialized to the generic parameter. /// `ErrorResponseType`. /// * `error_description` - OPTIONAL. Human-readable ASCII text providing additional /// information, used to assist the client developer in understanding the error that /// occurred. Values for this parameter MUST NOT include characters outside the set /// `%x20-21 / %x23-5B / %x5D-7E`. /// * `error_uri` - OPTIONAL. A URI identifying a human-readable web page with information /// about the error used to provide the client developer with additional information about /// the error. Values for the "error_uri" parameter MUST conform to the URI-reference /// syntax and thus MUST NOT include characters outside the set `%x21 / %x23-5B / %x5D-7E`. /// pub fn new(error: T, error_description: Option, error_uri: Option) -> Self { Self { error, error_description, error_uri, } } /// /// REQUIRED. A single ASCII error code deserialized to the generic parameter /// `ErrorResponseType`. /// pub fn error(&self) -> &T { &self.error } /// /// OPTIONAL. Human-readable ASCII text providing additional information, used to assist /// the client developer in understanding the error that occurred. Values for this /// parameter MUST NOT include characters outside the set `%x20-21 / %x23-5B / %x5D-7E`. /// pub fn error_description(&self) -> Option<&String> { self.error_description.as_ref() } /// /// OPTIONAL. URI identifying a human-readable web page with information about the error, /// used to provide the client developer with additional information about the error. /// Values for the "error_uri" parameter MUST conform to the URI-reference syntax and /// thus MUST NOT include characters outside the set `%x21 / %x23-5B / %x5D-7E`. /// pub fn error_uri(&self) -> Option<&String> { self.error_uri.as_ref() } } impl ErrorResponse for StandardErrorResponse where T: ErrorResponseType + 'static {} impl Display for StandardErrorResponse where TE: ErrorResponseType + Display, { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { let mut formatted = self.error().to_string(); if let Some(error_description) = self.error_description() { formatted.push_str(": "); formatted.push_str(error_description); } if let Some(error_uri) = self.error_uri() { formatted.push_str(" / See "); formatted.push_str(error_uri); } write!(f, "{}", formatted) } } /// /// Error encountered while requesting access token. /// #[derive(Debug, thiserror::Error)] pub enum RequestTokenError where RE: Error + 'static, T: ErrorResponse + 'static, { /// /// Error response returned by authorization server. Contains the parsed `ErrorResponse` /// returned by the server. /// #[error("Server returned error response")] ServerResponse(T), /// /// An error occurred while sending the request or receiving the response (e.g., network /// connectivity failed). /// #[error("Request failed")] Request(#[source] RE), /// /// Failed to parse server response. Parse errors may occur while parsing either successful /// or error responses. /// #[error("Failed to parse server response")] Parse( #[source] serde_path_to_error::Error, Vec, ), /// /// Some other type of error occurred (e.g., an unexpected server response). /// #[error("Other error: {}", _0)] Other(String), } oauth2-4.4.1/src/reqwest.rs000064400000000000000000000073051046102023000136700ustar 00000000000000use thiserror::Error; /// /// Error type returned by failed reqwest HTTP requests. /// #[derive(Debug, Error)] pub enum Error where T: std::error::Error + 'static, { /// Error returned by reqwest crate. #[error("request failed")] Reqwest(#[source] T), /// Non-reqwest HTTP error. #[error("HTTP error")] Http(#[source] http::Error), /// I/O error. #[error("I/O error")] Io(#[source] std::io::Error), /// Other error. #[error("Other error: {}", _0)] Other(String), } #[cfg(not(target_arch = "wasm32"))] pub use blocking::http_client; /// /// Error type returned by failed reqwest blocking HTTP requests. /// #[cfg(not(target_arch = "wasm32"))] pub type HttpClientError = Error; pub use async_client::async_http_client; /// /// Error type returned by failed reqwest async HTTP requests. /// pub type AsyncHttpClientError = Error; #[cfg(not(target_arch = "wasm32"))] mod blocking { use super::super::{HttpRequest, HttpResponse}; use super::Error; pub use reqwest; use reqwest::blocking; use reqwest::redirect::Policy as RedirectPolicy; use std::io::Read; /// /// Synchronous HTTP client. /// pub fn http_client(request: HttpRequest) -> Result> { let client = blocking::Client::builder() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(RedirectPolicy::none()) .build() .map_err(Error::Reqwest)?; #[cfg(feature = "reqwest")] let mut request_builder = client .request(request.method, request.url.as_str()) .body(request.body); for (name, value) in &request.headers { request_builder = request_builder.header(name.as_str(), value.as_bytes()); } let mut response = client .execute(request_builder.build().map_err(Error::Reqwest)?) .map_err(Error::Reqwest)?; let mut body = Vec::new(); response.read_to_end(&mut body).map_err(Error::Io)?; #[cfg(feature = "reqwest")] { Ok(HttpResponse { status_code: response.status(), headers: response.headers().to_owned(), body, }) } } } mod async_client { use super::super::{HttpRequest, HttpResponse}; use super::Error; pub use reqwest; /// /// Asynchronous HTTP client. /// pub async fn async_http_client( request: HttpRequest, ) -> Result> { let client = { let builder = reqwest::Client::builder(); // Following redirects opens the client up to SSRF vulnerabilities. // but this is not possible to prevent on wasm targets #[cfg(not(target_arch = "wasm32"))] let builder = builder.redirect(reqwest::redirect::Policy::none()); builder.build().map_err(Error::Reqwest)? }; let mut request_builder = client .request(request.method, request.url.as_str()) .body(request.body); for (name, value) in &request.headers { request_builder = request_builder.header(name.as_str(), value.as_bytes()); } let request = request_builder.build().map_err(Error::Reqwest)?; let response = client.execute(request).await.map_err(Error::Reqwest)?; let status_code = response.status(); let headers = response.headers().to_owned(); let chunks = response.bytes().await.map_err(Error::Reqwest)?; Ok(HttpResponse { status_code, headers, body: chunks.to_vec(), }) } } oauth2-4.4.1/src/revocation.rs000064400000000000000000000144671046102023000143560ustar 00000000000000use serde::{Deserialize, Serialize}; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; use crate::{basic::BasicErrorResponseType, ErrorResponseType}; use crate::{AccessToken, RefreshToken}; /// /// A revocable token. /// /// Implement this trait to indicate support for token revocation per [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009#section-2.2). /// pub trait RevocableToken { /// /// The actual token value to be revoked. /// fn secret(&self) -> &str; /// /// Indicates the type of the token being revoked, as defined by [RFC 7009, Section 2.1](https://tools.ietf.org/html/rfc7009#section-2.1). /// /// Implementations should return `Some(...)` values for token types that the target authorization servers are /// expected to know (e.g. because they are registered in the [OAuth Token Type Hints Registry](https://tools.ietf.org/html/rfc7009#section-4.1.2)) /// so that they can potentially optimize their search for the token to be revoked. /// fn type_hint(&self) -> Option<&str>; } /// /// A token representation usable with authorization servers that support [RFC 7009](https://tools.ietf.org/html/rfc7009) token revocation. /// /// For use with [`revoke_token()`]. /// /// Automatically reports the correct RFC 7009 [`token_type_hint`](https://tools.ietf.org/html/rfc7009#section-2.1) value corresponding to the token type variant used, i.e. /// `access_token` for [`AccessToken`] and `secret_token` for [`RefreshToken`]. /// /// # Example /// /// Per [RFC 7009, Section 2](https://tools.ietf.org/html/rfc7009#section-2) prefer revocation by refresh token which, /// if issued to the client, must be supported by the server, otherwise fallback to access token (which may or may not /// be supported by the server). /// /// ```ignore /// let token_to_revoke: StandardRevocableToken = match token_response.refresh_token() { /// Some(token) => token.into(), /// None => token_response.access_token().into(), /// }; /// /// client /// .revoke_token(token_to_revoke) /// .request(http_client) /// .unwrap(); /// ``` /// /// [`revoke_token()`]: crate::Client::revoke_token() /// #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub enum StandardRevocableToken { /// A representation of an [`AccessToken`] suitable for use with [`revoke_token()`](crate::Client::revoke_token()). AccessToken(AccessToken), /// A representation of an [`RefreshToken`] suitable for use with [`revoke_token()`](crate::Client::revoke_token()). RefreshToken(RefreshToken), } impl RevocableToken for StandardRevocableToken { fn secret(&self) -> &str { match self { Self::AccessToken(token) => token.secret(), Self::RefreshToken(token) => token.secret(), } } /// /// Indicates the type of the token to be revoked, as defined by [RFC 7009, Section 2.1](https://tools.ietf.org/html/rfc7009#section-2.1), i.e.: /// /// * `access_token`: An access token as defined in [RFC 6749, /// Section 1.4](https://tools.ietf.org/html/rfc6749#section-1.4) /// /// * `refresh_token`: A refresh token as defined in [RFC 6749, /// Section 1.5](https://tools.ietf.org/html/rfc6749#section-1.5) /// fn type_hint(&self) -> Option<&str> { match self { StandardRevocableToken::AccessToken(_) => Some("access_token"), StandardRevocableToken::RefreshToken(_) => Some("refresh_token"), } } } impl From for StandardRevocableToken { fn from(token: AccessToken) -> Self { Self::AccessToken(token) } } impl From<&AccessToken> for StandardRevocableToken { fn from(token: &AccessToken) -> Self { Self::AccessToken(token.clone()) } } impl From for StandardRevocableToken { fn from(token: RefreshToken) -> Self { Self::RefreshToken(token) } } impl From<&RefreshToken> for StandardRevocableToken { fn from(token: &RefreshToken) -> Self { Self::RefreshToken(token.clone()) } } /// /// OAuth 2.0 Token Revocation error response types. /// /// These error types are defined in /// [Section 2.2.1 of RFC 7009](https://tools.ietf.org/html/rfc7009#section-2.2.1) and /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc8628#section-5.2) /// #[derive(Clone, PartialEq)] pub enum RevocationErrorResponseType { /// /// The authorization server does not support the revocation of the presented token type. /// UnsupportedTokenType, /// /// The authorization server responded with some other error as defined [RFC 6749](https://tools.ietf.org/html/rfc6749) error. /// Basic(BasicErrorResponseType), } impl RevocationErrorResponseType { fn from_str(s: &str) -> Self { match BasicErrorResponseType::from_str(s) { BasicErrorResponseType::Extension(ext) => match ext.as_str() { "unsupported_token_type" => RevocationErrorResponseType::UnsupportedTokenType, _ => RevocationErrorResponseType::Basic(BasicErrorResponseType::Extension(ext)), }, basic => RevocationErrorResponseType::Basic(basic), } } } impl AsRef for RevocationErrorResponseType { fn as_ref(&self) -> &str { match self { RevocationErrorResponseType::UnsupportedTokenType => "unsupported_token_type", RevocationErrorResponseType::Basic(basic) => basic.as_ref(), } } } impl<'de> serde::Deserialize<'de> for RevocationErrorResponseType { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let variant_str = String::deserialize(deserializer)?; Ok(Self::from_str(&variant_str)) } } impl serde::ser::Serialize for RevocationErrorResponseType { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(self.as_ref()) } } impl ErrorResponseType for RevocationErrorResponseType {} impl Debug for RevocationErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { Display::fmt(self, f) } } impl Display for RevocationErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { write!(f, "{}", self.as_ref()) } } oauth2-4.4.1/src/tests.rs000064400000000000000000002546401046102023000133460ustar 00000000000000use http::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::status::StatusCode; use revocation::RevocationErrorResponseType; use thiserror::Error; use url::form_urlencoded::byte_serialize; use url::Url; use crate::revocation::StandardRevocableToken; use super::basic::*; use super::devicecode::*; use super::*; use chrono::TimeZone; fn new_client() -> BasicClient { BasicClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ) } fn mock_http_client( request_headers: Vec<(HeaderName, &'static str)>, request_body: &'static str, request_url: Option, response: HttpResponse, ) -> impl Fn(HttpRequest) -> Result { move |request: HttpRequest| { assert_eq!( &request.url, request_url .as_ref() .unwrap_or(&Url::parse("https://example.com/token").unwrap()) ); assert_eq!( request.headers, request_headers .iter() .map(|(name, value)| (name.clone(), HeaderValue::from_str(value).unwrap())) .collect(), ); assert_eq!(&String::from_utf8(request.body).unwrap(), request_body); Ok(response.clone()) } } #[test] #[should_panic] fn test_code_verifier_too_short() { PkceCodeChallenge::new_random_sha256_len(31); } #[test] #[should_panic] fn test_code_verifier_too_long() { PkceCodeChallenge::new_random_sha256_len(97); } #[test] fn test_code_verifier_min() { let code = PkceCodeChallenge::new_random_sha256_len(32); assert_eq!(code.1.secret().len(), 43); } #[test] fn test_code_verifier_max() { let code = PkceCodeChallenge::new_random_sha256_len(96); assert_eq!(code.1.secret().len(), 128); } #[test] fn test_code_verifier_challenge() { // Example from https://tools.ietf.org/html/rfc7636#appendix-B let code_verifier = PkceCodeVerifier::new("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string()); assert_eq!( PkceCodeChallenge::from_code_verifier_sha256(&code_verifier).as_str(), "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", ); } #[test] fn test_authorize_url() { let client = new_client(); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .url(); assert_eq!( Url::parse("https://example.com/auth?response_type=code&client_id=aaa&state=csrf_token") .unwrap(), url ); } #[test] fn test_authorize_random() { let client = new_client(); let (url, csrf_state) = client.authorize_url(CsrfToken::new_random).url(); assert_eq!( Url::parse(&format!( "https://example.com/auth?response_type=code&client_id=aaa&state={}", byte_serialize(csrf_state.secret().clone().into_bytes().as_slice()) .collect::>() .join("") )) .unwrap(), url ); } #[test] fn test_authorize_url_pkce() { // Example from https://tools.ietf.org/html/rfc7636#appendix-B let client = new_client(); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .set_pkce_challenge(PkceCodeChallenge::from_code_verifier_sha256( &PkceCodeVerifier::new("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string()), )) .url(); assert_eq!( Url::parse(concat!( "https://example.com/auth", "?response_type=code&client_id=aaa", "&state=csrf_token", "&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "&code_challenge_method=S256", )) .unwrap(), url ); } #[test] fn test_authorize_url_implicit() { let client = new_client(); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .use_implicit_flow() .url(); assert_eq!( Url::parse("https://example.com/auth?response_type=token&client_id=aaa&state=csrf_token") .unwrap(), url ); } #[test] fn test_authorize_url_with_param() { let client = BasicClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth?foo=bar".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .url(); assert_eq!( Url::parse( "https://example.com/auth?foo=bar&response_type=code&client_id=aaa&state=csrf_token" ) .unwrap(), url ); } #[test] fn test_authorize_url_with_scopes() { let scopes = vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]; let (url, _) = new_client() .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .add_scopes(scopes) .url(); assert_eq!( Url::parse( "https://example.com/auth\ ?response_type=code\ &client_id=aaa\ &state=csrf_token\ &scope=read+write" ) .unwrap(), url ); } #[test] fn test_authorize_url_with_one_scope() { let (url, _) = new_client() .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .add_scope(Scope::new("read".to_string())) .url(); assert_eq!( Url::parse( "https://example.com/auth\ ?response_type=code\ &client_id=aaa\ &state=csrf_token\ &scope=read" ) .unwrap(), url ); } #[test] fn test_authorize_url_with_extension_response_type() { let client = new_client(); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .set_response_type(&ResponseType::new("code token".to_string())) .add_extra_param("foo", "bar") .url(); assert_eq!( Url::parse( "https://example.com/auth?response_type=code+token&client_id=aaa&state=csrf_token\ &foo=bar" ) .unwrap(), url ); } #[test] fn test_authorize_url_with_redirect_url() { let client = new_client() .set_redirect_uri(RedirectUrl::new("https://localhost/redirect".to_string()).unwrap()); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .url(); assert_eq!( Url::parse( "https://example.com/auth?response_type=code\ &client_id=aaa\ &state=csrf_token\ &redirect_uri=https%3A%2F%2Flocalhost%2Fredirect" ) .unwrap(), url ); } #[test] fn test_authorize_url_with_redirect_url_override() { let client = new_client() .set_redirect_uri(RedirectUrl::new("https://localhost/redirect".to_string()).unwrap()); let (url, _) = client .authorize_url(|| CsrfToken::new("csrf_token".to_string())) .set_redirect_uri(Cow::Owned( RedirectUrl::new("https://localhost/alternative".to_string()).unwrap(), )) .url(); assert_eq!( Url::parse( "https://example.com/auth?response_type=code\ &client_id=aaa\ &state=csrf_token\ &redirect_uri=https%3A%2F%2Flocalhost%2Falternative" ) .unwrap(), url ); } #[derive(Debug, Error)] enum FakeError { #[error("error")] Err, } // Because the secret types don't implement PartialEq, we can't directly use == to compare tokens. fn assert_token_eq(a: &StandardTokenResponse, b: &StandardTokenResponse) where EF: ExtraTokenFields + PartialEq, TT: TokenType, { assert_eq!(a.access_token().secret(), b.access_token().secret()); assert_eq!(a.token_type(), b.token_type()); assert_eq!(a.expires_in(), b.expires_in()); assert_eq!( a.refresh_token().map(RefreshToken::secret), b.refresh_token().map(RefreshToken::secret) ); assert_eq!(a.scopes(), b.scopes()); assert_eq!(a.extra_fields(), b.extra_fields()); } #[test] fn test_exchange_code_successful_with_minimal_json_response() { let client = BasicClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::OK, headers: HeaderMap::new(), body: "{\"access_token\": \"12/34\", \"token_type\": \"BEARER\"}" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&token).unwrap(); assert_eq!( "{\"access_token\":\"12/34\",\"token_type\":\"bearer\"}".to_string(), serialized_json ); let deserialized_token = serde_json::from_str::(&serialized_json).unwrap(); assert_token_eq(&token, &deserialized_token); } #[test] fn test_exchange_code_successful_with_complete_json_response() { let client = new_client().set_auth_type(AuthType::RequestBody); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=authorization_code&code=ccc&client_id=aaa&client_secret=bbb", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\", \ \"expires_in\": 3600, \ \"refresh_token\": \"foobar\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(3600, token.expires_in().unwrap().as_secs()); assert_eq!("foobar", token.refresh_token().clone().unwrap().secret()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&token).unwrap(); assert_eq!( "{\"access_token\":\"12/34\",\"token_type\":\"bearer\",\"expires_in\":3600,\ \"refresh_token\":\"foobar\",\"scope\":\"read write\"}" .to_string(), serialized_json ); let deserialized_token = serde_json::from_str::(&serialized_json).unwrap(); assert_token_eq(&token, &deserialized_token); } #[test] fn test_exchange_client_credentials_with_basic_auth() { let client = BasicClient::new( ClientId::new("aaa/;&".to_string()), Some(ClientSecret::new("bbb/;&".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ) .set_auth_type(AuthType::BasicAuth); let token = client .exchange_client_credentials() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhJTJGJTNCJTI2OmJiYiUyRiUzQiUyNg=="), ], "grant_type=client_credentials", None, HttpResponse { status_code: StatusCode::OK, headers: HeaderMap::new(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_client_credentials_with_basic_auth_but_no_client_secret() { let client = BasicClient::new( ClientId::new("aaa/;&".to_string()), None, AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ) .set_auth_type(AuthType::BasicAuth); let token = client .exchange_client_credentials() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=client_credentials&client_id=aaa%2F%3B%26", None, HttpResponse { status_code: StatusCode::OK, headers: HeaderMap::new(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_client_credentials_with_body_auth_and_scope() { let client = new_client().set_auth_type(AuthType::RequestBody); let token = client .exchange_client_credentials() .add_scope(Scope::new("read".to_string())) .add_scope(Scope::new("write".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=client_credentials&scope=read+write&client_id=aaa&client_secret=bbb", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("APPLICATION/jSoN").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_refresh_token_with_basic_auth() { let client = new_client().set_auth_type(AuthType::BasicAuth); let token = client .exchange_refresh_token(&RefreshToken::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=refresh_token&refresh_token=ccc", None, HttpResponse { status_code: StatusCode::OK, headers: HeaderMap::new(), body: "{\"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_refresh_token_with_json_response() { let client = new_client(); let token = client .exchange_refresh_token(&RefreshToken::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=refresh_token&refresh_token=ccc", None, HttpResponse { status_code: StatusCode::OK, headers: HeaderMap::new(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_password_with_json_response() { let client = new_client(); let token = client .exchange_password( &ResourceOwnerUsername::new("user".to_string()), &ResourceOwnerPassword::new("pass".to_string()), ) .add_scope(Scope::new("read".to_string())) .add_scope(Scope::new("write".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=password&username=user&password=pass&scope=read+write", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_code_successful_with_redirect_url() { let client = new_client() .set_auth_type(AuthType::RequestBody) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=authorization_code&code=ccc&client_id=aaa&client_secret=bbb&\ redirect_uri=https%3A%2F%2Fredirect%2Fhere", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_code_successful_with_redirect_url_override() { let client = new_client() .set_auth_type(AuthType::RequestBody) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .set_redirect_uri(Cow::Owned( RedirectUrl::new("https://redirect/alternative".to_string()).unwrap(), )) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=authorization_code&code=ccc&client_id=aaa&client_secret=bbb&\ redirect_uri=https%3A%2F%2Fredirect%2Falternative", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_code_successful_with_basic_auth() { let client = new_client() .set_auth_type(AuthType::BasicAuth) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc&redirect_uri=https%3A%2F%2Fredirect%2Fhere", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_code_successful_with_pkce_and_extension() { let client = new_client() .set_auth_type(AuthType::BasicAuth) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .set_pkce_verifier(PkceCodeVerifier::new( "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string(), )) .add_extra_param("foo", "bar") .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code\ &code=ccc\ &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk\ &redirect_uri=https%3A%2F%2Fredirect%2Fhere\ &foo=bar", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_refresh_token_successful_with_extension() { let client = new_client() .set_auth_type(AuthType::BasicAuth) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()); let token = client .exchange_refresh_token(&RefreshToken::new("ccc".to_string())) .add_extra_param("foo", "bar") .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=refresh_token&refresh_token=ccc&foo=bar", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_exchange_code_with_simple_json_error() { let client = new_client(); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::BAD_REQUEST, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"error\": \"invalid_request\", \ \"error_description\": \"stuff happened\"\ }" .to_string() .into_bytes(), }, )); assert!(token.is_err()); let token_err = token.err().unwrap(); match &token_err { &RequestTokenError::ServerResponse(ref error_response) => { assert_eq!( BasicErrorResponseType::InvalidRequest, *error_response.error() ); assert_eq!( Some(&"stuff happened".to_string()), error_response.error_description() ); assert_eq!(None, error_response.error_uri()); // Test Debug trait for ErrorResponse assert_eq!( "StandardErrorResponse { error: invalid_request, \ error_description: Some(\"stuff happened\"), error_uri: None }", format!("{:?}", error_response) ); // Test Display trait for ErrorResponse assert_eq!( "invalid_request: stuff happened", format!("{}", error_response) ); // Test Debug trait for BasicErrorResponseType assert_eq!("invalid_request", format!("{:?}", error_response.error())); // Test Display trait for BasicErrorResponseType assert_eq!("invalid_request", format!("{}", error_response.error())); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&error_response).unwrap(); assert_eq!( "{\"error\":\"invalid_request\",\"error_description\":\"stuff happened\"}" .to_string(), serialized_json ); let deserialized_error = serde_json::from_str::(&serialized_json).unwrap(); assert_eq!(error_response, &deserialized_error); } other => panic!("Unexpected error: {:?}", other), } // Test Debug trait for RequestTokenError assert_eq!( "ServerResponse(StandardErrorResponse { error: invalid_request, \ error_description: Some(\"stuff happened\"), error_uri: None })", format!("{:?}", token_err) ); // Test Display trait for RequestTokenError assert_eq!("Server returned error response", format!("{}", token_err)); } #[test] fn test_exchange_code_with_json_parse_error() { let client = new_client(); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "broken json".to_string().into_bytes(), }, )); assert!(token.is_err()); match token.err().unwrap() { RequestTokenError::Parse(json_err, _) => { assert_eq!(".", json_err.path().to_string()); assert_eq!(1, json_err.inner().line()); assert_eq!(1, json_err.inner().column()); assert_eq!( serde_json::error::Category::Syntax, json_err.inner().classify() ); } other => panic!("Unexpected error: {:?}", other), } } #[test] fn test_exchange_code_with_unexpected_content_type() { let client = new_client(); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::OK, headers: vec![(CONTENT_TYPE, HeaderValue::from_str("text/plain").unwrap())] .into_iter() .collect(), body: "broken json".to_string().into_bytes(), }, )); assert!(token.is_err()); match token.err().unwrap() { RequestTokenError::Other(error_str) => { assert_eq!( "Unexpected response Content-Type: \"text/plain\", should be `application/json`", error_str ); } other => panic!("Unexpected error: {:?}", other), } } #[test] fn test_exchange_code_with_invalid_token_type() { let client = BasicClient::new( ClientId::new("aaa".to_string()), None, AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=authorization_code&code=ccc&client_id=aaa", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\"access_token\": \"12/34\", \"token_type\": 123}" .to_string() .into_bytes(), }, )); assert!(token.is_err()); match token.err().unwrap() { RequestTokenError::Parse(json_err, _) => { assert_eq!("token_type", json_err.path().to_string()); assert_eq!(1, json_err.inner().line()); assert_eq!(43, json_err.inner().column()); assert_eq!( serde_json::error::Category::Data, json_err.inner().classify() ); } other => panic!("Unexpected error: {:?}", other), } } #[test] fn test_exchange_code_with_400_status_code() { let body = r#"{"error":"invalid_request","error_description":"Expired code."}"#; let client = new_client(); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::BAD_REQUEST, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: body.to_string().into_bytes(), }, )); assert!(token.is_err()); match token.err().unwrap() { RequestTokenError::ServerResponse(error_response) => { assert_eq!( BasicErrorResponseType::InvalidRequest, *error_response.error() ); assert_eq!( Some(&"Expired code.".to_string()), error_response.error_description() ); assert_eq!(None, error_response.error_uri()); } other => panic!("Unexpected error: {:?}", other), } } #[test] fn test_exchange_code_fails_gracefully_on_transport_error() { let client = BasicClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://auth".to_string()).unwrap(), Some(TokenUrl::new("https://token".to_string()).unwrap()), ); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(|_| Err(FakeError::Err)); assert!(token.is_err()); match token.err().unwrap() { RequestTokenError::Request(FakeError::Err) => (), other => panic!("Unexpected error: {:?}", other), } } mod colorful_extension { extern crate serde_json; use super::super::*; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; pub type ColorfulClient = Client< StandardErrorResponse, StandardTokenResponse, ColorfulTokenType, StandardTokenIntrospectionResponse, ColorfulRevocableToken, StandardErrorResponse, >; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum ColorfulTokenType { Green, Red, } impl TokenType for ColorfulTokenType {} #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ColorfulFields { #[serde(rename = "shape")] #[serde(skip_serializing_if = "Option::is_none")] pub shape: Option, #[serde(rename = "height")] pub height: u32, } impl ColorfulFields { pub fn shape(&self) -> Option<&String> { self.shape.as_ref() } pub fn height(&self) -> u32 { self.height } } impl ExtraTokenFields for ColorfulFields {} #[derive(Clone, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ColorfulErrorResponseType { TooDark, TooLight, WrongColorSpace, } impl ColorfulErrorResponseType { fn to_str(&self) -> &str { match self { ColorfulErrorResponseType::TooDark => "too_dark", ColorfulErrorResponseType::TooLight => "too_light", ColorfulErrorResponseType::WrongColorSpace => "wrong_color_space", } } } impl ErrorResponseType for ColorfulErrorResponseType {} impl Debug for ColorfulErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { Display::fmt(self, f) } } impl Display for ColorfulErrorResponseType { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { let message: &str = self.to_str(); write!(f, "{}", message) } } pub type ColorfulTokenResponse = StandardTokenResponse; pub enum ColorfulRevocableToken { Red(String), } impl RevocableToken for ColorfulRevocableToken { fn secret(&self) -> &str { match self { ColorfulRevocableToken::Red(secret) => &secret, } } fn type_hint(&self) -> Option<&str> { match self { ColorfulRevocableToken::Red(_) => Some("red_token"), } } } } #[test] fn test_extension_successful_with_minimal_json_response() { use self::colorful_extension::*; let client = ColorfulClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\"access_token\": \"12/34\", \"token_type\": \"green\", \"height\": 10}" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(ColorfulTokenType::Green, *token.token_type()); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); assert_eq!(None, token.extra_fields().shape()); assert_eq!(10, token.extra_fields().height()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&token).unwrap(); assert_eq!( "{\"access_token\":\"12/34\",\"token_type\":\"green\",\"height\":10}".to_string(), serialized_json ); let deserialized_token = serde_json::from_str::(&serialized_json).unwrap(); assert_token_eq(&token, &deserialized_token); } #[test] fn test_extension_successful_with_complete_json_response() { use self::colorful_extension::*; let client = ColorfulClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ) .set_auth_type(AuthType::RequestBody); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), ], "grant_type=authorization_code&code=ccc&client_id=aaa&client_secret=bbb", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"red\", \ \"scope\": \"read write\", \ \"expires_in\": 3600, \ \"refresh_token\": \"foobar\", \ \"shape\": \"round\", \ \"height\": 12\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(ColorfulTokenType::Red, *token.token_type()); assert_eq!( Some(&vec![ Scope::new("read".to_string()), Scope::new("write".to_string()), ]), token.scopes() ); assert_eq!(3600, token.expires_in().unwrap().as_secs()); assert_eq!("foobar", token.refresh_token().clone().unwrap().secret()); assert_eq!(Some(&"round".to_string()), token.extra_fields().shape()); assert_eq!(12, token.extra_fields().height()); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&token).unwrap(); assert_eq!( "{\"access_token\":\"12/34\",\"token_type\":\"red\",\"expires_in\":3600,\ \"refresh_token\":\"foobar\",\"scope\":\"read write\",\"shape\":\"round\",\"height\":12}" .to_string(), serialized_json ); let deserialized_token = serde_json::from_str::(&serialized_json).unwrap(); assert_token_eq(&token, &deserialized_token); } #[test] fn test_extension_with_simple_json_error() { use self::colorful_extension::*; let client = ColorfulClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::BAD_REQUEST, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\"error\": \"too_light\", \"error_description\": \"stuff happened\", \ \"error_uri\": \"https://errors\"}" .to_string() .into_bytes(), }, )); assert!(token.is_err()); let token_err = token.err().unwrap(); match &token_err { &RequestTokenError::ServerResponse(ref error_response) => { assert_eq!(ColorfulErrorResponseType::TooLight, *error_response.error()); assert_eq!( Some(&"stuff happened".to_string()), error_response.error_description() ); assert_eq!( Some(&"https://errors".to_string()), error_response.error_uri() ); // Ensure that serialization produces an equivalent JSON value. let serialized_json = serde_json::to_string(&error_response).unwrap(); assert_eq!( "{\"error\":\"too_light\",\"error_description\":\"stuff happened\",\ \"error_uri\":\"https://errors\"}" .to_string(), serialized_json ); let deserialized_error = serde_json::from_str::< StandardErrorResponse, >(&serialized_json) .unwrap(); assert_eq!(error_response, &deserialized_error); } other => panic!("Unexpected error: {:?}", other), } // Test Debug trait for RequestTokenError assert_eq!( "ServerResponse(StandardErrorResponse { error: too_light, \ error_description: Some(\"stuff happened\"), error_uri: Some(\"https://errors\") })", format!("{:?}", token_err) ); // Test Display trait for RequestTokenError assert_eq!("Server returned error response", format!("{}", token_err)); } mod custom_errors { use std::fmt::Error as FormatterError; use std::fmt::{Display, Formatter}; extern crate serde_json; use super::super::*; use super::colorful_extension::*; #[derive(Serialize, Deserialize, Debug)] pub struct CustomErrorResponse { pub custom_error: String, } impl Display for CustomErrorResponse { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { write!(f, "Custom Error from server") } } impl ErrorResponse for CustomErrorResponse {} pub type CustomErrorClient = Client< CustomErrorResponse, StandardTokenResponse, ColorfulTokenType, StandardTokenIntrospectionResponse, ColorfulRevocableToken, CustomErrorResponse, >; } #[test] fn test_extension_with_custom_json_error() { use self::custom_errors::*; let client = CustomErrorClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=authorization_code&code=ccc", None, HttpResponse { status_code: StatusCode::BAD_REQUEST, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\"custom_error\": \"non-compliant oauth implementation ;-)\"}" .to_string() .into_bytes(), }, )); assert!(token.is_err()); match token.err().unwrap() { RequestTokenError::ServerResponse(e) => { assert_eq!("non-compliant oauth implementation ;-)", e.custom_error) } e => panic!("failed to correctly parse custom server error, got {:?}", e), }; } #[test] fn test_extension_serializer() { use self::colorful_extension::{ColorfulFields, ColorfulTokenResponse, ColorfulTokenType}; let mut token_response = ColorfulTokenResponse::new( AccessToken::new("mysecret".to_string()), ColorfulTokenType::Red, ColorfulFields { shape: Some("circle".to_string()), height: 10, }, ); token_response.set_expires_in(Some(&Duration::from_secs(3600))); token_response.set_refresh_token(Some(RefreshToken::new("myothersecret".to_string()))); let serialized = serde_json::to_string(&token_response).unwrap(); assert_eq!( "{\ \"access_token\":\"mysecret\",\ \"token_type\":\"red\",\ \"expires_in\":3600,\ \"refresh_token\":\"myothersecret\",\ \"shape\":\"circle\",\ \"height\":10\ }", serialized, ); } #[test] fn test_error_response_serializer() { assert_eq!( "{\"error\":\"unauthorized_client\"}", serde_json::to_string(&BasicErrorResponse::new( BasicErrorResponseType::UnauthorizedClient, None, None, )) .unwrap(), ); assert_eq!( "{\ \"error\":\"invalid_client\",\ \"error_description\":\"Invalid client_id\",\ \"error_uri\":\"https://example.com/errors/invalid_client\"\ }", serde_json::to_string(&BasicErrorResponse::new( BasicErrorResponseType::InvalidClient, Some("Invalid client_id".to_string()), Some("https://example.com/errors/invalid_client".to_string()), )) .unwrap(), ); } #[derive(Deserialize, Debug, Clone)] pub struct ObjectWithOptionalStringOrVecString { #[serde(deserialize_with = "helpers::deserialize_optional_string_or_vec_string")] pub strings: Option>, } #[test] fn test_deserialize_optional_string_or_vec_string_none() { let list_of_strings: ObjectWithOptionalStringOrVecString = serde_json::from_str(r#"{ "strings": null }"#).unwrap(); assert_eq!(None, list_of_strings.strings); } #[test] fn test_deserialize_optional_string_or_vec_string_single_value() { let list_of_strings: ObjectWithOptionalStringOrVecString = serde_json::from_str(r#"{ "strings": "v1" }"#).unwrap(); assert_eq!(Some(vec!["v1".to_string()]), list_of_strings.strings); } #[test] fn test_deserialize_optional_string_or_vec_string_vec() { let list_of_strings: ObjectWithOptionalStringOrVecString = serde_json::from_str(r#"{ "strings": ["v1", "v2"] }"#).unwrap(); assert_eq!( Some(vec!["v1".to_string(), "v2".to_string()]), list_of_strings.strings ); } #[test] fn test_token_introspection_successful_with_basic_auth_minimal_response() { let client = new_client() .set_auth_type(AuthType::BasicAuth) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()) .set_introspection_uri( IntrospectionUrl::new("https://introspection/url".to_string()).unwrap(), ); let introspection_response = client .introspect(&AccessToken::new("access_token_123".to_string())) .unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=access_token_123", Some("https://introspection/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"active\": true\ }" .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!(true, introspection_response.active); assert_eq!(None, introspection_response.scopes); assert_eq!(None, introspection_response.client_id); assert_eq!(None, introspection_response.username); assert_eq!(None, introspection_response.token_type); assert_eq!(None, introspection_response.exp); assert_eq!(None, introspection_response.iat); assert_eq!(None, introspection_response.nbf); assert_eq!(None, introspection_response.sub); assert_eq!(None, introspection_response.aud); assert_eq!(None, introspection_response.iss); assert_eq!(None, introspection_response.jti); } #[test] fn test_token_introspection_successful_with_basic_auth_full_response() { let client = new_client() .set_auth_type(AuthType::BasicAuth) .set_redirect_uri(RedirectUrl::new("https://redirect/here".to_string()).unwrap()) .set_introspection_uri( IntrospectionUrl::new("https://introspection/url".to_string()).unwrap(), ); let introspection_response = client .introspect(&AccessToken::new("access_token_123".to_string())) .unwrap() .set_token_type_hint("access_token") .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=access_token_123&token_type_hint=access_token", Some("https://introspection/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: r#"{ "active": true, "scope": "email profile", "client_id": "aaa", "username": "demo", "token_type": "bearer", "exp": 1604073517, "iat": 1604073217, "nbf": 1604073317, "sub": "demo", "aud": "demo", "iss": "http://127.0.0.1:8080/auth/realms/test-realm", "jti": "be1b7da2-fc18-47b3-bdf1-7a4f50bcf53f" }"# .to_string() .into_bytes(), }, )) .unwrap(); assert_eq!(true, introspection_response.active); assert_eq!( Some(vec![ Scope::new("email".to_string()), Scope::new("profile".to_string()) ]), introspection_response.scopes ); assert_eq!( Some(ClientId::new("aaa".to_string())), introspection_response.client_id ); assert_eq!(Some("demo".to_string()), introspection_response.username); assert_eq!( Some(BasicTokenType::Bearer), introspection_response.token_type ); assert_eq!( Some(Utc.timestamp(1604073517, 0)), introspection_response.exp ); assert_eq!( Some(Utc.timestamp(1604073217, 0)), introspection_response.iat ); assert_eq!( Some(Utc.timestamp(1604073317, 0)), introspection_response.nbf ); assert_eq!(Some("demo".to_string()), introspection_response.sub); assert_eq!(Some(vec!["demo".to_string()]), introspection_response.aud); assert_eq!( Some("http://127.0.0.1:8080/auth/realms/test-realm".to_string()), introspection_response.iss ); assert_eq!( Some("be1b7da2-fc18-47b3-bdf1-7a4f50bcf53f".to_string()), introspection_response.jti ); } #[test] fn test_token_revocation_with_missing_url() { let client = new_client(); let result = client .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap_err(); assert_eq!( format!("{}", result), "No revocation endpoint URL specified" ); } #[test] fn test_token_revocation_with_non_https_url() { let client = new_client(); let result = client .set_revocation_uri(RevocationUrl::new("http://revocation/url".to_string()).unwrap()) .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap_err(); assert_eq!( format!("{}", result), "Scheme for revocation endpoint URL must be HTTPS" ); } #[test] fn test_token_revocation_with_unsupported_token_type() { let client = new_client() .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); let revocation_response = client .revoke_token(AccessToken::new("access_token_123".to_string()).into()).unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=access_token_123&token_type_hint=access_token", Some("https://revocation/url".parse().unwrap()), HttpResponse { status_code: StatusCode::BAD_REQUEST, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\"error\": \"unsupported_token_type\", \"error_description\": \"stuff happened\", \ \"error_uri\": \"https://errors\"}" .to_string() .into_bytes(), }, )); assert!(matches!( revocation_response, Err(RequestTokenError::ServerResponse( BasicRevocationErrorResponse { error: RevocationErrorResponseType::UnsupportedTokenType, .. } )) )); } #[test] fn test_token_revocation_with_access_token_and_empty_json_response() { let client = new_client() .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); client .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=access_token_123&token_type_hint=access_token", Some("https://revocation/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: b"{}".to_vec(), }, )) .unwrap(); } #[test] fn test_token_revocation_with_access_token_and_empty_response() { let client = new_client() .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); client .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=access_token_123&token_type_hint=access_token", Some("https://revocation/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![].into_iter().collect(), body: vec![], }, )) .unwrap(); } #[test] fn test_token_revocation_with_access_token_and_non_json_response() { let client = new_client() .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); client .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=access_token_123&token_type_hint=access_token", Some("https://revocation/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/octet-stream").unwrap(), )] .into_iter() .collect(), body: vec![1, 2, 3], }, )) .unwrap(); } #[test] fn test_token_revocation_with_refresh_token() { let client = new_client() .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); client .revoke_token(RefreshToken::new("refresh_token_123".to_string()).into()) .unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=refresh_token_123&token_type_hint=refresh_token", Some("https://revocation/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: b"{}".to_vec(), }, )) .unwrap(); } #[test] fn test_extension_token_revocation_successful() { use self::colorful_extension::*; let client = ColorfulClient::new( ClientId::new("aaa".to_string()), Some(ClientSecret::new("bbb".to_string())), AuthUrl::new("https://example.com/auth".to_string()).unwrap(), Some(TokenUrl::new("https://example.com/token".to_string()).unwrap()), ) .set_revocation_uri(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); client .revoke_token(ColorfulRevocableToken::Red( "colorful_token_123".to_string(), )) .unwrap() .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "token=colorful_token_123&token_type_hint=red_token", Some("https://revocation/url".parse().unwrap()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: b"{}".to_vec(), }, )) .unwrap(); } #[test] fn test_secret_redaction() { let secret = ClientSecret::new("top_secret".to_string()); assert_eq!("ClientSecret([redacted])", format!("{:?}", secret)); } fn new_device_auth_details(expires_in: u32) -> StandardDeviceAuthorizationResponse { let body = format!( "{{\ \"device_code\": \"12345\", \ \"verification_uri\": \"https://verify/here\", \ \"user_code\": \"abcde\", \ \"verification_uri_complete\": \"https://verify/here?abcde\", \ \"expires_in\": {}, \ \"interval\": 1 \ }}", expires_in ); let device_auth_url = DeviceAuthorizationUrl::new("https://deviceauth/here".to_string()).unwrap(); let client = new_client().set_device_authorization_url(device_auth_url.clone()); client .exchange_device_code() .unwrap() .add_extra_param("foo", "bar") .add_scope(Scope::new("openid".to_string())) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "scope=openid&foo=bar", Some(device_auth_url.url().to_owned()), HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: body.into_bytes(), }, )) .unwrap() } struct IncreasingTime { times: std::ops::RangeFrom, } impl IncreasingTime { fn new() -> Self { Self { times: (0..) } } fn next(&mut self) -> DateTime { let next_value = self.times.next().unwrap(); let naive = chrono::NaiveDateTime::from_timestamp(next_value, 0); DateTime::::from_utc(naive, chrono::Utc) } } /// Creates a time function that increments by one second each time. fn mock_time_fn() -> impl Fn() -> DateTime + Send + Sync { let timer = std::sync::Mutex::new(IncreasingTime::new()); move || timer.lock().unwrap().next() } /// Mock sleep function that doesn't actually sleep. fn mock_sleep_fn(_: Duration) {} #[test] fn test_exchange_device_code_and_token() { let details = new_device_auth_details(3600); assert_eq!("12345", details.device_code().secret()); assert_eq!("https://verify/here", details.verification_uri().as_str()); assert_eq!("abcde", details.user_code().secret().as_str()); assert_eq!( "https://verify/here?abcde", details .verification_uri_complete() .unwrap() .secret() .as_str() ); assert_eq!(Duration::from_secs(3600), details.expires_in()); assert_eq!(Duration::from_secs(1), details.interval()); let token = new_client() .exchange_device_access_token(&details) .set_time_fn(mock_time_fn()) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=12345", None, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"openid\"\ }" .to_string() .into_bytes(), }, ), mock_sleep_fn, None) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![Scope::new("openid".to_string()),]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_device_token_authorization_timeout() { let details = new_device_auth_details(2); assert_eq!("12345", details.device_code().secret()); assert_eq!("https://verify/here", details.verification_uri().as_str()); assert_eq!("abcde", details.user_code().secret().as_str()); assert_eq!( "https://verify/here?abcde", details .verification_uri_complete() .unwrap() .secret() .as_str() ); assert_eq!(Duration::from_secs(2), details.expires_in()); assert_eq!(Duration::from_secs(1), details.interval()); let token = new_client() .exchange_device_access_token(&details) .set_time_fn(mock_time_fn()) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=12345", None, HttpResponse { status_code: StatusCode::from_u16(400).unwrap(), headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"error\": \"authorization_pending\", \ \"error_description\": \"Still waiting for user\"\ }" .to_string() .into_bytes(), }, ), mock_sleep_fn, None) .err() .unwrap(); match token { RequestTokenError::ServerResponse(msg) => assert_eq!( msg, DeviceCodeErrorResponse::new( DeviceCodeErrorResponseType::ExpiredToken, Some(String::from("This device code has expired.")), None, ) ), _ => unreachable!("Error should be an expiry"), } } #[test] fn test_device_token_access_denied() { let details = new_device_auth_details(2); assert_eq!("12345", details.device_code().secret()); assert_eq!("https://verify/here", details.verification_uri().as_str()); assert_eq!("abcde", details.user_code().secret().as_str()); assert_eq!( "https://verify/here?abcde", details .verification_uri_complete() .unwrap() .secret() .as_str() ); assert_eq!(Duration::from_secs(2), details.expires_in()); assert_eq!(Duration::from_secs(1), details.interval()); let token = new_client() .exchange_device_access_token(&details) .set_time_fn(mock_time_fn()) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=12345", None, HttpResponse { status_code: StatusCode::from_u16(400).unwrap(), headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"error\": \"access_denied\", \ \"error_description\": \"Access Denied\"\ }" .to_string() .into_bytes(), }, ), mock_sleep_fn, None) .err() .unwrap(); match token { RequestTokenError::ServerResponse(msg) => { assert_eq!(msg.error(), &DeviceCodeErrorResponseType::AccessDenied) } _ => unreachable!("Error should be Access Denied"), } } #[test] fn test_device_token_expired() { let details = new_device_auth_details(2); assert_eq!("12345", details.device_code().secret()); assert_eq!("https://verify/here", details.verification_uri().as_str()); assert_eq!("abcde", details.user_code().secret().as_str()); assert_eq!( "https://verify/here?abcde", details .verification_uri_complete() .unwrap() .secret() .as_str() ); assert_eq!(Duration::from_secs(2), details.expires_in()); assert_eq!(Duration::from_secs(1), details.interval()); let token = new_client() .exchange_device_access_token(&details) .set_time_fn(mock_time_fn()) .request(mock_http_client( vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=12345", None, HttpResponse { status_code: StatusCode::from_u16(400).unwrap(), headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"error\": \"expired_token\", \ \"error_description\": \"Token has expired\"\ }" .to_string() .into_bytes(), }, ), mock_sleep_fn, None) .err() .unwrap(); match token { RequestTokenError::ServerResponse(msg) => { assert_eq!(msg.error(), &DeviceCodeErrorResponseType::ExpiredToken) } _ => unreachable!("Error should be ExpiredToken"), } } fn mock_http_client_success_fail( request_url: Option, request_headers: Vec<(HeaderName, &'static str)>, request_body: &'static str, failure_response: HttpResponse, num_failures: usize, success_response: HttpResponse, ) -> impl Fn(HttpRequest) -> Result { let responses: Vec = std::iter::repeat(failure_response) .take(num_failures) .chain(std::iter::once(success_response)) .collect(); let sync_responses = std::sync::Mutex::new(responses); move |request: HttpRequest| { assert_eq!( &request.url, request_url .as_ref() .unwrap_or(&Url::parse("https://example.com/token").unwrap()) ); assert_eq!( request.headers, request_headers .iter() .map(|(name, value)| (name.clone(), HeaderValue::from_str(value).unwrap())) .collect(), ); assert_eq!(&String::from_utf8(request.body).unwrap(), request_body); { let mut rsp_vec = sync_responses.lock().unwrap(); if rsp_vec.len() == 0 { Err(FakeError::Err) } else { Ok(rsp_vec.remove(0)) } } } } #[test] fn test_device_token_pending_then_success() { let details = new_device_auth_details(20); assert_eq!("12345", details.device_code().secret()); assert_eq!("https://verify/here", details.verification_uri().as_str()); assert_eq!("abcde", details.user_code().secret().as_str()); assert_eq!( "https://verify/here?abcde", details .verification_uri_complete() .unwrap() .secret() .as_str() ); assert_eq!(Duration::from_secs(20), details.expires_in()); assert_eq!(Duration::from_secs(1), details.interval()); let token = new_client() .exchange_device_access_token(&details) .set_time_fn(mock_time_fn()) .request(mock_http_client_success_fail( None, vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=12345", HttpResponse { status_code: StatusCode::from_u16(400).unwrap(), headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"error\": \"authorization_pending\", \ \"error_description\": \"Still waiting for user\"\ }" .to_string() .into_bytes(), }, 5, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"openid\"\ }" .to_string() .into_bytes(), }, ), mock_sleep_fn, None) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![Scope::new("openid".to_string()),]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_device_token_slowdown_then_success() { let details = new_device_auth_details(3600); assert_eq!("12345", details.device_code().secret()); assert_eq!("https://verify/here", details.verification_uri().as_str()); assert_eq!("abcde", details.user_code().secret().as_str()); assert_eq!( "https://verify/here?abcde", details .verification_uri_complete() .unwrap() .secret() .as_str() ); assert_eq!(Duration::from_secs(3600), details.expires_in()); assert_eq!(Duration::from_secs(1), details.interval()); let token = new_client() .exchange_device_access_token(&details) .set_time_fn(mock_time_fn()) .request(mock_http_client_success_fail( None, vec![ (ACCEPT, "application/json"), (CONTENT_TYPE, "application/x-www-form-urlencoded"), (AUTHORIZATION, "Basic YWFhOmJiYg=="), ], "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=12345", HttpResponse { status_code: StatusCode::from_u16(400).unwrap(), headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"error\": \"slow_down\", \ \"error_description\": \"Woah there partner\"\ }" .to_string() .into_bytes(), }, 5, HttpResponse { status_code: StatusCode::OK, headers: vec![( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), )] .into_iter() .collect(), body: "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"openid\"\ }" .to_string() .into_bytes(), }, ), mock_sleep_fn, None) .unwrap(); assert_eq!("12/34", token.access_token().secret()); assert_eq!(BasicTokenType::Bearer, *token.token_type()); assert_eq!( Some(&vec![Scope::new("openid".to_string()),]), token.scopes() ); assert_eq!(None, token.expires_in()); assert!(token.refresh_token().is_none()); } #[test] fn test_send_sync_impl() { fn is_sync_and_send() {} #[derive(Debug)] struct TestError; impl std::fmt::Display for TestError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "TestError") } } impl std::error::Error for TestError {} is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< Client< StandardErrorResponse, StandardTokenResponse, BasicTokenType, StandardTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, >, >(); is_sync_and_send::< ClientCredentialsTokenRequest< StandardErrorResponse, StandardTokenResponse, BasicTokenType, >, >(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< CodeTokenRequest< StandardErrorResponse, StandardTokenResponse, BasicTokenType, >, >(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< PasswordTokenRequest< StandardErrorResponse, StandardTokenResponse, BasicTokenType, >, >(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< RefreshTokenRequest< StandardErrorResponse, StandardTokenResponse, BasicTokenType, >, >(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::>(); is_sync_and_send::>(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::>>( ); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< DeviceAccessTokenRequest< StandardTokenResponse, BasicTokenType, EmptyExtraDeviceAuthorizationFields, >, >(); is_sync_and_send::>>(); is_sync_and_send::(); is_sync_and_send::(); #[cfg(feature = "curl")] is_sync_and_send::(); #[cfg(feature = "reqwest")] is_sync_and_send::>(); } oauth2-4.4.1/src/types.rs000064400000000000000000000462561046102023000133520ustar 00000000000000use std::convert::Into; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Formatter}; use std::ops::Deref; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use url::Url; macro_rules! new_type { // Convenience pattern without an impl. ( $(#[$attr:meta])* $name:ident( $(#[$type_attr:meta])* $type:ty ) ) => { new_type![ @new_type $(#[$attr])*, $name( $(#[$type_attr])* $type ), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), impl {} ]; }; // Main entry point with an impl. ( $(#[$attr:meta])* $name:ident( $(#[$type_attr:meta])* $type:ty ) impl { $($item:tt)* } ) => { new_type![ @new_type $(#[$attr])*, $name( $(#[$type_attr])* $type ), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), impl { $($item)* } ]; }; // Actual implementation, after stringifying the #[doc] attr. ( @new_type $(#[$attr:meta])*, $name:ident( $(#[$type_attr:meta])* $type:ty ), $new_doc:expr, impl { $($item:tt)* } ) => { $(#[$attr])* #[derive(Clone, Debug, PartialEq)] pub struct $name( $(#[$type_attr])* $type ); impl $name { $($item)* #[doc = $new_doc] pub const fn new(s: $type) -> Self { $name(s) } } impl Deref for $name { type Target = $type; fn deref(&self) -> &$type { &self.0 } } impl Into<$type> for $name { fn into(self) -> $type { self.0 } } } } macro_rules! new_secret_type { ( $(#[$attr:meta])* $name:ident($type:ty) ) => { new_secret_type![ $(#[$attr])* $name($type) impl {} ]; }; ( $(#[$attr:meta])* $name:ident($type:ty) impl { $($item:tt)* } ) => { new_secret_type![ $(#[$attr])*, $name($type), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), concat!("Get the secret contained within this `", stringify!($name), "`."), impl { $($item)* } ]; }; ( $(#[$attr:meta])*, $name:ident($type:ty), $new_doc:expr, $secret_doc:expr, impl { $($item:tt)* } ) => { $( #[$attr] )* pub struct $name($type); impl $name { $($item)* #[doc = $new_doc] pub fn new(s: $type) -> Self { $name(s) } /// #[doc = $secret_doc] /// /// # Security Warning /// /// Leaking this value may compromise the security of the OAuth2 flow. /// pub fn secret(&self) -> &$type { &self.0 } } impl Debug for $name { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { write!(f, concat!(stringify!($name), "([redacted])")) } } }; } /// /// Creates a URL-specific new type /// /// Types created by this macro enforce during construction that the contained value represents a /// syntactically valid URL. However, comparisons and hashes of these types are based on the string /// representation given during construction, disregarding any canonicalization performed by the /// underlying `Url` struct. OpenID Connect requires certain URLs (e.g., ID token issuers) to be /// compared exactly, without canonicalization. /// /// In addition to the raw string representation, these types include a `url` method to retrieve a /// parsed `Url` struct. /// macro_rules! new_url_type { // Convenience pattern without an impl. ( $(#[$attr:meta])* $name:ident ) => { new_url_type![ @new_type_pub $(#[$attr])*, $name, concat!("Create a new `", stringify!($name), "` from a `String` to wrap a URL."), concat!("Create a new `", stringify!($name), "` from a `Url` to wrap a URL."), concat!("Return this `", stringify!($name), "` as a parsed `Url`."), impl {} ]; }; // Main entry point with an impl. ( $(#[$attr:meta])* $name:ident impl { $($item:tt)* } ) => { new_url_type![ @new_type_pub $(#[$attr])*, $name, concat!("Create a new `", stringify!($name), "` from a `String` to wrap a URL."), concat!("Create a new `", stringify!($name), "` from a `Url` to wrap a URL."), concat!("Return this `", stringify!($name), "` as a parsed `Url`."), impl { $($item)* } ]; }; // Actual implementation, after stringifying the #[doc] attr. ( @new_type_pub $(#[$attr:meta])*, $name:ident, $new_doc:expr, $from_url_doc:expr, $url_doc:expr, impl { $($item:tt)* } ) => { $(#[$attr])* #[derive(Clone)] pub struct $name(Url, String); impl $name { #[doc = $new_doc] pub fn new(url: String) -> Result { Ok($name(Url::parse(&url)?, url)) } #[doc = $from_url_doc] pub fn from_url(url: Url) -> Self { let s = url.to_string(); Self(url, s) } #[doc = $url_doc] pub fn url(&self) -> &Url { return &self.0; } $($item)* } impl Deref for $name { type Target = String; fn deref(&self) -> &String { &self.1 } } impl ::std::fmt::Debug for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { let mut debug_trait_builder = f.debug_tuple(stringify!($name)); debug_trait_builder.field(&self.1); debug_trait_builder.finish() } } impl<'de> ::serde::Deserialize<'de> for $name { fn deserialize(deserializer: D) -> Result where D: ::serde::de::Deserializer<'de>, { struct UrlVisitor; impl<'de> ::serde::de::Visitor<'de> for UrlVisitor { type Value = $name; fn expecting( &self, formatter: &mut ::std::fmt::Formatter ) -> ::std::fmt::Result { formatter.write_str(stringify!($name)) } fn visit_str(self, v: &str) -> Result where E: ::serde::de::Error, { $name::new(v.to_string()).map_err(E::custom) } } deserializer.deserialize_str(UrlVisitor {}) } } impl ::serde::Serialize for $name { fn serialize(&self, serializer: SE) -> Result where SE: ::serde::Serializer, { serializer.serialize_str(&self.1) } } impl ::std::hash::Hash for $name { fn hash(&self, state: &mut H) -> () { ::std::hash::Hash::hash(&(self.1), state); } } impl Ord for $name { fn cmp(&self, other: &$name) -> ::std::cmp::Ordering { self.1.cmp(&other.1) } } impl PartialOrd for $name { fn partial_cmp(&self, other: &$name) -> Option<::std::cmp::Ordering> { Some(self.cmp(other)) } } impl PartialEq for $name { fn eq(&self, other: &$name) -> bool { self.1 == other.1 } } impl Eq for $name {} }; } new_type![ /// /// Client identifier issued to the client during the registration process described by /// [Section 2.2](https://tools.ietf.org/html/rfc6749#section-2.2). /// #[derive(Deserialize, Serialize, Eq, Hash)] ClientId(String) ]; new_url_type![ /// /// URL of the authorization server's authorization endpoint. /// AuthUrl ]; new_url_type![ /// /// URL of the authorization server's token endpoint. /// TokenUrl ]; new_url_type![ /// /// URL of the client's redirection endpoint. /// RedirectUrl ]; new_url_type![ /// /// URL of the client's [RFC 7662 OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) endpoint. /// IntrospectionUrl ]; new_url_type![ /// /// URL of the authorization server's RFC 7009 token revocation endpoint. /// RevocationUrl ]; new_url_type![ /// /// URL of the client's device authorization endpoint. /// DeviceAuthorizationUrl ]; new_url_type![ /// /// URL of the end-user verification URI on the authorization server. /// EndUserVerificationUrl ]; new_type![ /// /// Authorization endpoint response (grant) type defined in /// [Section 3.1.1](https://tools.ietf.org/html/rfc6749#section-3.1.1). /// #[derive(Deserialize, Serialize, Eq, Hash)] ResponseType(String) ]; new_type![ /// /// Resource owner's username used directly as an authorization grant to obtain an access /// token. /// #[derive(Deserialize, Serialize, Eq, Hash)] ResourceOwnerUsername(String) ]; new_type![ /// /// Access token scope, as defined by the authorization server. /// #[derive(Deserialize, Serialize, Eq, Hash)] Scope(String) ]; impl AsRef for Scope { fn as_ref(&self) -> &str { self } } new_type![ /// /// Code Challenge Method used for [PKCE](https://tools.ietf.org/html/rfc7636) protection /// via the `code_challenge_method` parameter. /// #[derive(Deserialize, Serialize, Eq, Hash)] PkceCodeChallengeMethod(String) ]; // This type intentionally does not implement Clone in order to make it difficult to reuse PKCE // challenges across multiple requests. new_secret_type![ /// /// Code Verifier used for [PKCE](https://tools.ietf.org/html/rfc7636) protection via the /// `code_verifier` parameter. The value must have a minimum length of 43 characters and a /// maximum length of 128 characters. Each character must be ASCII alphanumeric or one of /// the characters "-" / "." / "_" / "~". /// #[derive(Deserialize, Serialize)] PkceCodeVerifier(String) ]; /// /// Code Challenge used for [PKCE](https://tools.ietf.org/html/rfc7636) protection via the /// `code_challenge` parameter. /// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct PkceCodeChallenge { code_challenge: String, code_challenge_method: PkceCodeChallengeMethod, } impl PkceCodeChallenge { /// /// Generate a new random, base64-encoded SHA-256 PKCE code. /// pub fn new_random_sha256() -> (Self, PkceCodeVerifier) { Self::new_random_sha256_len(32) } /// /// Generate a new random, base64-encoded SHA-256 PKCE challenge code and verifier. /// /// # Arguments /// /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. /// The value must be in the range 32 to 96 inclusive in order to generate a verifier /// with a suitable length. /// /// # Panics /// /// This method panics if the resulting PKCE code verifier is not of a suitable length /// to comply with [RFC 7636](https://tools.ietf.org/html/rfc7636). /// pub fn new_random_sha256_len(num_bytes: u32) -> (Self, PkceCodeVerifier) { let code_verifier = Self::new_random_len(num_bytes); ( Self::from_code_verifier_sha256(&code_verifier), code_verifier, ) } /// /// Generate a new random, base64-encoded PKCE code verifier. /// /// # Arguments /// /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. /// The value must be in the range 32 to 96 inclusive in order to generate a verifier /// with a suitable length. /// /// # Panics /// /// This method panics if the resulting PKCE code verifier is not of a suitable length /// to comply with [RFC 7636](https://tools.ietf.org/html/rfc7636). /// fn new_random_len(num_bytes: u32) -> PkceCodeVerifier { // The RFC specifies that the code verifier must have "a minimum length of 43 // characters and a maximum length of 128 characters". // This implies 32-96 octets of random data to be base64 encoded. assert!(num_bytes >= 32 && num_bytes <= 96); let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); PkceCodeVerifier::new(base64::encode_config( &random_bytes, base64::URL_SAFE_NO_PAD, )) } /// /// Generate a SHA-256 PKCE code challenge from the supplied PKCE code verifier. /// /// # Panics /// /// This method panics if the supplied PKCE code verifier is not of a suitable length /// to comply with [RFC 7636](https://tools.ietf.org/html/rfc7636). /// pub fn from_code_verifier_sha256(code_verifier: &PkceCodeVerifier) -> Self { // The RFC specifies that the code verifier must have "a minimum length of 43 // characters and a maximum length of 128 characters". assert!(code_verifier.secret().len() >= 43 && code_verifier.secret().len() <= 128); let digest = Sha256::digest(code_verifier.secret().as_bytes()); let code_challenge = base64::encode_config(&digest, base64::URL_SAFE_NO_PAD); Self { code_challenge, code_challenge_method: PkceCodeChallengeMethod::new("S256".to_string()), } } /// /// Generate a new random, base64-encoded PKCE code. /// Use is discouraged unless the endpoint does not support SHA-256. /// /// # Panics /// /// This method panics if the supplied PKCE code verifier is not of a suitable length /// to comply with [RFC 7636](https://tools.ietf.org/html/rfc7636). /// #[cfg(feature = "pkce-plain")] pub fn new_random_plain() -> (Self, PkceCodeVerifier) { let code_verifier = Self::new_random_len(32); ( Self::from_code_verifier_plain(&code_verifier), code_verifier, ) } /// /// Generate a plain PKCE code challenge from the supplied PKCE code verifier. /// Use is discouraged unless the endpoint does not support SHA-256. /// /// # Panics /// /// This method panics if the supplied PKCE code verifier is not of a suitable length /// to comply with [RFC 7636](https://tools.ietf.org/html/rfc7636). /// #[cfg(feature = "pkce-plain")] pub fn from_code_verifier_plain(code_verifier: &PkceCodeVerifier) -> Self { // The RFC specifies that the code verifier must have "a minimum length of 43 // characters and a maximum length of 128 characters". assert!(code_verifier.secret().len() >= 43 && code_verifier.secret().len() <= 128); let code_challenge = code_verifier.secret().clone(); Self { code_challenge, code_challenge_method: PkceCodeChallengeMethod::new("plain".to_string()), } } /// /// Returns the PKCE code challenge as a string. /// pub fn as_str(&self) -> &str { &self.code_challenge } /// /// Returns the PKCE code challenge method as a string. /// pub fn method(&self) -> &PkceCodeChallengeMethod { &self.code_challenge_method } } new_secret_type![ /// /// Client password issued to the client during the registration process described by /// [Section 2.2](https://tools.ietf.org/html/rfc6749#section-2.2). /// #[derive(Clone, Deserialize, Serialize)] ClientSecret(String) ]; new_secret_type![ /// /// Value used for [CSRF](https://tools.ietf.org/html/rfc6749#section-10.12) protection /// via the `state` parameter. /// #[must_use] #[derive(Clone, Deserialize, Serialize)] CsrfToken(String) impl { /// /// Generate a new random, base64-encoded 128-bit CSRF token. /// pub fn new_random() -> Self { CsrfToken::new_random_len(16) } /// /// Generate a new random, base64-encoded CSRF token of the specified length. /// /// # Arguments /// /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. /// pub fn new_random_len(num_bytes: u32) -> Self { let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); CsrfToken::new(base64::encode_config(&random_bytes, base64::URL_SAFE_NO_PAD)) } } ]; new_secret_type![ /// /// Authorization code returned from the authorization endpoint. /// #[derive(Clone, Deserialize, Serialize)] AuthorizationCode(String) ]; new_secret_type![ /// /// Refresh token used to obtain a new access token (if supported by the authorization server). /// #[derive(Clone, Deserialize, Serialize)] RefreshToken(String) ]; new_secret_type![ /// /// Access token returned by the token endpoint and used to access protected resources. /// #[derive(Clone, Deserialize, Serialize)] AccessToken(String) ]; new_secret_type![ /// /// Resource owner's password used directly as an authorization grant to obtain an access /// token. /// #[derive(Clone)] ResourceOwnerPassword(String) ]; new_secret_type![ /// /// Device code returned by the device authorization endpoint and used to query the token endpoint. /// #[derive(Clone, Deserialize, Serialize)] DeviceCode(String) ]; new_secret_type![ /// /// Verification URI returned by the device authorization endpoint and visited by the user /// to authorize. Contains the user code. /// #[derive(Clone, Deserialize, Serialize)] VerificationUriComplete(String) ]; new_secret_type![ /// /// User code returned by the device authorization endpoint and used by the user to authorize at /// the verification URI. /// #[derive(Clone, Deserialize, Serialize)] UserCode(String) ]; oauth2-4.4.1/src/ureq.rs000064400000000000000000000037111046102023000131470ustar 00000000000000use http::{ header::{HeaderMap, HeaderValue, CONTENT_TYPE}, method::Method, status::StatusCode, }; use super::{HttpRequest, HttpResponse}; /// /// Error type returned by failed ureq HTTP requests. /// #[derive(Debug, thiserror::Error)] pub enum Error { /// Non-ureq HTTP error. #[error("HTTP error")] Http(#[from] http::Error), /// IO error #[error("IO error")] IO(#[from] std::io::Error), /// Other error. #[error("Other error: {}", _0)] Other(String), /// Error returned by ureq crate. // boxed due to https://github.com/algesten/ureq/issues/296 #[error("ureq request failed")] Ureq(#[from] Box), } /// /// Synchronous HTTP client for ureq. /// pub fn http_client(request: HttpRequest) -> Result { let mut req = if let Method::POST = request.method { ureq::post(&request.url.to_string()) } else { ureq::get(&request.url.to_string()) }; for (name, value) in request.headers { if let Some(name) = name { req = req.set( &name.to_string(), value.to_str().map_err(|_| { Error::Other(format!( "invalid {} header value {:?}", name, value.as_bytes() )) })?, ); } } let response = if let Method::POST = request.method { req.send_bytes(&request.body) } else { req.call() } .map_err(Box::new)?; Ok(HttpResponse { status_code: StatusCode::from_u16(response.status()) .map_err(|err| Error::Http(err.into()))?, headers: vec![( CONTENT_TYPE, HeaderValue::from_str(response.content_type()) .map_err(|err| Error::Http(err.into()))?, )] .into_iter() .collect::(), body: response.into_string()?.as_bytes().into(), }) }