Skip to content

Commit

Permalink
Switch to HCaptcha for Auth-related captchas (modrinth#2945)
Browse files Browse the repository at this point in the history
* Switch to HCaptcha for Auth-related captchas

* run fmt
  • Loading branch information
Geometrically authored Nov 17, 2024
1 parent 5ab1263 commit b188b3f
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 163 deletions.
4 changes: 1 addition & 3 deletions apps/frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,6 @@ export default defineNuxtConfig({
globalThis.CF_PAGES_COMMIT_SHA ||
"unknown",

turnstile: { siteKey: "0x4AAAAAAAW3guHM6Eunbgwu" },

stripePublishableKey:
process.env.STRIPE_PUBLISHABLE_KEY ||
globalThis.STRIPE_PUBLISHABLE_KEY ||
Expand All @@ -362,7 +360,7 @@ export default defineNuxtConfig({
},
},
},
modules: ["@vintl/nuxt", "@nuxtjs/turnstile", "@pinia/nuxt"],
modules: ["@vintl/nuxt", "@pinia/nuxt"],
vintl: {
defaultLocale: "en-US",
locales: [
Expand Down
1 change: 0 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"devDependencies": {
"@formatjs/cli": "^6.2.12",
"@nuxt/devtools": "^1.3.3",
"@nuxtjs/turnstile": "^0.8.0",
"@types/dompurify": "^3.0.5",
"@types/node": "^20.1.0",
"@vintl/compact-number": "^2.0.5",
Expand Down
53 changes: 53 additions & 0 deletions apps/frontend/src/components/ui/HCaptcha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup>
const token = defineModel();
useHead({
script: [
{
src: "https://js.hcaptcha.com/1/api.js",
async: true,
defer: true,
},
],
});
function updateToken(newToken) {
token.value = newToken;
}
onMounted(() => {
window.updateCatpchaToken = updateToken;
});
defineExpose({
reset: () => {
token.value = null;
window.hcaptcha.reset();
},
});
</script>

<template>
<div
id="h-captcha"
class="h-captcha"
data-sitekey="4a7a2c80-68f2-4190-9d52-131c76e0c14e"
:data-theme="$theme.active === 'light' ? 'light' : 'dark'"
data-callback="updateCatpchaToken"
></div>
</template>

<style lang="scss">
.h-captcha {
display: flex;
justify-content: center;
overflow: hidden;
border-radius: var(--radius-md);
border: 2px solid var(--color-button-bg);
height: 78px;
iframe {
margin: -1px;
}
}
</style>
14 changes: 0 additions & 14 deletions apps/frontend/src/pages/auth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,6 @@
}
}
.turnstile {
display: flex;
justify-content: center;
overflow: hidden;
border-radius: var(--radius-md);
border: 2px solid var(--color-button-bg);
height: 66px;
iframe {
margin: -1px;
min-width: calc(100% + 2px);
}
}
.auth-form {
display: flex;
flex-direction: column;
Expand Down
14 changes: 5 additions & 9 deletions apps/frontend/src/pages/auth/reset-password.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@
/>
</div>

<NuxtTurnstile
ref="turnstile"
v-model="token"
class="turnstile"
:options="{ theme: $theme.active === 'light' ? 'light' : 'dark' }"
/>
<HCaptcha ref="captcha" v-model="token" />

<button class="btn btn-primary centered-btn" :disabled="!token" @click="recovery">
<SendIcon /> {{ formatMessage(methodChoiceMessages.action) }}
Expand Down Expand Up @@ -73,6 +68,7 @@
</template>
<script setup>
import { SendIcon, MailIcon, KeyIcon } from "@modrinth/assets";
import HCaptcha from "@/components/ui/HCaptcha.vue";
const { formatMessage } = useVIntl();
Expand Down Expand Up @@ -165,7 +161,7 @@ if (route.query.flow) {
step.value = "passed_challenge";
}
const turnstile = ref();
const captcha = ref();
const email = ref("");
const token = ref("");
Expand Down Expand Up @@ -194,7 +190,7 @@ async function recovery() {
text: err.data ? err.data.description : err,
type: "error",
});
turnstile.value?.reset();
captcha.value?.reset();
}
stopLoading();
}
Expand Down Expand Up @@ -227,7 +223,7 @@ async function changePassword() {
text: err.data ? err.data.description : err,
type: "error",
});
turnstile.value?.reset();
captcha.value?.reset();
}
stopLoading();
}
Expand Down
14 changes: 5 additions & 9 deletions apps/frontend/src/pages/auth/sign-in.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,7 @@
/>
</div>

<NuxtTurnstile
ref="turnstile"
v-model="token"
class="turnstile"
:options="{ theme: $theme.active === 'light' ? 'light' : 'dark' }"
/>
<HCaptcha ref="captcha" v-model="token" />

<button
class="btn btn-primary continue-btn centered-btn"
Expand Down Expand Up @@ -127,6 +122,7 @@ import {
KeyIcon,
MailIcon,
} from "@modrinth/assets";
import HCaptcha from "@/components/ui/HCaptcha.vue";
const { formatMessage } = useVIntl();
Expand Down Expand Up @@ -189,7 +185,7 @@ if (auth.value.user) {
await finishSignIn();
}
const turnstile = ref();
const captcha = ref();
const email = ref("");
const password = ref("");
Expand Down Expand Up @@ -225,7 +221,7 @@ async function beginPasswordSignIn() {
text: err.data ? err.data.description : err,
type: "error",
});
turnstile.value?.reset();
captcha.value?.reset();
}
stopLoading();
}
Expand All @@ -250,7 +246,7 @@ async function begin2FASignIn() {
text: err.data ? err.data.description : err,
type: "error",
});
turnstile.value?.reset();
captcha.value?.reset();
}
stopLoading();
}
Expand Down
14 changes: 5 additions & 9 deletions apps/frontend/src/pages/auth/sign-up.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,7 @@
</IntlFormatted>
</p>

<NuxtTurnstile
ref="turnstile"
v-model="token"
class="turnstile"
:options="{ theme: $theme.active === 'light' ? 'light' : 'dark' }"
/>
<HCaptcha ref="captcha" v-model="token" />

<button
class="btn btn-primary continue-btn centered-btn"
Expand Down Expand Up @@ -145,6 +140,7 @@ import {
SSOGitLabIcon,
} from "@modrinth/assets";
import { Checkbox } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue";
const { formatMessage } = useVIntl();
Expand Down Expand Up @@ -209,7 +205,7 @@ if (auth.value.user) {
await navigateTo("/dashboard");
}
const turnstile = ref();
const captcha = ref();
const email = ref("");
const username = ref("");
Expand All @@ -235,7 +231,7 @@ async function createAccount() {
}),
type: "error",
});
turnstile.value?.reset();
captcha.value?.reset();
}
const res = await useBaseFetch("auth/create", {
Expand Down Expand Up @@ -264,7 +260,7 @@ async function createAccount() {
text: err.data ? err.data.description : err,
type: "error",
});
turnstile.value?.reset();
captcha.value?.reset();
}
stopLoading();
}
Expand Down
2 changes: 1 addition & 1 deletion apps/labrinth/.env
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ TREMENDOUS_API_KEY=none
TREMENDOUS_PRIVATE_KEY=none
TREMENDOUS_CAMPAIGN_ID=none

TURNSTILE_SECRET=none
HCAPTCHA_SECRET=none

SMTP_USERNAME=none
SMTP_PASSWORD=none
Expand Down
2 changes: 1 addition & 1 deletion apps/labrinth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ pub fn check_env_vars() -> bool {
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");

failed |= check_var::<String>("TURNSTILE_SECRET");
failed |= check_var::<String>("HCAPTCHA_SECRET");

failed |= check_var::<String>("SMTP_USERNAME");
failed |= check_var::<String>("SMTP_PASSWORD");
Expand Down
40 changes: 4 additions & 36 deletions apps/labrinth/src/routes/internal/flows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::queue::session::AuthQueue;
use crate::queue::socket::ActiveSockets;
use crate::routes::internal::session::issue_session;
use crate::routes::ApiError;
use crate::util::captcha::check_turnstile_captcha;
use crate::util::captcha::check_hcaptcha;
use crate::util::env::parse_strings_from_var;
use crate::util::ext::get_image_ext;
use crate::util::img::upload_image_optimized;
Expand Down Expand Up @@ -1468,8 +1468,6 @@ pub struct NewAccount {
pub sign_up_newsletter: Option<bool>,
}

const NEW_ACCOUNT_LIMITER_NAMESPACE: &str = "new_account_ips";

#[post("create")]
pub async fn create_account_with_password(
req: HttpRequest,
Expand All @@ -1481,7 +1479,7 @@ pub async fn create_account_with_password(
ApiError::InvalidInput(validation_errors_to_string(err, None))
})?;

if !check_turnstile_captcha(&req, &new_account.challenge).await? {
if !check_hcaptcha(&req, &new_account.challenge).await? {
return Err(ApiError::Turnstile);
}

Expand Down Expand Up @@ -1566,36 +1564,6 @@ pub async fn create_account_with_password(
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
let res = crate::models::sessions::Session::from(session, true, None);

// We limit each ip to creating 5 accounts in a six hour period
let ip = crate::util::ip::convert_to_ip_v6(&res.ip).map_err(|_| {
ApiError::InvalidInput("unable to parse user ip!".to_string())
})?;
let stripped_ip = crate::util::ip::strip_ip(ip).to_string();

let mut conn = redis.connect().await?;
let uses = if let Some(res) = conn
.get(NEW_ACCOUNT_LIMITER_NAMESPACE, &stripped_ip)
.await?
{
res.parse::<u64>().unwrap_or(0)
} else {
0
};

if uses >= 5 {
return Err(ApiError::InvalidInput(
"IP has been rate-limited.".to_string(),
));
}

conn.set(
NEW_ACCOUNT_LIMITER_NAMESPACE,
&stripped_ip,
&(uses + 1).to_string(),
Some(60 * 60 * 6),
)
.await?;

let flow = Flow::ConfirmEmail {
user_id,
confirm_email: new_account.email.clone(),
Expand Down Expand Up @@ -1632,7 +1600,7 @@ pub async fn login_password(
redis: Data<RedisPool>,
login: web::Json<Login>,
) -> Result<HttpResponse, ApiError> {
if !check_turnstile_captcha(&req, &login.challenge).await? {
if !check_hcaptcha(&req, &login.challenge).await? {
return Err(ApiError::Turnstile);
}

Expand Down Expand Up @@ -2082,7 +2050,7 @@ pub async fn reset_password_begin(
redis: Data<RedisPool>,
reset_password: web::Json<ResetPassword>,
) -> Result<HttpResponse, ApiError> {
if !check_turnstile_captcha(&req, &reset_password.challenge).await? {
if !check_hcaptcha(&req, &reset_password.challenge).await? {
return Err(ApiError::Turnstile);
}

Expand Down
21 changes: 13 additions & 8 deletions apps/labrinth/src/util/captcha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use crate::routes::ApiError;
use crate::util::env::parse_var;
use actix_web::HttpRequest;
use serde::Deserialize;
use serde_json::json;
use std::collections::HashMap;

pub async fn check_turnstile_captcha(
pub async fn check_hcaptcha(
req: &HttpRequest,
challenge: &str,
) -> Result<bool, ApiError> {
Expand All @@ -19,20 +19,25 @@ pub async fn check_turnstile_captcha(
conn_info.peer_addr()
};

let ip_addr = ip_addr.ok_or(ApiError::Turnstile)?;

let client = reqwest::Client::new();

#[derive(Deserialize)]
struct Response {
success: bool,
}

let mut form = HashMap::new();

let secret = dotenvy::var("HCAPTCHA_SECRET")?;
form.insert("response", challenge);
form.insert("secret", &*secret);
form.insert("remoteip", ip_addr);

let val: Response = client
.post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
.json(&json!({
"secret": dotenvy::var("TURNSTILE_SECRET")?,
"response": challenge,
"remoteip": ip_addr,
}))
.post("https://api.hcaptcha.com/siteverify")
.form(&form)
.send()
.await
.map_err(|_| ApiError::Turnstile)?
Expand Down
Loading

0 comments on commit b188b3f

Please sign in to comment.