├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── .gitignore ├── Cargo.toml └── src │ ├── bin │ ├── google.rs │ ├── msgraph.rs │ ├── spotify.rs │ └── twitch.rs │ └── lib.rs └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '50 5 * * 4' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | msrv: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@1.82 21 | - run: cargo build --workspace --lib 22 | 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dtolnay/rust-toolchain@stable 28 | - run: cargo test --workspace --all-targets 29 | - run: cargo test --workspace --doc 30 | 31 | clippy: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@stable 36 | with: 37 | components: clippy 38 | - run: cargo clippy --workspace --all-features --all-targets -- -D warnings 39 | 40 | rustfmt: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: dtolnay/rust-toolchain@stable 45 | with: 46 | components: rustfmt 47 | - run: cargo fmt --check --all 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-oauth2" 3 | version = "0.5.0" 4 | authors = [ 5 | "Alex Crichton ", 6 | "Florin Lipan ", 7 | "David A. Ramos ", 8 | "John-John Tedro " 9 | ] 10 | edition = "2021" 11 | rust-version = "1.82" 12 | description = "An asynchronous OAuth2 flow implementation." 13 | documentation = "https://docs.rs/async-oauth2" 14 | readme = "README.md" 15 | homepage = "https://github.com/udoprog/async-oauth2" 16 | repository = "https://github.com/udoprog/async-oauth2" 17 | license = "MIT OR Apache-2.0" 18 | keywords = ["auth", "oauth2"] 19 | categories = ["authentication", "web-programming"] 20 | 21 | [lib] 22 | name = "oauth2" 23 | path = "src/lib.rs" 24 | 25 | [dependencies] 26 | base64 = "0.22.0" 27 | rand = "0.8.5" 28 | serde = { version = "1.0.197", features = ["derive"] } 29 | serde_json = "1.0.115" 30 | serde-aux = "4.5.0" 31 | sha2 = "0.10.6" 32 | url = "2.5.0" 33 | reqwest = "0.12.0" 34 | thiserror = "1.0.39" 35 | http = "1.1.0" 36 | bytes = "1.6.0" 37 | 38 | [dev-dependencies] 39 | tokio = { version = "1.26.0", features = ["full"] } 40 | 41 | [workspace] 42 | members = ["examples"] 43 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /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 | # async-oauth2 2 | 3 | [github](https://github.com/udoprog/async-oauth2) 4 | [crates.io](https://crates.io/crates/async-oauth2) 5 | [docs.rs](https://docs.rs/async-oauth2) 6 | [build status](https://github.com/udoprog/async-oauth2/actions?query=branch%3Amain) 7 | 8 | An asynchronous OAuth2 flow implementation, trying to adhere as much as 9 | possible to [RFC 6749]. 10 | 11 |
12 | 13 | ## Examples 14 | 15 | To see the library in action, you can go to one of our examples: 16 | 17 | - [Google] 18 | - [Spotify] 19 | - [Twitch] 20 | 21 | If you've checked out the project they can be run like this: 22 | 23 | ```sh 24 | cargo run --manifest-path=examples/Cargo.toml --bin spotify -- 25 | --client-id --client-secret 26 | cargo run --manifest-path=examples/Cargo.toml --bin google -- 27 | --client-id --client-secret 28 | cargo run --manifest-path=examples/Cargo.toml --bin twitch -- 29 | --client-id --client-secret 30 | ``` 31 | 32 | > Note: You need to configure your client integration to permit redirects to 33 | > `http://localhost:8080/api/auth/redirect` for these to work. How this is 34 | > done depends on the integration used. 35 | 36 |
37 | 38 | ## Authorization Code Grant 39 | 40 | This is the most common OAuth2 flow. 41 | 42 | ```rust 43 | use oauth2::*; 44 | use url::Url; 45 | 46 | pub struct ReceivedCode { 47 | pub code: AuthorizationCode, 48 | pub state: State, 49 | } 50 | 51 | let reqwest_client = reqwest::Client::new(); 52 | 53 | // Create an OAuth2 client by specifying the client ID, client secret, 54 | // authorization URL and token URL. 55 | let mut client = Client::new( 56 | "client_id", 57 | Url::parse("http://authorize")?, 58 | Url::parse("http://token")? 59 | ); 60 | 61 | client.set_client_secret("client_secret"); 62 | // Set the URL the user will be redirected to after the authorization 63 | // process. 64 | client.set_redirect_url(Url::parse("http://redirect")?); 65 | // Set the desired scopes. 66 | client.add_scope("read"); 67 | client.add_scope("write"); 68 | 69 | // Generate the full authorization URL. 70 | let state = State::new_random(); 71 | let auth_url = client.authorize_url(&state); 72 | 73 | // This is the URL you should redirect the user to, in order to trigger the 74 | // authorization process. 75 | println!("Browse to: {}", auth_url); 76 | 77 | // Once the user has been redirected to the redirect URL, you'll have the 78 | // access code. For security reasons, your code should verify that the 79 | // `state` parameter returned by the server matches `state`. 80 | let received: ReceivedCode = listen_for_code(8080).await?; 81 | 82 | if received.state != state { 83 | panic!("CSRF token mismatch :("); 84 | } 85 | 86 | // Now you can trade it for an access token. 87 | let token = client.exchange_code(received.code) 88 | .with_client(&reqwest_client) 89 | .execute::() 90 | .await?; 91 | 92 | ``` 93 | 94 |
95 | 96 | ## Implicit Grant 97 | 98 | This flow fetches an access token directly from the authorization endpoint. 99 | 100 | Be sure to understand the security implications of this flow before using 101 | it. In most cases the Authorization Code Grant flow above is preferred to 102 | the Implicit Grant flow. 103 | 104 | ```rust 105 | use oauth2::*; 106 | use url::Url; 107 | 108 | pub struct ReceivedCode { 109 | pub code: AuthorizationCode, 110 | pub state: State, 111 | } 112 | 113 | let mut client = Client::new( 114 | "client_id", 115 | Url::parse("http://authorize")?, 116 | Url::parse("http://token")? 117 | ); 118 | 119 | client.set_client_secret("client_secret"); 120 | 121 | // Generate the full authorization URL. 122 | let state = State::new_random(); 123 | let auth_url = client.authorize_url_implicit(&state); 124 | 125 | // This is the URL you should redirect the user to, in order to trigger the 126 | // authorization process. 127 | println!("Browse to: {}", auth_url); 128 | 129 | // Once the user has been redirected to the redirect URL, you'll have the 130 | // access code. For security reasons, your code should verify that the 131 | // `state` parameter returned by the server matches `state`. 132 | let received: ReceivedCode = get_code().await?; 133 | 134 | if received.state != state { 135 | panic!("CSRF token mismatch :("); 136 | } 137 | 138 | ``` 139 | 140 |
141 | 142 | ## Resource Owner Password Credentials Grant 143 | 144 | You can ask for a *password* access token by calling the 145 | `Client::exchange_password` method, while including the username and 146 | password. 147 | 148 | ```rust 149 | use oauth2::*; 150 | use url::Url; 151 | 152 | let reqwest_client = reqwest::Client::new(); 153 | 154 | let mut client = Client::new( 155 | "client_id", 156 | Url::parse("http://authorize")?, 157 | Url::parse("http://token")? 158 | ); 159 | 160 | client.set_client_secret("client_secret"); 161 | client.add_scope("read"); 162 | 163 | let token = client 164 | .exchange_password("user", "pass") 165 | .with_client(&reqwest_client) 166 | .execute::() 167 | .await?; 168 | 169 | ``` 170 | 171 |
172 | 173 | ## Client Credentials Grant 174 | 175 | You can ask for a *client credentials* access token by calling the 176 | `Client::exchange_client_credentials` method. 177 | 178 | ```rust 179 | use oauth2::*; 180 | use url::Url; 181 | 182 | let reqwest_client = reqwest::Client::new(); 183 | let mut client = Client::new( 184 | "client_id", 185 | Url::parse("http://authorize")?, 186 | Url::parse("http://token")? 187 | ); 188 | 189 | client.set_client_secret("client_secret"); 190 | client.add_scope("read"); 191 | 192 | let token_result = client.exchange_client_credentials() 193 | .with_client(&reqwest_client) 194 | .execute::(); 195 | 196 | ``` 197 | 198 |
199 | 200 | ## Relationship to oauth2-rs 201 | 202 | This is a fork of [oauth2-rs]. 203 | 204 | The main differences are: 205 | * Removal of unnecessary type parameters on Client ([see discussion here]). 206 | * Only support one client implementation ([reqwest]). 207 | * Remove most newtypes except `Scope` and the secret ones since they made the API harder to use. 208 | 209 | [RFC 6749]: https://tools.ietf.org/html/rfc6749 210 | [Google]: https://github.com/udoprog/async-oauth2/blob/master/examples/src/bin/google.rs 211 | [oauth2-rs]: https://github.com/ramosbugs/oauth2-rs 212 | [reqwest]: https://docs.rs/reqwest 213 | [see discussion here]: https://github.com/ramosbugs/oauth2-rs/issues/44#issuecomment-50158653 214 | [Spotify]: https://github.com/udoprog/async-oauth2/blob/master/examples/src/bin/spotify.rs 215 | [Twitch]: https://github.com/udoprog/async-oauth2/blob/master/examples/src/bin/twitch.rs 216 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-oauth2-examples" 3 | authors = ["John-John Tedro "] 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | name = "oauth2_examples" 9 | 10 | [dependencies] 11 | reqwest = "0.12.0" 12 | tokio = { version = "1.17.0", features = ["full"] } 13 | hyper = { version = "0.14.18", features = ["server", "http1", "tcp"] } 14 | serde = { version = "1.0.197", features = ["derive"] } 15 | serde_urlencoded = "0.7.1" 16 | tower-service = "0.3.2" 17 | log = "0.4.21" 18 | anyhow = "1.0.81" 19 | clap = "4.5.4" 20 | async-oauth2 = {path = ".."} 21 | -------------------------------------------------------------------------------- /examples/src/bin/google.rs: -------------------------------------------------------------------------------- 1 | use oauth2::{Client, StandardToken, State, Url}; 2 | use oauth2_examples::{config_from_args, listen_for_code}; 3 | 4 | #[tokio::main] 5 | async fn main() -> anyhow::Result<()> { 6 | let config = config_from_args("Google Example")?; 7 | 8 | let reqwest_client = reqwest::Client::new(); 9 | 10 | let auth_url = Url::parse("https://accounts.google.com/o/oauth2/v2/auth")?; 11 | let token_url = Url::parse("https://www.googleapis.com/oauth2/v4/token")?; 12 | let redirect_url = Url::parse("http://localhost:8080/api/auth/redirect")?; 13 | 14 | let mut client = Client::new(config.client_id, auth_url, token_url); 15 | client.set_client_secret(config.client_secret); 16 | client.add_scope("https://www.googleapis.com/auth/youtube.readonly"); 17 | client.set_redirect_url(redirect_url); 18 | 19 | let state = State::new_random(); 20 | let auth_url = client.authorize_url(&state); 21 | 22 | println!("Browse to: {}", auth_url); 23 | 24 | let received = listen_for_code(8080).await?; 25 | 26 | if received.state != state { 27 | panic!("CSRF token mismatch :("); 28 | } 29 | 30 | let token = client 31 | .exchange_code(received.code) 32 | .with_client(&reqwest_client) 33 | .execute::() 34 | .await?; 35 | 36 | println!("Token: {:?}", token); 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /examples/src/bin/msgraph.rs: -------------------------------------------------------------------------------- 1 | //! Showcases how to define and use a nonstandard token type. 2 | //! 3 | //! Note: MSGraph requires you to set `client_id` and `client_secret` as extra 4 | //! parameters when performing the token exchange (see below). 5 | 6 | use oauth2::{Client, StandardToken, Url}; 7 | 8 | use anyhow::{anyhow, Result}; 9 | 10 | pub struct ConfigMS { 11 | pub client_id: String, 12 | pub client_secret: String, 13 | /// Tennant ID, Required in the url by Microsoft. 14 | pub tenant_domain: String, 15 | } 16 | 17 | pub fn config_from_args_ms(name: &'static str) -> Result { 18 | let app = clap::Command::new(name) 19 | .about("Testing out OAuth 2.0 flows") 20 | .arg( 21 | clap::Arg::new("client-id") 22 | .long("client-id") 23 | .help("Client ID to use."), 24 | ) 25 | .arg( 26 | clap::Arg::new("client-secret") 27 | .long("client-secret") 28 | .help("Client Secret to use."), 29 | ) 30 | .arg( 31 | clap::Arg::new("tenant-domain") 32 | .long("tenant-domain") 33 | .help("Tenant domain to use."), 34 | ); 35 | 36 | let m = app.get_matches(); 37 | 38 | let client_id = m 39 | .get_one::("client-id") 40 | .ok_or_else(|| anyhow!("missing: --client-id "))? 41 | .to_owned(); 42 | 43 | let client_secret = m 44 | .get_one::("client-secret") 45 | .ok_or_else(|| anyhow!("missing: --client-secret "))? 46 | .to_owned(); 47 | 48 | let tenant_domain = m 49 | .get_one::("tenant-domain") 50 | .ok_or_else(|| anyhow!("missing: --tenant-domain "))? 51 | .to_owned(); 52 | 53 | Ok(ConfigMS { 54 | client_id, 55 | client_secret, 56 | tenant_domain, 57 | }) 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() -> anyhow::Result<()> { 62 | let config = config_from_args_ms("msgraph Example")?; 63 | 64 | let reqwest_client = reqwest::Client::new(); 65 | 66 | let auth_url = Url::parse( 67 | format!( 68 | "https://login.microsoftonline.com/{}/oauth2/authorize", 69 | config.tenant_domain 70 | ) 71 | .as_str(), 72 | )?; 73 | let token_url = Url::parse( 74 | format!( 75 | "https://login.microsoftonline.com/{}/oauth2/token", 76 | config.tenant_domain 77 | ) 78 | .as_str(), 79 | )?; 80 | let redirect_url = Url::parse("https://login.microsoftonline.com/common/oauth2/nativeclient")?; 81 | //let refresh_token_url = Url::parse(format!("https://login.microsoftonline.com/{}/oauth2/token", config.tenant_domain).as_str())?; 82 | 83 | let mut client = Client::new(&config.client_id, auth_url, token_url); 84 | client.set_client_secret(&config.client_secret); 85 | client.set_redirect_url(redirect_url); 86 | 87 | client.add_scope("User.ReadAll"); 88 | 89 | let token_result = client 90 | .exchange_client_credentials() 91 | .with_client(&reqwest_client) 92 | .execute::() 93 | .await?; 94 | 95 | println!("Token: {:?}", token_result); 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /examples/src/bin/spotify.rs: -------------------------------------------------------------------------------- 1 | use oauth2::{Client, StandardToken, State, Url}; 2 | use oauth2_examples::{config_from_args, listen_for_code}; 3 | 4 | #[tokio::main] 5 | async fn main() -> anyhow::Result<()> { 6 | let config = config_from_args("Spotify Example")?; 7 | 8 | let reqwest_client = reqwest::Client::new(); 9 | 10 | let auth_url = Url::parse("https://accounts.spotify.com/authorize")?; 11 | let token_url = Url::parse("https://accounts.spotify.com/api/token")?; 12 | let redirect_url = Url::parse("http://localhost:8080/api/auth/redirect")?; 13 | 14 | let mut client = Client::new(config.client_id, auth_url, token_url); 15 | client.set_client_secret(config.client_secret); 16 | client.add_scope("user-read-email"); 17 | client.set_redirect_url(redirect_url); 18 | 19 | let state = State::new_random(); 20 | let auth_url = client.authorize_url(&state); 21 | 22 | println!("Browse to: {}", auth_url); 23 | 24 | let received = listen_for_code(8080).await?; 25 | 26 | if received.state != state { 27 | panic!("CSRF token mismatch :("); 28 | } 29 | 30 | let token = client 31 | .exchange_code(received.code) 32 | .with_client(&reqwest_client) 33 | .execute::() 34 | .await?; 35 | 36 | println!("Token: {:?}", token); 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /examples/src/bin/twitch.rs: -------------------------------------------------------------------------------- 1 | //! Showcases how to define and use a nonstandard token type. 2 | //! 3 | //! Note: Twitch requires you to set `client_id` and `client_secret` as extra 4 | //! parameters when performing the token exchange (see below). 5 | 6 | use oauth2::{AccessToken, Client, RefreshToken, Scope, State, Token, TokenType, Url}; 7 | use oauth2_examples::{config_from_args, listen_for_code}; 8 | use std::time::Duration; 9 | 10 | #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] 11 | pub struct TwitchToken { 12 | access_token: AccessToken, 13 | token_type: TokenType, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | expires_in: Option, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | refresh_token: Option, 18 | #[serde(rename = "scope")] 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | #[serde(default)] 21 | scopes: Option>, 22 | } 23 | 24 | impl Token for TwitchToken { 25 | fn access_token(&self) -> &AccessToken { 26 | &self.access_token 27 | } 28 | 29 | fn token_type(&self) -> &TokenType { 30 | &self.token_type 31 | } 32 | 33 | fn expires_in(&self) -> Option { 34 | self.expires_in.map(Duration::from_secs) 35 | } 36 | 37 | fn refresh_token(&self) -> Option<&RefreshToken> { 38 | self.refresh_token.as_ref() 39 | } 40 | 41 | fn scopes(&self) -> Option<&Vec> { 42 | self.scopes.as_ref() 43 | } 44 | } 45 | 46 | #[tokio::main] 47 | async fn main() -> anyhow::Result<()> { 48 | let config = config_from_args("Twitch Example")?; 49 | 50 | let reqwest_client = reqwest::Client::new(); 51 | 52 | let auth_url = Url::parse("https://id.twitch.tv/oauth2/authorize")?; 53 | let token_url = Url::parse("https://id.twitch.tv/oauth2/token")?; 54 | let redirect_url = Url::parse("http://localhost:8080/api/auth/redirect")?; 55 | 56 | let mut client = Client::new(&config.client_id, auth_url, token_url); 57 | client.set_client_secret(&config.client_secret); 58 | client.set_redirect_url(redirect_url); 59 | 60 | let state = State::new_random(); 61 | let auth_url = client.authorize_url(&state); 62 | 63 | println!("Browse to: {}", auth_url); 64 | 65 | let received = listen_for_code(8080).await?; 66 | 67 | if received.state != state { 68 | panic!("CSRF token mismatch :("); 69 | } 70 | 71 | let token = client 72 | .exchange_code(received.code) 73 | .param("client_id", &config.client_id) 74 | .param("client_secret", &config.client_secret) 75 | .with_client(&reqwest_client) 76 | .execute::() 77 | .await?; 78 | 79 | println!("Token: {:?}", token); 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::future::ready; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | 6 | use anyhow::{anyhow, Result}; 7 | use hyper::{body::Body, server, service, Request, Response}; 8 | use oauth2::{AuthorizationCode, State}; 9 | use serde::Deserialize; 10 | use std::net::SocketAddr; 11 | use tokio::sync::oneshot; 12 | use tower_service::Service; 13 | 14 | type BoxFuture<'a, T> = Pin + Send + Sync + 'a>>; 15 | 16 | pub struct Config { 17 | pub client_id: String, 18 | pub client_secret: String, 19 | } 20 | 21 | #[derive(Deserialize)] 22 | pub struct ReceivedCode { 23 | pub code: AuthorizationCode, 24 | pub state: State, 25 | } 26 | 27 | /// Interface to the server. 28 | pub struct Server { 29 | channel: Option>, 30 | } 31 | 32 | impl Service> for Server { 33 | type Response = Response; 34 | type Error = anyhow::Error; 35 | type Future = BoxFuture<'static, Result>; 36 | 37 | fn poll_ready(&mut self, _: &mut Context) -> Poll> { 38 | Poll::Ready(Ok(())) 39 | } 40 | 41 | fn call(&mut self, req: Request) -> Self::Future { 42 | if let Ok(code) = 43 | serde_urlencoded::from_str::(req.uri().query().unwrap_or("")) 44 | { 45 | if let Some(channel) = self.channel.take() { 46 | let _ = channel.send(code); 47 | } 48 | } 49 | 50 | Box::pin(ready(Ok(Response::new(Body::empty())))) 51 | } 52 | } 53 | 54 | /// Get configuration from arguments. 55 | pub fn config_from_args(name: &'static str) -> Result { 56 | let app = clap::Command::new(name) 57 | .about("Testing out OAuth 2.0 flows") 58 | .arg( 59 | clap::Arg::new("client-id") 60 | .long("client-id") 61 | .help("Client ID to use."), 62 | ) 63 | .arg( 64 | clap::Arg::new("client-secret") 65 | .long("client-secret") 66 | .help("Client Secret to use."), 67 | ); 68 | 69 | let m = app.get_matches(); 70 | 71 | let client_id = m 72 | .get_one::("client-id") 73 | .ok_or_else(|| anyhow!("missing: --client-id "))? 74 | .to_owned(); 75 | 76 | let client_secret = m 77 | .get_one::("client-secret") 78 | .ok_or_else(|| anyhow!("missing: --client-secret "))? 79 | .to_owned(); 80 | 81 | Ok(Config { 82 | client_id, 83 | client_secret, 84 | }) 85 | } 86 | 87 | /// Listen for a code at the specified port. 88 | pub async fn listen_for_code(port: u32) -> Result { 89 | let bind = format!("127.0.0.1:{}", port); 90 | log::info!("Listening on: http://{}", bind); 91 | 92 | let addr: SocketAddr = str::parse(&bind)?; 93 | 94 | let (tx, rx) = oneshot::channel::(); 95 | 96 | let mut channel = Some(tx); 97 | 98 | let server_future = server::Server::bind(&addr).serve(service::make_service_fn(move |_| { 99 | let channel = channel.take().expect("channel is not available"); 100 | let mut server = Server { 101 | channel: Some(channel), 102 | }; 103 | let service = service::service_fn(move |req| server.call(req)); 104 | 105 | async move { Ok::<_, hyper::Error>(service) } 106 | })); 107 | 108 | tokio::select! { 109 | _ = server_future => panic!("server exited for some reason"), 110 | received = rx => Ok(received?), 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [github](https://github.com/udoprog/async-oauth2) 2 | //! [crates.io](https://crates.io/crates/async-oauth2) 3 | //! [docs.rs](https://docs.rs/async-oauth2) 4 | //! 5 | //! An asynchronous OAuth2 flow implementation, trying to adhere as much as 6 | //! possible to [RFC 6749]. 7 | //! 8 | //!
9 | //! 10 | //! ## Examples 11 | //! 12 | //! To see the library in action, you can go to one of our examples: 13 | //! 14 | //! - [Google] 15 | //! - [Spotify] 16 | //! - [Twitch] 17 | //! 18 | //! If you've checked out the project they can be run like this: 19 | //! 20 | //! ```sh 21 | //! cargo run --manifest-path=examples/Cargo.toml --bin spotify -- 22 | //! --client-id --client-secret 23 | //! cargo run --manifest-path=examples/Cargo.toml --bin google -- 24 | //! --client-id --client-secret 25 | //! cargo run --manifest-path=examples/Cargo.toml --bin twitch -- 26 | //! --client-id --client-secret 27 | //! ``` 28 | //! 29 | //! > Note: You need to configure your client integration to permit redirects to 30 | //! > `http://localhost:8080/api/auth/redirect` for these to work. How this is 31 | //! > done depends on the integration used. 32 | //! 33 | //!
34 | //! 35 | //! ## Authorization Code Grant 36 | //! 37 | //! This is the most common OAuth2 flow. 38 | //! 39 | //! ```no_run 40 | //! use oauth2::*; 41 | //! use url::Url; 42 | //! 43 | //! pub struct ReceivedCode { 44 | //! pub code: AuthorizationCode, 45 | //! pub state: State, 46 | //! } 47 | //! 48 | //! # async fn listen_for_code(port: u32) -> Result> { todo!() } 49 | //! # #[tokio::main] 50 | //! # async fn main() -> Result<(), Box> { 51 | //! let reqwest_client = reqwest::Client::new(); 52 | //! 53 | //! // Create an OAuth2 client by specifying the client ID, client secret, 54 | //! // authorization URL and token URL. 55 | //! let mut client = Client::new( 56 | //! "client_id", 57 | //! Url::parse("http://authorize")?, 58 | //! Url::parse("http://token")? 59 | //! ); 60 | //! 61 | //! client.set_client_secret("client_secret"); 62 | //! // Set the URL the user will be redirected to after the authorization 63 | //! // process. 64 | //! client.set_redirect_url(Url::parse("http://redirect")?); 65 | //! // Set the desired scopes. 66 | //! client.add_scope("read"); 67 | //! client.add_scope("write"); 68 | //! 69 | //! // Generate the full authorization URL. 70 | //! let state = State::new_random(); 71 | //! let auth_url = client.authorize_url(&state); 72 | //! 73 | //! // This is the URL you should redirect the user to, in order to trigger the 74 | //! // authorization process. 75 | //! println!("Browse to: {}", auth_url); 76 | //! 77 | //! // Once the user has been redirected to the redirect URL, you'll have the 78 | //! // access code. For security reasons, your code should verify that the 79 | //! // `state` parameter returned by the server matches `state`. 80 | //! let received: ReceivedCode = listen_for_code(8080).await?; 81 | //! 82 | //! if received.state != state { 83 | //! panic!("CSRF token mismatch :("); 84 | //! } 85 | //! 86 | //! // Now you can trade it for an access token. 87 | //! let token = client.exchange_code(received.code) 88 | //! .with_client(&reqwest_client) 89 | //! .execute::() 90 | //! .await?; 91 | //! 92 | //! # Ok(()) 93 | //! # } 94 | //! ``` 95 | //! 96 | //!
97 | //! 98 | //! ## Implicit Grant 99 | //! 100 | //! This flow fetches an access token directly from the authorization endpoint. 101 | //! 102 | //! Be sure to understand the security implications of this flow before using 103 | //! it. In most cases the Authorization Code Grant flow above is preferred to 104 | //! the Implicit Grant flow. 105 | //! 106 | //! ```no_run 107 | //! use oauth2::*; 108 | //! use url::Url; 109 | //! 110 | //! pub struct ReceivedCode { 111 | //! pub code: AuthorizationCode, 112 | //! pub state: State, 113 | //! } 114 | //! 115 | //! # async fn get_code() -> Result> { todo!() } 116 | //! # #[tokio::main] 117 | //! # async fn main() -> Result<(), Box> { 118 | //! let mut client = Client::new( 119 | //! "client_id", 120 | //! Url::parse("http://authorize")?, 121 | //! Url::parse("http://token")? 122 | //! ); 123 | //! 124 | //! client.set_client_secret("client_secret"); 125 | //! 126 | //! // Generate the full authorization URL. 127 | //! let state = State::new_random(); 128 | //! let auth_url = client.authorize_url_implicit(&state); 129 | //! 130 | //! // This is the URL you should redirect the user to, in order to trigger the 131 | //! // authorization process. 132 | //! println!("Browse to: {}", auth_url); 133 | //! 134 | //! // Once the user has been redirected to the redirect URL, you'll have the 135 | //! // access code. For security reasons, your code should verify that the 136 | //! // `state` parameter returned by the server matches `state`. 137 | //! let received: ReceivedCode = get_code().await?; 138 | //! 139 | //! if received.state != state { 140 | //! panic!("CSRF token mismatch :("); 141 | //! } 142 | //! 143 | //! # Ok(()) } 144 | //! ``` 145 | //! 146 | //!
147 | //! 148 | //! ## Resource Owner Password Credentials Grant 149 | //! 150 | //! You can ask for a *password* access token by calling the 151 | //! `Client::exchange_password` method, while including the username and 152 | //! password. 153 | //! 154 | //! ```no_run 155 | //! use oauth2::*; 156 | //! use url::Url; 157 | //! 158 | //! # #[tokio::main] 159 | //! # async fn main() -> Result<(), Box> { 160 | //! let reqwest_client = reqwest::Client::new(); 161 | //! 162 | //! let mut client = Client::new( 163 | //! "client_id", 164 | //! Url::parse("http://authorize")?, 165 | //! Url::parse("http://token")? 166 | //! ); 167 | //! 168 | //! client.set_client_secret("client_secret"); 169 | //! client.add_scope("read"); 170 | //! 171 | //! let token = client 172 | //! .exchange_password("user", "pass") 173 | //! .with_client(&reqwest_client) 174 | //! .execute::() 175 | //! .await?; 176 | //! 177 | //! # Ok(()) } 178 | //! ``` 179 | //! 180 | //!
181 | //! 182 | //! ## Client Credentials Grant 183 | //! 184 | //! You can ask for a *client credentials* access token by calling the 185 | //! `Client::exchange_client_credentials` method. 186 | //! 187 | //! ```no_run 188 | //! use oauth2::*; 189 | //! use url::Url; 190 | //! 191 | //! # #[tokio::main] 192 | //! # async fn main() -> Result<(), Box> { 193 | //! let reqwest_client = reqwest::Client::new(); 194 | //! let mut client = Client::new( 195 | //! "client_id", 196 | //! Url::parse("http://authorize")?, 197 | //! Url::parse("http://token")? 198 | //! ); 199 | //! 200 | //! client.set_client_secret("client_secret"); 201 | //! client.add_scope("read"); 202 | //! 203 | //! let token_result = client.exchange_client_credentials() 204 | //! .with_client(&reqwest_client) 205 | //! .execute::(); 206 | //! 207 | //! # Ok(()) } 208 | //! ``` 209 | //! 210 | //!
211 | //! 212 | //! ## Relationship to oauth2-rs 213 | //! 214 | //! This is a fork of [oauth2-rs]. 215 | //! 216 | //! The main differences are: 217 | //! * Removal of unnecessary type parameters on Client ([see discussion here]). 218 | //! * Only support one client implementation ([reqwest]). 219 | //! * Remove most newtypes except `Scope` and the secret ones since they made the API harder to use. 220 | //! 221 | //! [RFC 6749]: https://tools.ietf.org/html/rfc6749 222 | //! [Google]: https://github.com/udoprog/async-oauth2/blob/master/examples/src/bin/google.rs 223 | //! [oauth2-rs]: https://github.com/ramosbugs/oauth2-rs 224 | //! [reqwest]: https://docs.rs/reqwest 225 | //! [see discussion here]: https://github.com/ramosbugs/oauth2-rs/issues/44#issuecomment-50158653 226 | //! [Spotify]: https://github.com/udoprog/async-oauth2/blob/master/examples/src/bin/spotify.rs 227 | //! [Twitch]: https://github.com/udoprog/async-oauth2/blob/master/examples/src/bin/twitch.rs 228 | 229 | #![deny(missing_docs)] 230 | 231 | use std::{borrow::Cow, error, fmt, time::Duration}; 232 | 233 | use base64::prelude::{Engine as _, BASE64_URL_SAFE_NO_PAD}; 234 | use rand::{thread_rng, Rng}; 235 | use serde::{Deserialize, Serialize}; 236 | use serde_aux::prelude::*; 237 | use sha2::{Digest, Sha256}; 238 | use thiserror::Error; 239 | pub use url::Url; 240 | 241 | /// Indicates whether requests to the authorization server should use basic authentication or 242 | /// include the parameters in the request body for requests in which either is valid. 243 | /// 244 | /// The default AuthType is *BasicAuth*, following the recommendation of 245 | /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). 246 | #[derive(Clone, Copy, Debug)] 247 | pub enum AuthType { 248 | /// The client_id and client_secret will be included as part of the request body. 249 | RequestBody, 250 | /// The client_id and client_secret will be included using the basic auth authentication scheme. 251 | BasicAuth, 252 | } 253 | 254 | macro_rules! redacted_debug { 255 | ($name:ident) => { 256 | impl fmt::Debug for $name { 257 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 258 | write!(f, concat!(stringify!($name), "([redacted])")) 259 | } 260 | } 261 | }; 262 | } 263 | 264 | /// borrowed newtype plumbing 265 | macro_rules! borrowed_newtype { 266 | ($name:ident, $borrowed:ty) => { 267 | impl std::ops::Deref for $name { 268 | type Target = $borrowed; 269 | 270 | #[inline] 271 | fn deref(&self) -> &Self::Target { 272 | &self.0 273 | } 274 | } 275 | 276 | impl<'a> From<&'a $name> for Cow<'a, $borrowed> { 277 | #[inline] 278 | fn from(value: &'a $name) -> Cow<'a, $borrowed> { 279 | Cow::Borrowed(&value.0) 280 | } 281 | } 282 | 283 | impl AsRef<$borrowed> for $name { 284 | #[inline] 285 | fn as_ref(&self) -> &$borrowed { 286 | self 287 | } 288 | } 289 | }; 290 | } 291 | 292 | /// newtype plumbing 293 | macro_rules! newtype { 294 | ($name:ident, $owned:ty, $borrowed:ty) => { 295 | borrowed_newtype!($name, $borrowed); 296 | 297 | impl<'a> From<&'a $borrowed> for $name { 298 | #[inline] 299 | fn from(value: &'a $borrowed) -> Self { 300 | Self(value.to_owned()) 301 | } 302 | } 303 | 304 | impl From<$owned> for $name { 305 | #[inline] 306 | fn from(value: $owned) -> Self { 307 | Self(value) 308 | } 309 | } 310 | 311 | impl<'a> From<&'a $owned> for $name { 312 | #[inline] 313 | fn from(value: &'a $owned) -> Self { 314 | Self(value.to_owned()) 315 | } 316 | } 317 | 318 | impl From<$name> for $owned { 319 | #[inline] 320 | fn from(value: $name) -> $owned { 321 | value.0 322 | } 323 | } 324 | }; 325 | } 326 | 327 | /// Access token scope, as defined by the authorization server. 328 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] 329 | pub struct Scope(String); 330 | newtype!(Scope, String, str); 331 | 332 | /// Code Challenge used for [PKCE]((https://tools.ietf.org/html/rfc7636)) protection via the 333 | /// `code_challenge` parameter. 334 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] 335 | pub struct PkceCodeChallengeS256(String); 336 | newtype!(PkceCodeChallengeS256, String, str); 337 | 338 | /// Code Challenge Method used for [PKCE]((https://tools.ietf.org/html/rfc7636)) protection 339 | /// via the `code_challenge_method` parameter. 340 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] 341 | pub struct PkceCodeChallengeMethod(String); 342 | newtype!(PkceCodeChallengeMethod, String, str); 343 | 344 | /// Client password issued to the client during the registration process described by 345 | /// [Section 2.2](https://tools.ietf.org/html/rfc6749#section-2.2). 346 | #[derive(Clone, Deserialize, Serialize)] 347 | pub struct ClientSecret(String); 348 | redacted_debug!(ClientSecret); 349 | newtype!(ClientSecret, String, str); 350 | 351 | /// Value used for [CSRF]((https://tools.ietf.org/html/rfc6749#section-10.12)) protection 352 | /// via the `state` parameter. 353 | #[must_use] 354 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 355 | pub struct State([u8; 16]); 356 | redacted_debug!(State); 357 | borrowed_newtype!(State, [u8]); 358 | 359 | impl State { 360 | /// Generate a new random, base64-encoded 128-bit CSRF token. 361 | pub fn new_random() -> Self { 362 | let mut random_bytes = [0u8; 16]; 363 | thread_rng().fill(&mut random_bytes); 364 | State(random_bytes) 365 | } 366 | 367 | /// Convert into base64. 368 | pub fn to_base64(&self) -> String { 369 | BASE64_URL_SAFE_NO_PAD.encode(self.0) 370 | } 371 | } 372 | 373 | impl serde::Serialize for State { 374 | fn serialize(&self, serializer: S) -> Result 375 | where 376 | S: serde::Serializer, 377 | { 378 | self.to_base64().serialize(serializer) 379 | } 380 | } 381 | 382 | impl<'de> serde::Deserialize<'de> for State { 383 | fn deserialize(deserializer: D) -> Result 384 | where 385 | D: serde::Deserializer<'de>, 386 | { 387 | let s = String::deserialize(deserializer)?; 388 | let bytes = BASE64_URL_SAFE_NO_PAD 389 | .decode(s) 390 | .map_err(serde::de::Error::custom)?; 391 | let mut buf = [0u8; 16]; 392 | buf.copy_from_slice(&bytes); 393 | Ok(Self(buf)) 394 | } 395 | } 396 | 397 | /// Code Verifier used for [PKCE]((https://tools.ietf.org/html/rfc7636)) protection via the 398 | /// `code_verifier` parameter. The value must have a minimum length of 43 characters and a 399 | /// maximum length of 128 characters. Each character must be ASCII alphanumeric or one of 400 | /// the characters "-" / "." / "_" / "~". 401 | #[derive(Deserialize, Serialize)] 402 | pub struct PkceCodeVerifierS256(String); 403 | newtype!(PkceCodeVerifierS256, String, str); 404 | 405 | impl PkceCodeVerifierS256 { 406 | /// Generate a new random, base64-encoded code verifier. 407 | pub fn new_random() -> Self { 408 | PkceCodeVerifierS256::new_random_len(32) 409 | } 410 | 411 | /// Generate a new random, base64-encoded code verifier. 412 | /// 413 | /// # Arguments 414 | /// 415 | /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. 416 | /// The value must be in the range 32 to 96 inclusive in order to generate a verifier 417 | /// with a suitable length. 418 | pub fn new_random_len(num_bytes: u32) -> Self { 419 | // The RFC specifies that the code verifier must have "a minimum length of 43 420 | // characters and a maximum length of 128 characters". 421 | // This implies 32-96 octets of random data to be base64 encoded. 422 | assert!((32..=96).contains(&num_bytes)); 423 | let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); 424 | let code = BASE64_URL_SAFE_NO_PAD.encode(random_bytes); 425 | assert!(code.len() >= 43 && code.len() <= 128); 426 | PkceCodeVerifierS256(code) 427 | } 428 | 429 | /// Return the code challenge for the code verifier. 430 | pub fn code_challenge(&self) -> PkceCodeChallengeS256 { 431 | let digest = Sha256::digest(self.as_bytes()); 432 | PkceCodeChallengeS256::from(BASE64_URL_SAFE_NO_PAD.encode(digest)) 433 | } 434 | 435 | /// Return the code challenge method for this code verifier. 436 | pub fn code_challenge_method() -> PkceCodeChallengeMethod { 437 | PkceCodeChallengeMethod::from("S256".to_string()) 438 | } 439 | 440 | /// Return the extension params used for authorize_url. 441 | pub fn authorize_url_params(&self) -> Vec<(&'static str, String)> { 442 | vec![ 443 | ( 444 | "code_challenge_method", 445 | PkceCodeVerifierS256::code_challenge_method().into(), 446 | ), 447 | ("code_challenge", self.code_challenge().into()), 448 | ] 449 | } 450 | } 451 | 452 | /// Authorization code returned from the authorization endpoint. 453 | #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] 454 | pub struct AuthorizationCode(String); 455 | redacted_debug!(AuthorizationCode); 456 | newtype!(AuthorizationCode, String, str); 457 | 458 | /// Refresh token used to obtain a new access token (if supported by the authorization server). 459 | #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] 460 | pub struct RefreshToken(String); 461 | redacted_debug!(RefreshToken); 462 | newtype!(RefreshToken, String, str); 463 | 464 | /// Access token returned by the token endpoint and used to access protected resources. 465 | #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] 466 | pub struct AccessToken(String); 467 | redacted_debug!(AccessToken); 468 | newtype!(AccessToken, String, str); 469 | 470 | /// Resource owner's password used directly as an authorization grant to obtain an access 471 | /// token. 472 | pub struct ResourceOwnerPassword(String); 473 | newtype!(ResourceOwnerPassword, String, str); 474 | 475 | /// Stores the configuration for an OAuth2 client. 476 | #[derive(Clone, Debug)] 477 | pub struct Client { 478 | client_id: String, 479 | client_secret: Option, 480 | auth_url: Url, 481 | auth_type: AuthType, 482 | token_url: Url, 483 | scopes: Vec, 484 | redirect_url: Option, 485 | } 486 | 487 | impl Client { 488 | /// Initializes an OAuth2 client with the fields common to most OAuth2 flows. 489 | /// 490 | /// # Arguments 491 | /// 492 | /// * `client_id` - Client ID 493 | /// * `auth_url` - Authorization endpoint: used by the client to obtain authorization from 494 | /// the resource owner via user-agent redirection. This URL is used in all standard OAuth2 495 | /// flows except the [Resource Owner Password Credentials 496 | /// Grant](https://tools.ietf.org/html/rfc6749#section-4.3) and the 497 | /// [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). 498 | /// * `token_url` - Token endpoint: used by the client to exchange an authorization grant 499 | /// (code) for an access token, typically with client authentication. This URL is used in 500 | /// all standard OAuth2 flows except the 501 | /// [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2). If this value is set 502 | /// to `None`, the `exchange_*` methods will return `Err(ExecuteError::Other(_))`. 503 | pub fn new(client_id: impl AsRef, auth_url: Url, token_url: Url) -> Self { 504 | Client { 505 | client_id: client_id.as_ref().to_string(), 506 | client_secret: None, 507 | auth_url, 508 | auth_type: AuthType::BasicAuth, 509 | token_url, 510 | scopes: Vec::new(), 511 | redirect_url: None, 512 | } 513 | } 514 | 515 | /// Configure the client secret to use. 516 | pub fn set_client_secret(&mut self, client_secret: impl Into) { 517 | self.client_secret = Some(client_secret.into()); 518 | } 519 | 520 | /// Appends a new scope to the authorization URL. 521 | pub fn add_scope(&mut self, scope: impl Into) { 522 | self.scopes.push(scope.into()); 523 | } 524 | 525 | /// Configures the type of client authentication used for communicating with the authorization 526 | /// server. 527 | /// 528 | /// The default is to use HTTP Basic authentication, as recommended in 529 | /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). 530 | pub fn set_auth_type(&mut self, auth_type: AuthType) { 531 | self.auth_type = auth_type; 532 | } 533 | 534 | /// Sets the the redirect URL used by the authorization endpoint. 535 | pub fn set_redirect_url(&mut self, redirect_url: Url) { 536 | self.redirect_url = Some(redirect_url); 537 | } 538 | 539 | /// Produces the full authorization URL used by the 540 | /// [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1) 541 | /// flow, which is the most common OAuth2 flow. 542 | /// 543 | /// # Arguments 544 | /// 545 | /// * `state` - A state value to include in the request. The authorization 546 | /// server includes this value when redirecting the user-agent back to the 547 | /// client. 548 | /// 549 | /// # Security Warning 550 | /// 551 | /// Callers should use a fresh, unpredictable `state` for each authorization 552 | /// request and verify that this value matches the `state` parameter passed 553 | /// by the authorization server to the redirect URI. Doing so mitigates 554 | /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) 555 | /// attacks. 556 | pub fn authorize_url(&self, state: &State) -> Url { 557 | self.authorize_url_impl("code", state) 558 | } 559 | 560 | /// Produces the full authorization URL used by the 561 | /// [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2) flow. 562 | /// 563 | /// # Arguments 564 | /// 565 | /// * `state` - A state value to include in the request. The authorization 566 | /// server includes this value when redirecting the user-agent back to the 567 | /// client. 568 | /// 569 | /// # Security Warning 570 | /// 571 | /// Callers should use a fresh, unpredictable `state` for each authorization request and verify 572 | /// that this value matches the `state` parameter passed by the authorization server to the 573 | /// redirect URI. Doing so mitigates 574 | /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) 575 | /// attacks. 576 | pub fn authorize_url_implicit(&self, state: &State) -> Url { 577 | self.authorize_url_impl("token", state) 578 | } 579 | 580 | fn authorize_url_impl(&self, response_type: &str, state: &State) -> Url { 581 | let scopes = self 582 | .scopes 583 | .iter() 584 | .map(|s| s.to_string()) 585 | .collect::>() 586 | .join(" "); 587 | 588 | let mut url = self.auth_url.clone(); 589 | 590 | { 591 | let mut query = url.query_pairs_mut(); 592 | 593 | query.append_pair("response_type", response_type); 594 | query.append_pair("client_id", &self.client_id); 595 | 596 | if let Some(ref redirect_url) = self.redirect_url { 597 | query.append_pair("redirect_uri", redirect_url.as_str()); 598 | } 599 | 600 | if !scopes.is_empty() { 601 | query.append_pair("scope", &scopes); 602 | } 603 | 604 | query.append_pair("state", &state.to_base64()); 605 | } 606 | 607 | url 608 | } 609 | 610 | /// Exchanges a code produced by a successful authorization process with an access token. 611 | /// 612 | /// Acquires ownership of the `code` because authorization codes may only be used to retrieve 613 | /// an access token from the authorization server. 614 | /// 615 | /// See https://tools.ietf.org/html/rfc6749#section-4.1.3 616 | pub fn exchange_code(&self, code: impl Into) -> Request<'_> { 617 | let code = code.into(); 618 | 619 | self.request_token() 620 | .param("grant_type", "authorization_code") 621 | .param("code", code.to_string()) 622 | } 623 | 624 | /// Requests an access token for the *password* grant type. 625 | /// 626 | /// See https://tools.ietf.org/html/rfc6749#section-4.3.2 627 | pub fn exchange_password( 628 | &self, 629 | username: impl AsRef, 630 | password: impl AsRef, 631 | ) -> Request<'_> { 632 | let username = username.as_ref(); 633 | let password = password.as_ref(); 634 | 635 | let mut builder = self 636 | .request_token() 637 | .param("grant_type", "password") 638 | .param("username", username.to_string()) 639 | .param("password", password.to_string()); 640 | 641 | // Generate the space-delimited scopes String before initializing params so that it has 642 | // a long enough lifetime. 643 | if !self.scopes.is_empty() { 644 | let scopes = self 645 | .scopes 646 | .iter() 647 | .map(|s| s.to_string()) 648 | .collect::>() 649 | .join(" "); 650 | 651 | builder = builder.param("scope", scopes); 652 | } 653 | 654 | builder 655 | } 656 | 657 | /// Requests an access token for the *client credentials* grant type. 658 | /// 659 | /// See https://tools.ietf.org/html/rfc6749#section-4.4.2 660 | pub fn exchange_client_credentials(&self) -> Request<'_> { 661 | let mut builder = self 662 | .request_token() 663 | .param("grant_type", "client_credentials"); 664 | 665 | // Generate the space-delimited scopes String before initializing params so that it has 666 | // a long enough lifetime. 667 | if !self.scopes.is_empty() { 668 | let scopes = self 669 | .scopes 670 | .iter() 671 | .map(|s| s.to_string()) 672 | .collect::>() 673 | .join(" "); 674 | 675 | builder = builder.param("scopes", scopes); 676 | } 677 | 678 | builder 679 | } 680 | 681 | /// Exchanges a refresh token for an access token 682 | /// 683 | /// See https://tools.ietf.org/html/rfc6749#section-6 684 | pub fn exchange_refresh_token(&self, refresh_token: &RefreshToken) -> Request<'_> { 685 | self.request_token() 686 | .param("grant_type", "refresh_token") 687 | .param("refresh_token", refresh_token.to_string()) 688 | } 689 | 690 | /// Construct a request builder for the token URL. 691 | fn request_token(&self) -> Request<'_> { 692 | Request { 693 | token_url: &self.token_url, 694 | auth_type: self.auth_type, 695 | client_id: &self.client_id, 696 | client_secret: self.client_secret.as_ref(), 697 | redirect_url: self.redirect_url.as_ref(), 698 | params: vec![], 699 | } 700 | } 701 | } 702 | 703 | /// A request wrapped in a client, ready to be executed. 704 | pub struct ClientRequest<'a> { 705 | request: Request<'a>, 706 | client: &'a reqwest::Client, 707 | } 708 | 709 | impl ClientRequest<'_> { 710 | /// Execute the token request. 711 | pub async fn execute(self) -> Result 712 | where 713 | T: for<'de> Deserialize<'de>, 714 | { 715 | use reqwest::{header, Method}; 716 | 717 | let mut request = self 718 | .client 719 | .request(Method::POST, self.request.token_url.clone()); 720 | 721 | // Section 5.1 of RFC 6749 (https://tools.ietf.org/html/rfc6749#section-5.1) only permits 722 | // JSON responses for this request. Some providers such as GitHub have off-spec behavior 723 | // and not only support different response formats, but have non-JSON defaults. Explicitly 724 | // request JSON here. 725 | request = request.header( 726 | header::ACCEPT, 727 | header::HeaderValue::from_static(CONTENT_TYPE_JSON), 728 | ); 729 | 730 | let request = { 731 | let mut form = url::form_urlencoded::Serializer::new(String::new()); 732 | 733 | // FIXME: add support for auth extensions? e.g., client_secret_jwt and private_key_jwt 734 | match self.request.auth_type { 735 | AuthType::RequestBody => { 736 | form.append_pair("client_id", self.request.client_id); 737 | 738 | if let Some(client_secret) = self.request.client_secret { 739 | form.append_pair("client_secret", client_secret); 740 | } 741 | } 742 | AuthType::BasicAuth => { 743 | // Section 2.3.1 of RFC 6749 requires separately url-encoding the id and secret 744 | // before using them as HTTP Basic auth username and password. Note that this is 745 | // not standard for ordinary Basic auth, so curl won't do it for us. 746 | let username = url_encode(self.request.client_id); 747 | 748 | let password = self 749 | .request 750 | .client_secret 751 | .map(|client_secret| url_encode(client_secret)); 752 | 753 | request = request.basic_auth(username, password.as_ref()); 754 | } 755 | } 756 | 757 | for (key, value) in self.request.params { 758 | form.append_pair(key.as_ref(), value.as_ref()); 759 | } 760 | 761 | if let Some(redirect_url) = &self.request.redirect_url { 762 | form.append_pair("redirect_uri", redirect_url.as_str()); 763 | } 764 | 765 | request = request.header( 766 | header::CONTENT_TYPE, 767 | header::HeaderValue::from_static("application/x-www-form-urlencoded"), 768 | ); 769 | 770 | request.body(form.finish().into_bytes()) 771 | }; 772 | 773 | let res = request 774 | .send() 775 | .await 776 | .map_err(|error| ExecuteError::RequestError { error })?; 777 | 778 | let status = res.status(); 779 | 780 | let body = res 781 | .bytes() 782 | .await 783 | .map_err(|error| ExecuteError::RequestError { error })?; 784 | 785 | if body.is_empty() { 786 | return Err(ExecuteError::EmptyResponse { status }); 787 | } 788 | 789 | if !status.is_success() { 790 | let error = match serde_json::from_slice::(body.as_ref()) { 791 | Ok(error) => error, 792 | Err(error) => { 793 | return Err(ExecuteError::BadResponse { 794 | status, 795 | error, 796 | body, 797 | }); 798 | } 799 | }; 800 | 801 | return Err(ExecuteError::ErrorResponse { status, error }); 802 | } 803 | 804 | return serde_json::from_slice(body.as_ref()).map_err(|error| ExecuteError::BadResponse { 805 | status, 806 | error, 807 | body, 808 | }); 809 | 810 | fn url_encode(s: &str) -> String { 811 | url::form_urlencoded::byte_serialize(s.as_bytes()).collect::() 812 | } 813 | 814 | const CONTENT_TYPE_JSON: &str = "application/json"; 815 | } 816 | } 817 | 818 | /// A token request that is in progress. 819 | pub struct Request<'a> { 820 | token_url: &'a Url, 821 | auth_type: AuthType, 822 | client_id: &'a str, 823 | client_secret: Option<&'a ClientSecret>, 824 | /// Configured redirect URL. 825 | redirect_url: Option<&'a Url>, 826 | /// Extra parameters. 827 | params: Vec<(Cow<'a, str>, Cow<'a, str>)>, 828 | } 829 | 830 | impl<'a> Request<'a> { 831 | /// Set an additional request param. 832 | pub fn param(mut self, key: impl Into>, value: impl Into>) -> Self { 833 | self.params.push((key.into(), value.into())); 834 | self 835 | } 836 | 837 | /// Wrap the request in a client. 838 | pub fn with_client(self, client: &'a reqwest::Client) -> ClientRequest<'a> { 839 | ClientRequest { 840 | client, 841 | request: self, 842 | } 843 | } 844 | } 845 | 846 | /// Basic OAuth2 authorization token types. 847 | #[derive(Clone, Debug, PartialEq, Serialize)] 848 | #[serde(rename_all = "lowercase")] 849 | pub enum TokenType { 850 | /// Bearer token 851 | /// ([OAuth 2.0 Bearer Tokens - RFC 6750](https://tools.ietf.org/html/rfc6750)). 852 | Bearer, 853 | /// MAC ([OAuth 2.0 Message Authentication Code (MAC) 854 | /// Tokens](https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-05)). 855 | Mac, 856 | } 857 | 858 | impl<'de> serde::de::Deserialize<'de> for TokenType { 859 | fn deserialize(deserializer: D) -> Result 860 | where 861 | D: serde::de::Deserializer<'de>, 862 | { 863 | let value = String::deserialize(deserializer)?.to_lowercase(); 864 | 865 | return match value.as_str() { 866 | "bearer" => Ok(TokenType::Bearer), 867 | "mac" => Ok(TokenType::Mac), 868 | other => Err(serde::de::Error::custom(UnknownVariantError( 869 | other.to_string(), 870 | ))), 871 | }; 872 | 873 | #[derive(Debug)] 874 | struct UnknownVariantError(String); 875 | 876 | impl error::Error for UnknownVariantError {} 877 | 878 | impl fmt::Display for UnknownVariantError { 879 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 880 | write!(fmt, "unsupported variant: {}", self.0) 881 | } 882 | } 883 | } 884 | } 885 | 886 | /// Common methods shared by all OAuth2 token implementations. 887 | /// 888 | /// The methods in this trait are defined in 889 | /// [Section 5.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.1). This trait exists 890 | /// separately from the `StandardToken` struct to support customization by clients, 891 | /// such as supporting interoperability with non-standards-complaint OAuth2 providers. 892 | pub trait Token 893 | where 894 | Self: for<'a> serde::de::Deserialize<'a>, 895 | { 896 | /// REQUIRED. The access token issued by the authorization server. 897 | fn access_token(&self) -> &AccessToken; 898 | 899 | /// REQUIRED. The type of the token issued as described in 900 | /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). 901 | /// Value is case insensitive and deserialized to the generic `TokenType` parameter. 902 | fn token_type(&self) -> &TokenType; 903 | 904 | /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600 905 | /// denotes that the access token will expire in one hour from the time the response was 906 | /// generated. If omitted, the authorization server SHOULD provide the expiration time via 907 | /// other means or document the default value. 908 | fn expires_in(&self) -> Option; 909 | 910 | /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same 911 | /// authorization grant as described in 912 | /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). 913 | fn refresh_token(&self) -> Option<&RefreshToken>; 914 | 915 | /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The 916 | /// scipe of the access token as described by 917 | /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response, 918 | /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from 919 | /// the response, this field is `None`. 920 | fn scopes(&self) -> Option<&Vec>; 921 | } 922 | 923 | /// Standard OAuth2 token response. 924 | /// 925 | /// This struct includes the fields defined in 926 | /// [Section 5.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.1), as well as 927 | /// extensions defined by the `EF` type parameter. 928 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 929 | pub struct StandardToken { 930 | access_token: AccessToken, 931 | token_type: TokenType, 932 | #[serde( 933 | skip_serializing_if = "Option::is_none", 934 | deserialize_with = "deserialize_option_number_from_string" 935 | )] 936 | expires_in: Option, 937 | #[serde(skip_serializing_if = "Option::is_none")] 938 | refresh_token: Option, 939 | #[serde(rename = "scope")] 940 | #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] 941 | #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] 942 | #[serde(skip_serializing_if = "Option::is_none")] 943 | #[serde(default)] 944 | scopes: Option>, 945 | } 946 | 947 | impl Token for StandardToken { 948 | /// REQUIRED. The access token issued by the authorization server. 949 | fn access_token(&self) -> &AccessToken { 950 | &self.access_token 951 | } 952 | 953 | /// REQUIRED. The type of the token issued as described in 954 | /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). 955 | /// Value is case insensitive and deserialized to the generic `TokenType` parameter. 956 | fn token_type(&self) -> &TokenType { 957 | &self.token_type 958 | } 959 | 960 | /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600 961 | /// denotes that the access token will expire in one hour from the time the response was 962 | /// generated. If omitted, the authorization server SHOULD provide the expiration time via 963 | /// other means or document the default value. 964 | fn expires_in(&self) -> Option { 965 | self.expires_in.map(Duration::from_secs) 966 | } 967 | 968 | /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same 969 | /// authorization grant as described in 970 | /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). 971 | fn refresh_token(&self) -> Option<&RefreshToken> { 972 | self.refresh_token.as_ref() 973 | } 974 | 975 | /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The 976 | /// scipe of the access token as described by 977 | /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response, 978 | /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from 979 | /// the response, this field is `None`. 980 | fn scopes(&self) -> Option<&Vec> { 981 | self.scopes.as_ref() 982 | } 983 | } 984 | 985 | /// These error types are defined in 986 | /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2). 987 | #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] 988 | #[serde(rename_all = "snake_case")] 989 | pub enum ErrorField { 990 | /// The request is missing a required parameter, includes an unsupported parameter value 991 | /// (other than grant type), repeats a parameter, includes multiple credentials, utilizes 992 | /// more than one mechanism for authenticating the client, or is otherwise malformed. 993 | InvalidRequest, 994 | /// Client authentication failed (e.g., unknown client, no client authentication included, 995 | /// or unsupported authentication method). 996 | InvalidClient, 997 | /// The provided authorization grant (e.g., authorization code, resource owner credentials) 998 | /// or refresh token is invalid, expired, revoked, does not match the redirection URI used 999 | /// in the authorization request, or was issued to another client. 1000 | InvalidGrant, 1001 | /// The authenticated client is not authorized to use this authorization grant type. 1002 | UnauthorizedClient, 1003 | /// The authorization grant type is not supported by the authorization server. 1004 | UnsupportedGrantType, 1005 | /// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the 1006 | /// resource owner. 1007 | InvalidScope, 1008 | /// Other error type. 1009 | Other(String), 1010 | } 1011 | 1012 | impl fmt::Display for ErrorField { 1013 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 1014 | use self::ErrorField::*; 1015 | 1016 | match *self { 1017 | InvalidRequest => "invalid_request".fmt(fmt), 1018 | InvalidClient => "invalid_client".fmt(fmt), 1019 | InvalidGrant => "invalid_grant".fmt(fmt), 1020 | UnauthorizedClient => "unauthorized_client".fmt(fmt), 1021 | UnsupportedGrantType => "unsupported_grant_type".fmt(fmt), 1022 | InvalidScope => "invalid_scope".fmt(fmt), 1023 | Other(ref value) => value.fmt(fmt), 1024 | } 1025 | } 1026 | } 1027 | 1028 | /// Error response returned by server after requesting an access token. 1029 | /// 1030 | /// The fields in this structure are defined in 1031 | /// [Section 5.2 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2). 1032 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 1033 | pub struct ErrorResponse { 1034 | /// A single ASCII error code. 1035 | pub error: ErrorField, 1036 | #[serde(default)] 1037 | #[serde(skip_serializing_if = "Option::is_none")] 1038 | /// Human-readable ASCII text providing additional information, used to assist 1039 | /// the client developer in understanding the error that occurred. 1040 | pub error_description: Option, 1041 | #[serde(default)] 1042 | #[serde(skip_serializing_if = "Option::is_none")] 1043 | /// A URI identifying a human-readable web page with information about the error, 1044 | /// used to provide the client developer with additional information about the error. 1045 | pub error_uri: Option, 1046 | } 1047 | 1048 | impl fmt::Display for ErrorResponse { 1049 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1050 | let mut formatted = self.error.to_string(); 1051 | 1052 | if let Some(error_description) = self.error_description.as_ref() { 1053 | formatted.push_str(": "); 1054 | formatted.push_str(error_description); 1055 | } 1056 | 1057 | if let Some(error_uri) = self.error_uri.as_ref() { 1058 | formatted.push_str(" / See "); 1059 | formatted.push_str(error_uri); 1060 | } 1061 | 1062 | write!(f, "{}", formatted) 1063 | } 1064 | } 1065 | 1066 | impl error::Error for ErrorResponse {} 1067 | 1068 | /// Errors when creating new clients. 1069 | #[derive(Debug, Error)] 1070 | #[non_exhaustive] 1071 | pub enum NewClientError { 1072 | /// Error creating underlying reqwest client. 1073 | #[error("Failed to construct client")] 1074 | Reqwest(#[source] reqwest::Error), 1075 | } 1076 | 1077 | impl From for NewClientError { 1078 | fn from(error: reqwest::Error) -> Self { 1079 | Self::Reqwest(error) 1080 | } 1081 | } 1082 | 1083 | /// Error encountered while requesting access token. 1084 | #[derive(Debug, Error)] 1085 | #[non_exhaustive] 1086 | pub enum ExecuteError { 1087 | /// A client error that occured. 1088 | #[error("reqwest error")] 1089 | RequestError { 1090 | /// Original request error. 1091 | #[source] 1092 | error: reqwest::Error, 1093 | }, 1094 | /// Failed to parse server response. Parse errors may occur while parsing either successful 1095 | /// or error responses. 1096 | #[error("malformed server response: {status}")] 1097 | BadResponse { 1098 | /// The status code associated with the response. 1099 | status: http::status::StatusCode, 1100 | /// The body that couldn't be deserialized. 1101 | body: bytes::Bytes, 1102 | /// Deserialization error. 1103 | #[source] 1104 | error: serde_json::error::Error, 1105 | }, 1106 | /// Response with non-successful status code and a body that could be 1107 | /// successfully deserialized as an [ErrorResponse]. 1108 | #[error("request resulted in error response: {status}")] 1109 | ErrorResponse { 1110 | /// The status code associated with the response. 1111 | status: http::status::StatusCode, 1112 | /// The deserialized response. 1113 | #[source] 1114 | error: ErrorResponse, 1115 | }, 1116 | /// Server response was empty. 1117 | #[error("request resulted in empty response: {status}")] 1118 | EmptyResponse { 1119 | /// The status code associated with the empty response. 1120 | status: http::status::StatusCode, 1121 | }, 1122 | } 1123 | 1124 | impl ExecuteError { 1125 | /// Access the status code of the error if available. 1126 | pub fn status(&self) -> Option { 1127 | match *self { 1128 | Self::RequestError { ref error, .. } => error.status(), 1129 | Self::BadResponse { status, .. } => Some(status), 1130 | Self::ErrorResponse { status, .. } => Some(status), 1131 | Self::EmptyResponse { status, .. } => Some(status), 1132 | } 1133 | } 1134 | 1135 | /// The original response body if available. 1136 | pub fn body(&self) -> Option<&bytes::Bytes> { 1137 | match *self { 1138 | Self::BadResponse { ref body, .. } => Some(body), 1139 | _ => None, 1140 | } 1141 | } 1142 | } 1143 | 1144 | /// Helper methods used by OAuth2 implementations/extensions. 1145 | pub mod helpers { 1146 | use serde::{Deserialize, Deserializer, Serializer}; 1147 | use url::Url; 1148 | 1149 | /// Serde space-delimited string deserializer for a `Vec`. 1150 | /// 1151 | /// This function splits a JSON string at each space character into a `Vec` . 1152 | /// 1153 | /// # Example 1154 | /// 1155 | /// In example below, the JSON value `{"items": "foo bar baz"}` would deserialize to: 1156 | /// 1157 | /// ``` 1158 | /// # struct GroceryBasket { 1159 | /// # items: Vec, 1160 | /// # } 1161 | /// # fn main() { 1162 | /// GroceryBasket { 1163 | /// items: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()] 1164 | /// }; 1165 | /// # } 1166 | /// ``` 1167 | /// 1168 | /// Note: this example does not compile automatically due to 1169 | /// [Rust issue #29286](https://github.com/rust-lang/rust/issues/29286). 1170 | /// 1171 | /// ``` 1172 | /// # /* 1173 | /// use serde::Deserialize; 1174 | /// 1175 | /// #[derive(Deserialize)] 1176 | /// struct GroceryBasket { 1177 | /// #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] 1178 | /// items: Vec, 1179 | /// } 1180 | /// # */ 1181 | /// ``` 1182 | pub fn deserialize_space_delimited_vec<'de, T, D>(deserializer: D) -> Result 1183 | where 1184 | T: Default + Deserialize<'de>, 1185 | D: Deserializer<'de>, 1186 | { 1187 | use serde::de::Error; 1188 | use serde_json::Value; 1189 | 1190 | if let Some(space_delimited) = Option::::deserialize(deserializer)? { 1191 | let entries = space_delimited 1192 | .split(' ') 1193 | .map(|s| Value::String(s.to_string())) 1194 | .collect(); 1195 | return T::deserialize(Value::Array(entries)).map_err(Error::custom); 1196 | } 1197 | 1198 | // If the JSON value is null, use the default value. 1199 | Ok(T::default()) 1200 | } 1201 | 1202 | /// Serde space-delimited string serializer for an `Option>`. 1203 | /// 1204 | /// This function serializes a string vector into a single space-delimited string. 1205 | /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` 1206 | /// in the case of JSON serialization). 1207 | pub fn serialize_space_delimited_vec( 1208 | vec_opt: &Option>, 1209 | serializer: S, 1210 | ) -> Result 1211 | where 1212 | T: AsRef, 1213 | S: Serializer, 1214 | { 1215 | if let Some(ref vec) = *vec_opt { 1216 | let space_delimited = vec.iter().map(|s| s.as_ref()).collect::>().join(" "); 1217 | serializer.serialize_str(&space_delimited) 1218 | } else { 1219 | serializer.serialize_none() 1220 | } 1221 | } 1222 | 1223 | /// Serde string deserializer for a `Url`. 1224 | pub fn deserialize_url<'de, D>(deserializer: D) -> Result 1225 | where 1226 | D: Deserializer<'de>, 1227 | { 1228 | use serde::de::Error; 1229 | let url_str = String::deserialize(deserializer)?; 1230 | Url::parse(url_str.as_ref()).map_err(Error::custom) 1231 | } 1232 | 1233 | /// Serde string serializer for a `Url`. 1234 | pub fn serialize_url(url: &Url, serializer: S) -> Result 1235 | where 1236 | S: Serializer, 1237 | { 1238 | serializer.serialize_str(url.as_str()) 1239 | } 1240 | } 1241 | --------------------------------------------------------------------------------