2023-08-19 15:42:29 +02:00
|
|
|
use async_trait::async_trait;
|
2023-08-20 14:08:40 +02:00
|
|
|
use oauth2::reqwest::async_http_client;
|
|
|
|
use oauth2::url::Url;
|
2023-08-19 15:42:29 +02:00
|
|
|
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
|
2023-08-20 14:08:40 +02:00
|
|
|
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
|
2023-08-19 16:15:17 +02:00
|
|
|
use std::ops::Deref;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2023-08-20 01:25:46 +02:00
|
|
|
use crate::ZitadelClap;
|
|
|
|
|
2023-08-19 15:42:29 +02:00
|
|
|
#[async_trait]
|
|
|
|
pub trait OAuthClient {
|
|
|
|
async fn get_token(&self) -> anyhow::Result<()>;
|
2023-08-20 14:08:40 +02:00
|
|
|
async fn authorize_url(&self) -> anyhow::Result<Url>;
|
|
|
|
async fn exchange(&self, code: &str) -> anyhow::Result<String>;
|
2023-08-19 15:42:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct OAuth(Arc<dyn OAuthClient + Send + Sync + 'static>);
|
|
|
|
|
|
|
|
impl OAuth {
|
|
|
|
pub fn new_zitadel(config: ZitadelConfig) -> Self {
|
|
|
|
Self(Arc::new(ZitadelOAuthClient::from(config)))
|
|
|
|
}
|
|
|
|
pub fn new_noop() -> Self {
|
|
|
|
Self(Arc::new(NoopOAuthClient {}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Deref for OAuth {
|
|
|
|
type Target = Arc<dyn OAuthClient + Send + Sync + 'static>;
|
|
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ZitadelConfig> for OAuth {
|
|
|
|
fn from(value: ZitadelConfig) -> Self {
|
|
|
|
Self::new_zitadel(value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// -- Noop
|
2023-08-19 16:15:17 +02:00
|
|
|
#[derive(clap::Args, Clone)]
|
2023-08-19 15:42:29 +02:00
|
|
|
pub struct NoopOAuthClient;
|
|
|
|
#[async_trait]
|
|
|
|
impl OAuthClient for NoopOAuthClient {
|
|
|
|
async fn get_token(&self) -> anyhow::Result<()> {
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-08-20 14:08:40 +02:00
|
|
|
async fn authorize_url(&self) -> anyhow::Result<Url> {
|
|
|
|
Ok(Url::parse("http://localhost:3000/auth/zitadel").unwrap())
|
|
|
|
}
|
|
|
|
|
2023-08-20 14:09:15 +02:00
|
|
|
async fn exchange(&self, _code: &str) -> anyhow::Result<String> {
|
2023-08-20 14:08:40 +02:00
|
|
|
Ok(String::new())
|
|
|
|
}
|
2023-08-19 15:42:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// -- Zitadel
|
|
|
|
|
2023-08-20 01:15:11 +02:00
|
|
|
#[derive(Clone)]
|
2023-08-19 15:42:29 +02:00
|
|
|
pub struct ZitadelConfig {
|
2023-08-19 16:15:17 +02:00
|
|
|
auth_url: String,
|
2023-08-19 15:42:29 +02:00
|
|
|
client_id: String,
|
|
|
|
client_secret: String,
|
|
|
|
redirect_url: String,
|
|
|
|
token_url: String,
|
2023-08-20 01:15:11 +02:00
|
|
|
authority_url: String,
|
2023-08-19 15:42:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ZitadelOAuthClient {
|
|
|
|
client: BasicClient,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ZitadelOAuthClient {
|
|
|
|
pub fn new(
|
|
|
|
client_id: impl Into<String>,
|
|
|
|
client_secret: impl Into<String>,
|
|
|
|
redirect_url: impl Into<String>,
|
|
|
|
auth_url: impl Into<String>,
|
|
|
|
token_url: impl Into<String>,
|
2023-08-20 01:15:11 +02:00
|
|
|
authority_url: impl Into<String>,
|
2023-08-19 15:42:29 +02:00
|
|
|
) -> Self {
|
|
|
|
Self {
|
|
|
|
client: Self::oauth_client(ZitadelConfig {
|
|
|
|
client_id: client_id.into(),
|
|
|
|
client_secret: client_secret.into(),
|
|
|
|
redirect_url: redirect_url.into(),
|
|
|
|
auth_url: auth_url.into(),
|
|
|
|
token_url: token_url.into(),
|
2023-08-20 01:15:11 +02:00
|
|
|
authority_url: authority_url.into(),
|
2023-08-19 15:42:29 +02:00
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn oauth_client(config: ZitadelConfig) -> BasicClient {
|
|
|
|
BasicClient::new(
|
|
|
|
ClientId::new(config.client_id),
|
|
|
|
Some(ClientSecret::new(config.client_secret)),
|
|
|
|
AuthUrl::new(config.auth_url).unwrap(),
|
|
|
|
Some(TokenUrl::new(config.token_url).unwrap()),
|
|
|
|
)
|
|
|
|
.set_redirect_uri(RedirectUrl::new(config.redirect_url).unwrap())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ZitadelConfig> for ZitadelOAuthClient {
|
|
|
|
fn from(value: ZitadelConfig) -> Self {
|
|
|
|
Self::new(
|
|
|
|
value.client_id,
|
|
|
|
value.client_secret,
|
|
|
|
value.redirect_url,
|
|
|
|
value.auth_url,
|
|
|
|
value.token_url,
|
2023-08-20 01:15:11 +02:00
|
|
|
value.authority_url,
|
2023-08-19 15:42:29 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-20 01:25:46 +02:00
|
|
|
impl TryFrom<ZitadelClap> for ZitadelConfig {
|
|
|
|
type Error = anyhow::Error;
|
|
|
|
|
|
|
|
fn try_from(value: ZitadelClap) -> Result<Self, Self::Error> {
|
|
|
|
Ok(Self {
|
|
|
|
auth_url: value
|
|
|
|
.auth_url
|
|
|
|
.ok_or(anyhow::anyhow!("auth_url was not set"))?,
|
|
|
|
client_id: value
|
|
|
|
.client_id
|
|
|
|
.ok_or(anyhow::anyhow!("client_id was not set"))?,
|
|
|
|
client_secret: value
|
|
|
|
.client_secret
|
|
|
|
.ok_or(anyhow::anyhow!("client_secret was not set"))?,
|
|
|
|
redirect_url: value
|
|
|
|
.redirect_url
|
|
|
|
.ok_or(anyhow::anyhow!("redirect_url was not set"))?,
|
|
|
|
token_url: value
|
|
|
|
.token_url
|
|
|
|
.ok_or(anyhow::anyhow!("token_url was not set"))?,
|
|
|
|
authority_url: value
|
|
|
|
.authority_url
|
|
|
|
.ok_or(anyhow::anyhow!("authority_url was not set"))?,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-19 15:42:29 +02:00
|
|
|
#[async_trait]
|
|
|
|
impl OAuthClient for ZitadelOAuthClient {
|
|
|
|
async fn get_token(&self) -> anyhow::Result<()> {
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-08-20 14:08:40 +02:00
|
|
|
async fn authorize_url(&self) -> anyhow::Result<Url> {
|
|
|
|
let (auth_url, _csrf_token) = self
|
|
|
|
.client
|
|
|
|
.authorize_url(CsrfToken::new_random)
|
|
|
|
.add_scope(Scope::new("identify".to_string()))
|
|
|
|
.add_scope(Scope::new("openid".to_string()))
|
|
|
|
.url();
|
|
|
|
|
|
|
|
Ok(auth_url)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn exchange(&self, code: &str) -> anyhow::Result<String> {
|
|
|
|
let token = self
|
|
|
|
.client
|
|
|
|
.exchange_code(AuthorizationCode::new(code.to_string()))
|
|
|
|
.request_async(async_http_client)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(token.access_token().secret().clone())
|
|
|
|
}
|
2023-08-19 15:42:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2023-08-20 01:25:46 +02:00
|
|
|
use crate::ZitadelClap;
|
2023-08-19 16:15:17 +02:00
|
|
|
use clap::Parser;
|
2023-08-19 23:59:33 +02:00
|
|
|
use sealed_test::prelude::*;
|
2023-08-19 16:15:17 +02:00
|
|
|
|
|
|
|
#[derive(Parser)]
|
2023-08-19 16:43:50 +02:00
|
|
|
#[command(author, version, about, long_about = None)]
|
2023-08-19 16:15:17 +02:00
|
|
|
pub struct Cli {
|
|
|
|
#[clap(flatten)]
|
2023-08-20 01:15:11 +02:00
|
|
|
options: ZitadelClap,
|
2023-08-19 23:59:33 +02:00
|
|
|
}
|
2023-08-19 16:43:50 +02:00
|
|
|
|
2023-08-19 23:59:33 +02:00
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
#[command(author, version, about, long_about = None)]
|
|
|
|
pub struct CliSubCommand {
|
2023-08-19 16:43:50 +02:00
|
|
|
#[command(subcommand)]
|
|
|
|
command: Commands,
|
|
|
|
}
|
|
|
|
|
2023-08-19 23:59:33 +02:00
|
|
|
#[derive(clap::Subcommand, Clone, Debug, Eq, PartialEq)]
|
2023-08-19 16:43:50 +02:00
|
|
|
pub enum Commands {
|
2023-08-19 23:59:33 +02:00
|
|
|
One {
|
|
|
|
#[clap(flatten)]
|
2023-08-20 01:15:11 +02:00
|
|
|
options: ZitadelClap,
|
2023-08-19 23:59:33 +02:00
|
|
|
},
|
2023-08-19 16:15:17 +02:00
|
|
|
}
|
2023-08-19 15:42:29 +02:00
|
|
|
|
2023-08-19 16:43:50 +02:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_parse_clap_zitadel() {
|
|
|
|
let cli: Cli = Cli::parse_from(&[
|
|
|
|
"base",
|
|
|
|
"--zitadel-client-id=something",
|
|
|
|
"--zitadel-client-secret=something",
|
|
|
|
"--zitadel-auth-url=https://something",
|
|
|
|
"--zitadel-redirect-url=https://something",
|
|
|
|
"--zitadel-token-url=https://something",
|
2023-08-20 01:15:11 +02:00
|
|
|
"--zitadel-authority-url=https://something",
|
2023-08-19 16:43:50 +02:00
|
|
|
]);
|
2023-08-19 23:59:33 +02:00
|
|
|
println!("{:?}", cli.options);
|
2023-08-19 16:43:50 +02:00
|
|
|
|
|
|
|
pretty_assertions::assert_eq!(
|
2023-08-20 01:15:11 +02:00
|
|
|
cli.options,
|
2023-08-19 16:43:50 +02:00
|
|
|
ZitadelClap {
|
|
|
|
auth_url: Some("https://something".into()),
|
|
|
|
client_id: Some("something".into()),
|
|
|
|
client_secret: Some("something".into()),
|
|
|
|
redirect_url: Some("https://something".into()),
|
2023-08-20 01:15:11 +02:00
|
|
|
token_url: Some("https://something".into()),
|
|
|
|
authority_url: Some("https://something".into()),
|
2023-08-19 16:43:50 +02:00
|
|
|
}
|
|
|
|
);
|
2023-08-19 23:59:33 +02:00
|
|
|
}
|
2023-08-19 16:43:50 +02:00
|
|
|
|
2023-08-19 23:59:33 +02:00
|
|
|
#[test]
|
|
|
|
fn test_parse_clap_zitadel_fails_require_all() {
|
|
|
|
let cli = CliSubCommand::try_parse_from(&[
|
|
|
|
"base",
|
|
|
|
"one",
|
|
|
|
// "--zitadel-client-id=something", // We want to trigger missing variable
|
|
|
|
"--zitadel-client-secret=something",
|
|
|
|
"--zitadel-auth-url=https://something",
|
|
|
|
"--zitadel-redirect-url=https://something",
|
|
|
|
"--zitadel-token-url=https://something",
|
2023-08-20 01:15:11 +02:00
|
|
|
"--zitadel-authority-url=https://something",
|
2023-08-19 23:59:33 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
pretty_assertions::assert_eq!(cli.is_err(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[sealed_test]
|
|
|
|
fn test_parse_clap_env_zitadel() {
|
|
|
|
std::env::set_var("ZITADEL_CLIENT_ID", "something");
|
|
|
|
std::env::set_var("ZITADEL_CLIENT_SECRET", "something");
|
|
|
|
std::env::set_var("ZITADEL_AUTH_URL", "https://something");
|
|
|
|
std::env::set_var("ZITADEL_REDIRECT_URL", "https://something");
|
|
|
|
std::env::set_var("ZITADEL_TOKEN_URL", "https://something");
|
2023-08-20 01:15:11 +02:00
|
|
|
std::env::set_var("ZITADEL_AUTHORITY_URL", "https://something");
|
2023-08-19 23:59:33 +02:00
|
|
|
|
|
|
|
let cli = CliSubCommand::parse_from(&["base", "one"]);
|
|
|
|
|
|
|
|
pretty_assertions::assert_eq!(
|
|
|
|
cli.command,
|
|
|
|
Commands::One {
|
2023-08-20 01:15:11 +02:00
|
|
|
options: ZitadelClap {
|
|
|
|
auth_url: Some("https://something".into()),
|
|
|
|
client_id: Some("something".into()),
|
|
|
|
client_secret: Some("something".into()),
|
|
|
|
redirect_url: Some("https://something".into()),
|
|
|
|
token_url: Some("https://something".into()),
|
|
|
|
authority_url: Some("https://something".into()),
|
2023-08-19 23:59:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
#[test]
|
|
|
|
fn test_parse_clap_defaults_to_noop() {
|
|
|
|
let cli = CliSubCommand::parse_from(&["base", "one"]);
|
|
|
|
|
|
|
|
pretty_assertions::assert_eq!(
|
|
|
|
cli.command,
|
|
|
|
Commands::One {
|
2023-08-20 01:15:11 +02:00
|
|
|
options: ZitadelClap {
|
|
|
|
auth_url: None,
|
|
|
|
client_id: None,
|
|
|
|
client_secret: None,
|
|
|
|
redirect_url: None,
|
|
|
|
token_url: None,
|
|
|
|
authority_url: None,
|
|
|
|
},
|
2023-08-19 23:59:33 +02:00
|
|
|
}
|
|
|
|
);
|
2023-08-19 16:15:17 +02:00
|
|
|
}
|
2023-08-19 15:42:29 +02:00
|
|
|
}
|