Skip to content
Clone
e870254
177 lines 5.5 KB Rust
use actix_web::http::header::{HeaderMap, HeaderValue, SET_COOKIE};
use export::http::{Redirect, Response};
use serde::{Deserialize, Serialize};
use config::oauth::OAuthConfig;
use crate::auth::errors::AuthError;
use crate::auth::services::AuthService;
pub const STATE_TTL_SECS: u64 = 600;
pub const REFRESH_LOCK_TTL_SECS: u64 = 10;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginState {
pub redirect: String,
pub verifier: String,
pub nonce: String,
/// 本次握手实际发给 KenOAuth 的 redirect_uri。
///
/// 换码时必须原样回传(OAuth 要求两者完全一致),所以随 state 一起存。
/// `#[serde(default)]` 保证旧 state 仍能解析。
#[serde(default)]
pub redirect_uri: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub id_token: Option<String>,
#[serde(default)]
pub expires_in: i64,
}
pub fn random_token(bytes: usize) -> String {
use base64::Engine as _;
let mut buffer = vec![0u8; bytes];
rand::Rng::fill(&mut rand::thread_rng(), buffer.as_mut_slice());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buffer)
}
pub fn pkce_challenge(verifier: &str) -> String {
use base64::Engine as _;
use sha2::{Digest, Sha256};
let digest = Sha256::digest(verifier.as_bytes());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest)
}
pub fn read_cookie_header<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
let value = headers
.get(actix_web::http::header::COOKIE)?
.to_str()
.ok()?;
jwt::read_cookie(value, name)
}
pub fn append_cookie(response: &mut Response, cookie: &str) {
if let Ok(value) = HeaderValue::from_str(cookie) {
response.headers_mut().append(SET_COOKIE, value);
}
}
pub fn redirect_with_cookies(location: &str, cookies: &[String]) -> Response {
let mut response = Redirect::to(location).into_response();
for cookie in cookies {
append_cookie(&mut response, cookie);
}
response
}
pub async fn exchange_code(
service: &AuthService,
code: &str,
verifier: &str,
redirect_uri: &str,
) -> Result<TokenResponse, AuthError> {
let config = &service.config().oauth;
local_grant(
service,
auth::server::protocol::TokenRequest {
grant_type: "authorization_code".to_owned(),
code: Some(code.to_owned()),
redirect_uri: Some(redirect_uri.to_owned()),
code_verifier: Some(verifier.to_owned()),
refresh_token: None,
client_id: Some(config.client_id.clone()),
client_secret: (!config.client_secret.is_empty()).then(|| config.client_secret.clone()),
scope: None,
},
)
.await
}
pub async fn refresh_grant(
service: &AuthService,
refresh_token: &str,
) -> Result<TokenResponse, AuthError> {
let config = &service.config().oauth;
local_grant(
service,
auth::server::protocol::TokenRequest {
grant_type: "refresh_token".to_owned(),
code: None,
redirect_uri: None,
code_verifier: None,
refresh_token: Some(refresh_token.to_owned()),
client_id: Some(config.client_id.clone()),
client_secret: (!config.client_secret.is_empty()).then(|| config.client_secret.clone()),
scope: None,
},
)
.await
}
async fn local_grant(
service: &AuthService,
form: auth::server::protocol::TokenRequest,
) -> Result<TokenResponse, AuthError> {
let client = service
.state
.kenauth
.as_ref()
.ok_or_else(|| AuthError::Internal("identity service is unavailable".to_owned()))?;
let value = auth::server::protocol::exchange_local(client.service(), &form)
.await
.map_err(|error| {
if error.status >= 500 {
AuthError::Internal(error.message)
} else {
AuthError::BadRequest(error.message)
}
})?;
serde_json::from_value(value)
.map_err(|error| AuthError::Internal(format!("invalid token response: {error}")))
}
/// 本次请求应该用哪个 `redirect_uri`。
///
/// 取 `x-forwarded-host`(vite / 网关都会带),退化为 `host`;
/// 只有命中 `oauth.redirect_uri_hosts` 白名单才按该主机改写,路径沿用配置里的。
pub fn resolve_redirect_uri(config: &OAuthConfig, headers: &HeaderMap) -> String {
let host = headers
.get("x-forwarded-host")
.and_then(|value| value.to_str().ok())
.or_else(|| {
headers
.get(actix_web::http::header::HOST)
.and_then(|value| value.to_str().ok())
})
// 代理链可能给逗号分隔的列表,取第一跳。
.map(|value| {
value
.split(',')
.next()
.unwrap_or(value)
.trim()
.to_ascii_lowercase()
});
let Some(host) = host else {
return config.redirect_uri.clone();
};
if host.is_empty()
|| !config
.redirect_uri_hosts
.iter()
.any(|allowed| allowed.trim().eq_ignore_ascii_case(&host))
{
return config.redirect_uri.clone();
}
let path = url::Url::parse(&config.redirect_uri)
.map(|url| url.path().to_string())
.unwrap_or_else(|_| "/api/v1/auth/callback".to_string());
format!("http://{host}{path}")
}