├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE-MIT
├── README.md
├── examples
├── microsoft_devicecode_common_user.rs
├── microsoft_devicecode_tenant_user.rs
├── google_devicecode.rs
├── github.rs
├── github_async.rs
├── letterboxd.rs
├── google.rs
├── msgraph.rs
└── wunderlist.rs
├── src
├── ureq_client.rs
├── reqwest_client.rs
├── curl_client.rs
├── error.rs
├── basic.rs
├── endpoint.rs
├── tests.rs
├── helpers.rs
├── code.rs
├── introspection.rs
├── types.rs
└── revocation.rs
├── Cargo.toml
├── LICENSE-APACHE
└── UPGRADE.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [ramosbugs]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/**
2 | *.iml
3 | /target
4 | /Cargo.lock
5 | *~
6 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Alex Crichton
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OAuth2
2 |
3 |
4 | [](https://github.com/ramosbugs/oauth2-rs/actions/workflows/main.yml)
5 |
6 | An extensible, strongly-typed implementation of OAuth2
7 | ([RFC 6749](https://tools.ietf.org/html/rfc6749)).
8 |
9 | Documentation is available on [docs.rs](https://docs.rs/oauth2). Release notes are available on [GitHub](https://github.com/ramosbugs/oauth2-rs/releases).
10 |
11 | For authentication (e.g., single sign-on or social login) purposes, consider using the
12 | [`openidconnect`](https://github.com/ramosbugs/openidconnect-rs) crate, which is built on top of
13 | this one.
14 |
15 | ## Minimum Supported Rust Version (MSRV)
16 |
17 | The MSRV for *5.0* and newer releases of this crate is Rust **1.65**.
18 |
19 | The MSRV for *4.x* releases of this crate is Rust 1.45.
20 |
21 | Beginning with the 5.0.0 release, this crate will maintain a policy of supporting
22 | Rust releases going back at least 6 months. Changes that break compatibility with Rust releases
23 | older than 6 months will no longer be considered SemVer breaking changes and will not result in a
24 | new major version number for this crate. MSRV changes will coincide with minor version updates
25 | and will not happen in patch releases.
26 |
--------------------------------------------------------------------------------
/examples/microsoft_devicecode_common_user.rs:
--------------------------------------------------------------------------------
1 | use oauth2::basic::BasicClient;
2 | use oauth2::{
3 | AuthUrl, ClientId, DeviceAuthorizationUrl, Scope, StandardDeviceAuthorizationResponse, TokenUrl,
4 | };
5 |
6 | use std::error::Error;
7 |
8 | #[tokio::main]
9 | async fn main() -> Result<(), Box> {
10 | let client = BasicClient::new(ClientId::new("client_id".to_string()))
11 | .set_auth_uri(AuthUrl::new(
12 | "https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string(),
13 | )?)
14 | .set_token_uri(TokenUrl::new(
15 | "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string(),
16 | )?)
17 | .set_device_authorization_url(DeviceAuthorizationUrl::new(
18 | "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(),
19 | )?);
20 |
21 | let http_client = reqwest::ClientBuilder::new()
22 | // Following redirects opens the client up to SSRF vulnerabilities.
23 | .redirect(reqwest::redirect::Policy::none())
24 | .build()
25 | .expect("Client should build");
26 |
27 | let details: StandardDeviceAuthorizationResponse = client
28 | .exchange_device_code()
29 | .add_scope(Scope::new("read".to_string()))
30 | .request_async(&http_client)
31 | .await?;
32 |
33 | eprintln!(
34 | "Open this URL in your browser:\n{}\nand enter the code: {}",
35 | details.verification_uri(),
36 | details.user_code().secret(),
37 | );
38 |
39 | let token_result = client
40 | .exchange_device_access_token(&details)
41 | .request_async(&http_client, tokio::time::sleep, None)
42 | .await;
43 |
44 | eprintln!("Token:{token_result:?}");
45 |
46 | Ok(())
47 | }
48 |
--------------------------------------------------------------------------------
/examples/microsoft_devicecode_tenant_user.rs:
--------------------------------------------------------------------------------
1 | use oauth2::basic::BasicClient;
2 | use oauth2::reqwest;
3 | use oauth2::StandardDeviceAuthorizationResponse;
4 | use oauth2::{AuthUrl, ClientId, DeviceAuthorizationUrl, Scope, TokenUrl};
5 |
6 | use std::error::Error;
7 |
8 | // Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
9 | // Please use your tenant id when using this example
10 | const TENANT_ID: &str = "{tenant}";
11 |
12 | #[tokio::main]
13 | async fn main() -> Result<(), Box> {
14 | let client = BasicClient::new(ClientId::new("client_id".to_string()))
15 | .set_auth_uri(AuthUrl::new(format!(
16 | "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize",
17 | TENANT_ID
18 | ))?)
19 | .set_token_uri(TokenUrl::new(format!(
20 | "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
21 | TENANT_ID
22 | ))?)
23 | .set_device_authorization_url(DeviceAuthorizationUrl::new(format!(
24 | "https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode",
25 | TENANT_ID
26 | ))?);
27 |
28 | let http_client = reqwest::ClientBuilder::new()
29 | // Following redirects opens the client up to SSRF vulnerabilities.
30 | .redirect(reqwest::redirect::Policy::none())
31 | .build()
32 | .expect("Client should build");
33 |
34 | let details: StandardDeviceAuthorizationResponse = client
35 | .exchange_device_code()
36 | .add_scope(Scope::new("read".to_string()))
37 | .request_async(&http_client)
38 | .await?;
39 |
40 | eprintln!(
41 | "Open this URL in your browser:\n{}\nand enter the code: {}",
42 | details.verification_uri(),
43 | details.user_code().secret(),
44 | );
45 |
46 | let token_result = client
47 | .exchange_device_access_token(&details)
48 | .request_async(&http_client, tokio::time::sleep, None)
49 | .await;
50 |
51 | eprintln!("Token:{token_result:?}");
52 |
53 | Ok(())
54 | }
55 |
--------------------------------------------------------------------------------
/src/ureq_client.rs:
--------------------------------------------------------------------------------
1 | use crate::{HttpClientError, HttpRequest, HttpResponse};
2 |
3 | use http::{
4 | header::{HeaderValue, CONTENT_TYPE},
5 | method::Method,
6 | status::StatusCode,
7 | };
8 |
9 | use std::io::Read;
10 |
11 | impl crate::SyncHttpClient for ureq::Agent {
12 | type Error = HttpClientError;
13 |
14 | fn call(&self, request: HttpRequest) -> Result {
15 | let mut req = if *request.method() == Method::POST {
16 | self.post(&request.uri().to_string())
17 | } else {
18 | debug_assert_eq!(*request.method(), Method::GET);
19 | self.get(&request.uri().to_string())
20 | };
21 |
22 | for (name, value) in request.headers() {
23 | req = req.set(
24 | name.as_ref(),
25 | // TODO: In newer `ureq` it should be easier to convert arbitrary byte sequences
26 | // without unnecessary UTF-8 fallibility here.
27 | value.to_str().map_err(|_| {
28 | HttpClientError::Other(format!(
29 | "invalid `{name}` header value {:?}",
30 | value.as_bytes()
31 | ))
32 | })?,
33 | );
34 | }
35 |
36 | let response = if let Method::POST = *request.method() {
37 | req.send_bytes(request.body())
38 | } else {
39 | req.call()
40 | }
41 | .map_err(Box::new)?;
42 |
43 | let mut builder = http::Response::builder()
44 | .status(StatusCode::from_u16(response.status()).map_err(http::Error::from)?);
45 |
46 | if let Some(content_type) = response
47 | .header(CONTENT_TYPE.as_str())
48 | .map(HeaderValue::from_str)
49 | .transpose()
50 | .map_err(http::Error::from)?
51 | {
52 | builder = builder.header(CONTENT_TYPE, content_type);
53 | }
54 |
55 | let mut body = Vec::new();
56 | response.into_reader().read_to_end(&mut body)?;
57 |
58 | builder.body(body).map_err(HttpClientError::Http)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oauth2"
3 | authors = ["Alex Crichton ", "Florin Lipan ", "David A. Ramos "]
4 | version = "5.0.0-rc.1"
5 | license = "MIT OR Apache-2.0"
6 | description = "An extensible, strongly-typed implementation of OAuth2"
7 | repository = "https://github.com/ramosbugs/oauth2-rs"
8 | edition = "2021"
9 | readme = "README.md"
10 | rust-version = "1.65"
11 |
12 | [package.metadata.docs.rs]
13 | all-features = true
14 |
15 | [badges]
16 | maintenance = { status = "actively-developed" }
17 |
18 | [features]
19 | default = ["reqwest", "rustls-tls"]
20 | pkce-plain = []
21 | native-tls = ["reqwest/native-tls"]
22 | reqwest-blocking = ["reqwest/blocking"]
23 | rustls-tls = ["reqwest/rustls-tls"]
24 | timing-resistant-secret-traits = []
25 |
26 | [[example]]
27 | name = "github"
28 | required-features = ["reqwest-blocking"]
29 |
30 | [[example]]
31 | name = "google"
32 | required-features = ["reqwest-blocking"]
33 |
34 | [[example]]
35 | name = "google_devicecode"
36 | required-features = ["reqwest-blocking"]
37 |
38 | [[example]]
39 | name = "letterboxd"
40 | required-features = ["reqwest-blocking"]
41 |
42 | [[example]]
43 | name = "msgraph"
44 | required-features = ["reqwest-blocking"]
45 |
46 | [[example]]
47 | name = "wunderlist"
48 | required-features = ["reqwest-blocking"]
49 |
50 | [dependencies]
51 | base64 = ">= 0.21, <0.23"
52 | thiserror = "1.0"
53 | http = "1.0"
54 | rand = "0.8"
55 | reqwest = { version = "0.12", optional = true, default-features = false }
56 | serde = { version = "1.0", features = ["derive"] }
57 | serde_json = "1.0"
58 | sha2 = "0.10"
59 | ureq = { version = "2", optional = true }
60 | url = { version = "2.1", features = ["serde"] }
61 | chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde", "std", "wasmbind"] }
62 | serde_path_to_error = "0.1.2"
63 |
64 | [target.'cfg(target_arch = "wasm32")'.dependencies]
65 | getrandom = { version = "0.2", features = ["js"] }
66 |
67 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
68 | curl = { version = "0.4.0", optional = true }
69 |
70 | [dev-dependencies]
71 | hex = "0.4"
72 | hmac = "0.12"
73 | uuid = { version = "1.10", features = ["v4"] }
74 | anyhow = "1.0"
75 | tokio = { version = "1.0", features = ["full"] }
76 | async-std = "1.13"
77 |
--------------------------------------------------------------------------------
/src/reqwest_client.rs:
--------------------------------------------------------------------------------
1 | use crate::{AsyncHttpClient, HttpClientError, HttpRequest, HttpResponse};
2 |
3 | use std::future::Future;
4 | use std::pin::Pin;
5 |
6 | impl<'c> AsyncHttpClient<'c> for reqwest::Client {
7 | type Error = HttpClientError;
8 |
9 | #[cfg(target_arch = "wasm32")]
10 | type Future = Pin> + 'c>>;
11 | #[cfg(not(target_arch = "wasm32"))]
12 | type Future =
13 | Pin> + Send + Sync + 'c>>;
14 |
15 | fn call(&'c self, request: HttpRequest) -> Self::Future {
16 | Box::pin(async move {
17 | let response = self
18 | .execute(request.try_into().map_err(Box::new)?)
19 | .await
20 | .map_err(Box::new)?;
21 |
22 | let mut builder = http::Response::builder().status(response.status());
23 |
24 | #[cfg(not(target_arch = "wasm32"))]
25 | {
26 | builder = builder.version(response.version());
27 | }
28 |
29 | for (name, value) in response.headers().iter() {
30 | builder = builder.header(name, value);
31 | }
32 |
33 | builder
34 | .body(response.bytes().await.map_err(Box::new)?.to_vec())
35 | .map_err(HttpClientError::Http)
36 | })
37 | }
38 | }
39 |
40 | #[cfg(all(feature = "reqwest-blocking", not(target_arch = "wasm32")))]
41 | impl crate::SyncHttpClient for reqwest::blocking::Client {
42 | type Error = HttpClientError;
43 |
44 | fn call(&self, request: HttpRequest) -> Result {
45 | let mut response = self
46 | .execute(request.try_into().map_err(Box::new)?)
47 | .map_err(Box::new)?;
48 |
49 | let mut builder = http::Response::builder()
50 | .status(response.status())
51 | .version(response.version());
52 |
53 | for (name, value) in response.headers().iter() {
54 | builder = builder.header(name, value);
55 | }
56 |
57 | let mut body = Vec::new();
58 | ::read_to_end(&mut response, &mut body)?;
59 |
60 | builder.body(body).map_err(HttpClientError::Http)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/curl_client.rs:
--------------------------------------------------------------------------------
1 | use crate::{HttpClientError, HttpRequest, HttpResponse, SyncHttpClient};
2 |
3 | use curl::easy::Easy;
4 | use http::header::{HeaderValue, CONTENT_TYPE};
5 | use http::method::Method;
6 | use http::status::StatusCode;
7 |
8 | use std::io::Read;
9 |
10 | /// A synchronous HTTP client using [`curl`].
11 | pub struct CurlHttpClient;
12 | impl SyncHttpClient for CurlHttpClient {
13 | type Error = HttpClientError;
14 |
15 | fn call(&self, request: HttpRequest) -> Result {
16 | let mut easy = Easy::new();
17 | easy.url(&request.uri().to_string()[..]).map_err(Box::new)?;
18 |
19 | let mut headers = curl::easy::List::new();
20 | for (name, value) in request.headers() {
21 | headers
22 | .append(&format!(
23 | "{}: {}",
24 | name,
25 | // TODO: Unnecessary fallibility, curl uses a CString under the hood
26 | value.to_str().map_err(|_| HttpClientError::Other(format!(
27 | "invalid `{name}` header value {:?}",
28 | value.as_bytes()
29 | )))?
30 | ))
31 | .map_err(Box::new)?
32 | }
33 |
34 | easy.http_headers(headers).map_err(Box::new)?;
35 |
36 | if let Method::POST = *request.method() {
37 | easy.post(true).map_err(Box::new)?;
38 | easy.post_field_size(request.body().len() as u64)
39 | .map_err(Box::new)?;
40 | } else {
41 | assert_eq!(*request.method(), Method::GET);
42 | }
43 |
44 | let mut form_slice = &request.body()[..];
45 | let mut data = Vec::new();
46 | {
47 | let mut transfer = easy.transfer();
48 |
49 | transfer
50 | .read_function(|buf| Ok(form_slice.read(buf).unwrap_or(0)))
51 | .map_err(Box::new)?;
52 |
53 | transfer
54 | .write_function(|new_data| {
55 | data.extend_from_slice(new_data);
56 | Ok(new_data.len())
57 | })
58 | .map_err(Box::new)?;
59 |
60 | transfer.perform().map_err(Box::new)?;
61 | }
62 |
63 | let mut builder = http::Response::builder().status(
64 | StatusCode::from_u16(easy.response_code().map_err(Box::new)? as u16)
65 | .map_err(http::Error::from)?,
66 | );
67 |
68 | if let Some(content_type) = easy
69 | .content_type()
70 | .map_err(Box::new)?
71 | .map(HeaderValue::from_str)
72 | .transpose()
73 | .map_err(http::Error::from)?
74 | {
75 | builder = builder.header(CONTENT_TYPE, content_type);
76 | }
77 |
78 | builder.body(data).map_err(HttpClientError::Http)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # Controls when the workflow will run
4 | on:
5 | # Triggers the workflow on push or pull request events.
6 | push: {}
7 | pull_request: {}
8 | schedule:
9 | # Run daily to catch breakages in new Rust versions as well as new cargo audit findings.
10 | - cron: '0 16 * * *'
11 |
12 | # Allows you to run this workflow manually from the Actions tab
13 | workflow_dispatch:
14 |
15 | env:
16 | CARGO_TERM_COLOR: always
17 |
18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
19 | jobs:
20 | # This workflow contains a single job called "build"
21 | test:
22 | # The type of runner that the job will run on
23 | runs-on: ${{ matrix.rust_os.os }}
24 |
25 | strategy:
26 | fail-fast: false
27 | matrix:
28 | rust_os:
29 | - { rust: 1.65.0, os: ubuntu-20.04 }
30 | - { rust: stable, os: ubuntu-latest }
31 | - { rust: beta, os: ubuntu-latest }
32 | - { rust: nightly, os: ubuntu-latest }
33 |
34 | env:
35 | CARGO_NET_GIT_FETCH_WITH_CLI: "true"
36 |
37 | # Steps represent a sequence of tasks that will be executed as part of the job
38 | steps:
39 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
40 | - uses: actions/checkout@v2
41 |
42 | - name: Install Rust toolchain
43 | uses: actions-rs/toolchain@v1
44 | with:
45 | toolchain: ${{ matrix.rust_os.rust }}
46 | override: true
47 | components: clippy, rustfmt
48 | target: wasm32-unknown-unknown
49 |
50 | # Newer dependency versions may not support the MSRV, so we use a Cargo.lock file for those
51 | # builds.
52 | - name: Use Rust 1.65 lockfile
53 | if: ${{ matrix.rust_os.rust == '1.65.0' }}
54 | run: |
55 | cp Cargo-1.65.lock Cargo.lock
56 | echo "CARGO_LOCKED=--locked" >> $GITHUB_ENV
57 |
58 | - name: Run tests
59 | run: cargo ${CARGO_LOCKED} test --tests --examples
60 | - name: Doc tests
61 | run: |
62 | cargo ${CARGO_LOCKED} test --doc
63 | cargo ${CARGO_LOCKED} test --doc --no-default-features
64 | cargo ${CARGO_LOCKED} test --doc --all-features
65 | - name: Test with all features enabled
66 | run: cargo ${CARGO_LOCKED} test --all-features
67 | # Curl without reqwest (examples will not build)
68 | - name: Test with curl (w/o reqwest)
69 | run: cargo ${CARGO_LOCKED} test --tests --features curl --no-default-features
70 |
71 | - name: Check fmt
72 | if: ${{ matrix.rust_os.rust == '1.65.0' }}
73 | run: cargo ${CARGO_LOCKED} fmt --all -- --check
74 |
75 | - name: Clippy
76 | if: ${{ matrix.rust_os.rust == '1.65.0' }}
77 | run: cargo ${CARGO_LOCKED} clippy --all --all-features -- --deny warnings
78 |
79 | - name: Audit
80 | if: ${{ matrix.rust_os.rust == 'stable' }}
81 | run: |
82 | cargo ${CARGO_LOCKED} install --force cargo-audit
83 | # The chrono thread safety issue doesn't affect this crate since the crate does not rely
84 | # on the system's local time zone, only UTC. See:
85 | # https://github.com/chronotope/chrono/issues/499#issuecomment-946388161
86 | cargo ${CARGO_LOCKED} audit --ignore RUSTSEC-2020-0159
87 |
88 | - name: Check WASM build
89 | run: cargo ${CARGO_LOCKED} check --target wasm32-unknown-unknown
90 |
--------------------------------------------------------------------------------
/examples/google_devicecode.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Google OAuth2 process for requesting access to the Google Calendar features
3 | //! and the user's profile.
4 | //!
5 | //! Before running it, you'll need to generate your own Google OAuth2 credentials.
6 | //!
7 | //! In order to run the example call:
8 | //!
9 | //! ```sh
10 | //! GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy cargo run --example google
11 | //! ```
12 | //!
13 | //! ...and follow the instructions.
14 | //!
15 |
16 | use oauth2::basic::BasicClient;
17 | use oauth2::reqwest;
18 | use oauth2::{
19 | AuthType, AuthUrl, ClientId, ClientSecret, DeviceAuthorizationResponse, DeviceAuthorizationUrl,
20 | ExtraDeviceAuthorizationFields, Scope, TokenUrl,
21 | };
22 | use serde::{Deserialize, Serialize};
23 |
24 | use std::collections::HashMap;
25 | use std::env;
26 |
27 | #[derive(Debug, Serialize, Deserialize)]
28 | struct StoringFields(HashMap);
29 |
30 | impl ExtraDeviceAuthorizationFields for StoringFields {}
31 | type StoringDeviceAuthorizationResponse = DeviceAuthorizationResponse;
32 |
33 | fn main() {
34 | let google_client_id = ClientId::new(
35 | env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."),
36 | );
37 | let google_client_secret = ClientSecret::new(
38 | env::var("GOOGLE_CLIENT_SECRET")
39 | .expect("Missing the GOOGLE_CLIENT_SECRET environment variable."),
40 | );
41 | let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string())
42 | .expect("Invalid authorization endpoint URL");
43 | let token_url = TokenUrl::new("https://www.googleapis.com/oauth2/v3/token".to_string())
44 | .expect("Invalid token endpoint URL");
45 | let device_auth_url =
46 | DeviceAuthorizationUrl::new("https://oauth2.googleapis.com/device/code".to_string())
47 | .expect("Invalid device authorization endpoint URL");
48 |
49 | // Set up the config for the Google OAuth2 process.
50 | //
51 | // Google's OAuth endpoint expects the client_id to be in the request body,
52 | // so ensure that option is set.
53 | let device_client = BasicClient::new(google_client_id)
54 | .set_client_secret(google_client_secret)
55 | .set_auth_uri(auth_url)
56 | .set_token_uri(token_url)
57 | .set_device_authorization_url(device_auth_url)
58 | .set_auth_type(AuthType::RequestBody);
59 |
60 | let http_client = reqwest::blocking::ClientBuilder::new()
61 | // Following redirects opens the client up to SSRF vulnerabilities.
62 | .redirect(reqwest::redirect::Policy::none())
63 | .build()
64 | .expect("Client should build");
65 |
66 | // Request the set of codes from the Device Authorization endpoint.
67 | let details: StoringDeviceAuthorizationResponse = device_client
68 | .exchange_device_code()
69 | .add_scope(Scope::new("profile".to_string()))
70 | .request(&http_client)
71 | .expect("Failed to request codes from device auth endpoint");
72 |
73 | // Display the URL and user-code.
74 | println!(
75 | "Open this URL in your browser:\n{}\nand enter the code: {}",
76 | details.verification_uri(),
77 | details.user_code().secret(),
78 | );
79 |
80 | // Now poll for the token
81 | let token = device_client
82 | .exchange_device_access_token(&details)
83 | .request(&http_client, std::thread::sleep, None)
84 | .expect("Failed to get token");
85 |
86 | println!("Google returned the following token:\n{token:?}\n");
87 | }
88 |
--------------------------------------------------------------------------------
/examples/github.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Github OAuth2 process for requesting access to the user's public repos and
3 | //! email address.
4 | //!
5 | //! Before running it, you'll need to generate your own Github OAuth2 credentials.
6 | //!
7 | //! In order to run the example call:
8 | //!
9 | //! ```sh
10 | //! GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy cargo run --example github
11 | //! ```
12 | //!
13 | //! ...and follow the instructions.
14 | //!
15 |
16 | use oauth2::basic::BasicClient;
17 | use oauth2::reqwest;
18 | use oauth2::{
19 | AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope,
20 | TokenResponse, TokenUrl,
21 | };
22 | use url::Url;
23 |
24 | use std::env;
25 | use std::io::{BufRead, BufReader, Write};
26 | use std::net::TcpListener;
27 |
28 | fn main() {
29 | let github_client_id = ClientId::new(
30 | env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable."),
31 | );
32 | let github_client_secret = ClientSecret::new(
33 | env::var("GITHUB_CLIENT_SECRET")
34 | .expect("Missing the GITHUB_CLIENT_SECRET environment variable."),
35 | );
36 | let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string())
37 | .expect("Invalid authorization endpoint URL");
38 | let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string())
39 | .expect("Invalid token endpoint URL");
40 |
41 | // Set up the config for the Github OAuth2 process.
42 | let client = BasicClient::new(github_client_id)
43 | .set_client_secret(github_client_secret)
44 | .set_auth_uri(auth_url)
45 | .set_token_uri(token_url)
46 | // This example will be running its own server at localhost:8080.
47 | // See below for the server implementation.
48 | .set_redirect_uri(
49 | RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
50 | );
51 |
52 | let http_client = reqwest::blocking::ClientBuilder::new()
53 | // Following redirects opens the client up to SSRF vulnerabilities.
54 | .redirect(reqwest::redirect::Policy::none())
55 | .build()
56 | .expect("Client should build");
57 |
58 | // Generate the authorization URL to which we'll redirect the user.
59 | let (authorize_url, csrf_state) = client
60 | .authorize_url(CsrfToken::new_random)
61 | // This example is requesting access to the user's public repos and email.
62 | .add_scope(Scope::new("public_repo".to_string()))
63 | .add_scope(Scope::new("user:email".to_string()))
64 | .url();
65 |
66 | println!("Open this URL in your browser:\n{authorize_url}\n");
67 |
68 | let (code, state) = {
69 | // A very naive implementation of the redirect server.
70 | let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
71 |
72 | // The server will terminate itself after collecting the first code.
73 | let Some(mut stream) = listener.incoming().flatten().next() else {
74 | panic!("listener terminated without accepting a connection");
75 | };
76 |
77 | let mut reader = BufReader::new(&stream);
78 |
79 | let mut request_line = String::new();
80 | reader.read_line(&mut request_line).unwrap();
81 |
82 | let redirect_url = request_line.split_whitespace().nth(1).unwrap();
83 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
84 |
85 | let code = url
86 | .query_pairs()
87 | .find(|(key, _)| key == "code")
88 | .map(|(_, code)| AuthorizationCode::new(code.into_owned()))
89 | .unwrap();
90 |
91 | let state = url
92 | .query_pairs()
93 | .find(|(key, _)| key == "state")
94 | .map(|(_, state)| CsrfToken::new(state.into_owned()))
95 | .unwrap();
96 |
97 | let message = "Go back to your terminal :)";
98 | let response = format!(
99 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
100 | message.len(),
101 | message
102 | );
103 | stream.write_all(response.as_bytes()).unwrap();
104 |
105 | (code, state)
106 | };
107 |
108 | println!("Github returned the following code:\n{}\n", code.secret());
109 | println!(
110 | "Github returned the following state:\n{} (expected `{}`)\n",
111 | state.secret(),
112 | csrf_state.secret()
113 | );
114 |
115 | // Exchange the code with a token.
116 | let token_res = client.exchange_code(code).request(&http_client);
117 |
118 | println!("Github returned the following token:\n{token_res:?}\n");
119 |
120 | if let Ok(token) = token_res {
121 | // NB: Github returns a single comma-separated "scope" parameter instead of multiple
122 | // space-separated scopes. Github-specific clients can parse this scope into
123 | // multiple scopes by splitting at the commas. Note that it's not safe for the
124 | // library to do this by default because RFC 6749 allows scopes to contain commas.
125 | let scopes = if let Some(scopes_vec) = token.scopes() {
126 | scopes_vec
127 | .iter()
128 | .flat_map(|comma_separated| comma_separated.split(','))
129 | .collect::>()
130 | } else {
131 | Vec::new()
132 | };
133 | println!("Github returned the following scopes:\n{scopes:?}\n");
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/examples/github_async.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Github OAuth2 process for requesting access to the user's public repos and
3 | //! email address.
4 | //!
5 | //! Before running it, you'll need to generate your own Github OAuth2 credentials.
6 | //!
7 | //! In order to run the example call:
8 | //!
9 | //! ```sh
10 | //! GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy cargo run --example github
11 | //! ```
12 | //!
13 | //! ...and follow the instructions.
14 | //!
15 |
16 | use oauth2::basic::BasicClient;
17 | use oauth2::reqwest;
18 | use oauth2::{
19 | AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope,
20 | TokenResponse, TokenUrl,
21 | };
22 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
23 | use tokio::net::TcpListener;
24 | use url::Url;
25 |
26 | use std::env;
27 |
28 | #[tokio::main]
29 | async fn main() {
30 | let github_client_id = ClientId::new(
31 | env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable."),
32 | );
33 | let github_client_secret = ClientSecret::new(
34 | env::var("GITHUB_CLIENT_SECRET")
35 | .expect("Missing the GITHUB_CLIENT_SECRET environment variable."),
36 | );
37 | let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string())
38 | .expect("Invalid authorization endpoint URL");
39 | let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string())
40 | .expect("Invalid token endpoint URL");
41 |
42 | // Set up the config for the Github OAuth2 process.
43 | let client = BasicClient::new(github_client_id)
44 | .set_client_secret(github_client_secret)
45 | .set_auth_uri(auth_url)
46 | .set_token_uri(token_url)
47 | // This example will be running its own server at localhost:8080.
48 | // See below for the server implementation.
49 | .set_redirect_uri(
50 | RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
51 | );
52 |
53 | let http_client = reqwest::ClientBuilder::new()
54 | // Following redirects opens the client up to SSRF vulnerabilities.
55 | .redirect(reqwest::redirect::Policy::none())
56 | .build()
57 | .expect("Client should build");
58 |
59 | // Generate the authorization URL to which we'll redirect the user.
60 | let (authorize_url, csrf_state) = client
61 | .authorize_url(CsrfToken::new_random)
62 | // This example is requesting access to the user's public repos and email.
63 | .add_scope(Scope::new("public_repo".to_string()))
64 | .add_scope(Scope::new("user:email".to_string()))
65 | .url();
66 |
67 | println!("Open this URL in your browser:\n{authorize_url}\n");
68 |
69 | let (code, state) = {
70 | // A very naive implementation of the redirect server.
71 | let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
72 |
73 | loop {
74 | if let Ok((mut stream, _)) = listener.accept().await {
75 | let mut reader = BufReader::new(&mut stream);
76 |
77 | let mut request_line = String::new();
78 | reader.read_line(&mut request_line).await.unwrap();
79 |
80 | let redirect_url = request_line.split_whitespace().nth(1).unwrap();
81 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
82 |
83 | let code = url
84 | .query_pairs()
85 | .find(|(key, _)| key == "code")
86 | .map(|(_, code)| AuthorizationCode::new(code.into_owned()))
87 | .unwrap();
88 |
89 | let state = url
90 | .query_pairs()
91 | .find(|(key, _)| key == "state")
92 | .map(|(_, state)| CsrfToken::new(state.into_owned()))
93 | .unwrap();
94 |
95 | let message = "Go back to your terminal :)";
96 | let response = format!(
97 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
98 | message.len(),
99 | message
100 | );
101 | stream.write_all(response.as_bytes()).await.unwrap();
102 |
103 | // The server will terminate itself after collecting the first code.
104 | break (code, state);
105 | }
106 | }
107 | };
108 |
109 | println!("Github returned the following code:\n{}\n", code.secret());
110 | println!(
111 | "Github returned the following state:\n{} (expected `{}`)\n",
112 | state.secret(),
113 | csrf_state.secret()
114 | );
115 |
116 | // Exchange the code with a token.
117 | let token_res = client.exchange_code(code).request_async(&http_client).await;
118 |
119 | println!("Github returned the following token:\n{token_res:?}\n");
120 |
121 | if let Ok(token) = token_res {
122 | // NB: Github returns a single comma-separated "scope" parameter instead of multiple
123 | // space-separated scopes. Github-specific clients can parse this scope into
124 | // multiple scopes by splitting at the commas. Note that it's not safe for the
125 | // library to do this by default because RFC 6749 allows scopes to contain commas.
126 | let scopes = if let Some(scopes_vec) = token.scopes() {
127 | scopes_vec
128 | .iter()
129 | .flat_map(|comma_separated| comma_separated.split(','))
130 | .collect::>()
131 | } else {
132 | Vec::new()
133 | };
134 | println!("Github returned the following scopes:\n{scopes:?}\n");
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/examples/letterboxd.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Letterboxd OAuth2 process for requesting access
3 | //! to the API features restricted by authentication. Letterboxd requires all
4 | //! requests being signed as described in http://api-docs.letterboxd.com/#signing.
5 | //! So this serves as an example how to implement a custom client, which signs
6 | //! requests and appends the signature to the url query.
7 | //!
8 | //! Before running it, you'll need to get access to the API.
9 | //!
10 | //! In order to run the example call:
11 | //!
12 | //! ```sh
13 | //! LETTERBOXD_CLIENT_ID=xxx LETTERBOXD_CLIENT_SECRET=yyy LETTERBOXD_USERNAME=www LETTERBOXD_PASSWORD=zzz cargo run --example letterboxd
14 | //! ```
15 |
16 | use hex::ToHex;
17 | use hmac::{Hmac, Mac};
18 | use oauth2::{
19 | basic::BasicClient, AuthType, AuthUrl, ClientId, ClientSecret, HttpRequest, HttpResponse,
20 | ResourceOwnerPassword, ResourceOwnerUsername, SyncHttpClient, TokenUrl,
21 | };
22 | use sha2::Sha256;
23 | use url::Url;
24 |
25 | use std::env;
26 | use std::time;
27 |
28 | fn main() -> Result<(), anyhow::Error> {
29 | // a.k.a api key in Letterboxd API documentation
30 | let letterboxd_client_id = ClientId::new(
31 | env::var("LETTERBOXD_CLIENT_ID")
32 | .expect("Missing the LETTERBOXD_CLIENT_ID environment variable."),
33 | );
34 | // a.k.a api secret in Letterboxd API documentation
35 | let letterboxd_client_secret = ClientSecret::new(
36 | env::var("LETTERBOXD_CLIENT_SECRET")
37 | .expect("Missing the LETTERBOXD_CLIENT_SECRET environment variable."),
38 | );
39 | // Letterboxd uses the Resource Owner flow and does not have an auth url
40 | let auth_url = AuthUrl::new("https://api.letterboxd.com/api/v0/auth/404".to_string())?;
41 | let token_url = TokenUrl::new("https://api.letterboxd.com/api/v0/auth/token".to_string())?;
42 |
43 | // Set up the config for the Letterboxd OAuth2 process.
44 | let client = BasicClient::new(letterboxd_client_id.clone())
45 | .set_client_secret(letterboxd_client_secret.clone())
46 | .set_auth_uri(auth_url)
47 | .set_token_uri(token_url);
48 |
49 | // Resource Owner flow uses username and password for authentication
50 | let letterboxd_username = ResourceOwnerUsername::new(
51 | env::var("LETTERBOXD_USERNAME")
52 | .expect("Missing the LETTERBOXD_USERNAME environment variable."),
53 | );
54 | let letterboxd_password = ResourceOwnerPassword::new(
55 | env::var("LETTERBOXD_PASSWORD")
56 | .expect("Missing the LETTERBOXD_PASSWORD environment variable."),
57 | );
58 |
59 | // All API requests must be signed as described at http://api-docs.letterboxd.com/#signing;
60 | // for that, we use a custom http client.
61 | let http_client = SigningHttpClient::new(letterboxd_client_id, letterboxd_client_secret);
62 |
63 | let token_result = client
64 | .set_auth_type(AuthType::RequestBody)
65 | .exchange_password(&letterboxd_username, &letterboxd_password)
66 | .request(&|request| http_client.execute(request))?;
67 |
68 | println!("{token_result:?}");
69 |
70 | Ok(())
71 | }
72 |
73 | /// Custom HTTP client which signs requests.
74 | ///
75 | /// See http://api-docs.letterboxd.com/#signing.
76 | #[derive(Debug, Clone)]
77 | struct SigningHttpClient {
78 | client_id: ClientId,
79 | client_secret: ClientSecret,
80 | inner: reqwest::blocking::Client,
81 | }
82 |
83 | impl SigningHttpClient {
84 | fn new(client_id: ClientId, client_secret: ClientSecret) -> Self {
85 | Self {
86 | client_id,
87 | client_secret,
88 | inner: reqwest::blocking::ClientBuilder::new()
89 | // Following redirects opens the client up to SSRF vulnerabilities.
90 | .redirect(reqwest::redirect::Policy::none())
91 | .build()
92 | .expect("Client should build"),
93 | }
94 | }
95 |
96 | /// Signs the request before calling `oauth2::reqwest::http_client`.
97 | fn execute(&self, mut request: HttpRequest) -> Result {
98 | let signed_url = self.sign_url(
99 | Url::parse(&request.uri().to_string()).expect("http::Uri should be a valid url::Url"),
100 | request.method(),
101 | request.body(),
102 | );
103 | *request.uri_mut() = signed_url
104 | .as_str()
105 | .try_into()
106 | .expect("url::Url should be a valid http::Uri");
107 | self.inner.call(request)
108 | }
109 |
110 | /// Signs the request based on a random and unique nonce, timestamp, and
111 | /// client id and secret.
112 | ///
113 | /// The client id, nonce, timestamp and signature are added to the url's
114 | /// query.
115 | ///
116 | /// See http://api-docs.letterboxd.com/#signing.
117 | fn sign_url(&self, mut url: Url, method: &http::method::Method, body: &[u8]) -> Url {
118 | let nonce = uuid::Uuid::new_v4(); // use UUID as random and unique nonce
119 |
120 | let timestamp = time::SystemTime::now()
121 | .duration_since(time::UNIX_EPOCH)
122 | .expect("SystemTime::duration_since failed")
123 | .as_secs();
124 |
125 | url.query_pairs_mut()
126 | .append_pair("apikey", &self.client_id)
127 | .append_pair("nonce", &format!("{}", nonce))
128 | .append_pair("timestamp", &format!("{}", timestamp));
129 |
130 | // create signature
131 | let mut hmac = Hmac::::new_from_slice(self.client_secret.secret().as_bytes())
132 | .expect("HMAC can take key of any size");
133 | hmac.update(method.as_str().as_bytes());
134 | hmac.update(&[b'\0']);
135 | hmac.update(url.as_str().as_bytes());
136 | hmac.update(&[b'\0']);
137 | hmac.update(body);
138 | let signature: String = hmac.finalize().into_bytes().encode_hex();
139 |
140 | url.query_pairs_mut().append_pair("signature", &signature);
141 |
142 | url
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/examples/google.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Google OAuth2 process for requesting access to the Google Calendar features
3 | //! and the user's profile.
4 | //!
5 | //! Before running it, you'll need to generate your own Google OAuth2 credentials.
6 | //!
7 | //! In order to run the example call:
8 | //!
9 | //! ```sh
10 | //! GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy cargo run --example google
11 | //! ```
12 | //!
13 | //! ...and follow the instructions.
14 | //!
15 |
16 | use oauth2::reqwest;
17 | use oauth2::{basic::BasicClient, StandardRevocableToken, TokenResponse};
18 | use oauth2::{
19 | AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl,
20 | RevocationUrl, Scope, TokenUrl,
21 | };
22 | use url::Url;
23 |
24 | use std::env;
25 | use std::io::{BufRead, BufReader, Write};
26 | use std::net::TcpListener;
27 |
28 | fn main() {
29 | let google_client_id = ClientId::new(
30 | env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."),
31 | );
32 | let google_client_secret = ClientSecret::new(
33 | env::var("GOOGLE_CLIENT_SECRET")
34 | .expect("Missing the GOOGLE_CLIENT_SECRET environment variable."),
35 | );
36 | let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string())
37 | .expect("Invalid authorization endpoint URL");
38 | let token_url = TokenUrl::new("https://www.googleapis.com/oauth2/v3/token".to_string())
39 | .expect("Invalid token endpoint URL");
40 |
41 | // Set up the config for the Google OAuth2 process.
42 | let client = BasicClient::new(google_client_id)
43 | .set_client_secret(google_client_secret)
44 | .set_auth_uri(auth_url)
45 | .set_token_uri(token_url)
46 | // This example will be running its own server at localhost:8080.
47 | // See below for the server implementation.
48 | .set_redirect_uri(
49 | RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
50 | )
51 | // Google supports OAuth 2.0 Token Revocation (RFC-7009)
52 | .set_revocation_url(
53 | RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string())
54 | .expect("Invalid revocation endpoint URL"),
55 | );
56 |
57 | let http_client = reqwest::blocking::ClientBuilder::new()
58 | // Following redirects opens the client up to SSRF vulnerabilities.
59 | .redirect(reqwest::redirect::Policy::none())
60 | .build()
61 | .expect("Client should build");
62 |
63 | // Google supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/).
64 | // Create a PKCE code verifier and SHA-256 encode it as a code challenge.
65 | let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
66 |
67 | // Generate the authorization URL to which we'll redirect the user.
68 | let (authorize_url, csrf_state) = client
69 | .authorize_url(CsrfToken::new_random)
70 | // This example is requesting access to the "calendar" features and the user's profile.
71 | .add_scope(Scope::new(
72 | "https://www.googleapis.com/auth/calendar".to_string(),
73 | ))
74 | .add_scope(Scope::new(
75 | "https://www.googleapis.com/auth/plus.me".to_string(),
76 | ))
77 | .set_pkce_challenge(pkce_code_challenge)
78 | .url();
79 |
80 | println!("Open this URL in your browser:\n{authorize_url}\n");
81 |
82 | let (code, state) = {
83 | // A very naive implementation of the redirect server.
84 | let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
85 |
86 | // The server will terminate itself after collecting the first code.
87 | let Some(mut stream) = listener.incoming().flatten().next() else {
88 | panic!("listener terminated without accepting a connection");
89 | };
90 |
91 | let mut reader = BufReader::new(&stream);
92 |
93 | let mut request_line = String::new();
94 | reader.read_line(&mut request_line).unwrap();
95 |
96 | let redirect_url = request_line.split_whitespace().nth(1).unwrap();
97 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
98 |
99 | let code = url
100 | .query_pairs()
101 | .find(|(key, _)| key == "code")
102 | .map(|(_, code)| AuthorizationCode::new(code.into_owned()))
103 | .unwrap();
104 |
105 | let state = url
106 | .query_pairs()
107 | .find(|(key, _)| key == "state")
108 | .map(|(_, state)| CsrfToken::new(state.into_owned()))
109 | .unwrap();
110 |
111 | let message = "Go back to your terminal :)";
112 | let response = format!(
113 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
114 | message.len(),
115 | message
116 | );
117 | stream.write_all(response.as_bytes()).unwrap();
118 |
119 | (code, state)
120 | };
121 |
122 | println!("Google returned the following code:\n{}\n", code.secret());
123 | println!(
124 | "Google returned the following state:\n{} (expected `{}`)\n",
125 | state.secret(),
126 | csrf_state.secret()
127 | );
128 |
129 | // Exchange the code with a token.
130 | let token_response = client
131 | .exchange_code(code)
132 | .set_pkce_verifier(pkce_code_verifier)
133 | .request(&http_client);
134 |
135 | println!("Google returned the following token:\n{token_response:?}\n");
136 |
137 | // Revoke the obtained token
138 | let token_response = token_response.unwrap();
139 | let token_to_revoke: StandardRevocableToken = match token_response.refresh_token() {
140 | Some(token) => token.into(),
141 | None => token_response.access_token().into(),
142 | };
143 |
144 | client
145 | .revoke_token(token_to_revoke)
146 | .unwrap()
147 | .request(&http_client)
148 | .expect("Failed to revoke token");
149 | }
150 |
--------------------------------------------------------------------------------
/examples/msgraph.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Microsoft Graph OAuth2 process for requesting access to Microsoft
3 | //! services using PKCE.
4 | //!
5 | //! Before running it, you'll need to generate your own Microsoft OAuth2 credentials. See
6 | //! https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app
7 | //! * Register a `Web` application with a `Redirect URI` of `http://localhost:3003/redirect`.
8 | //! * In the left menu select `Overview`. Copy the `Application (client) ID` as the MSGRAPH_CLIENT_ID.
9 | //! * In the left menu select `Certificates & secrets` and add a new client secret. Copy the secret value
10 | //! as MSGRAPH_CLIENT_SECRET.
11 | //! * In the left menu select `API permissions` and add a permission. Select Microsoft Graph and
12 | //! `Delegated permissions`. Add the `Files.Read` permission.
13 | //!
14 | //! In order to run the example call:
15 | //!
16 | //! ```sh
17 | //! MSGRAPH_CLIENT_ID=xxx MSGRAPH_CLIENT_SECRET=yyy cargo run --example msgraph
18 | //! ```
19 | //!
20 | //! ...and follow the instructions.
21 | //!
22 |
23 | use oauth2::basic::BasicClient;
24 | use oauth2::reqwest;
25 | use oauth2::{
26 | AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge,
27 | RedirectUrl, Scope, TokenUrl,
28 | };
29 | use url::Url;
30 |
31 | use std::env;
32 | use std::io::{BufRead, BufReader, Write};
33 | use std::net::TcpListener;
34 |
35 | fn main() {
36 | let graph_client_id = ClientId::new(
37 | env::var("MSGRAPH_CLIENT_ID").expect("Missing the MSGRAPH_CLIENT_ID environment variable."),
38 | );
39 | let graph_client_secret = ClientSecret::new(
40 | env::var("MSGRAPH_CLIENT_SECRET")
41 | .expect("Missing the MSGRAPH_CLIENT_SECRET environment variable."),
42 | );
43 | let auth_url =
44 | AuthUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string())
45 | .expect("Invalid authorization endpoint URL");
46 | let token_url =
47 | TokenUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string())
48 | .expect("Invalid token endpoint URL");
49 |
50 | // Set up the config for the Microsoft Graph OAuth2 process.
51 | let client = BasicClient::new(graph_client_id)
52 | .set_client_secret(graph_client_secret)
53 | .set_auth_uri(auth_url)
54 | .set_token_uri(token_url)
55 | // Microsoft Graph requires client_id and client_secret in URL rather than
56 | // using Basic authentication.
57 | .set_auth_type(AuthType::RequestBody)
58 | // This example will be running its own server at localhost:3003.
59 | // See below for the server implementation.
60 | .set_redirect_uri(
61 | RedirectUrl::new("http://localhost:3003/redirect".to_string())
62 | .expect("Invalid redirect URL"),
63 | );
64 |
65 | let http_client = reqwest::blocking::ClientBuilder::new()
66 | // Following redirects opens the client up to SSRF vulnerabilities.
67 | .redirect(reqwest::redirect::Policy::none())
68 | .build()
69 | .expect("Client should build");
70 |
71 | // Microsoft Graph supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/).
72 | // Create a PKCE code verifier and SHA-256 encode it as a code challenge.
73 | let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
74 |
75 | // Generate the authorization URL to which we'll redirect the user.
76 | let (authorize_url, csrf_state) = client
77 | .authorize_url(CsrfToken::new_random)
78 | // This example requests read access to OneDrive.
79 | .add_scope(Scope::new(
80 | "https://graph.microsoft.com/Files.Read".to_string(),
81 | ))
82 | .set_pkce_challenge(pkce_code_challenge)
83 | .url();
84 |
85 | println!("Open this URL in your browser:\n{authorize_url}\n");
86 |
87 | let (code, state) = {
88 | // A very naive implementation of the redirect server.
89 | let listener = TcpListener::bind("127.0.0.1:3003").unwrap();
90 |
91 | // The server will terminate itself after collecting the first code.
92 | let Some(mut stream) = listener.incoming().flatten().next() else {
93 | panic!("listener terminated without accepting a connection");
94 | };
95 |
96 | let mut reader = BufReader::new(&stream);
97 |
98 | let mut request_line = String::new();
99 | reader.read_line(&mut request_line).unwrap();
100 |
101 | let redirect_url = request_line.split_whitespace().nth(1).unwrap();
102 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
103 |
104 | let code = url
105 | .query_pairs()
106 | .find(|(key, _)| key == "code")
107 | .map(|(_, code)| AuthorizationCode::new(code.into_owned()))
108 | .unwrap();
109 |
110 | let state = url
111 | .query_pairs()
112 | .find(|(key, _)| key == "state")
113 | .map(|(_, state)| CsrfToken::new(state.into_owned()))
114 | .unwrap();
115 |
116 | let message = "Go back to your terminal :)";
117 | let response = format!(
118 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
119 | message.len(),
120 | message
121 | );
122 | stream.write_all(response.as_bytes()).unwrap();
123 |
124 | (code, state)
125 | };
126 |
127 | println!("MS Graph returned the following code:\n{}\n", code.secret());
128 | println!(
129 | "MS Graph returned the following state:\n{} (expected `{}`)\n",
130 | state.secret(),
131 | csrf_state.secret()
132 | );
133 |
134 | // Exchange the code with a token.
135 | let token = client
136 | .exchange_code(code)
137 | // Send the PKCE code verifier in the token request
138 | .set_pkce_verifier(pkce_code_verifier)
139 | .request(&http_client);
140 |
141 | println!("MS Graph returned the following token:\n{token:?}\n");
142 | }
143 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use serde::de::DeserializeOwned;
2 | use serde::{Deserialize, Serialize};
3 |
4 | use std::error::Error;
5 | use std::fmt::{Debug, Display, Formatter};
6 |
7 | /// Server Error Response
8 | ///
9 | /// See [Section 5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) of RFC 6749.
10 | /// This trait exists separately from the `StandardErrorResponse` struct
11 | /// to support customization by clients, such as supporting interoperability with
12 | /// non-standards-complaint OAuth2 providers.
13 | ///
14 | /// The [`Display`] trait implementation for types implementing [`ErrorResponse`] should be a
15 | /// human-readable string suitable for printing (e.g., within a [`RequestTokenError`]).
16 | pub trait ErrorResponse: Debug + Display + DeserializeOwned + Serialize {}
17 |
18 | /// Error types enum.
19 | ///
20 | /// NOTE: The serialization must return the `snake_case` representation of
21 | /// this error type. This value must match the error type from the relevant OAuth 2.0 standards
22 | /// (RFC 6749 or an extension).
23 | pub trait ErrorResponseType: Debug + DeserializeOwned + Serialize {}
24 |
25 | /// Error response returned by server after requesting an access token.
26 | ///
27 | /// The fields in this structure are defined in
28 | /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2). This
29 | /// trait is parameterized by a `ErrorResponseType` to support error types specific to future OAuth2
30 | /// authentication schemes and extensions.
31 | #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
32 | pub struct StandardErrorResponse {
33 | #[serde(bound = "T: ErrorResponseType")]
34 | pub(crate) error: T,
35 | #[serde(default)]
36 | #[serde(skip_serializing_if = "Option::is_none")]
37 | pub(crate) error_description: Option,
38 | #[serde(default)]
39 | #[serde(skip_serializing_if = "Option::is_none")]
40 | pub(crate) error_uri: Option,
41 | }
42 |
43 | impl StandardErrorResponse {
44 | /// Instantiate a new `ErrorResponse`.
45 | ///
46 | /// # Arguments
47 | ///
48 | /// * `error` - REQUIRED. A single ASCII error code deserialized to the generic parameter.
49 | /// `ErrorResponseType`.
50 | /// * `error_description` - OPTIONAL. Human-readable ASCII text providing additional
51 | /// information, used to assist the client developer in understanding the error that
52 | /// occurred. Values for this parameter MUST NOT include characters outside the set
53 | /// `%x20-21 / %x23-5B / %x5D-7E`.
54 | /// * `error_uri` - OPTIONAL. A URI identifying a human-readable web page with information
55 | /// about the error used to provide the client developer with additional information about
56 | /// the error. Values for the "error_uri" parameter MUST conform to the URI-reference
57 | /// syntax and thus MUST NOT include characters outside the set `%x21 / %x23-5B / %x5D-7E`.
58 | pub fn new(error: T, error_description: Option, error_uri: Option) -> Self {
59 | Self {
60 | error,
61 | error_description,
62 | error_uri,
63 | }
64 | }
65 |
66 | /// REQUIRED. A single ASCII error code deserialized to the generic parameter
67 | /// `ErrorResponseType`.
68 | pub fn error(&self) -> &T {
69 | &self.error
70 | }
71 | /// OPTIONAL. Human-readable ASCII text providing additional information, used to assist
72 | /// the client developer in understanding the error that occurred. Values for this
73 | /// parameter MUST NOT include characters outside the set `%x20-21 / %x23-5B / %x5D-7E`.
74 | pub fn error_description(&self) -> Option<&String> {
75 | self.error_description.as_ref()
76 | }
77 | /// OPTIONAL. URI identifying a human-readable web page with information about the error,
78 | /// used to provide the client developer with additional information about the error.
79 | /// Values for the "error_uri" parameter MUST conform to the URI-reference syntax and
80 | /// thus MUST NOT include characters outside the set `%x21 / %x23-5B / %x5D-7E`.
81 | pub fn error_uri(&self) -> Option<&String> {
82 | self.error_uri.as_ref()
83 | }
84 | }
85 |
86 | impl ErrorResponse for StandardErrorResponse where T: ErrorResponseType + Display + 'static {}
87 |
88 | impl Display for StandardErrorResponse
89 | where
90 | TE: ErrorResponseType + Display,
91 | {
92 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
93 | let mut formatted = self.error().to_string();
94 |
95 | if let Some(error_description) = self.error_description() {
96 | formatted.push_str(": ");
97 | formatted.push_str(error_description);
98 | }
99 |
100 | if let Some(error_uri) = self.error_uri() {
101 | formatted.push_str(" (see ");
102 | formatted.push_str(error_uri);
103 | formatted.push(')');
104 | }
105 |
106 | write!(f, "{formatted}")
107 | }
108 | }
109 |
110 | /// Error encountered while requesting access token.
111 | #[derive(Debug, thiserror::Error)]
112 | pub enum RequestTokenError
113 | where
114 | RE: Error + 'static,
115 | T: ErrorResponse + 'static,
116 | {
117 | /// Error response returned by authorization server. Contains the parsed `ErrorResponse`
118 | /// returned by the server.
119 | #[error("Server returned error response: {0}")]
120 | ServerResponse(T),
121 | /// An error occurred while sending the request or receiving the response (e.g., network
122 | /// connectivity failed).
123 | #[error("Request failed")]
124 | Request(#[from] RE),
125 | /// Failed to parse server response. Parse errors may occur while parsing either successful
126 | /// or error responses.
127 | #[error("Failed to parse server response")]
128 | Parse(
129 | #[source] serde_path_to_error::Error,
130 | Vec,
131 | ),
132 | /// Some other type of error occurred (e.g., an unexpected server response).
133 | #[error("Other error: {}", _0)]
134 | Other(String),
135 | }
136 |
137 | #[cfg(test)]
138 | mod tests {
139 | use crate::basic::{BasicErrorResponse, BasicErrorResponseType};
140 |
141 | #[test]
142 | fn test_error_response_serializer() {
143 | assert_eq!(
144 | "{\"error\":\"unauthorized_client\"}",
145 | serde_json::to_string(&BasicErrorResponse::new(
146 | BasicErrorResponseType::UnauthorizedClient,
147 | None,
148 | None,
149 | ))
150 | .unwrap(),
151 | );
152 |
153 | assert_eq!(
154 | "{\
155 | \"error\":\"invalid_client\",\
156 | \"error_description\":\"Invalid client_id\",\
157 | \"error_uri\":\"https://example.com/errors/invalid_client\"\
158 | }",
159 | serde_json::to_string(&BasicErrorResponse::new(
160 | BasicErrorResponseType::InvalidClient,
161 | Some("Invalid client_id".to_string()),
162 | Some("https://example.com/errors/invalid_client".to_string()),
163 | ))
164 | .unwrap(),
165 | );
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/basic.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | revocation::{RevocationErrorResponseType, StandardRevocableToken},
3 | Client, EmptyExtraTokenFields, EndpointNotSet, ErrorResponseType, RequestTokenError,
4 | StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType,
5 | };
6 |
7 | use std::fmt::Error as FormatterError;
8 | use std::fmt::{Debug, Display, Formatter};
9 |
10 | /// Basic OAuth2 client specialization, suitable for most applications.
11 | pub type BasicClient<
12 | HasAuthUrl = EndpointNotSet,
13 | HasDeviceAuthUrl = EndpointNotSet,
14 | HasIntrospectionUrl = EndpointNotSet,
15 | HasRevocationUrl = EndpointNotSet,
16 | HasTokenUrl = EndpointNotSet,
17 | > = Client<
18 | BasicErrorResponse,
19 | BasicTokenResponse,
20 | BasicTokenIntrospectionResponse,
21 | StandardRevocableToken,
22 | BasicRevocationErrorResponse,
23 | HasAuthUrl,
24 | HasDeviceAuthUrl,
25 | HasIntrospectionUrl,
26 | HasRevocationUrl,
27 | HasTokenUrl,
28 | >;
29 |
30 | /// Basic OAuth2 authorization token types.
31 | #[derive(Clone, Debug, PartialEq, Eq)]
32 | pub enum BasicTokenType {
33 | /// Bearer token
34 | /// ([OAuth 2.0 Bearer Tokens - RFC 6750](https://tools.ietf.org/html/rfc6750)).
35 | Bearer,
36 | /// MAC ([OAuth 2.0 Message Authentication Code (MAC)
37 | /// Tokens](https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-05)).
38 | Mac,
39 | /// An extension not defined by RFC 6749.
40 | Extension(String),
41 | }
42 | impl BasicTokenType {
43 | fn from_str(s: &str) -> Self {
44 | match s {
45 | "bearer" => BasicTokenType::Bearer,
46 | "mac" => BasicTokenType::Mac,
47 | ext => BasicTokenType::Extension(ext.to_string()),
48 | }
49 | }
50 | }
51 | impl AsRef for BasicTokenType {
52 | fn as_ref(&self) -> &str {
53 | match *self {
54 | BasicTokenType::Bearer => "bearer",
55 | BasicTokenType::Mac => "mac",
56 | BasicTokenType::Extension(ref ext) => ext.as_str(),
57 | }
58 | }
59 | }
60 | impl<'de> serde::Deserialize<'de> for BasicTokenType {
61 | fn deserialize(deserializer: D) -> Result
62 | where
63 | D: serde::de::Deserializer<'de>,
64 | {
65 | let variant_str = String::deserialize(deserializer)?;
66 | Ok(Self::from_str(&variant_str))
67 | }
68 | }
69 | impl serde::ser::Serialize for BasicTokenType {
70 | fn serialize(&self, serializer: S) -> Result
71 | where
72 | S: serde::ser::Serializer,
73 | {
74 | serializer.serialize_str(self.as_ref())
75 | }
76 | }
77 | impl TokenType for BasicTokenType {}
78 |
79 | /// Basic OAuth2 token response.
80 | pub type BasicTokenResponse = StandardTokenResponse;
81 |
82 | /// Basic OAuth2 token introspection response.
83 | pub type BasicTokenIntrospectionResponse =
84 | StandardTokenIntrospectionResponse;
85 |
86 | /// Basic access token error types.
87 | ///
88 | /// These error types are defined in
89 | /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2).
90 | #[derive(Clone, PartialEq, Eq)]
91 | pub enum BasicErrorResponseType {
92 | /// Client authentication failed (e.g., unknown client, no client authentication included,
93 | /// or unsupported authentication method).
94 | InvalidClient,
95 | /// The provided authorization grant (e.g., authorization code, resource owner credentials)
96 | /// or refresh token is invalid, expired, revoked, does not match the redirection URI used
97 | /// in the authorization request, or was issued to another client.
98 | InvalidGrant,
99 | /// The request is missing a required parameter, includes an unsupported parameter value
100 | /// (other than grant type), repeats a parameter, includes multiple credentials, utilizes
101 | /// more than one mechanism for authenticating the client, or is otherwise malformed.
102 | InvalidRequest,
103 | /// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the
104 | /// resource owner.
105 | InvalidScope,
106 | /// The authenticated client is not authorized to use this authorization grant type.
107 | UnauthorizedClient,
108 | /// The authorization grant type is not supported by the authorization server.
109 | UnsupportedGrantType,
110 | /// An extension not defined by RFC 6749.
111 | Extension(String),
112 | }
113 | impl BasicErrorResponseType {
114 | pub(crate) fn from_str(s: &str) -> Self {
115 | match s {
116 | "invalid_client" => BasicErrorResponseType::InvalidClient,
117 | "invalid_grant" => BasicErrorResponseType::InvalidGrant,
118 | "invalid_request" => BasicErrorResponseType::InvalidRequest,
119 | "invalid_scope" => BasicErrorResponseType::InvalidScope,
120 | "unauthorized_client" => BasicErrorResponseType::UnauthorizedClient,
121 | "unsupported_grant_type" => BasicErrorResponseType::UnsupportedGrantType,
122 | ext => BasicErrorResponseType::Extension(ext.to_string()),
123 | }
124 | }
125 | }
126 | impl AsRef for BasicErrorResponseType {
127 | fn as_ref(&self) -> &str {
128 | match *self {
129 | BasicErrorResponseType::InvalidClient => "invalid_client",
130 | BasicErrorResponseType::InvalidGrant => "invalid_grant",
131 | BasicErrorResponseType::InvalidRequest => "invalid_request",
132 | BasicErrorResponseType::InvalidScope => "invalid_scope",
133 | BasicErrorResponseType::UnauthorizedClient => "unauthorized_client",
134 | BasicErrorResponseType::UnsupportedGrantType => "unsupported_grant_type",
135 | BasicErrorResponseType::Extension(ref ext) => ext.as_str(),
136 | }
137 | }
138 | }
139 | impl<'de> serde::Deserialize<'de> for BasicErrorResponseType {
140 | fn deserialize(deserializer: D) -> Result
141 | where
142 | D: serde::de::Deserializer<'de>,
143 | {
144 | let variant_str = String::deserialize(deserializer)?;
145 | Ok(Self::from_str(&variant_str))
146 | }
147 | }
148 | impl serde::ser::Serialize for BasicErrorResponseType {
149 | fn serialize(&self, serializer: S) -> Result
150 | where
151 | S: serde::ser::Serializer,
152 | {
153 | serializer.serialize_str(self.as_ref())
154 | }
155 | }
156 | impl ErrorResponseType for BasicErrorResponseType {}
157 | impl Debug for BasicErrorResponseType {
158 | fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> {
159 | Display::fmt(self, f)
160 | }
161 | }
162 |
163 | impl Display for BasicErrorResponseType {
164 | fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> {
165 | write!(f, "{}", self.as_ref())
166 | }
167 | }
168 |
169 | /// Error response specialization for basic OAuth2 implementation.
170 | pub type BasicErrorResponse = StandardErrorResponse;
171 |
172 | /// Token error specialization for basic OAuth2 implementation.
173 | pub type BasicRequestTokenError = RequestTokenError;
174 |
175 | /// Revocation error response specialization for basic OAuth2 implementation.
176 | pub type BasicRevocationErrorResponse = StandardErrorResponse;
177 |
--------------------------------------------------------------------------------
/examples/wunderlist.rs:
--------------------------------------------------------------------------------
1 | //!
2 | //! This example showcases the Wunderlist OAuth2 process for requesting access to the user's todo lists.
3 | //! Wunderlist does not implement the correct token response, so this serves as an example of how to
4 | //! implement a custom client.
5 | //!
6 | //! Before running it, you'll need to create your own wunderlist app.
7 | //!
8 | //! In order to run the example call:
9 | //!
10 | //! ```sh
11 | //! WUNDER_CLIENT_ID=xxx WUNDER_CLIENT_SECRET=yyy cargo run --example wunderlist
12 | //! ```
13 | //!
14 | //! ...and follow the instructions.
15 | //!
16 |
17 | use oauth2::basic::{
18 | BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
19 | BasicTokenType,
20 | };
21 | use oauth2::reqwest;
22 | use oauth2::StandardRevocableToken;
23 | use oauth2::{
24 | AccessToken, AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken,
25 | EmptyExtraTokenFields, EndpointNotSet, ExtraTokenFields, RedirectUrl, RefreshToken, Scope,
26 | TokenResponse, TokenUrl,
27 | };
28 | use serde::{Deserialize, Serialize};
29 | use url::Url;
30 |
31 | use std::env;
32 | use std::io::{BufRead, BufReader, Write};
33 | use std::net::TcpListener;
34 | use std::time::Duration;
35 |
36 | type SpecialTokenResponse = NonStandardTokenResponse;
37 | type SpecialClient<
38 | HasAuthUrl = EndpointNotSet,
39 | HasDeviceAuthUrl = EndpointNotSet,
40 | HasIntrospectionUrl = EndpointNotSet,
41 | HasRevocationUrl = EndpointNotSet,
42 | HasTokenUrl = EndpointNotSet,
43 | > = Client<
44 | BasicErrorResponse,
45 | SpecialTokenResponse,
46 | BasicTokenIntrospectionResponse,
47 | StandardRevocableToken,
48 | BasicRevocationErrorResponse,
49 | HasAuthUrl,
50 | HasDeviceAuthUrl,
51 | HasIntrospectionUrl,
52 | HasRevocationUrl,
53 | HasTokenUrl,
54 | >;
55 |
56 | fn default_token_type() -> Option {
57 | Some(BasicTokenType::Bearer)
58 | }
59 |
60 | /// Non Standard OAuth2 token response.
61 | ///
62 | /// This struct includes the fields defined in
63 | /// [Section 5.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.1), as well as
64 | /// extensions defined by the `EF` type parameter.
65 | /// In this particular example token_type is optional to showcase how to deal with a non
66 | /// compliant provider.
67 | #[derive(Clone, Debug, Deserialize, Serialize)]
68 | pub struct NonStandardTokenResponse {
69 | access_token: AccessToken,
70 | // In this example wunderlist does not follow the RFC specs and don't return the
71 | // token_type. `NonStandardTokenResponse` makes the `token_type` optional.
72 | #[serde(default = "default_token_type")]
73 | token_type: Option,
74 | #[serde(skip_serializing_if = "Option::is_none")]
75 | expires_in: Option,
76 | #[serde(skip_serializing_if = "Option::is_none")]
77 | refresh_token: Option,
78 | #[serde(rename = "scope")]
79 | #[serde(deserialize_with = "oauth2::helpers::deserialize_space_delimited_vec")]
80 | #[serde(serialize_with = "oauth2::helpers::serialize_space_delimited_vec")]
81 | #[serde(skip_serializing_if = "Option::is_none")]
82 | #[serde(default)]
83 | scopes: Option>,
84 |
85 | #[serde(bound = "EF: ExtraTokenFields")]
86 | #[serde(flatten)]
87 | extra_fields: EF,
88 | }
89 |
90 | impl TokenResponse for NonStandardTokenResponse
91 | where
92 | EF: ExtraTokenFields,
93 | {
94 | type TokenType = BasicTokenType;
95 | /// REQUIRED. The access token issued by the authorization server.
96 | fn access_token(&self) -> &AccessToken {
97 | &self.access_token
98 | }
99 | /// REQUIRED. The type of the token issued as described in
100 | /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1).
101 | /// Value is case insensitive and deserialized to the generic `TokenType` parameter.
102 | /// But in this particular case as the service is non compliant, it has a default value
103 | fn token_type(&self) -> &BasicTokenType {
104 | match &self.token_type {
105 | Some(t) => t,
106 | None => &BasicTokenType::Bearer,
107 | }
108 | }
109 | /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600
110 | /// denotes that the access token will expire in one hour from the time the response was
111 | /// generated. If omitted, the authorization server SHOULD provide the expiration time via
112 | /// other means or document the default value.
113 | fn expires_in(&self) -> Option {
114 | self.expires_in.map(Duration::from_secs)
115 | }
116 | /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same
117 | /// authorization grant as described in
118 | /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6).
119 | fn refresh_token(&self) -> Option<&RefreshToken> {
120 | self.refresh_token.as_ref()
121 | }
122 | /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The
123 | /// scope of the access token as described by
124 | /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response,
125 | /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from
126 | /// the response, this field is `None`.
127 | fn scopes(&self) -> Option<&Vec> {
128 | self.scopes.as_ref()
129 | }
130 | }
131 |
132 | fn main() {
133 | let client_id_str = env::var("WUNDERLIST_CLIENT_ID")
134 | .expect("Missing the WUNDERLIST_CLIENT_ID environment variable.");
135 |
136 | let client_secret_str = env::var("WUNDERLIST_CLIENT_SECRET")
137 | .expect("Missing the WUNDERLIST_CLIENT_SECRET environment variable.");
138 |
139 | let wunder_client_id = ClientId::new(client_id_str.clone());
140 | let wunderlist_client_secret = ClientSecret::new(client_secret_str.clone());
141 | let auth_url = AuthUrl::new("https://www.wunderlist.com/oauth/authorize".to_string())
142 | .expect("Invalid authorization endpoint URL");
143 | let token_url = TokenUrl::new("https://www.wunderlist.com/oauth/access_token".to_string())
144 | .expect("Invalid token endpoint URL");
145 |
146 | // Set up the config for the Wunderlist OAuth2 process.
147 | let client = SpecialClient::new(wunder_client_id)
148 | .set_client_secret(wunderlist_client_secret)
149 | .set_auth_uri(auth_url)
150 | .set_token_uri(token_url)
151 | // This example will be running its own server at localhost:8080.
152 | // See below for the server implementation.
153 | .set_redirect_uri(
154 | RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
155 | );
156 |
157 | let http_client = reqwest::blocking::ClientBuilder::new()
158 | // Following redirects opens the client up to SSRF vulnerabilities.
159 | .redirect(reqwest::redirect::Policy::none())
160 | .build()
161 | .expect("Client should build");
162 |
163 | // Generate the authorization URL to which we'll redirect the user.
164 | let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random).url();
165 |
166 | println!("Open this URL in your browser:\n{authorize_url}\n");
167 |
168 | let (code, state) = {
169 | // A very naive implementation of the redirect server.
170 | let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
171 |
172 | // The server will terminate itself after collecting the first code.
173 | let Some(mut stream) = listener.incoming().flatten().next() else {
174 | panic!("listener terminated without accepting a connection");
175 | };
176 |
177 | let mut reader = BufReader::new(&stream);
178 |
179 | let mut request_line = String::new();
180 | reader.read_line(&mut request_line).unwrap();
181 |
182 | let redirect_url = request_line.split_whitespace().nth(1).unwrap();
183 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
184 |
185 | let code = url
186 | .query_pairs()
187 | .find(|(key, _)| key == "code")
188 | .map(|(_, code)| AuthorizationCode::new(code.into_owned()))
189 | .unwrap();
190 |
191 | let state = url
192 | .query_pairs()
193 | .find(|(key, _)| key == "state")
194 | .map(|(_, state)| CsrfToken::new(state.into_owned()))
195 | .unwrap();
196 |
197 | let message = "Go back to your terminal :)";
198 | let response = format!(
199 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
200 | message.len(),
201 | message
202 | );
203 | stream.write_all(response.as_bytes()).unwrap();
204 |
205 | (code, state)
206 | };
207 |
208 | println!(
209 | "Wunderlist returned the following code:\n{}\n",
210 | code.secret()
211 | );
212 | println!(
213 | "Wunderlist returned the following state:\n{} (expected `{}`)\n",
214 | state.secret(),
215 | csrf_state.secret()
216 | );
217 |
218 | // Exchange the code with a token.
219 | let token_res = client
220 | .exchange_code(code)
221 | .add_extra_param("client_id", client_id_str)
222 | .add_extra_param("client_secret", client_secret_str)
223 | .request(&http_client);
224 |
225 | println!("Wunderlist returned the following token:\n{token_res:?}\n");
226 | }
227 |
--------------------------------------------------------------------------------
/src/endpoint.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | AuthType, ClientId, ClientSecret, ErrorResponse, RedirectUrl, RequestTokenError, Scope,
3 | CONTENT_TYPE_FORMENCODED, CONTENT_TYPE_JSON,
4 | };
5 |
6 | use base64::prelude::*;
7 | use http::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
8 | use http::{HeaderValue, StatusCode};
9 | use serde::de::DeserializeOwned;
10 | use url::{form_urlencoded, Url};
11 |
12 | use std::borrow::Cow;
13 | use std::error::Error;
14 | use std::future::Future;
15 |
16 | /// An HTTP request.
17 | pub type HttpRequest = http::Request>;
18 |
19 | /// An HTTP response.
20 | pub type HttpResponse = http::Response>;
21 |
22 | /// An asynchronous (future-based) HTTP client.
23 | pub trait AsyncHttpClient<'c> {
24 | /// Error type returned by HTTP client.
25 | type Error: Error + 'static;
26 |
27 | /// Future type returned by HTTP client.
28 | type Future: Future