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

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 {}),
))
}
}
}