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:
@@ -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
38
rc_auth/src/oauth/auth.rs
Normal 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"
|
||||
}
|
||||
9
rc_auth/src/oauth/jwks.rs
Normal file
9
rc_auth/src/oauth/jwks.rs
Normal 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
4
rc_auth/src/oauth/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod openid_config;
|
||||
pub mod jwks;
|
||||
pub mod auth;
|
||||
pub mod token;
|
||||
65
rc_auth/src/oauth/openid_config.rs
Normal file
65
rc_auth/src/oauth/openid_config.rs
Normal 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)
|
||||
}
|
||||
40
rc_auth/src/oauth/token.rs
Normal file
40
rc_auth/src/oauth/token.rs
Normal 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 {}),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
32
rc_auth/src/robocraft/displayname.rs
Normal file
32
rc_auth/src/robocraft/displayname.rs
Normal 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user