feat(lib): add auth-kit
This commit is contained in:
@@ -7,7 +7,7 @@ use crate::models::{
|
||||
TenantEnabledAppsResponse, TenantResponse, UpdateAppRequest, Permission, ListPermissionsQuery,
|
||||
UpdateRoleRequest, RolePermissionsRequest, RoleUsersRequest,
|
||||
UpdateTenantEnabledAppsRequest, UpdateTenantRequest, UpdateTenantStatusRequest,
|
||||
UpdateUserRequest, UpdateUserRolesRequest, User, UserResponse,
|
||||
UpdateUserRequest, UpdateUserRolesRequest, User, UserResponse, AuthorizationCheckRequest, AuthorizationCheckResponse,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
||||
@@ -140,6 +140,7 @@ fn apply_header_parameter_examples(openapi: &mut utoipa::openapi::OpenApi, cfg:
|
||||
handlers::auth::login_handler,
|
||||
handlers::auth::refresh_handler,
|
||||
handlers::authorization::my_permissions_handler,
|
||||
handlers::authorization::authorization_check_handler,
|
||||
handlers::permission::list_permissions_handler,
|
||||
handlers::platform::get_tenant_enabled_apps_handler,
|
||||
handlers::platform::set_tenant_enabled_apps_handler,
|
||||
@@ -211,6 +212,8 @@ fn apply_header_parameter_examples(openapi: &mut utoipa::openapi::OpenApi, cfg:
|
||||
RequestAppStatusChangeRequest,
|
||||
ApproveAppStatusChangeRequest,
|
||||
AppStatusChangeRequest
|
||||
,AuthorizationCheckRequest
|
||||
,AuthorizationCheckResponse
|
||||
)
|
||||
),
|
||||
tags(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::handlers::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use axum::extract::State;
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
use crate::models::{AuthorizationCheckRequest, AuthorizationCheckResponse};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -58,3 +59,49 @@ pub async fn my_permissions_handler(
|
||||
.await?;
|
||||
Ok(AppResponse::ok(permissions))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/authorize/check",
|
||||
tag = "Policy",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = AuthorizationCheckRequest,
|
||||
responses(
|
||||
(status = 200, description = "鉴权校验结果", body = AuthorizationCheckResponse),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "租户不匹配")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, body))]
|
||||
pub async fn authorization_check_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Json(body): Json<AuthorizationCheckRequest>,
|
||||
) -> Result<AppResponse<AuthorizationCheckResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
|
||||
let allowed = match state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, body.permission.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(()) => true,
|
||||
Err(AppError::PermissionDenied(_)) => false,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
Ok(AppResponse::ok(AuthorizationCheckResponse { allowed }))
|
||||
}
|
||||
|
||||
93
src/handlers/jwks.rs
Normal file
93
src/handlers/jwks.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Jwks {
|
||||
keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Jwk {
|
||||
kty: &'static str,
|
||||
kid: String,
|
||||
#[serde(rename = "use")]
|
||||
use_field: &'static str,
|
||||
alg: &'static str,
|
||||
n: String,
|
||||
e: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExtraJwkKey {
|
||||
kid: String,
|
||||
public_key_pem: String,
|
||||
}
|
||||
|
||||
fn build_jwks(extra_json: Option<&str>) -> Result<Jwks, &'static str> {
|
||||
let keys = crate::utils::keys::get_keys();
|
||||
let mut jwk_keys = vec![Jwk {
|
||||
kty: "RSA",
|
||||
kid: keys.kid.clone(),
|
||||
use_field: "sig",
|
||||
alg: "RS256",
|
||||
n: keys.public_n.clone(),
|
||||
e: keys.public_e.clone(),
|
||||
}];
|
||||
|
||||
if let Some(extra_json) = extra_json {
|
||||
let extra: Vec<ExtraJwkKey> =
|
||||
serde_json::from_str(extra_json).map_err(|_| "Invalid JWT_JWKS_EXTRA_KEYS_JSON")?;
|
||||
for k in extra {
|
||||
let (n, e) = crate::utils::keys::jwk_components_from_public_pem(&k.public_key_pem)
|
||||
.map_err(|_| "Invalid public_key_pem in JWT_JWKS_EXTRA_KEYS_JSON")?;
|
||||
jwk_keys.push(Jwk {
|
||||
kty: "RSA",
|
||||
kid: k.kid,
|
||||
use_field: "sig",
|
||||
alg: "RS256",
|
||||
n,
|
||||
e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Jwks { keys: jwk_keys })
|
||||
}
|
||||
|
||||
pub async fn jwks_handler() -> Response {
|
||||
let extra_json = std::env::var("JWT_JWKS_EXTRA_KEYS_JSON").ok();
|
||||
match build_jwks(extra_json.as_deref()) {
|
||||
Ok(jwks) => (StatusCode::OK, Json(jwks)).into_response(),
|
||||
Err(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_jwks;
|
||||
|
||||
#[test]
|
||||
fn build_jwks_rejects_invalid_extra_json() {
|
||||
assert!(matches!(
|
||||
build_jwks(Some("not-json")),
|
||||
Err("Invalid JWT_JWKS_EXTRA_KEYS_JSON")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_jwks_accepts_extra_key() {
|
||||
let active_public_pem = crate::utils::keys::get_keys().public_pem.clone();
|
||||
let extra_json = serde_json::json!([{
|
||||
"kid": "extra-kid",
|
||||
"public_key_pem": active_public_pem
|
||||
}])
|
||||
.to_string();
|
||||
|
||||
let jwks = build_jwks(Some(&extra_json)).unwrap();
|
||||
assert!(jwks.keys.iter().any(|k| k.kid == "extra-kid"));
|
||||
assert!(jwks.keys.len() >= 2);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod app;
|
||||
pub mod auth;
|
||||
pub mod authorization;
|
||||
pub mod jwks;
|
||||
pub mod permission;
|
||||
pub mod platform;
|
||||
pub mod role;
|
||||
@@ -18,7 +19,8 @@ pub use app::{
|
||||
request_app_status_change_handler, update_app_handler,
|
||||
};
|
||||
pub use auth::{login_handler, refresh_handler, register_handler};
|
||||
pub use authorization::my_permissions_handler;
|
||||
pub use authorization::{authorization_check_handler, my_permissions_handler};
|
||||
pub use jwks::jwks_handler;
|
||||
pub use permission::list_permissions_handler;
|
||||
pub use platform::{get_tenant_enabled_apps_handler, set_tenant_enabled_apps_handler};
|
||||
pub use role::{
|
||||
|
||||
52
src/main.rs
52
src/main.rs
@@ -11,14 +11,16 @@ use axum::{
|
||||
Router,
|
||||
http::StatusCode,
|
||||
middleware::from_fn,
|
||||
middleware::from_fn_with_state,
|
||||
routing::{get, post},
|
||||
};
|
||||
use config::AppConfig;
|
||||
use handlers::{
|
||||
AppState, approve_app_status_change_handler, create_app_handler, create_role_handler,
|
||||
create_tenant_handler, delete_app_handler, delete_role_handler, delete_tenant_handler,
|
||||
delete_user_handler, get_app_handler, get_role_handler, get_tenant_enabled_apps_handler,
|
||||
get_tenant_handler, get_user_handler, grant_role_permissions_handler, grant_role_users_handler,
|
||||
AppState, approve_app_status_change_handler, authorization_check_handler, create_app_handler,
|
||||
create_role_handler, create_tenant_handler, delete_app_handler, delete_role_handler,
|
||||
delete_tenant_handler, delete_user_handler, get_app_handler, get_role_handler,
|
||||
get_tenant_enabled_apps_handler, get_tenant_handler, get_user_handler,
|
||||
grant_role_permissions_handler, grant_role_users_handler, jwks_handler,
|
||||
list_app_status_change_requests_handler, list_apps_handler, list_permissions_handler,
|
||||
list_roles_handler, list_user_roles_handler, list_users_handler, login_handler,
|
||||
my_permissions_handler, refresh_handler, register_handler, reject_app_status_change_handler,
|
||||
@@ -35,6 +37,7 @@ use std::net::SocketAddr;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_scalar::{Scalar, Servable};
|
||||
// 引入 models 下的所有结构体以生成文档
|
||||
use auth_kit::jwt::JwtVerifyConfig;
|
||||
use common_telemetry::telemetry::{self, TelemetryConfig};
|
||||
use docs::ApiDoc;
|
||||
|
||||
@@ -94,8 +97,33 @@ async fn main() {
|
||||
permission_service,
|
||||
};
|
||||
|
||||
let auth_cfg = middleware::auth::AuthMiddlewareConfig {
|
||||
skip_exact_paths: vec![
|
||||
"/.well-known/jwks.json".to_string(),
|
||||
"/tenants/register".to_string(),
|
||||
"/auth/register".to_string(),
|
||||
"/auth/login".to_string(),
|
||||
"/auth/refresh".to_string(),
|
||||
],
|
||||
skip_path_prefixes: vec!["/scalar".to_string()],
|
||||
jwt: JwtVerifyConfig::rs256_from_pem(
|
||||
"iam-service",
|
||||
&crate::utils::keys::get_keys().public_pem,
|
||||
)
|
||||
.expect("invalid JWT_PUBLIC_KEY_PEM"),
|
||||
};
|
||||
let tenant_cfg = middleware::TenantMiddlewareConfig {
|
||||
skip_exact_paths: vec![
|
||||
"/.well-known/jwks.json".to_string(),
|
||||
"/tenants/register".to_string(),
|
||||
"/auth/refresh".to_string(),
|
||||
],
|
||||
skip_path_prefixes: vec!["/scalar".to_string()],
|
||||
};
|
||||
|
||||
// 5. 构建路由
|
||||
let api = Router::new()
|
||||
.route("/.well-known/jwks.json", get(jwks_handler))
|
||||
.route("/tenants/register", post(create_tenant_handler))
|
||||
.route(
|
||||
"/tenants/me",
|
||||
@@ -123,6 +151,7 @@ async fn main() {
|
||||
.layer(from_fn(middleware::rate_limit::log_rate_limit_login)),
|
||||
)
|
||||
.route("/me/permissions", get(my_permissions_handler))
|
||||
.route("/authorize/check", post(authorization_check_handler))
|
||||
.route("/users", get(list_users_handler))
|
||||
.route("/users/me/password/reset", post(reset_my_password_handler))
|
||||
.route("/permissions", get(list_permissions_handler))
|
||||
@@ -157,8 +186,14 @@ async fn main() {
|
||||
)
|
||||
.route("/roles/{id}/users/grant", post(grant_role_users_handler))
|
||||
.route("/roles/{id}/users/revoke", post(revoke_role_users_handler))
|
||||
.layer(from_fn(middleware::resolve_tenant))
|
||||
.layer(from_fn(middleware::auth::authenticate))
|
||||
.layer(from_fn_with_state(
|
||||
tenant_cfg.clone(),
|
||||
middleware::resolve_tenant_with_config,
|
||||
))
|
||||
.layer(from_fn_with_state(
|
||||
auth_cfg.clone(),
|
||||
middleware::auth::authenticate_with_config,
|
||||
))
|
||||
.layer(from_fn(
|
||||
common_telemetry::axum_middleware::trace_http_request,
|
||||
));
|
||||
@@ -194,7 +229,10 @@ async fn main() {
|
||||
"/platform/app-status-change-requests/{request_id}/reject",
|
||||
post(reject_app_status_change_handler),
|
||||
)
|
||||
.layer(from_fn(middleware::auth::authenticate))
|
||||
.layer(from_fn_with_state(
|
||||
auth_cfg,
|
||||
middleware::auth::authenticate_with_config,
|
||||
))
|
||||
.layer(from_fn(
|
||||
common_telemetry::axum_middleware::trace_http_request,
|
||||
));
|
||||
|
||||
@@ -1,68 +1,3 @@
|
||||
use axum::{
|
||||
extract::{FromRequestParts, Request},
|
||||
http::request::Parts,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
pub use auth_kit::middleware::auth::{
|
||||
AuthContext, AuthMiddlewareConfig, authenticate_with_config,
|
||||
};
|
||||
use common_telemetry::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthContext {
|
||||
pub tenant_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub roles: Vec<String>,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn authenticate(mut req: Request, next: Next) -> Result<Response, AppError> {
|
||||
let path = req.uri().path();
|
||||
if path.starts_with("/scalar")
|
||||
|| path == "/tenants/register"
|
||||
|| path == "/auth/register"
|
||||
|| path == "/auth/login"
|
||||
|| path == "/auth/refresh"
|
||||
{
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
|
||||
let token = req
|
||||
.headers()
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.ok_or(AppError::MissingAuthHeader)?;
|
||||
|
||||
let claims = crate::utils::verify(token)?;
|
||||
let tenant_id = Uuid::parse_str(&claims.tenant_id)
|
||||
.map_err(|_| AppError::AuthError("Invalid tenant_id claim".into()))?;
|
||||
let user_id = Uuid::parse_str(&claims.sub)
|
||||
.map_err(|_| AppError::AuthError("Invalid sub claim".into()))?;
|
||||
|
||||
tracing::Span::current().record("tenant_id", tracing::field::display(tenant_id));
|
||||
tracing::Span::current().record("user_id", tracing::field::display(user_id));
|
||||
|
||||
req.extensions_mut().insert(AuthContext {
|
||||
tenant_id,
|
||||
user_id,
|
||||
roles: claims.roles,
|
||||
permissions: claims.permissions,
|
||||
});
|
||||
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for AuthContext
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extensions
|
||||
.get::<AuthContext>()
|
||||
.cloned()
|
||||
.ok_or(AppError::MissingAuthHeader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod rate_limit;
|
||||
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::{extract::Request, middleware::Next, response::Response};
|
||||
use common_telemetry::AppError;
|
||||
use http::request::Parts;
|
||||
use uuid::Uuid;
|
||||
|
||||
// --- 1. 租户 ID 提取器 ---
|
||||
|
||||
#[derive(Clone, Debug)] // 这是一个类型安全的 Wrapper,用于在 Handler 中注入
|
||||
pub struct TenantId(pub Uuid);
|
||||
|
||||
pub async fn resolve_tenant(mut req: Request, next: Next) -> Result<Response, AppError> {
|
||||
let path = req.uri().path();
|
||||
if path.starts_with("/scalar") || path == "/tenants/register" || path == "/auth/refresh" {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
|
||||
if let Some(auth_tenant_id) = req
|
||||
.extensions()
|
||||
.get::<auth::AuthContext>()
|
||||
.map(|ctx| ctx.tenant_id)
|
||||
{
|
||||
if let Some(header_value) = req
|
||||
.headers()
|
||||
.get("X-Tenant-ID")
|
||||
.and_then(|val| val.to_str().ok())
|
||||
{
|
||||
let header_tenant_id = Uuid::parse_str(header_value)
|
||||
.map_err(|_| AppError::BadRequest("Invalid X-Tenant-ID format".into()))?;
|
||||
if header_tenant_id != auth_tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
}
|
||||
tracing::Span::current().record("tenant_id", tracing::field::display(auth_tenant_id));
|
||||
req.extensions_mut().insert(TenantId(auth_tenant_id));
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
// 尝试从 Header 获取 X-Tenant-ID
|
||||
let tenant_id_str = req
|
||||
.headers()
|
||||
.get("X-Tenant-ID")
|
||||
.and_then(|val| val.to_str().ok());
|
||||
|
||||
match tenant_id_str {
|
||||
Some(id_str) => {
|
||||
if let Ok(uuid) = Uuid::parse_str(id_str) {
|
||||
tracing::Span::current().record("tenant_id", tracing::field::display(uuid));
|
||||
// 验证成功,注入到 Extension 中
|
||||
req.extensions_mut().insert(TenantId(uuid));
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
Err(AppError::BadRequest("Invalid X-Tenant-ID format".into()))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// 如果是公开接口(如登录注册),可能不需要 TenantID,视业务而定
|
||||
// 这里假设严格模式,必须带 TenantID
|
||||
Err(AppError::BadRequest("Missing X-Tenant-ID header".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 FromRequestParts 让 Handler 可以直接写 `tid: TenantId`
|
||||
impl<S> FromRequestParts<S> for TenantId
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
if let Some(tid) = parts.extensions.get::<TenantId>().cloned() {
|
||||
return Ok(tid);
|
||||
}
|
||||
let tenant_id_str = parts
|
||||
.headers
|
||||
.get("X-Tenant-ID")
|
||||
.and_then(|val| val.to_str().ok());
|
||||
|
||||
match tenant_id_str {
|
||||
Some(id_str) => uuid::Uuid::parse_str(id_str)
|
||||
.map(TenantId)
|
||||
.map_err(|_| AppError::BadRequest("Invalid X-Tenant-ID format".into())),
|
||||
None => Err(AppError::BadRequest("Missing X-Tenant-ID header".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use auth_kit::middleware::tenant::{TenantId, TenantMiddlewareConfig, resolve_tenant_with_config};
|
||||
|
||||
@@ -407,3 +407,15 @@ pub struct RoleUsersRequest {
|
||||
#[serde(default)]
|
||||
pub user_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||
pub struct AuthorizationCheckRequest {
|
||||
#[serde(default)]
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema, IntoParams)]
|
||||
pub struct AuthorizationCheckResponse {
|
||||
#[serde(default)]
|
||||
pub allowed: bool,
|
||||
}
|
||||
|
||||
@@ -50,8 +50,9 @@ pub fn sign(
|
||||
};
|
||||
|
||||
let keys = get_keys();
|
||||
encode(&Header::new(Algorithm::RS256), &claims, &keys.encoding_key)
|
||||
.map_err(|e| AppError::AuthError(e.to_string()))
|
||||
let mut header = Header::new(Algorithm::RS256);
|
||||
header.kid = Some(keys.kid.clone());
|
||||
encode(&header, &claims, &keys.encoding_key).map_err(|e| AppError::AuthError(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn verify(token: &str) -> Result<Claims, AppError> {
|
||||
|
||||
@@ -1,40 +1,76 @@
|
||||
use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use rsa::pkcs1::DecodeRsaPublicKey;
|
||||
use rsa::pkcs8::{DecodePublicKey, EncodePrivateKey, EncodePublicKey};
|
||||
use rsa::rand_core::OsRng;
|
||||
use rsa::traits::PublicKeyParts;
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey, pkcs1::LineEnding};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub struct KeyPair {
|
||||
pub encoding_key: jsonwebtoken::EncodingKey,
|
||||
pub decoding_key: jsonwebtoken::DecodingKey,
|
||||
pub kid: String,
|
||||
pub public_n: String,
|
||||
pub public_e: String,
|
||||
pub public_pem: String,
|
||||
}
|
||||
|
||||
static KEYS: OnceLock<KeyPair> = OnceLock::new();
|
||||
|
||||
pub fn get_keys() -> &'static KeyPair {
|
||||
KEYS.get_or_init(|| {
|
||||
// In a real production app, you would load these from files or ENV variables
|
||||
// defined in your AppConfig.
|
||||
// For now, we generate a fresh key pair on startup.
|
||||
let kid = std::env::var("JWT_KEY_ID").unwrap_or_else(|_| "default".to_string());
|
||||
let private_pem = std::env::var("JWT_PRIVATE_KEY_PEM").ok();
|
||||
let public_pem = std::env::var("JWT_PUBLIC_KEY_PEM").ok();
|
||||
|
||||
let bits = 2048;
|
||||
let private_key = RsaPrivateKey::new(&mut OsRng, bits).expect("failed to generate a key");
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
|
||||
let private_pem = private_key
|
||||
.to_pkcs1_pem(LineEnding::LF)
|
||||
.expect("failed to encode private key");
|
||||
let public_pem = public_key
|
||||
.to_pkcs1_pem(LineEnding::LF)
|
||||
.expect("failed to encode public key");
|
||||
let (private_pem, public_pem, public_key) = match (private_pem, public_pem) {
|
||||
(Some(priv_pem), Some(pub_pem)) => {
|
||||
let public_key = RsaPublicKey::from_pkcs1_pem(&pub_pem)
|
||||
.or_else(|_| RsaPublicKey::from_public_key_pem(&pub_pem))
|
||||
.expect("invalid JWT_PUBLIC_KEY_PEM");
|
||||
(priv_pem, pub_pem, public_key)
|
||||
}
|
||||
_ => {
|
||||
let bits = 2048;
|
||||
let private_key =
|
||||
RsaPrivateKey::new(&mut OsRng, bits).expect("failed to generate a key");
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
let private_pem = private_key
|
||||
.to_pkcs8_pem(LineEnding::LF)
|
||||
.expect("failed to encode private key")
|
||||
.to_string();
|
||||
let public_pem = public_key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.expect("failed to encode public key")
|
||||
.to_string();
|
||||
(private_pem, public_pem, public_key)
|
||||
}
|
||||
};
|
||||
|
||||
let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(private_pem.as_bytes())
|
||||
.expect("failed to create encoding key");
|
||||
let decoding_key = jsonwebtoken::DecodingKey::from_rsa_pem(public_pem.as_bytes())
|
||||
.expect("failed to create decoding key");
|
||||
|
||||
let public_n = URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be());
|
||||
let public_e = URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be());
|
||||
|
||||
KeyPair {
|
||||
encoding_key,
|
||||
decoding_key,
|
||||
kid,
|
||||
public_n,
|
||||
public_e,
|
||||
public_pem,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn jwk_components_from_public_pem(public_pem: &str) -> Result<(String, String), String> {
|
||||
let public_key = RsaPublicKey::from_pkcs1_pem(public_pem)
|
||||
.or_else(|_| RsaPublicKey::from_public_key_pem(public_pem))
|
||||
.map_err(|_| "Invalid public key pem".to_string())?;
|
||||
let n = URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be());
|
||||
let e = URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be());
|
||||
Ok((n, e))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user