Skip to content
Clone
b7cd2b1
157 lines 5.0 KB Rust
use export::http::{Query, Redirect, Response, State};
use serde::Deserialize;
use url::form_urlencoded;
use config::oauth::OAuthConfig;
use crate::errors::ApiError;
use crate::oauth::{LoginState, pkce_challenge, random_token, resolve_redirect_uri, state};
use crate::services::AuthService;
#[derive(Debug, Deserialize)]
pub struct LoginQuery {
#[serde(default)]
pub redirect: Option<String>,
#[serde(default)]
pub silent: Option<u8>,
}
#[derive(Debug, Deserialize)]
pub struct SignupQuery {
#[serde(default)]
pub redirect: Option<String>,
}
/// 注册表单已迁移到 KenOAuth:跳它的 `/register`。
///
/// 不能直接跳 `/register` 了事——那样注册完的人没有会话,也没有回到 GitData 的路径。
/// 这里先按注册前的 `/auth/login` 跑一遍握手(PKCE + nonce 存 Redis),
/// 把授权地址作为 `callback_url` 交给 KenOAuth:用户在那里注册并登录后,
/// `/success` 页用 `location.replace` 回到授权端点,走正常 callback 落地。
///
/// 多出来的那一份 state 不会被 `take()`,600s 后自然过期。
pub async fn signup(
State(service): State<AuthService>,
Query(query): Query<SignupQuery>,
headers: export::http::HeaderMap,
) -> Result<Response, ApiError> {
let config = &service.config().oauth;
if !config.enabled {
return Err(ApiError::not_found("oauth is disabled"));
}
let redirect = sanitize_redirect(query.redirect.as_deref());
let redirect_uri = resolve_redirect_uri(config, &headers);
let state_token = random_token(32);
let verifier = random_token(48);
let nonce = random_token(16);
state::store(
&service,
&state_token,
&LoginState {
redirect: redirect.clone(),
verifier: verifier.clone(),
nonce: nonce.clone(),
redirect_uri: Some(redirect_uri.clone()),
},
)
.await
.map_err(ApiError::from)?;
// 注册后直接拿授权码,省掉一次「请再点一次登录」。
let authorization = authorize_url(
config,
&redirect_uri,
&state_token,
&verifier,
&nonce,
false,
);
let mut serializer = form_urlencoded::Serializer::new(String::new());
serializer.append_pair("callback_url", &authorization);
let location = format!(
"{}/register?{}",
config.issuer.trim_end_matches('/'),
serializer.finish()
);
Ok(Redirect::to(&location).into_response())
}
pub async fn login(
State(service): State<AuthService>,
Query(query): Query<LoginQuery>,
headers: export::http::HeaderMap,
) -> Result<Response, ApiError> {
let config = &service.config().oauth;
if !config.enabled {
return Err(ApiError::not_found("oauth is disabled"));
}
let redirect = sanitize_redirect(query.redirect.as_deref());
let redirect_uri = resolve_redirect_uri(config, &headers);
let state_token = random_token(32);
let verifier = random_token(48);
let nonce = random_token(16);
state::store(
&service,
&state_token,
&LoginState {
redirect: redirect.clone(),
verifier: verifier.clone(),
nonce: nonce.clone(),
redirect_uri: Some(redirect_uri.clone()),
},
)
.await
.map_err(ApiError::from)?;
let location = authorize_url(
config,
&redirect_uri,
&state_token,
&verifier,
&nonce,
query.silent == Some(1),
);
Ok(Redirect::to(&location).into_response())
}
fn authorize_url(
config: &OAuthConfig,
redirect_uri: &str,
state_token: &str,
verifier: &str,
nonce: &str,
silent: bool,
) -> String {
let mut serializer = form_urlencoded::Serializer::new(String::new());
serializer.append_pair("response_type", "code");
serializer.append_pair("client_id", &config.client_id);
serializer.append_pair("redirect_uri", redirect_uri);
// `event` 是 KenEvent 实时通道要求的 scope(KenEvent 用 gRPC
// IdentityService.Authenticate 校验时 required_scopes=[Event]);
// 少要这一个 scope,前端 socket 会被 KenOAuth 判成无权限。
serializer.append_pair("scope", "openid profile email event");
serializer.append_pair("state", state_token);
serializer.append_pair("nonce", nonce);
serializer.append_pair("code_challenge_method", "S256");
serializer.append_pair("code_challenge", &pkce_challenge(verifier));
if silent {
serializer.append_pair("prompt", "none");
}
format!(
"{}?{}",
config.authorization_endpoint(),
serializer.finish()
)
}
/// 未指定去向时的落点:登录/注册完成直接进工作区。
/// `/app` 在路由里会再跳一次 `/app/@me`。
pub const DEFAULT_REDIRECT: &str = "/app";
fn sanitize_redirect(raw: Option<&str>) -> String {
match raw.map(str::trim) {
Some(value) if value.starts_with('/') && !value.starts_with("//") => value.to_string(),
_ => DEFAULT_REDIRECT.to_string(),
}
}