feat: add turnstile as captcha provider to signup

This commit is contained in:
MiniDigger | Martin 2024-11-10 19:54:57 +01:00
parent b534447f30
commit bbe3335935
14 changed files with 1048 additions and 6 deletions

View File

@ -13,6 +13,7 @@ import io.papermc.hangar.components.auth.model.dto.SignupForm;
import io.papermc.hangar.components.auth.service.AuthService;
import io.papermc.hangar.components.auth.service.CredentialsService;
import io.papermc.hangar.components.auth.service.TokenService;
import io.papermc.hangar.components.auth.service.TurnstileService;
import io.papermc.hangar.components.auth.service.VerificationService;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.db.UserTable;
@ -47,18 +48,22 @@ public class AuthController extends HangarComponent {
private final VerificationService verificationService;
private final CredentialsService credentialsService;
private final UserService userService;
private final TurnstileService turnstileService;
public AuthController(final AuthService authService, final TokenService tokenService, final VerificationService verificationService, final CredentialsService credentialsService, final UserService userService) {
public AuthController(final AuthService authService, final TokenService tokenService, final VerificationService verificationService, final CredentialsService credentialsService, final UserService userService, final TurnstileService turnstileService) {
this.authService = authService;
this.tokenService = tokenService;
this.verificationService = verificationService;
this.credentialsService = credentialsService;
this.userService = userService;
this.turnstileService = turnstileService;
}
@Anyone
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody final SignupForm signupForm) {
this.turnstileService.validate(signupForm.captcha());
final UserTable userTable = this.authService.registerUser(signupForm);
if (userTable == null) {
return ResponseEntity.badRequest().build();

View File

@ -1,4 +1,4 @@
package io.papermc.hangar.components.auth.model.dto;
public record SignupForm(String username, String email, String password, boolean tos) {
public record SignupForm(String username, String email, String password, boolean tos, String captcha) {
}

View File

@ -0,0 +1,43 @@
package io.papermc.hangar.components.auth.service;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.util.RequestUtil;
import java.util.Arrays;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
@Service
public class TurnstileService extends HangarComponent {
private final RestTemplate restTemplate;
public TurnstileService(final RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public void validate(String token) {
if (this.config.security.turnstileSecret() != null && !this.config.security.turnstileSecret().isBlank()) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("secret", this.config.security.turnstileSecret());
formData.add("response", token);
formData.add("remoteip", RequestUtil.getRemoteAddress(this.request));
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.set("User-Agent", "Hangar/1.0");
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(formData, headers);
String url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
var response = this.restTemplate.postForEntity(url, entity, TurnstileResponse.class);
if (response.getBody() != null && !response.getBody().success()) {
throw new HangarApiException("error.captcha", Arrays.toString(response.getBody().errorCodes()));
}
}
}
record TurnstileResponse(boolean success, String[] errorCodes){}
}

View File

@ -21,7 +21,8 @@ public record HangarSecurityConfig(
String rpName,
String rpId,
List<OAuthProvider> oAuthProviders,
@DefaultValue("false") boolean oAuthEnabled
@DefaultValue("false") boolean oAuthEnabled,
String turnstileSecret
) {
public boolean checkSafe(final String url) {

View File

@ -166,6 +166,7 @@ hangar:
refresh-token-expiry: 30 # days
rp-name: "Hangar"
rp-id: "localhost"
turnstileSecret: ""
safe-download-hosts:
- "dev.bukkit.org"
- "github.com"

View File

@ -95,9 +95,9 @@ public class TestData {
HangarApplication.TEST_MODE = true;
logger.info("Preparing test data...");
logger.info("Creating some test users...");
USER_NORMAL = this.authService.registerUser(new SignupForm("TestUser", "testuser@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true));
USER_MEMBER = this.authService.registerUser(new SignupForm("TestMember", "testmember@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true));
USER_ADMIN = this.authService.registerUser(new SignupForm("TestAdmin", "testadmin@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true));
USER_NORMAL = this.authService.registerUser(new SignupForm("TestUser", "testuser@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
USER_MEMBER = this.authService.registerUser(new SignupForm("TestMember", "testmember@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
USER_ADMIN = this.authService.registerUser(new SignupForm("TestAdmin", "testadmin@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
USER_NORMAL.setEmailVerified(true);
USER_MEMBER.setEmailVerified(true);

View File

@ -50,6 +50,7 @@ stringData:
security:
token-secret: "{{ .Values.backend.config.tokenSecret }}"
rp-id: "{{ .Values.backend.config.rpId }}"
turnstile-secret: "{{ .Values.backend.config.turnstileSecret }}"
o-auth-enabled: {{ .Values.backend.config.oauthEnabled }}
o-auth-providers:
- name: "github"

View File

@ -13,3 +13,4 @@ stringData:
#DEBUG: "hangar:*"
#NITRO_CLUSTER_WORKERS: "4"
SENTRY_ENV: "{{ .Values.backend.config.sentry.environment }}"
NUXT_PUBLIC_TURNSTILE_SITE_KEY: "{{ .Values.frontend.config.turnstileSiteKey }}"

View File

@ -111,6 +111,7 @@ frontend:
config:
configEnv: "hangar.test"
backendHost: "http://hangar-backend:8080"
turnstileSiteKey: "todo"
backend:
replicaCount: 1
@ -186,6 +187,7 @@ backend:
options: "?currentSchema=hangar"
tokenSecret: "secret"
rpId: "localhost"
turnstileSecret: ""
oauthEnabled: true
githubClientId: "todo"
githubClientSecret: "todo"

View File

@ -56,6 +56,7 @@ export default defineNuxtConfig({
"@vueuse/nuxt",
"@nuxtjs/i18n",
"@sentry/nuxt/module",
"@nuxtjs/turnstile",
[
"unplugin-icons/nuxt",
{

View File

@ -65,6 +65,7 @@
"@iconify-json/mdi": "1.2.1",
"@nuxtjs/eslint-config-typescript": "12.1.0",
"@nuxtjs/i18n": "8.5.5",
"@nuxtjs/turnstile": "0.9.11",
"@sentry/bun": "8.37.1",
"@sentry/nuxt": "8.37.1",
"@sentry/profiling-node": "8.37.1",

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,11 @@ interface SignupForm {
email?: string;
password?: string;
tos?: boolean;
captcha?: string;
}
const config = useRuntimeConfig();
const done = ref(false);
const notification = useNotificationStore();
@ -90,6 +93,7 @@ useSeo(computed(() => ({ title: "Sign up", route })));
<InputText v-model="form.username" label="Username" name="username" autocomplete="username" :rules="[required()]" />
<InputText v-model="form.email" type="email" label="E-Mail" name="email" autocomplete="email" :rules="[required(), email()]" />
<InputPassword v-model="form.password" label="Password" name="new-password" :rules="[required()]" />
<LazyNuxtTurnstile v-if="config.public.turnstile?.siteKey != '1x00000000000000000000AA'" v-model="form.captcha" />
<div v-if="errorMessage" class="c-red">{{ errorMessage }}</div>
<Button type="submit" :disabled="loading" @click.prevent="submit">Sign up</Button>
<div class="w-max">

View File

@ -100,6 +100,11 @@ site. We strongly advise You to review the Privacy Policy of every site You visi
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
Cloudflare
----------
This Website is protected by various Cloudflare technologies. You can view their Privacy Policy here: https://www.cloudflare.com/privacypolicy/
Changes to this Privacy Policy
------------------------------