@@ -31,6 +31,7 @@ use htycommons::upyun::{generate_upyun_token, get_upyun_operator, get_upyun_pass
3131use 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 } ;
3232use htycommons:: wx:: { code2session, WxId , WxLogin , WxParams , WxSession } ;
3333use htycommons:: { db, uuid} ;
34+ use serde:: Deserialize ;
3435
3536use tracing:: { debug, warn, error} ;
3637use 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+
57695899async 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