mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-01-25 08:18:18 +00:00
797aac7281
* Check for error when fetching link metadata (fixes #5127) * use error_for_status everywhere * dont ignore errors * enable lint * fixes * review * more review
577 lines
18 KiB
Rust
577 lines
18 KiB
Rust
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
|
|
use actix_web::{web::Json, HttpRequest};
|
|
use lemmy_api_common::{
|
|
claims::Claims,
|
|
context::LemmyContext,
|
|
oauth_provider::AuthenticateWithOauth,
|
|
person::{LoginResponse, Register},
|
|
utils::{
|
|
check_email_verified,
|
|
check_registration_application,
|
|
check_user_valid,
|
|
generate_inbox_url,
|
|
generate_local_apub_endpoint,
|
|
honeypot_check,
|
|
local_site_to_slur_regex,
|
|
password_length_check,
|
|
send_new_applicant_email_to_admins,
|
|
send_verification_email,
|
|
EndpointType,
|
|
},
|
|
};
|
|
use lemmy_db_schema::{
|
|
aggregates::structs::PersonAggregates,
|
|
newtypes::{InstanceId, OAuthProviderId},
|
|
source::{
|
|
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
|
|
language::Language,
|
|
local_site::LocalSite,
|
|
local_user::{LocalUser, LocalUserInsertForm},
|
|
local_user_vote_display_mode::LocalUserVoteDisplayMode,
|
|
oauth_account::{OAuthAccount, OAuthAccountInsertForm},
|
|
oauth_provider::OAuthProvider,
|
|
person::{Person, PersonInsertForm},
|
|
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
|
|
},
|
|
traits::Crud,
|
|
RegistrationMode,
|
|
};
|
|
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
use lemmy_utils::{
|
|
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
|
|
utils::{
|
|
slurs::{check_slurs, check_slurs_opt},
|
|
validation::is_valid_actor_name,
|
|
},
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_with::skip_serializing_none;
|
|
use std::collections::HashSet;
|
|
|
|
#[skip_serializing_none]
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
|
/// Response from OAuth token endpoint
|
|
struct TokenResponse {
|
|
pub access_token: String,
|
|
pub token_type: String,
|
|
pub expires_in: Option<i64>,
|
|
pub refresh_token: Option<String>,
|
|
pub scope: Option<String>,
|
|
}
|
|
|
|
pub async fn register(
|
|
data: Json<Register>,
|
|
req: HttpRequest,
|
|
context: Data<LemmyContext>,
|
|
) -> LemmyResult<Json<LoginResponse>> {
|
|
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
|
let local_site = site_view.local_site;
|
|
let require_registration_application =
|
|
local_site.registration_mode == RegistrationMode::RequireApplication;
|
|
|
|
if local_site.registration_mode == RegistrationMode::Closed {
|
|
Err(LemmyErrorType::RegistrationClosed)?
|
|
}
|
|
|
|
password_length_check(&data.password)?;
|
|
honeypot_check(&data.honeypot)?;
|
|
|
|
if local_site.require_email_verification && data.email.is_none() {
|
|
Err(LemmyErrorType::EmailRequired)?
|
|
}
|
|
|
|
// make sure the registration answer is provided when the registration application is required
|
|
if local_site.site_setup {
|
|
validate_registration_answer(require_registration_application, &data.answer)?;
|
|
}
|
|
|
|
// Make sure passwords match
|
|
if data.password != data.password_verify {
|
|
Err(LemmyErrorType::PasswordsDoNotMatch)?
|
|
}
|
|
|
|
if local_site.site_setup && local_site.captcha_enabled {
|
|
let uuid = uuid::Uuid::parse_str(&data.captcha_uuid.clone().unwrap_or_default())?;
|
|
CaptchaAnswer::check_captcha(
|
|
&mut context.pool(),
|
|
CheckCaptchaAnswer {
|
|
uuid,
|
|
answer: data.captcha_answer.clone().unwrap_or_default(),
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
let slur_regex = local_site_to_slur_regex(&local_site);
|
|
check_slurs(&data.username, &slur_regex)?;
|
|
check_slurs_opt(&data.answer, &slur_regex)?;
|
|
|
|
Person::check_username_taken(&mut context.pool(), &data.username).await?;
|
|
|
|
if let Some(email) = &data.email {
|
|
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
|
|
}
|
|
|
|
// We have to create both a person, and local_user
|
|
let inserted_person = create_person(
|
|
data.username.clone(),
|
|
&local_site,
|
|
site_view.site.instance_id,
|
|
&context,
|
|
)
|
|
.await?;
|
|
|
|
// Automatically set their application as accepted, if they created this with open registration.
|
|
// Also fixes a bug which allows users to log in when registrations are changed to closed.
|
|
let accepted_application = Some(!require_registration_application);
|
|
|
|
// Show nsfw content if param is true, or if content_warning exists
|
|
let show_nsfw = data
|
|
.show_nsfw
|
|
.unwrap_or(site_view.site.content_warning.is_some());
|
|
|
|
let language_tags = get_language_tags(&req);
|
|
|
|
// Create the local user
|
|
let local_user_form = LocalUserInsertForm {
|
|
email: data.email.as_deref().map(str::to_lowercase),
|
|
show_nsfw: Some(show_nsfw),
|
|
accepted_application,
|
|
default_listing_type: Some(local_site.default_post_listing_type),
|
|
post_listing_mode: Some(local_site.default_post_listing_mode),
|
|
interface_language: language_tags.first().cloned(),
|
|
// If its the initial site setup, they are an admin
|
|
admin: Some(!local_site.site_setup),
|
|
..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string()))
|
|
};
|
|
|
|
let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?;
|
|
|
|
if local_site.site_setup && require_registration_application {
|
|
if let Some(answer) = data.answer.clone() {
|
|
// Create the registration application
|
|
let form = RegistrationApplicationInsertForm {
|
|
local_user_id: inserted_local_user.id,
|
|
answer,
|
|
};
|
|
|
|
RegistrationApplication::create(&mut context.pool(), &form).await?;
|
|
}
|
|
}
|
|
|
|
// Email the admins, only if email verification is not required
|
|
if local_site.application_email_admins && !local_site.require_email_verification {
|
|
send_new_applicant_email_to_admins(&data.username, &mut context.pool(), context.settings())
|
|
.await?;
|
|
}
|
|
|
|
let mut login_response = LoginResponse {
|
|
jwt: None,
|
|
registration_created: false,
|
|
verify_email_sent: false,
|
|
};
|
|
|
|
// Log the user in directly if the site is not setup, or email verification and application aren't
|
|
// required
|
|
if !local_site.site_setup
|
|
|| (!require_registration_application && !local_site.require_email_verification)
|
|
{
|
|
let jwt = Claims::generate(inserted_local_user.id, req, &context).await?;
|
|
login_response.jwt = Some(jwt);
|
|
} else {
|
|
login_response.verify_email_sent = send_verification_email_if_required(
|
|
&context,
|
|
&local_site,
|
|
&inserted_local_user,
|
|
&inserted_person,
|
|
)
|
|
.await?;
|
|
|
|
if require_registration_application {
|
|
login_response.registration_created = true;
|
|
}
|
|
}
|
|
|
|
Ok(Json(login_response))
|
|
}
|
|
|
|
#[tracing::instrument(skip(context))]
|
|
pub async fn authenticate_with_oauth(
|
|
data: Json<AuthenticateWithOauth>,
|
|
req: HttpRequest,
|
|
context: Data<LemmyContext>,
|
|
) -> LemmyResult<Json<LoginResponse>> {
|
|
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
|
let local_site = site_view.local_site.clone();
|
|
|
|
// validate inputs
|
|
if data.oauth_provider_id == OAuthProviderId(0) || data.code.is_empty() || data.code.len() > 300 {
|
|
return Err(LemmyErrorType::OauthAuthorizationInvalid)?;
|
|
}
|
|
|
|
// validate the redirect_uri
|
|
let redirect_uri = &data.redirect_uri;
|
|
if redirect_uri.host_str().unwrap_or("").is_empty()
|
|
|| !redirect_uri.path().eq(&String::from("/oauth/callback"))
|
|
|| !redirect_uri.query().unwrap_or("").is_empty()
|
|
{
|
|
Err(LemmyErrorType::OauthAuthorizationInvalid)?
|
|
}
|
|
|
|
// Fetch the OAUTH provider and make sure it's enabled
|
|
let oauth_provider_id = data.oauth_provider_id;
|
|
let oauth_provider = OAuthProvider::read(&mut context.pool(), oauth_provider_id)
|
|
.await
|
|
.ok()
|
|
.ok_or(LemmyErrorType::OauthAuthorizationInvalid)?;
|
|
|
|
if !oauth_provider.enabled {
|
|
return Err(LemmyErrorType::OauthAuthorizationInvalid)?;
|
|
}
|
|
|
|
let token_response =
|
|
oauth_request_access_token(&context, &oauth_provider, &data.code, redirect_uri.as_str())
|
|
.await?;
|
|
|
|
let user_info = oidc_get_user_info(
|
|
&context,
|
|
&oauth_provider,
|
|
token_response.access_token.as_str(),
|
|
)
|
|
.await?;
|
|
|
|
let oauth_user_id = read_user_info(&user_info, oauth_provider.id_claim.as_str())?;
|
|
|
|
let mut login_response = LoginResponse {
|
|
jwt: None,
|
|
registration_created: false,
|
|
verify_email_sent: false,
|
|
};
|
|
|
|
// Lookup user by oauth_user_id
|
|
let mut local_user_view =
|
|
LocalUserView::find_by_oauth_id(&mut context.pool(), oauth_provider.id, &oauth_user_id).await;
|
|
|
|
let local_user: LocalUser;
|
|
if let Ok(user_view) = local_user_view {
|
|
// user found by oauth_user_id => Login user
|
|
local_user = user_view.clone().local_user;
|
|
|
|
check_user_valid(&user_view.person)?;
|
|
check_email_verified(&user_view, &site_view)?;
|
|
check_registration_application(&user_view, &site_view.local_site, &mut context.pool()).await?;
|
|
} else {
|
|
// user has never previously registered using oauth
|
|
|
|
// prevent registration if registration is closed
|
|
if local_site.registration_mode == RegistrationMode::Closed {
|
|
Err(LemmyErrorType::RegistrationClosed)?
|
|
}
|
|
|
|
// prevent registration if registration is closed for OAUTH providers
|
|
if !local_site.oauth_registration {
|
|
return Err(LemmyErrorType::OauthRegistrationClosed)?;
|
|
}
|
|
|
|
// Extract the OAUTH email claim from the returned user_info
|
|
let email = read_user_info(&user_info, "email")?;
|
|
|
|
let require_registration_application =
|
|
local_site.registration_mode == RegistrationMode::RequireApplication;
|
|
|
|
// Lookup user by OAUTH email and link accounts
|
|
local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await;
|
|
|
|
let person;
|
|
if let Ok(user_view) = local_user_view {
|
|
// user found by email => link and login if linking is allowed
|
|
|
|
// we only allow linking by email when email_verification is required otherwise emails cannot
|
|
// be trusted
|
|
if oauth_provider.account_linking_enabled && site_view.local_site.require_email_verification {
|
|
// WARNING:
|
|
// If an admin switches the require_email_verification config from false to true,
|
|
// users who signed up before the switch could have accounts with unverified emails falsely
|
|
// marked as verified.
|
|
|
|
check_user_valid(&user_view.person)?;
|
|
check_email_verified(&user_view, &site_view)?;
|
|
check_registration_application(&user_view, &site_view.local_site, &mut context.pool())
|
|
.await?;
|
|
|
|
// Link with OAUTH => Login user
|
|
let oauth_account_form =
|
|
OAuthAccountInsertForm::new(user_view.local_user.id, oauth_provider.id, oauth_user_id);
|
|
|
|
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
|
|
|
|
local_user = user_view.local_user.clone();
|
|
} else {
|
|
return Err(LemmyErrorType::EmailAlreadyExists)?;
|
|
}
|
|
} else {
|
|
// No user was found by email => Register as new user
|
|
|
|
// make sure the registration answer is provided when the registration application is required
|
|
validate_registration_answer(require_registration_application, &data.answer)?;
|
|
|
|
// make sure the username is provided
|
|
let username = data
|
|
.username
|
|
.as_ref()
|
|
.ok_or(LemmyErrorType::RegistrationUsernameRequired)?;
|
|
|
|
let slur_regex = local_site_to_slur_regex(&local_site);
|
|
check_slurs(username, &slur_regex)?;
|
|
check_slurs_opt(&data.answer, &slur_regex)?;
|
|
|
|
Person::check_username_taken(&mut context.pool(), username).await?;
|
|
|
|
// We have to create a person, a local_user, and an oauth_account
|
|
person = create_person(
|
|
username.clone(),
|
|
&local_site,
|
|
site_view.site.instance_id,
|
|
&context,
|
|
)
|
|
.await?;
|
|
|
|
// Show nsfw content if param is true, or if content_warning exists
|
|
let show_nsfw = data
|
|
.show_nsfw
|
|
.unwrap_or(site_view.site.content_warning.is_some());
|
|
|
|
let language_tags = get_language_tags(&req);
|
|
|
|
// Create the local user
|
|
let local_user_form = LocalUserInsertForm {
|
|
email: Some(str::to_lowercase(&email)),
|
|
show_nsfw: Some(show_nsfw),
|
|
accepted_application: Some(!require_registration_application),
|
|
email_verified: Some(oauth_provider.auto_verify_email),
|
|
post_listing_mode: Some(local_site.default_post_listing_mode),
|
|
interface_language: language_tags.first().cloned(),
|
|
// If its the initial site setup, they are an admin
|
|
admin: Some(!local_site.site_setup),
|
|
..LocalUserInsertForm::new(person.id, None)
|
|
};
|
|
|
|
local_user = create_local_user(&context, language_tags, &local_user_form).await?;
|
|
|
|
// Create the oauth account
|
|
let oauth_account_form =
|
|
OAuthAccountInsertForm::new(local_user.id, oauth_provider.id, oauth_user_id);
|
|
|
|
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
|
|
|
|
// prevent sign in until application is accepted
|
|
if local_site.site_setup
|
|
&& require_registration_application
|
|
&& !local_user.accepted_application
|
|
&& !local_user.admin
|
|
{
|
|
if let Some(answer) = data.answer.clone() {
|
|
// Create the registration application
|
|
RegistrationApplication::create(
|
|
&mut context.pool(),
|
|
&RegistrationApplicationInsertForm {
|
|
local_user_id: local_user.id,
|
|
answer,
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
login_response.registration_created = true;
|
|
}
|
|
}
|
|
|
|
// Check email is verified when required
|
|
login_response.verify_email_sent =
|
|
send_verification_email_if_required(&context, &local_site, &local_user, &person).await?;
|
|
}
|
|
}
|
|
|
|
if !login_response.registration_created && !login_response.verify_email_sent {
|
|
let jwt = Claims::generate(local_user.id, req, &context).await?;
|
|
login_response.jwt = Some(jwt);
|
|
}
|
|
|
|
return Ok(Json(login_response));
|
|
}
|
|
|
|
async fn create_person(
|
|
username: String,
|
|
local_site: &LocalSite,
|
|
instance_id: InstanceId,
|
|
context: &Data<LemmyContext>,
|
|
) -> Result<Person, LemmyError> {
|
|
let actor_keypair = generate_actor_keypair()?;
|
|
is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?;
|
|
let actor_id = generate_local_apub_endpoint(
|
|
EndpointType::Person,
|
|
&username,
|
|
&context.settings().get_protocol_and_hostname(),
|
|
)?;
|
|
|
|
// Register the new person
|
|
let person_form = PersonInsertForm {
|
|
actor_id: Some(actor_id.clone()),
|
|
inbox_url: Some(generate_inbox_url()?),
|
|
private_key: Some(actor_keypair.private_key),
|
|
..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id)
|
|
};
|
|
|
|
// insert the person
|
|
let inserted_person = Person::create(&mut context.pool(), &person_form)
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::UserAlreadyExists)?;
|
|
|
|
Ok(inserted_person)
|
|
}
|
|
|
|
fn get_language_tags(req: &HttpRequest) -> Vec<String> {
|
|
req
|
|
.headers()
|
|
.get("Accept-Language")
|
|
.map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default()))
|
|
.iter()
|
|
.flatten()
|
|
// Remove the optional region code
|
|
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
|
|
.collect::<Vec<String>>()
|
|
}
|
|
|
|
async fn create_local_user(
|
|
context: &Data<LemmyContext>,
|
|
language_tags: Vec<String>,
|
|
local_user_form: &LocalUserInsertForm,
|
|
) -> Result<LocalUser, LemmyError> {
|
|
let all_languages = Language::read_all(&mut context.pool()).await?;
|
|
// use hashset to avoid duplicates
|
|
let mut language_ids = HashSet::new();
|
|
for l in language_tags {
|
|
if let Some(found) = all_languages.iter().find(|all| all.code == l) {
|
|
language_ids.insert(found.id);
|
|
}
|
|
}
|
|
let language_ids = language_ids.into_iter().collect();
|
|
|
|
let inserted_local_user =
|
|
LocalUser::create(&mut context.pool(), local_user_form, language_ids).await?;
|
|
|
|
Ok(inserted_local_user)
|
|
}
|
|
|
|
async fn send_verification_email_if_required(
|
|
context: &Data<LemmyContext>,
|
|
local_site: &LocalSite,
|
|
local_user: &LocalUser,
|
|
person: &Person,
|
|
) -> LemmyResult<bool> {
|
|
let mut sent = false;
|
|
if !local_user.admin && local_site.require_email_verification && !local_user.email_verified {
|
|
let local_user_view = LocalUserView {
|
|
local_user: local_user.clone(),
|
|
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
|
|
person: person.clone(),
|
|
counts: PersonAggregates::default(),
|
|
};
|
|
|
|
send_verification_email(
|
|
&local_user_view,
|
|
&local_user
|
|
.email
|
|
.clone()
|
|
.ok_or(LemmyErrorType::EmailRequired)?,
|
|
&mut context.pool(),
|
|
context.settings(),
|
|
)
|
|
.await?;
|
|
|
|
sent = true;
|
|
}
|
|
Ok(sent)
|
|
}
|
|
|
|
fn validate_registration_answer(
|
|
require_registration_application: bool,
|
|
answer: &Option<String>,
|
|
) -> LemmyResult<()> {
|
|
if require_registration_application && answer.is_none() {
|
|
Err(LemmyErrorType::RegistrationApplicationAnswerRequired)?
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn oauth_request_access_token(
|
|
context: &Data<LemmyContext>,
|
|
oauth_provider: &OAuthProvider,
|
|
code: &str,
|
|
redirect_uri: &str,
|
|
) -> LemmyResult<TokenResponse> {
|
|
// Request an Access Token from the OAUTH provider
|
|
let response = context
|
|
.client()
|
|
.post(oauth_provider.token_endpoint.as_str())
|
|
.header("Accept", "application/json")
|
|
.form(&[
|
|
("grant_type", "authorization_code"),
|
|
("code", code),
|
|
("redirect_uri", redirect_uri),
|
|
("client_id", &oauth_provider.client_id),
|
|
("client_secret", &oauth_provider.client_secret),
|
|
])
|
|
.send()
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
|
|
.error_for_status()
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
|
|
|
|
// Extract the access token
|
|
let token_response = response
|
|
.json::<TokenResponse>()
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
|
|
|
|
Ok(token_response)
|
|
}
|
|
|
|
async fn oidc_get_user_info(
|
|
context: &Data<LemmyContext>,
|
|
oauth_provider: &OAuthProvider,
|
|
access_token: &str,
|
|
) -> LemmyResult<serde_json::Value> {
|
|
// Request the user info from the OAUTH provider
|
|
let response = context
|
|
.client()
|
|
.get(oauth_provider.userinfo_endpoint.as_str())
|
|
.header("Accept", "application/json")
|
|
.bearer_auth(access_token)
|
|
.send()
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
|
|
.error_for_status()
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
|
|
|
|
// Extract the OAUTH user_id claim from the returned user_info
|
|
let user_info = response
|
|
.json::<serde_json::Value>()
|
|
.await
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
|
|
|
|
Ok(user_info)
|
|
}
|
|
|
|
fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult<String> {
|
|
if let Some(value) = user_info.get(key) {
|
|
let result = serde_json::from_value::<String>(value.clone())
|
|
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
|
|
return Ok(result);
|
|
}
|
|
Err(LemmyErrorType::OauthLoginFailed)?
|
|
}
|