Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request guards #56

Merged
merged 6 commits into from Sep 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -7,4 +7,5 @@ members = [
"examples/custom_schema",
"examples/uuid",
"examples/special-types",
"examples/secure_request_guard"
]
4 changes: 4 additions & 0 deletions examples/secure_request_guard/.gitignore
@@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
/.idea
14 changes: 14 additions & 0 deletions examples/secure_request_guard/Cargo.toml
@@ -0,0 +1,14 @@
[package]
name = "secure_request_guard"
version = "0.1.0"
authors = ["Kristoffer Ödmark <kristoffer.odmark90@gmail.com>", "Ralph Bisschops <ralph.bisschops.dev@gmail.com>"]
edition = "2018"

[dependencies]
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json", "secrets"] }
schemars = { version = "0.8"}
okapi = { version = "0.6.0-alpha-1", path = "../../okapi" }
rocket_okapi = { version = "0.8.0-alpha-1", path = "../../rocket-okapi", features = ["rapidoc", "swagger", "secrets"] }
serde = "1.0"
tokio = "1.6"
serde_json = "1.0"
8 changes: 8 additions & 0 deletions examples/secure_request_guard/README.md
@@ -0,0 +1,8 @@
# Secure Request Guard

A simple web API written using [Rocket](https://rocket.rs/) including openapi. It contains one route that needs the correct key provided
to allow access. This example implements the security called by an api key that needs to be included
in the header from the following: [apikeys] (https://swagger.io/docs/specification/authentication/api-keys/)

the key is "hello"

106 changes: 106 additions & 0 deletions examples/secure_request_guard/src/api_key.rs
@@ -0,0 +1,106 @@
//! ------ ApiKey (in http header, query or cookie) ------
use okapi::openapi3::{Object, Responses, SecurityRequirement, SecurityScheme, SecuritySchemeData};
use rocket::serde::json::Json;
use rocket::{
get,
http::Status,
request::{self, FromRequest, Outcome},
};
use rocket_okapi::{
gen::OpenApiGenerator,
openapi,
request::{OpenApiFromRequest, RequestHeaderInput},
};

pub struct ApiKey(String);

// Implement the actual checks for the authentication
#[rocket::async_trait]
impl<'a> FromRequest<'a> for ApiKey {
type Error = &'static str;
async fn from_request(
request: &'a request::Request<'_>,
) -> request::Outcome<Self, Self::Error> {
// Get the key from the http header
match request.headers().get_one("x-api-key") {
Some(key) => {
if key == "mykey" {
Outcome::Success(ApiKey(key.to_owned()))
} else {
Outcome::Failure((Status::Unauthorized, "Api key is invalid."))
}
}
None => Outcome::Failure((Status::BadRequest, "Missing `x-api-key` header.")),
}
// For more info see: https://rocket.rs/v0.5-rc/guide/state/#within-guards
}
}

impl<'a> OpenApiFromRequest<'a> for ApiKey {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
// Setup global requirement for Security scheme
let security_scheme = SecurityScheme {
description: Some("Requires an API key to access, key is: `mykey`.".to_owned()),
// Setup data requirements.
// This can be part of the `header`, `query` or `cookie`.
// In this case the header `x-api-key: mykey` needs to be set.
data: SecuritySchemeData::ApiKey {
name: "x-api-key".to_owned(),
location: "header".to_owned(),
},
extensions: Object::default(),
};
// Add the requirement for this route/endpoint
// This can change between routes.
let mut security_req = SecurityRequirement::new();
// Each security requirement needs to be met before access is allowed.
security_req.insert("ApiKeyAuth".to_owned(), Vec::new());
// These vvvvvvv-----^^^^^^^^^^ values need to match exactly!
Ok(RequestHeaderInput::Security(
"ApiKeyAuth".to_owned(),
security_scheme,
security_req,
))
}

// Optionally add responses
// Also see `main.rs` part of this.
fn get_responses(gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
let mut responses = Responses::default();
// It can return a "400 BadRequest" and a "401 Unauthorized"
// In both cases we just return a what we have set in the catches (if any).
// In our cases this is: `crate::MyError`
let schema = gen.json_schema::<crate::MyError>();
// Add "400 BadRequest"
rocket_okapi::util::add_schema_response(
&mut responses,
400,
"application/json",
schema.clone(),
)?;
// Add "401 Unauthorized"
rocket_okapi::util::add_schema_response(&mut responses, 401, "application/json", schema)?;
Ok(responses)
}
}

/// # ApiKey (in http header, query or cookie)
///
/// The key is: `mykey`
/// This is a common way of checking the authentication.
/// (make sure this is only sent over HTTPS, don't want secrets to leak)
///
/// Using `query` is not recommended for secrets!
/// For more info see:
/// https://owasp.org/www-community/vulnerabilities/Information_exposure_through_query_strings_in_url
#[openapi]
#[get("/apikey")]
pub fn api_key(key: ApiKey) -> Json<&'static str> {
// Use api key
let _seems_you_have_access = key;
Json("You got access")
}
72 changes: 72 additions & 0 deletions examples/secure_request_guard/src/cookies.rs
@@ -0,0 +1,72 @@
//! ------ Just Cookies (for just 1 route/endpoint) ------

use okapi::openapi3::{Object, Parameter, ParameterValue};
use rocket::outcome::IntoOutcome;
use rocket::serde::json::Json;
use rocket::{
get,
request::{self, FromRequest},
};
use rocket_okapi::{
gen::OpenApiGenerator,
openapi,
request::{OpenApiFromRequest, RequestHeaderInput},
};

pub struct CookieAuth(String);

// Implement the actual checks for the authentication
#[rocket::async_trait]
impl<'a> FromRequest<'a> for CookieAuth {
type Error = &'static str;
async fn from_request(
request: &'a request::Request<'_>,
) -> request::Outcome<Self, Self::Error> {
request
.cookies()
.get_private("user_id") // Requires "secrets" feature flag
.and_then(|cookie| cookie.value().parse().ok())
.map(CookieAuth)
.or_forward(())
}
}

impl<'a> OpenApiFromRequest<'a> for CookieAuth {
fn from_request_input(
gen: &mut OpenApiGenerator,
_name: String,
required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
let schema = gen.json_schema::<String>();
Ok(RequestHeaderInput::Parameter(Parameter {
name: "user_id".to_owned(),
location: "cookie".to_owned(),
description: None,
required,
deprecated: false,
allow_empty_value: false,
value: ParameterValue::Schema {
style: None,
explode: None,
allow_reserved: false,
schema,
example: None,
examples: None,
},
extensions: Object::default(),
}))
}
}

/// # Just Cookies (for just 1 route/endpoint)
///
/// (make sure this is only sent over HTTPS, don't want secrets to leak)
///
/// Note: Cookies will not work with the `Try` button because of
/// [Technical limitations](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name).
#[openapi]
#[get("/cookie_auth")]
pub fn cookie_auth(user: CookieAuth) -> Json<&'static str> {
let _seems_you_have_access = user;
Json("You got access")
}
85 changes: 85 additions & 0 deletions examples/secure_request_guard/src/http_auth.rs
@@ -0,0 +1,85 @@
//! ------ HTTP `Authorization` header ------

use okapi::openapi3::{Object, SecurityRequirement, SecurityScheme, SecuritySchemeData};
use rocket::serde::json::Json;
use rocket::{
get,
http::Status,
request::{self, FromRequest, Outcome},
};
use rocket_okapi::{
gen::OpenApiGenerator,
openapi,
request::{OpenApiFromRequest, RequestHeaderInput},
};

pub struct HttpAuth(String);

// Implement the actual checks for the authentication
#[rocket::async_trait]
impl<'a> FromRequest<'a> for HttpAuth {
type Error = &'static str;
async fn from_request(
request: &'a request::Request<'_>,
) -> request::Outcome<Self, Self::Error> {
// Get the token from the http header
match request.headers().get_one("Authorization") {
Some(token) => {
if token == "Bearer mytoken" {
Outcome::Success(HttpAuth(token.to_owned()))
} else {
Outcome::Failure((Status::Unauthorized, "Auth is invalid."))
}
}
None => Outcome::Failure((Status::BadRequest, "Missing `Authorization` header.")),
}
// For more info see: https://rocket.rs/v0.5-rc/guide/state/#within-guards
}
}

impl<'a> OpenApiFromRequest<'a> for HttpAuth {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
// Setup global requirement for Security scheme
let security_scheme = SecurityScheme {
description: Some(
"Requires an Bearer token to access, token is: `mytoken`.".to_owned(),
),
// Setup data requirements.
// In this case the header `Authorization: mytoken` needs to be set.
data: SecuritySchemeData::Http {
scheme: "bearer".to_owned(), // `basic`, `digest`, ...
// Just gives use a hint to the format used
bearer_format: Some("bearer".to_owned()),
},
extensions: Object::default(),
};
// Add the requirement for this route/endpoint
// This can change between routes.
let mut security_req = SecurityRequirement::new();
// Each security requirement needs to be met before access is allowed.
security_req.insert("HttpAuth".to_owned(), Vec::new());
// These vvvvvvv-----^^^^^^^^ values need to match exactly!
Ok(RequestHeaderInput::Security(
"HttpAuth".to_owned(),
security_scheme,
security_req,
))
}
}

/// # HTTP `Authorization` header
///
/// The token is: `mytoken`
/// This is a common way of checking the authentication.
/// (make sure this is only sent over HTTPS, don't want secrets to leak)
#[openapi]
#[get("/http_auth")]
pub fn http_auth(token: HttpAuth) -> Json<&'static str> {
// Use api key
let _seems_you_have_access = token;
Json("You got access")
}