chore: add openid with casdoor for Single Sign-On
This commit is contained in:
parent
9a0a65aa91
commit
1b0dafca9a
188
src/defs/openid.rs
Normal file
188
src/defs/openid.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
// use std::io::{Result, ErrorKind};
|
||||
use std::error::Error;
|
||||
//use std::str::FromStr;
|
||||
type BoxResult<T> = Result<T,Box<dyn Error>>;
|
||||
use url::Url;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
|
||||
use openidconnect::{
|
||||
core::{
|
||||
CoreClient,
|
||||
CoreProviderMetadata,
|
||||
CoreResponseType,
|
||||
},
|
||||
reqwest::async_http_client, HttpRequest,
|
||||
AuthenticationFlow,
|
||||
ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
|
||||
RedirectUrl, Scope, PkceCodeChallenge, PkceCodeVerifier,
|
||||
};
|
||||
|
||||
use axum::{http::method::Method, http::HeaderMap, http::header::CONTENT_TYPE};
|
||||
|
||||
use crate::defs::Config;
|
||||
|
||||
fn default_empty() -> String { String::from("") }
|
||||
fn default_expire_in() -> u64 { 0 }
|
||||
fn default_algorithm() -> String { String::from("RS256") }
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenidClaims {
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
pub email: String,
|
||||
// exp: i64,
|
||||
}
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct OpenidTokens {
|
||||
# [serde(default = "default_empty")]
|
||||
pub access_token: String,
|
||||
# [serde(default = "default_empty")]
|
||||
pub id_token: String,
|
||||
# [serde(default = "default_empty")]
|
||||
pub refresh_token: String,
|
||||
# [serde(default = "default_empty")]
|
||||
pub token_type: String,
|
||||
# [serde(default = "default_expire_in")]
|
||||
pub expire_in: u64,
|
||||
# [serde(default = "default_empty")]
|
||||
pub scope: String,
|
||||
}
|
||||
#[derive(Default,Deserialize,Serialize,Debug,Clone)]
|
||||
pub struct OpenidData {
|
||||
#[serde(default = "default_empty")]
|
||||
pub code: String,
|
||||
#[serde(default = "default_empty")]
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize,Default)]
|
||||
pub struct OpenidConf {
|
||||
#[serde(default = "default_algorithm")]
|
||||
pub algorithm: String,
|
||||
pub access_token_url: String,
|
||||
pub disable_signature: bool,
|
||||
pub endpoint: String,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub certificate: String,
|
||||
pub org_name: String,
|
||||
pub app_name: String,
|
||||
}
|
||||
|
||||
pub struct OpenidCli {
|
||||
pub client: CoreClient,
|
||||
pub pk_challenge: PkceCodeChallenge,
|
||||
pub pk_verifier: PkceCodeVerifier,
|
||||
}
|
||||
pub struct OpenidClient {
|
||||
pub url: Url,
|
||||
pub token: CsrfToken,
|
||||
pub nonce: Nonce,
|
||||
}
|
||||
/*
|
||||
fn handle_error<T: std::error::Error>(fail: &T, msg: &'static str) {
|
||||
let mut err_msg = format!("ERROR: {}", msg);
|
||||
let mut cur_fail: Option<&dyn std::error::Error> = Some(fail);
|
||||
while let Some(cause) = cur_fail {
|
||||
err_msg += &format!("\n caused by: {}", cause);
|
||||
cur_fail = cause.source();
|
||||
}
|
||||
println!("{}", err_msg);
|
||||
// exit(1);
|
||||
}
|
||||
*/
|
||||
impl OpenidConf {
|
||||
pub async fn client(&self, return_url: String) -> BoxResult<OpenidCli> {
|
||||
let app_client_id = ClientId::new(self.client_id.to_owned());
|
||||
let app_client_secret = ClientSecret::new(self.client_secret.to_owned());
|
||||
let issuer_url = IssuerUrl::new(self.endpoint.to_owned())?;
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client).await?;
|
||||
let redirect_url = RedirectUrl::new(return_url.to_owned())?;
|
||||
let (pk_challenge, pk_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
// Set up the config for the App OAuth2 process.
|
||||
Ok( OpenidCli {
|
||||
client: CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
app_client_id,
|
||||
Some(app_client_secret),
|
||||
).set_redirect_uri(redirect_url),
|
||||
pk_challenge,
|
||||
pk_verifier,
|
||||
}
|
||||
)
|
||||
}
|
||||
pub async fn get_auth(&self, openid_cli: &OpenidCli) -> BoxResult<OpenidClient> {
|
||||
// Generate the authorization URL to which we'll redirect the user.
|
||||
//let pk_challenge = openid_cli.pk_challenge.clone();
|
||||
let (url, token, nonce) =
|
||||
openid_cli.client
|
||||
.authorize_url(
|
||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
// This example is requesting access to the the user's profile including email.
|
||||
.add_scope(Scope::new("name".to_string()))
|
||||
.add_scope(Scope::new("email".to_string()))
|
||||
// Set the PKCE code challenge.
|
||||
// .set_pkce_challenge(pk_challenge)
|
||||
.url();
|
||||
Ok(OpenidClient { url, token, nonce })
|
||||
}
|
||||
pub async fn get_token(&self, _openid_cli: &OpenidCli, auth_code: String, _str_nonce: String) -> BoxResult<OpenidClaims> {
|
||||
// let nonce = Nonce::new(str_nonce);
|
||||
// this is because PkceCodeVerifier des not have clone derive
|
||||
// let str_pk_verifier = serde_json::to_string(&openid_cli.pk_verifier).unwrap_or_default();
|
||||
// let pk_verifier: PkceCodeVerifier = serde_json::from_str(&str_pk_verifier)?;
|
||||
|
||||
let mut header_map = HeaderMap::new();
|
||||
header_map.insert(CONTENT_TYPE, "application/json".parse()?);
|
||||
let mut body_data: HashMap<String, String> = HashMap::new();
|
||||
body_data.insert(String::from("grant_type"), String::from("authorization_code"));
|
||||
body_data.insert(String::from("client_id"), self.client_id.to_owned());
|
||||
body_data.insert(String::from("client_secret"), self.client_secret.to_owned());
|
||||
body_data.insert(String::from("code"), auth_code.to_owned());
|
||||
let str_body = serde_json::to_string(&body_data).unwrap_or_default();
|
||||
let body = str_body.as_bytes().to_vec();
|
||||
if self.access_token_url.is_empty() {
|
||||
return Err(Box::try_from(format!("Invalid openid access token url"))?);
|
||||
}
|
||||
let openid_request = HttpRequest{
|
||||
url: Url::parse(&self.access_token_url)?,
|
||||
method: Method::POST,
|
||||
headers: header_map,
|
||||
body
|
||||
};
|
||||
let openid_response = async_http_client(openid_request).await?;
|
||||
// dbg!(&openid_response.headers);
|
||||
let vec_to_string = String::from_utf8(openid_response.body)?;
|
||||
let openid_tokens: OpenidTokens = serde_json::from_str(&vec_to_string).unwrap_or(OpenidTokens::default());
|
||||
let algorithm = match self.algorithm.as_str() {
|
||||
"RS256" => Algorithm::RS256,
|
||||
_ => Algorithm::RS256,
|
||||
};
|
||||
let mut validation = Validation::new(algorithm);
|
||||
if self.disable_signature {
|
||||
validation.insecure_disable_signature_validation();
|
||||
}
|
||||
let res = jsonwebtoken::decode::<OpenidClaims>(
|
||||
&openid_tokens.id_token,
|
||||
&DecodingKey::from_secret(&[]),
|
||||
&validation
|
||||
)?;
|
||||
Ok(res.claims)
|
||||
}
|
||||
}
|
||||
pub async fn collect_openid_clients(config: Config, return_url: &str) -> HashMap<String,OpenidCli> {
|
||||
let mut clients = HashMap::new();
|
||||
for (key, openid_conf) in config.openid_auths.clone().into_iter() {
|
||||
match openid_conf.client(return_url.to_owned()).await {
|
||||
Ok(cli) => {
|
||||
clients.insert(key.to_owned(),cli);
|
||||
println!("OpenID {} added", key)
|
||||
},
|
||||
Err(e) => println!("Openid {} error: {} ",key, e),
|
||||
}
|
||||
}
|
||||
clients
|
||||
}
|
Loading…
Reference in New Issue
Block a user