├── .cirrus.yml ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── improvement.md ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── Contributors.txt ├── Migration.md ├── Readme.md ├── docs ├── CONTRIBUTING.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── PULL_REQUEST_TEMPLATE.md ├── rfc6749.txt ├── rfc6750.txt ├── rfc7009.txt ├── rfc7662.txt └── rfc8414.txt ├── examples └── support │ ├── client.rs │ ├── generic.rs │ ├── gotham.rs │ ├── iron.rs │ ├── rocket.rs │ └── rouille.rs ├── oxide-auth-actix ├── Cargo.toml ├── Changes.md ├── Readme.md ├── examples │ └── actix-example │ │ ├── Cargo.toml │ │ └── src │ │ ├── main.rs │ │ └── support.rs └── src │ ├── lib.rs │ └── operations.rs ├── oxide-auth-async ├── Cargo.toml ├── Changes.md ├── Readme.md └── src │ ├── code_grant.rs │ ├── endpoint │ ├── access_token.rs │ ├── authorization.rs │ ├── client_credentials.rs │ ├── mod.rs │ ├── refresh.rs │ └── resource.rs │ ├── frontends │ ├── mod.rs │ └── simple │ │ ├── extensions │ │ ├── extended.rs │ │ ├── list.rs │ │ └── mod.rs │ │ └── mod.rs │ ├── lib.rs │ ├── primitives.rs │ └── tests │ ├── access_token.rs │ ├── authorization.rs │ ├── client_credentials.rs │ ├── mod.rs │ ├── refresh.rs │ ├── resource.rs │ └── type_properties.rs ├── oxide-auth-axum ├── Cargo.toml ├── Readme.md └── src │ ├── error.rs │ ├── lib.rs │ ├── request.rs │ └── response.rs ├── oxide-auth-db ├── Cargo.toml ├── Changes.md ├── Readme.md ├── examples │ └── db-example │ │ ├── Cargo.toml │ │ └── src │ │ ├── main.rs │ │ └── support.rs └── src │ ├── db_service │ ├── mod.rs │ └── redis.rs │ ├── lib.rs │ └── primitives │ ├── db_registrar.rs │ └── mod.rs ├── oxide-auth-iron ├── Cargo.toml ├── Readme.md ├── examples │ └── iron.rs └── src │ └── lib.rs ├── oxide-auth-poem ├── Cargo.toml ├── Readme.md ├── examples │ └── poem-example │ │ ├── main.rs │ │ └── support.rs └── src │ ├── error.rs │ ├── lib.rs │ ├── request.rs │ └── response.rs ├── oxide-auth-rocket ├── Cargo.toml ├── Readme.md ├── examples │ └── rocket.rs └── src │ ├── failure.rs │ └── lib.rs ├── oxide-auth-rouille ├── Cargo.toml ├── Readme.md ├── examples │ └── rouille.rs └── src │ └── lib.rs ├── oxide-auth ├── Cargo.toml ├── Changes.md ├── Readme.md └── src │ ├── code_grant │ ├── accesstoken.rs │ ├── authorization.rs │ ├── client_credentials.rs │ ├── error.rs │ ├── extensions │ │ ├── mod.rs │ │ └── pkce.rs │ ├── mod.rs │ ├── refresh.rs │ └── resource.rs │ ├── endpoint │ ├── accesstoken.rs │ ├── authorization.rs │ ├── client_credentials.rs │ ├── error.rs │ ├── mod.rs │ ├── query.rs │ ├── refresh.rs │ ├── resource.rs │ └── tests │ │ ├── access_token.rs │ │ ├── authorization.rs │ │ ├── client_credentials.rs │ │ ├── mod.rs │ │ ├── pkce.rs │ │ ├── refresh.rs │ │ └── resource.rs │ ├── frontends │ ├── actix.rs │ ├── gotham.rs │ ├── iron.rs │ ├── mod.rs │ ├── rocket.rs │ ├── rouille.rs │ └── simple │ │ ├── endpoint.rs │ │ ├── extensions │ │ ├── extended.rs │ │ ├── list.rs │ │ ├── mod.rs │ │ └── pkce.rs │ │ ├── mod.rs │ │ └── request.rs │ ├── lib.rs │ └── primitives │ ├── authorizer.rs │ ├── generator.rs │ ├── grant.rs │ ├── issuer.rs │ ├── mod.rs │ ├── registrar.rs │ └── scope.rs ├── release └── rustfmt.toml /.cirrus.yml: -------------------------------------------------------------------------------- 1 | stable_task: 2 | container: 3 | image: rust:latest 4 | cargo_cache: 5 | folder: $CARGO_HOME/registry 6 | fingerprint_script: cargo update && cat Cargo.lock 7 | env: 8 | matrix: 9 | - CRATE: oxide-auth 10 | - CRATE: oxide-auth-async 11 | - CRATE: oxide-auth-actix 12 | - CRATE: actix-example 13 | - CRATE: oxide-auth-iron 14 | - CRATE: oxide-auth-poem 15 | - CRATE: oxide-auth-rouille 16 | - CRATE: oxide-auth-db 17 | OXIDE_AUTH_SKIP_REDIS: yes 18 | - CRATE: db-example 19 | build_script: cargo build -p "$CRATE" --examples 20 | test_script: cargo test -p "$CRATE" 21 | before_cache_script: rm -rf $CARGO_HOME/registry/index 22 | 23 | nightly_task: 24 | container: 25 | image: rustlang/rust:nightly 26 | cargo_cache: 27 | folder: $CARGO_HOME/registry 28 | fingerprint_script: cargo update && cat Cargo.lock 29 | env: 30 | matrix: 31 | - CRATE: oxide-auth 32 | - CRATE: oxide-auth-async 33 | - CRATE: oxide-auth-actix 34 | - CRATE: actix-example 35 | - CRATE: oxide-auth-iron 36 | - CRATE: oxide-auth-poem 37 | - CRATE: oxide-auth-rouille 38 | - CRATE: oxide-auth-rocket 39 | - CRATE: oxide-auth-db 40 | OXIDE_AUTH_SKIP_REDIS: yes 41 | - CRATE: db-example 42 | build_script: cargo build -p "$CRATE" --examples 43 | test_script: cargo test -p "$CRATE" 44 | before_cache_script: rm -rf $CARGO_HOME/registry/index 45 | 46 | release_task: 47 | only_if: $CIRRUS_BRANCH =~ 'release.*' 48 | container: 49 | image: rust:latest 50 | script: ./release 51 | 52 | doc_task: 53 | container: 54 | image: rustlang/rust:nightly 55 | script: cargo doc --no-deps --document-private-items --all-features 56 | -------------------------------------------------------------------------------- /.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 | # Bug report 11 | 12 | (Insert Brief threat assessment) causing (Execution/Information disclosure/Unexpected behaviour/Unsoundness) 13 | 14 | The effects of this bug are … 15 | 16 | ## Reproduction 17 | 18 | Relevant environment (http frontend library, OS, network setup?) 19 | 20 | Steps to reproduce the behavior: 21 | 1. Create a '...' 22 | 2. Send a request containing '....' 23 | 3. See error 24 | 25 | 26 | ## Expected behaviour 27 | 28 | According to [reference], it should … 29 | 30 | [Documentation]\(link me\) specifies … 31 | 32 | Common sense dictates, … 33 | 34 | Tracking pull request 35 | -------- 36 | 37 | - [ ] A pull request (does not yet exist/exists at #…) 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Feature 11 | 12 | One should be able to … 13 | 14 | Common use cases include … 15 | 16 | This could be achieved with … 17 | 18 | ## Alternatives 19 | 20 | Alternatively, one could … but … 21 | 22 | ## Context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | 26 | ## Tracking pull request 27 | 28 | - [ ] A pull request (does not yet exist/exists at #…) 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement 3 | about: Improving existing features 4 | title: '' 5 | labels: improvement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Project Improvement 11 | 12 | Description of what has been improved or what should be improved (documentation, comments, formatting, etc.) … 13 | 14 | Feel free to boast a bit, documentation experience is incredibly important and underappreciated. 15 | 16 | ## Other context 17 | 18 | A potential use case, similar improvements, ideas that depend on this, or leave empty. 19 | 20 | # Tracking pull request 21 | 22 | - [ ] A pull request (does not yet exist/exists at #…) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .vscode 6 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## `oxide-auth` [UNRELEASED] 4 | 5 | ### Changed 6 | 7 | - Updated `base64` to v0.21 8 | - Updated `rust-argon2` to v2.0.0 9 | - The `Argon2` hasher now uses the parameters recommended by RFC-9106 for memory constrained environments 10 | 11 | ## `oxide-auth-axum` v0.3.0 12 | 13 | ### Breaking 14 | 15 | - Updated *oxide-auth-axum* to Axum 0.6 and adapted `OAuthRequest` to `FromRequest` and `OAuthResource` to `FromRequestParts` per https://github.com/tokio-rs/axum/pull/1272 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.test] 2 | opt-level = 2 3 | 4 | [workspace] 5 | resolver = "2" 6 | members = [ 7 | "oxide-auth", 8 | "oxide-auth-async", 9 | "oxide-auth-actix", 10 | "oxide-auth-actix/examples/actix-example", 11 | "oxide-auth-axum", 12 | "oxide-auth-iron", 13 | "oxide-auth-poem", 14 | "oxide-auth-rocket", 15 | "oxide-auth-rouille", 16 | "oxide-auth-db", 17 | "oxide-auth-db/examples/db-example", 18 | ] 19 | -------------------------------------------------------------------------------- /Contributors.txt: -------------------------------------------------------------------------------- 1 | # A list of people who have contributed to this project, with optional metadata 2 | # The list is sorted chronologically, excluding the current maintainer who should always be listed first 3 | # Format: Foo Bar (comment e.g. company affiliation) 4 | HeroicKatora (initial author) 5 | ParisLiakos (Paris Liakos, gotham frontend) 6 | asonix (actix 1.0 update) 7 | Geobert Quach (on behalf of Isode Ltd.) 8 | robjtede (actix-web 3.0 update) 9 | Oleg Chirukhin 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth 2 | 3 | A OAuth2 server library, for use in combination with common web servers, 4 | featuring a set of configurable and pluggable backends. 5 | 6 | ## About 7 | 8 | `oxide-auth` aims at providing a comprehensive and extensible interface to 9 | managing OAuth2 tokens on a server. The core package is agnostic of the used 10 | front-end web server and adaptors for the actix, rocket, iron and rouille 11 | crates are provided in extension crates. Through an interface designed with 12 | traits, the frontend is as easily pluggable as the backend. 13 | 14 | ## Example 15 | 16 | > `$ cargo run example-actix` 17 | 18 | In the example folder you can find an [interactive example]. This configures 19 | a server, registers a public client and initializes a resource requiring an 20 | authorization token. A client is also activated which can be used to access the 21 | resource. The example assumes the user to be the validated resource owner, who 22 | can deny or allow the request by the client. 23 | 24 | ## Integration 25 | 26 | Some popular server libraries have ready-made integration. These still require 27 | some dependency on the base crate but generally wrap the interface into a user 28 | that is considered more idiomatic for their library. Besides the implementation 29 | of `oxide-auth` traits for the request type, specific error and response traits 30 | are also implemented. 31 | 32 | | What | Crate | Notes | Docs | 33 | |------------------|----------------------|---------|-----------------------------------------------------------------------------------------------------| 34 | | `actix` | `oxide-auth-actix` | - | [![actix docs](https://docs.rs/oxide-auth-actix/badge.svg)](https://docs.rs/oxide-auth-actix) | 35 | | `async` wrappers | `oxide-auth-async` | - | [![async docs](https://docs.rs/oxide-auth-async/badge.svg)](https://docs.rs/oxide-auth-async) | 36 | | `redis` | `oxide-auth-db` | - | [![redis docs](https://docs.rs/oxide-auth-db/badge.svg)](https://docs.rs/oxide-auth-db) | 37 | | `rocket` | `oxide-auth-rocket` | nightly | [![rocket docs](https://docs.rs/oxide-auth-rocket/badge.svg)](https://docs.rs/oxide-auth-rocket) | 38 | | `rouille` | `oxide-auth-rouille` | - | [![rouille docs](https://docs.rs/oxide-auth-rouille/badge.svg)](https://docs.rs/oxide-auth-rouille) | 39 | | `iron` | `oxide-auth-iron` | - | [![iron docs](https://docs.rs/oxide-auth-iron/badge.svg)](https://docs.rs/oxide-auth-iron) | 40 | | `poem` | `oxide-auth-poem` | - | [![poem docs](https://docs.rs/oxide-auth-poem/badge.svg)](https://docs.rs/oxide-auth-poem) | 41 | 42 | ## Additional 43 | 44 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth.svg)](https://crates.io/crates/oxide-auth) 45 | [![Docs.rs Status](https://docs.rs/oxide-auth/badge.svg)](https://docs.rs/oxide-auth/) 46 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 47 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 48 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 49 | 50 | A more or less comprehensive list of changes is contained in the 51 | [changelog][CHANGES]. Sometimes less as larger releases and reworks profit from 52 | a rough overview of the changes more than a cumulative list of detailed 53 | features. 54 | 55 | For some hints on upgrading from older versions see the [migration 56 | notes][MIGRATION]. 57 | 58 | More information about [contributing][CONTRIBUTING]. Please respect that I 59 | maintain this on my own currently and have limited time. I appreciate 60 | suggestions but sometimes the associate workload can seem daunting. That means 61 | that simplifications to the workflow are also *highly* appreciated. 62 | 63 | Licensed under either of 64 | 65 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 66 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 67 | at your option. 68 | 69 | The license applies to all parts of the source code, its documentation and 70 | supplementary files unless otherwise indicated. It does NOT apply to the 71 | replicated full-text copies of referenced RFCs which were included for the sake 72 | of completion. These are distributed as permitted by [IETF Trust License 73 | 4–Section 3.c.i][IETF4]. 74 | 75 | [actix]: https://crates.io/crates/actix-web 76 | 77 | [iron]: https://crates.io/crates/iron 78 | 79 | [rocket]: https://crates.io/crates/rocket 80 | 81 | [rouille]: https://crates.io/crates/rouille 82 | 83 | [interactive example]: oxide-auth-actix/examples/actix-example 84 | 85 | [CHANGES]: CHANGELOG.md 86 | 87 | [MIGRATION]: Migration.md 88 | 89 | [CONTRIBUTING]: docs/CONTRIBUTING.md 90 | 91 | [LICENSE-MIT]: docs/LICENSE-MIT 92 | 93 | [LICENSE-APACHE]: docs/LICENSE-APACHE 94 | 95 | [IETF4]: https://trustee.ietf.org/license-info/IETF-TLP-4.htm 96 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Help in development is always welcome! Whether you have a bugfix, new 5 | features, new ideas or just want to correct a typo in some documentation, the 6 | easiest way to track its progress is by opening a tracking issue. 7 | 8 | In case you already have working code - even better. Simply link the pull 9 | request in the issue. If you have not done so already, you _may_ also add 10 | yourself to the [list of constributors][contributors] (that's up to You, there 11 | is no need to so). 12 | 13 | Please respect that I maintain this on my own currently and have limited time. 14 | I appreciate suggestions but sometimes the associate workload can seem 15 | daunting. That means that simplifications to the workflow are also *highly* 16 | appreciated. I may also not get back to you immediately. 17 | 18 | By contributing code, you agree to make it available under both MIT license and 19 | Apache-2.0 license at the choice of the licensee and assure that you have the 20 | rights to do so (the Github terms of service also require this from You). 21 | 22 | This project generally follows a **full disclosure principle**. We believe 23 | that vulnerabilities are fixed most swiftly when shared openly. As this project 24 | is mainly a library and not end-user software, it also enables other developers 25 | to react in an informed manner. Should you nevertheless believe this method to 26 | be inappropriate, you may discuss bugs over [encrypted e-mail][pgp-key]. 27 | 28 | [contributors]: ../Contributors.txt 29 | [pgp-key]: http://pgp.mit.edu/pks/lookup?op=vindex&search=0x8BFB6B35887B56B8 30 | -------------------------------------------------------------------------------- /docs/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2018 Andreas Molzer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This changes/fixes/improves … 2 | 3 | - [ ] I have read the [contribution guidelines][Contributing] 4 | - [ ] This change has tests (remove for doc only) 5 | - [ ] This change has documentation 6 | - [ ] Corresponds to issue (number) 7 | 8 | 13 | 14 | [Contributing]: CONTRIBUTING.md 15 | -------------------------------------------------------------------------------- /examples/support/generic.rs: -------------------------------------------------------------------------------- 1 | //! Helper methods for several examples. 2 | //! 3 | //! The support files for each frontend include a client instance for several implemented 4 | //! frontends. These are not part of the main example code as this library focusses purely on the 5 | //! server side. This module contains code that can be shared between the different frontends. 6 | //! Since we want to be able to run the actix example but share it with rocket examples but 7 | //! rocket includes macros in its crate root, the module include order is a bit strange. 8 | //! 9 | //! On supported systems (which have the `x-www-browser` command), there is a utility to open 10 | //! a page in the browser. 11 | #![allow(unused)] 12 | 13 | /// Simplistic reqwest client. 14 | #[path="./client.rs"] 15 | mod client; 16 | 17 | use oxide_auth::endpoint::Solicitation; 18 | use std::fmt; 19 | 20 | pub use self::client::{Client, Config as ClientConfig, Error as ClientError}; 21 | 22 | /// Try to open the server url `http://localhost:{port}` in the browser, or print a guiding statement 23 | /// to the console if this is not possible. 24 | pub fn open_in_browser(port: u16) { 25 | use std::io::{Error, ErrorKind}; 26 | use std::process::Command; 27 | 28 | let target_address = format!("http://localhost:{port}/"); 29 | 30 | // As suggested by 31 | let open_with = if cfg!(target_os = "linux") { 32 | // `xdg-open` chosen over `x-www-browser` due to problems with the latter (#25) 33 | Ok("xdg-open") 34 | } else if cfg!(target_os = "windows") { 35 | Ok("explorer") 36 | } else if cfg!(target_os = "macos") { 37 | Ok("open") 38 | } else { 39 | Err(Error::new(ErrorKind::Other, "Open not supported")) 40 | }; 41 | 42 | open_with 43 | .and_then(|cmd| Command::new(cmd).arg(&target_address).status()) 44 | .and_then(|status| { 45 | if status.success() { 46 | Ok(()) 47 | } else { 48 | Err(Error::new(ErrorKind::Other, "Non zero status")) 49 | } 50 | }) 51 | .unwrap_or_else(|_| println!("Please navigate to {}", target_address)); 52 | } 53 | 54 | pub fn consent_page_html(route: &str, solicitation: Solicitation) -> String { 55 | macro_rules! template { 56 | () => { 57 | "'{0:}' (at {1:}) is requesting permission for '{2:}' 58 |
59 | 60 | 61 |
62 | " 63 | }; 64 | } 65 | 66 | let grant = solicitation.pre_grant(); 67 | let state = solicitation.state(); 68 | 69 | let mut extra = vec![ 70 | ("response_type", "code"), 71 | ("client_id", grant.client_id.as_str()), 72 | ("redirect_uri", grant.redirect_uri.as_str()), 73 | ]; 74 | 75 | if let Some(state) = state { 76 | extra.push(("state", state)); 77 | } 78 | 79 | format!(template!(), 80 | grant.client_id, 81 | grant.redirect_uri, 82 | grant.scope, 83 | serde_urlencoded::to_string(extra).unwrap(), 84 | &route, 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /examples/support/gotham.rs: -------------------------------------------------------------------------------- 1 | extern crate reqwest; 2 | extern crate gotham; 3 | extern crate mime; 4 | extern crate serde_urlencoded; 5 | extern crate serde; 6 | extern crate serde_json; 7 | extern crate hyper; 8 | 9 | use self::reqwest::header; 10 | use self::hyper::{StatusCode, Response}; 11 | use self::gotham::state::{FromState, State}; 12 | use self::gotham::http::response::create_response; 13 | 14 | use std::collections::HashMap; 15 | use std::io::Read; 16 | 17 | use main::OauthResultQueryExtractor; 18 | 19 | 20 | pub fn dummy_client(mut state: State) -> (State, Response) { 21 | let query_params = OauthResultQueryExtractor::take_from(&mut state); 22 | if let Some(cause) = query_params.error { 23 | let res = create_response( 24 | &state, 25 | StatusCode::Ok, 26 | Some((format!("Error during owner authorization: {:?}", cause).into_bytes(), mime::TEXT_PLAIN)), 27 | ); 28 | return (state, res); 29 | } 30 | 31 | let code = match query_params.code { 32 | None => { 33 | let res = create_response( 34 | &state, 35 | StatusCode::BadRequest, 36 | Some((String::from("Missing code").into_bytes(), mime::TEXT_PLAIN)), 37 | ); 38 | return (state, res) 39 | }, 40 | Some(code) => code, 41 | }; 42 | 43 | // Construct a request against http://localhost:8020/token, the access token endpoint 44 | let client = reqwest::blocking::Client::new(); 45 | let mut params = HashMap::new(); 46 | params.insert("grant_type", "authorization_code"); 47 | params.insert("client_id", "LocalClient"); 48 | params.insert("code", &code); 49 | params.insert("redirect_uri", "http://localhost:8021/endpoint"); 50 | let access_token_request = client 51 | .post("http://localhost:8020/token") 52 | .form(¶ms).build().unwrap(); 53 | let mut token_response = client.execute(access_token_request).unwrap(); 54 | let mut token = String::new(); 55 | token_response.read_to_string(&mut token).unwrap(); 56 | let token_map: HashMap = serde_json::from_str(&token).unwrap(); 57 | 58 | if token_map.get("error").is_some() || !token_map.get("access_token").is_some() { 59 | let res = create_response(&state, StatusCode::BadRequest, Some((token.into_bytes(), mime::TEXT_PLAIN))); 60 | return (state, res); 61 | } 62 | 63 | // Request the page with the oauth token 64 | let page_request = client 65 | .get("http://localhost:8020/") 66 | .header(header::AUTHORIZATION, "Bearer ".to_string() + token_map.get("access_token").unwrap()) 67 | .build().unwrap(); 68 | let mut page_response = client.execute(page_request).unwrap(); 69 | let mut protected_page = String::new(); 70 | page_response.read_to_string(&mut protected_page).unwrap(); 71 | 72 | let token = serde_json::to_string_pretty(&token_map).unwrap(); 73 | let token = token.replace(",", ",
"); 74 | let display_page = format!( 75 | " 80 |
81 | Used token to access 82 | http://localhost:8020/. 83 | Its contents are: 84 |
{}
85 |
", token, protected_page); 86 | 87 | let res = create_response( 88 | &state, 89 | StatusCode::Ok, 90 | Some((display_page.into_bytes(), mime::TEXT_HTML)), 91 | ); 92 | 93 | (state, res) 94 | } 95 | -------------------------------------------------------------------------------- /examples/support/iron.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | extern crate serde_json; 3 | 4 | #[path = "./generic.rs"] 5 | mod generic; 6 | 7 | pub use self::generic::*; 8 | 9 | use std::collections::HashMap; 10 | use std::io::Read; 11 | use std::sync::{Arc, RwLock}; 12 | 13 | use iron::{headers, modifiers, IronResult, Request, Response}; 14 | use iron::middleware::Handler; 15 | use iron::status::Status; 16 | use reqwest::header; 17 | 18 | /// Rough client function mirroring core functionality of an oauth client. This is not actually 19 | /// needed in your implementation but merely exists to provide an interactive example. It will 20 | /// always identify itself as `LocalClient` with redirect url `http://localhost:8021/endpoint`. 21 | 22 | #[derive(Default)] 23 | struct State { 24 | token: RwLock>, 25 | token_map: RwLock>, 26 | } 27 | 28 | pub fn dummy_client() -> impl Handler + 'static { 29 | let get_state = Arc::new(State::default()); 30 | let endpoint_state = get_state.clone(); 31 | let mut router = router::Router::new(); 32 | router.get("/endpoint", move |request: &mut Request| endpoint(get_state.clone(), request), "endpoint"); 33 | router.get("/", move |request: &mut Request| view(endpoint_state.clone(), request), "view"); 34 | router 35 | } 36 | 37 | /// Receive the authorization codes at 'http://localhost:8021/endpoint'. 38 | fn endpoint(state: Arc, req: &mut Request) -> IronResult { 39 | // Check the received parameters in the input 40 | let query = req.url.as_ref() 41 | .query_pairs() 42 | .collect::>(); 43 | 44 | if let Some(error) = query.get("error") { 45 | let message = format!("Error during owner authorization: {}", error.as_ref()); 46 | return Ok(Response::with((Status::Ok, message))); 47 | }; 48 | 49 | let code = match query.get("code") { 50 | None => return Ok(Response::with((Status::BadRequest, "Missing code"))), 51 | Some(v) => v.clone() 52 | }; 53 | 54 | // Construct a request against http://localhost:8020/token, the access token endpoint 55 | let client = reqwest::blocking::Client::new(); 56 | let mut params = HashMap::new(); 57 | params.insert("grant_type", "authorization_code"); 58 | params.insert("client_id", "LocalClient"); 59 | params.insert("code", &code); 60 | params.insert("redirect_uri", "http://localhost:8021/endpoint"); 61 | let access_token_request = client 62 | .post("http://localhost:8020/token") 63 | .form(¶ms).build().unwrap(); 64 | let mut token_response = match client.execute(access_token_request) { 65 | Ok(response) => response, 66 | Err(_) => return Ok(Response::with((Status::InternalServerError, "Could not fetch bearer token"))), 67 | }; 68 | 69 | let mut token = String::new(); 70 | token_response.read_to_string(&mut token).unwrap(); 71 | let token_map: HashMap = match serde_json::from_str(&token) { 72 | Ok(response) => response, 73 | Err(err) => return Ok(Response::with((Status::BadRequest, format!("Could not parse token response {:?}", err)))), 74 | }; 75 | 76 | if token_map.get("error").is_some() { 77 | return Ok(Response::with((Status::BadRequest, token))); 78 | } 79 | 80 | let token = match token_map.get("access_token") { 81 | None => return Ok(Response::with((Status::BadRequest, token))), 82 | Some(token) => token, 83 | }; 84 | 85 | let token_map = serde_json::to_string_pretty(&token_map).unwrap(); 86 | let token_map = token_map.replace(",", ",
"); 87 | 88 | let mut set_map = state.token_map.write().unwrap(); 89 | *set_map = Some(token_map); 90 | 91 | let mut set_token = state.token.write().unwrap(); 92 | *set_token = Some(token.to_string()); 93 | 94 | let mut response = Response::with(Status::Found); 95 | response.headers.set(headers::Location("/".into())); 96 | Ok(response) 97 | } 98 | 99 | fn view(state: Arc, _: &mut Request) -> IronResult { 100 | let token = state.token.read().unwrap(); 101 | let token = match *token { 102 | None => return Ok(Response::with((Status::Ok, "No token granted yet"))), 103 | Some(ref token) => token, 104 | }; 105 | 106 | let token_map = state.token_map.read().unwrap(); 107 | let token_map = token_map.as_ref().unwrap(); 108 | 109 | let client = reqwest::blocking::Client::new(); 110 | // Request the page with the oauth token 111 | let page_request = client 112 | .get("http://localhost:8020/") 113 | .header(header::AUTHORIZATION, format!("Bearer {}", token)) 114 | .build().unwrap(); 115 | 116 | let mut page_response = match client.execute(page_request) { 117 | Ok(response) => response, 118 | Err(_) => return Ok(Response::with((Status::BadRequest, "Could not access protected resource"))), 119 | }; 120 | 121 | let mut protected_page = String::new(); 122 | page_response.read_to_string(&mut protected_page).unwrap(); 123 | 124 | let display_page = format!( 125 | " 130 |
131 | Used token to access 132 | http://localhost:8020/. 133 | Its contents are: 134 |
{}
135 |
", token_map, protected_page); 136 | 137 | Ok(Response::with(( 138 | Status::Ok, 139 | modifiers::Header(headers::ContentType::html()), 140 | display_page, 141 | ))) 142 | } 143 | -------------------------------------------------------------------------------- /examples/support/rocket.rs: -------------------------------------------------------------------------------- 1 | extern crate rocket; 2 | 3 | #[path = "generic.rs"] 4 | mod generic; 5 | 6 | use self::generic::{Client, ClientConfig, ClientError}; 7 | 8 | use rocket::{Rocket, State}; 9 | use rocket::fairing::{Fairing, Info, Kind}; 10 | use rocket::http::Status; 11 | use rocket::response::{Redirect, content::Html, status::Custom}; 12 | 13 | pub use self::generic::consent_page_html; 14 | pub struct ClientFairing; 15 | 16 | impl Fairing for ClientFairing { 17 | fn info(&self) -> Info { 18 | Info { 19 | name: "Simple oauth client implementation", 20 | kind: Kind::Attach, 21 | } 22 | } 23 | 24 | fn on_attach(&self, rocket: Rocket) -> Result { 25 | let config = ClientConfig { 26 | client_id: "LocalClient".into(), 27 | protected_url: "http://localhost:8000/".into(), 28 | token_url: "http://localhost:8000/token".into(), 29 | refresh_url: "http://localhost:8000/refresh".into(), 30 | redirect_uri: "http://localhost:8000/clientside/endpoint".into(), 31 | client_secret: None 32 | }; 33 | Ok(rocket 34 | .manage(Client::new(config)) 35 | .mount("/clientside", routes![oauth_endpoint, client_view, client_debug, refresh])) 36 | } 37 | } 38 | 39 | #[get("/endpoint?&")] 40 | fn oauth_endpoint<'r>(code: Option, error: Option, state: State) 41 | -> Result> 42 | { 43 | if let Some(error) = error { 44 | return Err(Custom(Status::InternalServerError, 45 | format!("Error during owner authorization: {:?}", error))) 46 | } 47 | 48 | let code = code 49 | .ok_or_else(|| Custom(Status::BadRequest, 50 | "Endpoint hit without an authorization code".into()))?; 51 | state.authorize(&code) 52 | .map_err(internal_error)?; 53 | 54 | Ok(Redirect::found("/clientside")) 55 | } 56 | 57 | #[get("/")] 58 | fn client_view(state: State) -> Result, Custom> { 59 | let protected_page = state 60 | .retrieve_protected_page() 61 | .map_err(internal_error)?; 62 | 63 | let display_page = format!( 64 | " 69 |
70 | Used token to access 71 | http://localhost:8000/. 72 | Its contents are: 73 |
{:?}
74 |
75 |
", state.as_html(), protected_page); 76 | 77 | Ok(Html(display_page)) 78 | } 79 | 80 | #[post("/refresh")] 81 | fn refresh(state: State) -> Result> { 82 | state.refresh() 83 | .map_err(internal_error) 84 | .map(|()| Redirect::found("/clientside")) 85 | } 86 | 87 | #[get("/debug")] 88 | fn client_debug(state: State) -> Html { 89 | Html(state.as_html()) 90 | } 91 | 92 | fn internal_error(err: ClientError) -> Custom { 93 | Custom(Status::InternalServerError, err.to_string()) 94 | } 95 | -------------------------------------------------------------------------------- /examples/support/rouille.rs: -------------------------------------------------------------------------------- 1 | extern crate reqwest; 2 | extern crate rouille; 3 | extern crate serde_urlencoded; 4 | extern crate serde; 5 | extern crate serde_json; 6 | 7 | #[path="generic.rs"] 8 | mod generic; 9 | 10 | pub use self::generic::{Client, ClientConfig, ClientError}; 11 | pub use self::generic::{consent_page_html, open_in_browser}; 12 | 13 | use self::rouille::{Request, Response}; 14 | 15 | pub fn dummy_client() 16 | -> impl (Fn(&Request) -> Response) + 'static 17 | { 18 | let client = Client::new(ClientConfig { 19 | client_id: "LocalClient".into(), 20 | protected_url: "http://localhost:8020/".into(), 21 | token_url: "http://localhost:8020/token".into(), 22 | refresh_url: "http://localhost:8020/refresh".into(), 23 | redirect_uri: "http://localhost:8021/endpoint".into(), 24 | client_secret: None 25 | }); 26 | 27 | move |request| { 28 | router!(request, 29 | (GET) ["/"] => { 30 | client_impl(&client, request) 31 | }, 32 | (GET) ["/endpoint"] => { 33 | endpoint_impl(&client, request) 34 | }, 35 | (POST) ["/refresh"] => { 36 | refresh_impl(&client, request) 37 | }, 38 | _ => Response::empty_404(), 39 | ) 40 | } 41 | } 42 | 43 | pub fn client_impl(client: &Client, _: &Request) -> Response { 44 | let protected_page = match client.retrieve_protected_page() { 45 | Ok(page) => page, 46 | Err(err) => return internal_error(err), 47 | }; 48 | 49 | let display_page = format!( 50 | " 55 |
56 | Used token to access 57 | http://localhost:8020/. 58 | Its contents are: 59 |
{:?}
60 |
61 |
", client.as_html(), protected_page); 62 | 63 | Response::html(display_page) 64 | .with_status_code(200) 65 | } 66 | 67 | fn endpoint_impl(client: &Client, request: &Request) -> Response { 68 | if let Some(error) = request.get_param("error") { 69 | return Response::text(format!("Error during owner authorization: {:?}", error)) 70 | .with_status_code(400); 71 | } 72 | 73 | let code = match request.get_param("code") { 74 | Some(code) => code, 75 | None => return Response::text("Endpoint hit without an authorization code") 76 | .with_status_code(400), 77 | }; 78 | 79 | if let Err(err) = client.authorize(&code) { 80 | return internal_error(err); 81 | } 82 | 83 | Response::redirect_303("/") 84 | } 85 | 86 | fn refresh_impl(client: &Client, _: &Request) -> Response { 87 | client.refresh() 88 | .err() 89 | .map_or_else(|| Response::redirect_303("/"), internal_error) 90 | } 91 | 92 | fn internal_error(error: ClientError) -> Response { 93 | Response::text(error.to_string()).with_status_code(500) 94 | } 95 | -------------------------------------------------------------------------------- /oxide-auth-actix/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-actix" 3 | version = "0.3.0" 4 | authors = ["Andreas Molzer "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | 7 | description = "A OAuth2 server library for actix featuring a set of configurable and pluggable backends." 8 | readme = "Readme.md" 9 | keywords = ["oauth", "server", "oauth2"] 10 | categories = ["web-programming::http-server", "authentication"] 11 | license = "MIT OR Apache-2.0" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | actix = { version = "0.13", default-features = false } 16 | actix-web = { version = "4.2.1", default-features = false } 17 | futures = "0.3" 18 | oxide-auth = { version = "0.6.0", path = "../oxide-auth" } 19 | serde_urlencoded = "0.7" 20 | url = "2" 21 | 22 | [dev-dependencies] 23 | base64 = "0.21" 24 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 25 | serde = "1.0" 26 | serde_json = "1.0" 27 | -------------------------------------------------------------------------------- /oxide-auth-actix/Changes.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 2 | 3 | - Added support for the `ClientCredentials` flow. 4 | - Now compatible to `actix = "0.13"`. 5 | - Reduced feature dependencies from `chrono`. 6 | 7 | ## 0.2.0 8 | 9 | - Now compatible to `actix-web = "4"`. 10 | - No functional changes. 11 | -------------------------------------------------------------------------------- /oxide-auth-actix/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-actix 2 | 3 | Integrates `oxide-auth` with the actor model and web requests of `actix-web`. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-actix.svg)](https://crates.io/crates/oxide-auth-actix) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-actix/badge.svg)](https://docs.rs/oxide-auth-actix/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [LICENSE-MIT]: docs/LICENSE-MIT 19 | [LICENSE-APACHE]: docs/LICENSE-APACHE 20 | -------------------------------------------------------------------------------- /oxide-auth-actix/examples/actix-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-example" 3 | version = "0.0.0" 4 | authors = ["Andreas Molzer "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | actix = "0.13" 9 | actix-web = "4.2.1" 10 | env_logger = "0.9" 11 | futures = "0.3" 12 | oxide-auth = { version = "0.6.0", path = "./../../../oxide-auth" } 13 | oxide-auth-actix = { version = "0.3.0", path = "./../../" } 14 | reqwest = { version = "0.11.10", features = ["blocking"] } 15 | serde = "1.0" 16 | serde_json = "1.0" 17 | url = "2" 18 | serde_urlencoded = "0.7" 19 | tokio = "1.16.1" 20 | -------------------------------------------------------------------------------- /oxide-auth-actix/examples/actix-example/src/support.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | #[path = "../../../../examples/support/generic.rs"] 3 | mod generic; 4 | 5 | use std::collections::HashMap; 6 | 7 | pub use self::generic::{consent_page_html, open_in_browser, Client, ClientConfig, ClientError}; 8 | 9 | use actix_web::{ 10 | App, dev, 11 | web::{self, Data}, 12 | HttpServer, HttpResponse, Responder, 13 | middleware::{Logger, NormalizePath, TrailingSlash}, 14 | }; 15 | 16 | pub fn dummy_client() -> dev::Server { 17 | let client = Client::new(ClientConfig { 18 | client_id: "LocalClient".into(), 19 | client_secret: Some("SecretSecret".to_owned()), 20 | protected_url: "http://localhost:8020/".into(), 21 | token_url: "http://localhost:8020/token".into(), 22 | refresh_url: "http://localhost:8020/refresh".into(), 23 | redirect_uri: "http://localhost:8021/endpoint".into(), 24 | }); 25 | 26 | HttpServer::new(move || { 27 | App::new() 28 | .app_data(Data::new(client.clone())) 29 | .wrap(Logger::default()) 30 | .wrap(NormalizePath::new(TrailingSlash::Trim)) 31 | .route("/endpoint", web::get().to(endpoint_impl)) 32 | .route("/refresh", web::post().to(refresh)) 33 | .route("/", web::get().to(get_with_token)) 34 | }) 35 | .bind("localhost:8021") 36 | .expect("Failed to start dummy client") 37 | .run() 38 | } 39 | 40 | async fn endpoint_impl( 41 | (query, state): (web::Query>, web::Data), 42 | ) -> impl Responder { 43 | if let Some(cause) = query.get("error") { 44 | return HttpResponse::BadRequest() 45 | .body(format!("Error during owner authorization: {:?}", cause)); 46 | } 47 | 48 | let code = match query.get("code") { 49 | None => return HttpResponse::BadRequest().body("Missing code"), 50 | Some(code) => code.clone(), 51 | }; 52 | 53 | let auth_handle = tokio::task::spawn_blocking(move || { 54 | let res = state.authorize(&code); 55 | res 56 | }); 57 | let auth_result = auth_handle.await.unwrap(); 58 | 59 | match auth_result { 60 | Ok(()) => HttpResponse::Found().append_header(("Location", "/")).finish(), 61 | Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)), 62 | } 63 | } 64 | 65 | async fn refresh(state: web::Data) -> impl Responder { 66 | let refresh_handle = tokio::task::spawn_blocking(move || { 67 | let res = state.refresh(); 68 | res 69 | }); 70 | let refresh_result = refresh_handle.await.unwrap(); 71 | 72 | match refresh_result { 73 | Ok(()) => HttpResponse::Found().append_header(("Location", "/")).finish(), 74 | Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)), 75 | } 76 | } 77 | 78 | async fn get_with_token(state: web::Data) -> impl Responder { 79 | let html = state.as_html(); 80 | 81 | let protected_page_handle = tokio::task::spawn_blocking(move || { 82 | let res = state.retrieve_protected_page(); 83 | res 84 | }); 85 | let protected_page_result = protected_page_handle.await.unwrap(); 86 | 87 | let protected_page = match protected_page_result { 88 | Ok(page) => page, 89 | Err(err) => return HttpResponse::InternalServerError().body(format!("{}", err)), 90 | }; 91 | 92 | let display_page = format!( 93 | " 98 |
99 | Used token to access 100 | http://localhost:8020/. 101 | Its contents are: 102 |
{}
103 |
104 |
", html, protected_page); 105 | 106 | HttpResponse::Ok().content_type("text/html").body(display_page) 107 | } 108 | -------------------------------------------------------------------------------- /oxide-auth-actix/src/operations.rs: -------------------------------------------------------------------------------- 1 | use crate::{OAuthRequest, OAuthResponse, OAuthOperation, WebError}; 2 | use oxide_auth::{ 3 | endpoint::{ 4 | AccessTokenFlow, AuthorizationFlow, Endpoint, RefreshFlow, ResourceFlow, ClientCredentialsFlow, 5 | }, 6 | primitives::grant::Grant, 7 | }; 8 | 9 | /// Authorization-related operations 10 | pub struct Authorize(pub OAuthRequest); 11 | 12 | impl OAuthOperation for Authorize { 13 | type Item = OAuthResponse; 14 | type Error = WebError; 15 | 16 | fn run(self, endpoint: E) -> Result 17 | where 18 | E: Endpoint, 19 | WebError: From, 20 | { 21 | AuthorizationFlow::prepare(endpoint)? 22 | .execute(self.0) 23 | .map_err(WebError::from) 24 | } 25 | } 26 | 27 | /// Token-related operations 28 | pub struct Token(pub OAuthRequest); 29 | 30 | impl OAuthOperation for Token { 31 | type Item = OAuthResponse; 32 | type Error = WebError; 33 | 34 | fn run(self, endpoint: E) -> Result 35 | where 36 | E: Endpoint, 37 | WebError: From, 38 | { 39 | AccessTokenFlow::prepare(endpoint)? 40 | .execute(self.0) 41 | .map_err(WebError::from) 42 | } 43 | } 44 | 45 | /// Client Credentials related operations 46 | pub struct ClientCredentials(pub OAuthRequest); 47 | 48 | impl OAuthOperation for ClientCredentials { 49 | type Item = OAuthResponse; 50 | type Error = WebError; 51 | 52 | fn run(self, endpoint: E) -> Result 53 | where 54 | E: Endpoint, 55 | WebError: From, 56 | { 57 | ClientCredentialsFlow::prepare(endpoint)? 58 | .execute(self.0) 59 | .map_err(WebError::from) 60 | } 61 | } 62 | 63 | /// Refresh-related operations 64 | pub struct Refresh(pub OAuthRequest); 65 | 66 | impl OAuthOperation for Refresh { 67 | type Item = OAuthResponse; 68 | type Error = WebError; 69 | 70 | fn run(self, endpoint: E) -> Result 71 | where 72 | E: Endpoint, 73 | WebError: From, 74 | { 75 | RefreshFlow::prepare(endpoint)? 76 | .execute(self.0) 77 | .map_err(WebError::from) 78 | } 79 | } 80 | 81 | /// Resource-related operations 82 | pub struct Resource(pub OAuthRequest); 83 | 84 | impl OAuthOperation for Resource { 85 | type Item = Grant; 86 | type Error = Result; 87 | 88 | fn run(self, endpoint: E) -> Result 89 | where 90 | E: Endpoint, 91 | WebError: From, 92 | { 93 | ResourceFlow::prepare(endpoint) 94 | .map_err(|e| Err(WebError::from(e)))? 95 | .execute(self.0) 96 | .map_err(|r| r.map_err(WebError::from)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /oxide-auth-async/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-async" 3 | version = "0.2.1" 4 | authors = ["Andreas Molzer "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | 7 | description = "Combines oxide-auth with async and futures" 8 | readme = "Readme.md" 9 | keywords = ["oauth", "server", "oauth2"] 10 | categories = ["web-programming::http-server", "authentication"] 11 | license = "MIT OR Apache-2.0" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | async-trait = "0.1.59" 16 | oxide-auth = { version = "0.6.0", path = "../oxide-auth" } 17 | base64 = "0.21" 18 | url = "2.3.1" 19 | chrono = { version = "0.4.23", default-features = false, features = ["clock"] } 20 | 21 | [dev-dependencies] 22 | serde = "1.0.148" 23 | serde_derive = "1.0.148" 24 | serde_json = "1.0.89" 25 | smol = "1.3.0" 26 | -------------------------------------------------------------------------------- /oxide-auth-async/Changes.md: -------------------------------------------------------------------------------- 1 | # v0.1.1 (2023-Sep-23) 2 | 3 | Feature release: 4 | - Adds `client_credentials` module, implemented following the `oxide-auth` base implementation. 5 | - Adds the `ClientCredentialFlow` for asynchronous endpoint implementations. 6 | - Implements the asynchronous traits for `oxide_auth`'s basic endpoint wrapper types: 7 | `AddonList`, `Extended` from `oxide_auth::frontends::simple`. 8 | 9 | Maintenance changes: 10 | - Bumps `oxide_auth` required version to `0.5.4`. 11 | -------------------------------------------------------------------------------- /oxide-auth-async/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-async 2 | 3 | Integrates `oxide-auth` with futures and `async fn`. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-async.svg)](https://crates.io/crates/oxide-auth-async) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-async/badge.svg)](https://docs.rs/oxide-auth-async/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [LICENSE-MIT]: docs/LICENSE-MIT 19 | [LICENSE-APACHE]: docs/LICENSE-APACHE 20 | -------------------------------------------------------------------------------- /oxide-auth-async/src/endpoint/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use oxide_auth::endpoint::{OAuthError, Template, WebRequest, OwnerConsent, Solicitation, Scopes}; 3 | 4 | pub use crate::code_grant::access_token::{Extension as AccessTokenExtension}; 5 | pub use crate::code_grant::authorization::Extension as AuthorizationExtension; 6 | pub use crate::code_grant::client_credentials::{Extension as ClientCredentialsExtension}; 7 | use crate::primitives::{Authorizer, Registrar, Issuer}; 8 | 9 | pub mod authorization; 10 | pub mod access_token; 11 | pub mod client_credentials; 12 | pub mod refresh; 13 | pub mod resource; 14 | 15 | pub trait Endpoint 16 | where 17 | Request: WebRequest, 18 | { 19 | /// The error typed used as the error representation of each flow. 20 | type Error; 21 | 22 | /// A registrar if this endpoint can access one. 23 | /// 24 | /// Returning `None` will implicate failing any flow that requires a registrar but does not 25 | /// have any effect on flows that do not require one. 26 | fn registrar(&self) -> Option<&(dyn Registrar + Sync)>; 27 | 28 | /// An authorizer if this endpoint can access one. 29 | /// 30 | /// Returning `None` will implicate failing any flow that requires an authorizer but does not 31 | /// have any effect on flows that do not require one. 32 | fn authorizer_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)>; 33 | 34 | /// An issuer if this endpoint can access one. 35 | /// 36 | /// Returning `None` will implicate failing any flow that requires an issuer but does not have 37 | /// any effect on flows that do not require one. 38 | fn issuer_mut(&mut self) -> Option<&mut (dyn Issuer + Send)>; 39 | 40 | /// Return the system that checks owner consent. 41 | /// 42 | /// Returning `None` will implicated failing the authorization code flow but does have any 43 | /// effect on other flows. 44 | fn owner_solicitor(&mut self) -> Option<&mut (dyn OwnerSolicitor + Send)>; 45 | 46 | /// Determine the required scopes for a request. 47 | /// 48 | /// The client must fulfill any one scope, so returning an empty slice will always deny the 49 | /// request. 50 | fn scopes(&mut self) -> Option<&mut dyn Scopes>; 51 | 52 | /// Generate a prototype response. 53 | /// 54 | /// The endpoint can rely on this being called at most once for each flow, if it wants 55 | /// to preallocate the response or return a handle on an existing prototype. 56 | fn response( 57 | &mut self, request: &mut Request, kind: Template, 58 | ) -> Result; 59 | 60 | /// Wrap an error. 61 | fn error(&mut self, err: OAuthError) -> Self::Error; 62 | 63 | /// Wrap an error in the request/response types. 64 | fn web_error(&mut self, err: Request::Error) -> Self::Error; 65 | 66 | /// Get the central extension instance this endpoint. 67 | /// 68 | /// Returning `None` is the default implementation and acts as simply providing any extensions. 69 | fn extension(&mut self) -> Option<&mut (dyn Extension + Send)> { 70 | None 71 | } 72 | } 73 | 74 | pub trait Extension { 75 | /// The handler for authorization code extensions. 76 | fn authorization(&mut self) -> Option<&mut (dyn AuthorizationExtension + Send)> { 77 | None 78 | } 79 | 80 | /// The handler for access token extensions. 81 | fn access_token(&mut self) -> Option<&mut (dyn AccessTokenExtension + Send)> { 82 | None 83 | } 84 | 85 | /// The handler for client credentials extensions. 86 | fn client_credentials(&mut self) -> Option<&mut (dyn ClientCredentialsExtension + Send)> { 87 | None 88 | } 89 | } 90 | 91 | /// Checks consent with the owner of a resource, identified in a request. 92 | /// 93 | /// See [`frontends::simple`] for an implementation that permits arbitrary functions. 94 | /// 95 | /// [`frontends::simple`]: ../frontends/simple/endpoint/struct.FnSolicitor.html 96 | #[async_trait] 97 | pub trait OwnerSolicitor { 98 | /// Ensure that a user (resource owner) is currently authenticated (for example via a session 99 | /// cookie) and determine if he has agreed to the presented grants. 100 | async fn check_consent( 101 | &mut self, req: &mut Request, solicitation: Solicitation<'_>, 102 | ) -> OwnerConsent; 103 | } 104 | 105 | #[async_trait] 106 | impl OwnerSolicitor for T 107 | where 108 | T: oxide_auth::endpoint::OwnerSolicitor + ?Sized + Send, 109 | Request: Send, 110 | { 111 | async fn check_consent( 112 | &mut self, req: &mut Request, solicitation: Solicitation<'_>, 113 | ) -> OwnerConsent { 114 | oxide_auth::endpoint::OwnerSolicitor::check_consent(self, req, solicitation) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /oxide-auth-async/src/endpoint/resource.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, borrow::Cow}; 2 | 3 | use oxide_auth::code_grant::resource::{Error as ResourceError, Request as ResourceRequest}; 4 | use oxide_auth::{ 5 | endpoint::{Scope, WebResponse}, 6 | primitives::grant::Grant, 7 | }; 8 | 9 | use crate::code_grant::resource::{protect, Endpoint as ResourceEndpoint}; 10 | 11 | use super::*; 12 | 13 | /// Guards resources by requiring OAuth authorization. 14 | pub struct ResourceFlow 15 | where 16 | E: Endpoint, 17 | R: WebRequest, 18 | { 19 | endpoint: WrappedResource, 20 | } 21 | 22 | struct WrappedResource(E, PhantomData) 23 | where 24 | E: Endpoint, 25 | R: WebRequest; 26 | 27 | struct WrappedRequest { 28 | /// Original request. 29 | request: PhantomData, 30 | 31 | /// The authorization token. 32 | authorization: Option, 33 | 34 | /// An error if one occurred. 35 | /// 36 | /// Actual parsing of the authorization header is done in the lower level. 37 | error: Option, 38 | } 39 | 40 | struct Scoped<'a, E: 'a, R: 'a> { 41 | request: &'a mut R, 42 | endpoint: &'a mut E, 43 | } 44 | 45 | impl ResourceFlow 46 | where 47 | E: Endpoint + Send + Sync, 48 | R: WebRequest + Send + Sync, 49 | ::Error: Send + Sync, 50 | { 51 | /// Check that the endpoint supports the necessary operations for handling requests. 52 | /// 53 | /// Binds the endpoint to a particular type of request that it supports, for many 54 | /// implementations this is probably single type anyways. 55 | /// 56 | /// ## Panics 57 | /// 58 | /// Indirectly `execute` may panic when this flow is instantiated with an inconsistent 59 | /// endpoint, for details see the documentation of `Endpoint` and `execute`. For 60 | /// consistent endpoints, the panic is instead caught as an error here. 61 | pub fn prepare(mut endpoint: E) -> Result { 62 | if endpoint.issuer_mut().is_none() { 63 | return Err(endpoint.error(OAuthError::PrimitiveError)); 64 | } 65 | 66 | if endpoint.scopes().is_none() { 67 | return Err(endpoint.error(OAuthError::PrimitiveError)); 68 | } 69 | 70 | Ok(ResourceFlow { 71 | endpoint: WrappedResource(endpoint, PhantomData), 72 | }) 73 | } 74 | 75 | /// Use the checked endpoint to check for authorization for a resource. 76 | /// 77 | /// ## Panics 78 | /// 79 | /// When the issuer returned by the endpoint is suddenly `None` when previously it 80 | /// was `Some(_)`. 81 | pub async fn execute(&mut self, mut request: R) -> Result> { 82 | let protected = { 83 | let wrapped = WrappedRequest::new(&mut request); 84 | 85 | let mut scoped = Scoped { 86 | request: &mut request, 87 | endpoint: &mut self.endpoint.0, 88 | }; 89 | 90 | protect(&mut scoped, &wrapped).await 91 | }; 92 | 93 | protected.map_err(|err| self.denied(&mut request, err)) 94 | } 95 | 96 | fn denied(&mut self, request: &mut R, error: ResourceError) -> Result { 97 | let template = match &error { 98 | ResourceError::AccessDenied { .. } => Template::new_unauthorized(None, None), 99 | ResourceError::NoAuthentication { .. } => Template::new_unauthorized(None, None), 100 | ResourceError::InvalidRequest { .. } => Template::new_bad(None), 101 | ResourceError::PrimitiveError => { 102 | return Err(self.endpoint.0.error(OAuthError::PrimitiveError)) 103 | } 104 | }; 105 | 106 | let mut response = self.endpoint.0.response(request, template)?; 107 | response 108 | .unauthorized(&error.www_authenticate()) 109 | .map_err(|err| self.endpoint.0.web_error(err))?; 110 | 111 | Ok(response) 112 | } 113 | } 114 | 115 | impl WrappedRequest { 116 | fn new(request: &mut R) -> Self { 117 | let token = match request.authheader() { 118 | // TODO: this is unecessarily wasteful, we always clone. 119 | Ok(Some(token)) => Some(token.into_owned()), 120 | Ok(None) => None, 121 | Err(error) => return Self::from_error(error), 122 | }; 123 | 124 | WrappedRequest { 125 | request: PhantomData, 126 | authorization: token, 127 | error: None, 128 | } 129 | } 130 | 131 | fn from_error(error: R::Error) -> Self { 132 | WrappedRequest { 133 | request: PhantomData, 134 | authorization: None, 135 | error: Some(error), 136 | } 137 | } 138 | } 139 | 140 | impl<'a, E: 'a, R: 'a> ResourceEndpoint for Scoped<'a, E, R> 141 | where 142 | E: Endpoint, 143 | R: WebRequest, 144 | { 145 | fn scopes(&mut self) -> &[Scope] { 146 | self.endpoint.scopes().unwrap().scopes(self.request) 147 | } 148 | 149 | fn issuer(&mut self) -> &mut (dyn Issuer + Send) { 150 | self.endpoint.issuer_mut().unwrap() 151 | } 152 | } 153 | 154 | impl ResourceRequest for WrappedRequest { 155 | fn valid(&self) -> bool { 156 | self.error.is_none() 157 | } 158 | 159 | fn token(&self) -> Option> { 160 | self.authorization.as_deref().map(Cow::Borrowed) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /oxide-auth-async/src/frontends/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod simple; 2 | -------------------------------------------------------------------------------- /oxide-auth-async/src/frontends/simple/extensions/extended.rs: -------------------------------------------------------------------------------- 1 | use oxide_auth::{ 2 | frontends::simple::extensions::Extended, 3 | endpoint::{WebRequest, Scopes, Template, OAuthError}, 4 | }; 5 | 6 | use crate::{ 7 | endpoint::{Endpoint, Extension, OwnerSolicitor}, 8 | primitives::{Registrar, Authorizer, Issuer}, 9 | }; 10 | 11 | impl Endpoint for Extended 12 | where 13 | Request: WebRequest, 14 | Inner: Endpoint, 15 | Ext: Extension + Send, 16 | { 17 | type Error = Inner::Error; 18 | 19 | fn registrar(&self) -> Option<&(dyn Registrar + Sync)> { 20 | self.inner.registrar() 21 | } 22 | 23 | fn authorizer_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> { 24 | self.inner.authorizer_mut() 25 | } 26 | 27 | fn issuer_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> { 28 | self.inner.issuer_mut() 29 | } 30 | 31 | fn owner_solicitor(&mut self) -> Option<&mut (dyn OwnerSolicitor + Send)> { 32 | self.inner.owner_solicitor() 33 | } 34 | 35 | fn scopes(&mut self) -> Option<&mut dyn Scopes> { 36 | self.inner.scopes() 37 | } 38 | 39 | fn response( 40 | &mut self, request: &mut Request, kind: Template, 41 | ) -> Result { 42 | self.inner.response(request, kind) 43 | } 44 | 45 | fn error(&mut self, err: OAuthError) -> Self::Error { 46 | self.inner.error(err) 47 | } 48 | 49 | fn web_error(&mut self, err: Request::Error) -> Self::Error { 50 | self.inner.web_error(err) 51 | } 52 | 53 | fn extension(&mut self) -> Option<&mut (dyn Extension + Send)> { 54 | Some(&mut self.addons) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /oxide-auth-async/src/frontends/simple/extensions/list.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use oxide_auth::code_grant::authorization::Request; 3 | use oxide_auth::code_grant::accesstoken::Request as TokenRequest; 4 | use oxide_auth::code_grant::client_credentials::Request as ClientCredentialsRequest; 5 | use oxide_auth::frontends::simple::extensions::{AddonList, AddonResult}; 6 | use oxide_auth::primitives::grant::Extensions; 7 | 8 | use crate::endpoint::Extension; 9 | use crate::code_grant::access_token::{Extension as AccessTokenExtension}; 10 | use crate::code_grant::authorization::Extension as AuthorizationExtension; 11 | use crate::code_grant::client_credentials::{Extension as ClientCredentialsExtension}; 12 | 13 | impl Extension for AddonList { 14 | fn authorization(&mut self) -> Option<&mut (dyn AuthorizationExtension + Send)> { 15 | Some(self) 16 | } 17 | 18 | fn access_token(&mut self) -> Option<&mut (dyn AccessTokenExtension + Send)> { 19 | Some(self) 20 | } 21 | 22 | fn client_credentials(&mut self) -> Option<&mut (dyn ClientCredentialsExtension + Send)> { 23 | Some(self) 24 | } 25 | } 26 | 27 | #[async_trait] 28 | impl AuthorizationExtension for AddonList { 29 | async fn extend(&mut self, request: &(dyn Request + Sync)) -> std::result::Result { 30 | let mut result_data = Extensions::new(); 31 | 32 | for ext in self.authorization.iter() { 33 | let result = ext.execute(request); 34 | 35 | match result { 36 | AddonResult::Ok => (), 37 | AddonResult::Data(data) => result_data.set(ext, data), 38 | AddonResult::Err => return Err(()), 39 | } 40 | } 41 | 42 | Ok(result_data) 43 | } 44 | } 45 | 46 | #[async_trait] 47 | impl AccessTokenExtension for AddonList { 48 | async fn extend( 49 | &mut self, request: &(dyn TokenRequest + Sync), mut data: Extensions, 50 | ) -> std::result::Result { 51 | let mut result_data = Extensions::new(); 52 | 53 | for ext in self.access_token.iter() { 54 | let ext_data = data.remove(ext); 55 | let result = ext.execute(request, ext_data); 56 | 57 | match result { 58 | AddonResult::Ok => (), 59 | AddonResult::Data(data) => result_data.set(ext, data), 60 | AddonResult::Err => return Err(()), 61 | } 62 | } 63 | 64 | Ok(result_data) 65 | } 66 | } 67 | 68 | #[async_trait] 69 | impl ClientCredentialsExtension for AddonList { 70 | async fn extend( 71 | &mut self, request: &(dyn ClientCredentialsRequest + Sync), 72 | ) -> std::result::Result { 73 | let mut result_data = Extensions::new(); 74 | 75 | for ext in self.client_credentials.iter() { 76 | let result = ext.execute(request); 77 | 78 | match result { 79 | AddonResult::Ok => (), 80 | AddonResult::Data(data) => result_data.set(ext, data), 81 | AddonResult::Err => return Err(()), 82 | } 83 | } 84 | 85 | Ok(result_data) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /oxide-auth-async/src/frontends/simple/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod extended; 2 | pub mod list; 3 | -------------------------------------------------------------------------------- /oxide-auth-async/src/frontends/simple/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod extensions; 2 | -------------------------------------------------------------------------------- /oxide-auth-async/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod code_grant; 2 | pub mod endpoint; 3 | pub mod primitives; 4 | pub mod frontends; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /oxide-auth-async/src/primitives.rs: -------------------------------------------------------------------------------- 1 | //! Async versions of all primitives traits. 2 | use async_trait::async_trait; 3 | use oxide_auth::primitives::{grant::Grant, scope::Scope}; 4 | use oxide_auth::primitives::issuer::{IssuedToken, RefreshedToken}; 5 | use oxide_auth::primitives::{ 6 | authorizer, registrar, issuer, 7 | registrar::{ClientUrl, BoundClient, RegistrarError, PreGrant}, 8 | }; 9 | 10 | #[async_trait] 11 | pub trait Authorizer { 12 | async fn authorize(&mut self, _: Grant) -> Result; 13 | 14 | async fn extract(&mut self, _: &str) -> Result, ()>; 15 | } 16 | 17 | #[async_trait] 18 | impl Authorizer for T 19 | where 20 | T: authorizer::Authorizer + Send + ?Sized, 21 | { 22 | async fn authorize(&mut self, grant: Grant) -> Result { 23 | authorizer::Authorizer::authorize(self, grant) 24 | } 25 | 26 | async fn extract(&mut self, token: &str) -> Result, ()> { 27 | authorizer::Authorizer::extract(self, token) 28 | } 29 | } 30 | 31 | #[async_trait] 32 | pub trait Issuer { 33 | async fn issue(&mut self, _: Grant) -> Result; 34 | 35 | async fn refresh(&mut self, _: &str, _: Grant) -> Result; 36 | 37 | async fn recover_token(&mut self, _: &str) -> Result, ()>; 38 | 39 | async fn recover_refresh(&mut self, _: &str) -> Result, ()>; 40 | } 41 | 42 | #[async_trait] 43 | impl Issuer for T 44 | where 45 | T: issuer::Issuer + Send + ?Sized, 46 | { 47 | async fn issue(&mut self, grant: Grant) -> Result { 48 | issuer::Issuer::issue(self, grant) 49 | } 50 | 51 | async fn refresh(&mut self, token: &str, grant: Grant) -> Result { 52 | issuer::Issuer::refresh(self, token, grant) 53 | } 54 | 55 | async fn recover_token(&mut self, token: &str) -> Result, ()> { 56 | issuer::Issuer::recover_token(self, token) 57 | } 58 | 59 | async fn recover_refresh(&mut self, token: &str) -> Result, ()> { 60 | issuer::Issuer::recover_refresh(self, token) 61 | } 62 | } 63 | 64 | #[async_trait] 65 | pub trait Registrar { 66 | async fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result, RegistrarError>; 67 | 68 | async fn negotiate<'a>( 69 | &self, client: BoundClient<'a>, scope: Option, 70 | ) -> Result; 71 | 72 | async fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError>; 73 | } 74 | 75 | #[async_trait] 76 | impl Registrar for T 77 | where 78 | T: registrar::Registrar + Send + Sync + ?Sized, 79 | { 80 | async fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result, RegistrarError> { 81 | registrar::Registrar::bound_redirect(self, bound) 82 | } 83 | 84 | async fn negotiate<'a>( 85 | &self, client: BoundClient<'a>, scope: Option, 86 | ) -> Result { 87 | registrar::Registrar::negotiate(self, client, scope) 88 | } 89 | 90 | async fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> { 91 | registrar::Registrar::check(self, client_id, passphrase) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /oxide-auth-async/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | 4 | use oxide_auth::{ 5 | primitives::generator::TagGrant, 6 | endpoint::{WebRequest, WebResponse, OwnerConsent, QueryParameter, Solicitation}, 7 | primitives::grant::Grant, 8 | }; 9 | use url::Url; 10 | 11 | use crate::endpoint::OwnerSolicitor; 12 | 13 | /// Open and simple implementation of `WebRequest`. 14 | #[derive(Clone, Debug, Default)] 15 | struct CraftedRequest { 16 | /// The key-value pairs in the url query component. 17 | pub query: Option>>, 18 | 19 | /// The key-value pairs of a `x-www-form-urlencoded` body. 20 | pub urlbody: Option>>, 21 | 22 | /// Provided authorization header. 23 | pub auth: Option, 24 | } 25 | 26 | /// Open and simple implementation of `WebResponse`. 27 | #[derive(Debug, Default)] 28 | struct CraftedResponse { 29 | /// HTTP status code. 30 | pub status: Status, 31 | 32 | /// A location header, for example for redirects. 33 | pub location: Option, 34 | 35 | /// Indicates how the client should have authenticated. 36 | /// 37 | /// Only set with `Unauthorized` status. 38 | pub www_authenticate: Option, 39 | 40 | /// Encoded body of the response. 41 | /// 42 | /// One variant for each possible encoding type. 43 | pub body: Option, 44 | } 45 | 46 | /// An enum containing the necessary HTTP status codes. 47 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)] 48 | enum Status { 49 | /// Http status code 200. 50 | #[default] 51 | Ok, 52 | 53 | /// Http status code 302. 54 | Redirect, 55 | 56 | /// Http status code 400. 57 | BadRequest, 58 | 59 | /// Http status code 401. 60 | Unauthorized, 61 | } 62 | 63 | /// Models the necessary body contents. 64 | /// 65 | /// Real HTTP protocols should set a content type header for each of the body variants. 66 | #[derive(Clone, Debug)] 67 | enum Body { 68 | /// A pure text body. 69 | Text(String), 70 | 71 | /// A json encoded body, `application/json`. 72 | Json(String), 73 | } 74 | 75 | #[derive(Debug)] 76 | enum CraftedError { 77 | Crafted, 78 | } 79 | 80 | impl WebRequest for CraftedRequest { 81 | type Response = CraftedResponse; 82 | type Error = CraftedError; 83 | 84 | fn query(&mut self) -> Result, Self::Error> { 85 | self.query 86 | .as_ref() 87 | .map(|hm| Cow::Borrowed(hm as &dyn QueryParameter)) 88 | .ok_or(CraftedError::Crafted) 89 | } 90 | 91 | fn urlbody(&mut self) -> Result, Self::Error> { 92 | self.urlbody 93 | .as_ref() 94 | .map(|hm| Cow::Borrowed(hm as &dyn QueryParameter)) 95 | .ok_or(CraftedError::Crafted) 96 | } 97 | 98 | fn authheader(&mut self) -> Result>, Self::Error> { 99 | Ok(self.auth.as_ref().map(|bearer| bearer.as_str().into())) 100 | } 101 | } 102 | 103 | impl WebResponse for CraftedResponse { 104 | type Error = CraftedError; 105 | 106 | fn ok(&mut self) -> Result<(), Self::Error> { 107 | self.status = Status::Ok; 108 | self.location = None; 109 | self.www_authenticate = None; 110 | Ok(()) 111 | } 112 | 113 | /// A response which will redirect the user-agent to which the response is issued. 114 | fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { 115 | self.status = Status::Redirect; 116 | self.location = Some(url); 117 | self.www_authenticate = None; 118 | Ok(()) 119 | } 120 | 121 | /// Set the response status to 400. 122 | fn client_error(&mut self) -> Result<(), Self::Error> { 123 | self.status = Status::BadRequest; 124 | self.location = None; 125 | self.www_authenticate = None; 126 | Ok(()) 127 | } 128 | 129 | /// Set the response status to 401 and add a `WWW-Authenticate` header. 130 | fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> { 131 | self.status = Status::Unauthorized; 132 | self.location = None; 133 | self.www_authenticate = Some(header_value.to_owned()); 134 | Ok(()) 135 | } 136 | 137 | /// A pure text response with no special media type set. 138 | fn body_text(&mut self, text: &str) -> Result<(), Self::Error> { 139 | self.body = Some(Body::Text(text.to_owned())); 140 | Ok(()) 141 | } 142 | 143 | /// Json repsonse data, with media type `aplication/json. 144 | fn body_json(&mut self, data: &str) -> Result<(), Self::Error> { 145 | self.body = Some(Body::Json(data.to_owned())); 146 | Ok(()) 147 | } 148 | } 149 | 150 | struct TestGenerator(String); 151 | 152 | impl TagGrant for TestGenerator { 153 | fn tag(&mut self, _: u64, _grant: &Grant) -> Result { 154 | Ok(self.0.clone()) 155 | } 156 | } 157 | 158 | struct Allow(String); 159 | struct Deny; 160 | 161 | #[async_trait::async_trait] 162 | impl OwnerSolicitor for Allow { 163 | async fn check_consent( 164 | &mut self, _: &mut CraftedRequest, _: Solicitation<'_>, 165 | ) -> OwnerConsent { 166 | OwnerConsent::Authorized(self.0.clone()) 167 | } 168 | } 169 | 170 | #[async_trait::async_trait] 171 | impl OwnerSolicitor for Deny { 172 | async fn check_consent( 173 | &mut self, _: &mut CraftedRequest, _: Solicitation<'_>, 174 | ) -> OwnerConsent { 175 | OwnerConsent::Denied 176 | } 177 | } 178 | 179 | #[async_trait::async_trait] 180 | impl<'l> OwnerSolicitor for &'l Allow { 181 | async fn check_consent( 182 | &mut self, _: &mut CraftedRequest, _: Solicitation<'_>, 183 | ) -> OwnerConsent { 184 | OwnerConsent::Authorized(self.0.clone()) 185 | } 186 | } 187 | 188 | #[async_trait::async_trait] 189 | impl<'l> OwnerSolicitor for &'l Deny { 190 | async fn check_consent( 191 | &mut self, _: &mut CraftedRequest, _: Solicitation<'_>, 192 | ) -> OwnerConsent { 193 | OwnerConsent::Denied 194 | } 195 | } 196 | 197 | trait ToSingleValueQuery { 198 | fn to_single_value_query(self) -> HashMap>; 199 | } 200 | 201 | impl<'r, I, K, V> ToSingleValueQuery for I 202 | where 203 | I: Iterator, 204 | K: AsRef + 'r, 205 | V: AsRef + 'r, 206 | { 207 | fn to_single_value_query(self) -> HashMap> { 208 | self.map(|(k, v)| (k.as_ref().to_string(), vec![v.as_ref().to_string()])) 209 | .collect() 210 | } 211 | } 212 | 213 | fn assert_send(_val: &T) {} 214 | 215 | pub mod defaults { 216 | pub const EXAMPLE_CLIENT_ID: &str = "ClientId"; 217 | pub const EXAMPLE_OWNER_ID: &str = "Owner"; 218 | pub const EXAMPLE_PASSPHRASE: &str = "VGhpcyBpcyBhIHZlcnkgc2VjdXJlIHBhc3NwaHJhc2UK"; 219 | pub const EXAMPLE_REDIRECT_URI: &str = "https://client.example/endpoint"; 220 | pub const EXAMPLE_SCOPE: &str = "example default"; 221 | } 222 | 223 | mod authorization; 224 | mod access_token; 225 | mod client_credentials; 226 | mod type_properties; 227 | mod resource; 228 | mod refresh; 229 | // mod pkce; 230 | -------------------------------------------------------------------------------- /oxide-auth-async/src/tests/type_properties.rs: -------------------------------------------------------------------------------- 1 | use crate::code_grant; 2 | 3 | #[test] 4 | fn require_futures_have_send_bounds() { 5 | fn require_send T>(_: F) {} 6 | 7 | require_send(code_grant::access_token::access_token); 8 | require_send(code_grant::authorization::authorization_code); 9 | require_send(|pending, handler| { 10 | code_grant::authorization::Pending::authorize(pending, handler, "".into()) 11 | }); 12 | require_send(code_grant::client_credentials::client_credentials); 13 | require_send(code_grant::refresh::refresh); 14 | require_send(code_grant::resource::protect); 15 | } 16 | -------------------------------------------------------------------------------- /oxide-auth-axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-axum" 3 | version = "0.6.0" 4 | authors = ["Daniel Alvsåker "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | 7 | description = "A OAuth2 server library for Axum featuring a set of configurable and pluggable backends." 8 | readme = "Readme.md" 9 | keywords = ["oauth", "server", "oauth2"] 10 | categories = ["web-programming::http-server", "authentication"] 11 | license = "MIT OR Apache-2.0" 12 | edition = "2021" 13 | 14 | [dependencies] 15 | axum = { version = "0.8", default-features = false, features = [ 16 | "form", 17 | "query", 18 | ] } 19 | oxide-auth = { version = "0.6", path = "../oxide-auth" } 20 | -------------------------------------------------------------------------------- /oxide-auth-axum/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-axum 2 | 3 | Integrates `oxide-auth` with the [`axum`] web server library. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-axum.svg)](https://crates.io/crates/oxide-auth-axum) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-axum/badge.svg)](https://docs.rs/oxide-auth-axum/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [`axum`]: https://crates.io/crates/axum 19 | [LICENSE-MIT]: docs/LICENSE-MIT 20 | [LICENSE-APACHE]: docs/LICENSE-APACHE 21 | 22 | -------------------------------------------------------------------------------- /oxide-auth-axum/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::OAuthRequest; 2 | use axum::{ 3 | http::{header::InvalidHeaderValue, StatusCode}, 4 | response::{IntoResponse, Response}, 5 | }; 6 | use oxide_auth::frontends::{dev::OAuthError, simple::endpoint::Error}; 7 | 8 | #[derive(Debug)] 9 | /// The error type for Oxide Auth operations 10 | pub enum WebError { 11 | /// Errors occuring in Endpoint operations 12 | Endpoint(OAuthError), 13 | 14 | /// Errors occuring in Endpoint operations 15 | Header(InvalidHeaderValue), 16 | 17 | /// Errors with the request encoding 18 | Encoding, 19 | 20 | /// Request body could not be parsed as a form 21 | Form, 22 | 23 | /// Request query was absent or could not be parsed 24 | Query, 25 | 26 | /// Request query was absent or could not be parsed 27 | Body, 28 | 29 | /// The Authorization header was invalid 30 | Authorization, 31 | 32 | /// General internal server error 33 | InternalError(Option), 34 | } 35 | 36 | impl std::fmt::Display for WebError { 37 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 38 | match *self { 39 | WebError::Endpoint(ref e) => write!(f, "Endpoint, {}", e), 40 | WebError::Header(ref e) => write!(f, "Couldn't set header, {}", e), 41 | WebError::Encoding => write!(f, "Error decoding request"), 42 | WebError::Form => write!(f, "Request is not a form"), 43 | WebError::Query => write!(f, "No query present"), 44 | WebError::Body => write!(f, "No body present"), 45 | WebError::Authorization => write!(f, "Request has invalid Authorization headers"), 46 | WebError::InternalError(None) => write!(f, "An internal server error occured"), 47 | WebError::InternalError(Some(ref e)) => write!(f, "An internal server error occured: {}", e), 48 | } 49 | } 50 | } 51 | 52 | impl std::error::Error for WebError { 53 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 54 | match *self { 55 | WebError::Endpoint(ref e) => e.source(), 56 | WebError::Header(ref e) => e.source(), 57 | _ => None, 58 | } 59 | } 60 | } 61 | 62 | impl IntoResponse for WebError { 63 | fn into_response(self) -> Response { 64 | (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() 65 | } 66 | } 67 | 68 | impl From> for WebError { 69 | fn from(e: Error) -> Self { 70 | match e { 71 | Error::Web(e) => e, 72 | Error::OAuth(e) => e.into(), 73 | } 74 | } 75 | } 76 | 77 | impl From for WebError { 78 | fn from(e: OAuthError) -> Self { 79 | WebError::Endpoint(e) 80 | } 81 | } 82 | 83 | impl From for WebError { 84 | fn from(e: InvalidHeaderValue) -> Self { 85 | Self::Header(e) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /oxide-auth-axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Adaptations and integration for Axum. 2 | #![warn(missing_docs)] 3 | 4 | mod error; 5 | pub use error::WebError; 6 | 7 | mod request; 8 | pub use request::{OAuthResource, OAuthRequest}; 9 | 10 | mod response; 11 | pub use response::OAuthResponse; 12 | -------------------------------------------------------------------------------- /oxide-auth-axum/src/request.rs: -------------------------------------------------------------------------------- 1 | use oxide_auth::frontends::dev::{NormalizedParameter, QueryParameter, WebRequest}; 2 | use axum::{ 3 | extract::{Query, Form, FromRequest, FromRequestParts, Request}, 4 | http::{header, request::Parts}, 5 | }; 6 | use crate::{OAuthResponse, WebError}; 7 | use std::borrow::Cow; 8 | 9 | #[derive(Clone, Debug, Default)] 10 | /// Type implementing `WebRequest` as well as `FromRequest` for use in route handlers 11 | /// 12 | /// This type consumes the body of the Request upon extraction, so be careful not to use it in 13 | /// places you also expect an application payload 14 | pub struct OAuthRequest { 15 | auth: Option, 16 | query: Option, 17 | body: Option, 18 | } 19 | 20 | /// Type implementing `WebRequest` as well as `FromRequest` for use in guarding resources 21 | /// 22 | /// This is useful over [OAuthRequest] since [OAuthResource] doesn't consume the body of the 23 | /// request upon extraction 24 | pub struct OAuthResource { 25 | auth: Option, 26 | } 27 | 28 | impl OAuthRequest { 29 | /// Fetch the authorization header from the request 30 | pub fn authorization_header(&self) -> Option<&str> { 31 | self.auth.as_deref() 32 | } 33 | 34 | /// Fetch the query for this request 35 | pub fn query(&self) -> Option<&NormalizedParameter> { 36 | self.query.as_ref() 37 | } 38 | 39 | /// Fetch the query mutably 40 | pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> { 41 | self.query.as_mut() 42 | } 43 | 44 | /// Fetch the body of the request 45 | pub fn body(&self) -> Option<&NormalizedParameter> { 46 | self.body.as_ref() 47 | } 48 | } 49 | 50 | impl From for OAuthRequest { 51 | fn from(r: OAuthResource) -> OAuthRequest { 52 | OAuthRequest { 53 | auth: r.auth, 54 | ..Default::default() 55 | } 56 | } 57 | } 58 | 59 | impl WebRequest for OAuthRequest { 60 | type Error = WebError; 61 | type Response = OAuthResponse; 62 | 63 | fn query(&mut self) -> Result, Self::Error> { 64 | self.query 65 | .as_ref() 66 | .map(|q| Cow::Borrowed(q as &dyn QueryParameter)) 67 | .ok_or(WebError::Query) 68 | } 69 | 70 | fn urlbody(&mut self) -> Result, Self::Error> { 71 | self.body 72 | .as_ref() 73 | .map(|b| Cow::Borrowed(b as &dyn QueryParameter)) 74 | .ok_or(WebError::Body) 75 | } 76 | 77 | fn authheader(&mut self) -> Result>, Self::Error> { 78 | Ok(self.auth.as_deref().map(Cow::Borrowed)) 79 | } 80 | } 81 | 82 | impl FromRequest for OAuthRequest 83 | where 84 | S: Send + Sync, 85 | { 86 | type Rejection = WebError; 87 | 88 | async fn from_request(req: Request, state: &S) -> Result { 89 | let mut all_auth = req.headers().get_all(header::AUTHORIZATION).iter(); 90 | let optional = all_auth.next(); 91 | 92 | let auth = if all_auth.next().is_some() { 93 | return Err(WebError::Authorization); 94 | } else { 95 | optional.and_then(|hv| hv.to_str().ok().map(str::to_owned)) 96 | }; 97 | 98 | let (mut parts, body) = req.into_parts(); 99 | let query = Query::from_request_parts(&mut parts, state) 100 | .await 101 | .ok() 102 | .map(|q: Query| q.0); 103 | 104 | let req = Request::from_parts(parts, body); 105 | let body = Form::from_request(req, state) 106 | .await 107 | .ok() 108 | .map(|b: Form| b.0); 109 | 110 | Ok(Self { auth, query, body }) 111 | } 112 | } 113 | 114 | impl FromRequestParts for OAuthResource 115 | where 116 | S: Send + Sync, 117 | { 118 | type Rejection = WebError; 119 | 120 | async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { 121 | let mut all_auth = parts.headers.get_all(header::AUTHORIZATION).iter(); 122 | let optional = all_auth.next(); 123 | 124 | let auth = if all_auth.next().is_some() { 125 | return Err(WebError::Authorization); 126 | } else { 127 | optional.and_then(|hv| hv.to_str().ok().map(str::to_owned)) 128 | }; 129 | 130 | Ok(Self { auth }) 131 | } 132 | } 133 | 134 | impl OAuthResource { 135 | /// Fetch the authorization header from the request 136 | pub fn authorization_header(&self) -> Option<&str> { 137 | self.auth.as_deref() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /oxide-auth-axum/src/response.rs: -------------------------------------------------------------------------------- 1 | use crate::WebError; 2 | use axum::{ 3 | response::{IntoResponse, Response}, 4 | http::{ 5 | StatusCode, 6 | header::{self, HeaderMap, HeaderValue}, 7 | }, 8 | }; 9 | use oxide_auth::frontends::dev::{WebResponse, Url}; 10 | 11 | #[derive(Default, Clone, Debug)] 12 | /// Type implementing `WebResponse` and `IntoResponse` for use in route handlers 13 | pub struct OAuthResponse { 14 | status: StatusCode, 15 | headers: HeaderMap, 16 | body: Option, 17 | } 18 | 19 | impl OAuthResponse { 20 | /// Set the `ContentType` header on a response 21 | pub fn content_type(mut self, content_type: &str) -> Result { 22 | self.headers 23 | .insert(header::CONTENT_TYPE, content_type.try_into()?); 24 | Ok(self) 25 | } 26 | 27 | /// Set the body for the response 28 | pub fn body(mut self, body: &str) -> Self { 29 | self.body = Some(body.to_owned()); 30 | self 31 | } 32 | } 33 | 34 | impl WebResponse for OAuthResponse { 35 | type Error = WebError; 36 | 37 | fn ok(&mut self) -> Result<(), Self::Error> { 38 | self.status = StatusCode::OK; 39 | Ok(()) 40 | } 41 | 42 | fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { 43 | self.status = StatusCode::FOUND; 44 | self.headers.insert(header::LOCATION, url.as_ref().try_into()?); 45 | Ok(()) 46 | } 47 | 48 | fn client_error(&mut self) -> Result<(), Self::Error> { 49 | self.status = StatusCode::BAD_REQUEST; 50 | Ok(()) 51 | } 52 | 53 | fn unauthorized(&mut self, kind: &str) -> Result<(), Self::Error> { 54 | self.status = StatusCode::UNAUTHORIZED; 55 | self.headers.insert(header::WWW_AUTHENTICATE, kind.try_into()?); 56 | Ok(()) 57 | } 58 | 59 | fn body_text(&mut self, text: &str) -> Result<(), Self::Error> { 60 | self.body = Some(text.to_owned()); 61 | self.headers 62 | .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); 63 | Ok(()) 64 | } 65 | 66 | fn body_json(&mut self, json: &str) -> Result<(), Self::Error> { 67 | self.body = Some(json.to_owned()); 68 | self.headers 69 | .insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json")); 70 | Ok(()) 71 | } 72 | } 73 | 74 | impl IntoResponse for OAuthResponse { 75 | fn into_response(self) -> Response { 76 | (self.status, self.headers, self.body.unwrap_or_default()).into_response() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /oxide-auth-db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-db" 3 | version = "0.3.0" 4 | authors = ["liujing "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | description = "An implement of DB registrar with configurable databases." 7 | readme = "Readme.md" 8 | keywords = ["oauth", "server", "oauth2", "redis"] 9 | license = "MIT OR Apache-2.0" 10 | edition = "2018" 11 | 12 | [dependencies] 13 | oxide-auth = { version = "0.6.0", path = "../oxide-auth" } 14 | once_cell = "1.3.1" 15 | serde = { version = "1.0.101", features = ["derive"] } 16 | serde_json = "1.0" 17 | r2d2_redis = {version = "0.14", optional = true } 18 | url = "2" 19 | anyhow = "1.0" 20 | log = "0.4.8" 21 | 22 | 23 | [features] 24 | default = ["with-redis"] 25 | with-redis = ["r2d2_redis"] 26 | -------------------------------------------------------------------------------- /oxide-auth-db/Changes.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 2 | 3 | - Bump `r2d2_redis` to `0.14`. 4 | - Remove indirectly used dependencies. If you run into troubles you may need to 5 | manually add them to *your* project to enforce versions or features. No 6 | longer directly depends on: `rand`, `reqwest`, `rust-argon2`, `r2d2`, `tokio`. 7 | -------------------------------------------------------------------------------- /oxide-auth-db/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-db 2 | 3 | A DataBase Registrar Implementation for `oxide-auth`. 4 | 5 | ## About 6 | 7 | This cargo by-default provides a Redis Registrar Implementation. Users can add 8 | different Database Implementation in the db_service package. Then use the 9 | feature set to configure which db you want to use in the `Cargo.toml` file. 10 | 11 | ``` 12 | [features] 13 | default = ["with-redis"] 14 | with-redis = ["r2d2","r2d2_redis"] 15 | ``` 16 | 17 | 18 | ## Example 19 | 20 | Users should have a redis server in their environment and run the commands 21 | below to add a test client to redis. 22 | 23 | > `set LocalClient "{\"client_id\":\"LocalClient\",\"redirect_uri\":\"http://localhost:8021/endpoint\",\"additional_redirect_uris\":[],\"default_scope\":\"default-scope\",\"client_secret\":\"$argon2i$v=19$m=4096,t=3,p=1$FAnLM+AwjNhHrKA2aCVxQDmbPHC6jc4xyiX1ioxr66g$7PXkjalEW6ynIrkWDY86zaplnox919Tbd+wlDOmhLDg\"}"` 24 | 25 | Then you can run the db-example. 26 | 27 | > `$ cargo run db-example` 28 | 29 | You may have to wait a second after the html page automatically opened. 30 | 31 | ## Additional 32 | 33 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-db.svg)](https://crates.io/crates/oxide-auth-db) 34 | [![Docs.rs Status](https://docs.rs/oxide-auth-db/badge.svg)](https://docs.rs/oxide-auth-db/) 35 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 36 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 37 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 38 | 39 | Licensed under either of 40 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 41 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 42 | at your option. 43 | 44 | [LICENSE-MIT]: docs/LICENSE-MIT 45 | [LICENSE-APACHE]: docs/LICENSE-APACHE 46 | -------------------------------------------------------------------------------- /oxide-auth-db/examples/db-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db-example" 3 | version = "0.0.0" 4 | authors = ["liujing "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | oxide-auth = { version = "0.6.0", path = "../../../oxide-auth" } 11 | oxide-auth-actix = { version = "0.3.0", path = "./../../../oxide-auth-actix" } 12 | oxide-auth-db = { version = "0.3.0", path = "./../../", features = ["with-redis"] } 13 | 14 | anyhow = "1.0" 15 | actix = "0.13" 16 | actix-web = "4.2.1" 17 | env_logger = "0.9" 18 | futures = "0.3" 19 | reqwest = {version="0.11.10", features = ["blocking"]} 20 | r2d2_redis = {version = "0.14"} 21 | serde = "1.0" 22 | serde_json = "1.0" 23 | serde_urlencoded = "^0.7" 24 | url = "2" 25 | -------------------------------------------------------------------------------- /oxide-auth-db/examples/db-example/src/support.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | #[path = "../../../../examples/support/generic.rs"] 3 | mod generic; 4 | 5 | use std::collections::HashMap; 6 | 7 | pub use self::generic::{consent_page_html, open_in_browser, Client, ClientConfig, ClientError}; 8 | 9 | use actix_web::App; 10 | use actix_web::*; 11 | 12 | pub fn dummy_client() -> dev::Server { 13 | HttpServer::new(move || { 14 | let config = ClientConfig { 15 | client_id: "LocalClient".into(), 16 | client_secret: Option::from("test".to_string()), 17 | protected_url: "http://localhost:8020/".into(), 18 | token_url: "http://localhost:8020/token".into(), 19 | refresh_url: "http://localhost:8020/refresh".into(), 20 | redirect_uri: "http://localhost:8021/endpoint".into(), 21 | }; 22 | 23 | App::new() 24 | .app_data(Client::new(config)) 25 | .route("/endpoint", web::get().to(endpoint_impl)) 26 | .route("/refresh", web::post().to(refresh)) 27 | .route("/", web::get().to(get_with_token)) 28 | }) 29 | .bind("localhost:8021") 30 | .expect("Failed to start dummy client") 31 | .run() 32 | } 33 | 34 | async fn endpoint_impl( 35 | (query, state): (web::Query>, web::Data), 36 | ) -> HttpResponse { 37 | if let Some(cause) = query.get("error") { 38 | return HttpResponse::BadRequest() 39 | .body(format!("Error during owner authorization: {:?}", cause)); 40 | } 41 | 42 | let code = match query.get("code") { 43 | None => return HttpResponse::BadRequest().body("Missing code"), 44 | Some(code) => code.clone(), 45 | }; 46 | 47 | match state.authorize(&code) { 48 | Ok(()) => HttpResponse::Found().append_header(("Location", "/")).finish(), 49 | Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)), 50 | } 51 | } 52 | 53 | async fn refresh(state: web::Data) -> HttpResponse { 54 | match state.refresh() { 55 | Ok(()) => HttpResponse::Found().append_header(("Location", "/")).finish(), 56 | Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)), 57 | } 58 | } 59 | 60 | async fn get_with_token(state: web::Data) -> HttpResponse { 61 | let protected_page = match state.retrieve_protected_page() { 62 | Ok(page) => page, 63 | Err(err) => return HttpResponse::InternalServerError().body(format!("{}", err)), 64 | }; 65 | 66 | let display_page = format!( 67 | " 72 |
73 | Used token to access 74 | http://localhost:8020/. 75 | Its contents are: 76 |
{}
77 |
78 |
", state.as_html(), protected_page); 79 | 80 | HttpResponse::Ok().content_type("text/html").body(display_page) 81 | } 82 | -------------------------------------------------------------------------------- /oxide-auth-db/src/db_service/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-redis")] 2 | pub mod redis; 3 | 4 | #[cfg(feature = "with-redis")] 5 | use redis::RedisDataSource; 6 | 7 | #[cfg(feature = "with-redis")] 8 | 9 | /// A datasource service to restore clients; 10 | /// users can change to another database, mysql or postgresql .etc. and add corresponding implements. 11 | /// for example: pub type DataSource = MysqslDataSource; 12 | pub type DataSource = RedisDataSource; 13 | -------------------------------------------------------------------------------- /oxide-auth-db/src/db_service/redis.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::db_registrar::OauthClientDBRepository; 2 | 3 | use oxide_auth::primitives::prelude::Scope; 4 | use oxide_auth::primitives::registrar::{ClientType, EncodedClient, RegisteredUrl, ExactUrl}; 5 | 6 | use r2d2_redis::r2d2::Pool; 7 | use r2d2_redis::redis::{Commands, RedisError, ErrorKind}; 8 | use r2d2_redis::RedisConnectionManager; 9 | use std::str::FromStr; 10 | use serde::{Serialize, Deserialize}; 11 | use url::Url; 12 | 13 | // // TODO 参数化 14 | // pub const CLIENT_PREFIX: &str = "client:"; 15 | 16 | /// redis datasource to Client entries. 17 | #[derive(Debug, Clone)] 18 | pub struct RedisDataSource { 19 | url: String, 20 | pool: Pool, 21 | client_prefix: String, 22 | } 23 | 24 | /// A client whose credentials have been wrapped by a password policy. 25 | /// 26 | /// This provides a standard encoding for `Registrars` who wish to store their clients and makes it 27 | /// possible to test password policies. 28 | #[derive(Clone, Debug, Serialize, Deserialize)] 29 | pub struct StringfiedEncodedClient { 30 | /// The id of this client. If this is was registered at a `Registrar`, this should be a key 31 | /// to the instance. 32 | pub client_id: String, 33 | 34 | /// The registered redirect uri. 35 | /// Unlike `additional_redirect_uris`, this is registered as the default redirect uri 36 | /// and will be replaced if, for example, no `redirect_uri` is specified in the request parameter. 37 | pub redirect_uri: String, 38 | 39 | /// The redirect uris that can be registered in addition to the `redirect_uri`. 40 | /// If you want to register multiple redirect uris, register them together with `redirect_uri`. 41 | pub additional_redirect_uris: Vec, 42 | 43 | /// The scope the client gets if none was given. 44 | pub default_scope: Option, 45 | 46 | /// client_secret, for authentication. 47 | pub client_secret: Option, 48 | } 49 | 50 | impl StringfiedEncodedClient { 51 | pub fn to_encoded_client(&self) -> anyhow::Result { 52 | let redirect_uri = RegisteredUrl::from(ExactUrl::from_str(&self.redirect_uri)?); 53 | let uris = &self.additional_redirect_uris; 54 | let additional_redirect_uris = uris.iter().fold(vec![], |mut us, u| { 55 | us.push(RegisteredUrl::from(ExactUrl::from_str(u).unwrap())); 56 | us 57 | }); 58 | 59 | let client_type = match &self.client_secret { 60 | None => ClientType::Public, 61 | Some(secret) => ClientType::Confidential { 62 | passdata: secret.to_owned().into_bytes(), 63 | }, 64 | }; 65 | 66 | Ok(EncodedClient { 67 | client_id: (&self.client_id).parse().unwrap(), 68 | redirect_uri, 69 | additional_redirect_uris, 70 | default_scope: Scope::from_str( 71 | self.default_scope.as_ref().unwrap_or(&"".to_string()).as_ref(), 72 | ) 73 | .unwrap(), 74 | encoded_client: client_type, 75 | }) 76 | } 77 | 78 | pub fn from_encoded_client(encoded_client: &EncodedClient) -> Self { 79 | let additional_redirect_uris = encoded_client 80 | .additional_redirect_uris 81 | .iter() 82 | .map(|u| u.to_owned().as_str().parse().unwrap()) 83 | .collect(); 84 | let default_scope = Some(encoded_client.default_scope.to_string()); 85 | let client_secret = match &encoded_client.encoded_client { 86 | ClientType::Public => None, 87 | ClientType::Confidential { passdata } => Some(String::from_utf8(passdata.to_vec()).unwrap()), 88 | }; 89 | StringfiedEncodedClient { 90 | client_id: encoded_client.client_id.to_owned(), 91 | redirect_uri: encoded_client.redirect_uri.to_owned().as_str().parse().unwrap(), 92 | additional_redirect_uris, 93 | default_scope, 94 | client_secret, 95 | } 96 | } 97 | } 98 | 99 | impl RedisDataSource { 100 | pub fn new(url: String, max_pool_size: u32, client_prefix: String) -> Result { 101 | let manager = r2d2_redis::RedisConnectionManager::new(url.as_str())?; 102 | let pool = Pool::builder().max_size(max_pool_size).build(manager); 103 | match pool { 104 | Ok(pool) => Ok(RedisDataSource { 105 | url, 106 | pool, 107 | client_prefix, 108 | }), 109 | Err(_e) => Err(RedisError::from((ErrorKind::ClientError, "Build pool error."))), 110 | } 111 | } 112 | 113 | pub fn new_with_url( 114 | url: Url, max_pool_size: u32, client_prefix: String, 115 | ) -> Result { 116 | RedisDataSource::new(url.into(), max_pool_size, client_prefix) 117 | } 118 | 119 | pub fn get_url(&self) -> String { 120 | self.url.clone() 121 | } 122 | pub fn get_pool(&self) -> Pool { 123 | self.pool.clone() 124 | } 125 | } 126 | 127 | impl RedisDataSource { 128 | /// users can regist to redis a custom client struct which can be Serialized and Deserialized. 129 | pub fn regist(&self, detail: &StringfiedEncodedClient) -> anyhow::Result<()> { 130 | let mut pool = self.pool.get()?; 131 | let client_str = serde_json::to_string(&detail)?; 132 | pool.set(&(self.client_prefix.to_owned() + &detail.client_id), client_str)?; 133 | Ok(()) 134 | } 135 | } 136 | 137 | impl OauthClientDBRepository for RedisDataSource { 138 | fn list(&self) -> anyhow::Result> { 139 | let mut encoded_clients: Vec = vec![]; 140 | let mut r = self.pool.get()?; 141 | let keys = r.keys::<&str, Vec>(&self.client_prefix)?; 142 | for key in keys { 143 | let clients_str = r.get::(key)?; 144 | let stringfied_client = serde_json::from_str::(&clients_str)?; 145 | encoded_clients.push(stringfied_client.to_encoded_client()?); 146 | } 147 | Ok(encoded_clients) 148 | } 149 | 150 | fn find_client_by_id(&self, id: &str) -> anyhow::Result { 151 | let mut r = self.pool.get()?; 152 | let client_str = r.get::<&str, String>(&(self.client_prefix.to_owned() + id))?; 153 | let stringfied_client = serde_json::from_str::(&client_str)?; 154 | Ok(stringfied_client.to_encoded_client()?) 155 | } 156 | 157 | fn regist_from_encoded_client(&self, client: EncodedClient) -> anyhow::Result<()> { 158 | let detail = StringfiedEncodedClient::from_encoded_client(&client); 159 | self.regist(&detail) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /oxide-auth-db/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod db_service; 2 | pub mod primitives; 3 | 4 | #[cfg(test)] 5 | fn requires_redis_and_should_skip() -> bool { 6 | match std::env::var("OXIDE_AUTH_SKIP_REDIS") { 7 | Err(_) => false, 8 | Ok(st) => match st.as_str() { 9 | "1" | "yes" => true, 10 | _ => false, 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /oxide-auth-db/src/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_registrar; 2 | -------------------------------------------------------------------------------- /oxide-auth-iron/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-iron" 3 | version = "0.2.0" 4 | authors = ["Andreas Molzer "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | 7 | description = "A OAuth2 server library for iron featuring a set of configurable and pluggable backends." 8 | readme = "Readme.md" 9 | keywords = ["oauth", "server", "oauth2"] 10 | categories = ["web-programming::http-server", "authentication"] 11 | license = "MIT OR Apache-2.0" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | iron = "0.6" 16 | oxide-auth = { version = "0.6.0", path = "../oxide-auth" } 17 | serde_urlencoded = "0.7" 18 | url = "2" 19 | 20 | [dev-dependencies] 21 | reqwest = { version = "0.11.10", features = ["blocking"] } 22 | router = "0.6.0" 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = "1.0" 25 | -------------------------------------------------------------------------------- /oxide-auth-iron/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-iron 2 | 3 | Integrates `oxide-auth` with the [`iron`] web server library. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-iron.svg)](https://crates.io/crates/oxide-auth-iron) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-iron/badge.svg)](https://docs.rs/oxide-auth-iron/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [`iron`]: https://crates.io/crates/iron 19 | [LICENSE-MIT]: docs/LICENSE-MIT 20 | [LICENSE-APACHE]: docs/LICENSE-APACHE 21 | -------------------------------------------------------------------------------- /oxide-auth-iron/examples/iron.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate oxide_auth; 3 | extern crate oxide_auth_iron; 4 | extern crate router; 5 | 6 | use std::sync::{Arc, Mutex}; 7 | use std::thread::spawn; 8 | 9 | use iron::{Iron, Request, Response}; 10 | use iron::headers::ContentType; 11 | use iron::status::Status; 12 | use iron::middleware::Handler; 13 | 14 | use oxide_auth::endpoint::{OwnerConsent, Solicitation}; 15 | use oxide_auth::frontends::simple::endpoint::{FnSolicitor, Generic, Vacant}; 16 | use oxide_auth::primitives::prelude::*; 17 | use oxide_auth_iron::{OAuthRequest, OAuthResponse, OAuthError}; 18 | 19 | #[rustfmt::skip] 20 | #[path = "../../examples/support/iron.rs"] 21 | mod support; 22 | 23 | struct EndpointState { 24 | registrar: Mutex, 25 | authorizer: Mutex>, 26 | issuer: Mutex, 27 | } 28 | 29 | fn main_router() -> impl Handler + 'static { 30 | let state = Arc::new(EndpointState::preconfigured()); 31 | 32 | // One clone for each of the move-closures below. 33 | let (auth_get_state, auth_post_state, token_state, get_state) = 34 | (state.clone(), state.clone(), state.clone(), state.clone()); 35 | let mut router = router::Router::new(); 36 | router.get( 37 | "/authorize", 38 | move |request: &mut Request| { 39 | let state = auth_get_state.clone(); 40 | let response = state 41 | .endpoint() 42 | .with_solicitor(FnSolicitor(consent_form)) 43 | .authorization_flow() 44 | .execute(request.into()) 45 | .map_err(|e| { 46 | let e: OAuthError = e.into(); 47 | e.into() 48 | })?; 49 | Ok(response.into()) 50 | }, 51 | "authorization_get", 52 | ); 53 | router.post( 54 | "/authorize", 55 | move |request: &mut Request| { 56 | let state = auth_post_state.clone(); 57 | let response = state 58 | .endpoint() 59 | .with_solicitor(FnSolicitor(consent_decision)) 60 | .authorization_flow() 61 | .execute(request.into()) 62 | .map_err(|e| { 63 | let e: OAuthError = e.into(); 64 | e.into() 65 | })?; 66 | Ok(response.into()) 67 | }, 68 | "authorization_post", 69 | ); 70 | router.post( 71 | "/token", 72 | move |request: &mut Request| { 73 | let state = token_state.clone(); 74 | let response = state 75 | .endpoint() 76 | .access_token_flow() 77 | .execute(request.into()) 78 | .map_err(|e| { 79 | let e: OAuthError = e.into(); 80 | e.into() 81 | })?; 82 | Ok(response.into()) 83 | }, 84 | "token", 85 | ); 86 | router.get( 87 | "/", 88 | move |request: &mut Request| { 89 | let oauth_request: OAuthRequest = request.into(); 90 | 91 | let state = get_state.clone(); 92 | let protect = state 93 | .endpoint() 94 | .with_scopes(vec!["default-scope".parse().unwrap()]) 95 | .resource_flow() 96 | .execute(oauth_request); 97 | 98 | let _grant = match protect { 99 | Ok(grant) => grant, 100 | Err(Ok(mut response)) => { 101 | response.set_header(ContentType::html()); 102 | response.set_body(EndpointState::DENY_TEXT); 103 | return Ok(response.into()); 104 | } 105 | Err(Err(error)) => { 106 | let error: OAuthError = error.into(); 107 | return Err(error.into()); 108 | } 109 | }; 110 | 111 | Ok(Response::with((Status::Ok, "Hello, world!"))) 112 | }, 113 | "protected", 114 | ); 115 | 116 | router 117 | } 118 | 119 | fn main() { 120 | let server = spawn(|| { 121 | Iron::new(main_router()) 122 | .http("127.0.0.1:8020") 123 | .expect("Failed to launch authorization server"); 124 | }); 125 | 126 | let client = spawn(|| { 127 | Iron::new(support::dummy_client()) 128 | .http("127.0.0.1:8021") 129 | .expect("Failed to launch client"); 130 | }); 131 | 132 | support::open_in_browser(8020); 133 | 134 | server.join().unwrap(); 135 | client.join().unwrap(); 136 | } 137 | 138 | impl EndpointState { 139 | const DENY_TEXT: &'static str = " 140 | This page should be accessed via an oauth token from the client in the example. Click 141 | 142 | here to begin the authorization process. 143 | 144 | "; 145 | 146 | fn preconfigured() -> Self { 147 | EndpointState { 148 | registrar: Mutex::new( 149 | vec![Client::public( 150 | "LocalClient", 151 | "http://localhost:8021/endpoint" 152 | .parse::() 153 | .unwrap() 154 | .into(), 155 | "default-scope".parse().unwrap(), 156 | )] 157 | .into_iter() 158 | .collect(), 159 | ), 160 | authorizer: Mutex::new(AuthMap::new(RandomGenerator::new(16))), 161 | issuer: Mutex::new(TokenSigner::ephemeral()), 162 | } 163 | } 164 | 165 | /// In larger app, you'd likey wrap it in your own Endpoint instead of `Generic`. 166 | pub fn endpoint( 167 | &self, 168 | ) -> Generic< 169 | impl Registrar + '_, 170 | impl Authorizer + '_, 171 | impl Issuer + '_, 172 | Vacant, 173 | Vacant, 174 | fn() -> OAuthResponse, 175 | > { 176 | Generic { 177 | registrar: self.registrar.lock().unwrap(), 178 | authorizer: self.authorizer.lock().unwrap(), 179 | issuer: self.issuer.lock().unwrap(), 180 | // Solicitor configured later. 181 | solicitor: Vacant, 182 | // Scope configured later. 183 | scopes: Vacant, 184 | // `iron::Response` is not `Default`, so we choose a constructor. 185 | response: OAuthResponse::new, 186 | } 187 | } 188 | } 189 | 190 | fn consent_form(_: &mut OAuthRequest, solication: Solicitation) -> OwnerConsent { 191 | let mut response = OAuthResponse::new(); 192 | response.set_status(Status::Ok); 193 | response.set_header(ContentType::html()); 194 | response.set_body(&support::consent_page_html("/authorize", solication)); 195 | OwnerConsent::InProgress(response) 196 | } 197 | 198 | fn consent_decision(request: &mut OAuthRequest, _: Solicitation) -> OwnerConsent { 199 | // Authenticate the request better in a real app! 200 | let allowed = request.url().query_pairs().any(|(key, _)| key == "allow"); 201 | if allowed { 202 | OwnerConsent::Authorized("dummy user".into()) 203 | } else { 204 | OwnerConsent::Denied 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /oxide-auth-poem/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-poem" 3 | version = "0.3.0" 4 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 5 | authors = ["l1npengtul "] 6 | description = "A OAuth2 server library for Poem featuring a set of configurable and pluggable backends." 7 | readme = "Readme.md" 8 | keywords = ["oauth", "server", "oauth2", "poem"] 9 | categories = ["web-programming::http-server", "authentication"] 10 | license = "MIT OR Apache-2.0" 11 | edition = "2021" 12 | 13 | [features] 14 | default = [] 15 | 16 | [dependencies] 17 | poem = "3.1" 18 | oxide-auth = { version = "0.6", path = "../oxide-auth" } 19 | thiserror = "2.0" 20 | 21 | [dev-dependencies] 22 | reqwest = { version = "0.12", features = ["blocking"] } 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_urlencoded = "0.7" 25 | serde_json = "1.0" 26 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 27 | url = "2" 28 | -------------------------------------------------------------------------------- /oxide-auth-poem/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-poem 2 | 3 | Integrates `oxide-auth` with the [`poem`] web server library. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-poem.svg)](https://crates.io/crates/oxide-auth-poem) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-iron/badge.svg)](https://docs.rs/oxide-auth-iron/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [`poem`]: https://crates.io/crates/poem 19 | [LICENSE-MIT]: docs/LICENSE-MIT 20 | [LICENSE-APACHE]: docs/LICENSE-APACHE 21 | -------------------------------------------------------------------------------- /oxide-auth-poem/examples/poem-example/support.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc}; 2 | use poem::{get, handler, post, EndpointExt, IntoResponse, Route}; 3 | use poem::http::StatusCode; 4 | use poem::web::{Data, Query}; 5 | use serde::Deserialize; 6 | 7 | #[path = "../../../examples/support/generic.rs"] 8 | mod generic; 9 | 10 | pub use self::generic::{consent_page_html, open_in_browser}; 11 | 12 | use self::generic::{Client, ClientConfig}; 13 | 14 | #[derive(Deserialize)] 15 | struct EndpointQuery { 16 | error: Option, 17 | code: Option, 18 | } 19 | 20 | #[handler] 21 | async fn endpoint_handler( 22 | Query(EndpointQuery { error, code }): Query, client: Data<&Arc>, 23 | ) -> poem::Result { 24 | if let Some(error) = error { 25 | let message = format!("Error during authorization: {}", error); 26 | return Ok((StatusCode::OK, message).into()); 27 | } 28 | 29 | let code = code.ok_or_else(|| poem::Error::from_string("Missing code", StatusCode::BAD_REQUEST))?; 30 | 31 | let client = client.clone(); 32 | 33 | let auth_handle = tokio::task::spawn_blocking(move || client.authorize(&code)); 34 | 35 | auth_handle 36 | .await 37 | .unwrap() 38 | .map_err(|e| poem::Error::from_string(e.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; 39 | 40 | Ok(poem::web::Redirect::see_other("/").into_response()) 41 | } 42 | 43 | #[handler] 44 | async fn refresh_handler(client: Data<&Arc>) -> poem::Result { 45 | let client = client.clone(); 46 | 47 | let refresh_handle = tokio::task::spawn_blocking(move || client.refresh()); 48 | 49 | refresh_handle 50 | .await 51 | .unwrap() 52 | .map_err(|e| poem::Error::from_string(e.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; 53 | 54 | Ok(poem::web::Redirect::see_other("/").into_response()) 55 | } 56 | 57 | #[handler] 58 | async fn index_handler(client: Data<&Arc>) -> poem::Result { 59 | let html = client.as_html(); 60 | 61 | let protected_client = client.clone(); 62 | 63 | let protected_page_handle = 64 | tokio::task::spawn_blocking(move || protected_client.retrieve_protected_page()); 65 | let protected_page_result = protected_page_handle.await.unwrap(); 66 | 67 | let protected_page = protected_page_result 68 | .map_err(|err| poem::Error::from_string(err.to_string(), StatusCode::OK))?; 69 | 70 | let protected_url = client.config.protected_url.as_str(); 71 | 72 | let display_page = format!( 73 | " 78 |
79 | Used token to access 80 | {protected_url}. 81 | Its contents are: 82 |
{protected_page}
83 |
84 |
"); 85 | 86 | Ok(poem::Response::builder() 87 | .content_type("text/html") 88 | .body(display_page)) 89 | } 90 | 91 | pub fn dummy_client_routes(client_port: u16, server_port: u16) -> impl poem::Endpoint { 92 | let client = Arc::new(Client::new(ClientConfig { 93 | client_id: "LocalClient".into(), 94 | client_secret: Some("SecretSecret".to_owned()), 95 | protected_url: format!("http://localhost:{server_port}/"), 96 | token_url: format!("http://localhost:{server_port}/token"), 97 | refresh_url: format!("http://localhost:{server_port}/token"), 98 | redirect_uri: format!("http://localhost:{client_port}/endpoint"), 99 | })); 100 | 101 | Route::new() 102 | .at("/endpoint", get(endpoint_handler)) 103 | .at("/refresh", post(refresh_handler)) 104 | .at("/", get(index_handler)) 105 | .data(client) 106 | } 107 | -------------------------------------------------------------------------------- /oxide-auth-poem/src/error.rs: -------------------------------------------------------------------------------- 1 | use super::request::OAuthRequest; 2 | use oxide_auth::endpoint::OAuthError as EndpointError; 3 | use oxide_auth::frontends::simple::endpoint::Error as SimpleError; 4 | use poem::error::{BadRequest, InternalServerError, Unauthorized}; 5 | use poem::http::StatusCode; 6 | use thiserror::Error; 7 | 8 | /// Errors that may be generated by this crate. 9 | /// - Request 10 | /// - Authorization 11 | /// - Header 12 | /// - Server 13 | /// - This is the general server error. 14 | #[derive(Clone, Debug, Error)] 15 | pub enum OxidePoemError { 16 | /// This is returned when the client request cannot be properly parsed 17 | #[error("There was a problem with the request")] 18 | Request, 19 | /// This is returned when the client request contains an invalid "Authorization" header 20 | #[error("Invalid Authorization Header")] 21 | Authorization, 22 | /// This is for header parsing related errors (server side) 23 | #[error("Error while parsing header: {0}")] 24 | Header(String), 25 | #[error("There was a problem with the server")] 26 | /// This is the general server error. 27 | Server, 28 | } 29 | 30 | impl From for poem::Error { 31 | fn from(ox_err: OxidePoemError) -> Self { 32 | match &ox_err { 33 | OxidePoemError::Request => BadRequest(ox_err), 34 | OxidePoemError::Authorization => Unauthorized(ox_err), 35 | OxidePoemError::Header(_) | OxidePoemError::Server => InternalServerError(ox_err), 36 | } 37 | } 38 | } 39 | 40 | /// Generic error type produced by Oxide Auth operations that can be coerced into an `poem::Error` 41 | pub struct OAuthError(poem::Error); 42 | 43 | impl From> for OAuthError { 44 | fn from(error: SimpleError) -> Self { 45 | let poem_error = match error { 46 | SimpleError::Web(p) => p.into(), 47 | SimpleError::OAuth(oauth) => { 48 | let status = match oauth { 49 | EndpointError::BadRequest | EndpointError::DenySilently => StatusCode::BAD_REQUEST, 50 | EndpointError::PrimitiveError => StatusCode::INTERNAL_SERVER_ERROR, 51 | }; 52 | 53 | poem::Error::new(oauth, status) 54 | } 55 | }; 56 | 57 | OAuthError(poem_error) 58 | } 59 | } 60 | 61 | impl From for OAuthError { 62 | fn from(e: poem::Error) -> Self { 63 | OAuthError(e) 64 | } 65 | } 66 | 67 | impl From for poem::Error { 68 | fn from(value: OAuthError) -> poem::Error { 69 | value.0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /oxide-auth-poem/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Adaptations and integration for Poem. 2 | #![warn(missing_docs)] 3 | #![deny(clippy::pedantic)] 4 | 5 | /// Things related to Requests (from the client) 6 | pub mod request; 7 | /// Errors for this crate. 8 | pub mod error; 9 | /// Things related to Responses (from the server) 10 | pub mod response; 11 | -------------------------------------------------------------------------------- /oxide-auth-poem/src/request.rs: -------------------------------------------------------------------------------- 1 | use super::{error::OxidePoemError, response::OAuthResponse}; 2 | use oxide_auth::endpoint::{NormalizedParameter, QueryParameter, WebRequest}; 3 | use poem::{error::BadRequest, web::Form, FromRequest, Request, RequestBody}; 4 | use std::borrow::Cow; 5 | 6 | #[derive(Clone, Debug, Default)] 7 | /// Type implementing `WebRequest` as well as `Request` for use in route handlers 8 | /// 9 | /// This type consumes the body of the Request upon extraction, so be careful not to use it in 10 | /// places you also expect an application payload 11 | pub struct OAuthRequest { 12 | auth: Option, 13 | query: Option, 14 | body: Option, 15 | } 16 | 17 | impl OAuthRequest { 18 | /// Fetch the authorization header from the request 19 | #[must_use] 20 | pub fn authorization_header(&self) -> Option<&str> { 21 | self.auth.as_deref() 22 | } 23 | 24 | /// Fetch the query for this request 25 | #[must_use] 26 | pub fn query(&self) -> Option<&NormalizedParameter> { 27 | self.query.as_ref() 28 | } 29 | 30 | /// Fetch the query mutably 31 | pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> { 32 | self.query.as_mut() 33 | } 34 | 35 | /// Fetch the body of the request 36 | #[must_use] 37 | pub fn body(&self) -> Option<&NormalizedParameter> { 38 | self.body.as_ref() 39 | } 40 | } 41 | 42 | impl WebRequest for OAuthRequest { 43 | type Error = OxidePoemError; 44 | type Response = OAuthResponse; 45 | 46 | fn query(&mut self) -> Result, Self::Error> { 47 | self.query 48 | .as_ref() 49 | .map(|q| Cow::Borrowed(q as &dyn QueryParameter)) 50 | .ok_or(OxidePoemError::Request) 51 | } 52 | 53 | fn urlbody(&mut self) -> Result, Self::Error> { 54 | self.body 55 | .as_ref() 56 | .map(|b| Cow::Borrowed(b as &dyn QueryParameter)) 57 | .ok_or(OxidePoemError::Request) 58 | } 59 | 60 | fn authheader(&mut self) -> Result>, Self::Error> { 61 | Ok(self.auth.as_deref().map(Cow::Borrowed)) 62 | } 63 | } 64 | 65 | impl<'a> FromRequest<'a> for OAuthRequest { 66 | async fn from_request(req: &'a Request, body: &mut RequestBody) -> poem::Result { 67 | use poem::web::Query; 68 | 69 | let query = as FromRequest>::from_request(req, body) 70 | .await 71 | .ok() 72 | .map(|f| f.0); 73 | 74 | let body = as FromRequest>::from_request(req, body) 75 | .await 76 | .ok() 77 | .map(|f| f.0); 78 | 79 | let mut all_auth = req.headers().get_all("Authorization").into_iter(); 80 | let optional = all_auth.next(); 81 | 82 | let auth = match all_auth.next() { 83 | Some(_) => return Err(BadRequest(OxidePoemError::Authorization)), 84 | None => optional.and_then(|header| header.to_str().ok().map(str::to_owned)), 85 | }; 86 | 87 | Ok(Self { auth, query, body }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /oxide-auth-poem/src/response.rs: -------------------------------------------------------------------------------- 1 | use super::error::OxidePoemError; 2 | use oxide_auth::{endpoint::WebResponse, frontends::dev::Url}; 3 | use poem::{ 4 | http::{ 5 | header::{InvalidHeaderValue, CONTENT_TYPE, LOCATION, WWW_AUTHENTICATE}, 6 | Extensions, HeaderMap, HeaderValue, StatusCode, Version, 7 | }, 8 | Body, IntoResponse, Response, ResponseParts, 9 | }; 10 | 11 | #[derive(Default, Clone, Debug)] 12 | /// Type implementing `WebResponse` and `IntoResponse` for use in route handlers 13 | pub struct OAuthResponse { 14 | status: StatusCode, 15 | headers: HeaderMap, 16 | body: Option, 17 | } 18 | 19 | impl OAuthResponse { 20 | /// Set the `ContentType` header on a response 21 | /// # Errors 22 | /// In case the `content_type` cannot be parsed, this will return an [`OxidePoemError::Header(_)`] 23 | pub fn content_type(mut self, content_type: &str) -> Result { 24 | // the explicit typedef is probably unnecessary but my IDE is giving me errors otherwise so /shrug/ 25 | self.headers.insert( 26 | CONTENT_TYPE, 27 | content_type 28 | .parse() 29 | .map_err(|err: InvalidHeaderValue| OxidePoemError::Header(err.to_string()))?, 30 | ); 31 | Ok(self) 32 | } 33 | 34 | /// Set the body for the response 35 | #[must_use] 36 | pub fn body(mut self, body: &str) -> Self { 37 | self.body = Some(body.to_owned()); 38 | self 39 | } 40 | } 41 | 42 | impl WebResponse for OAuthResponse { 43 | type Error = OxidePoemError; 44 | 45 | fn ok(&mut self) -> Result<(), Self::Error> { 46 | self.status = StatusCode::OK; 47 | Ok(()) 48 | } 49 | 50 | fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { 51 | self.status = StatusCode::FOUND; 52 | self.headers.insert( 53 | LOCATION, 54 | HeaderValue::from_str(url.as_str()) 55 | .map_err(|header_err| OxidePoemError::Header(header_err.to_string()))?, // This is an `Infallible` type! 56 | ); 57 | Ok(()) 58 | } 59 | 60 | fn client_error(&mut self) -> Result<(), Self::Error> { 61 | self.status = StatusCode::BAD_REQUEST; 62 | Ok(()) 63 | } 64 | 65 | fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> { 66 | self.status = StatusCode::UNAUTHORIZED; 67 | self.headers.insert( 68 | WWW_AUTHENTICATE, 69 | header_value 70 | .parse() 71 | .map_err(|err: InvalidHeaderValue| OxidePoemError::Header(err.to_string()))?, 72 | ); 73 | Ok(()) 74 | } 75 | 76 | fn body_text(&mut self, text: &str) -> Result<(), Self::Error> { 77 | self.body = Some(text.to_owned()); 78 | self.headers 79 | .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); 80 | Ok(()) 81 | } 82 | 83 | fn body_json(&mut self, json: &str) -> Result<(), Self::Error> { 84 | self.body = Some(json.to_owned()); 85 | self.headers 86 | .insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 87 | Ok(()) 88 | } 89 | } 90 | 91 | impl IntoResponse for OAuthResponse { 92 | fn into_response(self) -> Response { 93 | Response::from_parts( 94 | ResponseParts { 95 | status: self.status, 96 | version: Version::default(), 97 | headers: self.headers, 98 | extensions: Extensions::default(), 99 | }, 100 | match self.body { 101 | Some(content) => Body::from(content), 102 | None => Body::empty(), 103 | }, 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /oxide-auth-rocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-rocket" 3 | version = "0.2.0" 4 | authors = ["Andreas Molzer "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | 7 | description = "A OAuth2 server library for rocket featuring a set of configurable and pluggable backends." 8 | readme = "Readme.md" 9 | keywords = ["oauth", "server", "oauth2"] 10 | categories = ["web-programming::http-server", "authentication"] 11 | license = "MIT OR Apache-2.0" 12 | edition = "2018" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | rocket = "0.4.2" 18 | oxide-auth = { version = "0.6.0", path = "../oxide-auth" } 19 | serde_urlencoded = "0.7" 20 | 21 | [dev-dependencies] 22 | reqwest = { version = "0.11.10", features = ["blocking"] } 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = "1.0" 25 | -------------------------------------------------------------------------------- /oxide-auth-rocket/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-rocket 2 | 3 | Integrates `oxide-auth` with the [`rocket`] web server library. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-rocket.svg)](https://crates.io/crates/oxide-auth-rocket) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-rocket/badge.svg)](https://docs.rs/oxide-auth-rocket/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [`rocket`]: https://crates.io/crates/rocket 19 | [LICENSE-MIT]: docs/LICENSE-MIT 20 | [LICENSE-APACHE]: docs/LICENSE-APACHE 21 | -------------------------------------------------------------------------------- /oxide-auth-rocket/examples/rocket.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | 3 | extern crate oxide_auth; 4 | extern crate oxide_auth_rocket; 5 | #[macro_use] 6 | extern crate rocket; 7 | 8 | #[rustfmt::skip] 9 | #[path = "../../examples/support/rocket.rs"] 10 | mod support; 11 | 12 | use std::io; 13 | use std::sync::Mutex; 14 | 15 | use oxide_auth::endpoint::{OwnerConsent, Solicitation}; 16 | use oxide_auth::frontends::simple::endpoint::{FnSolicitor, Generic, Vacant}; 17 | use oxide_auth::primitives::prelude::*; 18 | use oxide_auth::primitives::registrar::RegisteredUrl; 19 | use oxide_auth_rocket::{OAuthResponse, OAuthRequest, OAuthFailure}; 20 | 21 | use rocket::{Data, State, Response, http}; 22 | use rocket::http::ContentType; 23 | use rocket::response::Responder; 24 | 25 | struct MyState { 26 | registrar: Mutex, 27 | authorizer: Mutex>, 28 | issuer: Mutex>, 29 | } 30 | 31 | #[get("/authorize")] 32 | fn authorize<'r>( 33 | oauth: OAuthRequest<'r>, state: State, 34 | ) -> Result, OAuthFailure> { 35 | state 36 | .endpoint() 37 | .with_solicitor(FnSolicitor(consent_form)) 38 | .authorization_flow() 39 | .execute(oauth) 40 | .map_err(|err| err.pack::()) 41 | } 42 | 43 | #[post("/authorize?")] 44 | fn authorize_consent<'r>( 45 | oauth: OAuthRequest<'r>, allow: Option, state: State, 46 | ) -> Result, OAuthFailure> { 47 | let allowed = allow.unwrap_or(false); 48 | state 49 | .endpoint() 50 | .with_solicitor(FnSolicitor(move |_: &mut _, grant: Solicitation<'_>| { 51 | consent_decision(allowed, grant) 52 | })) 53 | .authorization_flow() 54 | .execute(oauth) 55 | .map_err(|err| err.pack::()) 56 | } 57 | 58 | #[post("/token", data = "")] 59 | fn token<'r>( 60 | mut oauth: OAuthRequest<'r>, body: Data, state: State, 61 | ) -> Result, OAuthFailure> { 62 | oauth.add_body(body); 63 | state 64 | .endpoint() 65 | .access_token_flow() 66 | .execute(oauth) 67 | .map_err(|err| err.pack::()) 68 | } 69 | 70 | #[post("/refresh", data = "")] 71 | fn refresh<'r>( 72 | mut oauth: OAuthRequest<'r>, body: Data, state: State, 73 | ) -> Result, OAuthFailure> { 74 | oauth.add_body(body); 75 | state 76 | .endpoint() 77 | .refresh_flow() 78 | .execute(oauth) 79 | .map_err(|err| err.pack::()) 80 | } 81 | 82 | #[get("/")] 83 | fn protected_resource<'r>(oauth: OAuthRequest<'r>, state: State) -> impl Responder<'r> { 84 | const DENY_TEXT: &str = " 85 | This page should be accessed via an oauth token from the client in the example. Click 86 | 87 | here to begin the authorization process. 88 | 89 | "; 90 | 91 | let protect = state 92 | .endpoint() 93 | .with_scopes(vec!["default-scope".parse().unwrap()]) 94 | .resource_flow() 95 | .execute(oauth); 96 | match protect { 97 | Ok(_grant) => Ok("Hello, world"), 98 | Err(Ok(response)) => { 99 | let error: OAuthResponse = Response::build_from(response.into()) 100 | .header(ContentType::HTML) 101 | .sized_body(io::Cursor::new(DENY_TEXT)) 102 | .finalize() 103 | .into(); 104 | Err(Ok(error)) 105 | } 106 | Err(Err(err)) => Err(Err(err.pack::())), 107 | } 108 | } 109 | 110 | fn main() { 111 | rocket::ignite() 112 | .mount( 113 | "/", 114 | routes![authorize, authorize_consent, token, protected_resource, refresh,], 115 | ) 116 | // We only attach the test client here because there can only be one rocket. 117 | .attach(support::ClientFairing) 118 | .manage(MyState::preconfigured()) 119 | .launch(); 120 | } 121 | 122 | impl MyState { 123 | pub fn preconfigured() -> Self { 124 | MyState { 125 | registrar: Mutex::new( 126 | vec![Client::public( 127 | "LocalClient", 128 | RegisteredUrl::Semantic( 129 | "http://localhost:8000/clientside/endpoint".parse().unwrap(), 130 | ), 131 | "default-scope".parse().unwrap(), 132 | )] 133 | .into_iter() 134 | .collect(), 135 | ), 136 | // Authorization tokens are 16 byte random keys to a memory hash map. 137 | authorizer: Mutex::new(AuthMap::new(RandomGenerator::new(16))), 138 | // Bearer tokens are also random generated but 256-bit tokens, since they live longer 139 | // and this example is somewhat paranoid. 140 | // 141 | // We could also use a `TokenSigner::ephemeral` here to create signed tokens which can 142 | // be read and parsed by anyone, but not maliciously created. However, they can not be 143 | // revoked and thus don't offer even longer lived refresh tokens. 144 | issuer: Mutex::new(TokenMap::new(RandomGenerator::new(16))), 145 | } 146 | } 147 | 148 | pub fn endpoint(&self) -> Generic { 149 | Generic { 150 | registrar: self.registrar.lock().unwrap(), 151 | authorizer: self.authorizer.lock().unwrap(), 152 | issuer: self.issuer.lock().unwrap(), 153 | // Solicitor configured later. 154 | solicitor: Vacant, 155 | // Scope configured later. 156 | scopes: Vacant, 157 | // `rocket::Response` is `Default`, so we don't need more configuration. 158 | response: Vacant, 159 | } 160 | } 161 | } 162 | 163 | fn consent_form<'r>( 164 | _: &mut OAuthRequest<'r>, solicitation: Solicitation, 165 | ) -> OwnerConsent> { 166 | OwnerConsent::InProgress( 167 | Response::build() 168 | .status(http::Status::Ok) 169 | .header(http::ContentType::HTML) 170 | .sized_body(io::Cursor::new(support::consent_page_html( 171 | "/authorize", 172 | solicitation, 173 | ))) 174 | .finalize() 175 | .into(), 176 | ) 177 | } 178 | 179 | fn consent_decision<'r>(allowed: bool, _: Solicitation) -> OwnerConsent> { 180 | if allowed { 181 | OwnerConsent::Authorized("dummy user".into()) 182 | } else { 183 | OwnerConsent::Denied 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /oxide-auth-rocket/src/failure.rs: -------------------------------------------------------------------------------- 1 | use super::WebError; 2 | use oxide_auth::endpoint::OAuthError; 3 | 4 | use rocket::Request; 5 | use rocket::http::Status; 6 | use rocket::response::{Responder, Result}; 7 | use self::OAuthError::*; 8 | use self::Kind::*; 9 | 10 | /// Failed handling of an oauth request, providing a response. 11 | /// 12 | /// The error responses generated by this type are *not* part of the stable interface. To create 13 | /// stable error pages or to build more meaningful errors, either destructure this using the 14 | /// `oauth` and `web` method or avoid turning errors into this type by providing a custom error 15 | /// representation. 16 | #[derive(Clone, Debug)] 17 | pub struct OAuthFailure { 18 | inner: Kind, 19 | } 20 | 21 | impl OAuthFailure { 22 | /// Get the `OAuthError` causing this failure. 23 | pub fn oauth(&self) -> Option { 24 | match &self.inner { 25 | OAuth(err) => Some(*err), 26 | _ => None, 27 | } 28 | } 29 | 30 | /// Get the `WebError` causing this failure. 31 | pub fn web(&self) -> Option { 32 | match &self.inner { 33 | Web(err) => Some(*err), 34 | _ => None, 35 | } 36 | } 37 | } 38 | 39 | #[derive(Clone, Debug)] 40 | enum Kind { 41 | Web(WebError), 42 | OAuth(OAuthError), 43 | } 44 | 45 | impl<'r> Responder<'r> for OAuthFailure { 46 | fn respond_to(self, _: &Request) -> Result<'r> { 47 | match self.inner { 48 | Web(_) | OAuth(DenySilently) | OAuth(BadRequest) => Err(Status::BadRequest), 49 | OAuth(PrimitiveError) => Err(Status::InternalServerError), 50 | } 51 | } 52 | } 53 | 54 | impl From for OAuthFailure { 55 | fn from(err: OAuthError) -> Self { 56 | OAuthFailure { inner: OAuth(err) } 57 | } 58 | } 59 | 60 | impl From for OAuthFailure { 61 | fn from(err: WebError) -> Self { 62 | OAuthFailure { inner: Web(err) } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /oxide-auth-rouille/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth-rouille" 3 | version = "0.2.0" 4 | authors = ["Andreas Molzer "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | edition = "2018" 7 | 8 | description = "Combines oxide-auth with a rouille web server" 9 | readme = "Readme.md" 10 | keywords = ["oauth", "server", "oauth2"] 11 | categories = ["web-programming::http-server", "authentication"] 12 | license = "MIT OR Apache-2.0" 13 | 14 | [dependencies] 15 | rouille = "3.0" 16 | oxide-auth = { version = "0.6.0", path = "../oxide-auth" } 17 | serde_urlencoded = "0.7" 18 | url = "2" 19 | 20 | [dev-dependencies] 21 | reqwest = { version = "0.11.10", features = ["blocking"] } 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0" 24 | -------------------------------------------------------------------------------- /oxide-auth-rouille/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth-rouille 2 | 3 | Integrates `oxide-auth` with the [`rouille`] web server library. 4 | 5 | ## Additional 6 | 7 | [![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-rouille.svg)](https://crates.io/crates/oxide-auth-rouille) 8 | [![Docs.rs Status](https://docs.rs/oxide-auth-rouille/badge.svg)](https://docs.rs/oxide-auth-rouille/) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) 10 | [![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) 11 | [![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) 12 | 13 | Licensed under either of 14 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 15 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 16 | at your option. 17 | 18 | [`rouille`]: https://crates.io/crates/rouille 19 | [LICENSE-MIT]: docs/LICENSE-MIT 20 | [LICENSE-APACHE]: docs/LICENSE-APACHE 21 | -------------------------------------------------------------------------------- /oxide-auth-rouille/examples/rouille.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rouille; 3 | 4 | #[rustfmt::skip] 5 | #[path = "../../examples/support/rouille.rs"] 6 | mod support; 7 | 8 | use std::sync::Mutex; 9 | use std::thread; 10 | 11 | use oxide_auth::endpoint::{ 12 | AuthorizationFlow, AccessTokenFlow, OwnerConsent, RefreshFlow, ResourceFlow, Solicitation, 13 | }; 14 | use oxide_auth::primitives::prelude::*; 15 | use oxide_auth_rouille::{Request, Response as OAuthResponse}; 16 | use oxide_auth_rouille::{FnSolicitor, GenericEndpoint}; 17 | use rouille::{Response, ResponseBody, Server}; 18 | 19 | /// Example of a main function of a rouille server supporting oauth. 20 | pub fn main() { 21 | // Stores clients in a simple in-memory hash map. 22 | let registrar = { 23 | let mut clients = ClientMap::new(); 24 | // Register a dummy client instance 25 | let client = Client::public( 26 | "LocalClient", // Client id 27 | "http://localhost:8021/endpoint" 28 | .parse::() 29 | .unwrap() 30 | .into(), // Redirection url 31 | "default".parse().unwrap(), 32 | ); // Allowed client scope 33 | clients.register_client(client); 34 | clients 35 | }; 36 | 37 | // Authorization tokens are 16 byte random keys to a memory hash map. 38 | let authorizer = AuthMap::new(RandomGenerator::new(16)); 39 | 40 | // Bearer tokens are also random generated but 256-bit tokens, since they live longer and this 41 | // example is somewhat paranoid. 42 | // 43 | // We could also use a `TokenSigner::ephemeral` here to create signed tokens which can be read 44 | // and parsed by anyone, but not maliciously created. However, they can not be revoked and thus 45 | // don't offer even longer lived refresh tokens. 46 | let issuer = TokenMap::new(RandomGenerator::new(32)); 47 | 48 | let endpoint = Mutex::new(GenericEndpoint { 49 | registrar, 50 | authorizer, 51 | issuer, 52 | solicitor: FnSolicitor(solicitor), 53 | scopes: vec!["default".parse::().unwrap()], 54 | response: || OAuthResponse::from(Response::empty_404()), 55 | }); 56 | 57 | // Create the main server instance 58 | let server = Server::new(("localhost", 8020), move |request| { 59 | router!(request, 60 | (GET) ["/"] => { 61 | let mut locked = endpoint.lock().unwrap(); 62 | if let Err(err) = ResourceFlow::prepare(&mut *locked) 63 | .expect("Can not fail") 64 | .execute(Request::new(request)) 65 | { // Does not have the proper authorization token 66 | let mut response = err 67 | .map(OAuthResponse::into_inner) 68 | .unwrap_or_else(|_| Response::empty_400()); 69 | let text = " 70 | This page should be accessed via an oauth token from the client in the example. Click 71 | 72 | here to begin the authorization process. 73 | 74 | "; 75 | response.data = ResponseBody::from_string(text); 76 | response.with_unique_header("Content-Type", "text/html; charset=utf8") 77 | } else { // Allowed to access! 78 | Response::text("Hello world!") 79 | } 80 | }, 81 | (GET) ["/authorize"] => { 82 | let mut locked = endpoint.lock().unwrap(); 83 | AuthorizationFlow::prepare(&mut *locked) 84 | .expect("Can not fail") 85 | .execute(Request::new(request)) 86 | .map(OAuthResponse::into_inner) 87 | .unwrap_or_else(|_| Response::empty_400()) 88 | }, 89 | (POST) ["/authorize"] => { 90 | let mut locked = endpoint.lock().unwrap(); 91 | AuthorizationFlow::prepare(&mut *locked) 92 | .expect("Can not fail") 93 | .execute(Request::new(request)) 94 | .map(OAuthResponse::into_inner) 95 | .unwrap_or_else(|_| Response::empty_400()) 96 | }, 97 | (POST) ["/token"] => { 98 | let mut locked = endpoint.lock().unwrap(); 99 | AccessTokenFlow::prepare(&mut *locked) 100 | .expect("Can not fail") 101 | .execute(Request::new(request)) 102 | .map(OAuthResponse::into_inner) 103 | .unwrap_or_else(|_| Response::empty_400()) 104 | }, 105 | (POST) ["/refresh"] => { 106 | let mut locked = endpoint.lock().unwrap(); 107 | RefreshFlow::prepare(&mut *locked) 108 | .expect("Can not fail") 109 | .execute(Request::new(request)) 110 | .map(OAuthResponse::into_inner) 111 | .unwrap_or_else(|_| Response::empty_400()) 112 | }, 113 | _ => Response::empty_404() 114 | ) 115 | }); 116 | 117 | // Run the server main loop in another thread 118 | let join = thread::spawn(move || server.expect("Failed to start server").run()); 119 | // Start a dummy client instance which simply relays the token/response 120 | let client = thread::spawn(|| { 121 | Server::new(("localhost", 8021), support::dummy_client()) 122 | .expect("Failed to start client") 123 | .run() 124 | }); 125 | 126 | // Try to direct the browser to an url initiating the flow 127 | support::open_in_browser(8020); 128 | join.join().expect("Failed to run"); 129 | client.join().expect("Failed to run client"); 130 | } 131 | 132 | /// A simple implementation of an 'owner solicitor'. 133 | /// 134 | /// In a POST request, this will display a page to the user asking for his permission to proceed. 135 | /// The submitted form will then trigger the other authorization handler which actually completes 136 | /// the flow. 137 | fn solicitor(request: &mut Request, grant: Solicitation<'_>) -> OwnerConsent { 138 | if request.method() == "GET" { 139 | let text = support::consent_page_html("/authorize".into(), grant); 140 | let response = Response::html(text); 141 | OwnerConsent::InProgress(response.into()) 142 | } else if request.method() == "POST" { 143 | // No real user authentication is done here, in production you MUST use session keys or equivalent 144 | if let Some(_) = request.get_param("allow") { 145 | OwnerConsent::Authorized("dummy user".to_string()) 146 | } else { 147 | OwnerConsent::Denied 148 | } 149 | } else { 150 | unreachable!("Authorization only mounted on GET and POST") 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /oxide-auth-rouille/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Offers bindings for the code_grant module with rouille servers. 2 | //! 3 | //! Following the simplistic and minimal style of rouille, this module defines only the 4 | //! implementations for `WebRequest` and `WebResponse` and re-exports the available flows. 5 | #![warn(missing_docs)] 6 | 7 | use core::ops::Deref; 8 | use std::borrow::Cow; 9 | 10 | use oxide_auth::endpoint::{QueryParameter, WebRequest, WebResponse}; 11 | 12 | use rouille; 13 | use url::Url; 14 | 15 | // In the spirit of simplicity, this module does not implement any wrapper structures. In order to 16 | // allow efficient and intuitive usage, we simply re-export common structures. 17 | pub use oxide_auth::frontends::simple::endpoint::{FnSolicitor, Generic as GenericEndpoint, Vacant}; 18 | 19 | /// Something went wrong with the rouille http request or response. 20 | #[derive(Debug)] 21 | pub enum WebError { 22 | /// A parameter was encoded incorrectly. 23 | /// 24 | /// This may happen for example due to a query parameter that is not valid utf8 when the query 25 | /// parameters are necessary for OAuth processing. 26 | Encoding, 27 | } 28 | 29 | #[derive(Debug)] 30 | /// The Request type used by Oxide Auth to extract required information 31 | pub struct Request<'a> { 32 | inner: &'a rouille::Request, 33 | } 34 | 35 | #[derive(Debug)] 36 | /// The type Oxide Auth provides in response to a request. 37 | pub struct Response { 38 | inner: rouille::Response, 39 | } 40 | 41 | impl<'a> Request<'a> { 42 | /// Create a new Request from a `rouille::Request` 43 | pub fn new(inner: &'a rouille::Request) -> Self { 44 | Request { inner } 45 | } 46 | } 47 | 48 | impl Response { 49 | /// Produce a `rouille::Response` from a `Response` 50 | pub fn into_inner(self) -> rouille::Response { 51 | self.inner 52 | } 53 | } 54 | 55 | impl From for Response { 56 | fn from(inner: rouille::Response) -> Self { 57 | Response { inner } 58 | } 59 | } 60 | 61 | impl From for rouille::Response { 62 | fn from(response: Response) -> Self { 63 | response.inner 64 | } 65 | } 66 | 67 | impl<'a> WebRequest for Request<'a> { 68 | type Error = WebError; 69 | type Response = Response; 70 | 71 | fn query(&mut self) -> Result, Self::Error> { 72 | let query = self.inner.raw_query_string(); 73 | let data = serde_urlencoded::from_str(query).map_err(|_| WebError::Encoding)?; 74 | Ok(Cow::Owned(data)) 75 | } 76 | 77 | fn urlbody(&mut self) -> Result, Self::Error> { 78 | match self.inner.header("Content-Type") { 79 | None | Some("application/x-www-form-urlencoded") => (), 80 | _ => return Err(WebError::Encoding), 81 | } 82 | 83 | let body = self.inner.data().ok_or(WebError::Encoding)?; 84 | let data = serde_urlencoded::from_reader(body).map_err(|_| WebError::Encoding)?; 85 | Ok(Cow::Owned(data)) 86 | } 87 | 88 | fn authheader(&mut self) -> Result>, Self::Error> { 89 | Ok(self.inner.header("Authorization").map(|st| st.into())) 90 | } 91 | } 92 | 93 | impl WebResponse for Response { 94 | type Error = WebError; 95 | 96 | fn ok(&mut self) -> Result<(), Self::Error> { 97 | self.inner.status_code = 200; 98 | Ok(()) 99 | } 100 | 101 | fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { 102 | self.inner.status_code = 302; 103 | self.inner 104 | .headers 105 | .retain(|header| !header.0.eq_ignore_ascii_case("Location")); 106 | self.inner 107 | .headers 108 | .push(("Location".into(), String::from(url).into())); 109 | Ok(()) 110 | } 111 | 112 | fn client_error(&mut self) -> Result<(), Self::Error> { 113 | self.inner.status_code = 400; 114 | Ok(()) 115 | } 116 | 117 | fn unauthorized(&mut self, kind: &str) -> Result<(), Self::Error> { 118 | self.inner.status_code = 401; 119 | self.inner 120 | .headers 121 | .retain(|header| !header.0.eq_ignore_ascii_case("www-authenticate")); 122 | self.inner 123 | .headers 124 | .push(("WWW-Authenticate".into(), kind.to_string().into())); 125 | Ok(()) 126 | } 127 | 128 | fn body_text(&mut self, text: &str) -> Result<(), Self::Error> { 129 | self.inner 130 | .headers 131 | .retain(|header| !header.0.eq_ignore_ascii_case("Content-Type")); 132 | self.inner 133 | .headers 134 | .push(("Content-Type".into(), "text/plain".into())); 135 | self.inner.data = rouille::ResponseBody::from_string(text); 136 | Ok(()) 137 | } 138 | 139 | fn body_json(&mut self, data: &str) -> Result<(), Self::Error> { 140 | self.inner 141 | .headers 142 | .retain(|header| !header.0.eq_ignore_ascii_case("Content-Type")); 143 | self.inner 144 | .headers 145 | .push(("Content-Type".into(), "application/json".into())); 146 | self.inner.data = rouille::ResponseBody::from_string(data); 147 | Ok(()) 148 | } 149 | } 150 | 151 | impl Deref for Request<'_> { 152 | type Target = rouille::Request; 153 | 154 | fn deref(&self) -> &Self::Target { 155 | self.inner 156 | } 157 | } 158 | 159 | #[cfg(test)] 160 | mod tests { 161 | use super::*; 162 | 163 | #[test] 164 | fn multi_query() { 165 | let request = 166 | &rouille::Request::fake_http("GET", "/authorize?fine=val¶m=a¶m=b", vec![], vec![]); 167 | let mut request = Request::new(request); 168 | let query = WebRequest::query(&mut request).unwrap(); 169 | 170 | assert_eq!(Some(Cow::Borrowed("val")), query.unique_value("fine")); 171 | assert_eq!(None, query.unique_value("param")); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /oxide-auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxide-auth" 3 | version = "0.6.1" 4 | authors = ["Andreas Molzer "] 5 | repository = "https://github.com/HeroicKatora/oxide-auth.git" 6 | edition = "2018" 7 | 8 | description = "A OAuth2 library for common web servers, featuring a set of configurable and pluggable backends." 9 | readme = "Readme.md" 10 | 11 | keywords = ["oauth", "server", "oauth2"] 12 | categories = ["web-programming::http-server", "authentication"] 13 | license = "MIT OR Apache-2.0" 14 | 15 | autoexamples = false 16 | 17 | [dependencies] 18 | base64 = "0.21" 19 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 20 | hmac = "0.12.0" 21 | once_cell = "1.3.1" 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_derive = "1.0" 24 | serde_json = "1.0" 25 | sha2 = "0.10.1" 26 | subtle = "2.4.1" 27 | rand = "0.8" 28 | rust-argon2 = "2.0" 29 | rmp-serde = "1.1" 30 | url = { version = "2.2.2", features = ["serde"] } 31 | 32 | [dev-dependencies] 33 | reqwest = { version = "0.11.10", features = ["blocking"] } 34 | 35 | [package.metadata.docs.rs] 36 | features = [] 37 | -------------------------------------------------------------------------------- /oxide-auth/Readme.md: -------------------------------------------------------------------------------- 1 | # oxide-auth 2 | 3 | A OAuth2 server library, for use in combination with common web servers, 4 | featuring a set of configurable and pluggable backends. 5 | 6 | ## About 7 | 8 | `oxide-auth` aims at providing a comprehensive and extensible interface to 9 | managing OAuth2 tokens on a server. The core package is agnostic of the used 10 | front-end web server and adaptors for the actix, rocket, iron and rouille 11 | crates are provided in extension crates. Through an interface designed with 12 | traits, the frontend is as easily pluggable as the backend. You can provide 13 | your own request, response and error types as well as choose any custom method 14 | of authenticating clients and users by implement the appropriate traits. 15 | 16 | ## Integration 17 | 18 | Some popular server libraries have ready-made integration. These still require 19 | some dependency on the base crate but generally wrap the interface into a user 20 | that is considered more idiomatic for their library. Besides the implementation 21 | of `oxide-auth` traits for the request type, specific error and response traits 22 | are also implemented. 23 | 24 | | What | Crate | Notes | Docs | 25 | |-|-|-|-| 26 | | `actix` | `oxide-auth-actix` | - | [![actix docs](https://docs.rs/oxide-auth-actix/badge.svg)](https://docs.rs/oxide-auth-actix) | 27 | | `async` wrappers | `oxide-auth-async` | - | [![async docs](https://docs.rs/oxide-auth-async/badge.svg)](https://docs.rs/oxide-auth-async) | 28 | | `redis` | `oxide-auth-db` | - | [![redis docs](https://docs.rs/oxide-auth-db/badge.svg)](https://docs.rs/oxide-auth-db) | 29 | | `rocket` | `oxide-auth-rocket` | nightly | [![rocket docs](https://docs.rs/oxide-auth-rocket/badge.svg)](https://docs.rs/oxide-auth-rocket) | 30 | | `rouille` | `oxide-auth-rouille` | - | [![rouille docs](https://docs.rs/oxide-auth-rouille/badge.svg)](https://docs.rs/oxide-auth-rouille) | 31 | | `iron` | `oxide-auth-iron` | - | [![iron docs](https://docs.rs/oxide-auth-iron/badge.svg)](https://docs.rs/oxide-auth-iron) | 32 | 33 | 34 | ## Additional 35 | 36 | Licensed under either of 37 | * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) 38 | * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) 39 | at your option. 40 | 41 | The license applies to all parts of the source code, its documentation and 42 | supplementary files unless otherwise indicated. It does NOT apply to the 43 | replicated full-text copies of referenced RFCs which were included for the sake 44 | of completion. These are distributed as permitted by [IETF Trust License 45 | 4–Section 3.c.i][IETF4]. 46 | -------------------------------------------------------------------------------- /oxide-auth/src/code_grant/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides standard extensions to the OAuth process. 2 | mod pkce; 3 | 4 | pub use self::pkce::Pkce; 5 | -------------------------------------------------------------------------------- /oxide-auth/src/code_grant/extensions/pkce.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::primitives::grant::{GrantExtension, Value}; 4 | 5 | use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 6 | use sha2::{Digest, Sha256}; 7 | use subtle::ConstantTimeEq; 8 | 9 | /// Proof Key for Code Exchange by OAuth Public Clients 10 | /// 11 | /// > Auth 2.0 public clients utilizing the Authorization Code Grant are 12 | /// susceptible to the authorization code interception attack. This 13 | /// specification describes the attack as well as a technique to mitigate 14 | /// against the threat through the use of Proof Key for Code Exchange 15 | /// (PKCE, pronounced "pixy"). 16 | /// 17 | /// (from the respective [RFC 7636]) 18 | /// 19 | /// In short, public clients share a verifier for a secret token when requesting their initial 20 | /// authorization code. When they then make a second request to the autorization server, trading 21 | /// this code for an access token, they can credible assure the server of their identity by 22 | /// presenting the secret token. 23 | /// 24 | /// The simple `plain` method only prevents attackers unable to snoop on the connection from 25 | /// impersonating the client, while the `S256` method, which uses one-way hash functions, makes 26 | /// any attack short of reading the victim client's memory infeasible. 27 | /// 28 | /// Support for the `plain` method is OPTIONAL and must be turned on explicitely. 29 | /// 30 | /// [RFC 7636]: https://tools.ietf.org/html/rfc7636 31 | pub struct Pkce { 32 | required: bool, 33 | allow_plain: bool, 34 | } 35 | 36 | enum Method { 37 | Plain(String), 38 | Sha256(String), 39 | } 40 | 41 | impl Pkce { 42 | /// A pkce extensions which requires clients to use it. 43 | pub fn required() -> Pkce { 44 | Pkce { 45 | required: true, 46 | allow_plain: false, 47 | } 48 | } 49 | 50 | /// Pkce extension which will check verifiers if present but not require them. 51 | pub fn optional() -> Pkce { 52 | Pkce { 53 | required: false, 54 | allow_plain: false, 55 | } 56 | } 57 | 58 | /// Allow usage of the less secure `plain` verification method. This method is NOT secure 59 | /// an eavesdropping attacker such as rogue processes capturing a devices requests. 60 | pub fn allow_plain(&mut self) { 61 | self.allow_plain = true; 62 | } 63 | 64 | /// Create the encoded method for proposed method and challenge. 65 | /// 66 | /// The method defaults to `plain` when none is given, effectively offering increased 67 | /// compatibility but less security. Support for `plain` is optional and needs to be enabled 68 | /// explicitely through `Pkce::allow_plain`. This extension may also require clients to use it, 69 | /// in which case giving no challenge also leads to an error. 70 | /// 71 | /// The resulting string MUST NOT be publicly available to the client. Otherwise, it would be 72 | /// trivial for a third party to impersonate the client in the access token request phase. For 73 | /// a SHA256 methods the results would not be quite as severe but still bad practice. 74 | pub fn challenge( 75 | &self, method: Option>, challenge: Option>, 76 | ) -> Result, ()> { 77 | let method = method.unwrap_or(Cow::Borrowed("plain")); 78 | 79 | let challenge = match challenge { 80 | None if self.required => return Err(()), 81 | None => return Ok(None), 82 | Some(challenge) => challenge, 83 | }; 84 | 85 | let method = Method::from_parameter(method, challenge)?; 86 | let method = method.assert_supported_method(self.allow_plain)?; 87 | 88 | Ok(Some(Value::private(Some(method.encode())))) 89 | } 90 | 91 | /// Verify against the encoded challenge. 92 | /// 93 | /// When the challenge is required, ensure again that a challenge was made and a corresponding 94 | /// method data is present as an extension. This is not strictly necessary since clients should 95 | /// not be able to delete private extension data but this check does not cost a lot. 96 | /// 97 | /// When a challenge was agreed upon but no verifier is present, this method will return an 98 | /// error. 99 | pub fn verify(&self, method: Option, verifier: Option>) -> Result<(), ()> { 100 | let (method, verifier) = match (method, verifier) { 101 | (None, _) if self.required => return Err(()), 102 | (None, _) => return Ok(()), 103 | // An internal saved method but no verifier 104 | (Some(_), None) => return Err(()), 105 | (Some(method), Some(verifier)) => (method, verifier), 106 | }; 107 | 108 | let method = match method.into_private_value() { 109 | Ok(Some(method)) => method, 110 | _ => return Err(()), 111 | }; 112 | 113 | let method = Method::from_encoded(Cow::Owned(method))?; 114 | 115 | method.verify(&verifier) 116 | } 117 | } 118 | 119 | impl GrantExtension for Pkce { 120 | fn identifier(&self) -> &'static str { 121 | "pkce" 122 | } 123 | } 124 | 125 | /// Base 64 encoding without padding 126 | fn b64encode(data: &[u8]) -> String { 127 | URL_SAFE_NO_PAD.encode(data) 128 | } 129 | 130 | impl Method { 131 | fn from_parameter(method: Cow, challenge: Cow) -> Result { 132 | match method.as_ref() { 133 | "plain" => Ok(Method::Plain(challenge.into_owned())), 134 | "S256" => Ok(Method::Sha256(challenge.into_owned())), 135 | _ => Err(()), 136 | } 137 | } 138 | 139 | fn assert_supported_method(self, allow_plain: bool) -> Result { 140 | match (self, allow_plain) { 141 | (this, true) => Ok(this), 142 | (Method::Sha256(content), false) => Ok(Method::Sha256(content)), 143 | (Method::Plain(_), false) => Err(()), 144 | } 145 | } 146 | 147 | fn encode(self) -> String { 148 | match self { 149 | Method::Plain(challenge) => challenge + "p", 150 | Method::Sha256(challenge) => challenge + "S", 151 | } 152 | } 153 | 154 | fn from_encoded(encoded: Cow) -> Result { 155 | // TODO: avoid allocation in case of borrow and invalid. 156 | let mut encoded = encoded.into_owned(); 157 | match encoded.pop() { 158 | None => Err(()), 159 | Some('p') => Ok(Method::Plain(encoded)), 160 | Some('S') => Ok(Method::Sha256(encoded)), 161 | _ => Err(()), 162 | } 163 | } 164 | 165 | fn verify(&self, verifier: &str) -> Result<(), ()> { 166 | match self { 167 | Method::Plain(encoded) => { 168 | if encoded.as_bytes().ct_eq(verifier.as_bytes()).into() { 169 | Ok(()) 170 | } else { 171 | Err(()) 172 | } 173 | } 174 | Method::Sha256(encoded) => { 175 | let mut hasher = Sha256::new(); 176 | hasher.update(verifier.as_bytes()); 177 | let b64digest = b64encode(&hasher.finalize()); 178 | if encoded.as_bytes().ct_eq(b64digest.as_bytes()).into() { 179 | Ok(()) 180 | } else { 181 | Err(()) 182 | } 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /oxide-auth/src/code_grant/mod.rs: -------------------------------------------------------------------------------- 1 | //! Available backend algorithms. 2 | //! 3 | //! The backend codifies the requirements for the from the [RFC 6749] into types and functions as 4 | //! safely as possible. The result of the backend are abstract results, actions which should be 5 | //! executed or relayed by the frontend using its available types. Abstract in this sense means 6 | //! that the reponses from the backend are not generic on an input type. 7 | //! 8 | //! Another consideration is the possiblilty of reusing some components with other oauth schemes. 9 | //! In this way, the backend is used to group necessary types and as an interface to implementors, 10 | //! to be able to infer the range of applicable end effectors (i.e. authorizers, issuer, 11 | //! registrars). 12 | //! 13 | //! ## Usage 14 | //! 15 | //! For all purposes that offer user interaction through an access point, you should probably have 16 | //! a look at the encapsulation provided by [`endpoint`] instead. You should only fallback to this 17 | //! if the flows provided there are too generic (unlikely) or your use case makes an [`Endpoint`] 18 | //! implementation impossible. 19 | //! 20 | //! ## Limitations 21 | //! 22 | //! The only supported authentication method for clients is password based. This is not to be 23 | //! confused with users in the sense of people registering accounts on a social media platform. In 24 | //! OAuth nomenclature, those are resource owners while a client is a user of a (Bearer) token. 25 | //! 26 | //! [RFC 6479]: https://tools.ietf.org/html/rfc6749 27 | //! [`endpoint`]: ../endpoint/index.html 28 | //! [`Endpoint`]: ../endpoint/trait.Endpoint.html 29 | 30 | pub mod accesstoken; 31 | pub mod authorization; 32 | pub mod client_credentials; 33 | pub mod error; 34 | pub mod extensions; 35 | pub mod refresh; 36 | pub mod resource; 37 | -------------------------------------------------------------------------------- /oxide-auth/src/endpoint/error.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | 4 | /// Errors which should not or need not be communicated to the requesting party but which are of 5 | /// interest to the server. See the documentation for each enum variant for more documentation on 6 | /// each as some may have an expected response. These include badly formatted headers or url encoded 7 | /// body, unexpected parameters, or security relevant required parameters. 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 9 | pub enum OAuthError { 10 | /// Deny authorization to the client by essentially dropping the request. 11 | /// 12 | /// For example, this response is given when an incorrect client has been provided in the 13 | /// authorization request in order to avoid potential indirect denial of service vulnerabilities. 14 | DenySilently, 15 | 16 | /// One of the primitives used to complete the operation failed. 17 | /// 18 | /// This indicates a problem in the server configuration or the frontend library or the 19 | /// implementation of the primitive underlying those two. 20 | PrimitiveError, 21 | 22 | /// The incoming request was malformed. 23 | /// 24 | /// This implies that it did not change any internal state. Note that this differs from an 25 | /// `InvalidRequest` as in the OAuth specification. `BadRequest` is reported by a frontend 26 | /// implementation of a request, due to http non-compliance, while an `InvalidRequest` is a 27 | /// type of response to an authorization request by a user-agent that is sent to the specified 28 | /// client (although it may be caused by a bad request). 29 | BadRequest, 30 | } 31 | 32 | impl fmt::Display for OAuthError { 33 | fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { 34 | match self { 35 | OAuthError::DenySilently => fmt.write_str("OAuthError: Request should be silently denied"), 36 | OAuthError::PrimitiveError => fmt.write_str("OAuthError: Server component failed"), 37 | OAuthError::BadRequest => fmt.write_str("OAuthError: Bad request"), 38 | } 39 | } 40 | } 41 | 42 | impl error::Error for OAuthError {} 43 | -------------------------------------------------------------------------------- /oxide-auth/src/endpoint/resource.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::code_grant::resource::{ 4 | protect, Error as ResourceError, Endpoint as ResourceEndpoint, Request as ResourceRequest, 5 | }; 6 | use crate::primitives::grant::Grant; 7 | 8 | use super::*; 9 | 10 | /// Guards resources by requiring OAuth authorization. 11 | pub struct ResourceFlow 12 | where 13 | E: Endpoint, 14 | R: WebRequest, 15 | { 16 | endpoint: WrappedResource, 17 | } 18 | 19 | struct WrappedResource, R: WebRequest>(E, PhantomData); 20 | 21 | struct WrappedRequest { 22 | /// Original request. 23 | request: PhantomData, 24 | 25 | /// The authorization token. 26 | authorization: Option, 27 | 28 | /// An error if one occurred. 29 | /// 30 | /// Actual parsing of the authorization header is done in the lower level. 31 | error: Option, 32 | } 33 | 34 | struct Scoped<'a, E: 'a, R: 'a> { 35 | request: &'a mut R, 36 | endpoint: &'a mut E, 37 | } 38 | 39 | impl ResourceFlow 40 | where 41 | E: Endpoint, 42 | R: WebRequest, 43 | { 44 | /// Check that the endpoint supports the necessary operations for handling requests. 45 | /// 46 | /// Binds the endpoint to a particular type of request that it supports, for many 47 | /// implementations this is probably single type anyways. 48 | /// 49 | /// ## Panics 50 | /// 51 | /// Indirectly `execute` may panic when this flow is instantiated with an inconsistent 52 | /// endpoint, for details see the documentation of `Endpoint` and `execute`. For 53 | /// consistent endpoints, the panic is instead caught as an error here. 54 | pub fn prepare(mut endpoint: E) -> Result { 55 | if endpoint.issuer_mut().is_none() { 56 | return Err(endpoint.error(OAuthError::PrimitiveError)); 57 | } 58 | 59 | if endpoint.scopes().is_none() { 60 | return Err(endpoint.error(OAuthError::PrimitiveError)); 61 | } 62 | 63 | Ok(ResourceFlow { 64 | endpoint: WrappedResource(endpoint, PhantomData), 65 | }) 66 | } 67 | 68 | /// Use the checked endpoint to check for authorization for a resource. 69 | /// 70 | /// ## Panics 71 | /// 72 | /// When the issuer returned by the endpoint is suddenly `None` when previously it 73 | /// was `Some(_)`. 74 | pub fn execute(&mut self, mut request: R) -> Result> { 75 | let protected = { 76 | let wrapped = WrappedRequest::new(&mut request); 77 | 78 | let mut scoped = Scoped { 79 | request: &mut request, 80 | endpoint: &mut self.endpoint.0, 81 | }; 82 | 83 | protect(&mut scoped, &wrapped) 84 | }; 85 | 86 | protected.map_err(|err| self.denied(&mut request, err)) 87 | } 88 | 89 | fn denied(&mut self, request: &mut R, error: ResourceError) -> Result { 90 | let template = match &error { 91 | ResourceError::AccessDenied { .. } => InnerTemplate::Unauthorized { 92 | error: None, 93 | access_token_error: None, 94 | }, 95 | ResourceError::NoAuthentication { .. } => InnerTemplate::Unauthorized { 96 | error: None, 97 | access_token_error: None, 98 | }, 99 | ResourceError::InvalidRequest { .. } => InnerTemplate::BadRequest { 100 | access_token_error: None, 101 | }, 102 | ResourceError::PrimitiveError => { 103 | return Err(self.endpoint.0.error(OAuthError::PrimitiveError)) 104 | } 105 | }; 106 | 107 | let mut response = self.endpoint.0.response(request, template.into())?; 108 | response 109 | .unauthorized(&error.www_authenticate()) 110 | .map_err(|err| self.endpoint.0.web_error(err))?; 111 | 112 | Ok(response) 113 | } 114 | } 115 | 116 | impl WrappedRequest { 117 | fn new(request: &mut R) -> Self { 118 | let token = match request.authheader() { 119 | // TODO: this is unecessarily wasteful, we always clone. 120 | Ok(Some(token)) => Some(token.into_owned()), 121 | Ok(None) => None, 122 | Err(error) => return Self::from_error(error), 123 | }; 124 | 125 | WrappedRequest { 126 | request: PhantomData, 127 | authorization: token, 128 | error: None, 129 | } 130 | } 131 | 132 | fn from_error(error: R::Error) -> Self { 133 | WrappedRequest { 134 | request: PhantomData, 135 | authorization: None, 136 | error: Some(error), 137 | } 138 | } 139 | } 140 | 141 | impl<'a, E: Endpoint + 'a, R: WebRequest + 'a> ResourceEndpoint for Scoped<'a, E, R> { 142 | fn scopes(&mut self) -> &[Scope] { 143 | self.endpoint.scopes().unwrap().scopes(self.request) 144 | } 145 | 146 | fn issuer(&mut self) -> &dyn Issuer { 147 | self.endpoint.issuer_mut().unwrap() 148 | } 149 | } 150 | 151 | impl ResourceRequest for WrappedRequest { 152 | fn valid(&self) -> bool { 153 | self.error.is_none() 154 | } 155 | 156 | fn token(&self) -> Option> { 157 | self.authorization.as_deref().map(Cow::Borrowed) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /oxide-auth/src/endpoint/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::endpoint::*; 2 | use crate::primitives::generator::TagGrant; 3 | use crate::primitives::grant::Grant; 4 | 5 | use std::borrow::Cow; 6 | use std::collections::HashMap; 7 | 8 | use url::Url; 9 | 10 | /// Open and simple implementation of `WebRequest`. 11 | #[derive(Clone, Debug, Default)] 12 | struct CraftedRequest { 13 | /// The key-value pairs in the url query component. 14 | pub query: Option>>, 15 | 16 | /// The key-value pairs of a `x-www-form-urlencoded` body. 17 | pub urlbody: Option>>, 18 | 19 | /// Provided authorization header. 20 | pub auth: Option, 21 | } 22 | 23 | /// Open and simple implementation of `WebResponse`. 24 | #[derive(Debug, Default)] 25 | struct CraftedResponse { 26 | /// HTTP status code. 27 | pub status: Status, 28 | 29 | /// A location header, for example for redirects. 30 | pub location: Option, 31 | 32 | /// Indicates how the client should have authenticated. 33 | /// 34 | /// Only set with `Unauthorized` status. 35 | pub www_authenticate: Option, 36 | 37 | /// Encoded body of the response. 38 | /// 39 | /// One variant for each possible encoding type. 40 | pub body: Option, 41 | } 42 | 43 | /// An enum containing the necessary HTTP status codes. 44 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 45 | enum Status { 46 | /// Http status code 200. 47 | Ok, 48 | 49 | /// Http status code 302. 50 | Redirect, 51 | 52 | /// Http status code 400. 53 | BadRequest, 54 | 55 | /// Http status code 401. 56 | Unauthorized, 57 | } 58 | 59 | /// Models the necessary body contents. 60 | /// 61 | /// Real HTTP protocols should set a content type header for each of the body variants. 62 | #[derive(Clone, Debug)] 63 | enum Body { 64 | /// A pure text body. 65 | Text(String), 66 | 67 | /// A json encoded body, `application/json`. 68 | Json(String), 69 | } 70 | 71 | #[derive(Debug)] 72 | enum CraftedError { 73 | Crafted, 74 | } 75 | 76 | impl WebRequest for CraftedRequest { 77 | type Response = CraftedResponse; 78 | type Error = CraftedError; 79 | 80 | fn query(&mut self) -> Result, Self::Error> { 81 | self.query 82 | .as_ref() 83 | .map(|hm| Cow::Borrowed(hm as &dyn QueryParameter)) 84 | .ok_or(CraftedError::Crafted) 85 | } 86 | 87 | fn urlbody(&mut self) -> Result, Self::Error> { 88 | self.urlbody 89 | .as_ref() 90 | .map(|hm| Cow::Borrowed(hm as &dyn QueryParameter)) 91 | .ok_or(CraftedError::Crafted) 92 | } 93 | 94 | fn authheader(&mut self) -> Result>, Self::Error> { 95 | Ok(self.auth.as_ref().map(|bearer| bearer.as_str().into())) 96 | } 97 | } 98 | 99 | impl WebResponse for CraftedResponse { 100 | type Error = CraftedError; 101 | 102 | fn ok(&mut self) -> Result<(), Self::Error> { 103 | self.status = Status::Ok; 104 | self.location = None; 105 | self.www_authenticate = None; 106 | Ok(()) 107 | } 108 | 109 | /// A response which will redirect the user-agent to which the response is issued. 110 | fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { 111 | self.status = Status::Redirect; 112 | self.location = Some(url); 113 | self.www_authenticate = None; 114 | Ok(()) 115 | } 116 | 117 | /// Set the response status to 400. 118 | fn client_error(&mut self) -> Result<(), Self::Error> { 119 | self.status = Status::BadRequest; 120 | self.location = None; 121 | self.www_authenticate = None; 122 | Ok(()) 123 | } 124 | 125 | /// Set the response status to 401 and add a `WWW-Authenticate` header. 126 | fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> { 127 | self.status = Status::Unauthorized; 128 | self.location = None; 129 | self.www_authenticate = Some(header_value.to_owned()); 130 | Ok(()) 131 | } 132 | 133 | /// A pure text response with no special media type set. 134 | fn body_text(&mut self, text: &str) -> Result<(), Self::Error> { 135 | self.body = Some(Body::Text(text.to_owned())); 136 | Ok(()) 137 | } 138 | 139 | /// Json repsonse data, with media type `aplication/json. 140 | fn body_json(&mut self, data: &str) -> Result<(), Self::Error> { 141 | self.body = Some(Body::Json(data.to_owned())); 142 | Ok(()) 143 | } 144 | } 145 | 146 | struct TestGenerator(String); 147 | 148 | impl TagGrant for TestGenerator { 149 | fn tag(&mut self, _: u64, _grant: &Grant) -> Result { 150 | Ok(self.0.clone()) 151 | } 152 | } 153 | 154 | struct Allow(String); 155 | struct Deny; 156 | 157 | impl OwnerSolicitor for Allow { 158 | fn check_consent( 159 | &mut self, _: &mut CraftedRequest, _: Solicitation, 160 | ) -> OwnerConsent { 161 | OwnerConsent::Authorized(self.0.clone()) 162 | } 163 | } 164 | 165 | impl OwnerSolicitor for Deny { 166 | fn check_consent( 167 | &mut self, _: &mut CraftedRequest, _: Solicitation, 168 | ) -> OwnerConsent { 169 | OwnerConsent::Denied 170 | } 171 | } 172 | 173 | impl<'l> OwnerSolicitor for &'l Allow { 174 | fn check_consent( 175 | &mut self, _: &mut CraftedRequest, _: Solicitation, 176 | ) -> OwnerConsent { 177 | OwnerConsent::Authorized(self.0.clone()) 178 | } 179 | } 180 | 181 | impl<'l> OwnerSolicitor for &'l Deny { 182 | fn check_consent( 183 | &mut self, _: &mut CraftedRequest, _: Solicitation, 184 | ) -> OwnerConsent { 185 | OwnerConsent::Denied 186 | } 187 | } 188 | 189 | trait ToSingleValueQuery { 190 | fn to_single_value_query(self) -> HashMap>; 191 | } 192 | 193 | impl<'r, I, K, V> ToSingleValueQuery for I 194 | where 195 | I: Iterator, 196 | K: AsRef + 'r, 197 | V: AsRef + 'r, 198 | { 199 | fn to_single_value_query(self) -> HashMap> { 200 | self.map(|&(ref k, ref v)| (k.as_ref().to_string(), vec![v.as_ref().to_string()])) 201 | .collect() 202 | } 203 | } 204 | 205 | impl Default for Status { 206 | fn default() -> Self { 207 | Status::Ok 208 | } 209 | } 210 | 211 | pub mod defaults { 212 | pub const EXAMPLE_CLIENT_ID: &str = "ClientId"; 213 | pub const EXAMPLE_OWNER_ID: &str = "Owner"; 214 | pub const EXAMPLE_PASSPHRASE: &str = "VGhpcyBpcyBhIHZlcnkgc2VjdXJlIHBhc3NwaHJhc2UK"; 215 | pub const EXAMPLE_REDIRECT_URI: &str = "https://client.example/endpoint"; 216 | pub const EXAMPLE_SCOPE: &str = "example default"; 217 | } 218 | 219 | mod authorization; 220 | mod access_token; 221 | mod client_credentials; 222 | mod resource; 223 | mod refresh; 224 | mod pkce; 225 | -------------------------------------------------------------------------------- /oxide-auth/src/endpoint/tests/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::issuer::TokenMap; 2 | use crate::primitives::generator::RandomGenerator; 3 | use crate::primitives::grant::{Grant, Extensions}; 4 | use crate::primitives::scope::Scope; 5 | 6 | use crate::frontends::simple::endpoint::resource_flow; 7 | 8 | use chrono::{Utc, Duration}; 9 | 10 | use super::CraftedRequest; 11 | use super::defaults::*; 12 | 13 | struct ResourceSetup { 14 | issuer: TokenMap, 15 | authtoken: String, 16 | wrong_scope_token: String, 17 | small_scope_token: String, 18 | resource_scope: [Scope; 1], 19 | } 20 | 21 | impl ResourceSetup { 22 | fn new() -> ResourceSetup { 23 | use crate::primitives::issuer::Issuer; 24 | 25 | // Ensure that valid tokens are 16 bytes long, so we can craft an invalid one 26 | let mut issuer = TokenMap::new(RandomGenerator::new(16)); 27 | 28 | let authtoken = issuer 29 | .issue(Grant { 30 | client_id: EXAMPLE_CLIENT_ID.to_string(), 31 | owner_id: EXAMPLE_OWNER_ID.to_string(), 32 | redirect_uri: EXAMPLE_REDIRECT_URI.parse().unwrap(), 33 | scope: "legit needed andmore".parse().unwrap(), 34 | until: Utc::now() + Duration::hours(1), 35 | extensions: Extensions::new(), 36 | }) 37 | .unwrap(); 38 | 39 | let wrong_scope_token = issuer 40 | .issue(Grant { 41 | client_id: EXAMPLE_CLIENT_ID.to_string(), 42 | owner_id: EXAMPLE_OWNER_ID.to_string(), 43 | redirect_uri: EXAMPLE_REDIRECT_URI.parse().unwrap(), 44 | scope: "wrong needed".parse().unwrap(), 45 | until: Utc::now() + Duration::hours(1), 46 | extensions: Extensions::new(), 47 | }) 48 | .unwrap(); 49 | 50 | let small_scope_token = issuer 51 | .issue(Grant { 52 | client_id: EXAMPLE_CLIENT_ID.to_string(), 53 | owner_id: EXAMPLE_OWNER_ID.to_string(), 54 | redirect_uri: EXAMPLE_REDIRECT_URI.parse().unwrap(), 55 | scope: "legit".parse().unwrap(), 56 | until: Utc::now() + Duration::hours(1), 57 | extensions: Extensions::new(), 58 | }) 59 | .unwrap(); 60 | 61 | ResourceSetup { 62 | issuer, 63 | authtoken: authtoken.token, 64 | wrong_scope_token: wrong_scope_token.token, 65 | small_scope_token: small_scope_token.token, 66 | resource_scope: ["needed legit".parse().unwrap()], 67 | } 68 | } 69 | 70 | fn test_access_success(&mut self, request: CraftedRequest) { 71 | match resource_flow(&mut self.issuer, &self.resource_scope).execute(request) { 72 | Ok(_) => (), 73 | Err(ohno) => panic!("Expected an error instead of {:?}", ohno), 74 | } 75 | } 76 | 77 | fn test_access_error(&mut self, request: CraftedRequest) { 78 | match resource_flow(&mut self.issuer, &self.resource_scope).execute(request) { 79 | Ok(resp) => panic!("Expected an error instead of {:?}", resp), 80 | Err(_) => (), 81 | } 82 | } 83 | } 84 | 85 | #[test] 86 | fn resource_success() { 87 | let mut setup = ResourceSetup::new(); 88 | let success = CraftedRequest { 89 | query: None, 90 | urlbody: None, 91 | auth: Some("Bearer ".to_string() + &setup.authtoken), 92 | }; 93 | 94 | setup.test_access_success(success); 95 | } 96 | 97 | #[test] 98 | fn resource_casing_success() { 99 | let mut setup = ResourceSetup::new(); 100 | let success = CraftedRequest { 101 | query: None, 102 | urlbody: None, 103 | auth: Some("bearer ".to_string() + &setup.authtoken), 104 | }; 105 | 106 | setup.test_access_success(success); 107 | 108 | let success = CraftedRequest { 109 | query: None, 110 | urlbody: None, 111 | auth: Some("bEArEr ".to_string() + &setup.authtoken), 112 | }; 113 | 114 | setup.test_access_success(success); 115 | } 116 | 117 | #[test] 118 | fn resource_no_authorization() { 119 | // Does not have any authorization 120 | let no_authorization = CraftedRequest { 121 | query: None, 122 | urlbody: None, 123 | auth: None, 124 | }; 125 | 126 | ResourceSetup::new().test_access_error(no_authorization); 127 | } 128 | 129 | #[test] 130 | fn resource_invalid_token() { 131 | // Does not have any authorization 132 | let invalid_token = CraftedRequest { 133 | query: None, 134 | urlbody: None, 135 | auth: Some("Bearer ThisisnotavalidtokenTooLong".to_string()), 136 | }; 137 | 138 | ResourceSetup::new().test_access_error(invalid_token); 139 | } 140 | 141 | #[test] 142 | fn resource_wrong_method() { 143 | let mut setup = ResourceSetup::new(); 144 | // Not indicating the `Bearer` authorization method 145 | let wrong_method = CraftedRequest { 146 | query: None, 147 | urlbody: None, 148 | auth: Some("NotBearer ".to_string() + &setup.authtoken), 149 | }; 150 | 151 | setup.test_access_error(wrong_method); 152 | } 153 | 154 | #[test] 155 | fn resource_scope_too_small() { 156 | let mut setup = ResourceSetup::new(); 157 | // Scope of used token is too small for access 158 | let scope_too_small = CraftedRequest { 159 | query: None, 160 | urlbody: None, 161 | auth: Some("Bearer ".to_string() + &setup.small_scope_token), 162 | }; 163 | 164 | setup.test_access_error(scope_too_small); 165 | } 166 | 167 | #[test] 168 | fn resource_wrong_scope() { 169 | let mut setup = ResourceSetup::new(); 170 | // Scope of used token does not match the access 171 | let wrong_scope = CraftedRequest { 172 | query: None, 173 | urlbody: None, 174 | auth: Some("Bearer ".to_string() + &setup.wrong_scope_token), 175 | }; 176 | 177 | setup.test_access_error(wrong_scope); 178 | } 179 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/actix.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/iron.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/rocket.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/rouille.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/simple/extensions/extended.rs: -------------------------------------------------------------------------------- 1 | use crate::endpoint::{Endpoint, Extension, OAuthError, OwnerSolicitor, Scopes, Template, WebRequest}; 2 | use crate::primitives::authorizer::Authorizer; 3 | use crate::primitives::issuer::Issuer; 4 | use crate::primitives::registrar::Registrar; 5 | 6 | use super::AddonList; 7 | 8 | /// An inner endpoint with simple extensions. 9 | /// 10 | /// If the inner endpoint had any extension, it will simply never be provided to any flow and 11 | /// overwritten. Therefore, this is mainly useful for other endpoints that did not implement 12 | /// extensions by themselves such as `frontends::simple::endpoint::Generic`. 13 | pub struct Extended { 14 | /// Endpoint being extended. This field is `pub` for `oxide-auth-async` be able to implement 15 | /// async version of some traits. 16 | pub inner: Inner, 17 | 18 | /// Extensions of the endpoint. This field is `pub` for `oxide-auth-async` be able to implement 19 | /// async version of some traits. 20 | pub addons: Extension, 21 | } 22 | 23 | impl Extended { 24 | /// Wrap an endpoint with a standard extension system. 25 | pub fn new(inner: Inner) -> Self { 26 | Extended { 27 | inner, 28 | addons: AddonList::default(), 29 | } 30 | } 31 | } 32 | 33 | impl Extended { 34 | /// Wrap an inner endpoint with a preconstructed extension instance. 35 | pub fn extend_with(inner: Inner, extension: E) -> Self { 36 | Extended { 37 | inner, 38 | addons: extension, 39 | } 40 | } 41 | 42 | /// A reference to the extension. 43 | pub fn extension(&self) -> &E { 44 | &self.addons 45 | } 46 | 47 | /// A mutable reference to the extension. 48 | pub fn extension_mut(&mut self) -> &mut E { 49 | &mut self.addons 50 | } 51 | } 52 | 53 | impl Endpoint for Extended 54 | where 55 | Request: WebRequest, 56 | Inner: Endpoint, 57 | Ext: Extension, 58 | { 59 | type Error = Inner::Error; 60 | 61 | fn registrar(&self) -> Option<&dyn Registrar> { 62 | self.inner.registrar() 63 | } 64 | 65 | fn authorizer_mut(&mut self) -> Option<&mut dyn Authorizer> { 66 | self.inner.authorizer_mut() 67 | } 68 | 69 | fn issuer_mut(&mut self) -> Option<&mut dyn Issuer> { 70 | self.inner.issuer_mut() 71 | } 72 | 73 | fn owner_solicitor(&mut self) -> Option<&mut dyn OwnerSolicitor> { 74 | self.inner.owner_solicitor() 75 | } 76 | 77 | fn scopes(&mut self) -> Option<&mut dyn Scopes> { 78 | self.inner.scopes() 79 | } 80 | 81 | fn response( 82 | &mut self, request: &mut Request, kind: Template, 83 | ) -> Result { 84 | self.inner.response(request, kind) 85 | } 86 | 87 | fn error(&mut self, err: OAuthError) -> Self::Error { 88 | self.inner.error(err) 89 | } 90 | 91 | fn web_error(&mut self, err: Request::Error) -> Self::Error { 92 | self.inner.web_error(err) 93 | } 94 | 95 | fn extension(&mut self) -> Option<&mut dyn Extension> { 96 | Some(&mut self.addons) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/simple/extensions/list.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::sync::Arc; 3 | 4 | use super::{AuthorizationAddon, AccessTokenAddon, AddonResult, ClientCredentialsAddon}; 5 | use crate::code_grant::accesstoken::{Extension as AccessTokenExtension, Request}; 6 | use crate::code_grant::authorization::{Extension as AuthorizationExtension, Request as AuthRequest}; 7 | use crate::code_grant::client_credentials::{ 8 | Extension as ClientCredentialsExtension, Request as ClientCredentialsRequest, 9 | }; 10 | use crate::endpoint::Extension; 11 | use crate::primitives::grant::{Extensions, GrantExtension}; 12 | 13 | /// A simple list of loosely related authorization and access addons. 14 | /// 15 | /// The owning representation of access extensions can be switched out to `Box<_>`, `Rc<_>` or 16 | /// other types. 17 | #[derive(Clone)] 18 | pub struct AddonList { 19 | /// Extension to be applied on authorize. This field is `pub` for `oxide-auth-async` be able to 20 | /// implement async version of some traits. 21 | pub authorization: Vec>, 22 | 23 | /// Extension to be applied on get token. This field is `pub` for `oxide-auth-async` be able to 24 | /// implement async version of some traits. 25 | pub access_token: Vec>, 26 | 27 | /// Extension to be applied on get token. This field is `pub` for `oxide-auth-async` be able to 28 | /// implement async version of some traits. 29 | pub client_credentials: Vec>, 30 | } 31 | 32 | impl AddonList { 33 | /// Create an empty extension system. 34 | pub fn new() -> Self { 35 | AddonList { 36 | authorization: vec![], 37 | access_token: vec![], 38 | client_credentials: vec![], 39 | } 40 | } 41 | 42 | /// Add an addon that only applies to authorization. 43 | pub fn push_authorization(&mut self, addon: A) 44 | where 45 | A: AuthorizationAddon + Send + Sync + 'static, 46 | { 47 | self.authorization.push(Arc::new(addon)) 48 | } 49 | 50 | /// Add an addon that only applies to access_token. 51 | pub fn push_access_token(&mut self, addon: A) 52 | where 53 | A: AccessTokenAddon + Send + Sync + 'static, 54 | { 55 | self.access_token.push(Arc::new(addon)) 56 | } 57 | 58 | /// Add an addon that only applies to client_credentials. 59 | pub fn push_client_credentials(&mut self, addon: A) 60 | where 61 | A: ClientCredentialsAddon + Send + Sync + 'static, 62 | { 63 | self.client_credentials.push(Arc::new(addon)) 64 | } 65 | 66 | /// Add an addon that applies to the whole code grant flow. 67 | /// 68 | /// The addon gets added both the authorization and access token addons. 69 | pub fn push_code(&mut self, addon: A) 70 | where 71 | A: AuthorizationAddon + AccessTokenAddon + Send + Sync + 'static, 72 | { 73 | let arc = Arc::new(addon); 74 | self.authorization.push(arc.clone()); 75 | self.access_token.push(arc) 76 | } 77 | } 78 | 79 | impl Default for AddonList { 80 | fn default() -> Self { 81 | AddonList::new() 82 | } 83 | } 84 | 85 | impl Extension for AddonList { 86 | fn authorization(&mut self) -> Option<&mut dyn AuthorizationExtension> { 87 | Some(self) 88 | } 89 | 90 | fn access_token(&mut self) -> Option<&mut dyn AccessTokenExtension> { 91 | Some(self) 92 | } 93 | 94 | fn client_credentials(&mut self) -> Option<&mut dyn ClientCredentialsExtension> { 95 | Some(self) 96 | } 97 | } 98 | 99 | impl Extension for &mut AddonList { 100 | fn authorization(&mut self) -> Option<&mut dyn AuthorizationExtension> { 101 | Some(self) 102 | } 103 | 104 | fn access_token(&mut self) -> Option<&mut dyn AccessTokenExtension> { 105 | Some(self) 106 | } 107 | 108 | fn client_credentials(&mut self) -> Option<&mut dyn ClientCredentialsExtension> { 109 | Some(self) 110 | } 111 | } 112 | 113 | impl AccessTokenExtension for AddonList { 114 | fn extend( 115 | &mut self, request: &dyn Request, mut data: Extensions, 116 | ) -> std::result::Result { 117 | let mut result_data = Extensions::new(); 118 | 119 | for ext in self.access_token.iter() { 120 | let ext_data = data.remove(ext); 121 | let result = ext.execute(request, ext_data); 122 | 123 | match result { 124 | AddonResult::Ok => (), 125 | AddonResult::Data(data) => result_data.set(ext, data), 126 | AddonResult::Err => return Err(()), 127 | } 128 | } 129 | 130 | Ok(result_data) 131 | } 132 | } 133 | 134 | impl AccessTokenExtension for &mut AddonList { 135 | fn extend(&mut self, request: &dyn Request, data: Extensions) -> Result { 136 | AccessTokenExtension::extend(*self, request, data) 137 | } 138 | } 139 | 140 | impl AuthorizationExtension for AddonList { 141 | fn extend(&mut self, request: &dyn AuthRequest) -> Result { 142 | let mut result_data = Extensions::new(); 143 | 144 | for ext in self.authorization.iter() { 145 | let result = ext.execute(request); 146 | 147 | match result { 148 | AddonResult::Ok => (), 149 | AddonResult::Data(data) => result_data.set(ext, data), 150 | AddonResult::Err => return Err(()), 151 | } 152 | } 153 | 154 | Ok(result_data) 155 | } 156 | } 157 | 158 | impl AuthorizationExtension for &mut AddonList { 159 | fn extend(&mut self, request: &dyn AuthRequest) -> Result { 160 | AuthorizationExtension::extend(*self, request) 161 | } 162 | } 163 | 164 | impl ClientCredentialsExtension for AddonList { 165 | fn extend(&mut self, request: &dyn ClientCredentialsRequest) -> Result { 166 | let mut result_data = Extensions::new(); 167 | 168 | for ext in self.client_credentials.iter() { 169 | let result = ext.execute(request); 170 | 171 | match result { 172 | AddonResult::Ok => (), 173 | AddonResult::Data(data) => result_data.set(ext, data), 174 | AddonResult::Err => return Err(()), 175 | } 176 | } 177 | 178 | Ok(result_data) 179 | } 180 | } 181 | 182 | impl ClientCredentialsExtension for &mut AddonList { 183 | fn extend(&mut self, request: &dyn ClientCredentialsRequest) -> Result { 184 | ClientCredentialsExtension::extend(*self, request) 185 | } 186 | } 187 | 188 | impl fmt::Debug for AddonList { 189 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 190 | use std::slice::Iter; 191 | struct ExtIter<'a, T: GrantExtension + 'a>(Iter<'a, T>); 192 | 193 | impl<'a, T: GrantExtension> fmt::Debug for ExtIter<'a, T> { 194 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 195 | f.debug_list().entries(self.0.clone().map(T::identifier)).finish() 196 | } 197 | } 198 | 199 | f.debug_struct("AddonList") 200 | .field("authorization", &ExtIter(self.authorization.iter())) 201 | .field("access_token", &ExtIter(self.access_token.iter())) 202 | .field("client_credentials", &ExtIter(self.client_credentials.iter())) 203 | .finish() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/simple/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Basic extension systems. 2 | //! 3 | //! Note that extensions will probably return in `v0.4` but not its preview versions. 4 | pub use crate::code_grant::authorization::Request as AuthorizationRequest; 5 | pub use crate::code_grant::accesstoken::Request as AccessTokenRequest; 6 | pub use crate::code_grant::client_credentials::Request as ClientCredentialsRequest; 7 | 8 | mod extended; 9 | mod pkce; 10 | mod list; 11 | 12 | use std::borrow::{Cow, ToOwned}; 13 | use std::rc::Rc; 14 | use std::sync::Arc; 15 | 16 | pub use self::extended::Extended; 17 | pub use self::pkce::Pkce; 18 | pub use self::list::AddonList; 19 | use crate::primitives::grant::{GrantExtension, Value}; 20 | 21 | /// Result of extension processing. 22 | #[must_use = "This type is similar to std::result::Result and should not be ignored."] 23 | pub enum AddonResult { 24 | /// Allow the request unchanged. 25 | Ok, 26 | 27 | /// Allow the request and attach additional data to the response. 28 | Data(Value), 29 | 30 | /// Do not permit the request. 31 | Err, 32 | } 33 | 34 | /// An extension reacting to an initial authorization code request. 35 | pub trait AuthorizationAddon: GrantExtension { 36 | /// Provides data for this request or signals faulty data. 37 | /// 38 | /// There may be two main types of extensions: 39 | /// - Extensions storing additional information about the client 40 | /// - Validators asserting additional requirements 41 | /// 42 | /// Derived information which needs to be bound to the returned grant can be stored in an 43 | /// encoded form by returning `Ok(extension_data)` while errors can be signaled via `Err(())`. 44 | /// Extensions can also store their pure existance by initializing the extension struct without 45 | /// data. Specifically, the data can be used in a corresponding `AccessTokenExtension`. 46 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult; 47 | } 48 | 49 | /// An extension reacting to an access token request with a provided access token. 50 | pub trait AccessTokenAddon: GrantExtension { 51 | /// Process an access token request, utilizing the extensions stored data if any. 52 | /// 53 | /// The semantics are equivalent to that of `CodeExtension` except that any data which was 54 | /// returned as a response to the authorization code request is provided as an additional 55 | /// parameter. 56 | fn execute(&self, request: &dyn AccessTokenRequest, code_data: Option) -> AddonResult; 57 | } 58 | 59 | /// An extension reacting to a client credentials request.. 60 | pub trait ClientCredentialsAddon: GrantExtension { 61 | /// Process a client credentials request, utilizing the extensions stored data if any. 62 | fn execute(&self, request: &dyn ClientCredentialsRequest) -> AddonResult; 63 | } 64 | 65 | impl<'a, T: AuthorizationAddon + ?Sized> AuthorizationAddon for &'a T { 66 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult { 67 | (**self).execute(request) 68 | } 69 | } 70 | 71 | impl<'a, T: AuthorizationAddon + ?Sized> AuthorizationAddon for Cow<'a, T> 72 | where 73 | T: Clone + ToOwned, 74 | { 75 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult { 76 | self.as_ref().execute(request) 77 | } 78 | } 79 | 80 | impl AuthorizationAddon for Box { 81 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult { 82 | (**self).execute(request) 83 | } 84 | } 85 | 86 | impl AuthorizationAddon for Arc { 87 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult { 88 | (**self).execute(request) 89 | } 90 | } 91 | 92 | impl AuthorizationAddon for Rc { 93 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult { 94 | (**self).execute(request) 95 | } 96 | } 97 | 98 | impl<'a, T: AccessTokenAddon + ?Sized> AccessTokenAddon for &'a T { 99 | fn execute(&self, request: &dyn AccessTokenRequest, data: Option) -> AddonResult { 100 | (**self).execute(request, data) 101 | } 102 | } 103 | 104 | impl<'a, T: AccessTokenAddon + ?Sized> AccessTokenAddon for Cow<'a, T> 105 | where 106 | T: Clone + ToOwned, 107 | { 108 | fn execute(&self, request: &dyn AccessTokenRequest, data: Option) -> AddonResult { 109 | self.as_ref().execute(request, data) 110 | } 111 | } 112 | 113 | impl AccessTokenAddon for Box { 114 | fn execute(&self, request: &dyn AccessTokenRequest, data: Option) -> AddonResult { 115 | (**self).execute(request, data) 116 | } 117 | } 118 | 119 | impl AccessTokenAddon for Arc { 120 | fn execute(&self, request: &dyn AccessTokenRequest, data: Option) -> AddonResult { 121 | (**self).execute(request, data) 122 | } 123 | } 124 | 125 | impl AccessTokenAddon for Rc { 126 | fn execute(&self, request: &dyn AccessTokenRequest, data: Option) -> AddonResult { 127 | (**self).execute(request, data) 128 | } 129 | } 130 | 131 | impl<'a, T: ClientCredentialsAddon + ?Sized> ClientCredentialsAddon for &'a T { 132 | fn execute(&self, request: &dyn ClientCredentialsRequest) -> AddonResult { 133 | (**self).execute(request) 134 | } 135 | } 136 | 137 | impl<'a, T: ClientCredentialsAddon + ?Sized> ClientCredentialsAddon for Cow<'a, T> 138 | where 139 | T: Clone + ToOwned, 140 | { 141 | fn execute(&self, request: &dyn ClientCredentialsRequest) -> AddonResult { 142 | self.as_ref().execute(request) 143 | } 144 | } 145 | 146 | impl ClientCredentialsAddon for Box { 147 | fn execute(&self, request: &dyn ClientCredentialsRequest) -> AddonResult { 148 | (**self).execute(request) 149 | } 150 | } 151 | 152 | impl ClientCredentialsAddon for Arc { 153 | fn execute(&self, request: &dyn ClientCredentialsRequest) -> AddonResult { 154 | (**self).execute(request) 155 | } 156 | } 157 | 158 | impl ClientCredentialsAddon for Rc { 159 | fn execute(&self, request: &dyn ClientCredentialsRequest) -> AddonResult { 160 | (**self).execute(request) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/simple/extensions/pkce.rs: -------------------------------------------------------------------------------- 1 | use super::{AuthorizationAddon, AuthorizationRequest, AccessTokenAddon, AccessTokenRequest}; 2 | use super::{AddonResult, Value}; 3 | 4 | pub use crate::code_grant::extensions::Pkce; 5 | 6 | impl AuthorizationAddon for Pkce { 7 | fn execute(&self, request: &dyn AuthorizationRequest) -> AddonResult { 8 | let method = request.extension("code_challenge_method"); 9 | let challenge = request.extension("code_challenge"); 10 | 11 | let encoded = match self.challenge(method, challenge) { 12 | Err(()) => return AddonResult::Err, 13 | Ok(None) => return AddonResult::Ok, 14 | Ok(Some(encoded)) => encoded, 15 | }; 16 | 17 | AddonResult::Data(encoded) 18 | } 19 | } 20 | 21 | impl AccessTokenAddon for Pkce { 22 | fn execute(&self, request: &dyn AccessTokenRequest, data: Option) -> AddonResult { 23 | let verifier = request.extension("code_verifier"); 24 | 25 | match self.verify(data, verifier) { 26 | Ok(_) => AddonResult::Ok, 27 | Err(_) => AddonResult::Err, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /oxide-auth/src/frontends/simple/mod.rs: -------------------------------------------------------------------------------- 1 | //! A baseline implemention of [`Endpoint`] and [`WebRequest`]. 2 | //! 3 | //! Contains primitive extension implementations as well as straightforward request formats 4 | //! suitable for HTTP-less OAuth applications. This is useful for testing as well as token 5 | //! endpoints that operate behind an HTTP portal, or even for applying OAuth2 outside the web 6 | //! domain. 7 | //! 8 | //! [`Endpoint`]: ../../endpoint/trait.Endpoint.html 9 | //! [`WebRequest`]: ../../endpoint/trait.Endpoint.html 10 | pub mod endpoint; 11 | 12 | pub mod extensions; 13 | 14 | pub mod request; 15 | -------------------------------------------------------------------------------- /oxide-auth/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # oxide-auth 2 | //! 3 | //! An OAuth2 server library, for use in combination with actix-web or other front-ends, featuring a 4 | //! set of configurable and pluggable back-ends. 5 | //! 6 | //! ## About 7 | //! 8 | //! `oxide-auth` aims at providing a comprehensive and extensible interface to managing OAuth2 9 | //! tokens on a server. This depends on both a front-end facing web server for network operations 10 | //! and a back-end implementation for policies and data storage. The main interface is designed 11 | //! around traits in both directions, so that the front-end is as easily pluggable as the back-end. 12 | //! There are many adaptations for specific web server crates (`actix`, `rocket`, `iron`, 13 | //! `rouille`) in associated crates 14 | //! 15 | //! ## Create a web server with OAuth security 16 | //! 17 | //! So you want to build a new OAuth provider? Instead of only relying on tokens provided by other 18 | //! large internet entities, you want to make your own tokens? Examples of this use case: A web 19 | //! facing data portal, automation endpoints (in the style of Reddit or Discord), or even to 20 | //! restrict the authorization of different components of your own software by applying these 21 | //! techniques to your `REST`/`GraphQL`/.. back-end. 22 | //! 23 | //! Choose one of the available adaptor crates, a complete list can be found in the [`frontends`] 24 | //! module, or translate the HTTP to the generic software endpoint found in [`frontends::simple`]. 25 | //! 26 | //! Next, a set of [`primitives`] needs to be chosen. These will depend on the policies need for 27 | //! Your use case but will in general encompass a [`Registrar`], an [`Authorizer`], and an 28 | //! [`Issuer`]. There is a simple, in-memory implementation provided for each of those. More 29 | //! complex solutions might require a customized trait implementation especially when specific 30 | //! cryptographic standards or consistency requirements are needed. (It would be appreciated if 31 | //! those were shared with the community as an open-source project, for example as a complementary 32 | //! crate, but not mandatory). 33 | //! 34 | //! And finally, an implementation of an [`Endpoint`] is required, handling the request type that 35 | //! has been chosen and forwarding it to the primitives. In very simple cases this can be an 36 | //! instantiation of the [`Generic`] struct. But for most complex cases it should instead be a 37 | //! custom trait implementation that is tailored to Your specific requirements. Besides the 38 | //! previously chosen primitives, the endpoint require You to choose two more interface: An 39 | //! [`OwnerSolicitor`] to interact with Your session handling, user consent, and CSRF protection; 40 | //! and the [`Scopes`] deciding required permissions for a request. 41 | //! 42 | //! ## Custom Front-Ends 43 | //! 44 | //! A key feature is the ability to add your own front-end without jeopardizing safety 45 | //! requirements. For example to add your in-house server and request representation! This requires 46 | //! custom, related implementations of [`WebRequest`] and [`WebResponse`]. In theory, you are not 47 | //! even restricted to HTTP as long as the parameters can be transmitted safely. _WARNING_: Custom 48 | //! front-ends MUST ensure a secure transportation layer with confidential clients. This means 49 | //! using TLS for communication over HTTPS. 50 | //! 51 | //! For more information, see the documentation of [`endpoint`] and [`frontends`]. 52 | //! 53 | //! [`WebRequest`]: code_grant/frontend/trait.WebRequest.html 54 | //! [`WebResponse`]: code_grant/frontend/trait.WebResponse.html 55 | //! [`endpoint`]: endpoint/index.html 56 | //! [`Endpoint`]: endpoint/trait.Endpoint.html 57 | //! [`frontends`]: frontends/index.html 58 | //! [`frontends::simple`]: frontends/simple/index.html 59 | //! [`Generic`]: frontends/simple/endpoint/struct.Generic.html 60 | //! [`primitives`]: primitives/index.html 61 | //! [`Registrar`]: primitives/registrar/trait.Registrar.html 62 | //! [`Authorizer`]: primitives/authorizer/trait.Authorizer.html 63 | //! [`Issuer`]: primitives/issuer/trait.Issuer.html 64 | //! [`OwnerSolicitor`]: endpoint/trait.OwnerSolicitor.html 65 | //! [`Scopes`]: endpoint/trait.Scopes.html 66 | #![warn(missing_docs)] 67 | 68 | pub mod code_grant; 69 | pub mod endpoint; 70 | pub mod frontends; 71 | pub mod primitives; 72 | -------------------------------------------------------------------------------- /oxide-auth/src/primitives/authorizer.rs: -------------------------------------------------------------------------------- 1 | //! Authorizers are need to exchange code grants for bearer tokens. 2 | //! 3 | //! The role of an authorizer is the ensure the consistency and security of request in which a 4 | //! client is willing to trade a code grant for a bearer token. As such, it will first issue grants 5 | //! to client according to parameters given by the resource owner and the registrar. Upon a client 6 | //! side request, it will then check the given parameters to determine the authorization of such 7 | //! clients. 8 | use std::collections::HashMap; 9 | use std::sync::{MutexGuard, RwLockWriteGuard}; 10 | 11 | use super::grant::Grant; 12 | use super::generator::TagGrant; 13 | 14 | /// Authorizers create and manage authorization codes. 15 | /// 16 | /// The authorization code can be traded for a bearer token at the token endpoint. 17 | pub trait Authorizer { 18 | /// Create a code which allows retrieval of a bearer token at a later time. 19 | fn authorize(&mut self, _: Grant) -> Result; 20 | 21 | /// Retrieve the parameters associated with a token, invalidating the code in the process. In 22 | /// particular, a code should not be usable twice (there is no stateless implementation of an 23 | /// authorizer for this reason). 24 | fn extract(&mut self, token: &str) -> Result, ()>; 25 | } 26 | 27 | /// An in-memory hash map. 28 | /// 29 | /// This authorizer saves a mapping of generated strings to their associated grants. The generator 30 | /// is itself trait based and can be chosen during construction. It is assumed to not be possible 31 | /// for two different grants to generate the same token in the issuer. 32 | pub struct AuthMap> { 33 | tagger: I, 34 | usage: u64, 35 | tokens: HashMap, 36 | } 37 | 38 | impl AuthMap { 39 | /// Create an authorizer generating tokens with the `tagger`. 40 | /// 41 | /// The token map is initially empty and is filled by methods provided in its [`Authorizer`] 42 | /// implementation. 43 | /// 44 | /// [`Authorizer`]: ./trait.Authorizer.html 45 | pub fn new(tagger: I) -> Self { 46 | AuthMap { 47 | tagger, 48 | usage: 0, 49 | tokens: HashMap::new(), 50 | } 51 | } 52 | } 53 | 54 | impl<'a, A: Authorizer + ?Sized> Authorizer for &'a mut A { 55 | fn authorize(&mut self, grant: Grant) -> Result { 56 | (**self).authorize(grant) 57 | } 58 | 59 | fn extract(&mut self, code: &str) -> Result, ()> { 60 | (**self).extract(code) 61 | } 62 | } 63 | 64 | impl Authorizer for Box { 65 | fn authorize(&mut self, grant: Grant) -> Result { 66 | (**self).authorize(grant) 67 | } 68 | 69 | fn extract(&mut self, code: &str) -> Result, ()> { 70 | (**self).extract(code) 71 | } 72 | } 73 | 74 | impl<'a, A: Authorizer + ?Sized> Authorizer for MutexGuard<'a, A> { 75 | fn authorize(&mut self, grant: Grant) -> Result { 76 | (**self).authorize(grant) 77 | } 78 | 79 | fn extract(&mut self, code: &str) -> Result, ()> { 80 | (**self).extract(code) 81 | } 82 | } 83 | 84 | impl<'a, A: Authorizer + ?Sized> Authorizer for RwLockWriteGuard<'a, A> { 85 | fn authorize(&mut self, grant: Grant) -> Result { 86 | (**self).authorize(grant) 87 | } 88 | 89 | fn extract(&mut self, code: &str) -> Result, ()> { 90 | (**self).extract(code) 91 | } 92 | } 93 | 94 | impl Authorizer for AuthMap { 95 | fn authorize(&mut self, grant: Grant) -> Result { 96 | // The (usage, grant) tuple needs to be unique. Since this wraps after 2^64 operations, we 97 | // expect the validity time of the grant to have changed by then. This works when you don't 98 | // set your system time forward/backward ~20billion seconds, assuming ~10^9 operations per 99 | // second. 100 | let next_usage = self.usage.wrapping_add(1); 101 | let token = self.tagger.tag(next_usage - 1, &grant)?; 102 | self.tokens.insert(token.clone(), grant); 103 | self.usage = next_usage; 104 | Ok(token) 105 | } 106 | 107 | fn extract<'a>(&mut self, grant: &'a str) -> Result, ()> { 108 | Ok(self.tokens.remove(grant)) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | /// Tests for authorizer implementations, including those provided here. 114 | pub mod tests { 115 | use super::*; 116 | use chrono::Utc; 117 | use crate::primitives::grant::Extensions; 118 | use crate::primitives::generator::{Assertion, AssertionKind, RandomGenerator}; 119 | 120 | /// Tests some invariants that should be upheld by all authorizers. 121 | /// 122 | /// Custom implementations may want to import and use this in their own tests. 123 | pub fn simple_test_suite(authorizer: &mut dyn Authorizer) { 124 | let grant = Grant { 125 | owner_id: "Owner".to_string(), 126 | client_id: "Client".to_string(), 127 | scope: "One two three scopes".parse().unwrap(), 128 | redirect_uri: "https://example.com/redirect_me".parse().unwrap(), 129 | until: Utc::now(), 130 | extensions: Extensions::new(), 131 | }; 132 | 133 | let token = authorizer 134 | .authorize(grant.clone()) 135 | .expect("Authorization should not fail here"); 136 | let recovered_grant = authorizer 137 | .extract(&token) 138 | .expect("Primitive failed extracting grant") 139 | .expect("Could not extract grant for valid token"); 140 | 141 | if grant != recovered_grant { 142 | panic!("Grant was not stored correctly"); 143 | } 144 | 145 | if authorizer.extract(&token).unwrap().is_some() { 146 | panic!("Token must only be usable once"); 147 | } 148 | 149 | // Authorize the same token again. 150 | let token_again = authorizer 151 | .authorize(grant.clone()) 152 | .expect("Authorization should not fail here"); 153 | // We don't produce the same token twice. 154 | assert_ne!(token, token_again); 155 | } 156 | 157 | #[test] 158 | fn random_test_suite() { 159 | let mut storage = AuthMap::new(RandomGenerator::new(16)); 160 | simple_test_suite(&mut storage); 161 | } 162 | 163 | #[test] 164 | fn signing_test_suite() { 165 | let assertion = Assertion::new( 166 | AssertionKind::HmacSha256, 167 | b"7EGgy8zManReq9l/ez0AyYE+xPpcTbssgW+8gBnIv3s=", 168 | ); 169 | let mut storage = AuthMap::new(assertion); 170 | simple_test_suite(&mut storage); 171 | } 172 | 173 | #[test] 174 | #[should_panic] 175 | fn bad_generator() { 176 | struct BadGenerator; 177 | impl TagGrant for BadGenerator { 178 | fn tag(&mut self, _: u64, _: &Grant) -> Result { 179 | Ok("YOLO.HowBadCanItBeToRepeatTokens?".into()) 180 | } 181 | } 182 | 183 | let mut storage = AuthMap::new(BadGenerator); 184 | simple_test_suite(&mut storage); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /oxide-auth/src/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | //! A collection of primites useful for more than one authorization method. 2 | //! 3 | //! A primitive is the smallest independent unit of policy used in OAuth related endpoints. For 4 | //! example, an `authorizer` generates and verifies Authorization Codes. There only is, as you 5 | //! might have noticed, only the OAuth2 code grant method. But abstracting away the underlying 6 | //! primitives makes it possible to provide –e.g.– a independent database based implementation. 7 | //! 8 | //! These should be used to build or instantiate an `Endpoint`, for example [`Generic`] or your 9 | //! own. 10 | //! 11 | //! ``` 12 | //! # extern crate oxide_auth; 13 | //! # use oxide_auth::frontends::simple::endpoint::Vacant; 14 | //! use oxide_auth::frontends::simple::endpoint::Generic; 15 | //! use oxide_auth::primitives::{ 16 | //! authorizer::AuthMap, 17 | //! generator::RandomGenerator, 18 | //! issuer::TokenMap, 19 | //! registrar::ClientMap, 20 | //! }; 21 | //! 22 | //! Generic { 23 | //! authorizer: AuthMap::new(RandomGenerator::new(16)), 24 | //! registrar: ClientMap::new(), 25 | //! issuer: TokenMap::new(RandomGenerator::new(16)), 26 | //! // ... 27 | //! # scopes: Vacant, 28 | //! # solicitor: Vacant, 29 | //! # response: Vacant, 30 | //! }; 31 | //! ``` 32 | //! 33 | //! [`Generic`]: ../frontends/simple/endpoint/struct.Generic.html 34 | 35 | use chrono::DateTime; 36 | use chrono::Utc; 37 | use url::Url; 38 | 39 | pub mod authorizer; 40 | pub mod generator; 41 | pub mod grant; 42 | pub mod issuer; 43 | pub mod registrar; 44 | pub mod scope; 45 | 46 | type Time = DateTime; 47 | 48 | /// Commonly used primitives for frontends and backends. 49 | pub mod prelude { 50 | pub use super::authorizer::{Authorizer, AuthMap}; 51 | pub use super::issuer::{IssuedToken, Issuer, TokenMap, TokenSigner}; 52 | pub use super::generator::{Assertion, TagGrant, RandomGenerator}; 53 | pub use super::registrar::{Registrar, Client, ClientUrl, ClientMap, PreGrant}; 54 | pub use super::scope::Scope; 55 | } 56 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | determine_new_version() { 3 | grep "version = " "$1" | sed -Ee 's/version = "(.*)"/\1/' | head -1 4 | } 5 | 6 | check_notexists_version() { 7 | cirrus_agent="${CIRRUS_CI/?*/(Cirrus-CI)}" 8 | final_agent="Release-Script/1.0 ${cirrus_agent:-(local)} (for static-alloc) (author:HeroicKatora)" 9 | echo $final_agent 10 | # Does the api information start with: '{"errors":' 11 | [[ $(wget -U "%final_agent" "https://crates.io/api/v1/crates/oxide-auth/$new_version" -qO -) == "{\"errors\":"* ]] 12 | } 13 | 14 | git_considered_clean() { 15 | [[ -z $(git status -s) ]] 16 | } 17 | 18 | count_wip_marker() { 19 | # WIP alone is not a marker 20 | [[ -z $(grep "\[WIP\]" Migration.md Readme.md) ]] 21 | } 22 | 23 | check_release_changes() { 24 | [[ -z $(grep "# v$new_version" $1) ]] 25 | } 26 | 27 | check_target_features() { 28 | if [[ -z $CIRRUS_CI ]]; then 29 | cargo test -p oxide-auth-actix && 30 | cargo +nightly test -p oxide-auth-rocket && 31 | cargo test -p oxide-auth-iron && 32 | cargo test -p oxide-auth-rouille 33 | else 34 | echo "Executing on CI, skipping feature checks" 35 | fi 36 | } 37 | 38 | check_packaging() { 39 | pushd $1 && cargo package && popd 40 | } 41 | 42 | make_git_tag() { 43 | # Extract the verion specific section from Changes.md 44 | # Delete lines until $new_version header 45 | # Delete lines starting from the next header 46 | # Delete all empty lines at the start 47 | # Use as the initial message for a signed tag, but open edit anyways 48 | sed -e '1,/'"$new_version"'/d;/\#/,$d;/./,$!d' oxide-auth/Changes.md | git tag -s $is_force -F - -e "v$new_version" 49 | } 50 | 51 | is_force="" 52 | do_tag="" 53 | do_version_check="yes" 54 | 55 | for param in $@ 56 | do 57 | case "$param" in 58 | -f) is_force="-f";; 59 | --no-version-check) do_version_check="";; 60 | --tag) do_tag="yes";; 61 | --help) ;& 62 | -h) { cat << EOF 63 | usage: release [-f] [-h|--help] 64 | 65 | Automates checks and tagging of new releases. Encourages a workflow where 66 | planned changes are integrated into readme and migration documentation early, 67 | with WIP markers to help produce complete logs. 68 | 69 | -f Force usage of version, even if such a tag already exists. 70 | -h, --help Display this help 71 | A semantic version number matching [0-9a-zA-Z.-]* 72 | 73 | EOF 74 | exit 1; } ;; 75 | esac 76 | done 77 | 78 | # Check that the working dir is clean. May comment this out if it produces problems. 79 | git_considered_clean || { echo "Fail: Working directory is not clean"; exit 1; } 80 | 81 | new_version="$(determine_new_version "oxide-auth/Cargo.toml")" 82 | 83 | # check it is a sane version number 84 | [[ -z $(grep -vE '[0-9a-zA-Z.-]*' <<< "$new_version" ) ]] || { echo "Fail: Check version number: ${new_version}"; exit 1; } 85 | 86 | [[ -z $version_check ]] || check_notexists_version || { echo "Version $new_version appears already published"; exit 1; } 87 | 88 | # Check there are no more [WIP] markers in Migrate and Readme 89 | count_wip_marker || { echo "Fail: Work in progress in documentation"; exit 1; } 90 | 91 | # Find a matching header in the changelog 92 | check_release_changes "oxide-auth/Changes.md" && { echo "Fail: No changelog regarding this release"; exit 1; } 93 | 94 | # Packaging works. Note: does not publish the version. 95 | check_packaging oxide-auth || { echo "Fail: cargo could not package successfully"; exit 1; } 96 | 97 | check_target_features || { echo "Fail: one or more required target-feature combinations doesnt compile its example properly"; exit 1; } 98 | 99 | [[ -z $do_tag ]] || make_git_tag 100 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = false 2 | fn_params_layout = "Compressed" 3 | reorder_modules = false 4 | max_width = 105 5 | --------------------------------------------------------------------------------