use axum::extract::{Query, State}; use axum::response::{IntoResponse, Redirect, Response}; 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, #[serde(default)] pub silent: Option, } #[derive(Debug, Deserialize)] pub struct SignupQuery { #[serde(default)] pub redirect: Option, } /// 注册表单已迁移到 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, Query(query): Query, headers: axum::http::HeaderMap, ) -> Result { 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, Query(query): Query, headers: axum::http::HeaderMap, ) -> Result { 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(), } }