Skip to content

Commit 4b3854f

Browse files
liweinancursoragent
andcommitted
feat(htyuc): SSO apps/issue/exchange API
支持 admin/ts/teacher 六域 Web 单点登录码签发与兑换。 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4fa088e commit 4b3854f

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

htyuc/src/lib.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use htycommons::upyun::{generate_upyun_token, get_upyun_operator, get_upyun_pass
3131
use htycommons::web::{get_uc_url, skip_post_login, skip_post_registration, wrap_json_anyhow_err, wrap_json_hty_err, wrap_json_ok_resp, AuthorizationHeader, HtyHostHeader, HtySudoerTokenHeader, HtyToken, ReqHtyAction, ReqHtyLabel, ReqHtyRole, ReqHtyTag, ReqHtyTagRef, ReqTagRefsByRefId, UnionIdHeader, ReqKV};
3232
use htycommons::wx::{code2session, WxId, WxLogin, WxParams, WxSession};
3333
use htycommons::{db, uuid};
34+
use serde::Deserialize;
3435

3536
use tracing::{debug, warn, error};
3637
use htycommons::models::*;
@@ -5766,6 +5767,135 @@ pub async fn wx_qr_login(State(db_pool): State<Arc<DbState>>, host: HtyHostHeade
57665767
}
57675768
}
57685769

5770+
const SSO_REDIS_PREFIX: &str = "SSO_C_";
5771+
const SSO_CODE_TTL_SECS: usize = 30;
5772+
5773+
fn sso_allowed_domains() -> &'static [&'static str] {
5774+
&[
5775+
"admin.moicen.com",
5776+
"ts.moicen.com",
5777+
"teacher.moicen.com",
5778+
"admin.huiwings.cn",
5779+
"ts.huiwings.cn",
5780+
"teacher.huiwings.cn",
5781+
]
5782+
}
5783+
5784+
fn is_sso_allowed_domain(domain: &str) -> bool {
5785+
sso_allowed_domains().contains(&domain)
5786+
}
5787+
5788+
#[derive(Deserialize)]
5789+
struct ReqSsoIssue {
5790+
jwt: String,
5791+
app: String,
5792+
}
5793+
5794+
#[derive(Deserialize)]
5795+
struct ReqSsoExchange {
5796+
code: String,
5797+
}
5798+
5799+
#[derive(serde::Serialize, serde::Deserialize)]
5800+
struct SsoCodePayload {
5801+
jwt: String,
5802+
app: String,
5803+
}
5804+
5805+
async fn sso_apps() -> Json<HtyResponse<Vec<String>>> {
5806+
wrap_json_ok_resp(
5807+
sso_allowed_domains()
5808+
.iter()
5809+
.map(|d| d.to_string())
5810+
.collect(),
5811+
)
5812+
}
5813+
5814+
async fn sso_issue(Json(req): Json<ReqSsoIssue>) -> impl IntoResponse {
5815+
if !is_sso_allowed_domain(&req.app) {
5816+
return (
5817+
StatusCode::BAD_REQUEST,
5818+
wrap_json_hty_err::<String>(HtyErr {
5819+
code: HtyErrCode::WebErr,
5820+
reason: Some("app not allowed for SSO".into()),
5821+
}),
5822+
);
5823+
}
5824+
5825+
if jwt_decode_token(&req.jwt).is_err() {
5826+
return (
5827+
StatusCode::UNAUTHORIZED,
5828+
wrap_json_hty_err::<String>(HtyErr {
5829+
code: HtyErrCode::AuthenticationFailed,
5830+
reason: Some("invalid jwt".into()),
5831+
}),
5832+
);
5833+
}
5834+
5835+
let code = uuid().replace('-', "");
5836+
let payload = SsoCodePayload {
5837+
jwt: req.jwt,
5838+
app: req.app,
5839+
};
5840+
let redis_key = format!("{}{}", SSO_REDIS_PREFIX, code);
5841+
match save_kv_to_redis_with_exp_secs(
5842+
&redis_key,
5843+
&serde_json::to_string(&payload).unwrap_or_default(),
5844+
SSO_CODE_TTL_SECS,
5845+
) {
5846+
Ok(_) => (StatusCode::OK, wrap_json_ok_resp(code)),
5847+
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, wrap_json_anyhow_err(e)),
5848+
}
5849+
}
5850+
5851+
async fn sso_exchange(host: HtyHostHeader, Json(req): Json<ReqSsoExchange>) -> impl IntoResponse {
5852+
let app_domain = (*host).clone();
5853+
if !is_sso_allowed_domain(&app_domain) {
5854+
return (
5855+
StatusCode::BAD_REQUEST,
5856+
wrap_json_hty_err::<String>(HtyErr {
5857+
code: HtyErrCode::WebErr,
5858+
reason: Some("HtyHost not allowed for SSO exchange".into()),
5859+
}),
5860+
);
5861+
}
5862+
5863+
let redis_key = format!("{}{}", SSO_REDIS_PREFIX, req.code);
5864+
let stored = match get_value_from_redis(&redis_key) {
5865+
Ok(v) => v,
5866+
Err(e) => {
5867+
return (
5868+
StatusCode::UNAUTHORIZED,
5869+
wrap_json_anyhow_err(e),
5870+
);
5871+
}
5872+
};
5873+
5874+
let _ = del_from_redis(&redis_key);
5875+
5876+
let payload: SsoCodePayload = match serde_json::from_str(&stored) {
5877+
Ok(p) => p,
5878+
Err(e) => {
5879+
return (
5880+
StatusCode::INTERNAL_SERVER_ERROR,
5881+
wrap_json_anyhow_err(anyhow!(e)),
5882+
);
5883+
}
5884+
};
5885+
5886+
if payload.app != app_domain {
5887+
return (
5888+
StatusCode::FORBIDDEN,
5889+
wrap_json_hty_err::<String>(HtyErr {
5890+
code: HtyErrCode::AuthenticationFailed,
5891+
reason: Some("SSO code app mismatch".into()),
5892+
}),
5893+
);
5894+
}
5895+
5896+
(StatusCode::OK, wrap_json_ok_resp(payload.jwt))
5897+
}
5898+
57695899
async fn raw_wx_qr_login(code: String, app_domain: String, db_pool: Arc<DbState>) -> anyhow::Result<String> {
57705900
debug!("raw_wx_qr_login -> domain: {:?} / code: {:?}", &app_domain, &code);
57715901

@@ -7199,6 +7329,9 @@ pub fn uc_rocket(db_url: &str) -> Router {
71997329
.route("/api/v1/uc/login_with_password", post(login_with_password))
72007330
.route("/api/v1/uc/login2_with_unionid", get(login2_with_unionid))
72017331
.route("/api/v1/uc/wx_qr_login", post(wx_qr_login))
7332+
.route("/api/v1/uc/sso/apps", get(sso_apps))
7333+
.route("/api/v1/uc/sso/issue", post(sso_issue))
7334+
.route("/api/v1/uc/sso/exchange", post(sso_exchange))
72027335
.route(
72037336
"/api/v1/uc/find_hty_resources_by_task_id/{task_id}",
72047337
get(find_hty_resources_by_task_id),

0 commit comments

Comments
 (0)