#241 [sec] add captcha to login

This commit is contained in:
Avris 2021-08-07 12:03:49 +02:00
parent 5f3e89a83c
commit b96ed0c347
17 changed files with 165 additions and 36 deletions

View File

@ -24,3 +24,6 @@ AWS_REGION=
AWS_KEY=
AWS_SECRET=
AWS_S3_BUCKET=
HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000

35
components/Captcha.vue Normal file
View File

@ -0,0 +1,35 @@
<template>
<div class="h-captcha"
:data-theme="isDark ? 'dark' : 'light'"
:data-sitekey="siteKey"
data-callback="hCaptchaDone"
></div>
</template>
<script>
import dark from "../plugins/dark";
export default {
mixins: [dark],
props: {
value: {},
},
data() {
return {
siteKey: process.env.HCAPTCHA_SITEKEY,
isDark: false,
};
},
mounted() {
if (!process.client) {
return false;
}
this.isDark = this.detectDark();
this.$loadScript('hcaptcha', 'https://js.hcaptcha.com/1/api.js');
window.hCaptchaDone = (token) => {
this.$emit('input', token);
}
},
}
</script>

View File

@ -3,34 +3,45 @@
<Alert type="danger" :message="error"/>
<div v-if="token === null">
<form @submit.prevent="login" :disabled="saving">
<p>
<div class="alert alert-info mb-3">
<p class="mb-0">
<Icon v="info-circle"/>
<T>user.login.why</T>
</p>
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="usernameOrEmail"
:placeholder="$t('user.login.placeholder')" autofocus required/>
<button class="btn btn-primary">
<Icon v="sign-in"/>
<T>user.login.action</T>
</button>
</div>
<div class="row">
<div class="col-12 col-md-8">
<form @submit.prevent="login" :disabled="saving">
<input type="text" class="form-control mb-3" v-model="usernameOrEmail"
:placeholder="$t('user.login.placeholder')" autofocus required/>
<p class="small text-muted mb-1">
<Icon v="info-circle"/>
<T>captcha.reason</T>
</p>
<Captcha class="h-captcha" v-model="captchaToken"/>
<button class="btn btn-primary mt-3" :disabled="!canInit">
<Icon v="sign-in"/>
<T>user.login.action</T>
</button>
</form>
</div>
<div class="btn-group w-100 mb-3">
<a :href="`/api/connect/${provider}`" v-for="(providerOptions, provider) in socialProviders" class="btn btn-outline-primary">
<Icon :v="providerOptions.icon || provider" set="b"/>
{{ providerOptions.name }}
</a>
<div class="col-12 col-md-4">
<div class="btn-group-vertical w-100 mb-3">
<a :href="`/api/connect/${provider}`" v-for="(providerOptions, provider) in socialProviders" class="btn btn-outline-primary">
<Icon :v="providerOptions.icon || provider" set="b"/>
{{ providerOptions.name }}
</a>
</div>
<p class="small text-muted">
<Icon v="gavel"/>
<T>terms.consent</T>
</p>
<p class="small text-muted">
<Icon v="lock"/>
<T>user.login.passwordless</T>
</p>
</div>
<p class="small text-muted">
<Icon v="gavel"/>
<T>terms.consent</T>
</p>
<p class="small text-muted">
<Icon v="lock"/>
<T>user.login.passwordless</T>
</p>
</form>
</div>
</div>
<div v-else-if="payload && !payload.code">
<div class="alert alert-success">
@ -74,6 +85,8 @@
socialProviders,
saving: false,
captchaToken: null,
};
},
computed: {
@ -90,7 +103,10 @@
audience: this.$base,
issuer: this.$base,
});
}
},
canInit() {
return this.usernameOrEmail && this.captchaToken;
},
},
methods: {
async login() {
@ -98,24 +114,31 @@
return;
}
this.saving = true;
await this.post(`/user/init`, {
usernameOrEmail: this.usernameOrEmail
});
this.saving = false;
try {
await this.post(`/user/init`, {
usernameOrEmail: this.usernameOrEmail,
captchaToken: this.captchaToken,
});
} finally {
this.saving = false;
}
},
async validate() {
if (this.saving) {
return;
}
this.saving = true;
await this.post(`/user/validate`, {
code: this.code
}, {
headers: {
authorization: 'Bearer ' + this.token,
},
});
this.saving = false;
try {
await this.post(`/user/validate`, {
code: this.code
}, {
headers: {
authorization: 'Bearer ' + this.token,
},
});
} finally {
this.saving = false;
}
},
async post(url, data, options = {}) {
this.error = '';

View File

@ -526,6 +526,11 @@ images:
error:
generic: 'Etwas ist schief gelaufen. Bitte versuche es erneut…'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Dunkelmodus'
light: 'Lichtmodus'

View File

@ -611,6 +611,10 @@ images:
error:
generic: 'Something went wrong, please try again…'
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Dark mode'
light: 'Light mode'

View File

@ -538,6 +538,11 @@ images:
error:
generic: 'Algo salió mal. Por favor, vuelve a intentarlo…'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Modo oscuro'
light: 'Modo claro'

View File

@ -531,6 +531,11 @@ images:
error:
generic: 'Quelque chose sest mal passé, réessayez sil vous plaît…'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Mode sombre'
light: 'Mode clair'

View File

@ -521,6 +521,11 @@ images:
error:
generic: 'Er is iets misgegaan, probeer het opnieuw…'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Donkere modus'
light: 'Lichte modus'

View File

@ -1149,6 +1149,10 @@ images:
error:
generic: 'Coś poszło nie tak, spróbuj ponownie…'
captcha:
reason: 'Prosimy o udowodnienie, że nie jesteś botem, by chronić się przed spamem i DDoS-ami'
invalid: 'Nieprawidłowa CAPTCHA, spróbuj ponownie'
mode:
dark: 'Tryb nocny'
light: 'Tryb dzienny'

View File

@ -536,6 +536,11 @@ images:
error:
generic: 'Alguma coisa deu errado, tente novamente'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Modo escuro'
light: 'Modo claro'

View File

@ -1095,6 +1095,11 @@ images:
error:
generic: 'Coś poszło nie tak, spróbuj ponownie…'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'Dark mode' # TODO
light: 'Light mode'

View File

@ -539,6 +539,11 @@ images:
error:
generic: 'Something went wrong, please try again…'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: 'טונקלמאָדוס'
light: 'ליכטמאָדוס'

View File

@ -506,6 +506,11 @@ images:
error:
generic: '出了點問題,請重試...'
# TODO
captcha:
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
invalid: 'Invalid CAPTCHA, please try again'
mode:
dark: '黑暗模式'
light: '明亮模式'

View File

@ -153,6 +153,7 @@ export default {
FLAGS: buildFlags(),
BUCKET: `https://${process.env.AWS_S3_BUCKET}.s3-${process.env.AWS_REGION}.amazonaws.com`,
STATS_FILE: process.env.STATS_FILE,
HCAPTCHA_SITEKEY: process.env.HCAPTCHA_SITEKEY,
},
serverMiddleware: ['~/server/no-ssr.js', '~/server/index.js'],
axios: {

View File

@ -33,6 +33,7 @@
"mailer": "^0.6.7",
"markdown-loader": "^6.0.0",
"multer": "^1.4.2",
"node-fetch": "^2.6.1",
"nuxt": "^2.15.2",
"pageres": "^6.2.3",
"rtlcss": "^3.1.2",

13
server/captcha.js Normal file
View File

@ -0,0 +1,13 @@
import fetch from 'node-fetch';
export const validateCaptcha = async (token) => {
const res = await fetch('https://hcaptcha.com/siteverify', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: `response=${encodeURIComponent(token)}&secret=${encodeURIComponent(process.env.HCAPTCHA_SECRET)}`
});
const body = await res.json();
return body['success'];
}

View File

@ -8,6 +8,7 @@ import { loadSuml } from '../loader';
import avatar from '../avatar';
import { config as socialLoginConfig, handlers as socialLoginHandlers } from '../social';
import cookieSettings from "../../src/cookieSettings";
import {validateCaptcha} from "../captcha";
const config = loadSuml('config');
const translations = loadSuml('translations');
@ -199,6 +200,10 @@ router.post('/user/init', handleErrorAsync(async (req, res) => {
return;
}
if (!await validateCaptcha(req.body.captchaToken)) {
return res.json({error: 'captcha.invalid'});
}
let user = undefined;
let usernameOrEmail = req.body.usernameOrEmail;