oauth2-5.0.0/.cargo_vcs_info.json0000644000000001360000000000100122400ustar { "git": { "sha1": "f3424b4b2190c83c6d031fdc71eed2351d49e0df" }, "path_in_vcs": "" }oauth2-5.0.0/.github/FUNDING.yml000064400000000000000000000000241046102023000142010ustar 00000000000000github: [ramosbugs] oauth2-5.0.0/.github/workflows/main.yml000064400000000000000000000061021046102023000160730ustar 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.65.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 may not support the MSRV, so we use a Cargo.lock file for those # builds. - name: Use Rust 1.65 lockfile if: ${{ matrix.rust_os.rust == '1.65.0' }} run: | cp Cargo-1.65.lock Cargo.lock echo "CARGO_LOCKED=--locked" >> $GITHUB_ENV - name: Run tests run: cargo ${CARGO_LOCKED} test --tests --examples - name: Doc tests run: | cargo ${CARGO_LOCKED} test --doc cargo ${CARGO_LOCKED} test --doc --no-default-features cargo ${CARGO_LOCKED} test --doc --all-features - name: Test with all features enabled run: cargo ${CARGO_LOCKED} test --all-features # Curl without reqwest (examples will not build) - name: Test with curl (w/o reqwest) run: cargo ${CARGO_LOCKED} test --tests --features curl --no-default-features - name: Check fmt if: ${{ matrix.rust_os.rust == '1.65.0' }} run: cargo ${CARGO_LOCKED} fmt --all -- --check - name: Clippy if: ${{ matrix.rust_os.rust == '1.65.0' }} run: cargo ${CARGO_LOCKED} clippy --all --all-features -- --deny warnings - name: Audit if: ${{ matrix.rust_os.rust == 'stable' }} run: | cargo ${CARGO_LOCKED} 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 ${CARGO_LOCKED} audit --ignore RUSTSEC-2020-0159 - name: Check WASM build run: cargo ${CARGO_LOCKED} check --target wasm32-unknown-unknown oauth2-5.0.0/.gitignore000064400000000000000000000000461046102023000130200ustar 00000000000000.idea/** *.iml /target /Cargo.lock *~ oauth2-5.0.0/Cargo-1.65.lock000064400000000000000000001367151046102023000133410ustar 00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "async-channel" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", "event-listener 5.2.0", "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ "async-lock", "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-global-executor" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.2.0", "async-executor", "async-io", "async-lock", "blocking", "futures-lite", "once_cell", ] [[package]] name = "async-io" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-lock" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", "event-listener-strategy 0.4.0", "pin-project-lite", ] [[package]] name = "async-std" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-channel 1.9.0", "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.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel 2.2.0", "async-lock", "async-task", "fastrand", "futures-io", "futures-lite", "piper", "tracing", ] [[package]] name = "bumpalo" version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[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.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", "windows-targets 0.52.4", ] [[package]] name = "concurrent-queue" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "curl" version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2", "windows-sys 0.52.0", ] [[package]] name = "curl-sys" version = "0.4.72+curl-8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "windows-sys 0.52.0", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener 4.0.3", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" dependencies = [ "event-listener 5.2.0", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[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.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[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 = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "hyper" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "httparse", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", "socket2", "tokio", "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 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.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libz-sys" version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "value-bag", ] [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "oauth2" version = "5.0.0" 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 = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[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.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", "rustix", "tracing", "windows-sys 0.52.0", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "reqwest" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", "winreg", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64", ] [[package]] name = "rustls-pki-types" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" [[package]] name = "rustls-webpki" version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "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", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", "rustls-pki-types", "tokio", ] [[package]] name = "tower" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[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.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" version = "2.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" dependencies = [ "base64", "flate2", "log", "once_cell", "rustls", "rustls-pki-types", "rustls-webpki", "url", "webpki-roots", ] [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "uuid" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] [[package]] name = "value-bag" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "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.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" dependencies = [ "rustls-pki-types", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" oauth2-5.0.0/Cargo.lock0000644000001367150000000000100102300ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "async-channel" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", "event-listener 5.2.0", "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ "async-lock", "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-global-executor" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.2.0", "async-executor", "async-io", "async-lock", "blocking", "futures-lite", "once_cell", ] [[package]] name = "async-io" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-lock" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", "event-listener-strategy 0.4.0", "pin-project-lite", ] [[package]] name = "async-std" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-channel 1.9.0", "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.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel 2.2.0", "async-lock", "async-task", "fastrand", "futures-io", "futures-lite", "piper", "tracing", ] [[package]] name = "bumpalo" version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[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.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", "windows-targets 0.52.4", ] [[package]] name = "concurrent-queue" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "curl" version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2", "windows-sys 0.52.0", ] [[package]] name = "curl-sys" version = "0.4.72+curl-8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "windows-sys 0.52.0", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener 4.0.3", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" dependencies = [ "event-listener 5.2.0", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[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.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[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 = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "hyper" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "httparse", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", "socket2", "tokio", "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 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.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libz-sys" version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "value-bag", ] [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "oauth2" version = "5.0.0" 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 = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[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.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "parking" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", "rustix", "tracing", "windows-sys 0.52.0", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "reqwest" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", "winreg", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64", ] [[package]] name = "rustls-pki-types" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" [[package]] name = "rustls-webpki" version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "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", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", "rustls-pki-types", "tokio", ] [[package]] name = "tower" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[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.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" version = "2.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" dependencies = [ "base64", "flate2", "log", "once_cell", "rustls", "rustls-pki-types", "rustls-webpki", "url", "webpki-roots", ] [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "uuid" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] [[package]] name = "value-bag" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "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.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" dependencies = [ "rustls-pki-types", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" oauth2-5.0.0/Cargo.toml0000644000000054000000000000100102350ustar # 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 = "2021" rust-version = "1.65" name = "oauth2" version = "5.0.0" 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 [[example]] name = "github" required-features = ["reqwest-blocking"] [[example]] name = "google" required-features = ["reqwest-blocking"] [[example]] name = "google_devicecode" required-features = ["reqwest-blocking"] [[example]] name = "letterboxd" required-features = ["reqwest-blocking"] [[example]] name = "msgraph" required-features = ["reqwest-blocking"] [[example]] name = "wunderlist" required-features = ["reqwest-blocking"] [dependencies.base64] version = ">= 0.21, <0.23" [dependencies.chrono] version = "0.4.31" features = [ "clock", "serde", "std", "wasmbind", ] default-features = false [dependencies.http] version = "1.0" [dependencies.rand] version = "0.8" [dependencies.reqwest] version = "0.12" 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.13" [dev-dependencies.hex] version = "0.4" [dev-dependencies.hmac] version = "0.12" [dev-dependencies.tokio] version = "1.0" features = ["full"] [dev-dependencies.uuid] version = "1.10" features = ["v4"] [features] default = [ "reqwest", "rustls-tls", ] native-tls = ["reqwest/native-tls"] pkce-plain = [] reqwest-blocking = ["reqwest/blocking"] rustls-tls = ["reqwest/rustls-tls"] timing-resistant-secret-traits = [] [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-5.0.0/Cargo.toml.orig000064400000000000000000000037271046102023000137300ustar 00000000000000[package] name = "oauth2" authors = ["Alex Crichton ", "Florin Lipan ", "David A. Ramos "] version = "5.0.0" license = "MIT OR Apache-2.0" description = "An extensible, strongly-typed implementation of OAuth2" repository = "https://github.com/ramosbugs/oauth2-rs" edition = "2021" readme = "README.md" rust-version = "1.65" [package.metadata.docs.rs] all-features = true [badges] maintenance = { status = "actively-developed" } [features] default = ["reqwest", "rustls-tls"] pkce-plain = [] native-tls = ["reqwest/native-tls"] reqwest-blocking = ["reqwest/blocking"] rustls-tls = ["reqwest/rustls-tls"] timing-resistant-secret-traits = [] [[example]] name = "github" required-features = ["reqwest-blocking"] [[example]] name = "google" required-features = ["reqwest-blocking"] [[example]] name = "google_devicecode" required-features = ["reqwest-blocking"] [[example]] name = "letterboxd" required-features = ["reqwest-blocking"] [[example]] name = "msgraph" required-features = ["reqwest-blocking"] [[example]] name = "wunderlist" required-features = ["reqwest-blocking"] [dependencies] base64 = ">= 0.21, <0.23" thiserror = "1.0" http = "1.0" rand = "0.8" reqwest = { version = "0.12", optional = true, default-features = false } 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.31", default-features = false, features = ["clock", "serde", "std", "wasmbind"] } 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 = "1.10", features = ["v4"] } anyhow = "1.0" tokio = { version = "1.0", features = ["full"] } async-std = "1.13" oauth2-5.0.0/LICENSE-APACHE000064400000000000000000000251371046102023000127640ustar 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-5.0.0/LICENSE-MIT000064400000000000000000000020411046102023000124610ustar 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-5.0.0/README.md000064400000000000000000000024401046102023000123070ustar 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). For authentication (e.g., single sign-on or social login) purposes, consider using the [`openidconnect`](https://github.com/ramosbugs/openidconnect-rs) crate, which is built on top of this one. ## Minimum Supported Rust Version (MSRV) The MSRV for *5.0* and newer releases of this crate is Rust **1.65**. The MSRV for *4.x* releases of this crate is Rust 1.45. Beginning with the 5.0.0 release, this crate will maintain a policy of supporting Rust releases going back at least 6 months. Changes that break compatibility with Rust releases older than 6 months will no longer be considered SemVer breaking changes and will not result in a new major version number for this crate. MSRV changes will coincide with minor version updates and will not happen in patch releases. oauth2-5.0.0/UPGRADE.md000064400000000000000000000275201046102023000124470ustar 00000000000000# Upgrade Guide ## Upgrading from 4.x to 5.x The 5.0 release includes breaking changes to address several long-standing API issues, along with a few minor improvements. Consider following the tips below to help ensure a smooth upgrade process. ### Upgrade Rust to 1.65 or newer The minimum supported Rust version (MSRV) is now 1.65. Going forward, this crate will maintain a policy of supporting Rust releases going back at least 6 months. Changes that break compatibility with Rust releases older than 6 months will no longer be considered SemVer breaking changes and will not result in a new major version number for this crate. MSRV changes will coincide with minor version updates and will not happen in patch releases. ### Add typestate generic types to `Client` Each auth flow depends on one or more server endpoints. For example, the authorization code flow depends on both an authorization endpoint and a token endpoint, while the client credentials flow only depends on a token endpoint. Previously, it was possible to instantiate a `Client` without a token endpoint and then attempt to use an auth flow that required a token endpoint, leading to errors at runtime. Also, the authorization endpoint was always required, even for auth flows that do not use it. In the 5.0 release, all endpoints are optional. [Typestates](https://cliffle.com/blog/rust-typestate/) are used to statically track, at compile time, which endpoints' setters (e.g., `set_auth_uri()`) have been called. Auth flows that depend on an endpoint cannot be used without first calling the corresponding setter, which is enforced by the compiler's type checker. This guarantees that certain errors will not arise at runtime. In addition to unconditional setters (e.g., `set_auth_uri()`), each endpoint has a corresponding conditional setter (e.g., `set_auth_uri_option()`) that sets a conditional typestate (`EndpointMaybeSet`). When the conditional typestate is set, endpoints can be used via fallible methods that return `Err(ConfigurationError::MissingUrl(_))` if an endpoint has not been set. This is useful in dynamic scenarios such as [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), in which it cannot be determined until runtime whether an endpoint is configured. There are three possible typestates, each implementing the `EndpointState` trait: * `EndpointNotSet`: the corresponding endpoint has **not** been set and cannot be used. * `EndpointSet`: the corresponding endpoint **has** been set and is ready to be used. * `EndpointMaybeSet`: the corresponding endpoint **may have** been set and can be used via fallible methods that return `Result<_, ConfigurationError>`. The following code changes are required to support the new interface: 1. Update calls to [`Client::new()`](https://docs.rs/oauth2/latest/oauth2/struct.Client.html#method.new) to use the single-argument constructor (which accepts only a `ClientId`). Use the `set_auth_uri()`, `set_token_uri()`, and `set_client_secret()` methods to set the authorization endpoint, token endpoint, and client secret, respectively, if applicable to your application's auth flows. 2. If required by your usage of the `Client` or `BasicClient` types (i.e., if you see related compiler errors), add the following generic parameters: ```rust HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, ``` For example, if you store a `BasicClient` within another data type, you may need to annotate it as `BasicClient` if it has both an authorization endpoint and a token endpoint set. Compiler error messages will likely guide you to the appropriate combination of typestates. If, instead of using `BasicClient`, you are directly using `Client` with a different set of type parameters, you will need to append the five generic typestate parameters. For example, replace: ```rust type SpecialClient = Client< BasicErrorResponse, SpecialTokenResponse, BasicTokenType, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, >; ``` with: ```rust type SpecialClient< HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointNotSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet, > = Client< BasicErrorResponse, SpecialTokenResponse, BasicTokenType, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, >; ``` The default values (`= EndpointNotSet`) are optional but often helpful since they will allow you to instantiate a client using `SpecialClient::new()` instead of having to specify `SpecialClient::::new()`. ### Rename endpoint getters and setters for consistency The 4.0 release aimed to align the naming of each endpoint with the terminology used in the relevant RFC. For example, [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1) uses the term "endpoint URI" to refer to the authorization and token endpoints, while [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009#section-2) refers to the "token revocation endpoint URL," and [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2) uses neither "URI" nor "URL" to describe the introspection endpoint. However, the renaming in 4.0 was both internally inconsistent, and inconsistent with the specs. In 5.0, the `Client`'s getters and setters for each endpoint are now named as follows: * Authorization endpoint: `auth_uri()`/`set_auth_uri()` * Token endpoint: `token_uri()`/`set_token_uri()` * Redirect: `redirect_uri()`/`set_redirect_uri()` * Revocation endpoint: `revocation_url()`/`set_revocation_url()` * Introspection endpoint: `introspection_url()`/`set_introspection_url()` * Device authorization endpoint: `device_authorization_url()`/`set_device_authorization_url()` (no change) ### Use stateful HTTP clients Previously, the HTTP clients provided by this crate were stateless. For example, the `oauth2::reqwest::async_http_client()` method would instantiate a new `reqwest::Client` for each request. This meant that TCP connections could not be reused across requests, and customizing HTTP clients (e.g., adding a custom request header to every request) was inconvenient. The 5.0 release introduces two new traits: `AsyncHttpClient` and `SyncHttpClient`. Each `request_async()` and `request()` method now accepts a reference to a type that implements these traits, respectively, rather than a function type. > [!WARNING] > To prevent [SSRF](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) vulnerabilities, be sure to configure the HTTP client **not to follow redirects**. For example, use > [`redirect::Policy::none`](https://docs.rs/reqwest/latest/reqwest/redirect/struct.Policy.html#method.none) > when using `reqwest`, or > [`redirects(0)`](https://docs.rs/ureq/latest/ureq/struct.AgentBuilder.html#method.redirects) > when using `ureq`. The `AsyncHttpClient` trait is implemented for the following types: * `reqwest::Client` (when the default `reqwest` feature is enabled) * Any function type that implements: ```rust Fn(HttpRequest) -> F where E: std::error::Error + 'static, F: Future>, ``` To implement a custom asynchronous HTTP client, either directly implement the `AsyncHttpClient` trait, or use a function that implements the signature above. The `SyncHttpClient` trait is implemented for the following types: * `reqwest::blocking::Client` (when the `reqwest-blocking` feature is enabled; see below) * `ureq::Agent` (when the `ureq` feature is enabled) * `oauth2::CurlHttpClient` (when the `curl` feature is enabled) * Any function type that implements: ```rust Fn(HttpRequest) -> Result where E: std::error::Error + 'static, ``` To implement a custom synchronous HTTP client, either directly implement the `SyncHttpClient` trait, or use a function that implements the signature above. ### Upgrade `http` to 1.0 and `reqwest` to 0.12 The 5.0 release of this crate depends on the new stable [`http`](https://docs.rs/http/latest/http/) 1.0 release, which affects various public interfaces. In particular, `reqwest` has been upgraded to 0.12, which uses `http` 1.0. ### Enable the `reqwest-blocking` feature to use the synchronous `reqwest` HTTP client In 4.0, enabling the (default) `reqwest` feature also enabled `reqwest`'s `blocking` feature. To reduce dependencies and improve compilation speed, the `reqwest` feature now only enables `reqwest`'s asynchronous (non-blocking) client. To use the synchronous (blocking) client, enable the `reqwest-blocking` feature in `Cargo.toml`: ```toml oauth2 = { version = "5", features = ["reqwest-blocking" ] } ``` ### Use `http::{Request, Response}` for custom HTTP clients The `HttpRequest` and `HttpResponse` structs have been replaced with type aliases to [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) and [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html), respectively. Custom HTTP clients will need to be updated to use the `http` types. See the [`reqwest` client implementations](https://github.com/ramosbugs/oauth2-rs/blob/23b952b23e6069525bc7e4c4f2c4924b8d28ce3a/src/reqwest.rs) for an example. ### Import device code flow and token revocation types from the root Previously, certain types were exported from both the root of the crate and the `devicecode` or `revocation` modules. These modules are no longer public, and their public types are exported from the root. For example, if you were previously importing `oauth2::devicecode::DeviceAuthorizationResponse`, instead import `oauth2::DeviceAuthorizationResponse`. ### Replace `TT` generic type parameter in `TokenResponse` with associated type Previously, the `TokenResponse` and `TokenIntrospectionResponse` traits had a generic type parameter `TT: TokenType`. This has been replaced with an associated type called `TokenType`. Uses of `BasicTokenResponse` and `BasicTokenIntrospectionResponse` should continue to work without changes, but custom implementations of either trait will need to be updated to replace the type parameter with an associated type. #### Remove `TT` generic type parameter from `Client` and each `*Request` type Removing the `TT` generic type parameter from `TokenResponse` (see above) made the `TT` parameters to `Client` and each `*Request` (e.g., `CodeTokenRequest`) redundant. Consequently, the `TT` parameter has been removed from each of these types. `BasicClient` should continue to work without any changes, but code that provides generic types for `Client` or any of the `*Response` types will need to be updated to remove the `TT` type parameter. ### Add `Display` to `ErrorResponse` trait To improve error messages, the [`RequestTokenError::ServerResponse`](https://docs.rs/oauth2/latest/oauth2/enum.RequestTokenError.html#variant.ServerResponse) enum variant now prints a message describing the server response using the `Display` trait. For most users (i.e., those using the default [`StandardErrorResponse`](https://docs.rs/oauth2/latest/oauth2/struct.StandardErrorResponse.html)), this does not require any code changes. However, users providing their own implementations of the `ErrorResponse` trait must now implement the `Display` trait. See `StandardErrorResponse`'s [`Display` implementation](https://github.com/ramosbugs/oauth2-rs/blob/9d8f11addf819134f15c6d7f03276adb3d32e80b/src/error.rs#L88-L108) for an example. oauth2-5.0.0/examples/github.rs000064400000000000000000000116301046102023000144770ustar 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; use oauth2::reqwest; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use url::Url; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; 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) .set_client_secret(github_client_secret) .set_auth_uri(auth_url) .set_token_uri(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"), ); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); // 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{authorize_url}\n"); let (code, state) = { // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // The server will terminate itself after collecting the first code. let Some(mut stream) = listener.incoming().flatten().next() else { panic!("listener terminated without accepting a connection"); }; 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 = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); 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(); (code, state) }; 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{token_res:?}\n"); 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() .flat_map(|comma_separated| comma_separated.split(',')) .collect::>() } else { Vec::new() }; println!("Github returned the following scopes:\n{scopes:?}\n"); } } oauth2-5.0.0/examples/github_async.rs000064400000000000000000000121511046102023000156730ustar 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; use oauth2::reqwest; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpListener; use url::Url; use std::env; #[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) .set_client_secret(github_client_secret) .set_auth_uri(auth_url) .set_token_uri(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"), ); let http_client = reqwest::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); // 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{authorize_url}\n"); let (code, state) = { // 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 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 = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); 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(); // The server will terminate itself after collecting the first code. break (code, state); } } }; 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(&http_client).await; println!("Github returned the following token:\n{token_res:?}\n"); 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() .flat_map(|comma_separated| comma_separated.split(',')) .collect::>() } else { Vec::new() }; println!("Github returned the following scopes:\n{scopes:?}\n"); } } oauth2-5.0.0/examples/google.rs000064400000000000000000000126031046102023000144720ustar 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::reqwest; use oauth2::{basic::BasicClient, StandardRevocableToken, TokenResponse}; use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, RevocationUrl, Scope, TokenUrl, }; use url::Url; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; 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) .set_client_secret(google_client_secret) .set_auth_uri(auth_url) .set_token_uri(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_url( RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string()) .expect("Invalid revocation endpoint URL"), ); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); // 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{authorize_url}\n"); let (code, state) = { // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // The server will terminate itself after collecting the first code. let Some(mut stream) = listener.incoming().flatten().next() else { panic!("listener terminated without accepting a connection"); }; 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 = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); 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(); (code, state) }; 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{token_response:?}\n"); // 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"); } oauth2-5.0.0/examples/google_devicecode.rs000064400000000000000000000063251046102023000166500ustar 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; use oauth2::reqwest; use oauth2::{ AuthType, AuthUrl, ClientId, ClientSecret, DeviceAuthorizationResponse, DeviceAuthorizationUrl, ExtraDeviceAuthorizationFields, 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) .set_client_secret(google_client_secret) .set_auth_uri(auth_url) .set_token_uri(token_url) .set_device_authorization_url(device_auth_url) .set_auth_type(AuthType::RequestBody); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); // Request the set of codes from the Device Authorization endpoint. let details: StoringDeviceAuthorizationResponse = device_client .exchange_device_code() .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(), details.user_code().secret(), ); // 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{token:?}\n"); } oauth2-5.0.0/examples/letterboxd.rs000064400000000000000000000126151046102023000153750ustar 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, SyncHttpClient, 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()) .set_client_secret(letterboxd_client_secret.clone()) .set_auth_uri(auth_url) .set_token_uri(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, inner: reqwest::blocking::Client, } impl SigningHttpClient { fn new(client_id: ClientId, client_secret: ClientSecret) -> Self { Self { client_id, client_secret, inner: reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"), } } /// Signs the request before calling `oauth2::reqwest::http_client`. fn execute(&self, mut request: HttpRequest) -> Result { let signed_url = self.sign_url( Url::parse(&request.uri().to_string()).expect("http::Uri should be a valid url::Url"), request.method(), request.body(), ); *request.uri_mut() = signed_url .as_str() .try_into() .expect("url::Url should be a valid http::Uri"); self.inner.call(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-5.0.0/examples/microsoft_devicecode_common_user.rs000064400000000000000000000030511046102023000220000ustar 00000000000000use oauth2::basic::BasicClient; use oauth2::{ AuthUrl, ClientId, DeviceAuthorizationUrl, Scope, StandardDeviceAuthorizationResponse, TokenUrl, }; use std::error::Error; #[tokio::main] async fn main() -> Result<(), Box> { let client = BasicClient::new(ClientId::new("client_id".to_string())) .set_auth_uri(AuthUrl::new( "https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string(), )?) .set_token_uri(TokenUrl::new( "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string(), )?) .set_device_authorization_url(DeviceAuthorizationUrl::new( "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(), )?); let http_client = reqwest::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); let details: StandardDeviceAuthorizationResponse = client .exchange_device_code() .add_scope(Scope::new("read".to_string())) .request_async(&http_client) .await?; eprintln!( "Open this URL in your browser:\n{}\nand enter the code: {}", details.verification_uri(), details.user_code().secret(), ); let token_result = client .exchange_device_access_token(&details) .request_async(&http_client, tokio::time::sleep, None) .await; eprintln!("Token:{token_result:?}"); Ok(()) } oauth2-5.0.0/examples/microsoft_devicecode_tenant_user.rs000064400000000000000000000034571046102023000220130ustar 00000000000000use oauth2::basic::BasicClient; use oauth2::reqwest; use oauth2::StandardDeviceAuthorizationResponse; use oauth2::{AuthUrl, ClientId, DeviceAuthorizationUrl, Scope, TokenUrl}; use std::error::Error; // Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code // Please use your tenant id when using this example const TENANT_ID: &str = "{tenant}"; #[tokio::main] async fn main() -> Result<(), Box> { let client = BasicClient::new(ClientId::new("client_id".to_string())) .set_auth_uri(AuthUrl::new(format!( "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize", TENANT_ID ))?) .set_token_uri(TokenUrl::new(format!( "https://login.microsoftonline.com/{}/oauth2/v2.0/token", TENANT_ID ))?) .set_device_authorization_url(DeviceAuthorizationUrl::new(format!( "https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode", TENANT_ID ))?); let http_client = reqwest::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); let details: StandardDeviceAuthorizationResponse = client .exchange_device_code() .add_scope(Scope::new("read".to_string())) .request_async(&http_client) .await?; eprintln!( "Open this URL in your browser:\n{}\nand enter the code: {}", details.verification_uri(), details.user_code().secret(), ); let token_result = client .exchange_device_access_token(&details) .request_async(&http_client, tokio::time::sleep, None) .await; eprintln!("Token:{token_result:?}"); Ok(()) } oauth2-5.0.0/examples/msgraph.rs000064400000000000000000000126541046102023000146650ustar 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; use oauth2::reqwest; use oauth2::{ AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, TokenUrl, }; use url::Url; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; 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) .set_client_secret(graph_client_secret) .set_auth_uri(auth_url) .set_token_uri(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"), ); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); // 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{authorize_url}\n"); let (code, state) = { // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:3003").unwrap(); // The server will terminate itself after collecting the first code. let Some(mut stream) = listener.incoming().flatten().next() else { panic!("listener terminated without accepting a connection"); }; 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 = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); 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(); (code, state) }; 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{token:?}\n"); } oauth2-5.0.0/examples/wunderlist.rs000064400000000000000000000207111046102023000154150ustar 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::basic::{ BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenType, }; use oauth2::reqwest; use oauth2::StandardRevocableToken; use oauth2::{ AccessToken, AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, EndpointNotSet, ExtraTokenFields, RedirectUrl, RefreshToken, Scope, TokenResponse, TokenUrl, }; use serde::{Deserialize, Serialize}; use url::Url; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use std::time::Duration; type SpecialTokenResponse = NonStandardTokenResponse; type SpecialClient< HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointNotSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet, > = Client< BasicErrorResponse, SpecialTokenResponse, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, >; 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 = "oauth2::helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "oauth2::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, { type TokenType = BasicTokenType; /// 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 /// 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() } } 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) .set_client_secret(wunderlist_client_secret) .set_auth_uri(auth_url) .set_token_uri(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"), ); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .expect("Client should build"); // 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{authorize_url}\n"); let (code, state) = { // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // The server will terminate itself after collecting the first code. let Some(mut stream) = listener.incoming().flatten().next() else { panic!("listener terminated without accepting a connection"); }; 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 = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); 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(); (code, state) }; 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{token_res:?}\n"); } oauth2-5.0.0/src/basic.rs000064400000000000000000000150671046102023000132570ustar 00000000000000use crate::{ revocation::{RevocationErrorResponseType, StandardRevocableToken}, Client, EmptyExtraTokenFields, EndpointNotSet, ErrorResponseType, RequestTokenError, StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType, }; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; /// Basic OAuth2 client specialization, suitable for most applications. pub type BasicClient< HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointNotSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet, > = Client< BasicErrorResponse, BasicTokenResponse, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, >; /// Basic OAuth2 authorization token types. #[derive(Clone, Debug, PartialEq, Eq)] 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, Eq)] 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-5.0.0/src/client.rs000064400000000000000000001232031046102023000134440ustar 00000000000000use crate::{ AccessToken, AuthType, AuthUrl, AuthorizationCode, AuthorizationRequest, ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, ConfigurationError, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationResponse, DeviceAuthorizationUrl, ErrorResponse, ExtraDeviceAuthorizationFields, IntrospectionRequest, IntrospectionUrl, PasswordTokenRequest, RedirectUrl, RefreshToken, RefreshTokenRequest, ResourceOwnerPassword, ResourceOwnerUsername, RevocableToken, RevocationRequest, RevocationUrl, TokenIntrospectionResponse, TokenResponse, TokenUrl, }; use std::marker::PhantomData; mod private { /// Private trait to make `EndpointState` a sealed trait. pub trait EndpointStateSealed {} } /// [Typestate](https://cliffle.com/blog/rust-typestate/) base trait indicating whether an endpoint /// has been configured via its corresponding setter. pub trait EndpointState: private::EndpointStateSealed {} /// [Typestate](https://cliffle.com/blog/rust-typestate/) indicating that an endpoint has not been /// set and cannot be used. #[derive(Clone, Debug)] pub struct EndpointNotSet; impl EndpointState for EndpointNotSet {} impl private::EndpointStateSealed for EndpointNotSet {} /// [Typestate](https://cliffle.com/blog/rust-typestate/) indicating that an endpoint has been set /// and is ready to be used. #[derive(Clone, Debug)] pub struct EndpointSet; impl EndpointState for EndpointSet {} impl private::EndpointStateSealed for EndpointSet {} /// [Typestate](https://cliffle.com/blog/rust-typestate/) indicating that an endpoint may have been /// set and can be used via fallible methods. #[derive(Clone, Debug)] pub struct EndpointMaybeSet; impl EndpointState for EndpointMaybeSet {} impl private::EndpointStateSealed for EndpointMaybeSet {} /// Stores the configuration for an OAuth2 client. /// /// This type implements the /// [Builder Pattern](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html) together with /// [typestates](https://cliffle.com/blog/rust-typestate/#what-are-typestates) to encode whether /// certain fields have been set that are prerequisites to certain authentication flows. For /// example, the authorization endpoint must be set via [`set_auth_uri()`](Client::set_auth_uri) /// before [`authorize_url()`](Client::authorize_url) can be called. Each endpoint has a /// corresponding generic type /// parameter (e.g., `HasAuthUrl`) used to statically enforce these dependencies. These generics /// are set automatically by the corresponding setter functions, and in most cases user code should /// not need to deal with them directly. /// /// In addition to unconditional setters (e.g., [`set_auth_uri()`](Client::set_auth_uri)), each /// endpoint has a corresponding conditional setter (e.g., /// [`set_auth_uri_option()`](Client::set_auth_uri_option)) that sets a /// conditional typestate ([`EndpointMaybeSet`]). When the conditional typestate is set, endpoints /// can be used via fallible methods that return [`ConfigurationError::MissingUrl`] if an /// endpoint has not been set. This is useful in dynamic scenarios such as /// [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), in which /// it cannot be determined until runtime whether an endpoint is configured. /// /// # 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 http::Response; /// # use oauth2::{*, basic::*}; /// # /// # let client = BasicClient::new(ClientId::new("aaa".to_string())) /// # .set_client_secret(ClientSecret::new("bbb".to_string())) /// # .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) /// # .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap()) /// # .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); /// # /// # #[derive(Debug, Error)] /// # enum FakeError { /// # #[error("error")] /// # Err, /// # } /// # /// # let http_client = |_| -> Result { /// # Ok(Response::builder() /// # .status(StatusCode::BAD_REQUEST) /// # .header(CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap()) /// # .body( /// # r#"{"error": "unsupported_token_type", /// # "error_description": "stuff happened", /// # "error_uri": "https://errors"}"# /// # .to_string() /// # .into_bytes(), /// # ) /// # .unwrap()) /// # }; /// # /// 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))); /// ``` /// /// # Examples /// /// See the [crate] root documentation for usage examples. #[derive(Clone, Debug)] pub struct Client< TE, TR, TIR, RT, TRE, HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointNotSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet, > where TE: ErrorResponse, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { pub(crate) client_id: ClientId, pub(crate) client_secret: Option, pub(crate) auth_url: Option, pub(crate) auth_type: AuthType, pub(crate) token_url: Option, pub(crate) redirect_url: Option, pub(crate) introspection_url: Option, pub(crate) revocation_url: Option, pub(crate) device_authorization_url: Option, #[allow(clippy::type_complexity)] pub(crate) phantom: PhantomData<( TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, )>, } impl Client< TE, TR, TIR, RT, TRE, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, { /// Initializes an OAuth2 client with the specified client ID. pub fn new(client_id: ClientId) -> Self { Self { client_id, client_secret: None, auth_url: None, auth_type: AuthType::BasicAuth, token_url: None, redirect_url: None, introspection_url: None, revocation_url: None, device_authorization_url: None, phantom: PhantomData, } } } impl< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Set 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., [`set_client_secret()`](Self::set_client_secret) is not /// called), [`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 } /// Set the authorization endpoint. /// /// The client uses the authorization endpoint 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). pub fn set_auth_uri( self, auth_url: AuthUrl, ) -> Client< TE, TR, TIR, RT, TRE, EndpointSet, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: Some(auth_url), auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Conditionally set the authorization endpoint. /// /// The client uses the authorization endpoint 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). pub fn set_auth_uri_option( self, auth_url: Option, ) -> Client< TE, TR, TIR, RT, TRE, EndpointMaybeSet, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Set the client secret. /// /// A client secret is generally used for confidential (i.e., server-side) OAuth2 clients and /// omitted from public (browser or native app) OAuth2 clients (see /// [RFC 8252](https://tools.ietf.org/html/rfc8252)). pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self { self.client_secret = Some(client_secret); self } /// Set the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint used /// for the Device Authorization Flow. /// /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn set_device_authorization_url( self, device_authorization_url: DeviceAuthorizationUrl, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, EndpointSet, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: Some(device_authorization_url), phantom: PhantomData, } } /// Conditionally set the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization /// endpoint used for the Device Authorization Flow. /// /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn set_device_authorization_url_option( self, device_authorization_url: Option, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, EndpointMaybeSet, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url, phantom: PhantomData, } } /// Set the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. /// /// See [`introspect()`](Self::introspect). pub fn set_introspection_url( self, introspection_url: IntrospectionUrl, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, EndpointSet, HasRevocationUrl, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: Some(introspection_url), revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Conditionally set the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection /// endpoint. /// /// See [`introspect()`](Self::introspect). pub fn set_introspection_url_option( self, introspection_url: Option, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, EndpointMaybeSet, HasRevocationUrl, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Set 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 } /// Set the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// /// See [`revoke_token()`](Self::revoke_token()). pub fn set_revocation_url( self, revocation_url: RevocationUrl, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, EndpointSet, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: Some(revocation_url), device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Conditionally set the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation /// endpoint. /// /// See [`revoke_token()`](Self::revoke_token()). pub fn set_revocation_url_option( self, revocation_url: Option, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, EndpointMaybeSet, HasTokenUrl, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: self.token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Set the token endpoint. /// /// The client uses the token endpoint to exchange an authorization 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). pub fn set_token_uri( self, token_url: TokenUrl, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointSet, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url: Some(token_url), redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Conditionally set the token endpoint. /// /// The client uses the token endpoint to exchange an authorization 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). pub fn set_token_uri_option( self, token_url: Option, ) -> Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointMaybeSet, > { Client { client_id: self.client_id, client_secret: self.client_secret, auth_url: self.auth_url, auth_type: self.auth_type, token_url, redirect_url: self.redirect_url, introspection_url: self.introspection_url, revocation_url: self.revocation_url, device_authorization_url: self.device_authorization_url, phantom: PhantomData, } } /// Return the Client ID. pub fn client_id(&self) -> &ClientId { &self.client_id } /// Return the type of client authentication used for communicating with the authorization /// server. pub fn auth_type(&self) -> &AuthType { &self.auth_type } /// Return the redirect URL used by the authorization endpoint. pub fn redirect_uri(&self) -> Option<&RedirectUrl> { self.redirect_url.as_ref() } } /// Methods requiring an authorization endpoint. impl< TE, TR, TIR, RT, TRE, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, EndpointSet, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Return the authorization endpoint. pub fn auth_uri(&self) -> &AuthUrl { // This is enforced statically via the HasAuthUrl generic type. self.auth_url.as_ref().expect("should have auth_url") } /// Generate an authorization URL for a new authorization request. /// /// Requires [`set_auth_uri()`](Self::set_auth_uri) to have been previously /// called to set the authorization endpoint. /// /// # 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, { self.authorize_url_impl(self.auth_uri(), state_fn) } } /// Methods with a possibly-set authorization endpoint. impl< TE, TR, TIR, RT, TRE, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, EndpointMaybeSet, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Return the authorization endpoint. pub fn auth_uri(&self) -> Option<&AuthUrl> { self.auth_url.as_ref() } /// Generate an authorization URL for a new authorization request. /// /// Requires [`set_auth_uri_option()`](Self::set_auth_uri_option) to have been previously /// called to set the authorization endpoint. /// /// # 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) -> Result where S: FnOnce() -> CsrfToken, { Ok(self.authorize_url_impl( self.auth_uri() .ok_or(ConfigurationError::MissingUrl("authorization"))?, state_fn, )) } } /// Methods requiring a token endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointSet, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, { /// Request an access token using the /// [Client Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). /// /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously /// called to set the token endpoint. pub fn exchange_client_credentials(&self) -> ClientCredentialsTokenRequest { self.exchange_client_credentials_impl(self.token_uri()) } /// Exchange a code returned during the /// [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) /// for 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. /// /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously /// called to set the token endpoint. pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest { self.exchange_code_impl(self.token_uri(), code) } /// Exchange an [RFC 8628](https://tools.ietf.org/html/rfc8628#section-3.2) Device Authorization /// Response returned by [`exchange_device_code()`](Self::exchange_device_code) for an access /// token. /// /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously /// called to set the token endpoint. pub fn exchange_device_access_token<'a, EF>( &'a self, auth_response: &'a DeviceAuthorizationResponse, ) -> DeviceAccessTokenRequest<'a, 'static, TR, EF> where EF: ExtraDeviceAuthorizationFields, { self.exchange_device_access_token_impl(self.token_uri(), auth_response) } /// Request an access token using the /// [Resource Owner Password Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). /// /// Requires /// [`set_token_uri()`](Self::set_token_uri) to have /// been previously called to set the token endpoint. pub fn exchange_password<'a>( &'a self, username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, ) -> PasswordTokenRequest<'a, TE, TR> { self.exchange_password_impl(self.token_uri(), username, password) } /// Exchange a refresh token for an access token. /// /// See . /// /// Requires /// [`set_token_uri()`](Self::set_token_uri) to have /// been previously called to set the token endpoint. pub fn exchange_refresh_token<'a>( &'a self, refresh_token: &'a RefreshToken, ) -> RefreshTokenRequest<'a, TE, TR> { self.exchange_refresh_token_impl(self.token_uri(), refresh_token) } /// Return the token endpoint. pub fn token_uri(&self) -> &TokenUrl { // This is enforced statically via the HasTokenUrl generic type. self.token_url.as_ref().expect("should have token_url") } } /// Methods with a possibly-set token endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointMaybeSet, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, { /// Request an access token using the /// [Client Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). /// /// Requires [`set_token_uri_option()`](Self::set_token_uri_option) to have been previously /// called to set the token endpoint. pub fn exchange_client_credentials( &self, ) -> Result, ConfigurationError> { Ok(self.exchange_client_credentials_impl( self.token_url .as_ref() .ok_or(ConfigurationError::MissingUrl("token"))?, )) } /// Exchange a code returned during the /// [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) /// for 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. /// /// Requires [`set_token_uri_option()`](Self::set_token_uri_option) to have been previously /// called to set the token endpoint. pub fn exchange_code( &self, code: AuthorizationCode, ) -> Result, ConfigurationError> { Ok(self.exchange_code_impl( self.token_url .as_ref() .ok_or(ConfigurationError::MissingUrl("token"))?, code, )) } /// Exchange an [RFC 8628](https://tools.ietf.org/html/rfc8628#section-3.2) Device Authorization /// Response returned by [`exchange_device_code()`](Self::exchange_device_code) for an access /// token. /// /// Requires [`set_token_uri_option()`](Self::set_token_uri_option) to have been previously /// called to set the token endpoint. pub fn exchange_device_access_token<'a, EF>( &'a self, auth_response: &'a DeviceAuthorizationResponse, ) -> Result, ConfigurationError> where EF: ExtraDeviceAuthorizationFields, { Ok(self.exchange_device_access_token_impl( self.token_url .as_ref() .ok_or(ConfigurationError::MissingUrl("token"))?, auth_response, )) } /// Request an access token using the /// [Resource Owner Password Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). /// /// Requires /// [`set_token_uri_option()`](Self::set_token_uri_option) to have /// been previously called to set the token endpoint. pub fn exchange_password<'a>( &'a self, username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, ) -> Result, ConfigurationError> { Ok(self.exchange_password_impl( self.token_url .as_ref() .ok_or(ConfigurationError::MissingUrl("token"))?, username, password, )) } /// Exchange a refresh token for an access token. /// /// See . /// /// Requires /// [`set_token_uri_option()`](Self::set_token_uri_option) to have /// been previously called to set the token endpoint. pub fn exchange_refresh_token<'a>( &'a self, refresh_token: &'a RefreshToken, ) -> Result, ConfigurationError> { Ok(self.exchange_refresh_token_impl( self.token_url .as_ref() .ok_or(ConfigurationError::MissingUrl("token"))?, refresh_token, )) } /// Return the token endpoint. pub fn token_uri(&self) -> Option<&TokenUrl> { self.token_url.as_ref() } } /// Methods requiring a device authorization endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, EndpointSet, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Begin the [RFC 8628](https://tools.ietf.org/html/rfc8628) Device Authorization Flow and /// retrieve a Device Authorization Response. /// /// Requires /// [`set_device_authorization_url()`](Self::set_device_authorization_url) to have /// been previously called to set the device authorization endpoint. /// /// See [`exchange_device_access_token()`](Self::exchange_device_access_token). pub fn exchange_device_code(&self) -> DeviceAuthorizationRequest { self.exchange_device_code_impl(self.device_authorization_url()) } /// Return the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint /// used for the Device Authorization Flow. /// /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn device_authorization_url(&self) -> &DeviceAuthorizationUrl { // This is enforced statically via the HasDeviceAuthUrl generic type. self.device_authorization_url .as_ref() .expect("should have device_authorization_url") } } /// Methods with a possibly-set device authorization endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, EndpointMaybeSet, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Begin the [RFC 8628](https://tools.ietf.org/html/rfc8628) Device Authorization Flow. /// /// Requires /// [`set_device_authorization_url_option()`](Self::set_device_authorization_url_option) to have /// been previously called to set the device authorization endpoint. /// /// See [`exchange_device_access_token()`](Self::exchange_device_access_token). pub fn exchange_device_code( &self, ) -> Result, ConfigurationError> { Ok(self.exchange_device_code_impl( self.device_authorization_url .as_ref() .ok_or(ConfigurationError::MissingUrl("device authorization"))?, )) } /// Return the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint /// used for the Device Authorization Flow. /// /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn device_authorization_url(&self) -> Option<&DeviceAuthorizationUrl> { self.device_authorization_url.as_ref() } } /// Methods requiring an introspection endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, EndpointSet, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Retrieve metadata for an access token using the /// [`RFC 7662`](https://tools.ietf.org/html/rfc7662) introspection endpoint. /// /// Requires [`set_introspection_url()`](Self::set_introspection_url) to have been previously /// called to set the introspection endpoint. pub fn introspect<'a>(&'a self, token: &'a AccessToken) -> IntrospectionRequest<'a, TE, TIR> { self.introspect_impl(self.introspection_url(), token) } /// Return the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. pub fn introspection_url(&self) -> &IntrospectionUrl { // This is enforced statically via the HasIntrospectionUrl generic type. self.introspection_url .as_ref() .expect("should have introspection_url") } } /// Methods with a possibly-set introspection endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, EndpointMaybeSet, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Retrieve metadata for an access token using the /// [`RFC 7662`](https://tools.ietf.org/html/rfc7662) introspection endpoint. /// /// Requires [`set_introspection_url_option()`](Self::set_introspection_url_option) to have been /// previously called to set the introspection endpoint. pub fn introspect<'a>( &'a self, token: &'a AccessToken, ) -> Result, ConfigurationError> { Ok(self.introspect_impl( self.introspection_url .as_ref() .ok_or(ConfigurationError::MissingUrl("introspection"))?, token, )) } /// Return the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. pub fn introspection_url(&self) -> Option<&IntrospectionUrl> { self.introspection_url.as_ref() } } /// Methods requiring a revocation endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, EndpointSet, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasTokenUrl: EndpointState, { /// Revoke an access or refresh token using the [RFC 7009](https://tools.ietf.org/html/rfc7009) /// revocation endpoint. /// /// Requires [`set_revocation_url()`](Self::set_revocation_url) to have been previously /// called to set the revocation endpoint. pub fn revoke_token( &self, token: RT, ) -> Result, ConfigurationError> { self.revoke_token_impl(self.revocation_url(), token) } /// Return the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// /// See [`revoke_token()`](Self::revoke_token()). pub fn revocation_url(&self) -> &RevocationUrl { // This is enforced statically via the HasRevocationUrl generic type. self.revocation_url .as_ref() .expect("should have revocation_url") } } /// Methods with a possible-set revocation endpoint. impl Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, EndpointMaybeSet, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasTokenUrl: EndpointState, { /// Revoke an access or refresh token using the [RFC 7009](https://tools.ietf.org/html/rfc7009) /// revocation endpoint. /// /// Requires [`set_revocation_url_option()`](Self::set_revocation_url_option) to have been /// previously called to set the revocation endpoint. pub fn revoke_token( &self, token: RT, ) -> Result, ConfigurationError> { self.revoke_token_impl( self.revocation_url .as_ref() .ok_or(ConfigurationError::MissingUrl("revocation"))?, token, ) } /// Return the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// /// See [`revoke_token()`](Self::revoke_token()). pub fn revocation_url(&self) -> Option<&RevocationUrl> { self.revocation_url.as_ref() } } oauth2-5.0.0/src/code.rs000064400000000000000000000301501046102023000130760ustar 00000000000000use crate::{ AuthUrl, Client, ClientId, CsrfToken, EndpointState, ErrorResponse, PkceCodeChallenge, RedirectUrl, ResponseType, RevocableToken, Scope, TokenIntrospectionResponse, TokenResponse, }; use url::Url; use std::borrow::Cow; impl< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { pub(crate) fn authorize_url_impl<'a, S>( &'a self, auth_url: &'a AuthUrl, state_fn: S, ) -> AuthorizationRequest<'a> where S: FnOnce() -> CsrfToken, { AuthorizationRequest { 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(), } } } /// A request to the authorization endpoint #[derive(Debug)] pub struct AuthorizationRequest<'a> { pub(crate) auth_url: &'a AuthUrl, pub(crate) client_id: &'a ClientId, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) pkce_challenge: Option, pub(crate) redirect_url: Option>, pub(crate) response_type: Cow<'a, str>, pub(crate) scopes: Vec>, pub(crate) 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) } } #[cfg(test)] mod tests { use crate::basic::BasicClient; use crate::tests::new_client; use crate::{ AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, ResponseType, Scope, TokenUrl, }; use url::form_urlencoded::byte_serialize; use url::Url; use std::borrow::Cow; #[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())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth?foo=bar".to_string()).unwrap()) .set_token_uri(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 ); } } oauth2-5.0.0/src/curl_client.rs000064400000000000000000000050411046102023000144700ustar 00000000000000use crate::{HttpClientError, HttpRequest, HttpResponse, SyncHttpClient}; use curl::easy::Easy; use http::header::{HeaderValue, CONTENT_TYPE}; use http::method::Method; use http::status::StatusCode; use std::io::Read; /// A synchronous HTTP client using [`curl`]. pub struct CurlHttpClient; impl SyncHttpClient for CurlHttpClient { type Error = HttpClientError; fn call(&self, request: HttpRequest) -> Result { let mut easy = Easy::new(); easy.url(&request.uri().to_string()[..]).map_err(Box::new)?; let mut headers = curl::easy::List::new(); for (name, value) in request.headers() { headers .append(&format!( "{}: {}", name, // TODO: Unnecessary fallibility, curl uses a CString under the hood value.to_str().map_err(|_| HttpClientError::Other(format!( "invalid `{name}` header value {:?}", value.as_bytes() )))? )) .map_err(Box::new)? } easy.http_headers(headers).map_err(Box::new)?; if let Method::POST = *request.method() { easy.post(true).map_err(Box::new)?; easy.post_field_size(request.body().len() as u64) .map_err(Box::new)?; } 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(Box::new)?; transfer .write_function(|new_data| { data.extend_from_slice(new_data); Ok(new_data.len()) }) .map_err(Box::new)?; transfer.perform().map_err(Box::new)?; } let mut builder = http::Response::builder().status( StatusCode::from_u16(easy.response_code().map_err(Box::new)? as u16) .map_err(http::Error::from)?, ); if let Some(content_type) = easy .content_type() .map_err(Box::new)? .map(HeaderValue::from_str) .transpose() .map_err(http::Error::from)? { builder = builder.header(CONTENT_TYPE, content_type); } builder.body(data).map_err(HttpClientError::Http) } } oauth2-5.0.0/src/devicecode.rs000064400000000000000000001262131046102023000142640ustar 00000000000000use crate::basic::BasicErrorResponseType; use crate::endpoint::{endpoint_request, endpoint_response}; use crate::types::VerificationUriComplete; use crate::{ AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, DeviceAuthorizationUrl, DeviceCode, EndUserVerificationUrl, EndpointState, ErrorResponse, ErrorResponseType, HttpRequest, HttpResponse, RequestTokenError, RevocableToken, Scope, StandardErrorResponse, SyncHttpClient, TokenIntrospectionResponse, TokenResponse, TokenUrl, UserCode, }; use chrono::{DateTime, Utc}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; 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; impl< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { pub(crate) fn exchange_device_code_impl<'a>( &'a self, device_authorization_url: &'a DeviceAuthorizationUrl, ) -> DeviceAuthorizationRequest<'a, TE> { 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, _phantom: PhantomData, } } pub(crate) fn exchange_device_access_token_impl<'a, EF>( &'a self, token_url: &'a TokenUrl, auth_response: &'a DeviceAuthorizationResponse, ) -> DeviceAccessTokenRequest<'a, 'static, TR, EF> where 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, dev_auth_resp: auth_response, time_fn: Arc::new(Utc::now), max_backoff_interval: None, _phantom: PhantomData, } } } /// The request for a set of verification codes from the authorization server. /// /// See . #[derive(Debug)] pub struct DeviceAuthorizationRequest<'a, TE> where TE: ErrorResponse, { pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) scopes: Vec>, pub(crate) device_authorization_url: &'a DeviceAuthorizationUrl, pub(crate) _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, { endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.device_authorization_url.url(), vec![], ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } /// Synchronously sends the request to the authorization server and awaits a response. pub fn request( self, http_client: &C, ) -> Result, RequestTokenError<::Error, TE>> where C: SyncHttpClient, EF: ExtraDeviceAuthorizationFields, { endpoint_response(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and returns a Future. pub fn request_async<'c, C, EF>( self, http_client: &'c C, ) -> impl Future< Output = Result< DeviceAuthorizationResponse, RequestTokenError<>::Error, TE>, >, > + 'c where Self: 'c, C: AsyncHttpClient<'c>, EF: ExtraDeviceAuthorizationFields, { Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) }) } } /// The request for a device access token from the authorization server. /// /// See . #[derive(Clone)] pub struct DeviceAccessTokenRequest<'a, 'b, TR, EF> where TR: TokenResponse, EF: ExtraDeviceAuthorizationFields, { pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) token_url: &'a TokenUrl, pub(crate) dev_auth_resp: &'a DeviceAuthorizationResponse, pub(crate) time_fn: Arc DateTime + Send + Sync + 'b>, pub(crate) max_backoff_interval: Option, pub(crate) _phantom: PhantomData<(TR, EF)>, } impl<'a, 'b, TR, EF> DeviceAccessTokenRequest<'a, 'b, TR, EF> where TR: TokenResponse, 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<'t, T>(self, time_fn: T) -> DeviceAccessTokenRequest<'a, 't, TR, EF> where T: Fn() -> DateTime + Send + Sync + 't, { DeviceAccessTokenRequest { auth_type: self.auth_type, client_id: self.client_id, client_secret: self.client_secret, extra_params: self.extra_params, token_url: self.token_url, dev_auth_resp: self.dev_auth_resp, time_fn: Arc::new(time_fn), max_backoff_interval: self.max_backoff_interval, _phantom: PhantomData, } } /// 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: &C, sleep_fn: S, timeout: Option, ) -> Result::Error, DeviceCodeErrorResponse>> where C: SyncHttpClient, S: Fn(Duration), { // 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.call(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 fn request_async<'c, C, S, SF>( self, http_client: &'c C, sleep_fn: S, timeout: Option, ) -> impl Future< Output = Result< TR, RequestTokenError<>::Error, DeviceCodeErrorResponse>, >, > + 'c where Self: 'c, C: AsyncHttpClient<'c>, S: Fn(Duration) -> SF + 'c, SF: Future, { Box::pin(async move { // 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.call(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, TE: ErrorResponse + 'static, { endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, self.token_url.url(), vec![ ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), ("device_code", self.dev_auth_resp.device_code().secret()), ], ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } 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, ))), } } // On any other success or failure, return the failure. res => DeviceAccessTokenPollResult::Done(res), } } 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(|e| { RequestTokenError::Other(format!( "failed to convert `{timeout_dur:?}` to `chrono::Duration`: {e}" )) })?; // 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) } } /// 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 } fn deserialize_devicecode_interval<'de, D>(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { struct NumOrNull; impl<'de> serde::de::Visitor<'de> for NumOrNull { type Value = u64; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { formatter.write_str("non-negative integer or null") } fn visit_u64(self, v: u64) -> Result where E: Error, { Ok(v) } fn visit_unit(self) -> Result where E: serde::de::Error, { Ok(default_devicecode_interval()) } } deserializer.deserialize_any(NumOrNull) } /// 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 authorization 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", deserialize_with = "deserialize_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, Eq)] 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, RE: Error + 'static, { ContinueWithNewPollInterval(Duration), Done(Result>), } #[cfg(test)] mod tests { use crate::basic::BasicTokenType; use crate::devicecode::default_devicecode_interval; use crate::tests::{mock_http_client, mock_http_client_success_fail, new_client}; use crate::{ DeviceAuthorizationResponse, DeviceAuthorizationUrl, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, RequestTokenError, Scope, StandardDeviceAuthorizationResponse, TokenResponse, }; use chrono::{DateTime, Utc}; use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::{HeaderValue, Response, StatusCode}; use std::time::Duration; 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\": {expires_in}, \ \"interval\": 1 \ }}" ); 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() .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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body(body.into_bytes()) .unwrap(), )) .unwrap() } #[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", Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"error\": \"authorization_pending\", \ \"error_description\": \"Still waiting for user\"\ }" .to_string() .into_bytes()) .unwrap(), 5, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"openid\"\ }" .to_string() .into_bytes()) .unwrap(), ), 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", Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"error\": \"slow_down\", \ \"error_description\": \"Woah there partner\"\ }" .to_string() .into_bytes()) .unwrap(), 5, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"openid\"\ }" .to_string() .into_bytes()) .unwrap(), ), 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()); } 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(); DateTime::from_timestamp(next_value, 0).unwrap() } } /// 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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"openid\"\ }" .to_string() .into_bytes()) .unwrap(), ), 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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"error\": \"authorization_pending\", \ \"error_description\": \"Still waiting for user\"\ }" .to_string() .into_bytes()) .unwrap(), ), 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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"error\": \"access_denied\", \ \"error_description\": \"Access Denied\"\ }" .to_string() .into_bytes()) .unwrap(), ), 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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("{\ \"error\": \"expired_token\", \ \"error_description\": \"Token has expired\"\ }" .to_string() .into_bytes()) .unwrap(), ), mock_sleep_fn, None) .err() .unwrap(); match token { RequestTokenError::ServerResponse(msg) => { assert_eq!(msg.error(), &DeviceCodeErrorResponseType::ExpiredToken) } _ => unreachable!("Error should be ExpiredToken"), } } #[test] fn test_device_auth_response_default_interval() { let response: DeviceAuthorizationResponse = serde_json::from_str( r#"{ "device_code": "12345", "verification_uri": "https://verify/here", "user_code": "abcde", "expires_in": 300 }"#, ) .unwrap(); assert_eq!(response.interval, default_devicecode_interval()); } #[test] fn test_device_auth_response_non_default_interval() { let response: DeviceAuthorizationResponse = serde_json::from_str( r#"{ "device_code": "12345", "verification_uri": "https://verify/here", "user_code": "abcde", "expires_in": 300, "interval": 10 }"#, ) .unwrap(); assert_eq!(response.interval, 10); } #[test] fn test_device_auth_response_null_interval() { let response: DeviceAuthorizationResponse = serde_json::from_str( r#"{ "device_code": "12345", "verification_uri": "https://verify/here", "user_code": "abcde", "expires_in": 300, "interval": null }"#, ) .unwrap(); assert_eq!(response.interval, default_devicecode_interval()); } } oauth2-5.0.0/src/endpoint.rs000064400000000000000000000207651046102023000140170ustar 00000000000000use crate::{ AuthType, ClientId, ClientSecret, ErrorResponse, RedirectUrl, RequestTokenError, Scope, CONTENT_TYPE_FORMENCODED, CONTENT_TYPE_JSON, }; use base64::prelude::*; use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::{HeaderValue, StatusCode}; use serde::de::DeserializeOwned; use url::{form_urlencoded, Url}; use std::borrow::Cow; use std::error::Error; use std::future::Future; /// An HTTP request. pub type HttpRequest = http::Request>; /// An HTTP response. pub type HttpResponse = http::Response>; /// An asynchronous (future-based) HTTP client. pub trait AsyncHttpClient<'c> { /// Error type returned by HTTP client. type Error: Error + 'static; /// Future type returned by HTTP client. type Future: Future> + 'c; /// Perform a single HTTP request. fn call(&'c self, request: HttpRequest) -> Self::Future; } impl<'c, E, F, T> AsyncHttpClient<'c> for T where E: Error + 'static, F: Future> + 'c, // We can't implement this for FnOnce because the device authorization flow requires clients to // supportmultiple calls. T: Fn(HttpRequest) -> F, { type Error = E; type Future = F; fn call(&'c self, request: HttpRequest) -> Self::Future { self(request) } } /// A synchronous (blocking) HTTP client. pub trait SyncHttpClient { /// Error type returned by HTTP client. type Error: Error + 'static; /// Perform a single HTTP request. fn call(&self, request: HttpRequest) -> Result; } impl SyncHttpClient for T where E: Error + 'static, // We can't implement this for FnOnce because the device authorization flow requires clients to // support multiple calls. T: Fn(HttpRequest) -> Result, { type Error = E; fn call(&self, request: HttpRequest) -> Result { self(request) } } #[allow(clippy::too_many_arguments)] pub(crate) 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)>, ) -> Result { let mut builder = http::Request::builder() .uri(url.to_string()) .method(http::Method::POST) .header(ACCEPT, HeaderValue::from_static(CONTENT_TYPE_JSON)) .header( 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_STANDARD.encode(format!("{}:{}", &urlencoded_id, urlencoded_secret)); builder = builder.header( AUTHORIZATION, HeaderValue::from_str(&format!("Basic {}", &b64_credential)).unwrap(), ); } (AuthType::RequestBody, _) | (AuthType::BasicAuth, None) => { params.push(("client_id", client_id)); if let Some(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(|(k, v)| (k.as_ref(), v.as_ref())) .collect::>() .as_slice(), ); let body = form_urlencoded::Serializer::new(String::new()) .extend_pairs(params) .finish() .into_bytes(); builder.body(body) } pub(crate) fn endpoint_response( http_response: HttpResponse, ) -> Result> where RE: Error, 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())) } pub(crate) 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() != StatusCode::OK { let reason = http_response.body().as_slice(); if reason.is_empty() { 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()), }; Err(error) } } else { 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: {content_type:?}, should be `{CONTENT_TYPE_JSON}`", ) ) ) } else { Ok(()) } )?; if http_response.body().is_empty() { return Err(RequestTokenError::Other( "server returned empty response body".to_string(), )); } Ok(()) } #[cfg(test)] mod tests { use crate::tests::{new_client, FakeError}; use crate::{AuthorizationCode, TokenResponse}; use http::{Response, StatusCode}; #[tokio::test] async fn test_async_client_closure() { let client = new_client(); let http_response = Response::builder() .status(StatusCode::OK) .body( "{\"access_token\": \"12/34\", \"token_type\": \"BEARER\"}" .to_string() .into_bytes(), ) .unwrap(); let token = client .exchange_code(AuthorizationCode::new("ccc".to_string())) // NB: This tests that the closure doesn't require a static lifetime. .request_async(&|_| async { Ok(http_response.clone()) as Result<_, FakeError> }) .await .unwrap(); assert_eq!("12/34", token.access_token().secret()); } } oauth2-5.0.0/src/error.rs000064400000000000000000000147211046102023000133230ustar 00000000000000use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; /// Server Error Response /// /// See [Section 5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) of RFC 6749. /// This trait exists separately from the `StandardErrorResponse` struct /// to support customization by clients, such as supporting interoperability with /// non-standards-complaint OAuth2 providers. /// /// The [`Display`] trait implementation for types implementing [`ErrorResponse`] should be a /// human-readable string suitable for printing (e.g., within a [`RequestTokenError`]). pub trait ErrorResponse: Debug + Display + 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, Eq, Serialize)] pub struct StandardErrorResponse { #[serde(bound = "T: ErrorResponseType")] pub(crate) error: T, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) error_description: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) 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 + Display + 'static {} impl Display for StandardErrorResponse where TE: ErrorResponseType + Display, { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { 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); formatted.push(')'); } 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: {0}")] ServerResponse(T), /// An error occurred while sending the request or receiving the response (e.g., network /// connectivity failed). #[error("Request failed")] Request(#[from] 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), } #[cfg(test)] mod tests { use crate::basic::{BasicErrorResponse, BasicErrorResponseType}; #[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(), ); } } oauth2-5.0.0/src/helpers.rs000064400000000000000000000272671046102023000136450ustar 00000000000000use serde::de::value::SeqAccessDeserializer; use serde::ser::{Impossible, SerializeStructVariant, SerializeTupleVariant}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use std::error::Error; use std::fmt::{Display, Formatter}; 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>, { T::deserialize(Value::String( String::deserialize(deserializer)?.to_lowercase(), )) .map_err(serde::de::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>, { 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(serde::de::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> serde::de::Visitor<'de> for StringOrVec { type Value = Option>; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { formatter.write_str("string or list of strings") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { Ok(Some(vec![value.to_owned()])) } fn visit_none(self) -> Result where E: serde::de::Error, { Ok(None) } fn visit_unit(self) -> Result where E: serde::de::Error, { Ok(None) } fn visit_seq(self, visitor: S) -> Result where S: serde::de::SeqAccess<'de>, { Deserialize::deserialize(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 Error for NotEnum { fn description(&self) -> &str { "not struct" } } impl Display for NotEnum { fn fmt(&self, _f: &mut Formatter) -> std::fmt::Result { unimplemented!() } } impl serde::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() } #[cfg(test)] mod tests { use serde::Deserialize; #[derive(Deserialize, Debug, Clone)] pub struct ObjectWithOptionalStringOrVecString { #[serde(deserialize_with = "crate::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 ); } } oauth2-5.0.0/src/introspection.rs000064400000000000000000000521131046102023000150670ustar 00000000000000use crate::endpoint::{endpoint_request, endpoint_response}; use crate::{ AccessToken, AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, EndpointState, ErrorResponse, ExtraTokenFields, HttpRequest, IntrospectionUrl, RequestTokenError, RevocableToken, Scope, SyncHttpClient, TokenResponse, TokenType, }; use chrono::serde::ts_seconds_option; use chrono::{DateTime, Utc}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::error::Error; use std::fmt::Debug; use std::future::Future; use std::marker::PhantomData; impl< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { pub(crate) fn introspect_impl<'a>( &'a self, introspection_url: &'a IntrospectionUrl, token: &'a AccessToken, ) -> IntrospectionRequest<'a, TE, TIR> { IntrospectionRequest { auth_type: &self.auth_type, client_id: &self.client_id, client_secret: self.client_secret.as_ref(), extra_params: Vec::new(), introspection_url, token, token_type_hint: None, _phantom: PhantomData, } } } /// A request to introspect an access token. /// /// See . #[derive(Debug)] pub struct IntrospectionRequest<'a, TE, TIR> where TE: ErrorResponse, TIR: TokenIntrospectionResponse, { pub(crate) token: &'a AccessToken, pub(crate) token_type_hint: Option>, pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) introspection_url: &'a IntrospectionUrl, pub(crate) _phantom: PhantomData<(TE, TIR)>, } impl<'a, TE, TIR> IntrospectionRequest<'a, TE, TIR> where TE: ErrorResponse + 'static, TIR: TokenIntrospectionResponse, { /// 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)); } endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, self.introspection_url.url(), params, ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } /// Synchronously sends the request to the authorization server and awaits a response. pub fn request( self, http_client: &C, ) -> Result::Error, TE>> where C: SyncHttpClient, { endpoint_response(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and returns a Future. pub fn request_async<'c, C>( self, http_client: &'c C, ) -> impl Future>::Error, TE>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) }) } } /// 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 { /// Type of OAuth2 access token included in this response. type TokenType: 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<&Self::TokenType>; /// 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 = "crate::helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "crate::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 = "crate::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 = "crate::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, { type TokenType = TT; 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() } } #[cfg(test)] mod tests { use crate::basic::BasicTokenType; use crate::tests::{mock_http_client, new_client}; use crate::{AccessToken, AuthType, ClientId, IntrospectionUrl, RedirectUrl, Scope}; use chrono::DateTime; use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::{HeaderValue, Response, StatusCode}; #[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_url( IntrospectionUrl::new("https://introspection/url".to_string()).unwrap(), ); let introspection_response = client .introspect(&AccessToken::new("access_token_123".to_string())) .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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"active\": true\ }" .to_string() .into_bytes(), ) .unwrap(), )) .unwrap(); assert!(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_url( IntrospectionUrl::new("https://introspection/url".to_string()).unwrap(), ); let introspection_response = client .introspect(&AccessToken::new("access_token_123".to_string())) .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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .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(), )) .unwrap(); assert!(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(DateTime::from_timestamp(1604073517, 0).unwrap()), introspection_response.exp ); assert_eq!( Some(DateTime::from_timestamp(1604073217, 0).unwrap()), introspection_response.iat ); assert_eq!( Some(DateTime::from_timestamp(1604073317, 0).unwrap()), 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 ); } } oauth2-5.0.0/src/lib.rs000064400000000000000000000543141046102023000127420ustar 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 Authorization Flow](#device-authorization-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)** //! //! NOTE: Be careful not to use a blocking HTTP client within `async` Rust code, which may panic //! or cause other issues. The //! [`tokio::task::spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html) //! function may be useful in this situation. //! * **Asynchronous** //! //! ## Security Warning //! //! To prevent //! [SSRF](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) //! vulnerabilities, be sure to configure the HTTP client **not to follow redirects**. For example, //! use [`redirect::Policy::none`](reqwest::redirect::Policy::none) when using //! [`reqwest`], or [`redirects(0)`](ureq::AgentBuilder::redirects) when using [`ureq`]. //! //! ## HTTP Clients //! //! For the HTTP client modes described above, the following HTTP client implementations can be //! used: //! * **[`reqwest`](reqwest)** //! //! The `reqwest` HTTP client supports both the synchronous and asynchronous modes and is enabled //! by default. //! //! Synchronous client: [`reqwest::blocking::Client`] (requires the //! `reqwest-blocking` feature flag) //! //! Asynchronous client: [`reqwest::Client`] (requires either the //! `reqwest` or `reqwest-blocking` feature flags) //! //! * **[`curl`](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: [`oauth2::CurlHttpClient`](CurlHttpClient) //! //! * **[`ureq`](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. //! //! Synchronous client: [`ureq::Agent`] //! //! * **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 [`SyncHttpClient`] trait, which is //! automatically implemented for any function/closure that implements: //! ```rust,ignore //! Fn(HttpRequest) -> Result //! where //! E: std::error::Error + 'static //! ``` //! //! Asynchronous HTTP clients should implement the [`AsyncHttpClient`] trait, which is //! automatically implemented for any function/closure that implements: //! ```rust,ignore //! Fn(HttpRequest) -> F //! where //! E: std::error::Error + 'static, //! F: Future>, //! ``` //! //! # Comparing secrets securely //! //! OAuth flows require comparing secrets received from the provider servers. To do so securely //! while avoiding [timing side-channels](https://en.wikipedia.org/wiki/Timing_attack), the //! comparison must be done in constant time, either using a constant-time crate such as //! [`constant_time_eq`](https://crates.io/crates/constant_time_eq) (which could break if a future //! compiler version decides to be overly smart //! about its optimizations), or by first computing a cryptographically-secure hash (e.g., SHA-256) //! of both values and then comparing the hashes using `==`. //! //! The `timing-resistant-secret-traits` feature flag adds a safe (but comparatively expensive) //! [`PartialEq`] implementation to the secret types. Timing side-channels are why [`PartialEq`] is //! not auto-derived for this crate's secret types, and the lack of [`PartialEq`] is intended to //! prompt users to think more carefully about these comparisons. //! //! # 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 oauth2::{ //! AuthorizationCode, //! AuthUrl, //! ClientId, //! ClientSecret, //! CsrfToken, //! PkceCodeChallenge, //! RedirectUrl, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] //! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] //! # 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())) //! .set_client_secret(ClientSecret::new("client_secret".to_string())) //! .set_auth_uri(AuthUrl::new("http://authorize".to_string())?) //! .set_token_uri(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_token`. //! //! let http_client = reqwest::blocking::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! // 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 oauth2::{ //! AuthorizationCode, //! AuthUrl, //! ClientId, //! ClientSecret, //! CsrfToken, //! PkceCodeChallenge, //! RedirectUrl, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest")] //! use oauth2::reqwest; //! 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())) //! .set_client_secret(ClientSecret::new("client_secret".to_string())) //! .set_auth_uri(AuthUrl::new("http://authorize".to_string())?) //! .set_token_uri(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_token`. //! //! let http_client = reqwest::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! // 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(&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 oauth2::{ //! AuthUrl, //! ClientId, //! 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())) //! .set_auth_uri(AuthUrl::new("http://authorize".to_string())?); //! //! // 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_token`. //! //! # 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 oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! ResourceOwnerPassword, //! ResourceOwnerUsername, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] //! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let client = BasicClient::new(ClientId::new("client_id".to_string())) //! .set_client_secret(ClientSecret::new("client_secret".to_string())) //! .set_auth_uri(AuthUrl::new("http://authorize".to_string())?) //! .set_token_uri(TokenUrl::new("http://token".to_string())?); //! //! let http_client = reqwest::blocking::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! 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 oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! Scope, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] //! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let client = BasicClient::new(ClientId::new("client_id".to_string())) //! .set_client_secret(ClientSecret::new("client_secret".to_string())) //! .set_auth_uri(AuthUrl::new("http://authorize".to_string())?) //! .set_token_uri(TokenUrl::new("http://token".to_string())?); //! //! let http_client = reqwest::blocking::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! let token_result = client //! .exchange_client_credentials() //! .add_scope(Scope::new("read".to_string())) //! .request(&http_client)?; //! # Ok(()) //! # } //! ``` //! //! # Device Authorization Flow //! //! Device Authorization 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 oauth2::{ //! AuthUrl, //! ClientId, //! ClientSecret, //! DeviceAuthorizationUrl, //! Scope, //! StandardDeviceAuthorizationResponse, //! TokenResponse, //! TokenUrl //! }; //! use oauth2::basic::BasicClient; //! # #[cfg(feature = "reqwest-blocking")] //! use oauth2::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] //! # 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())) //! .set_client_secret(ClientSecret::new("client_secret".to_string())) //! .set_auth_uri(AuthUrl::new("http://authorize".to_string())?) //! .set_token_uri(TokenUrl::new("http://token".to_string())?) //! .set_device_authorization_url(device_auth_url); //! //! let http_client = reqwest::blocking::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! 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 Authorization 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) //! /// Basic OAuth2 implementation with no extensions /// ([RFC 6749](https://tools.ietf.org/html/rfc6749)). pub mod basic; mod client; mod code; /// HTTP client backed by the [curl](https://crates.io/crates/curl) crate. /// Requires "curl" feature. #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] mod curl_client; #[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 Authorization Flow OAuth2 implementation /// ([RFC 8628](https://tools.ietf.org/html/rfc8628)). mod devicecode; mod endpoint; mod error; /// Helper methods used by OAuth2 implementations/extensions. pub mod helpers; mod introspection; /// HTTP client backed by the [reqwest](https://crates.io/crates/reqwest) crate. /// Requires "reqwest" feature. #[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] mod reqwest_client; /// OAuth 2.0 Token Revocation implementation /// ([RFC 7009](https://tools.ietf.org/html/rfc7009)). mod revocation; #[cfg(test)] mod tests; mod token; mod types; /// HTTP client backed by the [ureq](https://crates.io/crates/ureq) crate. /// Requires "ureq" feature. #[cfg(feature = "ureq")] mod ureq_client; pub use crate::client::{Client, EndpointMaybeSet, EndpointNotSet, EndpointSet, EndpointState}; pub use crate::code::AuthorizationRequest; #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] pub use crate::curl_client::CurlHttpClient; pub use crate::devicecode::{ DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationResponse, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, ExtraDeviceAuthorizationFields, StandardDeviceAuthorizationResponse, }; pub use crate::endpoint::{AsyncHttpClient, HttpRequest, HttpResponse, SyncHttpClient}; pub use crate::error::{ ErrorResponse, ErrorResponseType, RequestTokenError, StandardErrorResponse, }; pub use crate::introspection::{ IntrospectionRequest, StandardTokenIntrospectionResponse, TokenIntrospectionResponse, }; pub use crate::revocation::{ RevocableToken, RevocationErrorResponseType, RevocationRequest, StandardRevocableToken, }; pub use crate::token::{ ClientCredentialsTokenRequest, CodeTokenRequest, EmptyExtraTokenFields, ExtraTokenFields, PasswordTokenRequest, RefreshTokenRequest, StandardTokenResponse, TokenResponse, TokenType, }; pub use crate::types::{ AccessToken, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, DeviceAuthorizationUrl, DeviceCode, EndUserVerificationUrl, IntrospectionUrl, PkceCodeChallenge, PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, ResourceOwnerPassword, ResourceOwnerUsername, ResponseType, RevocationUrl, Scope, TokenUrl, UserCode, VerificationUriComplete, }; use std::error::Error; /// Public re-exports of types used for HTTP client interfaces. pub use http; pub use url; #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] pub use ::curl; #[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] pub use ::reqwest; #[cfg(feature = "ureq")] pub use ::ureq; 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 is not set. #[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, } /// Error type returned by built-in HTTP clients when requests fail. #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum HttpClientError where RE: Error + 'static, { /// Error returned by reqwest crate. #[error("client error")] Reqwest(#[from] Box), /// Non-reqwest HTTP error. #[error("HTTP error")] Http(#[from] http::Error), /// I/O error. #[error("I/O error")] Io(#[from] std::io::Error), /// Other error. #[error("{}", _0)] Other(String), } oauth2-5.0.0/src/reqwest_client.rs000064400000000000000000000040661046102023000152230ustar 00000000000000use crate::{AsyncHttpClient, HttpClientError, HttpRequest, HttpResponse}; use std::future::Future; use std::pin::Pin; impl<'c> AsyncHttpClient<'c> for reqwest::Client { type Error = HttpClientError; #[cfg(target_arch = "wasm32")] type Future = Pin> + 'c>>; #[cfg(not(target_arch = "wasm32"))] type Future = Pin> + Send + Sync + 'c>>; fn call(&'c self, request: HttpRequest) -> Self::Future { Box::pin(async move { let response = self .execute(request.try_into().map_err(Box::new)?) .await .map_err(Box::new)?; let mut builder = http::Response::builder().status(response.status()); #[cfg(not(target_arch = "wasm32"))] { builder = builder.version(response.version()); } for (name, value) in response.headers().iter() { builder = builder.header(name, value); } builder .body(response.bytes().await.map_err(Box::new)?.to_vec()) .map_err(HttpClientError::Http) }) } } #[cfg(all(feature = "reqwest-blocking", not(target_arch = "wasm32")))] impl crate::SyncHttpClient for reqwest::blocking::Client { type Error = HttpClientError; fn call(&self, request: HttpRequest) -> Result { let mut response = self .execute(request.try_into().map_err(Box::new)?) .map_err(Box::new)?; let mut builder = http::Response::builder() .status(response.status()) .version(response.version()); for (name, value) in response.headers().iter() { builder = builder.header(name, value); } let mut body = Vec::new(); ::read_to_end(&mut response, &mut body)?; builder.body(body).map_err(HttpClientError::Http) } } oauth2-5.0.0/src/revocation.rs000064400000000000000000000530711046102023000143440ustar 00000000000000use crate::basic::BasicErrorResponseType; use crate::endpoint::{endpoint_request, endpoint_response_status_only}; use crate::{ AccessToken, AsyncHttpClient, AuthType, Client, ClientId, ClientSecret, ConfigurationError, EndpointState, ErrorResponse, ErrorResponseType, HttpRequest, RefreshToken, RequestTokenError, RevocationUrl, SyncHttpClient, TokenIntrospectionResponse, TokenResponse, }; use serde::{Deserialize, Serialize}; 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; impl< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { pub(crate) fn revoke_token_impl<'a>( &'a self, revocation_url: &'a RevocationUrl, 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." if revocation_url.url().scheme() != "https" { return Err(ConfigurationError::InsecureUrl("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, }) } } /// 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). /// /// ```rust /// # use http::{Response, StatusCode}; /// # use oauth2::{ /// # AccessToken, AuthUrl, ClientId, EmptyExtraTokenFields, HttpResponse, RequestTokenError, /// # RevocationUrl, StandardRevocableToken, StandardTokenResponse, TokenResponse, TokenUrl, /// # }; /// # use oauth2::basic::{BasicClient, BasicRequestTokenError, BasicTokenResponse, BasicTokenType}; /// # /// # fn err_wrapper() -> Result<(), anyhow::Error> { /// # /// # let token_response = BasicTokenResponse::new( /// # AccessToken::new("access".to_string()), /// # BasicTokenType::Bearer, /// # EmptyExtraTokenFields {}, /// # ); /// # /// # #[derive(Debug, thiserror::Error)] /// # enum FakeError {} /// # /// # let http_client = |_| -> Result> { /// # Ok(Response::builder() /// # .status(StatusCode::OK) /// # .body(Vec::new()) /// # .unwrap()) /// # }; /// # /// let client = BasicClient::new(ClientId::new("aaa".to_string())) /// .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) /// .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap()) /// // Be sure to set a revocation URL. /// .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).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)? /// .request(&http_client) /// # .unwrap(); /// # Ok(()) /// # } /// ``` /// /// [`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()) } } /// 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, { pub(crate) token: RT, pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) revocation_url: &'a RevocationUrl, pub(crate) _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)); } endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, None, self.revocation_url.url(), params, ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } /// 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`](RevocationErrorResponseType::UnsupportedTokenType) will be returned if the /// type of token type given is not supported by the server. pub fn request( self, http_client: &C, ) -> Result<(), RequestTokenError<::Error, TE>> where C: SyncHttpClient, { // 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." endpoint_response_status_only(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and returns a Future. pub fn request_async<'c, C>( self, http_client: &'c C, ) -> impl Future>::Error, TE>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { endpoint_response_status_only(http_client.call(self.prepare_request()?).await?) }) } } /// 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, Eq)] 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()) } } #[cfg(test)] mod tests { use crate::basic::BasicRevocationErrorResponse; use crate::tests::colorful_extension::{ColorfulClient, ColorfulRevocableToken}; use crate::tests::{mock_http_client, new_client}; use crate::{ AccessToken, AuthUrl, ClientId, ClientSecret, RefreshToken, RequestTokenError, RevocationErrorResponseType, RevocationUrl, TokenUrl, }; use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::{HeaderValue, Response, StatusCode}; #[test] fn test_token_revocation_with_missing_url() { let client = new_client().set_revocation_url_option(None); let result = client .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap_err(); assert_eq!(result.to_string(), "No revocation endpoint URL specified"); } #[test] fn test_token_revocation_with_non_https_url() { let client = new_client(); let result = client .set_revocation_url(RevocationUrl::new("http://revocation/url".to_string()).unwrap()) .revoke_token(AccessToken::new("access_token_123".to_string()).into()) .unwrap_err(); assert_eq!( result.to_string(), "Scheme for revocation endpoint URL must be HTTPS" ); } #[test] fn test_token_revocation_with_unsupported_token_type() { let client = new_client() .set_revocation_url(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()), Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"error\": \"unsupported_token_type\", \"error_description\": \"stuff happened\", \ \"error_uri\": \"https://errors\"\ }" .to_string() .into_bytes(), ) .unwrap(), )); 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_url(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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body(b"{}".to_vec()) .unwrap(), )) .unwrap(); } #[test] fn test_token_revocation_with_access_token_and_empty_response() { let client = new_client() .set_revocation_url(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()), Response::builder() .status(StatusCode::OK) .body(vec![]) .unwrap(), )) .unwrap(); } #[test] fn test_token_revocation_with_access_token_and_non_json_response() { let client = new_client() .set_revocation_url(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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/octet-stream").unwrap(), ) .body(vec![1, 2, 3]) .unwrap(), )) .unwrap(); } #[test] fn test_token_revocation_with_refresh_token() { let client = new_client() .set_revocation_url(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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body(b"{}".to_vec()) .unwrap(), )) .unwrap(); } #[test] fn test_extension_token_revocation_successful() { let client = ColorfulClient::new(ClientId::new("aaa".to_string())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap()) .set_revocation_url(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()), Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body(b"{}".to_vec()) .unwrap(), )) .unwrap(); } } oauth2-5.0.0/src/tests.rs000064400000000000000000000254751046102023000133440ustar 00000000000000use crate::basic::{ BasicClient, BasicErrorResponseType, BasicRevocationErrorResponse, BasicTokenType, }; use crate::{ AccessToken, AuthType, AuthUrl, AuthorizationCode, AuthorizationRequest, Client, ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationUrl, DeviceCode, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, EmptyExtraTokenFields, EndUserVerificationUrl, EndpointNotSet, EndpointSet, HttpClientError, HttpRequest, HttpResponse, PasswordTokenRequest, PkceCodeChallenge, PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, RefreshTokenRequest, RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, ResponseType, Scope, StandardDeviceAuthorizationResponse, StandardErrorResponse, StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenUrl, UserCode, }; use http::header::HeaderName; use http::HeaderValue; use thiserror::Error; use url::Url; pub(crate) fn new_client( ) -> BasicClient { BasicClient::new(ClientId::new("aaa".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(TokenUrl::new("https://example.com/token".to_string()).unwrap()) .set_client_secret(ClientSecret::new("bbb".to_string())) } pub(crate) 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!( &Url::parse(&request.uri().to_string()).unwrap(), 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().to_owned()).unwrap(), request_body ); Ok(response.clone()) } } #[derive(Debug, Error)] pub(crate) enum FakeError { #[error("error")] Err, } pub(crate) mod colorful_extension { extern crate serde_json; use crate::{ Client, ErrorResponseType, ExtraTokenFields, RevocableToken, StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType, }; use serde::{Deserialize, Serialize}; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Display, Formatter}; pub type ColorfulClient< HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > = Client< StandardErrorResponse, StandardTokenResponse, StandardTokenIntrospectionResponse, ColorfulRevocableToken, StandardErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, >; #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "lowercase")] pub enum ColorfulTokenType { Green, Red, } impl TokenType for ColorfulTokenType {} #[derive(Clone, Debug, Deserialize, PartialEq, Eq, 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, Eq, 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"), } } } } pub(crate) 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::from_fn(|| Some(failure_response.clone())) .take(num_failures) .chain(std::iter::once(success_response)) .collect(); let sync_responses = std::sync::Mutex::new(responses); move |request: HttpRequest| { assert_eq!( &Url::parse(&request.uri().to_string()).unwrap(), 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().to_owned()).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_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, StandardTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, >, >(); is_sync_and_send::< ClientCredentialsTokenRequest< StandardErrorResponse, StandardTokenResponse, >, >(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< CodeTokenRequest< StandardErrorResponse, StandardTokenResponse, >, >(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::(); is_sync_and_send::< PasswordTokenRequest< StandardErrorResponse, StandardTokenResponse, >, >(); 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, >, >(); 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, EmptyExtraDeviceAuthorizationFields, >, >(); is_sync_and_send::>>(); is_sync_and_send::(); is_sync_and_send::(); #[cfg(feature = "curl")] is_sync_and_send::>(); #[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] is_sync_and_send::>(); #[cfg(feature = "ureq")] is_sync_and_send::>(); } oauth2-5.0.0/src/token/mod.rs000064400000000000000000000601161046102023000140700ustar 00000000000000use crate::endpoint::{endpoint_request, endpoint_response}; use crate::{ AccessToken, AsyncHttpClient, AuthType, AuthorizationCode, Client, ClientId, ClientSecret, EndpointState, ErrorResponse, HttpRequest, PkceCodeVerifier, RedirectUrl, RefreshToken, RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, RevocableToken, Scope, SyncHttpClient, TokenIntrospectionResponse, TokenUrl, }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::error::Error; use std::fmt::Debug; use std::future::Future; use std::marker::PhantomData; use std::time::Duration; #[cfg(test)] mod tests; impl< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > where TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { pub(crate) fn exchange_client_credentials_impl<'a>( &'a self, token_url: &'a TokenUrl, ) -> ClientCredentialsTokenRequest<'a, TE, TR> { 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, _phantom: PhantomData, } } pub(crate) fn exchange_code_impl<'a>( &'a self, token_url: &'a TokenUrl, code: AuthorizationCode, ) -> CodeTokenRequest<'a, TE, TR> { 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, redirect_url: self.redirect_url.as_ref().map(Cow::Borrowed), _phantom: PhantomData, } } pub(crate) fn exchange_password_impl<'a>( &'a self, token_url: &'a TokenUrl, username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, ) -> PasswordTokenRequest<'a, TE, TR> { PasswordTokenRequest { 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, _phantom: PhantomData, } } pub(crate) fn exchange_refresh_token_impl<'a>( &'a self, token_url: &'a TokenUrl, refresh_token: &'a RefreshToken, ) -> RefreshTokenRequest<'a, TE, TR> { 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, _phantom: PhantomData, } } } /// A request to exchange an authorization code for an access token. /// /// See . #[derive(Debug)] pub struct CodeTokenRequest<'a, TE, TR> where TE: ErrorResponse, TR: TokenResponse, { pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) code: AuthorizationCode, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) pkce_verifier: Option, pub(crate) token_url: &'a TokenUrl, pub(crate) redirect_url: Option>, pub(crate) _phantom: PhantomData<(TE, TR)>, } impl<'a, TE, TR> CodeTokenRequest<'a, TE, TR> where TE: ErrorResponse + 'static, TR: TokenResponse, { /// 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 [`crate::AuthorizationRequest::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())); } endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, self.redirect_url, None, self.token_url.url(), params, ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } /// Synchronously sends the request to the authorization server and awaits a response. pub fn request( self, http_client: &C, ) -> Result::Error, TE>> where C: SyncHttpClient, { endpoint_response(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and returns a Future. pub fn request_async<'c, C>( self, http_client: &'c C, ) -> impl Future>::Error, TE>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) }) } } /// A request to exchange a refresh token for an access token. /// /// See . #[derive(Debug)] pub struct RefreshTokenRequest<'a, TE, TR> where TE: ErrorResponse, TR: TokenResponse, { pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) refresh_token: &'a RefreshToken, pub(crate) scopes: Vec>, pub(crate) token_url: &'a TokenUrl, pub(crate) _phantom: PhantomData<(TE, TR)>, } impl<'a, TE, TR> RefreshTokenRequest<'a, TE, TR> where TE: ErrorResponse + 'static, TR: TokenResponse, { /// 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: &C, ) -> Result::Error, TE>> where C: SyncHttpClient, { endpoint_response(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and awaits a response. pub fn request_async<'c, C>( self, http_client: &'c C, ) -> impl Future>::Error, TE>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) }) } fn prepare_request(&self) -> Result> where RE: Error + 'static, { endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.token_url.url(), vec![ ("grant_type", "refresh_token"), ("refresh_token", self.refresh_token.secret()), ], ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } } /// A request to exchange resource owner credentials for an access token. /// /// See . #[derive(Debug)] pub struct PasswordTokenRequest<'a, TE, TR> where TE: ErrorResponse, TR: TokenResponse, { pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) username: &'a ResourceOwnerUsername, pub(crate) password: &'a ResourceOwnerPassword, pub(crate) scopes: Vec>, pub(crate) token_url: &'a TokenUrl, pub(crate) _phantom: PhantomData<(TE, TR)>, } impl<'a, TE, TR> PasswordTokenRequest<'a, TE, TR> where TE: ErrorResponse + 'static, TR: TokenResponse, { /// 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: &C, ) -> Result::Error, TE>> where C: SyncHttpClient, { endpoint_response(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and awaits a response. pub fn request_async<'c, C>( self, http_client: &'c C, ) -> impl Future>::Error, TE>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) }) } fn prepare_request(&self) -> Result> where RE: Error + 'static, { endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.token_url.url(), vec![ ("grant_type", "password"), ("username", self.username), ("password", self.password.secret()), ], ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } } /// A request to exchange client credentials for an access token. /// /// See . #[derive(Debug)] pub struct ClientCredentialsTokenRequest<'a, TE, TR> where TE: ErrorResponse, TR: TokenResponse, { pub(crate) auth_type: &'a AuthType, pub(crate) client_id: &'a ClientId, pub(crate) client_secret: Option<&'a ClientSecret>, pub(crate) extra_params: Vec<(Cow<'a, str>, Cow<'a, str>)>, pub(crate) scopes: Vec>, pub(crate) token_url: &'a TokenUrl, pub(crate) _phantom: PhantomData<(TE, TR)>, } impl<'a, TE, TR> ClientCredentialsTokenRequest<'a, TE, TR> where TE: ErrorResponse + 'static, TR: TokenResponse, { /// 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: &C, ) -> Result::Error, TE>> where C: SyncHttpClient, { endpoint_response(http_client.call(self.prepare_request()?)?) } /// Asynchronously sends the request to the authorization server and awaits a response. pub fn request_async<'c, C>( self, http_client: &'c C, ) -> impl Future>::Error, TE>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { endpoint_response(http_client.call(self.prepare_request()?).await?) }) } fn prepare_request(&self) -> Result> where RE: Error + 'static, { endpoint_request( self.auth_type, self.client_id, self.client_secret, &self.extra_params, None, Some(&self.scopes), self.token_url.url(), vec![("grant_type", "client_credentials")], ) .map_err(|err| RequestTokenError::Other(format!("failed to prepare request: {err}"))) } } /// Type of OAuth2 access token. 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, Eq, 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 { /// Type of OAuth2 access token included in this response. type TokenType: 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) -> &Self::TokenType; /// 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 = "crate::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 = "crate::helpers::deserialize_space_delimited_vec")] #[serde(serialize_with = "crate::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, { type TokenType = TT; /// 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() } } oauth2-5.0.0/src/token/tests.rs000064400000000000000000001257711046102023000144640ustar 00000000000000use crate::basic::{ BasicClient, BasicErrorResponse, BasicErrorResponseType, BasicTokenResponse, BasicTokenType, }; use crate::tests::colorful_extension::{ ColorfulClient, ColorfulErrorResponseType, ColorfulFields, ColorfulTokenResponse, ColorfulTokenType, }; use crate::tests::{mock_http_client, new_client, FakeError}; use crate::token::tests::custom_errors::CustomErrorClient; use crate::{ AccessToken, AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, ExtraTokenFields, PkceCodeVerifier, RedirectUrl, RefreshToken, RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, Scope, StandardErrorResponse, StandardTokenResponse, TokenResponse, TokenType, TokenUrl, }; use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use http::{HeaderValue, Response, StatusCode}; use std::borrow::Cow; use std::time::Duration; // 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())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::OK) .body( "{\"access_token\": \"12/34\", \"token_type\": \"BEARER\"}" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\", \ \"expires_in\": 3600, \ \"refresh_token\": \"foobar\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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().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())) .set_client_secret(ClientSecret::new("bbb/;&".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::OK) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::OK) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("APPLICATION/jSoN").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .body( "{\"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"access_token\": \"12/34\", \ \"token_type\": \"bearer\", \ \"scope\": \"read write\"\ }" .to_string() .into_bytes(), ) .unwrap(), )) .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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\ \"error\": \"invalid_request\", \ \"error_description\": \"stuff happened\"\ }" .to_string() .into_bytes(), ) .unwrap(), )); 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: invalid_request: stuff happened", token_err.to_string() ); } #[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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body("broken json".to_string().into_bytes()) .unwrap(), )); 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, Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, HeaderValue::from_str("text/plain").unwrap()) .body("broken json".to_string().into_bytes()) .unwrap(), )); 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())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\"access_token\": \"12/34\", \"token_type\": 123}" .to_string() .into_bytes(), ) .unwrap(), )); 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_err = 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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body(body.to_string().into_bytes()) .unwrap(), )) .err() .unwrap(); match token_err { RequestTokenError::ServerResponse(ref 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:?}"), } assert_eq!( "Server returned error response: invalid_request: Expired code.", token_err.to_string(), ); } #[test] fn test_exchange_code_fails_gracefully_on_transport_error() { let client = BasicClient::new(ClientId::new("aaa".to_string())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://auth".to_string()).unwrap()) .set_token_uri(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:?}"), } } #[test] fn test_extension_successful_with_minimal_json_response() { let client = ColorfulClient::new(ClientId::new("aaa".to_string())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\"access_token\": \"12/34\", \"token_type\": \"green\", \"height\": 10}" .to_string() .into_bytes(), ) .unwrap(), )) .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() { let client = ColorfulClient::new(ClientId::new("aaa".to_string())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::OK) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .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(), )) .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().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() { let client = ColorfulClient::new(ClientId::new("aaa".to_string())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\"error\": \"too_light\", \"error_description\": \"stuff happened\", \ \"error_uri\": \"https://errors\"}" .to_string() .into_bytes(), ) .unwrap(), )); 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: too_light: stuff happened (see https://errors)", token_err.to_string() ); } mod custom_errors { use crate::tests::colorful_extension::{ ColorfulFields, ColorfulRevocableToken, ColorfulTokenType, }; use crate::{Client, ErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse}; use serde::{Deserialize, Serialize}; use std::fmt::Error as FormatterError; use std::fmt::{Display, Formatter}; extern crate serde_json; #[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< HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > = Client< CustomErrorResponse, StandardTokenResponse, StandardTokenIntrospectionResponse, ColorfulRevocableToken, CustomErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, >; } #[test] fn test_extension_with_custom_json_error() { let client = CustomErrorClient::new(ClientId::new("aaa".to_string())) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example.com/auth".to_string()).unwrap()) .set_token_uri(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, Response::builder() .status(StatusCode::BAD_REQUEST) .header( CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap(), ) .body( "{\"custom_error\": \"non-compliant oauth implementation ;-)\"}" .to_string() .into_bytes(), ) .unwrap(), )); 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() { 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, ); } oauth2-5.0.0/src/types.rs000064400000000000000000000520351046102023000133360ustar 00000000000000use base64::prelude::*; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use url::Url; use std::fmt::Error as FormatterError; use std::fmt::{Debug, Formatter}; #[cfg(feature = "timing-resistant-secret-traits")] use std::hash::{Hash, Hasher}; use std::ops::Deref; 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 From<$name> for $type { fn from(t: $name) -> $type { t.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] )* #[cfg_attr(feature = "timing-resistant-secret-traits", derive(Eq))] 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 } #[doc = $secret_doc] /// /// # Security Warning /// /// Leaking this value may compromise the security of the OAuth2 flow. pub fn into_secret(self) -> $type { self.0 } } impl Debug for $name { fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { write!(f, concat!(stringify!($name), "([redacted])")) } } #[cfg(feature = "timing-resistant-secret-traits")] impl PartialEq for $name { fn eq(&self, other: &Self) -> bool { Sha256::digest(&self.0) == Sha256::digest(&other.0) } } #[cfg(feature = "timing-resistant-secret-traits")] impl Hash for $name { fn hash(&self, state: &mut H) { Sha256::digest(&self.0).hash(state) } } }; } /// 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::Display for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { write!(f, "{}", 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, Eq, 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!((32..=96).contains(&num_bytes)); let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); PkceCodeVerifier::new(BASE64_URL_SAFE_NO_PAD.encode(random_bytes)) } /// 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_URL_SAFE_NO_PAD.encode(digest); 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_URL_SAFE_NO_PAD.encode(random_bytes)) } } ]; 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) ]; #[cfg(test)] mod tests { use crate::{ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier}; #[test] fn test_secret_conversion() { let secret = CsrfToken::new("top_secret".into()); assert_eq!(secret.into_secret().into_boxed_str(), "top_secret".into()); } #[test] fn test_secret_redaction() { let secret = ClientSecret::new("top_secret".to_string()); assert_eq!("ClientSecret([redacted])", format!("{secret:?}")); } #[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", ); } } oauth2-5.0.0/src/ureq_client.rs000064400000000000000000000035661046102023000145110ustar 00000000000000use crate::{HttpClientError, HttpRequest, HttpResponse}; use http::{ header::{HeaderValue, CONTENT_TYPE}, method::Method, status::StatusCode, }; use std::io::Read; impl crate::SyncHttpClient for ureq::Agent { type Error = HttpClientError; fn call(&self, request: HttpRequest) -> Result { let mut req = if *request.method() == Method::POST { self.post(&request.uri().to_string()) } else { debug_assert_eq!(*request.method(), Method::GET); self.get(&request.uri().to_string()) }; for (name, value) in request.headers() { req = req.set( name.as_ref(), // TODO: In newer `ureq` it should be easier to convert arbitrary byte sequences // without unnecessary UTF-8 fallibility here. value.to_str().map_err(|_| { HttpClientError::Other(format!( "invalid `{name}` header value {:?}", value.as_bytes() )) })?, ); } let response = if let Method::POST = *request.method() { req.send_bytes(request.body()) } else { req.call() } .map_err(Box::new)?; let mut builder = http::Response::builder() .status(StatusCode::from_u16(response.status()).map_err(http::Error::from)?); if let Some(content_type) = response .header(CONTENT_TYPE.as_str()) .map(HeaderValue::from_str) .transpose() .map_err(http::Error::from)? { builder = builder.header(CONTENT_TYPE, content_type); } let mut body = Vec::new(); response.into_reader().read_to_end(&mut body)?; builder.body(body).map_err(HttpClientError::Http) } }