1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Implement federated login; #123

This commit is contained in:
NG (Graham)
2026-07-05 15:45:36 -04:00
committed by NGnius
parent 6601c188d0
commit 2adf8249bb
35 changed files with 1466 additions and 121 deletions

View File

@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
mod cli;
mod robocraft;
mod oauth;
use actix_web::{App, HttpServer, Responder};
@@ -56,7 +57,7 @@ async fn main() -> std::io::Result<()> {
App::new()
.wrap_fn(|req, srv| {
use actix_web::dev::Service;
log::trace!("Request {} {}", req.method(), req.path());
log::debug!("Request {} {}", req.method(), req.path());
srv.call(req)
})
.app_data(cli_args2.clone())
@@ -73,12 +74,18 @@ async fn main() -> std::io::Result<()> {
.service(robocraft::email::email_password_auth)
.service(robocraft::steam::steam_auth)
.service(robocraft::username::user_password_auth)
.service(robocraft::displayname::displaye_password_auth)
.service(robocraft::intercom::services_ws)
.service(robocraft::intercom::service_msg)
.service(robocraft::intercom::lobby_state_ws)
.service(robocraft::intercom::lobby_state_msg)
.service(robocraft::intercom::status_get)
.service(robocraft::intercom::status_set)
.service(oauth::openid_config::get_openid_configuration)
.service(oauth::jwks::get_oauth_jwks)
.service(oauth::auth::post_oauth_auth)
.service(oauth::auth::get_oauth_auth)
.service(oauth::token::post_oauth_token)
})
.bind((cli_args.ip, cli_args.port))?
.run()

38
rc_auth/src/oauth/auth.rs Normal file
View File

@@ -0,0 +1,38 @@
use actix_web::{get, post, web::{Data, Form, Query, Redirect}};
use serde::{Serialize, Deserialize};
use oj_rc_core::persist::user::FederatedAuthenticator;
#[derive(Serialize, Deserialize, Clone)]
struct AuthQuery {
pub response_type: Option<String>,
pub client_id: String,
pub redirect_uri: Option<String>,
pub scope: String,
pub state: String,
pub code_challenge: String,
pub code_challenge_method: String,
}
#[post("/authenticate/oauth2/auth")]
pub async fn post_oauth_auth(body: Form<oj_rc_core::persist::user::federation::FederatedAuthenticationPayload>, query: Query<AuthQuery>, config: Data<crate::robocraft::RcConfig>) -> impl actix_web::Responder {
let access_token = match config.account_provider.remote_auth(&body, &query.code_challenge).await {
Ok(x) => x,
Err(e) => {
log::error!("Failed to OAuth authenticate {} from {}: {}", body.display_name, body.domain_source, e.message);
return Redirect::to("/")
.temporary()
}
};
let redirect_root = query.redirect_uri.as_ref().map(|x| x.to_owned()).unwrap_or_else(|| "/authenticate/oauth2/auth".to_owned());
let redirect_url = format!("{}?code={}&state={}", redirect_root, access_token, query.state);
#[cfg(debug_assertions)]
log::debug!("Redirecting to {}", redirect_url);
Redirect::to(redirect_url)
.temporary()
}
#[get("/authenticate/oauth2/auth")]
pub async fn get_oauth_auth() -> &'static str {
"This is unimplemented and should not be used for standard OAuth flows"
}

View File

@@ -0,0 +1,9 @@
use actix_web::{get, web::Json};
use openidconnect::core::{CoreJsonWebKeySet, /*CoreJsonWebKey*/};
#[get("/authenticate/oauth2/jwks")]
pub async fn get_oauth_jwks() -> Json<CoreJsonWebKeySet> {
Json(CoreJsonWebKeySet::new(vec![
// TODO ???
]))
}

4
rc_auth/src/oauth/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod openid_config;
pub mod jwks;
pub mod auth;
pub mod token;

View File

@@ -0,0 +1,65 @@
use actix_web::{get, web::{Data, Json}};
use oj_rc_core::persist::user::federation::DiscoveryMetadata;
const OAUTH_AUTH_URL: &'static str = "authenticate/oauth2/auth";
const OAUTH_JWKS_URL: &'static str = "authenticate/oauth2/jwks";
const OAUTH_TOKEN_URL: &'static str = "authenticate/oauth2/token";
#[get("/.well-known/openid-configuration")]
pub async fn get_openid_configuration(server_config: Data<oj_rc_core::persist::config::ServerConfig>) -> Json<DiscoveryMetadata> {
let fallback_url = "http://127.0.0.1/fallback";
let auth_url = format!("{}/{}", server_config.auth_url, OAUTH_AUTH_URL);
let jwks_url = format!("{}/{}", server_config.auth_url, OAUTH_JWKS_URL); // TODO
let token_url = format!("{}/{}", server_config.auth_url, OAUTH_TOKEN_URL);
let meta = DiscoveryMetadata::new(
openidconnect::IssuerUrl::new(server_config.auth_url.clone())
.unwrap_or_else(|e| {
log::error!("Failed to parse issuer url {}: {}", server_config.auth_url, e);
openidconnect::IssuerUrl::new(fallback_url.to_owned()).unwrap()
}),
openidconnect::AuthUrl::new(auth_url.clone())
.unwrap_or_else(|e| {
log::error!("Failed to parse auth url {}: {}", auth_url, e);
openidconnect::AuthUrl::new(fallback_url.to_owned()).unwrap()
}),
openidconnect::JsonWebKeySetUrl::new(jwks_url.clone())
.unwrap_or_else(|e| {
log::error!("Failed to parse jwks url {}: {}", jwks_url, e);
openidconnect::JsonWebKeySetUrl::new(fallback_url.to_owned()).unwrap()
}),
vec![
openidconnect::ResponseTypes::new(vec![openidconnect::core::CoreResponseType::Code]),
openidconnect::ResponseTypes::new(vec![openidconnect::core::CoreResponseType::IdToken]),
],
vec![
openidconnect::core::CoreSubjectIdentifierType::Public,
],
vec![
//openidconnect::core::CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
openidconnect::core::CoreJwsSigningAlgorithm::HmacSha256,
],
openidconnect::EmptyAdditionalProviderMetadata::default(),
)
.set_token_endpoint(Some(openidconnect::TokenUrl::new(token_url)
.unwrap_or_else(|e| {
log::error!("Failed to parse token url {}: {}", server_config.auth_url, e);
openidconnect::TokenUrl::new(fallback_url.to_owned()).unwrap()
})
))
.set_scopes_supported(Some(vec![
openidconnect::Scope::new("openid".to_owned()),
openidconnect::Scope::new("read".to_owned()),
openidconnect::Scope::new("write".to_owned()),
openidconnect::Scope::new("federate".to_owned()),
]))
.set_claims_supported(Some(vec![
openidconnect::core::CoreClaimName::new("aud".to_owned()),
openidconnect::core::CoreClaimName::new("exp".to_owned()),
openidconnect::core::CoreClaimName::new("iat".to_owned()),
openidconnect::core::CoreClaimName::new("iss".to_owned()),
openidconnect::core::CoreClaimName::new("sub".to_owned()),
openidconnect::core::CoreClaimName::new("name".to_owned()),
openidconnect::core::CoreClaimName::new("preferred_username".to_owned()),
]));
Json(meta)
}

View File

@@ -0,0 +1,40 @@
use std::str::FromStr;
use actix_web::{post, web::{Data, Form, Json}};
use serde::{Serialize, Deserialize};
use oj_rc_core::persist::user::{FederatedAuthenticator, federation::TokenResponsePayload};
#[derive(Serialize, Deserialize, Clone)]
struct TokenQuery {
pub code: String,
pub client_id: String,
pub code_verifier: String,
}
#[post("/authenticate/oauth2/token")]
pub async fn post_oauth_token(body: Form<TokenQuery>, config: Data<crate::robocraft::RcConfig>) -> Json<TokenResponsePayload> {
// TODO make errors compliant with OAuth2 spec
match config.account_provider.remote_token(&body.code, &body.code_verifier).await {
Ok(login_info) => {
let mut token_resp = TokenResponsePayload::new(
openidconnect::AccessToken::new(login_info.response.token.clone()),
openidconnect::core::CoreTokenType::Bearer,
openidconnect::core::CoreIdTokenFields::new(
Some(openidconnect::IdToken::from_str(&login_info.response.token).expect("Bad token")),
openidconnect::EmptyExtraTokenFields {},
),
);
token_resp.set_refresh_token(Some(openidconnect::RefreshToken::new(login_info.response.refresh_token)));
Json(token_resp)
},
Err(e) => {
log::error!("Failed OAuth2 token auth: {}", e.message);
Json(TokenResponsePayload::new(
openidconnect::AccessToken::new(String::default()),
openidconnect::core::CoreTokenType::Bearer,
openidconnect::core::CoreIdTokenFields::new(None, openidconnect::EmptyExtraTokenFields {}),
))
}
}
}

View File

@@ -0,0 +1,32 @@
use oj_rc_core::persist::user::FederatedAuthenticator;
use actix_web::{post, web::{Data, Json}};
#[post("/authenticate/displayname/game")]
pub async fn displaye_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: Data<super::RcConfig>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, super::ErrorTy> {
if body.display_name.is_none() {
return Err(super::ErrorTy::from_err(oj_rc_core::persist::user::AuthError {
message: "Missing display_name".to_owned(),
code: oj_rc_core::data::error_codes::AuthErrorCode::BadCredentials,
}));
}
let display_name = body.display_name.clone().unwrap();
if let Some((display_name, domain)) = display_name.split_once('#') {
log::info!("Authenticating {} user {} for domain {}", body.target, display_name, domain);
let user_info = oj_rc_core::persist::user::FederatedAuthInfo {
display_name: display_name.to_owned(),
password: body.password.clone(),
domain: domain.to_owned(),
};
let response = config.account_provider.local_login(user_info).await
.map_err(|e| {
log::error!("Failed to authenticate {} user {}#{}: {}", body.target, display_name, domain, e.message);
super::ErrorTy::from_err(e)
})?;
Ok(Json(response.response))
} else {
Err(super::ErrorTy::from_err(oj_rc_core::persist::user::AuthError {
message: "Missing display_name domain".to_owned(),
code: oj_rc_core::data::error_codes::AuthErrorCode::InvalidDisplayName,
}))
}
}

View File

@@ -4,7 +4,8 @@ use actix_web::{rt, web::{Payload, Data, Path, Json}, Error, HttpRequest, HttpRe
#[get("/intercom/.oj_services/{name}")]
pub async fn services_ws(req: HttpRequest, stream: Payload, auth: Data<super::IntercomAuth>, reg: Data<super::Users>, name: Path<String>) -> Result<HttpResponse, Error> {
auth.validate(&req, &format!(".oj_services/{}", name))?;
log::debug!("intercom/.oj_services name is {}", name);
auth.validate(&req, &format!(".oj_services/{}", urlencoding::encode(&*name)))?;
let (res, mut session, _stream) = actix_ws::handle(&req, stream)?;
/*let mut stream = stream

View File

@@ -3,6 +3,7 @@ pub mod registration;
pub mod steam;
pub mod username;
pub mod intercom;
pub mod displayname;
pub struct RcConfig {
//pub data: std::path::PathBuf,