Skip to content

Commit ed888d0

Browse files
authored
Merge pull request #268 from seth-shi/feature/oauth-login
feat: 添加OAuth2.0认证支持
2 parents 29299af + 861d0f5 commit ed888d0

File tree

17 files changed

+614
-4
lines changed

17 files changed

+614
-4
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ ldap3 = { version="0.11", default-features = false, features = ["tls-rustls"] }
124124
reqwest = {version="0.11", default-features = false, features = ["stream", "rustls-tls", "json"]}
125125
rand = "0.8"
126126
serde_yml = "0.0.12"
127+
oauth2 = "4.4"
127128

128129
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os="windows"))'.dependencies]
129130
fs2 = "0.4.3"

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ helm install r-nacos rnacos/rnacos
207207
|RNACOS_LDAP_USER_ADMIN_GROUP|LDAP管理员角色包含的用户组(多个用逗号分隔,用户只要包含一个就是管理员)|空集合|admin_group1,admin_group2|0.6.19|
208208
|RNACOS_LDAP_USER_DEFAULT_ROLE|LDAP用户默认角色,支持的值有:访客:VISITOR,开发者:DEVELOPER,管理员:ADMIN|VISITOR|DEVELOPER|0.6.19|
209209
|RNACOS_MCP_HTTP_TIMEOUT_SECOND|MCP服务HTTP请求超时时间,单位为秒|30|60|0.7.3|
210+
|RNACOS_OAUTH2_ENABLE|是否启用OAuth2.0认证|false|true|x|
211+
|RNACOS_OAUTH2_CLIENT_ID|OAuth2.0客户端ID|空字符串|your_client_id|x|
212+
|RNACOS_OAUTH2_CLIENT_SECRET|OAuth2.0客户端密钥|空字符串|your_client_secret|x|
213+
|RNACOS_OAUTH2_REDIRECT_URI|OAuth2.0回调完整URI(只修改域名部分即可)|空字符串|http://localhost:10848/rnacos/p/login|x|
214+
|RNACOS_OAUTH2_AUTHORIZATION_URL|OAuth2.0授权端点完整URL|空字符串|https://oauth.example.com/oauth/authorize|x|
215+
|RNACOS_OAUTH2_TOKEN_URL|OAuth2.0 Token端点完整URL|空字符串|https://oauth.example.com/oauth/token|x|
216+
|RNACOS_OAUTH2_USERINFO_URL|OAuth2.0用户信息端点完整URL|空字符串|https://oauth.example.com/oauth/userinfo|x|
217+
|RNACOS_OAUTH2_SCOPES|OAuth2.0请求的权限范围|openid profile|openid profile email|x|
218+
|RNACOS_OAUTH2_USERNAME_CLAIM_NAME|OAuth2.0用户名claim字段名|username|username|x|
219+
|RNACOS_OAUTH2_NICKNAME_CLAIM_NAME|OAuth2.0昵称claim字段名|name|name|x|
220+
|RNACOS_OAUTH2_USER_DEFAULT_ROLE|OAuth2.0用户默认角色,支持的值有:访客:VISITOR,开发者:DEVELOPER,管理员:ADMIN|DEVELOPER|VISITOR|x|
221+
|RNACOS_OAUTH2_BUTTON|OAuth2.0登录按钮显示文本|OAuth2.0 登录|OAuth2.0 登录|x|
210222

211223
启动配置方式可以参考: [运行参数说明](https://r-nacos.github.io/docs/notes/env_config/)
212224

@@ -437,6 +449,8 @@ nacos_rust_client = "0.3.0"
437449
1. 支持管理用户列表
438450
2. 支持用户角色权限管理
439451
3. 支持用户密码重置
452+
4. 支持LDAP认证登录
453+
5. 支持OAuth2.0认证登录
440454

441455
命名空间:
442456

src/common/appdata.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::namespace::NamespaceActor;
1010
use crate::naming::cluster::node_manage::{InnerNodeManage, NodeManage};
1111
use crate::naming::cluster::route::NamingRoute;
1212
use crate::naming::core::NamingActor;
13+
use crate::oauth2::core::OAuth2Manager;
1314
use crate::raft::cache::route::CacheRoute;
1415
use crate::raft::cache::CacheManager;
1516
use crate::raft::cluster::route::{ConfigRoute, RaftRequestRoute};
@@ -54,6 +55,7 @@ pub struct AppShareData {
5455
pub transfer_import_manager: Addr<TransferImportManager>,
5556
pub health_manager: Addr<HealthManager>,
5657
pub ldap_manager: Addr<LdapManager>,
58+
pub oauth2_manager: Addr<OAuth2Manager>,
5759
pub sequence_db_manager: Addr<SequenceDbManager>,
5860
pub sequence_manager: Addr<SequenceManager>,
5961
pub mcp_manager: Addr<McpManager>,

src/common/mod.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::common::string_utils::StringUtils;
22
use crate::ldap::model::LdapConfig;
3+
use crate::oauth2::model::OAuth2Config;
34
use crate::user::permission;
45
use crate::user::permission::UserRoleHelper;
56
use std::collections::HashSet;
@@ -103,6 +104,19 @@ pub struct AppSysConfig {
103104
pub ldap_user_admin_groups: Arc<HashSet<String>>,
104105
pub ldap_user_default_role: Arc<String>,
105106
pub mcp_http_timeout: u64,
107+
pub oauth2_enable: bool,
108+
pub oauth2_server_url: Arc<String>,
109+
pub oauth2_client_id: Arc<String>,
110+
pub oauth2_client_secret: Arc<String>,
111+
pub oauth2_authorization_url: Arc<String>,
112+
pub oauth2_token_url: Arc<String>,
113+
pub oauth2_userinfo_url: Arc<String>,
114+
pub oauth2_redirect_uri: Arc<String>,
115+
pub oauth2_scopes: Arc<String>,
116+
pub oauth2_username_claim_name: Arc<String>,
117+
pub oauth2_nickname_claim_name: Arc<String>,
118+
pub oauth2_user_default_role: Arc<String>,
119+
pub oauth2_button: Arc<String>,
106120
}
107121

108122
impl AppSysConfig {
@@ -265,6 +279,62 @@ impl AppSysConfig {
265279
.unwrap_or("30".to_owned())
266280
.parse()
267281
.unwrap_or(30);
282+
let oauth2_enable = std::env::var("RNACOS_OAUTH2_ENABLE")
283+
.unwrap_or("false".to_owned())
284+
.parse()
285+
.unwrap_or(false);
286+
let oauth2_server_url = std::env::var("RNACOS_OAUTH2_SERVER_URL")
287+
.map(Arc::new)
288+
.unwrap_or(constant::EMPTY_ARC_STRING.clone());
289+
let oauth2_client_id = std::env::var("RNACOS_OAUTH2_CLIENT_ID")
290+
.map(Arc::new)
291+
.unwrap_or(constant::EMPTY_ARC_STRING.clone());
292+
let oauth2_client_secret = std::env::var("RNACOS_OAUTH2_CLIENT_SECRET")
293+
.map(Arc::new)
294+
.unwrap_or(constant::EMPTY_ARC_STRING.clone());
295+
// OAuth2 endpoints should be full URLs
296+
let oauth2_authorization_url = std::env::var("RNACOS_OAUTH2_AUTHORIZATION_URL")
297+
.map(Arc::new)
298+
.unwrap_or_else(|_| {
299+
let server_url = std::env::var("RNACOS_OAUTH2_SERVER_URL")
300+
.unwrap_or_default();
301+
Arc::new(format!("{}/oauth/authorize", server_url))
302+
});
303+
let oauth2_token_url = std::env::var("RNACOS_OAUTH2_TOKEN_URL")
304+
.map(Arc::new)
305+
.unwrap_or_else(|_| {
306+
let server_url = std::env::var("RNACOS_OAUTH2_SERVER_URL")
307+
.unwrap_or_default();
308+
Arc::new(format!("{}/oauth/token", server_url))
309+
});
310+
let oauth2_userinfo_url = std::env::var("RNACOS_OAUTH2_USERINFO_URL")
311+
.map(Arc::new)
312+
.unwrap_or_else(|_| {
313+
let server_url = std::env::var("RNACOS_OAUTH2_SERVER_URL")
314+
.unwrap_or_default();
315+
Arc::new(format!("{}/oauth/userinfo", server_url))
316+
});
317+
let oauth2_redirect_uri = std::env::var("RNACOS_OAUTH2_REDIRECT_URI")
318+
.map(Arc::new)
319+
.unwrap_or(constant::EMPTY_ARC_STRING.clone());
320+
let oauth2_scopes = std::env::var("RNACOS_OAUTH2_SCOPES")
321+
.map(Arc::new)
322+
.unwrap_or_else(|_| Arc::new("openid profile".to_string()));
323+
let oauth2_username_claim_name = std::env::var("RNACOS_OAUTH2_USERNAME_CLAIM_NAME")
324+
.map(Arc::new)
325+
.unwrap_or_else(|_| Arc::new("username".to_string()));
326+
let oauth2_nickname_claim_name = std::env::var("RNACOS_OAUTH2_NICKNAME_CLAIM_NAME")
327+
.map(Arc::new)
328+
.unwrap_or_else(|_| Arc::new("name".to_string()));
329+
let oauth2_user_default_role = std::env::var("RNACOS_OAUTH2_USER_DEFAULT_ROLE")
330+
.map(|v| {
331+
let upper = v.to_uppercase();
332+
UserRoleHelper::get_role_by_name(&upper, permission::USER_ROLE_DEVELOPER.clone())
333+
})
334+
.unwrap_or(permission::USER_ROLE_DEVELOPER.clone());
335+
let oauth2_button = std::env::var("RNACOS_OAUTH2_BUTTON")
336+
.map(Arc::new)
337+
.unwrap_or_else(|_| Arc::new("OAuth2.0 登录".to_string()));
268338
Self {
269339
local_db_dir,
270340
config_db_file,
@@ -305,6 +375,19 @@ impl AppSysConfig {
305375
ldap_user_admin_groups,
306376
ldap_user_default_role,
307377
mcp_http_timeout,
378+
oauth2_enable,
379+
oauth2_server_url,
380+
oauth2_client_id,
381+
oauth2_client_secret,
382+
oauth2_authorization_url,
383+
oauth2_token_url,
384+
oauth2_userinfo_url,
385+
oauth2_redirect_uri,
386+
oauth2_scopes,
387+
oauth2_username_claim_name,
388+
oauth2_nickname_claim_name,
389+
oauth2_user_default_role,
390+
oauth2_button,
308391
}
309392
}
310393

@@ -352,6 +435,22 @@ impl AppSysConfig {
352435
ldap_user_default_role: self.ldap_user_default_role.clone(),
353436
})
354437
}
438+
439+
pub fn get_oauth2_config(&self) -> Arc<OAuth2Config> {
440+
Arc::new(OAuth2Config {
441+
oauth2_server_url: self.oauth2_server_url.clone(),
442+
oauth2_client_id: self.oauth2_client_id.clone(),
443+
oauth2_client_secret: self.oauth2_client_secret.clone(),
444+
oauth2_authorization_url: self.oauth2_authorization_url.clone(),
445+
oauth2_token_url: self.oauth2_token_url.clone(),
446+
oauth2_userinfo_url: self.oauth2_userinfo_url.clone(),
447+
oauth2_redirect_uri: self.oauth2_redirect_uri.clone(),
448+
oauth2_scopes: self.oauth2_scopes.clone(),
449+
oauth2_username_claim_name: self.oauth2_username_claim_name.clone(),
450+
oauth2_nickname_claim_name: self.oauth2_nickname_claim_name.clone(),
451+
oauth2_user_default_role: self.oauth2_user_default_role.clone(),
452+
})
453+
}
355454
}
356455

357456
/**

src/console/api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ pub fn console_api_config_v2(config: &mut web::ServiceConfig) {
224224
web::resource("/login/captcha").route(web::get().to(v2::login_api::gen_captcha)),
225225
)
226226
.service(web::resource("/login/logout").route(web::post().to(v2::login_api::logout)))
227+
.service(web::resource("/login/config").route(web::get().to(login_api::get_login_config)))
228+
.service(web::resource("/login/oauth2/login").route(web::post().to(v2::login_api::oauth2_callback)))
227229
.service(web::resource("/user/info").route(web::get().to(v2::user_api::get_user_info)))
228230
.service(
229231
web::resource("/user/list").route(web::get().to(v2::user_api::get_user_page_list)),

src/console/login_api.rs

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashMap;
22
use std::sync::Arc;
33

4-
use super::model::login_model::{LoginParam, LoginToken};
4+
use super::model::login_model::{LoginConfig, LoginParam, LoginToken};
55
use crate::ldap::model::actor_model::{LdapMsgReq, LdapMsgResult};
66
use crate::ldap::model::LdapUserParam;
7+
use crate::oauth2::model::actor_model::{OAuth2MsgReq, OAuth2MsgResult};
8+
use crate::oauth2::model::OAuth2UserParam;
79
use crate::{
810
common::{
911
appdata::AppShareData,
@@ -17,12 +19,13 @@ use crate::{
1719
},
1820
user::{UserManagerReq, UserManagerResult},
1921
};
20-
use actix_web::http::header;
22+
use actix_web::http::{header, StatusCode};
2123
use actix_web::{
2224
cookie::Cookie,
2325
web::{self, Data},
2426
HttpRequest, HttpResponse, Responder,
2527
};
28+
use serde::Deserialize;
2629
use captcha::filters::{Grid, Noise};
2730
use captcha::Captcha;
2831

@@ -82,7 +85,10 @@ pub async fn login(
8285
}));
8386
}
8487
}
85-
if !app.sys_config.ldap_enable && session.is_none() {
88+
if !app.sys_config.ldap_enable
89+
&& !app.sys_config.oauth2_enable
90+
&& session.is_none()
91+
{
8692
return HttpResponse::Ok().json(ApiResult::<()>::error(error_code, None));
8793
}
8894
} else {
@@ -107,6 +113,112 @@ pub async fn login(
107113
HttpResponse::Ok().json(ApiResult::<()>::error(error_code, None))
108114
}
109115

116+
#[derive(Deserialize)]
117+
pub struct OAuth2CallbackQuery {
118+
code: String,
119+
state: Option<String>,
120+
}
121+
122+
pub async fn oauth2_callback(
123+
_request: HttpRequest,
124+
app: Data<Arc<AppShareData>>,
125+
param: web::Json<OAuth2CallbackQuery>,
126+
) -> HttpResponse {
127+
if !app.sys_config.oauth2_enable {
128+
return HttpResponse::Ok()
129+
.status(StatusCode::NOT_FOUND)
130+
.json(ApiResult::<()>::error(
131+
"OAUTH2_NOT_ENABLED".to_owned(),
132+
None,
133+
));
134+
}
135+
136+
let limit_key = Arc::new(format!("USER_L#oauth2"));
137+
if let Some(value) = login_limit(&app, &limit_key).await {
138+
return value;
139+
}
140+
141+
let res = app
142+
.oauth2_manager
143+
.send(OAuth2MsgReq::Authenticate(OAuth2UserParam {
144+
code: param.code.clone(),
145+
state: param.state.clone(),
146+
}))
147+
.await;
148+
149+
let session = match res {
150+
Ok(Ok(OAuth2MsgResult::UserMeta(meta))) => {
151+
Some(Arc::new(UserSession {
152+
username: Arc::new(meta.user_name.clone()),
153+
nickname: Some(meta.user_name),
154+
roles: vec![meta.role],
155+
namespace_privilege: meta.namespace_privilege,
156+
extend_infos: HashMap::default(),
157+
refresh_time: now_second_i32() as u32,
158+
}))
159+
}
160+
Ok(Ok(OAuth2MsgResult::None)) => None,
161+
Ok(Ok(OAuth2MsgResult::AuthorizeUrl(_))) => {
162+
// This should not happen in callback
163+
return HttpResponse::Ok().json(ApiResult::<()>::error(
164+
"OAUTH2_AUTH_ERROR".to_owned(),
165+
Some("Unexpected response type".to_owned()),
166+
));
167+
}
168+
Ok(Err(e)) => {
169+
log::error!("OAuth2 authentication error: {}", e);
170+
return HttpResponse::Ok().json(ApiResult::<()>::error(
171+
"OAUTH2_AUTH_ERROR".to_owned(),
172+
Some(e.to_string()),
173+
));
174+
}
175+
Err(e) => {
176+
log::error!("OAuth2 manager error: {}", e);
177+
return HttpResponse::Ok().json(ApiResult::<()>::error(
178+
"SYSTEM_ERROR".to_owned(),
179+
Some(e.to_string()),
180+
));
181+
}
182+
};
183+
184+
if let Some(session) = session {
185+
if let Some(value) = apply_session(app, limit_key, session) {
186+
return value;
187+
}
188+
}
189+
190+
HttpResponse::Ok().json(ApiResult::<()>::error(
191+
"OAUTH2_AUTH_ERROR".to_owned(),
192+
None,
193+
))
194+
}
195+
196+
pub async fn get_login_config(app: Data<Arc<AppShareData>>) -> actix_web::Result<impl Responder> {
197+
let mut oauth2_button = None;
198+
let mut oauth2_authorize_url = None;
199+
200+
if app.sys_config.oauth2_enable && !app.sys_config.oauth2_button.is_empty() {
201+
oauth2_button = Some(app.sys_config.oauth2_button.as_ref().clone());
202+
203+
// Generate OAuth2 authorize URL
204+
let res = app
205+
.oauth2_manager
206+
.send(OAuth2MsgReq::GetAuthorizeUrl)
207+
.await;
208+
209+
if let Ok(Ok(OAuth2MsgResult::AuthorizeUrl(auth_url))) = res {
210+
oauth2_authorize_url = Some(auth_url);
211+
}
212+
}
213+
214+
let config = LoginConfig {
215+
oauth2_enable: app.sys_config.oauth2_enable,
216+
oauth2_button,
217+
oauth2_authorize_url,
218+
};
219+
Ok(HttpResponse::Ok().json(ApiResult::success(Some(config))))
220+
}
221+
110222
fn apply_session(
111223
app: Data<Arc<AppShareData>>,
112224
limit_key: Arc<String>,

src/console/middle/login_middle.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ lazy_static::lazy_static! {
2525
"/rnacos/p/login", "/rnacos/404",
2626
"/rnacos/api/console/login/login", "/rnacos/api/console/login/captcha",
2727
"/rnacos/api/console/v2/login/login", "/rnacos/api/console/v2/login/captcha",
28+
"/rnacos/api/console/v2/login/config", "/rnacos/api/console/v2/login/oauth2/login",
2829
];
2930
pub static ref STATIC_FILE_PATH: Regex= Regex::new(r"(?i).*\.(js|css|png|jpg|jpeg|bmp|svg)").unwrap();
3031
pub static ref API_PATH: Regex = Regex::new(r"(?i)/(api|nacos)/.*").unwrap();

src/console/model/login_model.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,11 @@ pub struct LoginParam {
1414
pub struct LoginToken {
1515
pub token: String,
1616
}
17+
18+
#[derive(Debug, Default, Serialize, Deserialize)]
19+
#[serde(rename_all = "camelCase")]
20+
pub struct LoginConfig {
21+
pub oauth2_enable: bool,
22+
pub oauth2_button: Option<String>,
23+
pub oauth2_authorize_url: Option<String>,
24+
}

src/console/v2/login_api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pub use crate::console::login_api::{gen_captcha, login, logout};
1+
pub use crate::console::login_api::{gen_captcha, get_login_config, login, logout, oauth2_callback};

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod transfer;
1717

1818
pub mod ldap;
1919
pub mod mcp;
20+
pub mod oauth2;
2021
pub mod sequence;
2122

2223
pub use inner_mem_cache::TimeoutSet;

0 commit comments

Comments
 (0)