Skip to content

Commit

Permalink
Make auth extractor extendable (refactored), better redirect for OIDC…
Browse files Browse the repository at this point in the history
…, add link to swagger UI for utoipa, update utoipa dependency version, fix path normalization for swagger docs
  • Loading branch information
Wulf committed Oct 28, 2023
1 parent 0c79825 commit 73391ef
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 144 deletions.
11 changes: 5 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions create-rust-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ base64 = { optional = true, version = "0.21.2" }
openidconnect = { optional = true, version = "3.3.0" }

# plugin_utoipa dependencies
utoipa = { optional = true, version = "3", features = [
utoipa = { optional = true, version = "4", features = [
"actix_extras",
"chrono",
"openapi_extensions",
Expand Down Expand Up @@ -177,7 +177,7 @@ plugin_storage = [
"futures-util"
]
plugin_graphql = []
plugin_utoipa = ["utoipa", "backend_actix-web"]
plugin_utoipa = ["utoipa", "backend_actix-web"] # for now, only works with actix-web!
plugin_tasks = ["fang", "tokio"]
backend_poem = ["poem", "anyhow", "mime_guess", "tokio"]
backend_actix-web = [
Expand Down
48 changes: 48 additions & 0 deletions create-rust-app/src/auth/extractors/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::collections::HashSet;

use crate::auth::{Permission, ID};

#[derive(Debug, Clone)]
/// roles and permissions available to a User
///
/// use to control what users are and are not allowed to do
pub struct Auth {
pub user_id: ID,
pub roles: HashSet<String>,
pub permissions: HashSet<Permission>,
}

impl Auth {
/// does the user with the id [`self.user_id`](`ID`) have the given `permission`
pub fn has_permission(&self, permission: String) -> bool {
self.permissions.contains(&Permission {
permission,
from_role: String::new(),
})
}

/// does the user with the id [`self.user_id`](`ID`) have all of the given `perms`
pub fn has_all_permissions(&self, perms: Vec<String>) -> bool {
perms.iter().all(|p| self.has_permission(p.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have any of the given `perms`
pub fn has_any_permission(&self, perms: Vec<String>) -> bool {
perms.iter().any(|p| self.has_permission(p.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have the given `role`
pub fn has_role(&self, role: String) -> bool {
self.roles.contains(&role)
}

/// does the user with the id [`self.user_id`](`ID`) have all of the given `roles`
pub fn has_all_roles(&self, roles: Vec<String>) -> bool {
roles.iter().all(|r| self.has_role(r.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have any of the given `roles`
pub fn has_any_roles(&self, roles: Vec<String>) -> bool {
roles.iter().any(|r| self.has_role(r.to_string()))
}
}
84 changes: 24 additions & 60 deletions create-rust-app/src/auth/extractors/auth_actixweb.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::auth::{permissions::Permission, AccessTokenClaims, ID};
use super::auth::Auth;
use crate::auth::{permissions::Permission, AccessTokenClaims};
use actix_http::header::HeaderValue;
use actix_web::dev::Payload;
use actix_web::error::ResponseError;
Expand All @@ -13,63 +14,30 @@ use serde_json::json;
use std::collections::HashSet;
use std::iter::FromIterator;

#[derive(Debug, Clone)]
/// roles and permissions available to a User
///
/// use to control what users are and are not allowed to do
pub struct Auth {
pub user_id: ID,
pub roles: HashSet<String>,
pub permissions: HashSet<Permission>,
#[derive(Debug, Display, Error)]
#[display(fmt = "Unauthorized ({:?}), reason: {:?}", status, reason)]
/// custom error type for Authorization related errors
pub struct AuthError {
reason: String,
status: StatusCode,
}

impl Auth {
/// does the user with the id [`self.user_id`](`ID`) have the given `permission`
pub fn has_permission(&self, permission: String) -> bool {
self.permissions.contains(&Permission {
permission,
from_role: String::new(),
})
}

/// does the user with the id [`self.user_id`](`ID`) have all of the given `perms`
pub fn has_all_permissions(&self, perms: Vec<String>) -> bool {
perms.iter().all(|p| self.has_permission(p.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have any of the given `perms`
pub fn has_any_permission(&self, perms: Vec<String>) -> bool {
perms.iter().any(|p| self.has_permission(p.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have the given `role`
pub fn has_role(&self, role: String) -> bool {
self.roles.contains(&role)
impl AuthError {
pub fn new(reason: String, status: StatusCode) -> Self {
Self { reason, status }
}

/// does the user with the id [`self.user_id`](`ID`) have all of the given `roles`
pub fn has_all_roles(&self, roles: Vec<String>) -> bool {
roles.iter().all(|r| self.has_role(r.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have any of the given `roles`
pub fn has_any_roles(&self, roles: Vec<String>) -> bool {
roles.iter().any(|r| self.has_role(r.to_string()))
pub fn reason(reason: String) -> Self {
Self {
reason,
status: StatusCode::UNAUTHORIZED,
}
}
}

#[derive(Debug, Display, Error)]
#[display(fmt = "Unauthorized, reason: {}", self.reason)]
/// custom error type for Authorization related errors
pub struct AuthError {
reason: String,
}

impl ResponseError for AuthError {
/// builds an [`HttpResponse`] for [`self`](`AuthError`)
fn error_response(&self) -> HttpResponse {
// HttpResponse::Unauthorized().json(self.reason.as_str())
// println!("error_response");
HttpResponse::build(self.status_code()).body(
json!({
"message": self.reason.as_str()
Expand All @@ -93,17 +61,17 @@ impl FromRequest for Auth {
let auth_header_opt: Option<&HeaderValue> = req.headers().get("Authorization");

if auth_header_opt.is_none() {
return ready(Err(AuthError {
reason: "Authorization header required".to_string(),
}));
return ready(Err(AuthError::reason(
"Authorization header required".to_string(),
)));
}

let access_token_str = auth_header_opt.unwrap().to_str().unwrap_or("");

if !access_token_str.starts_with("Bearer ") {
return ready(Err(AuthError {
reason: "Invalid authorization header".to_string(),
}));
return ready(Err(AuthError::reason(
"Invalid authorization header".to_string(),
)));
}

let access_token = decode::<AccessTokenClaims>(
Expand All @@ -113,9 +81,7 @@ impl FromRequest for Auth {
);

if access_token.is_err() {
return ready(Err(AuthError {
reason: "Invalid access token".to_string(),
}));
return ready(Err(AuthError::reason("Invalid access token".to_string())));
}

let access_token = access_token.unwrap();
Expand All @@ -125,9 +91,7 @@ impl FromRequest for Auth {
.token_type
.eq_ignore_ascii_case("access_token")
{
return ready(Err(AuthError {
reason: "Invalid access token".to_string(),
}));
return ready(Err(AuthError::reason("Invalid access token".to_string())));
}

let user_id = access_token.claims.sub;
Expand Down
46 changes: 1 addition & 45 deletions create-rust-app/src/auth/extractors/auth_poem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,13 @@ use poem::{
};
use std::collections::HashSet;

use super::auth::Auth;
use crate::auth::{permissions::Permission, AccessTokenClaims, ID};
use jsonwebtoken::decode;
use jsonwebtoken::DecodingKey;
use jsonwebtoken::Validation;
use std::iter::FromIterator;

#[derive(Debug, Clone)]
/// roles and permissions available to a User
///
/// use to control what users are and are not allowed to do
pub struct Auth {
pub user_id: ID,
pub roles: HashSet<String>,
pub permissions: HashSet<Permission>,
}

impl Auth {
/// does the user with the id [`self.user_id`](`ID`) have the given `permission`
pub fn has_permission(&self, permission: String) -> bool {
self.permissions.contains(&Permission {
permission,
from_role: String::new(),
})
}

/// does the user with the id [`self.user_id`](`ID`) have all of the given `perms`
pub fn has_all_permissions(&self, perms: Vec<String>) -> bool {
perms.iter().all(|p| self.has_permission(p.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have any of the given `perms`
pub fn has_any_permissions(&self, perms: Vec<String>) -> bool {
perms.iter().any(|p| self.has_permission(p.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have the given `role`
pub fn has_role(&self, role: String) -> bool {
self.roles.contains(&role)
}

/// does the user with the id [`self.user_id`](`ID`) have all of the given `roles`
pub fn has_all_roles(&self, roles: Vec<String>) -> bool {
roles.iter().all(|r| self.has_role(r.to_string()))
}

/// does the user with the id [`self.user_id`](`ID`) have any of the given `roles`
pub fn has_any_roles(&self, roles: Vec<String>) -> bool {
roles.iter().any(|r| self.has_role(r.to_string()))
}
}

#[async_trait]
impl<'a> FromRequest<'a> for Auth {
/// extracts [`Auth`] from the given [`req`](`Request`)
Expand Down
7 changes: 4 additions & 3 deletions create-rust-app/src/auth/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
mod auth;
pub use auth::Auth;

#[cfg(feature = "backend_actix-web")]
mod auth_actixweb;
#[cfg(feature = "backend_actix-web")]
pub use auth_actixweb::Auth;
pub use auth_actixweb::AuthError;

#[cfg(feature = "backend_poem")]
mod auth_poem;
#[cfg(feature = "backend_poem")]
pub use auth_poem::Auth;
2 changes: 1 addition & 1 deletion create-rust-app/src/auth/oidc/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub async fn oidc_login_url(

let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

// TODO: factor in nonce
// TODO: set redirect_uri from provider config
let (auth_url, csrf_token, nonce) = client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
Expand Down
1 change: 1 addition & 0 deletions create-rust-app/src/tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub fn queue() -> &'static Queue {
static QUEUE: OnceCell<Queue> = OnceCell::new();

QUEUE.get_or_init(|| {
// TODO: make the number of connections in the pool configurable
let db = Database::new();

Queue::builder().connection_pool(db.pool.clone()).build()
Expand Down
12 changes: 6 additions & 6 deletions create-rust-app_cli/src/plugins/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ import { ResetPage } from './containers/ResetPage'"#,
"frontend/src/App.tsx",
r#"{/* CRA: routes */}"#,
r#"{/* CRA: routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/recovery" element={<RecoveryPage />} />
<Route path="/reset" element={<ResetPage />} />
<Route path="/activate" element={<ActivationPage />} />
<Route path="/register" element={<RegistrationPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/recovery" element={<RecoveryPage />} />
<Route path="/reset" element={<ResetPage />} />
<Route path="/activate" element={<ActivationPage />} />
<Route path="/register" element={<RegistrationPage />} />
<Route path="/account" element={<AccountPage />} />
"#,
)?;
fs::replace(
Expand Down
25 changes: 23 additions & 2 deletions create-rust-app_cli/src/plugins/auth_oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,24 @@ completeOIDCLogin"#},
"frontend/src/hooks/useAuth.tsx",
"const logout = async ()",
indoc! {r#"
const loginOIDC = async (provider: string) => {
const loginOIDC = async (
provider: string,
options?: { redirectUrl?: 'current-url' | string }
) => {
if (options?.redirectUrl) {
localStorage.setItem(
'create_rust_app_oauth_redirect',
options?.redirectUrl === 'current-url'
? window.location.href
: options.redirectUrl
)
} else {
localStorage.removeItem('create_rust_app_oauth_redirect')
}
window.location.href = `/api/auth/oidc/${provider}`
}
const completeOIDCLogin = (): boolean => {
const params = new URLSearchParams(window.location.search);
let access_token = params.get('access_token');
Expand All @@ -213,6 +227,13 @@ const completeOIDCLogin = (): boolean => {
hasRole: permissions.hasRole,
})
if (localStorage.getItem('create_rust_app_oauth_redirect')) {
window.location.href = localStorage.getItem(
'create_rust_app_oauth_redirect'
) as string
}
return true
}
}
Expand Down
Loading

0 comments on commit 73391ef

Please sign in to comment.