From b6a5179fc5cfb8aa779b83c40ddb7a4073546130 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Sun, 9 Jun 2024 22:04:31 +0200 Subject: [PATCH 01/12] added rocket_okapi base --- Cargo.toml | 3 +++ src/rocket/introspection/guard.rs | 37 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index af5c289..3543fc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,8 @@ oidc = ["credentials", "dep:base64-compat"] ## Refer to the rocket module for more information. rocket = ["credentials", "oidc", "dep:rocket"] +rocket_okapi = ["rocket", "dep:rocket_okapi"] + [dependencies] actix-web = { version = "4.5.1", optional = true } async-trait = { version = "0.1.80", optional = true } @@ -83,6 +85,7 @@ tonic = { version = "0.11", features = [ "tls-roots-common", ], optional = true } tonic-types = { version = "0.11", optional = true } +rocket_okapi = { version = "0.8.0", optional = true, default-features = false } [dev-dependencies] chrono = "0.4.38" diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index e8e5fd1..8794aa6 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -3,6 +3,14 @@ use openidconnect::TokenIntrospectionResponse; use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use rocket::{async_trait, Request}; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::gen::OpenApiGenerator; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::okapi::openapi3::{SecurityScheme, SecurityRequirement, Responses}; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::okapi::Map; use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use crate::rocket::introspection::IntrospectionConfig; @@ -142,6 +150,35 @@ impl<'request> FromRequest<'request> for &'request IntrospectedUser { } } +#[cfg(feature = "rocket_okapi")] +impl<'a> OpenApiFromRequest<'a> for &'a IntrospectedUser { + fn from_request_input( + _gen: &mut OpenApiGenerator, + name: String, + required: bool, + ) -> rocket_okapi::Result { + let security_scheme = SecurityScheme { + data: rocket_okapi::okapi::openapi3::SecuritySchemeData::Http { + scheme: "bearer".to_string(), + bearer_format: None, + }, + description: Some("Bearer token for accessing the API".to_string()), + extensions: Map::new(), + }; + let mut requirement = SecurityRequirement::new(); + requirement.insert(name, vec![]); + Ok(RequestHeaderInput::Security( + "Authorization".to_string(), + security_scheme, + requirement, + )) + } + + fn get_responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { + Ok(Responses::default()) + } +} + #[cfg(test)] mod tests { #![allow(clippy::all)] From e74f5d2a8eab1580a42a73b1428f6e3a1790b1ab Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Wed, 3 Jul 2024 13:01:25 +0200 Subject: [PATCH 02/12] added openid auth for openapi --- src/rocket/introspection/guard.rs | 40 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index 8794aa6..d931749 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -4,13 +4,13 @@ use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use rocket::{async_trait, Request}; #[cfg(feature = "rocket_okapi")] -use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; -#[cfg(feature = "rocket_okapi")] use rocket_okapi::gen::OpenApiGenerator; #[cfg(feature = "rocket_okapi")] -use rocket_okapi::okapi::openapi3::{SecurityScheme, SecurityRequirement, Responses}; +use rocket_okapi::okapi::openapi3::{Responses, SecurityRequirement, SecurityScheme}; #[cfg(feature = "rocket_okapi")] use rocket_okapi::okapi::Map; +#[cfg(feature = "rocket_okapi")] +use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use crate::rocket::introspection::IntrospectionConfig; @@ -154,29 +154,33 @@ impl<'request> FromRequest<'request> for &'request IntrospectedUser { impl<'a> OpenApiFromRequest<'a> for &'a IntrospectedUser { fn from_request_input( _gen: &mut OpenApiGenerator, - name: String, - required: bool, + _name: String, + _required: bool, + request: &Request, ) -> rocket_okapi::Result { + // Setup global requirement for Security scheme let security_scheme = SecurityScheme { - data: rocket_okapi::okapi::openapi3::SecuritySchemeData::Http { - scheme: "bearer".to_string(), - bearer_format: None, + description: Some( + "Use OpenID Connect to authenticate. (does not work in RapiDoc at all)".to_owned(), + ), + data: SecuritySchemeData::OpenIdConnect { + open_id_connect_url: "https://auth.domain.com/.well-known/openid-configuration" + .to_owned(), }, - description: Some("Bearer token for accessing the API".to_string()), - extensions: Map::new(), + extensions: Object::default(), }; - let mut requirement = SecurityRequirement::new(); - requirement.insert(name, vec![]); + // 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("OpenID".to_owned(), Vec::new()); + // These vvvv-------^^^^^^^ values need to match exactly! Ok(RequestHeaderInput::Security( - "Authorization".to_string(), + "OpenID".to_owned(), security_scheme, - requirement, + security_req, )) } - - fn get_responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - Ok(Responses::default()) - } } #[cfg(test)] From 2d1e453927bcfea6cbe8d58554280251c4b6bff2 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Wed, 3 Jul 2024 16:24:53 +0200 Subject: [PATCH 03/12] removed 4th param --- src/rocket/introspection/guard.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index d931749..8afb06e 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -156,7 +156,6 @@ impl<'a> OpenApiFromRequest<'a> for &'a IntrospectedUser { _gen: &mut OpenApiGenerator, _name: String, _required: bool, - request: &Request, ) -> rocket_okapi::Result { // Setup global requirement for Security scheme let security_scheme = SecurityScheme { From af2e7b194859404bf3fe9bbb4578130b9fe2e179 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Wed, 3 Jul 2024 16:44:35 +0200 Subject: [PATCH 04/12] fixxed imports --- src/rocket/introspection/guard.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index 8afb06e..dfed5f0 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -6,7 +6,9 @@ use rocket::{async_trait, Request}; #[cfg(feature = "rocket_okapi")] use rocket_okapi::gen::OpenApiGenerator; #[cfg(feature = "rocket_okapi")] -use rocket_okapi::okapi::openapi3::{Responses, SecurityRequirement, SecurityScheme}; +use rocket_okapi::okapi::openapi3::{ + Object, Responses, SecurityRequirement, SecurityScheme, SecuritySchemeData, +}; #[cfg(feature = "rocket_okapi")] use rocket_okapi::okapi::Map; #[cfg(feature = "rocket_okapi")] From 03b7300e7ef0247292854006adb697cbcb698446 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Mon, 29 Jul 2024 09:43:08 +0200 Subject: [PATCH 05/12] chore: Update .gitignore to exclude .vscode directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 84a5eb3..05fe274 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Cargo.lock temp* src/api/generated +/.vscode From 81b78fe6ba01deb216a59f0fd6ae5d02e847b29b Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Mon, 29 Jul 2024 09:45:34 +0200 Subject: [PATCH 06/12] feat: Add support for OAuth token introspection in Rocket with Openapi - Added a new struct `IntrospectionRocketConfig` for configuring OAuth token introspection from a Rocket.toml file. --- src/rocket/introspection/config.rs | 21 +++++++++++++++++++++ src/rocket/introspection/guard.rs | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/rocket/introspection/config.rs b/src/rocket/introspection/config.rs index 476207b..04130b3 100644 --- a/src/rocket/introspection/config.rs +++ b/src/rocket/introspection/config.rs @@ -1,4 +1,5 @@ use openidconnect::IntrospectionUrl; +use serde::Deserialize; use crate::oidc::introspection::{cache::IntrospectionCache, AuthorityAuthentication}; @@ -16,3 +17,23 @@ pub struct IntrospectionConfig { #[cfg(feature = "introspection_cache")] pub(crate) cache: Option>, } + +#[cfg(feature = "rocket_okapi")] +/// Configuration for OAuth token introspection read from a Rocket.toml file. +/// +/// # Fields +/// - `authority`: A string representing the authority URL used for introspection. This is typically +/// the base URL of the OAuth provider that will validate the tokens. +/// +/// # Example +/// ```toml +/// [default] +/// authority = "https://auth.example.com/" +/// ``` +/// +/// # Features +/// This struct is only available when the `rocket_okapi` feature is enabled. +#[derive(Debug, Deserialize)] +pub struct IntrospectionRocketConfig { + pub(crate) authority: String, +} diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index dfed5f0..5d0146c 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -1,5 +1,6 @@ use custom_error::custom_error; use openidconnect::TokenIntrospectionResponse; +use rocket::figment::Figment; use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use rocket::{async_trait, Request}; @@ -17,6 +18,8 @@ use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use crate::rocket::introspection::IntrospectionConfig; +use super::config::IntrospectionRocketConfig; + custom_error! { /// Error type for guard related errors. pub IntrospectionGuardError @@ -159,14 +162,21 @@ impl<'a> OpenApiFromRequest<'a> for &'a IntrospectedUser { _name: String, _required: bool, ) -> rocket_okapi::Result { + let figment: Figment = rocket::Config::figment(); + let config: IntrospectionRocketConfig = figment + .extract() + .expect("authority must be set in Rocket.toml"); + // Setup global requirement for Security scheme let security_scheme = SecurityScheme { description: Some( "Use OpenID Connect to authenticate. (does not work in RapiDoc at all)".to_owned(), ), data: SecuritySchemeData::OpenIdConnect { - open_id_connect_url: "https://auth.domain.com/.well-known/openid-configuration" - .to_owned(), + open_id_connect_url: format!( + "{}/.well-known/openid-configuration", + config.authority + ), }, extensions: Object::default(), }; From 5a939c79535c7b077af2ea542f8abfc1741734b7 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Sat, 31 Aug 2024 19:14:18 +0200 Subject: [PATCH 07/12] feat: added schemars for get_responses --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3543fc2..12ffec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ oidc = ["credentials", "dep:base64-compat"] ## Refer to the rocket module for more information. rocket = ["credentials", "oidc", "dep:rocket"] -rocket_okapi = ["rocket", "dep:rocket_okapi"] +rocket_okapi = ["rocket", "dep:rocket_okapi", "dep:schemars"] [dependencies] actix-web = { version = "4.5.1", optional = true } @@ -86,6 +86,7 @@ tonic = { version = "0.11", features = [ ], optional = true } tonic-types = { version = "0.11", optional = true } rocket_okapi = { version = "0.8.0", optional = true, default-features = false } +schemars = {version = "0.8.21", optional = true} [dev-dependencies] chrono = "0.4.38" From 0871e587bab451052a2bde43ebb406b77f1b3947 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Sat, 31 Aug 2024 19:14:55 +0200 Subject: [PATCH 08/12] fix: sorted imports --- src/rocket/introspection/guard.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index 5d0146c..5f3a7e4 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -5,15 +5,20 @@ use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use rocket::{async_trait, Request}; #[cfg(feature = "rocket_okapi")] -use rocket_okapi::gen::OpenApiGenerator; -#[cfg(feature = "rocket_okapi")] -use rocket_okapi::okapi::openapi3::{ - Object, Responses, SecurityRequirement, SecurityScheme, SecuritySchemeData, +use rocket_okapi::{ + gen::OpenApiGenerator, + okapi::openapi3::{ + Object, Responses, SecurityRequirement, SecurityScheme, SecuritySchemeData, MediaType, + RefOr, Response, + }, + okapi::Map, + request::{OpenApiFromRequest, RequestHeaderInput}, }; + #[cfg(feature = "rocket_okapi")] -use rocket_okapi::okapi::Map; +use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject}; #[cfg(feature = "rocket_okapi")] -use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; +use std::collections::BTreeSet; use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use crate::rocket::introspection::IntrospectionConfig; From a0174136acac2df3bdf4a3e575fd73800e9cc4fa Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Sat, 31 Aug 2024 19:15:18 +0200 Subject: [PATCH 09/12] feat: added get_responses for better docs gen --- src/rocket/introspection/guard.rs | 84 ++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index 5f3a7e4..cfa0e4d 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -197,7 +197,89 @@ impl<'a> OpenApiFromRequest<'a> for &'a IntrospectedUser { security_req, )) } -} + + fn get_responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { + let mut res = Responses::default(); + + // Manually defining the error response schema + let error_detail_schema = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: { + let mut properties = Map::new(); + properties.insert( + "code".to_owned(), + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Integer.into()), + ..Default::default() + }), + ); + properties.insert( + "reason".to_owned(), + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + ..Default::default() + }), + ); + properties.insert( + "description".to_owned(), + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + ..Default::default() + }), + ); + properties + }, + required: vec!["code".to_owned(), "reason".to_owned(), "description".to_owned()] + .into_iter() + .collect::>(), // Convert Vec to BTreeSet + ..Default::default() + })), + ..Default::default() + }; + + let error_response_schema = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: { + let mut properties = Map::new(); + properties.insert("error".to_owned(), Schema::Object(error_detail_schema)); + properties + }, + required: vec!["error".to_owned()].into_iter().collect::>(), // Convert Vec to BTreeSet + ..Default::default() + })), + ..Default::default() + }; + + // Create the content for the error response + let mut content = Map::new(); + content.insert( + "application/json".to_owned(), + MediaType { + schema: Some(Schema::Object(error_response_schema).into()), + ..Default::default() + }, + ); + + // Adding 400 BadRequest response + let bad_request_response = Response { + description: "Bad Request - Multiple authorization headers found.".to_owned(), + content: content.clone(), + ..Default::default() + }; + res.responses.insert("400".to_owned(), RefOr::Object(bad_request_response)); + + // Adding 401 Unauthorized response + let unauthorized_response = Response { + description: "Unauthorized - The request requires user authentication.".to_owned(), + content: content.clone(), + ..Default::default() + }; + res.responses.insert("401".to_owned(), RefOr::Object(unauthorized_response)); + + Ok(res) + }} #[cfg(test)] mod tests { From 5341bb346b59f0105c70389f3c9fea27d09b911e Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Wed, 18 Sep 2024 15:49:10 +0200 Subject: [PATCH 10/12] fix: removed conflict --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 60485ab..897afd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -168,7 +168,6 @@ tokio = { version = "1.37.0", optional = true, features = [ tonic = { version = "0.12.1", features = [ "tls", ], optional = true } -tonic-types = { version = "0.11", optional = true } rocket_okapi = { version = "0.8.0", optional = true, default-features = false } schemars = {version = "0.8.21", optional = true} tonic-types = { version = "0.12.1", optional = true } From 3da2e41ee484377bce4437f31b36cfd0653fead6 Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Wed, 18 Sep 2024 15:49:45 +0200 Subject: [PATCH 11/12] docs: added small description for feature rocket_okapi --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 897afd6..1e87871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,9 @@ oidc = ["credentials", "dep:base64-compat"] ## Refer to the rocket module for more information. rocket = ["credentials", "oidc", "dep:rocket"] +## Feature that enables support for the [rocket okapi](https://github.com/GREsau/okapi). rocket_okapi = ["rocket", "dep:rocket_okapi", "dep:schemars"] + # @@protoc_deletion_point(features) # This section is automatically generated by protoc-gen-prost-crate. # Changes in this area may be lost on regeneration. From 8cbc30d1fd471b622fc5311c5ba5d94b30ff5e2c Mon Sep 17 00:00:00 2001 From: Dominik Spitzli Date: Wed, 18 Sep 2024 15:50:08 +0200 Subject: [PATCH 12/12] style: changed import positions --- src/rocket/introspection/guard.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index 7fcac7e..42f23c5 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -4,6 +4,9 @@ use rocket::figment::Figment; use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use rocket::{async_trait, Request}; +use std::collections::BTreeSet; +use std::collections::HashMap; + #[cfg(feature = "rocket_okapi")] use rocket_okapi::{ gen::OpenApiGenerator, @@ -14,12 +17,9 @@ use rocket_okapi::{ okapi::Map, request::{OpenApiFromRequest, RequestHeaderInput}, }; - #[cfg(feature = "rocket_okapi")] use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject}; #[cfg(feature = "rocket_okapi")] -use std::collections::BTreeSet; -use std::collections::HashMap; use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use crate::rocket::introspection::IntrospectionConfig;