├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .mergify.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── deny.toml ├── examples ├── default_creds.rs ├── default_creds_id_token.rs ├── svc_account.rs └── svc_account_id_token.rs ├── release.toml ├── src ├── error.rs ├── gcp.rs ├── gcp │ ├── end_user.rs │ ├── metadata_server.rs │ └── service_account.rs ├── id_token.rs ├── jwt.rs ├── lib.rs ├── token.rs └── token_cache.rs └── tests └── svc_key.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Jake-Shadle 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Device:** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - "*" 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | name: CI 14 | jobs: 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-20.04 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | override: true 24 | 25 | # make sure all code has been formatted with rustfmt 26 | - run: rustup component add rustfmt 27 | - run: cargo fmt -- --check --color always 28 | 29 | # run clippy to verify we have no warnings 30 | - run: rustup component add clippy 31 | - run: cargo fetch 32 | - run: cargo clippy --all-features --all-targets -- -D warnings 33 | 34 | test: 35 | name: Test 36 | strategy: 37 | matrix: 38 | os: [ubuntu-20.04, windows-latest, macOS-latest] 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | override: true 46 | - run: cargo fetch 47 | - name: cargo test build 48 | run: cargo test --all-features --no-run 49 | - name: cargo test 50 | run: cargo test --all-features 51 | 52 | deny-check: 53 | name: cargo-deny check 54 | runs-on: ubuntu-20.04 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: EmbarkStudios/cargo-deny-action@v1 58 | 59 | publish-check: 60 | name: Publish Check 61 | runs-on: ubuntu-20.04 62 | steps: 63 | - uses: actions/checkout@v1 64 | - uses: actions-rs/toolchain@v1 65 | with: 66 | toolchain: stable 67 | override: true 68 | - run: cargo fetch 69 | - run: cargo publish --dry-run 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge when CI passes and 1 reviews 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - "#review-requested=0" 6 | - "#changes-requested-reviews-by=0" 7 | - "#commented-reviews-by=0" 8 | - base=main 9 | - label!=work-in-progress 10 | actions: 11 | merge: 12 | method: squash 13 | - name: delete head branch after merge 14 | conditions: [] 15 | actions: 16 | delete_head_branch: {} 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | 10 | ## [Unreleased] - ReleaseDate 11 | ## [0.10.0] - 2024-03-21 12 | ### Changed 13 | - [PR#72](https://github.com/EmbarkStudios/tame-oauth/pull/72) update `http` -> 1.1.0. 14 | 15 | ## [0.9.6] - 2023-11-20 16 | ### Changed 17 | - [PR#67](https://github.com/EmbarkStudios/tame-oauth/pull/67) upgraded `ring` from 0.16 -> 0.17. 18 | 19 | ## [0.9.4] - 2023-10-04 20 | ### Changed 21 | - [PR#66](https://github.com/EmbarkStudios/tame-oauth/pull/66) replaced `base64` with `data-encoding`. 22 | 23 | ## [0.9.3] - 2023-06-09 24 | ### Fixed 25 | - [PR#65](https://github.com/EmbarkStudios/tame-oauth/pull/65) Use url safe base64 when decoding jwt claims from id tokens. 26 | 27 | ## [0.9.2] - 2023-04-25 28 | ### Fixed 29 | - [PR#63](https://github.com/EmbarkStudios/tame-oauth/pull/63) Use correct base64 padding when decoding jwt claims from id tokens. 30 | 31 | ## [0.9.1] - 2023-03-29 32 | ### Added 33 | - Support for id tokens, a new trait for this was added (`IdTokenProvider`) and implemented for all current token providers so both access tokens and id tokens can be fetched. 34 | - Added `is_*_provider` methods to `TokenProviderWrapper` for asserting the inner type. 35 | - [PR#61](https://github.com/EmbarkStudios/tame-oauth/pull/61) added debug implementations for all the providers (excludes sensitive data in the output). 36 | 37 | ### Changed 38 | - `RequestReason::ScopesChanged` was renamed to `RequestReason::ParametersChanged` 39 | - [PR#59](https://github.com/EmbarkStudios/tame-oauth/pull/59) update outdated base64 dependency 40 | - Moved the placement of the `CachedTokenProvider` on `TokenProviderWrapper` so that it wraps the outer type instead of the inner, that way the uncached provider can be accessed via `.inner()`. 41 | 42 | ### Fixed 43 | - [PR#57](https://github.com/EmbarkStudios/tame-oauth/pull/57) Documentation improvements 44 | 45 | ## 0.9.0 - 2023-03-29 46 | Release failed and was yanked. Released as 0.9.1 instead. 47 | 48 | ## [0.8.1] - 2023-01-10 49 | ### Fixed 50 | - [PR#54](https://github.com/EmbarkStudios/tame-oauth/pull/54) re-adds `get_account_info` to the outer `ServiceAccountProvider` implementation. It was accidentally removed in #51. 51 | 52 | ## [0.8.0] - 2023-01-10 53 | ### Changed 54 | - [PR#51](https://github.com/EmbarkStudios/tame-oauth/pull/51) moved the token cache out of `ServiceAccountProvider` into a public type, and added a cached token provider that can wrap any other token provider. This wrapper now wraps all the current gcp token providers, making them cached by default. 55 | - [PR#53](https://github.com/EmbarkStudios/tame-oauth/pull/53) changed the cache lock from a Mutex into a RwLock. 56 | 57 | ## [0.7.0] - 2022-02-02 58 | ### Changed 59 | - [PR#47](https://github.com/EmbarkStudios/tame-oauth/pull/47) removed the dependency upon `chrono` as it was overkill and brought in multiple security advisories and is only lightly maintained. 60 | 61 | ## [0.6.0] - 2021-08-07 62 | ### Added 63 | - [PR#40](https://github.com/EmbarkStudios/tame-oauth/pull/40) added support for [`Metadata Server Auth`](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) so that you can obtain oauth tokens when running inside GCP. Thanks [@boulos](https://github.com/boulos)! 64 | - [PR#42](https://github.com/EmbarkStudios/tame-oauth/pull/42) resolved [#39](https://github.com/EmbarkStudios/tame-oauth/issues/39) by adding support for the same default credentials flow as the the Go [oauth2](https://github.com/golang/oauth2/blob/f6687ab2804cbebdfdeef385bee94918b1ce83de/google/default.go#L111) implementation for Google oauth. This included adding support for `EndUserCredentials`. Thanks [@boulos](https://github.com/boulos)! 65 | 66 | ## [0.5.2] - 2021-06-18 67 | ### Added 68 | - [PR#38](https://github.com/EmbarkStudios/tame-oauth/pull/38) added `ServiceAccountAccess::get_token_with_subject` to allow control over the JWT `subject` field. Thanks [@fosskers](https://github.com/fosskers)! 69 | 70 | ## [0.5.1] - 2021-06-05 71 | ### Removed 72 | - Removed unused dependency on `lock_api`, which was lingering after [PR#21](https://github.com/EmbarkStudios/tame-oauth/pull/21). 73 | 74 | ## [0.5.0] - 2021-06-05 75 | ### Added 76 | - Added new field to `Error::InvalidRsaKey` 77 | - Added `Error::InvalidRsaKeyRejected` variant 78 | - [PR#37](https://github.com/EmbarkStudios/tame-oauth/pull/37) Added new feature `wasm-web`, which enables additional features in `chrono` and `ring` to allow `tame-oauth` to be used in a wasm browser context, as part of a fix for [#36](https://github.com/EmbarkStudios/tame-oauth/issues/36). 79 | 80 | ### Changed 81 | - Changed name of `Error::AuthError` to `Error::Auth` 82 | - [PR#37](https://github.com/EmbarkStudios/tame-oauth/pull/37) replaced the usage of `parking_lot::Mutex` with just regular `std::sync::Mutex` as part of the fix for [#36](https://github.com/EmbarkStudios/tame-oauth/issues/36), this includes adding `Error::Poisoned`. 83 | 84 | ### Removed 85 | - Removed `Error:Io` as it was never actually used. 86 | 87 | ## [0.4.7] - 2021-01-18 88 | ### Changed 89 | - Updated `base64` to `0.13`, matching the version used by rustls 90 | 91 | ## [0.4.6] - 2021-01-09 92 | ### Changed 93 | - Updated url to 2.2 94 | 95 | ## [0.4.5] - 2020-10-30 96 | ### Added 97 | - Added `ServiceAccountAccess::get_account_info`. 98 | 99 | ## [0.4.4] - 2020-10-10 100 | ### Fixed 101 | - [#21](https://github.com/EmbarkStudios/tame-oauth/pull/21) Fixed a rather serious bug [#20](https://github.com/EmbarkStudios/tame-oauth/issues/20) due to a terribly implemented spinlock. Thanks for the report [@fasterthanlime](https://github.com/fasterthanlime)! 102 | 103 | ## [0.4.3] - 2020-06-04 104 | ### Changed 105 | - Updated dependencies 106 | 107 | ## [0.4.2] - 2020-01-21 108 | ### Changed 109 | - Updated dependencies 110 | - Made `svc_account` example async 111 | 112 | ## [0.4.1] - 2019-12-20 113 | ### Removed 114 | - Removed `bytes` dependency which was only used by the svc_account example 115 | 116 | ## [0.4.0] - 2019-12-20 117 | ### Changed 118 | - Upgraded `http` to `0.2.0` 119 | 120 | ## [0.3.1] - 2019-12-05 121 | ### Changed 122 | - Updated several dependencies 123 | 124 | ## [0.3.0] - 2019-10-10 125 | ### Changed 126 | - Upgraded `ring` to `0.16.9` 127 | 128 | ### Removed 129 | - Removed use of failure 130 | 131 | ## [0.2.1] - 2019-07-15 132 | ### Changed 133 | - Updated `parking_lot`. 134 | 135 | ## [0.2.0] - 2019-07-03 136 | ### Added 137 | - Fleshed out documentation. 138 | - Added prelude for `gcp` 139 | 140 | ### Fixed 141 | - Correctly used rustls in tests/examples. 142 | 143 | ## [0.1.0] - 2019-07-02 144 | ### Added 145 | - Initial add of `tame-oauth` 146 | 147 | 148 | [Unreleased]: https://github.com/EmbarkStudios/tame-oauth/compare/0.10.0...HEAD 149 | [0.10.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.9.6...0.10.0 150 | [0.9.6]: https://github.com/EmbarkStudios/tame-oauth/compare/0.9.4...0.9.6 151 | [0.9.4]: https://github.com/EmbarkStudios/tame-oauth/compare/0.9.3...0.9.4 152 | [0.9.3]: https://github.com/EmbarkStudios/tame-oauth/compare/0.9.2...0.9.3 153 | [0.9.2]: https://github.com/EmbarkStudios/tame-oauth/compare/0.9.1...0.9.2 154 | [0.9.1]: https://github.com/EmbarkStudios/tame-oauth/compare/0.8.1...0.9.1 155 | [0.8.1]: https://github.com/EmbarkStudios/tame-oauth/compare/0.8.0...0.8.1 156 | [0.8.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.7.0...0.8.0 157 | [0.7.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.6.0...0.7.0 158 | [0.6.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.5.2...0.6.0 159 | [0.5.2]: https://github.com/EmbarkStudios/tame-oauth/compare/0.5.1...0.5.2 160 | [0.5.1]: https://github.com/EmbarkStudios/tame-oauth/compare/0.5.0...0.5.1 161 | [0.5.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.7...0.5.0 162 | [0.4.7]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.6...0.4.7 163 | [0.4.6]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.5...0.4.6 164 | [0.4.5]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.4...0.4.5 165 | [0.4.4]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.3...0.4.4 166 | [0.4.3]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.2...0.4.3 167 | [0.4.2]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.1...0.4.2 168 | [0.4.1]: https://github.com/EmbarkStudios/tame-oauth/compare/0.4.0...0.4.1 169 | [0.4.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.3.1...0.4.0 170 | [0.3.1]: https://github.com/EmbarkStudios/tame-oauth/compare/0.3.0...0.3.1 171 | [0.3.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.2.1...0.3.0 172 | [0.2.1]: https://github.com/EmbarkStudios/tame-oauth/compare/0.2.0...0.2.1 173 | [0.2.0]: https://github.com/EmbarkStudios/tame-oauth/compare/0.1.0...0.2.0 174 | [0.1.0]: https://github.com/EmbarkStudios/tame-oauth/releases/tag/0.1.0 175 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | opensource@embark-studios.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Embark Contributor Guidelines 2 | 3 | Welcome! This project is created by the team at [Embark Studios](https://embark.games). We're glad you're interested in contributing! We welcome contributions from people of all backgrounds who are interested in making great software with us. 4 | 5 | At Embark, we aspire to empower everyone to create interactive experiences. To do this, we're exploring and pushing the boundaries of new technologies, and sharing our learnings with the open source community. 6 | 7 | If you have any difficulties getting involved or finding answers to your questions, please don't hesitate to ask your questions in our [Discord server](https://discord.com/invite/8TW9nfF). 8 | 9 | If you have ideas for collaboration, email us at opensource@embark-studios.com. 10 | 11 | We're also hiring full-time engineers to work with us in Stockholm! Check out our current job postings [here](https://www.embark-studios.com/jobs). 12 | 13 | ## Issues 14 | 15 | ### Feature Requests 16 | 17 | If you have ideas or how to improve our projects, you can suggest features by opening a GitHub issue. Make sure to include details about the feature or change, and describe any uses cases it would enable. 18 | 19 | Feature requests will be tagged as `enhancement` and their status will be updated in the comments of the issue. 20 | 21 | ### Bugs 22 | 23 | When reporting a bug or unexpected behaviour in a project, make sure your issue describes steps to reproduce the behaviour, including the platform you were using, what steps you took, and any error messages. 24 | 25 | Reproducible bugs will be tagged as `bug` and their status will be updated in the comments of the issue. 26 | 27 | ### Wontfix 28 | 29 | Issues will be closed and tagged as `wontfix` if we decide that we do not wish to implement it, usually due to being misaligned with the project vision or out of scope. We will comment on the issue with more detailed reasoning. 30 | 31 | ## Contribution Workflow 32 | 33 | ### Open Issues 34 | 35 | If you're ready to contribute, start by looking at our open issues tagged as [`help wanted`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted") or [`good first issue`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue"). 36 | 37 | You can comment on the issue to let others know you're interested in working on it or to ask questions. 38 | 39 | ### Making Changes 40 | 41 | 1. Fork the repository. 42 | 43 | 2. Create a new feature branch. 44 | 45 | 3. Make your changes. Ensure that there are no build errors by running the project with your changes locally. 46 | 47 | 4. Open a pull request with a name and description of what you did. You can read more about working with pull requests on GitHub [here](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork). 48 | 49 | 5. A maintainer will review your pull request and may ask you to make changes. 50 | 51 | ## Licensing 52 | 53 | Unless otherwise specified, all Embark open source projects are licensed under a dual MIT OR Apache-2.0 license, allowing licensees to chose either at their option. You can read more in each project's respective README. 54 | 55 | ## Code of Conduct 56 | 57 | Please note that our projects are released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md) to ensure that they are welcoming places for everyone to contribute. By participating in any Embark open source project, you agree to abide by these terms. 58 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "backtrace" 22 | version = "0.3.69" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 25 | dependencies = [ 26 | "addr2line", 27 | "cc", 28 | "cfg-if", 29 | "libc", 30 | "miniz_oxide", 31 | "object", 32 | "rustc-demangle", 33 | ] 34 | 35 | [[package]] 36 | name = "base64" 37 | version = "0.21.7" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 40 | 41 | [[package]] 42 | name = "bumpalo" 43 | version = "3.15.4" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" 46 | 47 | [[package]] 48 | name = "bytes" 49 | version = "1.5.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 52 | 53 | [[package]] 54 | name = "cc" 55 | version = "1.0.90" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "data-encoding" 67 | version = "2.5.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" 70 | 71 | [[package]] 72 | name = "fnv" 73 | version = "1.0.7" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 76 | 77 | [[package]] 78 | name = "form_urlencoded" 79 | version = "1.2.1" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 82 | dependencies = [ 83 | "percent-encoding", 84 | ] 85 | 86 | [[package]] 87 | name = "futures-channel" 88 | version = "0.3.30" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 91 | dependencies = [ 92 | "futures-core", 93 | ] 94 | 95 | [[package]] 96 | name = "futures-core" 97 | version = "0.3.30" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 100 | 101 | [[package]] 102 | name = "futures-task" 103 | version = "0.3.30" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 106 | 107 | [[package]] 108 | name = "futures-util" 109 | version = "0.3.30" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 112 | dependencies = [ 113 | "futures-core", 114 | "futures-task", 115 | "pin-project-lite", 116 | "pin-utils", 117 | ] 118 | 119 | [[package]] 120 | name = "getrandom" 121 | version = "0.2.12" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 124 | dependencies = [ 125 | "cfg-if", 126 | "js-sys", 127 | "libc", 128 | "wasi", 129 | "wasm-bindgen", 130 | ] 131 | 132 | [[package]] 133 | name = "gimli" 134 | version = "0.28.1" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 137 | 138 | [[package]] 139 | name = "hermit-abi" 140 | version = "0.3.9" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 143 | 144 | [[package]] 145 | name = "http" 146 | version = "1.1.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 149 | dependencies = [ 150 | "bytes", 151 | "fnv", 152 | "itoa", 153 | ] 154 | 155 | [[package]] 156 | name = "http-body" 157 | version = "1.0.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 160 | dependencies = [ 161 | "bytes", 162 | "http", 163 | ] 164 | 165 | [[package]] 166 | name = "http-body-util" 167 | version = "0.1.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" 170 | dependencies = [ 171 | "bytes", 172 | "futures-core", 173 | "http", 174 | "http-body", 175 | "pin-project-lite", 176 | ] 177 | 178 | [[package]] 179 | name = "httparse" 180 | version = "1.8.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 183 | 184 | [[package]] 185 | name = "hyper" 186 | version = "1.2.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" 189 | dependencies = [ 190 | "bytes", 191 | "futures-channel", 192 | "futures-util", 193 | "http", 194 | "http-body", 195 | "httparse", 196 | "itoa", 197 | "pin-project-lite", 198 | "smallvec", 199 | "tokio", 200 | "want", 201 | ] 202 | 203 | [[package]] 204 | name = "hyper-rustls" 205 | version = "0.26.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" 208 | dependencies = [ 209 | "futures-util", 210 | "http", 211 | "hyper", 212 | "hyper-util", 213 | "rustls", 214 | "rustls-pki-types", 215 | "tokio", 216 | "tokio-rustls", 217 | "tower-service", 218 | ] 219 | 220 | [[package]] 221 | name = "hyper-util" 222 | version = "0.1.3" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" 225 | dependencies = [ 226 | "bytes", 227 | "futures-channel", 228 | "futures-util", 229 | "http", 230 | "http-body", 231 | "hyper", 232 | "pin-project-lite", 233 | "socket2", 234 | "tokio", 235 | "tower", 236 | "tower-service", 237 | "tracing", 238 | ] 239 | 240 | [[package]] 241 | name = "idna" 242 | version = "0.5.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 245 | dependencies = [ 246 | "unicode-bidi", 247 | "unicode-normalization", 248 | ] 249 | 250 | [[package]] 251 | name = "ipnet" 252 | version = "2.9.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 255 | 256 | [[package]] 257 | name = "itoa" 258 | version = "1.0.10" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 261 | 262 | [[package]] 263 | name = "js-sys" 264 | version = "0.3.69" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 267 | dependencies = [ 268 | "wasm-bindgen", 269 | ] 270 | 271 | [[package]] 272 | name = "libc" 273 | version = "0.2.153" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 276 | 277 | [[package]] 278 | name = "log" 279 | version = "0.4.21" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 282 | 283 | [[package]] 284 | name = "memchr" 285 | version = "2.7.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 288 | 289 | [[package]] 290 | name = "mime" 291 | version = "0.3.17" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 294 | 295 | [[package]] 296 | name = "miniz_oxide" 297 | version = "0.7.2" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 300 | dependencies = [ 301 | "adler", 302 | ] 303 | 304 | [[package]] 305 | name = "mio" 306 | version = "0.8.11" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 309 | dependencies = [ 310 | "libc", 311 | "wasi", 312 | "windows-sys 0.48.0", 313 | ] 314 | 315 | [[package]] 316 | name = "num_cpus" 317 | version = "1.16.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 320 | dependencies = [ 321 | "hermit-abi", 322 | "libc", 323 | ] 324 | 325 | [[package]] 326 | name = "object" 327 | version = "0.32.2" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 330 | dependencies = [ 331 | "memchr", 332 | ] 333 | 334 | [[package]] 335 | name = "once_cell" 336 | version = "1.19.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 339 | 340 | [[package]] 341 | name = "percent-encoding" 342 | version = "2.3.1" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 345 | 346 | [[package]] 347 | name = "pin-project" 348 | version = "1.1.5" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 351 | dependencies = [ 352 | "pin-project-internal", 353 | ] 354 | 355 | [[package]] 356 | name = "pin-project-internal" 357 | version = "1.1.5" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 360 | dependencies = [ 361 | "proc-macro2", 362 | "quote", 363 | "syn", 364 | ] 365 | 366 | [[package]] 367 | name = "pin-project-lite" 368 | version = "0.2.13" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 371 | 372 | [[package]] 373 | name = "pin-utils" 374 | version = "0.1.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 377 | 378 | [[package]] 379 | name = "proc-macro2" 380 | version = "1.0.79" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 383 | dependencies = [ 384 | "unicode-ident", 385 | ] 386 | 387 | [[package]] 388 | name = "quote" 389 | version = "1.0.35" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 392 | dependencies = [ 393 | "proc-macro2", 394 | ] 395 | 396 | [[package]] 397 | name = "reqwest" 398 | version = "0.12.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88" 401 | dependencies = [ 402 | "base64", 403 | "bytes", 404 | "futures-core", 405 | "futures-util", 406 | "http", 407 | "http-body", 408 | "http-body-util", 409 | "hyper", 410 | "hyper-rustls", 411 | "hyper-util", 412 | "ipnet", 413 | "js-sys", 414 | "log", 415 | "mime", 416 | "once_cell", 417 | "percent-encoding", 418 | "pin-project-lite", 419 | "rustls", 420 | "rustls-pemfile", 421 | "rustls-pki-types", 422 | "serde", 423 | "serde_json", 424 | "serde_urlencoded", 425 | "sync_wrapper", 426 | "tokio", 427 | "tokio-rustls", 428 | "tower-service", 429 | "url", 430 | "wasm-bindgen", 431 | "wasm-bindgen-futures", 432 | "web-sys", 433 | "webpki-roots", 434 | "winreg", 435 | ] 436 | 437 | [[package]] 438 | name = "ring" 439 | version = "0.17.8" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 442 | dependencies = [ 443 | "cc", 444 | "cfg-if", 445 | "getrandom", 446 | "libc", 447 | "spin", 448 | "untrusted", 449 | "windows-sys 0.52.0", 450 | ] 451 | 452 | [[package]] 453 | name = "rustc-demangle" 454 | version = "0.1.23" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 457 | 458 | [[package]] 459 | name = "rustls" 460 | version = "0.22.4" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" 463 | dependencies = [ 464 | "log", 465 | "ring", 466 | "rustls-pki-types", 467 | "rustls-webpki", 468 | "subtle", 469 | "zeroize", 470 | ] 471 | 472 | [[package]] 473 | name = "rustls-pemfile" 474 | version = "1.0.4" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 477 | dependencies = [ 478 | "base64", 479 | ] 480 | 481 | [[package]] 482 | name = "rustls-pki-types" 483 | version = "1.3.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" 486 | 487 | [[package]] 488 | name = "rustls-webpki" 489 | version = "0.102.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" 492 | dependencies = [ 493 | "ring", 494 | "rustls-pki-types", 495 | "untrusted", 496 | ] 497 | 498 | [[package]] 499 | name = "ryu" 500 | version = "1.0.17" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 503 | 504 | [[package]] 505 | name = "serde" 506 | version = "1.0.197" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 509 | dependencies = [ 510 | "serde_derive", 511 | ] 512 | 513 | [[package]] 514 | name = "serde_derive" 515 | version = "1.0.197" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 518 | dependencies = [ 519 | "proc-macro2", 520 | "quote", 521 | "syn", 522 | ] 523 | 524 | [[package]] 525 | name = "serde_json" 526 | version = "1.0.114" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 529 | dependencies = [ 530 | "itoa", 531 | "ryu", 532 | "serde", 533 | ] 534 | 535 | [[package]] 536 | name = "serde_urlencoded" 537 | version = "0.7.1" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 540 | dependencies = [ 541 | "form_urlencoded", 542 | "itoa", 543 | "ryu", 544 | "serde", 545 | ] 546 | 547 | [[package]] 548 | name = "smallvec" 549 | version = "1.13.2" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 552 | 553 | [[package]] 554 | name = "socket2" 555 | version = "0.5.6" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 558 | dependencies = [ 559 | "libc", 560 | "windows-sys 0.52.0", 561 | ] 562 | 563 | [[package]] 564 | name = "spin" 565 | version = "0.9.8" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 568 | 569 | [[package]] 570 | name = "static_assertions" 571 | version = "1.1.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 574 | 575 | [[package]] 576 | name = "subtle" 577 | version = "2.5.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 580 | 581 | [[package]] 582 | name = "syn" 583 | version = "2.0.53" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" 586 | dependencies = [ 587 | "proc-macro2", 588 | "quote", 589 | "unicode-ident", 590 | ] 591 | 592 | [[package]] 593 | name = "sync_wrapper" 594 | version = "0.1.2" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 597 | 598 | [[package]] 599 | name = "tame-oauth" 600 | version = "0.10.0" 601 | dependencies = [ 602 | "bytes", 603 | "data-encoding", 604 | "http", 605 | "reqwest", 606 | "ring", 607 | "serde", 608 | "serde_json", 609 | "tokio", 610 | "twox-hash", 611 | "url", 612 | ] 613 | 614 | [[package]] 615 | name = "tinyvec" 616 | version = "1.6.0" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 619 | dependencies = [ 620 | "tinyvec_macros", 621 | ] 622 | 623 | [[package]] 624 | name = "tinyvec_macros" 625 | version = "0.1.1" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 628 | 629 | [[package]] 630 | name = "tokio" 631 | version = "1.36.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" 634 | dependencies = [ 635 | "backtrace", 636 | "bytes", 637 | "libc", 638 | "mio", 639 | "num_cpus", 640 | "pin-project-lite", 641 | "socket2", 642 | "tokio-macros", 643 | "windows-sys 0.48.0", 644 | ] 645 | 646 | [[package]] 647 | name = "tokio-macros" 648 | version = "2.2.0" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 651 | dependencies = [ 652 | "proc-macro2", 653 | "quote", 654 | "syn", 655 | ] 656 | 657 | [[package]] 658 | name = "tokio-rustls" 659 | version = "0.25.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" 662 | dependencies = [ 663 | "rustls", 664 | "rustls-pki-types", 665 | "tokio", 666 | ] 667 | 668 | [[package]] 669 | name = "tower" 670 | version = "0.4.13" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 673 | dependencies = [ 674 | "futures-core", 675 | "futures-util", 676 | "pin-project", 677 | "pin-project-lite", 678 | "tokio", 679 | "tower-layer", 680 | "tower-service", 681 | "tracing", 682 | ] 683 | 684 | [[package]] 685 | name = "tower-layer" 686 | version = "0.3.2" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 689 | 690 | [[package]] 691 | name = "tower-service" 692 | version = "0.3.2" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 695 | 696 | [[package]] 697 | name = "tracing" 698 | version = "0.1.40" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 701 | dependencies = [ 702 | "log", 703 | "pin-project-lite", 704 | "tracing-core", 705 | ] 706 | 707 | [[package]] 708 | name = "tracing-core" 709 | version = "0.1.32" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 712 | dependencies = [ 713 | "once_cell", 714 | ] 715 | 716 | [[package]] 717 | name = "try-lock" 718 | version = "0.2.5" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 721 | 722 | [[package]] 723 | name = "twox-hash" 724 | version = "1.6.3" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" 727 | dependencies = [ 728 | "cfg-if", 729 | "static_assertions", 730 | ] 731 | 732 | [[package]] 733 | name = "unicode-bidi" 734 | version = "0.3.15" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 737 | 738 | [[package]] 739 | name = "unicode-ident" 740 | version = "1.0.12" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 743 | 744 | [[package]] 745 | name = "unicode-normalization" 746 | version = "0.1.23" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 749 | dependencies = [ 750 | "tinyvec", 751 | ] 752 | 753 | [[package]] 754 | name = "untrusted" 755 | version = "0.9.0" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 758 | 759 | [[package]] 760 | name = "url" 761 | version = "2.5.0" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 764 | dependencies = [ 765 | "form_urlencoded", 766 | "idna", 767 | "percent-encoding", 768 | ] 769 | 770 | [[package]] 771 | name = "want" 772 | version = "0.3.1" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 775 | dependencies = [ 776 | "try-lock", 777 | ] 778 | 779 | [[package]] 780 | name = "wasi" 781 | version = "0.11.0+wasi-snapshot-preview1" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 784 | 785 | [[package]] 786 | name = "wasm-bindgen" 787 | version = "0.2.92" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 790 | dependencies = [ 791 | "cfg-if", 792 | "wasm-bindgen-macro", 793 | ] 794 | 795 | [[package]] 796 | name = "wasm-bindgen-backend" 797 | version = "0.2.92" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 800 | dependencies = [ 801 | "bumpalo", 802 | "log", 803 | "once_cell", 804 | "proc-macro2", 805 | "quote", 806 | "syn", 807 | "wasm-bindgen-shared", 808 | ] 809 | 810 | [[package]] 811 | name = "wasm-bindgen-futures" 812 | version = "0.4.42" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 815 | dependencies = [ 816 | "cfg-if", 817 | "js-sys", 818 | "wasm-bindgen", 819 | "web-sys", 820 | ] 821 | 822 | [[package]] 823 | name = "wasm-bindgen-macro" 824 | version = "0.2.92" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 827 | dependencies = [ 828 | "quote", 829 | "wasm-bindgen-macro-support", 830 | ] 831 | 832 | [[package]] 833 | name = "wasm-bindgen-macro-support" 834 | version = "0.2.92" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 837 | dependencies = [ 838 | "proc-macro2", 839 | "quote", 840 | "syn", 841 | "wasm-bindgen-backend", 842 | "wasm-bindgen-shared", 843 | ] 844 | 845 | [[package]] 846 | name = "wasm-bindgen-shared" 847 | version = "0.2.92" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 850 | 851 | [[package]] 852 | name = "web-sys" 853 | version = "0.3.69" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 856 | dependencies = [ 857 | "js-sys", 858 | "wasm-bindgen", 859 | ] 860 | 861 | [[package]] 862 | name = "webpki-roots" 863 | version = "0.26.1" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" 866 | dependencies = [ 867 | "rustls-pki-types", 868 | ] 869 | 870 | [[package]] 871 | name = "windows-sys" 872 | version = "0.48.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 875 | dependencies = [ 876 | "windows-targets 0.48.5", 877 | ] 878 | 879 | [[package]] 880 | name = "windows-sys" 881 | version = "0.52.0" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 884 | dependencies = [ 885 | "windows-targets 0.52.4", 886 | ] 887 | 888 | [[package]] 889 | name = "windows-targets" 890 | version = "0.48.5" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 893 | dependencies = [ 894 | "windows_aarch64_gnullvm 0.48.5", 895 | "windows_aarch64_msvc 0.48.5", 896 | "windows_i686_gnu 0.48.5", 897 | "windows_i686_msvc 0.48.5", 898 | "windows_x86_64_gnu 0.48.5", 899 | "windows_x86_64_gnullvm 0.48.5", 900 | "windows_x86_64_msvc 0.48.5", 901 | ] 902 | 903 | [[package]] 904 | name = "windows-targets" 905 | version = "0.52.4" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 908 | dependencies = [ 909 | "windows_aarch64_gnullvm 0.52.4", 910 | "windows_aarch64_msvc 0.52.4", 911 | "windows_i686_gnu 0.52.4", 912 | "windows_i686_msvc 0.52.4", 913 | "windows_x86_64_gnu 0.52.4", 914 | "windows_x86_64_gnullvm 0.52.4", 915 | "windows_x86_64_msvc 0.52.4", 916 | ] 917 | 918 | [[package]] 919 | name = "windows_aarch64_gnullvm" 920 | version = "0.48.5" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 923 | 924 | [[package]] 925 | name = "windows_aarch64_gnullvm" 926 | version = "0.52.4" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 929 | 930 | [[package]] 931 | name = "windows_aarch64_msvc" 932 | version = "0.48.5" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 935 | 936 | [[package]] 937 | name = "windows_aarch64_msvc" 938 | version = "0.52.4" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 941 | 942 | [[package]] 943 | name = "windows_i686_gnu" 944 | version = "0.48.5" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 947 | 948 | [[package]] 949 | name = "windows_i686_gnu" 950 | version = "0.52.4" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 953 | 954 | [[package]] 955 | name = "windows_i686_msvc" 956 | version = "0.48.5" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 959 | 960 | [[package]] 961 | name = "windows_i686_msvc" 962 | version = "0.52.4" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 965 | 966 | [[package]] 967 | name = "windows_x86_64_gnu" 968 | version = "0.48.5" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 971 | 972 | [[package]] 973 | name = "windows_x86_64_gnu" 974 | version = "0.52.4" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 977 | 978 | [[package]] 979 | name = "windows_x86_64_gnullvm" 980 | version = "0.48.5" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 983 | 984 | [[package]] 985 | name = "windows_x86_64_gnullvm" 986 | version = "0.52.4" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 989 | 990 | [[package]] 991 | name = "windows_x86_64_msvc" 992 | version = "0.48.5" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 995 | 996 | [[package]] 997 | name = "windows_x86_64_msvc" 998 | version = "0.52.4" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1001 | 1002 | [[package]] 1003 | name = "winreg" 1004 | version = "0.50.0" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1007 | dependencies = [ 1008 | "cfg-if", 1009 | "windows-sys 0.48.0", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "zeroize" 1014 | version = "1.7.0" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 1017 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tame-oauth" 3 | version = "0.10.0" 4 | authors = [ 5 | "Embark ", 6 | "Jake Shadle ", 7 | ] 8 | edition = "2018" 9 | description = "A (very) simple oauth 2.0 library" 10 | license = "MIT OR Apache-2.0" 11 | documentation = "https://docs.rs/tame-oauth" 12 | homepage = "https://github.com/EmbarkStudios/tame-oauth" 13 | repository = "https://github.com/EmbarkStudios/tame-oauth" 14 | keywords = ["oauth", "tame", "sans-io", "gcp"] 15 | categories = ["authentication"] 16 | readme = "README.md" 17 | 18 | [badges] 19 | maintenance = { status = "actively-developed" } 20 | 21 | [lib] 22 | doctest = false 23 | path = "src/lib.rs" 24 | 25 | [features] 26 | # This library was first created to support GCP oauth, if we add support for 27 | # other oauth providers this will most likely change to not have any default features 28 | default = ["gcp"] 29 | # Supports for GCP oauth2 30 | gcp = ["jwt", "url"] 31 | # Support for Json Web Tokens, ring is used for signing 32 | jwt = ["ring"] 33 | # This enables features in chrono and ring that are necessary to use this library 34 | # in a wasm32 web (browser) context. If you are using wasm outside the browser 35 | # you will need to target wasm32-wasi for the requisite functionality (time and random) 36 | wasm-web = ["ring/wasm32_unknown_unknown_js"] 37 | 38 | [dependencies] 39 | data-encoding = "2.4" 40 | http = "1.1" 41 | ring = { version = "0.17", optional = true } 42 | serde = { version = "1.0", features = ["derive"] } 43 | serde_json = "1.0" 44 | twox-hash = { version = "1.5.0", default-features = false } 45 | url = { version = "2.2", optional = true } 46 | 47 | [dev-dependencies.reqwest] 48 | version = "0.12" 49 | default-features = false 50 | features = ["rustls-tls"] 51 | 52 | [dev-dependencies.tokio] 53 | version = "1.0" 54 | features = ["macros", "rt-multi-thread"] 55 | 56 | [dev-dependencies.bytes] 57 | version = "1.4" 58 | -------------------------------------------------------------------------------- /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) 2019 Embark Studios 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 |
2 | 3 | # `🔐 tame-oauth` 4 | 5 | [![Embark](https://img.shields.io/badge/embark-open%20source-blueviolet.svg)](http://embark.games) 6 | [![Embark](https://img.shields.io/badge/discord-embark-%237289da.svg?logo=discord)](https://discord.gg/dAuKfZS) 7 | [![Crates.io](https://img.shields.io/crates/v/tame-oauth.svg)](https://crates.io/crates/tame-oauth) 8 | [![Docs](https://docs.rs/tame-oauth/badge.svg)](https://docs.rs/tame-oauth) 9 | [![dependency status](https://deps.rs/repo/github/EmbarkStudios/tame-oauth/status.svg)](https://deps.rs/repo/github/EmbarkStudios/tame-oauth) 10 | [![Build status](https://github.com/gleam-lang/gleam/workflows/ci/badge.svg?branch=main)](https://github.com/EmbarkStudios/tame-oauth/actions) 11 | 12 | `tame-oauth` is a small oauth crate that follows the [sans-io](https://sans-io.readthedocs.io/) approach. 13 | 14 |
15 | 16 | ## Why? 17 | 18 | * You want to control how you actually make oauth HTTP requests 19 | 20 | ## Why not? 21 | 22 | * The only auth flows that is currently implemented is the service account, user credentials and metadata server flow for GCP. Other flows can be added, but right now GCP is the only provider we need. 23 | * There are several other oauth crates available that have many more features and are easier to work with, if you don't care about what HTTP clients they use. 24 | * This crate requires more boilerplate to use. 25 | 26 | ## Features 27 | 28 | * `gcp` (default) - Support for [GCP oauth2](https://developers.google.com/identity/protocols/oauth2) 29 | * `wasm-web` - Enables wasm features in `ring` needed for `tame-oauth` to be used in a wasm browser context. Note this feature should not be used when targeting wasm outside the browser context, in which case you would likely need to target `wasm32-wasi`. 30 | * `jwt` (default) - Support for [JSON Web Tokens](https://jwt.io/), required for `gcp` 31 | * `url` (default) - Url parsing, required for `gcp` 32 | 33 | ## Examples 34 | 35 | ### [svc_account](examples/svc_account.rs) 36 | 37 | Usage: `cargo run --example svc_account -- ` 38 | 39 | A small example of using `tame-oauth` together with [reqwest](https://github.com/seanmonstar/reqwest). Given a key file and 1 or more scopes, it will attempt to get a token that could be used to access resources in those scopes. 40 | 41 | `cargo run --example svc_account -- ~/.secrets/super-sekret.json https://www.googleapis.com/auth/pubsub https://www.googleapis.com/auth/devstorage.read_only` 42 | 43 | ### [default_creds](examples/default_creds.rs) 44 | 45 | Usage: `cargo run --example default_creds -- ` 46 | 47 | Attempts to find and use the default credentials to get a token. Note that scopes are not used in all cases as eg. end user credentials only ever have the cloud platform scope. 48 | 49 | `cargo run --example default_creds -- https://www.googleapis.com/auth/devstorage.read_only` 50 | 51 | ## Contributing 52 | 53 | [![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4-ff69b4.svg)](../CODE_OF_CONDUCT.md) 54 | 55 | We welcome community contributions to this project. 56 | 57 | Please read our [Contributor Guide](CONTRIBUTING.md) for more information on how to get started. 58 | 59 | ## License 60 | 61 | Licensed under either of 62 | 63 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 64 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 65 | 66 | at your option. 67 | 68 | ### Contribution 69 | 70 | Unless you explicitly state otherwise, any contribution intentionally 71 | submitted for inclusion in the work by you, as defined in the Apache-2.0 72 | license, shall be dual licensed as above, without any additional terms or 73 | conditions. 74 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | targets = [ 3 | "x86_64-unknown-linux-gnu", 4 | "x86_64-unknown-linux-musl", 5 | "x86_64-pc-windows-msvc", 6 | "x86_64-apple-darwin", 7 | ] 8 | all-features = true 9 | 10 | [advisories] 11 | version = 2 12 | ignore = [] 13 | 14 | [bans] 15 | multiple-versions = "deny" 16 | deny = ["openssl", "openssl-sys"] 17 | 18 | [licenses] 19 | version = 2 20 | # We want really high confidence when inferring licenses from text 21 | confidence-threshold = 0.92 22 | allow = ["Apache-2.0", "MIT", "Unicode-DFS-2016"] 23 | exceptions = [ 24 | { allow = [ 25 | "ISC", 26 | ], name = "untrusted" }, 27 | { allow = [ 28 | "ISC", 29 | "MIT", 30 | "OpenSSL", 31 | ], name = "ring" }, 32 | { allow = [ 33 | "Zlib", 34 | ], name = "tinyvec" }, 35 | ] 36 | 37 | [[licenses.clarify]] 38 | name = "ring" 39 | # SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses 40 | # https://spdx.org/licenses/OpenSSL.html 41 | # ISC - Both BoringSSL and ring use this for their new files 42 | # MIT - "Files in third_party/ have their own licenses, as described therein. The MIT 43 | # license, for third_party/fiat, which, unlike other third_party directories, is 44 | # compiled into non-test libraries, is included below." 45 | # OpenSSL - Obviously 46 | expression = "ISC AND MIT AND OpenSSL" 47 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 48 | 49 | [[licenses.clarify]] 50 | name = "encoding_rs" 51 | expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause" 52 | license-files = [{ path = "COPYRIGHT", hash = 0x39f8ad31 }] 53 | 54 | [[licenses.clarify]] 55 | name = "webpki" 56 | expression = "ISC" 57 | license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] 58 | 59 | [[licenses.clarify]] 60 | name = "rustls-webpki" 61 | expression = "ISC" 62 | license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] 63 | -------------------------------------------------------------------------------- /examples/default_creds.rs: -------------------------------------------------------------------------------- 1 | use tame_oauth::gcp::*; 2 | 3 | // This example shows the basics for creating a token provider for the default 4 | // credentials on the system. If you want to use a service account, set 5 | // `GOOGLE_APPLICATION_CREDENTIALS` to a service account key path, if have 6 | // gcloud installed, you can just run this as is and it will work as long as 7 | // you have done `gcloud auth application-default login` previously, and that 8 | // token hasn't expired 9 | #[tokio::main] 10 | async fn main() { 11 | let scopes: Vec<_> = std::env::args().skip(1).collect(); 12 | 13 | let provider = TokenProviderWrapper::get_default_provider() 14 | .expect("unable to read default token provider") 15 | .expect("unable to find default token provider"); 16 | 17 | println!("Using {}", provider.kind()); 18 | 19 | // Attempt to get a token, since we have never used this accessor 20 | // before, it's guaranteed that we will need to make an HTTPS 21 | // request to the token provider to retrieve a token. This 22 | // will also happen if we want to get a token for a different set 23 | // of scopes, or if the token has expired. 24 | match provider.get_token(&scopes).unwrap() { 25 | TokenOrRequest::Request { 26 | // This is an http::Request that we can use to build 27 | // a client request for whichever HTTP client implementation 28 | // you wish to use 29 | request, 30 | scope_hash, 31 | .. 32 | } => { 33 | let client = reqwest::Client::new(); 34 | 35 | let (parts, body) = request.into_parts(); 36 | let uri = parts.uri.to_string(); 37 | 38 | // This will always be a POST, but for completeness sake... 39 | let builder = match parts.method { 40 | http::Method::GET => client.get(&uri), 41 | http::Method::POST => client.post(&uri), 42 | http::Method::DELETE => client.delete(&uri), 43 | http::Method::PUT => client.put(&uri), 44 | method => unimplemented!("{} not implemented", method), 45 | }; 46 | 47 | // Build the full request from the headers and body that were 48 | // passed to you, without modifying them. 49 | let request = builder.headers(parts.headers).body(body).build().unwrap(); 50 | 51 | // Send the actual request 52 | let response = client.execute(request).await.unwrap(); 53 | 54 | let mut builder = http::Response::builder() 55 | .status(response.status()) 56 | .version(response.version()); 57 | 58 | let headers = builder.headers_mut().unwrap(); 59 | 60 | // Unfortunately http doesn't expose a way to just use 61 | // an existing HeaderMap, so we have to copy them :( 62 | headers.extend( 63 | response 64 | .headers() 65 | .into_iter() 66 | .map(|(k, v)| (k.clone(), v.clone())), 67 | ); 68 | 69 | let buffer = response.bytes().await.unwrap(); 70 | let response = builder.body(buffer).unwrap(); 71 | 72 | provider 73 | .parse_token_response(scope_hash, response) 74 | .expect("invalid token response"); 75 | 76 | println!("cool, we were able to receive a token!"); 77 | } 78 | _ => unreachable!(), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/default_creds_id_token.rs: -------------------------------------------------------------------------------- 1 | use tame_oauth::gcp::*; 2 | 3 | // This example shows the basics for creating a token provider for the default 4 | // credentials on the system. If you want to use a service account, set 5 | // `GOOGLE_APPLICATION_CREDENTIALS` to a service account key path, if have 6 | // gcloud installed, you can just run this as is and it will work as long as 7 | // you have done `gcloud auth application-default login` previously, and that 8 | // token hasn't expired 9 | #[tokio::main] 10 | async fn main() { 11 | let provider = TokenProviderWrapper::get_default_provider() 12 | .expect("unable to read default token provider") 13 | .expect("unable to find default token provider"); 14 | 15 | println!("Using {}", provider.kind()); 16 | 17 | // Attempt to get a token, since we have never used this accessor 18 | // before, it's guaranteed that we will need to make an HTTPS 19 | // request to the token provider to retrieve a token. This 20 | // will also happen if we want to get a token for a different 21 | // audience, or if the token has expired. 22 | match provider.get_id_token("my-audience").unwrap() { 23 | IdTokenOrRequest::IdTokenRequest { 24 | // This is an http::Request that we can use to build 25 | // a client request for whichever HTTP client implementation 26 | // you wish to use 27 | request, 28 | audience_hash, 29 | .. 30 | } => { 31 | let client = reqwest::Client::new(); 32 | 33 | let (parts, body) = request.into_parts(); 34 | let uri = parts.uri.to_string(); 35 | 36 | // This will always be a POST, but for completeness sake... 37 | let builder = match parts.method { 38 | http::Method::GET => client.get(&uri), 39 | http::Method::POST => client.post(&uri), 40 | http::Method::DELETE => client.delete(&uri), 41 | http::Method::PUT => client.put(&uri), 42 | method => unimplemented!("{} not implemented", method), 43 | }; 44 | 45 | // Build the full request from the headers and body that were 46 | // passed to you, without modifying them. 47 | let request = builder.headers(parts.headers).body(body).build().unwrap(); 48 | 49 | // Send the actual request 50 | let response = client.execute(request).await.unwrap(); 51 | 52 | let mut builder = http::Response::builder() 53 | .status(response.status()) 54 | .version(response.version()); 55 | 56 | let headers = builder.headers_mut().unwrap(); 57 | 58 | // Unfortunately http doesn't expose a way to just use 59 | // an existing HeaderMap, so we have to copy them :( 60 | headers.extend( 61 | response 62 | .headers() 63 | .into_iter() 64 | .map(|(k, v)| (k.clone(), v.clone())), 65 | ); 66 | 67 | let buffer = response.bytes().await.unwrap(); 68 | let response = builder.body(buffer).unwrap(); 69 | 70 | let _token = provider 71 | .parse_id_token_response(audience_hash, response) 72 | .expect("invalid token response"); 73 | 74 | println!("cool, we were able to receive a id token!"); 75 | } 76 | _ => unreachable!(), 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/svc_account.rs: -------------------------------------------------------------------------------- 1 | use tame_oauth::gcp::*; 2 | 3 | // This example shows the basics for creating a GCP service account token 4 | // provider and requesting a token from it. This particular example uses the 5 | // reqwest HTTP client, but the point of this crate is that you can use 6 | // whichever one you like as long as you don't mind doing a little bit of 7 | // boilerplate to convert between from http::Request and to http::Response 8 | #[tokio::main] 9 | async fn main() { 10 | let mut args = std::env::args().skip(1); 11 | 12 | let key_path = args 13 | .next() 14 | .expect("expected path to a service account json file"); 15 | let scopes: Vec<_> = args.collect(); 16 | let service_key = std::fs::read_to_string(key_path).expect("failed to read json key"); 17 | 18 | // Deserialize the service account info from the json data 19 | let acct_info = ServiceAccountInfo::deserialize(service_key).unwrap(); 20 | 21 | // Create the token provider! 22 | let sa_provider = ServiceAccountProvider::new(acct_info).unwrap(); 23 | 24 | // Attempt to get a token, since we have never used this accessor 25 | // before, it's guaranteed that we will need to make an HTTPS 26 | // request to the token provider to retrieve a token. This 27 | // will also happen if we want to get a token for a different set 28 | // of scopes, or if the token has expired. 29 | let token = match sa_provider.get_token(&scopes).unwrap() { 30 | TokenOrRequest::Request { 31 | // This is an http::Request that we can use to build 32 | // a client request for whichever HTTP client implementation 33 | // you wish to use 34 | request, 35 | scope_hash, 36 | .. 37 | } => { 38 | let client = reqwest::Client::new(); 39 | 40 | let (parts, body) = request.into_parts(); 41 | let uri = parts.uri.to_string(); 42 | 43 | // This will always be a POST, but for completeness sake... 44 | let builder = match parts.method { 45 | http::Method::GET => client.get(&uri), 46 | http::Method::POST => client.post(&uri), 47 | http::Method::DELETE => client.delete(&uri), 48 | http::Method::PUT => client.put(&uri), 49 | method => unimplemented!("{} not implemented", method), 50 | }; 51 | 52 | // Build the full request from the headers and body that were 53 | // passed to you, without modifying them. 54 | let request = builder.headers(parts.headers).body(body).build().unwrap(); 55 | 56 | // Send the actual request 57 | let response = client.execute(request).await.unwrap(); 58 | 59 | let mut builder = http::Response::builder() 60 | .status(response.status()) 61 | .version(response.version()); 62 | 63 | let headers = builder.headers_mut().unwrap(); 64 | 65 | // Unfortunately http doesn't expose a way to just use 66 | // an existing HeaderMap, so we have to copy them :( 67 | headers.extend( 68 | response 69 | .headers() 70 | .into_iter() 71 | .map(|(k, v)| (k.clone(), v.clone())), 72 | ); 73 | 74 | let buffer = response.bytes().await.unwrap(); 75 | let response = builder.body(buffer).unwrap(); 76 | 77 | // Tell our accessor about the response, also passing 78 | // the scope_hash for the scopes we initially requested, 79 | // this will allow future token requests for those scopes 80 | // to use a cached token, at least until it expires (~1 hour) 81 | sa_provider 82 | .parse_token_response(scope_hash, response) 83 | .unwrap() 84 | } 85 | _ => unreachable!(), 86 | }; 87 | 88 | // Uncomment this if you want to go to lunch and see an unreachable panic 89 | // when you get back 90 | // std::thread::sleep(std::time::Duration::from_secs(60 * 60)) 91 | 92 | // Retrieving a token for the same scopes for which a token has been acquired 93 | // will use the cached token until it expires 94 | match sa_provider.get_token(&scopes).unwrap() { 95 | TokenOrRequest::Token(tk) => { 96 | assert_eq!(tk, token); 97 | println!( 98 | "cool, you were able to retrieve a token for the {:?} scope{}!", 99 | scopes, 100 | if scopes.len() == 1 { "" } else { "s" } 101 | ); 102 | } 103 | _ => unreachable!(), 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /examples/svc_account_id_token.rs: -------------------------------------------------------------------------------- 1 | use tame_oauth::gcp::*; 2 | 3 | use bytes::Bytes; 4 | 5 | // This example shows the basics for creating a GCP service account token 6 | // provider and requesting a token from it. This particular example uses the 7 | // reqwest HTTP client, but the point of this crate is that you can use 8 | // whichever one you like as long as you don't mind doing a little bit of 9 | // boilerplate to convert between from http::Request and to http::Response 10 | #[tokio::main] 11 | async fn main() { 12 | let mut args = std::env::args().skip(1); 13 | 14 | let key_path = args 15 | .next() 16 | .expect("expected path to a service account json file"); 17 | let service_key = std::fs::read_to_string(key_path).expect("failed to read json key"); 18 | 19 | // Deserialize the service account info from the json data 20 | let acct_info = ServiceAccountInfo::deserialize(service_key).unwrap(); 21 | 22 | // Create the token provider! 23 | let sa_provider = ServiceAccountProvider::new(acct_info).unwrap(); 24 | 25 | let audience = "my-audience"; 26 | 27 | // Attempt to get a token, since we have never used this accessor 28 | // before, it's guaranteed that we will need to make an HTTPS 29 | // request to the token provider to retrieve a token. This 30 | // will also happen if we want to get a token for a different 31 | // audience, or if the token has expired. 32 | let token = match sa_provider.get_id_token(audience).unwrap() { 33 | IdTokenOrRequest::AccessTokenRequest { 34 | request, 35 | audience_hash, 36 | .. 37 | } => { 38 | let access_token_response = execute_request(request).await; 39 | 40 | let id_token_request = sa_provider 41 | .get_id_token_with_access_token(audience, access_token_response) 42 | .unwrap(); 43 | 44 | let id_token_response = execute_request(id_token_request).await; 45 | 46 | sa_provider 47 | .parse_id_token_response(audience_hash, id_token_response) 48 | .unwrap() 49 | } 50 | _ => unreachable!(), 51 | }; 52 | 53 | // Uncomment this if you want to go to lunch and see an unreachable panic 54 | // when you get back 55 | // std::thread::sleep(std::time::Duration::from_secs(60 * 60)) 56 | 57 | // Retrieving a token for the same scopes for which a token has been acquired 58 | // will use the cached token until it expires 59 | match sa_provider.get_id_token(audience).unwrap() { 60 | IdTokenOrRequest::IdToken(tk) => { 61 | assert_eq!(tk, token); 62 | println!( 63 | "cool, you were able to retrieve a token with aud {}!", 64 | audience, 65 | ); 66 | } 67 | _ => unreachable!(), 68 | } 69 | } 70 | 71 | async fn execute_request(request: http::Request>) -> http::Response { 72 | let client = reqwest::Client::new(); 73 | 74 | let (parts, body) = request.into_parts(); 75 | let uri = parts.uri.to_string(); 76 | 77 | // This will always be a POST, but for completeness sake... 78 | let builder = match parts.method { 79 | http::Method::GET => client.get(&uri), 80 | http::Method::POST => client.post(&uri), 81 | http::Method::DELETE => client.delete(&uri), 82 | http::Method::PUT => client.put(&uri), 83 | method => unimplemented!("{} not implemented", method), 84 | }; 85 | 86 | // Build the full request from the headers and body that were 87 | // passed to you, without modifying them. 88 | let request = builder.headers(parts.headers).body(body).build().unwrap(); 89 | 90 | // Send the actual request 91 | let response = client.execute(request).await.unwrap(); 92 | 93 | let mut builder = http::Response::builder() 94 | .status(response.status()) 95 | .version(response.version()); 96 | 97 | let headers = builder.headers_mut().unwrap(); 98 | 99 | // Unfortunately http doesn't expose a way to just use 100 | // an existing HeaderMap, so we have to copy them :( 101 | headers.extend( 102 | response 103 | .headers() 104 | .into_iter() 105 | .map(|(k, v)| (k.clone(), v.clone())), 106 | ); 107 | 108 | let buffer = response.bytes().await.unwrap(); 109 | 110 | builder.body(buffer).unwrap() 111 | } 112 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-commit-message = "Release {{version}}" 2 | tag-message = "Release {{version}}" 3 | tag-name = "{{version}}" 4 | pre-release-replacements = [ 5 | { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}" }, 6 | { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}" }, 7 | { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" }, 8 | { file = "CHANGELOG.md", search = "", replace = "\n## [Unreleased] - ReleaseDate" }, 9 | { file = "CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/EmbarkStudios/tame-oauth/compare/{{tag_name}}...HEAD" }, 10 | ] 11 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as Err, fmt}; 2 | 3 | #[derive(Debug)] 4 | pub enum Error { 5 | /// The private_key field in the [Service Account Key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) 6 | /// is invalid and cannot be parsed 7 | #[cfg(feature = "jwt")] 8 | InvalidKeyFormat, 9 | /// Unable to deserialize the base64 encoded RSA key 10 | Base64Decode(data_encoding::DecodeError), 11 | /// An error occurred trying to create an HTTP request 12 | Http(http::Error), 13 | /// Failed to authenticate and retrieve an oauth token, and were unable to 14 | /// deserialize a more exact reason from the error response 15 | HttpStatus(http::StatusCode), 16 | /// Failed to de/serialize JSON 17 | Json(serde_json::Error), 18 | /// Failed to authenticate and retrieve an oauth token 19 | Auth(AuthError), 20 | /// The RSA key seems valid, but is unable to sign a payload 21 | #[cfg(feature = "jwt")] 22 | InvalidRsaKey(ring::error::Unspecified), 23 | /// The RSA key is invalid and cannot be used to sign 24 | #[cfg(feature = "jwt")] 25 | InvalidRsaKeyRejected(ring::error::KeyRejected), 26 | /// A mutex has been poisoned due to a panic while a lock was held 27 | Poisoned, 28 | /// An I/O error occurred when reading credentials 29 | #[cfg(feature = "gcp")] 30 | Io(std::io::Error), 31 | /// Failed to load valid credentials from a file on disk 32 | #[cfg(feature = "gcp")] 33 | InvalidCredentials { 34 | file: std::path::PathBuf, 35 | error: Box, 36 | }, 37 | /// An error occurred due to [`SystemTime`](std::time::SystemTime) 38 | SystemTime(std::time::SystemTimeError), 39 | /// Unable to parse the returned token 40 | InvalidTokenFormat, 41 | } 42 | 43 | impl fmt::Display for Error { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | #![allow(clippy::enum_glob_use)] 46 | use Error::*; 47 | 48 | match self { 49 | #[cfg(feature = "jwt")] 50 | InvalidKeyFormat => f.write_str("The key format is invalid or unknown"), 51 | Base64Decode(err) => write!(f, "{}", err), 52 | Http(err) => write!(f, "{}", err), 53 | HttpStatus(sc) => write!(f, "HTTP error status: {}", sc), 54 | Json(err) => write!(f, "{}", err), 55 | Auth(err) => write!(f, "{}", err), 56 | #[cfg(feature = "jwt")] 57 | InvalidRsaKey(_err) => f.write_str("RSA key is invalid"), 58 | #[cfg(feature = "jwt")] 59 | InvalidRsaKeyRejected(err) => write!(f, "RSA key is invalid: {}", err), 60 | Poisoned => f.write_str("A mutex is poisoned"), 61 | #[cfg(feature = "gcp")] 62 | Io(inner) => write!(f, "{}", inner), 63 | #[cfg(feature = "gcp")] 64 | InvalidCredentials { file, error } => { 65 | write!(f, "Invalid credentials in '{}': {}", file.display(), error) 66 | } 67 | SystemTime(te) => { 68 | write!(f, "System Time error: {}", te) 69 | } 70 | InvalidTokenFormat => { 71 | write!(f, "Invalid token format") 72 | } 73 | } 74 | } 75 | } 76 | 77 | impl std::error::Error for Error { 78 | fn source(&self) -> Option<&(dyn Err + 'static)> { 79 | #![allow(clippy::enum_glob_use)] 80 | use Error::*; 81 | 82 | match self { 83 | Base64Decode(err) => Some(err as &dyn Err), 84 | Http(err) => Some(err as &dyn Err), 85 | Json(err) => Some(err as &dyn Err), 86 | Auth(err) => Some(err as &dyn Err), 87 | SystemTime(err) => Some(err as &dyn Err), 88 | _ => None, 89 | } 90 | } 91 | } 92 | 93 | impl From for Error { 94 | fn from(e: data_encoding::DecodeError) -> Self { 95 | Error::Base64Decode(e) 96 | } 97 | } 98 | 99 | impl From for Error { 100 | fn from(e: http::Error) -> Self { 101 | Error::Http(e) 102 | } 103 | } 104 | 105 | impl From for Error { 106 | fn from(e: serde_json::Error) -> Self { 107 | Error::Json(e) 108 | } 109 | } 110 | 111 | impl From for Error { 112 | fn from(e: std::time::SystemTimeError) -> Self { 113 | Error::SystemTime(e) 114 | } 115 | } 116 | 117 | #[derive(serde::Deserialize, Debug)] 118 | pub struct AuthError { 119 | /// Top level error type 120 | pub error: Option, 121 | /// More specific details on the error 122 | pub error_description: Option, 123 | } 124 | 125 | impl fmt::Display for AuthError { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | if let Some(ref err) = self.error { 128 | write!(f, "{}", err)?; 129 | 130 | if let Some(ref desc) = self.error_description { 131 | write!(f, "desc: {}", desc)?; 132 | } 133 | } 134 | 135 | Ok(()) 136 | } 137 | } 138 | 139 | impl std::error::Error for AuthError {} 140 | -------------------------------------------------------------------------------- /src/gcp.rs: -------------------------------------------------------------------------------- 1 | //! Provides functionality for 2 | //! [Google oauth](https://developers.google.com/identity/protocols/oauth2) 3 | 4 | use crate::token_cache::CachedTokenProvider; 5 | use crate::{error::Error, jwt}; 6 | 7 | pub mod end_user; 8 | pub mod metadata_server; 9 | pub mod service_account; 10 | 11 | use end_user as eu; 12 | use metadata_server as ms; 13 | use service_account as sa; 14 | 15 | pub use crate::id_token::{ 16 | AccessTokenResponse, IdToken, IdTokenOrRequest, IdTokenProvider, IdTokenRequest, 17 | IdTokenResponse, 18 | }; 19 | pub use crate::token::{Token, TokenOrRequest, TokenProvider}; 20 | pub use { 21 | end_user::{EndUserCredentials, EndUserCredentialsInfo}, 22 | metadata_server::MetadataServerProvider, 23 | service_account::{ServiceAccountInfo, ServiceAccountProvider}, 24 | }; 25 | 26 | /// Both the [`ServiceAccountProvider`] and [`MetadataServerProvider`] get back 27 | /// JSON responses with this schema from their endpoints. 28 | #[derive(serde::Deserialize, Debug)] 29 | struct TokenResponse { 30 | /// The actual token 31 | access_token: String, 32 | /// The token type, pretty much always Header 33 | token_type: String, 34 | /// The time until the token expires and a new one needs to be requested 35 | expires_in: i64, 36 | } 37 | 38 | pub type TokenProviderWrapper = CachedTokenProvider; 39 | impl TokenProviderWrapper { 40 | /// Get a `TokenProvider` following the "Google Default Credentials" 41 | /// flow, in order: 42 | /// 43 | /// * If the `GOOGLE_APPLICATION_CREDENTIALS` environment variable is 44 | /// set, use that as a path to a [`ServiceAccountInfo`](sa::ServiceAccountInfo). 45 | /// 46 | /// * Check for a gcloud's 47 | /// [Application Default Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) 48 | /// for [`EndUserCredentials`](eu::EndUserCredentials) 49 | /// 50 | /// * If we're running on GCP, use the local metadata server. 51 | /// 52 | /// * Otherwise, return None. 53 | /// 54 | /// If it appears that a method is being used, but is actually invalid, 55 | /// eg `GOOGLE_APPLICATION_CREDENTIALS` is set but the file doesn't exist or 56 | /// contains invalid JSON, an error is returned with the details 57 | pub fn get_default_provider() -> Result, Error> { 58 | TokenProviderWrapperInner::get_default_provider() 59 | .map(|provider| provider.map(CachedTokenProvider::wrap)) 60 | } 61 | 62 | /// Gets the kind of token provider 63 | pub fn kind(&self) -> &'static str { 64 | self.inner().kind() 65 | } 66 | 67 | pub fn is_service_account_provider(&self) -> bool { 68 | self.inner().is_service_account_provider() 69 | } 70 | pub fn is_metadata_server_provider(&self) -> bool { 71 | self.inner().is_metadata_server_provider() 72 | } 73 | pub fn is_end_user_credentials_provider(&self) -> bool { 74 | self.inner().is_end_user_credentials_provider() 75 | } 76 | } 77 | 78 | /// Wrapper around the different providers that are supported. Implements both `TokenProvider` and `IdTokenProvider`. 79 | /// Should not be used directly as it is not cached. Use `TokenProviderWrapper` instead. 80 | #[derive(Debug)] 81 | pub enum TokenProviderWrapperInner { 82 | EndUser(eu::EndUserCredentialsInner), 83 | Metadata(ms::MetadataServerProviderInner), 84 | ServiceAccount(sa::ServiceAccountProviderInner), 85 | } 86 | 87 | impl TokenProviderWrapperInner { 88 | /// Get a `TokenProvider` following the "Google Default Credentials" flow. 89 | /// Returns a uncached token provider, use `TokenProviderWrapper::get_default_provider` 90 | /// instead. 91 | pub fn get_default_provider() -> Result, Error> { 92 | use std::{fs::read_to_string, path::PathBuf}; 93 | 94 | // If the environment variable is present, try to open it as a 95 | // Service Account. 96 | if let Some(cred_path) = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS") { 97 | let key_data = match read_to_string(&cred_path) { 98 | Ok(kd) => kd, 99 | Err(e) => { 100 | return Err(Error::InvalidCredentials { 101 | file: cred_path.into(), 102 | error: Box::new(Error::Io(e)), 103 | }); 104 | } 105 | }; 106 | 107 | let sa_info = match sa::ServiceAccountInfo::deserialize(key_data) { 108 | Ok(si) => si, 109 | Err(e) => { 110 | return Err(Error::InvalidCredentials { 111 | file: cred_path.into(), 112 | error: Box::new(e), 113 | }); 114 | } 115 | }; 116 | 117 | return Ok(Some(TokenProviderWrapperInner::ServiceAccount( 118 | sa::ServiceAccountProviderInner::new(sa_info).map_err(|e| { 119 | Error::InvalidCredentials { 120 | file: cred_path.into(), 121 | error: Box::new(e), 122 | } 123 | })?, 124 | ))); 125 | } 126 | 127 | /// Get the path to the gcloud `application_default_credentials.json` 128 | /// file. This function respects the `CLOUDSDK_CONFIG` environment 129 | /// variable. If unset, it looks in the platform-specific gcloud 130 | /// configuration directories 131 | fn gcloud_config_file() -> Option { 132 | let cred_file = "application_default_credentials.json"; 133 | 134 | // If the user has set CLOUDSDK_CONFIG, that overrides the default directory. 135 | if let Some(override_dir) = std::env::var_os("CLOUDSDK_CONFIG") { 136 | let mut pb = PathBuf::from(override_dir); 137 | pb.push(cred_file); 138 | return Some(pb); 139 | } 140 | 141 | // Otherwise, use the default for the platform. 142 | // * Windows - %APPDATA%/gcloud/ 143 | // * Unix - $HOME/.config/gcloud/ 144 | if cfg!(windows) { 145 | std::env::var_os("APPDATA").map(PathBuf::from) 146 | } else { 147 | std::env::var_os("HOME").map(|pb| { 148 | let mut pb = PathBuf::from(pb); 149 | pb.push(".config"); 150 | pb 151 | }) 152 | } 153 | .map(|mut bd| { 154 | bd.push("gcloud"); 155 | bd.push(cred_file); 156 | bd 157 | }) 158 | } 159 | 160 | if let Some(gcloud_file) = gcloud_config_file() { 161 | match read_to_string(&gcloud_file) { 162 | Ok(json_data) => { 163 | let end_user_credentials = eu::EndUserCredentialsInfo::deserialize(json_data) 164 | .map_err(|e| Error::InvalidCredentials { 165 | file: gcloud_file, 166 | error: Box::new(e), 167 | })?; 168 | 169 | return Ok(Some(TokenProviderWrapperInner::EndUser( 170 | eu::EndUserCredentialsInner::new(end_user_credentials), 171 | ))); 172 | } 173 | // Skip not found errors, and fall back to the metadata server check 174 | Err(nf) if nf.kind() == std::io::ErrorKind::NotFound => {} 175 | Err(err) => { 176 | return Err(Error::InvalidCredentials { 177 | file: gcloud_file, 178 | error: Box::new(Error::Io(err)), 179 | }); 180 | } 181 | } 182 | } 183 | 184 | // Finally, if we are on GCP, use the metadata server. If we're not on 185 | // GCP, this will just fail to read the file. 186 | if let Ok(full_name) = read_to_string("/sys/class/dmi/id/product_name") { 187 | // The product name can annoyingly include a newline... 188 | let trimmed = full_name.trim(); 189 | match trimmed { 190 | // This matches the Golang client. If new products 191 | // add additional values, this will need to be updated. 192 | "Google" | "Google Compute Engine" => { 193 | return Ok(Some(TokenProviderWrapperInner::Metadata( 194 | ms::MetadataServerProviderInner::new(None), 195 | ))); 196 | } 197 | _ => {} 198 | } 199 | } 200 | 201 | // None of our checks worked. Give up. 202 | Ok(None) 203 | } 204 | 205 | /// Gets the kind of token provider 206 | pub fn kind(&self) -> &'static str { 207 | match self { 208 | Self::EndUser(_) => "End User", 209 | Self::Metadata(_) => "Metadata Server", 210 | Self::ServiceAccount(_) => "Service Account", 211 | } 212 | } 213 | 214 | pub fn is_service_account_provider(&self) -> bool { 215 | matches!(self, TokenProviderWrapperInner::ServiceAccount(_)) 216 | } 217 | pub fn is_metadata_server_provider(&self) -> bool { 218 | matches!(self, TokenProviderWrapperInner::Metadata(_)) 219 | } 220 | pub fn is_end_user_credentials_provider(&self) -> bool { 221 | matches!(self, TokenProviderWrapperInner::EndUser(_)) 222 | } 223 | } 224 | 225 | impl TokenProvider for TokenProviderWrapperInner { 226 | fn get_token_with_subject<'a, S, I, T>( 227 | &self, 228 | subject: Option, 229 | scopes: I, 230 | ) -> Result 231 | where 232 | S: AsRef + 'a, 233 | I: IntoIterator + Clone, 234 | T: Into, 235 | { 236 | match self { 237 | Self::EndUser(token_provider) => token_provider.get_token_with_subject(subject, scopes), 238 | Self::Metadata(token_provider) => { 239 | token_provider.get_token_with_subject(subject, scopes) 240 | } 241 | Self::ServiceAccount(token_provider) => { 242 | token_provider.get_token_with_subject(subject, scopes) 243 | } 244 | } 245 | } 246 | 247 | fn parse_token_response( 248 | &self, 249 | hash: u64, 250 | response: http::Response, 251 | ) -> Result 252 | where 253 | S: AsRef<[u8]>, 254 | { 255 | match self { 256 | Self::EndUser(token_provider) => token_provider.parse_token_response(hash, response), 257 | Self::Metadata(token_provider) => token_provider.parse_token_response(hash, response), 258 | Self::ServiceAccount(token_provider) => { 259 | token_provider.parse_token_response(hash, response) 260 | } 261 | } 262 | } 263 | } 264 | 265 | impl IdTokenProvider for TokenProviderWrapperInner { 266 | fn get_id_token(&self, audience: &str) -> Result { 267 | match self { 268 | Self::EndUser(token_provider) => token_provider.get_id_token(audience), 269 | Self::Metadata(token_provider) => token_provider.get_id_token(audience), 270 | Self::ServiceAccount(token_provider) => token_provider.get_id_token(audience), 271 | } 272 | } 273 | 274 | fn get_id_token_with_access_token( 275 | &self, 276 | audience: &str, 277 | response: AccessTokenResponse, 278 | ) -> Result 279 | where 280 | S: AsRef<[u8]>, 281 | { 282 | match self { 283 | Self::EndUser(token_provider) => { 284 | token_provider.get_id_token_with_access_token(audience, response) 285 | } 286 | Self::Metadata(token_provider) => { 287 | token_provider.get_id_token_with_access_token(audience, response) 288 | } 289 | Self::ServiceAccount(token_provider) => { 290 | token_provider.get_id_token_with_access_token(audience, response) 291 | } 292 | } 293 | } 294 | 295 | fn parse_id_token_response( 296 | &self, 297 | hash: u64, 298 | response: http::Response, 299 | ) -> Result 300 | where 301 | S: AsRef<[u8]>, 302 | { 303 | match self { 304 | Self::EndUser(token_provider) => token_provider.parse_id_token_response(hash, response), 305 | Self::Metadata(token_provider) => { 306 | token_provider.parse_id_token_response(hash, response) 307 | } 308 | Self::ServiceAccount(token_provider) => { 309 | token_provider.parse_id_token_response(hash, response) 310 | } 311 | } 312 | } 313 | } 314 | 315 | impl From for Token { 316 | fn from(tr: TokenResponse) -> Self { 317 | Self { 318 | access_token: tr.access_token, 319 | token_type: tr.token_type, 320 | refresh_token: String::new(), 321 | expires_in: Some(tr.expires_in), 322 | expires_in_timestamp: std::time::SystemTime::now() 323 | .checked_add(std::time::Duration::from_secs(tr.expires_in as u64)), 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/gcp/end_user.rs: -------------------------------------------------------------------------------- 1 | use super::TokenResponse; 2 | use crate::{ 3 | error::{self, Error}, 4 | id_token::{ 5 | AccessTokenResponse, IdTokenOrRequest, IdTokenProvider, IdTokenRequest, IdTokenResponse, 6 | }, 7 | token::{RequestReason, Token, TokenOrRequest, TokenProvider}, 8 | token_cache::CachedTokenProvider, 9 | IdToken, 10 | }; 11 | 12 | /// Provides tokens using 13 | /// [default application credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) 14 | /// Caches tokens internally. 15 | pub type EndUserCredentials = CachedTokenProvider; 16 | impl EndUserCredentials { 17 | pub fn new(info: EndUserCredentialsInfo) -> Self { 18 | CachedTokenProvider::wrap(EndUserCredentialsInner::new(info)) 19 | } 20 | } 21 | 22 | /// Provides tokens using 23 | /// [default application credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) 24 | #[derive(serde::Deserialize, Debug, Clone)] 25 | pub struct EndUserCredentialsInfo { 26 | /// The OAuth2 client_id 27 | pub client_id: String, 28 | /// The OAuth2 client_secret 29 | pub client_secret: String, 30 | /// The OAuth2 refresh_token 31 | pub refresh_token: String, 32 | /// The client type (the value must be authorized_user) 33 | #[serde(rename = "type")] 34 | pub client_type: String, 35 | } 36 | 37 | impl EndUserCredentialsInfo { 38 | /// Deserializes the `EndUserCredentials` from a byte slice. This 39 | /// data is typically acquired by reading an 40 | /// `application_default_credentials.json` file from disk. 41 | pub fn deserialize(key_data: T) -> Result 42 | where 43 | T: AsRef<[u8]>, 44 | { 45 | let slice = key_data.as_ref(); 46 | 47 | let account_info: Self = serde_json::from_slice(slice)?; 48 | Ok(account_info) 49 | } 50 | } 51 | 52 | /// A token provider for 53 | /// [default application credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) 54 | /// Should not be used directly as it is not cached. Use `EndUserCredentials` instead. 55 | pub struct EndUserCredentialsInner { 56 | info: EndUserCredentialsInfo, 57 | } 58 | 59 | impl std::fmt::Debug for EndUserCredentialsInner { 60 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 | f.debug_struct("EndUserCredentialsInner") 62 | .finish_non_exhaustive() 63 | } 64 | } 65 | 66 | impl EndUserCredentialsInner { 67 | pub fn new(info: EndUserCredentialsInfo) -> Self { 68 | Self { info } 69 | } 70 | } 71 | 72 | #[derive(serde::Deserialize, Debug)] 73 | struct IdTokenResponseBody { 74 | /// The actual token 75 | id_token: String, 76 | } 77 | 78 | impl EndUserCredentialsInner { 79 | fn prepare_token_request(&self) -> Result>, Error> { 80 | // To get an access token or id_token, we need to perform a refresh 81 | // following the instructions at 82 | // https://developers.google.com/identity/protocols/oauth2/web-server#offline 83 | // (i.e., POST our client data as a refresh_token request to 84 | // the /token endpoint). 85 | // The response will include both a access token and a id token 86 | let url = "https://oauth2.googleapis.com/token"; 87 | 88 | // Build up the parameters as a form encoded string. 89 | let body = url::form_urlencoded::Serializer::new(String::new()) 90 | .append_pair("client_id", &self.info.client_id) 91 | .append_pair("client_secret", &self.info.client_secret) 92 | .append_pair("grant_type", "refresh_token") 93 | .append_pair("refresh_token", &self.info.refresh_token) 94 | .finish(); 95 | 96 | let body = Vec::from(body); 97 | 98 | let request = http::Request::builder() 99 | .method("POST") 100 | .uri(url) 101 | .header( 102 | http::header::CONTENT_TYPE, 103 | "application/x-www-form-urlencoded", 104 | ) 105 | .header(http::header::CONTENT_LENGTH, body.len()) 106 | .body(body)?; 107 | 108 | Ok(request) 109 | } 110 | } 111 | 112 | impl TokenProvider for EndUserCredentialsInner { 113 | fn get_token_with_subject<'a, S, I, T>( 114 | &self, 115 | subject: Option, 116 | // EndUserCredentials only have the scopes they were granted 117 | // via their authorization. So whatever scopes you're asking 118 | // for, better have been handled when authorized. `gcloud auth 119 | // application-default login` will get the 120 | // https://www.googleapis.com/auth/cloud-platform which 121 | // includes all *GCP* APIs. 122 | _scopes: I, 123 | ) -> Result 124 | where 125 | S: AsRef + 'a, 126 | I: IntoIterator, 127 | T: Into, 128 | { 129 | // We can only support subject being none 130 | if subject.is_some() { 131 | return Err(Error::Auth(error::AuthError { 132 | error: Some("Unsupported".to_string()), 133 | error_description: Some( 134 | "ADC / User tokens do not support jwt subjects".to_string(), 135 | ), 136 | })); 137 | } 138 | 139 | let request = self.prepare_token_request()?; 140 | 141 | Ok(TokenOrRequest::Request { 142 | request, 143 | reason: RequestReason::ParametersChanged, 144 | scope_hash: 0, 145 | }) 146 | } 147 | 148 | fn parse_token_response( 149 | &self, 150 | _hash: u64, 151 | response: http::Response, 152 | ) -> Result 153 | where 154 | S: AsRef<[u8]>, 155 | { 156 | let (parts, body) = response.into_parts(); 157 | 158 | if !parts.status.is_success() { 159 | return Err(Error::HttpStatus(parts.status)); 160 | } 161 | 162 | // Deserialize our response, or fail. 163 | let token_res: TokenResponse = serde_json::from_slice(body.as_ref())?; 164 | 165 | // TODO(boulos): The response also includes the set of scopes 166 | // (as "scope") that we're granted. We could check that 167 | // cloud-platform is in it. 168 | 169 | // Convert it into our output. 170 | let token: Token = token_res.into(); 171 | Ok(token) 172 | } 173 | } 174 | 175 | impl IdTokenProvider for EndUserCredentialsInner { 176 | fn get_id_token(&self, _audience: &str) -> Result { 177 | let request = self.prepare_token_request()?; 178 | 179 | Ok(IdTokenOrRequest::IdTokenRequest { 180 | request, 181 | reason: RequestReason::ParametersChanged, 182 | audience_hash: 0, 183 | }) 184 | } 185 | 186 | fn get_id_token_with_access_token( 187 | &self, 188 | _audience: &str, 189 | _response: AccessTokenResponse, 190 | ) -> Result 191 | where 192 | S: AsRef<[u8]>, 193 | { 194 | // ID token via access token is not supported with user credentials 195 | // The token is fetched via the same token request as the access token 196 | Err(Error::Auth(error::AuthError { 197 | error: Some("Unsupported".to_string()), 198 | error_description: Some( 199 | "User credentials id tokens via access token not supported".to_string(), 200 | ), 201 | })) 202 | } 203 | 204 | fn parse_id_token_response( 205 | &self, 206 | _hash: u64, 207 | response: IdTokenResponse, 208 | ) -> Result 209 | where 210 | S: AsRef<[u8]>, 211 | { 212 | let (parts, body) = response.into_parts(); 213 | 214 | if !parts.status.is_success() { 215 | let body_bytes = body.as_ref(); 216 | 217 | if parts 218 | .headers 219 | .get(http::header::CONTENT_TYPE) 220 | .and_then(|ct| ct.to_str().ok()) 221 | == Some("application/json; charset=utf-8") 222 | { 223 | if let Ok(auth_error) = serde_json::from_slice::(body_bytes) { 224 | return Err(Error::Auth(auth_error)); 225 | } 226 | } 227 | 228 | return Err(Error::HttpStatus(parts.status)); 229 | } 230 | 231 | let token_res: IdTokenResponseBody = serde_json::from_slice(body.as_ref())?; 232 | let token = IdToken::new(token_res.id_token)?; 233 | 234 | Ok(token) 235 | } 236 | } 237 | 238 | #[cfg(test)] 239 | mod test { 240 | use super::*; 241 | 242 | #[test] 243 | fn end_user_credentials() { 244 | let provider = EndUserCredentialsInner::new(EndUserCredentialsInfo { 245 | client_id: "fake_client@domain.com".into(), 246 | client_secret: "TOP_SECRET".into(), 247 | refresh_token: "REFRESH_TOKEN".into(), 248 | client_type: "authorized_user".into(), 249 | }); 250 | 251 | // End-user credentials don't let you override scopes. 252 | let scopes = vec!["better_not_be_there"]; 253 | 254 | let token_or_req = provider 255 | .get_token(&scopes) 256 | .expect("Should have gotten a request"); 257 | 258 | match token_or_req { 259 | TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"), 260 | TokenOrRequest::Request { request, .. } => { 261 | // Should be the Google oauth2 API 262 | assert_eq!(request.uri().host(), Some("oauth2.googleapis.com")); 263 | // Scopes aren't passed for end user credentials 264 | assert_eq!(request.uri().query(), None); 265 | } 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/gcp/metadata_server.rs: -------------------------------------------------------------------------------- 1 | use super::TokenResponse; 2 | use crate::{ 3 | error::{self, Error}, 4 | id_token::{IdTokenOrRequest, IdTokenProvider}, 5 | token::{RequestReason, Token, TokenOrRequest, TokenProvider}, 6 | token_cache::CachedTokenProvider, 7 | IdToken, 8 | }; 9 | 10 | const METADATA_URL: &str = 11 | "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts"; 12 | 13 | /// [Provides tokens](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) 14 | /// using the metadata server accessible when running from within GCP. 15 | /// Caches tokens internally. 16 | pub type MetadataServerProvider = CachedTokenProvider; 17 | impl MetadataServerProvider { 18 | pub fn new(account_name: Option) -> Self { 19 | CachedTokenProvider::wrap(MetadataServerProviderInner::new(account_name)) 20 | } 21 | } 22 | 23 | /// [Provides tokens](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) 24 | /// using the metadata server accessible when running from within GCP. Should not be used directly as it 25 | /// is not cached. Use `MetadataServerProvider` instead. 26 | #[derive(Debug)] 27 | pub struct MetadataServerProviderInner { 28 | account_name: String, 29 | } 30 | 31 | impl MetadataServerProviderInner { 32 | pub fn new(account_name: Option) -> Self { 33 | Self { 34 | account_name: account_name.unwrap_or_else(|| "default".into()), 35 | } 36 | } 37 | } 38 | 39 | impl TokenProvider for MetadataServerProviderInner { 40 | fn get_token_with_subject<'a, S, I, T>( 41 | &self, 42 | subject: Option, 43 | scopes: I, 44 | ) -> Result 45 | where 46 | S: AsRef + 'a, 47 | I: IntoIterator, 48 | T: Into, 49 | { 50 | // We can only support subject being none 51 | if subject.is_some() { 52 | return Err(Error::Auth(error::AuthError { 53 | error: Some("Unsupported".to_string()), 54 | error_description: Some( 55 | "Metadata server tokens do not support jwt subjects".to_string(), 56 | ), 57 | })); 58 | } 59 | 60 | // Regardless of GCE or GAE, the token_uri is 61 | // `computeMetadata/v1/instance/service-accounts//token`. 62 | let mut url = format!("{}/{}/token", METADATA_URL, self.account_name); 63 | 64 | // Merge all the scopes into a single string. 65 | let scopes_str = scopes 66 | .into_iter() 67 | .map(|s| s.as_ref()) 68 | .collect::>() 69 | .join(","); 70 | 71 | // If we have any scopes, pass them along in the querystring. 72 | if !scopes_str.is_empty() { 73 | url.push_str("?scopes="); 74 | url.push_str(&scopes_str); 75 | } 76 | 77 | let request = http::Request::builder() 78 | .method("GET") 79 | .uri(url) 80 | // To get responses from GCE, we must pass along the 81 | // Metadata-Flavor header with a value of "Google". 82 | .header("Metadata-Flavor", "Google") 83 | .body(Vec::new())?; 84 | 85 | Ok(TokenOrRequest::Request { 86 | request, 87 | reason: RequestReason::ParametersChanged, 88 | scope_hash: 0, 89 | }) 90 | } 91 | 92 | fn parse_token_response( 93 | &self, 94 | _hash: u64, 95 | response: http::Response, 96 | ) -> Result 97 | where 98 | S: AsRef<[u8]>, 99 | { 100 | let (parts, body) = response.into_parts(); 101 | 102 | if !parts.status.is_success() { 103 | return Err(Error::HttpStatus(parts.status)); 104 | } 105 | 106 | // Deserialize our response, or fail. 107 | let token_res: TokenResponse = serde_json::from_slice(body.as_ref())?; 108 | 109 | // Convert it into our output. 110 | let token: Token = token_res.into(); 111 | Ok(token) 112 | } 113 | } 114 | 115 | impl IdTokenProvider for MetadataServerProviderInner { 116 | fn get_id_token(&self, audience: &str) -> Result { 117 | let url = format!( 118 | "{}/{}/identity?audience={}", 119 | METADATA_URL, self.account_name, audience, 120 | ); 121 | 122 | let request = http::Request::builder() 123 | .method("GET") 124 | .uri(url) 125 | .header("Metadata-Flavor", "Google") 126 | .body(Vec::new())?; 127 | 128 | Ok(IdTokenOrRequest::IdTokenRequest { 129 | request, 130 | reason: RequestReason::ParametersChanged, 131 | audience_hash: 0, 132 | }) 133 | } 134 | 135 | fn parse_id_token_response( 136 | &self, 137 | _hash: u64, 138 | response: http::Response, 139 | ) -> Result 140 | where 141 | S: AsRef<[u8]>, 142 | { 143 | let (parts, body) = response.into_parts(); 144 | 145 | if !parts.status.is_success() { 146 | return Err(Error::HttpStatus(parts.status)); 147 | } 148 | 149 | let token = IdToken::new(String::from_utf8_lossy(body.as_ref()).into_owned())?; 150 | 151 | Ok(token) 152 | } 153 | 154 | fn get_id_token_with_access_token( 155 | &self, 156 | _audience: &str, 157 | _access_token_resp: crate::id_token::AccessTokenResponse, 158 | ) -> Result 159 | where 160 | S: AsRef<[u8]>, 161 | { 162 | // ID token via access token is not supported in the metadata service 163 | // The token can be fetched directly via the metadataservice. 164 | Err(Error::Auth(error::AuthError { 165 | error: Some("Unsupported".to_string()), 166 | error_description: Some( 167 | "Metadata server id tokens via access token not supported".to_string(), 168 | ), 169 | })) 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod test { 175 | use super::*; 176 | 177 | #[test] 178 | fn metadata_noscopes() { 179 | let provider = MetadataServerProvider::new(None); 180 | 181 | let scopes: &[&str] = &[]; 182 | 183 | let token_or_req = provider 184 | .get_token(scopes) 185 | .expect("Should have gotten a request"); 186 | 187 | match token_or_req { 188 | TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"), 189 | TokenOrRequest::Request { request, .. } => { 190 | // Should be the metadata server 191 | assert_eq!(request.uri().host(), Some("metadata.google.internal")); 192 | // Since we had no scopes, no querystring. 193 | assert_eq!(request.uri().query(), None); 194 | } 195 | } 196 | } 197 | 198 | #[test] 199 | fn metadata_with_scopes() { 200 | let provider = MetadataServerProvider::new(None); 201 | 202 | let scopes = ["scope1", "scope2"]; 203 | 204 | let token_or_req = provider 205 | .get_token(&scopes) 206 | .expect("Should have gotten a request"); 207 | 208 | match token_or_req { 209 | TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"), 210 | TokenOrRequest::Request { request, .. } => { 211 | // Should be the metadata server 212 | assert_eq!(request.uri().host(), Some("metadata.google.internal")); 213 | // Since we had some scopes, we should have a querystring. 214 | assert!(request.uri().query().is_some()); 215 | 216 | let query_string = request.uri().query().unwrap(); 217 | // We don't care about ordering, but the query_string 218 | // should be comma-separated and only include the 219 | // scopes. 220 | assert!( 221 | query_string == "scopes=scope1,scope2" 222 | || query_string == "scopes=scope2,scope1" 223 | ); 224 | } 225 | } 226 | } 227 | 228 | #[test] 229 | fn wrapper_dispatch() { 230 | // Wrap the metadata server provider. 231 | let provider = 232 | crate::gcp::TokenProviderWrapperInner::Metadata(MetadataServerProviderInner::new(None)); 233 | 234 | // And then have the same test as metadata_with_scopes 235 | let scopes = ["scope1", "scope2"]; 236 | 237 | let token_or_req = provider 238 | .get_token(&scopes) 239 | .expect("Should have gotten a request"); 240 | 241 | match token_or_req { 242 | TokenOrRequest::Token(_) => panic!("Shouldn't have gotten a token"), 243 | TokenOrRequest::Request { request, .. } => { 244 | // Should be the metadata server 245 | assert_eq!(request.uri().host(), Some("metadata.google.internal")); 246 | // Since we had some scopes, we should have a querystring. 247 | assert!(request.uri().query().is_some()); 248 | 249 | let query_string = request.uri().query().unwrap(); 250 | // We don't care about ordering, but the query_string 251 | // should be comma-separated and only include the 252 | // scopes. 253 | assert!( 254 | query_string == "scopes=scope1,scope2" 255 | || query_string == "scopes=scope2,scope1" 256 | ); 257 | } 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/gcp/service_account.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use super::{ 4 | jwt::{self, Algorithm, Header, Key}, 5 | TokenResponse, 6 | }; 7 | use crate::{ 8 | error::{self, Error}, 9 | id_token::{ 10 | AccessTokenRequest, AccessTokenResponse, IdTokenOrRequest, IdTokenProvider, IdTokenRequest, 11 | IdTokenResponse, 12 | }, 13 | token::{RequestReason, Token, TokenOrRequest, TokenProvider}, 14 | token_cache::CachedTokenProvider, 15 | IdToken, 16 | }; 17 | 18 | const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer"; 19 | 20 | /// Minimal parts needed from a GCP service account key for token acquisition 21 | #[derive(serde::Deserialize, Debug, Clone)] 22 | pub struct ServiceAccountInfo { 23 | /// The private key we use to sign 24 | pub private_key: String, 25 | /// The unique id used as the issuer of the JWT claim 26 | pub client_email: String, 27 | /// The URI we send the token requests to, eg 28 | pub token_uri: String, 29 | } 30 | 31 | #[derive(serde::Deserialize, Debug)] 32 | struct IdTokenResponseBody { 33 | /// The actual token 34 | token: String, 35 | } 36 | 37 | impl ServiceAccountInfo { 38 | /// Deserializes service account from a byte slice. This data is typically 39 | /// acquired by reading a service account JSON file from disk 40 | pub fn deserialize(key_data: T) -> Result 41 | where 42 | T: AsRef<[u8]>, 43 | { 44 | let slice = key_data.as_ref(); 45 | 46 | let account_info: Self = serde_json::from_slice(slice)?; 47 | Ok(account_info) 48 | } 49 | } 50 | 51 | /// A token provider for a GCP service account. 52 | /// Caches tokens internally. 53 | pub type ServiceAccountProvider = CachedTokenProvider; 54 | impl ServiceAccountProvider { 55 | pub fn new(info: ServiceAccountInfo) -> Result { 56 | Ok(CachedTokenProvider::wrap(ServiceAccountProviderInner::new( 57 | info, 58 | )?)) 59 | } 60 | 61 | /// Gets the [`ServiceAccountInfo`] this was created for 62 | pub fn get_account_info(&self) -> &ServiceAccountInfo { 63 | &self.inner().info 64 | } 65 | } 66 | 67 | /// A token provider for a GCP service account. Should not be used directly as it is not cached. Use `ServiceAccountProvider` instead. 68 | pub struct ServiceAccountProviderInner { 69 | info: ServiceAccountInfo, 70 | priv_key: Vec, 71 | } 72 | 73 | impl std::fmt::Debug for ServiceAccountProviderInner { 74 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 75 | f.debug_struct("ServiceAccountProviderInner") 76 | .finish_non_exhaustive() 77 | } 78 | } 79 | 80 | impl ServiceAccountProviderInner { 81 | /// Creates a new `ServiceAccountAccess` given the provided service 82 | /// account info. This can fail if the private key is encoded incorrectly. 83 | pub fn new(info: ServiceAccountInfo) -> Result { 84 | let key_string = info 85 | .private_key 86 | .split("-----") 87 | .nth(2) 88 | .ok_or(Error::InvalidKeyFormat)?; 89 | 90 | // Strip out all of the newlines 91 | let key_string = key_string.split_whitespace().fold( 92 | String::with_capacity(key_string.len()), 93 | |mut s, line| { 94 | s.push_str(line); 95 | s 96 | }, 97 | ); 98 | 99 | let key_bytes = data_encoding::BASE64.decode(key_string.as_bytes())?; 100 | 101 | Ok(Self { 102 | info, 103 | priv_key: key_bytes, 104 | }) 105 | } 106 | 107 | /// Gets the [`ServiceAccountInfo`] this was created for 108 | pub fn get_account_info(&self) -> &ServiceAccountInfo { 109 | &self.info 110 | } 111 | 112 | fn prepare_access_token_request<'a, S, I, T>( 113 | &self, 114 | subject: Option, 115 | scopes: I, 116 | ) -> Result 117 | where 118 | S: AsRef + 'a, 119 | I: IntoIterator, 120 | T: Into, 121 | { 122 | let scopes = scopes 123 | .into_iter() 124 | .map(|s| s.as_ref()) 125 | .collect::>() 126 | .join(" "); 127 | 128 | let issued_at = std::time::SystemTime::now() 129 | .duration_since(std::time::SystemTime::UNIX_EPOCH)? 130 | .as_secs() as i64; 131 | 132 | let claims = jwt::Claims { 133 | issuer: self.info.client_email.clone(), 134 | scope: scopes, 135 | audience: self.info.token_uri.clone(), 136 | expiration: issued_at + 3600 - 5, // Give us some wiggle room near the hour mark 137 | issued_at, 138 | subject: subject.map(|s| s.into()), 139 | }; 140 | 141 | let assertion = jwt::encode( 142 | &Header::new(Algorithm::RS256), 143 | &claims, 144 | Key::Pkcs8(&self.priv_key), 145 | )?; 146 | 147 | let body = url::form_urlencoded::Serializer::new(String::new()) 148 | .append_pair("grant_type", GRANT_TYPE) 149 | .append_pair("assertion", &assertion) 150 | .finish(); 151 | 152 | let body = Vec::from(body); 153 | 154 | let request = http::Request::builder() 155 | .method("POST") 156 | .uri(&self.info.token_uri) 157 | .header( 158 | http::header::CONTENT_TYPE, 159 | "application/x-www-form-urlencoded", 160 | ) 161 | .header(http::header::CONTENT_LENGTH, body.len()) 162 | .body(body)?; 163 | 164 | Ok(request) 165 | } 166 | } 167 | 168 | impl TokenProvider for ServiceAccountProviderInner { 169 | /// Like [`ServiceAccountProviderInner::get_token`], but allows the JWT "subject" 170 | /// to be passed in. 171 | fn get_token_with_subject<'a, S, I, T>( 172 | &self, 173 | subject: Option, 174 | scopes: I, 175 | ) -> Result 176 | where 177 | S: AsRef + 'a, 178 | I: IntoIterator, 179 | T: Into, 180 | { 181 | let request = self.prepare_access_token_request(subject, scopes)?; 182 | Ok(TokenOrRequest::Request { 183 | reason: RequestReason::ParametersChanged, 184 | request, 185 | scope_hash: 0, 186 | }) 187 | } 188 | 189 | /// Handle responses from the token URI request we generated in 190 | /// `get_token`. This method deserializes the response and stores 191 | /// the token in a local cache, so that future lookups for the 192 | /// same scopes don't require new http requests. 193 | fn parse_token_response( 194 | &self, 195 | _hash: u64, 196 | response: http::Response, 197 | ) -> Result 198 | where 199 | S: AsRef<[u8]>, 200 | { 201 | let (parts, body) = response.into_parts(); 202 | 203 | if !parts.status.is_success() { 204 | let body_bytes = body.as_ref(); 205 | 206 | if parts 207 | .headers 208 | .get(http::header::CONTENT_TYPE) 209 | .and_then(|ct| ct.to_str().ok()) 210 | == Some("application/json; charset=utf-8") 211 | { 212 | if let Ok(auth_error) = serde_json::from_slice::(body_bytes) { 213 | return Err(Error::Auth(auth_error)); 214 | } 215 | } 216 | 217 | return Err(Error::HttpStatus(parts.status)); 218 | } 219 | 220 | let token_res: TokenResponse = serde_json::from_slice(body.as_ref())?; 221 | let token: Token = token_res.into(); 222 | 223 | Ok(token) 224 | } 225 | } 226 | 227 | impl IdTokenProvider for ServiceAccountProviderInner { 228 | fn get_id_token(&self, _audience: &str) -> Result { 229 | let request = self 230 | .prepare_access_token_request(None::<&str>, &["https://www.googleapis.com/auth/iam"])?; 231 | 232 | Ok(IdTokenOrRequest::AccessTokenRequest { 233 | request, 234 | reason: RequestReason::ParametersChanged, 235 | audience_hash: 0, 236 | }) 237 | } 238 | 239 | fn get_id_token_with_access_token( 240 | &self, 241 | audience: &str, 242 | response: AccessTokenResponse, 243 | ) -> Result 244 | where 245 | S: AsRef<[u8]>, 246 | { 247 | let token = self.parse_token_response(0, response)?; 248 | 249 | let sa_email = self.info.client_email.clone(); 250 | // See https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oidc 251 | // for details on what it is we're doing 252 | let json_body = serde_json::to_vec(&serde_json::json!({ 253 | "audience": audience, 254 | "includeEmail": true, 255 | }))?; 256 | 257 | let token_header_value: http::HeaderValue = token.try_into()?; 258 | 259 | let request = http::Request::builder() 260 | .method("POST") 261 | .uri(format!("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateIdToken", sa_email)) 262 | .header( 263 | http::header::CONTENT_TYPE, 264 | "application/json; charset=utf-8", 265 | ) 266 | .header(http::header::CONTENT_LENGTH, json_body.len()) 267 | .header(http::header::AUTHORIZATION, token_header_value) 268 | .body(json_body)?; 269 | 270 | Ok(request) 271 | } 272 | 273 | fn parse_id_token_response( 274 | &self, 275 | _hash: u64, 276 | response: IdTokenResponse, 277 | ) -> Result 278 | where 279 | S: AsRef<[u8]>, 280 | { 281 | let (parts, body) = response.into_parts(); 282 | 283 | if !parts.status.is_success() { 284 | let body_bytes = body.as_ref(); 285 | 286 | if parts 287 | .headers 288 | .get(http::header::CONTENT_TYPE) 289 | .and_then(|ct| ct.to_str().ok()) 290 | == Some("application/json; charset=utf-8") 291 | { 292 | if let Ok(auth_error) = serde_json::from_slice::(body_bytes) { 293 | return Err(Error::Auth(auth_error)); 294 | } 295 | } 296 | 297 | return Err(Error::HttpStatus(parts.status)); 298 | } 299 | 300 | let token_res: IdTokenResponseBody = serde_json::from_slice(body.as_ref())?; 301 | let token = IdToken::new(token_res.token)?; 302 | 303 | Ok(token) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/id_token.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use crate::{token::RequestReason, token_cache::CacheableToken, Error}; 4 | 5 | /// Represents a id token as returned by `OAuth2` servers. 6 | #[derive(Clone, PartialEq, Eq, Debug)] 7 | pub struct IdToken { 8 | pub token: String, 9 | pub expiration: SystemTime, 10 | } 11 | 12 | impl IdToken { 13 | pub fn new(token: String) -> Result { 14 | // Extract the exp claim from the token, so we can know if the token is expired or not. 15 | let claims = token.split('.').nth(1).ok_or(Error::InvalidTokenFormat)?; 16 | 17 | let decoded = data_encoding::BASE64URL_NOPAD.decode(claims.as_bytes())?; 18 | let claims: TokenClaims = serde_json::from_slice(&decoded)?; 19 | 20 | Ok(Self { 21 | token, 22 | expiration: SystemTime::UNIX_EPOCH 23 | .checked_add(std::time::Duration::from_secs(claims.exp)) 24 | .unwrap_or(SystemTime::UNIX_EPOCH), 25 | }) 26 | } 27 | } 28 | 29 | impl CacheableToken for IdToken { 30 | /// Returns true if token is expired. 31 | #[inline] 32 | fn has_expired(&self) -> bool { 33 | if self.token.is_empty() { 34 | return true; 35 | } 36 | 37 | self.expiration <= SystemTime::now() 38 | } 39 | } 40 | 41 | /// Either a valid token, or an HTTP request. With some token sources, two different 42 | /// HTTP requests needs to be performed, one to get an access token and one to get 43 | /// the actual id token. 44 | pub enum IdTokenOrRequest { 45 | AccessTokenRequest { 46 | request: AccessTokenRequest, 47 | reason: RequestReason, 48 | audience_hash: u64, 49 | }, 50 | IdTokenRequest { 51 | request: IdTokenRequest, 52 | reason: RequestReason, 53 | audience_hash: u64, 54 | }, 55 | IdToken(IdToken), 56 | } 57 | 58 | pub type IdTokenRequest = http::Request>; 59 | pub type AccessTokenRequest = http::Request>; 60 | 61 | pub type AccessTokenResponse = http::Response; 62 | pub type IdTokenResponse = http::Response; 63 | 64 | /// A `IdTokenProvider` supplies all methods needed for all different flows to get a id token. 65 | pub trait IdTokenProvider { 66 | /// Attempts to retrieve an id token that can be used when communicating via IAP etc. 67 | fn get_id_token(&self, audience: &str) -> Result; 68 | 69 | /// Some token sources require a access token to be used to generte a id token. 70 | /// If `get_id_token` returns a `AccessTokenResponse`, this method should be called. 71 | fn get_id_token_with_access_token( 72 | &self, 73 | audience: &str, 74 | response: AccessTokenResponse, 75 | ) -> Result 76 | where 77 | S: AsRef<[u8]>; 78 | 79 | /// Once a `IdTokenResponse` has been received for an id token request, call this method 80 | /// to deserialize the token. 81 | fn parse_id_token_response( 82 | &self, 83 | hash: u64, 84 | response: IdTokenResponse, 85 | ) -> Result 86 | where 87 | S: AsRef<[u8]>; 88 | } 89 | 90 | #[derive(serde::Deserialize, Debug)] 91 | struct TokenClaims { 92 | exp: u64, 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use std::time::SystemTime; 98 | 99 | use super::IdToken; 100 | 101 | #[test] 102 | fn test_decode_jwt() { 103 | /* raw token claims 104 | { 105 | "aud": "my-aud", 106 | "azp": "123", 107 | "email": "test@example.com", 108 | "email_verified": true, 109 | "exp": 1676641773, 110 | "iat": 1676638173, 111 | "iss": "https://accounts.google.com", 112 | "sub": "1234", 113 | "key": "~~~?" 114 | } 115 | */ 116 | 117 | let raw_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJteS1hdWQiLCJhenAiOiIxMjMiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjc2NjQxNzczLCJpYXQiOjE2NzY2MzgxNzMsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjEyMzQiLCJrZXkiOiJ-fn4_In0.RpaD4p5ugL-MH_bkQ3jQ6RPANCDl1nV32xbE5raJF7tZkteQG4ULfRAcVsRnhF3j0yw3e8X9WJJ0rBdnF79MxYbaGB61hl8i6vjoa13zuEw2yaY-pNfEkfsqyf0WcY80_uV3jt-vmcPAlikgtss1YCVl9SW3i2bFXTw_kV-UE8stuCjNcjkORI9hZxEoYZoDJcc4Y8W7JuYD8V8fF8iBtZLCtGCPK64ERrZFkTqLX6FcypEAo6Y5JvmrKGQSMx9q8ozkpqMRTxxfPw6HVTEQJacjkkdJoCrs3zARzzjvm1xyWfJSGGS_g4wismCbDKLtsCSNmugjS-7ruf7rnqUTBg"; 118 | 119 | // Make sure that the claims part base64 is encoded without padding, this is to make sure that padding is handled correctly. 120 | // Note that when changing the test token, this might fail, in that case, just add a character somewhere in the claims. 121 | let claims = raw_token.split('.').nth(1).unwrap(); 122 | assert_ne!(claims.len() % 4, 0); 123 | 124 | // assert that the test token includes url safe encoded characters in the base64 encoded claims part 125 | assert!(claims.contains('_')); 126 | assert!(claims.contains('-')); 127 | 128 | let id_token = IdToken::new(raw_token.to_owned()).unwrap(); 129 | 130 | assert_eq!(id_token.token, raw_token); 131 | assert_eq!( 132 | id_token 133 | .expiration 134 | .duration_since(SystemTime::UNIX_EPOCH) 135 | .unwrap() 136 | .as_secs(), 137 | 1676641773 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/jwt.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use ring::signature; 3 | use serde::Serialize; 4 | 5 | #[derive(Serialize)] 6 | pub(crate) struct Claims { 7 | #[serde(rename = "iss")] 8 | pub(crate) issuer: String, 9 | #[serde(rename = "aud")] 10 | pub(crate) audience: String, 11 | #[serde(rename = "exp")] 12 | pub(crate) expiration: i64, 13 | #[serde(rename = "iat")] 14 | pub(crate) issued_at: i64, 15 | #[serde(rename = "sub")] 16 | pub(crate) subject: Option, 17 | pub(crate) scope: String, 18 | } 19 | 20 | /// A basic JWT header, the alg defaults to HS256 and typ is automatically 21 | /// set to `JWT`. All the other fields are optional. 22 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)] 23 | pub struct Header { 24 | /// The type of JWS: it can only be "JWT" here 25 | /// 26 | /// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9). 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub typ: Option, 29 | /// The algorithm used 30 | /// 31 | /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). 32 | pub alg: Algorithm, 33 | /// Content type 34 | /// 35 | /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2). 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub cty: Option, 38 | /// JSON Key URL 39 | /// 40 | /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2). 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub jku: Option, 43 | /// Key ID 44 | /// 45 | /// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4). 46 | #[serde(skip_serializing_if = "Option::is_none")] 47 | pub kid: Option, 48 | /// X.509 URL 49 | /// 50 | /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5). 51 | #[serde(skip_serializing_if = "Option::is_none")] 52 | pub x5u: Option, 53 | /// X.509 certificate thumbprint 54 | /// 55 | /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7). 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | pub x5t: Option, 58 | } 59 | 60 | impl Header { 61 | /// Returns a JWT header with the algorithm given 62 | pub fn new(algorithm: Algorithm) -> Header { 63 | Header { 64 | typ: Some("JWT".to_string()), 65 | alg: algorithm, 66 | cty: None, 67 | jku: None, 68 | kid: None, 69 | x5u: None, 70 | x5t: None, 71 | } 72 | } 73 | } 74 | 75 | impl Default for Header { 76 | /// Returns a JWT header using the default Algorithm, HS256 77 | fn default() -> Self { 78 | Header::new(Algorithm::default()) 79 | } 80 | } 81 | 82 | /// The algorithms supported for signing/verifying 83 | #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, serde::Deserialize)] 84 | #[allow(clippy::upper_case_acronyms)] 85 | #[derive(Default)] 86 | pub enum Algorithm { 87 | /// HMAC using SHA-256 88 | #[default] 89 | HS256, 90 | /// HMAC using SHA-384 91 | HS384, 92 | /// HMAC using SHA-512 93 | HS512, 94 | 95 | /// ECDSA using SHA-256 96 | ES256, 97 | /// ECDSA using SHA-384 98 | ES384, 99 | 100 | /// RSASSA-PKCS1-v1_5 using SHA-256 101 | RS256, 102 | /// RSASSA-PKCS1-v1_5 using SHA-384 103 | RS384, 104 | /// RSASSA-PKCS1-v1_5 using SHA-512 105 | RS512, 106 | 107 | /// RSASSA-PSS using SHA-256 108 | PS256, 109 | /// RSASSA-PSS using SHA-384 110 | PS384, 111 | /// RSASSA-PSS using SHA-512 112 | PS512, 113 | } 114 | 115 | /// The supported RSA key formats, see the documentation for [`ring::signature::RsaKeyPair`] 116 | /// for more information 117 | pub enum Key<'a> { 118 | /// An unencrypted PKCS#8-encoded key. Can be used with both ECDSA and RSA 119 | /// algorithms when signing. See ring for information. 120 | Pkcs8(&'a [u8]), 121 | } 122 | 123 | /// Serializes to JSON and encodes to base64 124 | pub fn to_jwt_part(input: &T) -> Result { 125 | let json = serde_json::to_string(input)?; 126 | Ok(data_encoding::BASE64URL_NOPAD.encode(json.as_bytes())) 127 | } 128 | 129 | /// The actual RSA signing + encoding 130 | /// Taken from Ring doc 131 | fn sign_rsa( 132 | alg: &'static dyn signature::RsaEncoding, 133 | key: Key<'_>, 134 | signing_input: &str, 135 | ) -> Result { 136 | let key_pair = match key { 137 | Key::Pkcs8(bytes) => { 138 | signature::RsaKeyPair::from_pkcs8(bytes).map_err(Error::InvalidRsaKeyRejected)? 139 | } 140 | }; 141 | 142 | let key_pair = std::sync::Arc::new(key_pair); 143 | let mut signature = vec![0; key_pair.public().modulus_len()]; 144 | let rng = ring::rand::SystemRandom::new(); 145 | key_pair 146 | .sign(alg, &rng, signing_input.as_bytes(), &mut signature) 147 | .map_err(Error::InvalidRsaKey)?; 148 | 149 | Ok(data_encoding::BASE64_NOPAD.encode(&signature)) 150 | } 151 | 152 | /// Take the payload of a JWT, sign it using the algorithm given and return 153 | /// the base64 url safe encoded of the result. 154 | /// 155 | /// Only use this function if you want to do something other than JWT. 156 | pub fn sign(signing_input: &str, key: Key<'_>, algorithm: Algorithm) -> Result { 157 | match algorithm { 158 | Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input), 159 | Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input), 160 | Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input), 161 | 162 | Algorithm::PS256 => sign_rsa(&signature::RSA_PSS_SHA256, key, signing_input), 163 | Algorithm::PS384 => sign_rsa(&signature::RSA_PSS_SHA384, key, signing_input), 164 | Algorithm::PS512 => sign_rsa(&signature::RSA_PSS_SHA512, key, signing_input), 165 | _ => panic!("Unsupported algorithm {:?}", algorithm), 166 | } 167 | } 168 | 169 | pub fn encode(header: &Header, claims: &T, key: Key<'_>) -> Result { 170 | let encoded_header = to_jwt_part(&header)?; 171 | let encoded_claims = to_jwt_part(&claims)?; 172 | let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); 173 | let signature = sign(&signing_input, key, header.alg)?; 174 | 175 | Ok([signing_input, signature].join(".")) 176 | } 177 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | // BEGIN - Embark standard lints v6 for Rust 1.55+ 3 | // do not change or add/remove here, but one can add exceptions after this section 4 | // for more info see: 5 | #![deny(unsafe_code)] 6 | #![warn( 7 | clippy::all, 8 | clippy::await_holding_lock, 9 | clippy::char_lit_as_u8, 10 | clippy::checked_conversions, 11 | clippy::dbg_macro, 12 | clippy::debug_assert_with_mut_call, 13 | clippy::doc_markdown, 14 | clippy::empty_enum, 15 | clippy::enum_glob_use, 16 | clippy::exit, 17 | clippy::expl_impl_clone_on_copy, 18 | clippy::explicit_deref_methods, 19 | clippy::explicit_into_iter_loop, 20 | clippy::fallible_impl_from, 21 | clippy::filter_map_next, 22 | clippy::flat_map_option, 23 | clippy::float_cmp_const, 24 | clippy::fn_params_excessive_bools, 25 | clippy::from_iter_instead_of_collect, 26 | clippy::if_let_mutex, 27 | clippy::implicit_clone, 28 | clippy::imprecise_flops, 29 | clippy::inefficient_to_string, 30 | clippy::invalid_upcast_comparisons, 31 | clippy::large_digit_groups, 32 | clippy::large_stack_arrays, 33 | clippy::large_types_passed_by_value, 34 | clippy::let_unit_value, 35 | clippy::linkedlist, 36 | clippy::lossy_float_literal, 37 | clippy::macro_use_imports, 38 | clippy::manual_ok_or, 39 | clippy::map_err_ignore, 40 | clippy::map_flatten, 41 | clippy::map_unwrap_or, 42 | clippy::match_on_vec_items, 43 | clippy::match_same_arms, 44 | clippy::match_wild_err_arm, 45 | clippy::match_wildcard_for_single_variants, 46 | clippy::mem_forget, 47 | clippy::mismatched_target_os, 48 | clippy::missing_enforced_import_renames, 49 | clippy::mut_mut, 50 | clippy::mutex_integer, 51 | clippy::needless_borrow, 52 | clippy::needless_continue, 53 | clippy::needless_for_each, 54 | clippy::option_option, 55 | clippy::path_buf_push_overwrite, 56 | clippy::ptr_as_ptr, 57 | clippy::rc_mutex, 58 | clippy::ref_option_ref, 59 | clippy::rest_pat_in_fully_bound_structs, 60 | clippy::same_functions_in_if_condition, 61 | clippy::semicolon_if_nothing_returned, 62 | clippy::single_match_else, 63 | clippy::string_add_assign, 64 | clippy::string_add, 65 | clippy::string_lit_as_bytes, 66 | clippy::string_to_string, 67 | clippy::todo, 68 | clippy::trait_duplication_in_bounds, 69 | clippy::unimplemented, 70 | clippy::unnested_or_patterns, 71 | clippy::unused_self, 72 | clippy::useless_transmute, 73 | clippy::verbose_file_reads, 74 | clippy::zero_sized_map_values, 75 | future_incompatible, 76 | nonstandard_style, 77 | rust_2018_idioms 78 | )] 79 | // END - Embark standard lints v6 for Rust 1.55+ 80 | // crate-specific exceptions: 81 | 82 | #[cfg(feature = "gcp")] 83 | pub mod gcp; 84 | #[cfg(feature = "jwt")] 85 | mod jwt; 86 | 87 | mod error; 88 | mod id_token; 89 | mod token; 90 | pub mod token_cache; 91 | 92 | pub use crate::{error::Error, id_token::IdToken, token::Token}; 93 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::Error, token_cache::CacheableToken}; 2 | use std::time::SystemTime; 3 | 4 | /// Represents a access token as returned by `OAuth2` servers. 5 | /// 6 | /// * It is produced by all authentication flows. 7 | /// * It authenticates certain operations, and must be refreshed once it has 8 | /// reached its expiry date. 9 | /// 10 | /// The type is tuned to be suitable for direct de-serialization from server 11 | /// replies, as well as for serialization for later reuse. This is the reason 12 | /// for the two fields dealing with expiry - once in relative in and once in 13 | /// absolute terms. 14 | #[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] 15 | pub struct Token { 16 | /// used when authenticating calls to oauth2 enabled services. 17 | pub access_token: String, 18 | /// used to refresh an expired access_token. 19 | pub refresh_token: String, 20 | /// The token type as string - usually 'Bearer'. 21 | pub token_type: String, 22 | /// access_token will expire after this amount of time. 23 | /// Prefer using expiry_date() 24 | pub expires_in: Option, 25 | /// timestamp is seconds since epoch indicating when the token will expire 26 | /// in absolute terms. 27 | pub expires_in_timestamp: Option, 28 | } 29 | 30 | impl CacheableToken for Token { 31 | /// Returns true if we are expired. 32 | #[inline] 33 | fn has_expired(&self) -> bool { 34 | if self.access_token.is_empty() { 35 | return true; 36 | } 37 | 38 | let expiry = self.expires_in_timestamp.unwrap_or_else(SystemTime::now); 39 | 40 | expiry <= SystemTime::now() 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | pub enum RequestReason { 46 | /// An existing token has expired 47 | Expired, 48 | /// The requested scopes or audience have never been seen before 49 | ParametersChanged, 50 | } 51 | 52 | /// Either a valid token, or an HTTP request that can be used to acquire one 53 | #[derive(Debug)] 54 | pub enum TokenOrRequest { 55 | /// A valid token that can be supplied in an API request 56 | Token(Token), 57 | Request { 58 | /// The parts of an HTTP request that must be sent to acquire the token, 59 | /// in the client of your choice 60 | request: http::Request>, 61 | /// The reason we need to retrieve a new token 62 | reason: RequestReason, 63 | /// An opaque hash of the unique parameters for which the request was constructed 64 | scope_hash: u64, 65 | }, 66 | } 67 | 68 | /// A `TokenProvider` has a single method to implement `get_token_with_subject`. 69 | /// Implementations are free to perform caching or always return a `Request` in 70 | /// the `TokenOrRequest`. 71 | pub trait TokenProvider { 72 | /// Attempts to retrieve a token that can be used in an API request, if we 73 | /// haven't already retrieved a token for the specified scopes, or the token 74 | /// has expired, an HTTP request is returned that can be used to retrieve a 75 | /// token. 76 | /// 77 | /// Note that the scopes are not sorted or in any other way manipulated, so 78 | /// any modifications to them will require a new token to be requested. 79 | #[inline] 80 | fn get_token<'a, S, I>(&self, scopes: I) -> Result 81 | where 82 | S: AsRef + 'a, 83 | I: IntoIterator + Clone, 84 | { 85 | self.get_token_with_subject::(None, scopes) 86 | } 87 | 88 | /// Like [`TokenProvider::get_token`], but allows the JWT 89 | /// ["subject"](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields) 90 | /// to be passed in. 91 | fn get_token_with_subject<'a, S, I, T>( 92 | &self, 93 | subject: Option, 94 | scopes: I, 95 | ) -> Result 96 | where 97 | S: AsRef + 'a, 98 | I: IntoIterator + Clone, 99 | T: Into; 100 | 101 | /// Once a response has been received for a token request, call this method 102 | /// to deserialize the token (and potentially store it in a local cache for 103 | /// reuse until it expires). 104 | fn parse_token_response( 105 | &self, 106 | hash: u64, 107 | response: http::Response, 108 | ) -> Result 109 | where 110 | S: AsRef<[u8]>; 111 | } 112 | 113 | impl std::convert::TryInto for Token { 114 | type Error = crate::Error; 115 | 116 | fn try_into(self) -> Result { 117 | let auth_header_val = format!("{} {}", self.token_type, self.access_token); 118 | http::header::HeaderValue::from_str(&auth_header_val) 119 | .map_err(|e| crate::Error::from(http::Error::from(e))) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/token_cache.rs: -------------------------------------------------------------------------------- 1 | //! Provides functionality for caching access tokens and id tokens. 2 | 3 | use crate::id_token::{IdTokenOrRequest, IdTokenProvider}; 4 | use crate::token::{TokenOrRequest, TokenProvider}; 5 | use crate::{error::Error, token::RequestReason, IdToken, Token}; 6 | 7 | use std::hash::Hasher; 8 | use std::sync::RwLock; 9 | 10 | type Hash = u64; 11 | 12 | #[derive(Debug)] 13 | struct Entry { 14 | hash: Hash, 15 | token: T, 16 | } 17 | 18 | /// An in-memory cache for caching tokens. 19 | #[derive(Debug)] 20 | pub struct TokenCache { 21 | cache: RwLock>>, 22 | } 23 | 24 | pub enum TokenOrRequestReason { 25 | Token(T), 26 | RequestReason(RequestReason), 27 | } 28 | 29 | impl TokenCache { 30 | pub fn new() -> Self { 31 | Self { 32 | cache: RwLock::new(Vec::new()), 33 | } 34 | } 35 | 36 | /// Get a token from the cache that matches the hash 37 | pub fn get(&self, hash: Hash) -> Result, Error> 38 | where 39 | T: CacheableToken + Clone, 40 | { 41 | let reason = { 42 | let cache = self.cache.read().map_err(|_e| Error::Poisoned)?; 43 | match cache.binary_search_by(|i| i.hash.cmp(&hash)) { 44 | Ok(i) => { 45 | let token = &cache[i].token; 46 | 47 | if !token.has_expired() { 48 | return Ok(TokenOrRequestReason::Token(token.clone())); 49 | } 50 | 51 | RequestReason::Expired 52 | } 53 | Err(_) => RequestReason::ParametersChanged, 54 | } 55 | }; 56 | 57 | Ok(TokenOrRequestReason::RequestReason(reason)) 58 | } 59 | 60 | /// Insert a token into the cache 61 | pub fn insert(&self, token: T, hash: Hash) -> Result<(), Error> { 62 | // Last token wins, which...should?...be fine 63 | let mut cache = self.cache.write().map_err(|_e| Error::Poisoned)?; 64 | match cache.binary_search_by(|i| i.hash.cmp(&hash)) { 65 | Ok(i) => cache[i].token = token, 66 | Err(i) => { 67 | cache.insert(i, Entry { hash, token }); 68 | } 69 | }; 70 | 71 | Ok(()) 72 | } 73 | } 74 | 75 | impl Default for TokenCache { 76 | fn default() -> Self { 77 | Self::new() 78 | } 79 | } 80 | 81 | pub trait CacheableToken { 82 | fn has_expired(&self) -> bool; 83 | } 84 | 85 | /// Wraps a `TokenProvider` in a cache, only invokes the inner `TokenProvider` if 86 | /// the token in cache is expired, or if it doesn't exist. 87 | pub struct CachedTokenProvider

{ 88 | access_tokens: TokenCache, 89 | id_tokens: TokenCache, 90 | inner: P, 91 | } 92 | 93 | impl std::fmt::Debug for CachedTokenProvider

{ 94 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 95 | f.debug_struct("CachedTokenProvider") 96 | .field("inner", &self.inner) 97 | .finish_non_exhaustive() 98 | } 99 | } 100 | 101 | impl

CachedTokenProvider

{ 102 | /// Wraps a token provider with a cache 103 | pub fn wrap(token_provider: P) -> Self { 104 | Self { 105 | access_tokens: TokenCache::new(), 106 | id_tokens: TokenCache::new(), 107 | inner: token_provider, 108 | } 109 | } 110 | 111 | /// Gets a reference to the wrapped (uncached) token provider 112 | pub fn inner(&self) -> &P { 113 | &self.inner 114 | } 115 | } 116 | 117 | impl

TokenProvider for CachedTokenProvider

118 | where 119 | P: TokenProvider, 120 | { 121 | fn get_token_with_subject<'a, S, I, T>( 122 | &self, 123 | subject: Option, 124 | scopes: I, 125 | ) -> Result 126 | where 127 | S: AsRef + 'a, 128 | I: IntoIterator + Clone, 129 | T: Into, 130 | { 131 | let scope_hash = hash_scopes(&scopes); 132 | 133 | let reason = match self.access_tokens.get(scope_hash)? { 134 | TokenOrRequestReason::Token(token) => return Ok(TokenOrRequest::Token(token)), 135 | TokenOrRequestReason::RequestReason(reason) => reason, 136 | }; 137 | 138 | match self.inner.get_token_with_subject(subject, scopes)? { 139 | TokenOrRequest::Token(token) => Ok(TokenOrRequest::Token(token)), 140 | TokenOrRequest::Request { request, .. } => Ok(TokenOrRequest::Request { 141 | request, 142 | reason, 143 | scope_hash, 144 | }), 145 | } 146 | } 147 | 148 | fn parse_token_response( 149 | &self, 150 | hash: u64, 151 | response: http::Response, 152 | ) -> Result 153 | where 154 | S: AsRef<[u8]>, 155 | { 156 | let token = self.inner.parse_token_response(hash, response)?; 157 | 158 | self.access_tokens.insert(token.clone(), hash)?; 159 | Ok(token) 160 | } 161 | } 162 | 163 | impl

IdTokenProvider for CachedTokenProvider

164 | where 165 | P: IdTokenProvider, 166 | { 167 | fn get_id_token(&self, audience: &str) -> Result { 168 | let hash = hash_str(audience); 169 | 170 | let reason = match self.id_tokens.get(hash)? { 171 | TokenOrRequestReason::Token(token) => return Ok(IdTokenOrRequest::IdToken(token)), 172 | TokenOrRequestReason::RequestReason(reason) => reason, 173 | }; 174 | 175 | match self.inner.get_id_token(audience)? { 176 | IdTokenOrRequest::IdToken(token) => Ok(IdTokenOrRequest::IdToken(token)), 177 | IdTokenOrRequest::AccessTokenRequest { request, .. } => { 178 | Ok(IdTokenOrRequest::AccessTokenRequest { 179 | request, 180 | reason, 181 | audience_hash: hash, 182 | }) 183 | } 184 | IdTokenOrRequest::IdTokenRequest { request, .. } => { 185 | Ok(IdTokenOrRequest::IdTokenRequest { 186 | request, 187 | reason, 188 | audience_hash: hash, 189 | }) 190 | } 191 | } 192 | } 193 | 194 | fn get_id_token_with_access_token( 195 | &self, 196 | audience: &str, 197 | response: crate::id_token::AccessTokenResponse, 198 | ) -> Result 199 | where 200 | S: AsRef<[u8]>, 201 | { 202 | self.inner 203 | .get_id_token_with_access_token(audience, response) 204 | } 205 | 206 | fn parse_id_token_response( 207 | &self, 208 | hash: u64, 209 | response: http::Response, 210 | ) -> Result 211 | where 212 | S: AsRef<[u8]>, 213 | { 214 | let token = self.inner.parse_id_token_response(hash, response)?; 215 | 216 | self.id_tokens.insert(token.clone(), hash)?; 217 | Ok(token) 218 | } 219 | } 220 | 221 | fn hash_str(str: &str) -> Hash { 222 | let hash = { 223 | let mut hasher = twox_hash::XxHash::default(); 224 | hasher.write(str.as_bytes()); 225 | hasher.finish() 226 | }; 227 | 228 | hash 229 | } 230 | 231 | fn hash_scopes<'a, I, S>(scopes: &I) -> Hash 232 | where 233 | S: AsRef + 'a, 234 | I: IntoIterator + Clone, 235 | { 236 | let scopes_str = scopes 237 | .clone() 238 | .into_iter() 239 | .map(|s| s.as_ref()) 240 | .collect::>() 241 | .join("|"); 242 | 243 | hash_str(&scopes_str) 244 | } 245 | 246 | #[cfg(test)] 247 | mod test { 248 | use std::{ 249 | ops::Add, 250 | ops::Sub, 251 | time::{Duration, SystemTime}, 252 | }; 253 | 254 | use super::*; 255 | 256 | #[test] 257 | fn test_hash_scopes() { 258 | use std::hash::Hasher; 259 | 260 | let expected = { 261 | let mut hasher = twox_hash::XxHash::default(); 262 | hasher.write(b"scope1|"); 263 | hasher.write(b"scope2|"); 264 | hasher.write(b"scope3"); 265 | hasher.finish() 266 | }; 267 | 268 | let hash = hash_scopes(&["scope1", "scope2", "scope3"].iter()); 269 | 270 | assert_eq!(expected, hash); 271 | 272 | let hash = hash_scopes( 273 | &[ 274 | "scope1".to_owned(), 275 | "scope2".to_owned(), 276 | "scope3".to_owned(), 277 | ] 278 | .iter(), 279 | ); 280 | 281 | assert_eq!(expected, hash); 282 | } 283 | 284 | #[test] 285 | fn test_cache() { 286 | let cache = TokenCache::new(); 287 | let hash = hash_scopes(&["scope1", "scope2"].iter()); 288 | let token = mock_token(100); 289 | let expired_token = mock_token(-100); 290 | 291 | assert!(matches!( 292 | cache.get(hash).unwrap(), 293 | TokenOrRequestReason::RequestReason(RequestReason::ParametersChanged) 294 | )); 295 | 296 | cache.insert(expired_token, hash).unwrap(); 297 | 298 | assert!(matches!( 299 | cache.get(hash).unwrap(), 300 | TokenOrRequestReason::RequestReason(RequestReason::Expired) 301 | )); 302 | 303 | cache.insert(token, hash).unwrap(); 304 | 305 | assert!(matches!( 306 | cache.get(hash).unwrap(), 307 | TokenOrRequestReason::Token(..) 308 | )); 309 | } 310 | 311 | #[test] 312 | fn test_cache_wrapper() { 313 | let cached_provider = CachedTokenProvider::wrap(PanicProvider); 314 | 315 | let hash = hash_scopes(&["scope1", "scope2"].iter()); 316 | let token = mock_token(100); 317 | 318 | cached_provider.access_tokens.insert(token, hash).unwrap(); 319 | 320 | let tor = cached_provider.get_token(&["scope1", "scope2"]).unwrap(); 321 | 322 | // check that a token in returned 323 | assert!(matches!(tor, TokenOrRequest::Token(..))); 324 | } 325 | 326 | fn mock_token(expires_in: i64) -> Token { 327 | let expires_in_timestamp = if expires_in > 0 { 328 | SystemTime::now().add(Duration::from_secs(expires_in as u64)) 329 | } else { 330 | SystemTime::now().sub(Duration::from_secs(expires_in.unsigned_abs())) 331 | }; 332 | 333 | Token { 334 | access_token: "access-token".to_string(), 335 | refresh_token: "refresh-token".to_string(), 336 | token_type: "token-type".to_string(), 337 | expires_in: Some(expires_in), 338 | expires_in_timestamp: Some(expires_in_timestamp), 339 | } 340 | } 341 | 342 | /// `PanicProvider` is a mock token provider that panics if called, as a way of 343 | /// testing that the cache wrapper handles the request. 344 | struct PanicProvider; 345 | impl TokenProvider for PanicProvider { 346 | fn get_token_with_subject<'a, S, I, T>( 347 | &self, 348 | _subject: Option, 349 | _scopes: I, 350 | ) -> Result 351 | where 352 | S: AsRef + 'a, 353 | I: IntoIterator + Clone, 354 | T: Into, 355 | { 356 | panic!("should not have been reached") 357 | } 358 | 359 | fn parse_token_response( 360 | &self, 361 | _hash: u64, 362 | _response: http::Response, 363 | ) -> Result 364 | where 365 | S: AsRef<[u8]>, 366 | { 367 | panic!("should not have been reached") 368 | } 369 | } 370 | 371 | impl IdTokenProvider for PanicProvider { 372 | fn get_id_token(&self, _audience: &str) -> Result { 373 | panic!("should not have been reached") 374 | } 375 | 376 | fn parse_id_token_response( 377 | &self, 378 | _hash: u64, 379 | _response: http::Response, 380 | ) -> Result 381 | where 382 | S: AsRef<[u8]>, 383 | { 384 | panic!("should not have been reached") 385 | } 386 | 387 | fn get_id_token_with_access_token( 388 | &self, 389 | _audience: &str, 390 | _response: crate::id_token::AccessTokenResponse, 391 | ) -> Result 392 | where 393 | S: AsRef<[u8]>, 394 | { 395 | panic!("should not have been reached") 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /tests/svc_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "sanguine-rhythm-105020", 4 | "private_key_id": "0c4fffc10a02b3a700d6c17e2a51fbabada8c27d", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDX1h/UiSbHBf3c\n5L3hAb4QUZ5/nL9CTY11m8uXmWAGaAhjlE7vv15jTaxhjKm+RpfJW3shZao0lxSJ\ngayYY8Ub5Kx3npPfToff/N/sBV7RmZ4FYb6b1XsZpjTy4Bt1LpRtSq/L4lH+boPG\nCBdJv7uwJkdBTRPBJRnynlpNOreJ08fo80G6qaQn2ZA3P8YusonIS72hW+tdnhrr\npxvbIJT/NN2dFfVXxs3KkL6Qu3FuaqvEARqdjuPOdjDkDRwA4gfTlagGPtJVQ4gU\nl6rgXRTX2o0iGLXIxehHfLrbYP5UOjrfpAiTRdf+E3Ho5KJps09MrHk5yVzauKEU\nauJ9M+a3AgMBAAECggEBALvM1GVZ8SO7UuihH5ZorbgFTKQ8/y3xzORIax29lo/8\ndVAv+38gRECjlRpMCmZFhkzuDHVCwJaB3pzG+CagqSFcF7T9hi0HZ7K9lRkIkzhN\nMfH82p09Y58tv2SVG08a+IsgMVZ11mJMRtxIrfq9mdHrfJSVPFsSrUEuB+Sq8og4\n5KpU/kcPQQ+nlCKzrXwgMi2cqj+cb+9/jnm200VRUFq2iC3lLKtyhVhluCzg0ecp\nSIpIFHB8mNgDiiLrmV99UCeVoa/E2MrZQTpzQeCA52pIBvMf8LEDLjRNrWq+HRxA\nNuTrPYRJRLlDhVVKwXW5bCLZBvTFXHmV8ejFOsaZQAECgYEA81UftXi+BbXTxdge\nxBYms4h77KWJTLh5H12MU/U6epfsiZSq7OeWO9sOchUNUK8v3sUDL7FZPg/vSEIb\nkp5KQzbX6poy/uVrzL7niR+bJGssycaxxGsFJ6QZIMVtlPpLVcBAU96c1fhqMYs8\n5/obVNZSY47qMBeSEZfEP849wzcCgYEA4xKP2I7cWsl42B3fbVaLoQiUhZRbvnvY\nRyZb8ZOPz4DSqKDHt6CbO7D3vL2mqcL8hqi40tJGRRztU+quUiudA16CzFacmHdg\nJ0cWJWY9cz9bXofDAtEFkY5u8XbeZu806iCsPh4TRmRwXmw8dOckCZAv8tcQOo7r\ndxIYin7juIECgYEAkqUSXwNNQZO69NiyceoHmNsAFDYO8LWcCVMPZum7PHaijqeR\n+wP2fkweAJK/W4i4iMCikvOGnOhthFaS12Gdz7QVm8UiRots1A+Y6gKqNOCCNXgR\nWhZFHQbAPge9arMNA7jBC8p1Kl5zYThQlF0ea5pePLG8YQ9TcFbOZsWcYzECgYEA\nsRJufe+ZwmpOBCn3a2oL5H2uZCR3DqnA1GsDU/VANg49OCZ416c0pm2wIsy5xLQ6\n/D9iMXSsO4T9RW1Clu1Puarf0LzRzMt6feafTHbYAKEtfR/dYLri3sj1lvKdKCPt\nXY4xAxes7D2yqs84rej5X0PDQFmZXDDLScUgwg+FQQECgYBFaDzuYxNpiBG6RzFi\naNfaJ1GNuIZgkI86HTwSma2XttuiOGpgWumfz0JPwercewKNhyu/2QkavPQlf6OQ\n7gbeOrA+LqEisLkyBSwzFghQItU7/OoTspe1P4+yVEbNGD3bSNsu1xe5p3mSw8/t\nVQWfhniMeji3k4Lv96kLfYXcZA==\n-----END PRIVATE KEY-----\n", 6 | "client_email": "oauth2-public-test@sanguine-rhythm-105020.iam.gserviceaccount.com", 7 | "client_id": "110673384954054291652", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/oauth2-public-test%40sanguine-rhythm-105020.iam.gserviceaccount.com" 12 | } --------------------------------------------------------------------------------