Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CAPTCHA] Replace ReCAPTCHA with Turnstile #3249

Merged
merged 27 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
83e1825
Replace CAPTCHA with Turnstile (with test keys)
imnasnainaec Jul 16, 2024
969ccc9
Complete CAPTCHA replacement
imnasnainaec Jul 16, 2024
80ba196
Setup secret key
imnasnainaec Jul 16, 2024
3dc89a2
Fix App tests
imnasnainaec Jul 16, 2024
eec3ba0
Merge branch 'master' into turnstile
imnasnainaec Jul 16, 2024
b9f029a
Fix failing tests
imnasnainaec Jul 16, 2024
2674e73
Fix App tests
imnasnainaec Jul 16, 2024
d805fb4
Tidy Turnstile component
imnasnainaec Jul 17, 2024
1db2d1a
Add for Turnstile: environment variables, Context, Service
imnasnainaec Jul 17, 2024
b2eab3f
Replace frontend CAPTCHA configs with Turnstile config
imnasnainaec Jul 17, 2024
0bb8630
Clean up residue
imnasnainaec Jul 18, 2024
18cf48c
Simplify bool conditional
imnasnainaec Jul 18, 2024
cef2a5e
Add turnstileSiteKey config
imnasnainaec Jul 18, 2024
4f0035b
Restore frontend Captcha configs; Add backend Enabled configs
imnasnainaec Jul 19, 2024
caeccde
Unify frontend and backend captcha/email configs
imnasnainaec Jul 19, 2024
cedee67
Clean up loose ends
imnasnainaec Jul 19, 2024
7c30de3
Clean up
imnasnainaec Jul 19, 2024
bf7ac17
Move up
imnasnainaec Jul 19, 2024
fac46f2
Update from review
imnasnainaec Jul 22, 2024
1677530
Reenforce lowercase bool config strings
imnasnainaec Jul 22, 2024
303c745
Clean up bool configs
imnasnainaec Jul 23, 2024
c510663
Deploy to QA in this branch for testing
imnasnainaec Jul 23, 2024
34e4dcb
Restore deploy_qa safeguards
imnasnainaec Jul 24, 2024
68b4a31
Make subcharts stand-alone
imnasnainaec Jul 24, 2024
003edad
None-ify the pullSecretName default
imnasnainaec Jul 24, 2024
934944b
Merge branch 'master' into turnstile
imnasnainaec Jul 24, 2024
01b67b4
Merge branch 'master' into turnstile
imnasnainaec Jul 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/deploy_qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
#- turnstile

permissions:
contents: read
Expand Down Expand Up @@ -94,7 +95,7 @@ jobs:
deploy_update:
needs: build
# Only push to the QA server when built on the master branch
if: ${{ github.ref_name == 'master' }}
#if: ${{ github.ref_name == 'master' }}
runs-on: [self-hosted, thecombine]
steps:
- name: Harden Runner
Expand Down
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"piptools",
"Prenoun",
"Preverb",
"recaptcha",
"reportgenerator",
"sched",
"signup",
Expand Down
2 changes: 1 addition & 1 deletion Backend.Tests/Controllers/UserControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void Setup()
_userRepo = new UserRepositoryMock();
_permissionService = new PermissionServiceMock(_userRepo);
_userController = new UserController(_userRepo, _permissionService,
new EmailServiceMock(), new PasswordResetServiceMock());
new EmailServiceMock(), new PasswordResetServiceMock(), new TurnstileServiceMock());
}

private static User RandomUser()
Expand Down
13 changes: 13 additions & 0 deletions Backend.Tests/Mocks/TurnstileServiceMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Threading.Tasks;
using BackendFramework.Interfaces;

namespace Backend.Tests.Mocks
{
sealed internal class TurnstileServiceMock : ITurnstileService
{
public Task<bool> VerifyToken(string token)
{
return Task.FromResult(true);
}
}
}
19 changes: 19 additions & 0 deletions Backend/Contexts/TurnstileContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using BackendFramework.Interfaces;
using Microsoft.Extensions.Options;

namespace BackendFramework.Contexts
{
[ExcludeFromCodeCoverage]
public class TurnstileContext : ITurnstileContext
{
public string? TurnstileSecretKey { get; }
public string? TurnstileVerifyUrl { get; }

public TurnstileContext(IOptions<Startup.Settings> options)
{
TurnstileSecretKey = options.Value.TurnstileSecretKey;
TurnstileVerifyUrl = options.Value.TurnstileVerifyUrl;
}
}
}
13 changes: 12 additions & 1 deletion Backend/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,25 @@ public class UserController : Controller
private readonly IEmailService _emailService;
private readonly IPasswordResetService _passwordResetService;
private readonly IPermissionService _permissionService;
private readonly ITurnstileService _turnstileService;

public UserController(IUserRepository userRepo, IPermissionService permissionService,
IEmailService emailService, IPasswordResetService passwordResetService)
IEmailService emailService, IPasswordResetService passwordResetService, ITurnstileService turnstileService)
{
_userRepo = userRepo;
_emailService = emailService;
_passwordResetService = passwordResetService;
_permissionService = permissionService;
_turnstileService = turnstileService;
}

/// <summary> Validates a Cloudflare Turnstile token </summary>
[AllowAnonymous]
[HttpGet("turnstile/{token}", Name = "ValidateTurnstile")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> ValidateTurnstile(string token)
{
return await _turnstileService.VerifyToken(token) ? Ok() : BadRequest();
}

/// <summary> Sends a password reset request </summary>
Expand Down
8 changes: 8 additions & 0 deletions Backend/Interfaces/ITurnstileContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BackendFramework.Interfaces
{
public interface ITurnstileContext
{
string? TurnstileSecretKey { get; }
string? TurnstileVerifyUrl { get; }
}
}
9 changes: 9 additions & 0 deletions Backend/Interfaces/ITurnstileService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading.Tasks;

namespace BackendFramework.Interfaces
{
public interface ITurnstileService
{
Task<bool> VerifyToken(string token);
}
}
10 changes: 8 additions & 2 deletions Backend/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"launchBrowser": true,
"launchUrl": "v1",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"COMBINE_JWT_SECRET_KEY": "0123456789abcdefghijklmnopqrstuvwxyz",
"TURNSTILE_SECRET_KEY": "1x0000000000000000000000000000000AA",
"TURNSTILE_VERIFY_URL": "https://challenges.cloudflare.com/turnstile/v0/siteverify"
}
},
"BackendFramework": {
Expand All @@ -23,7 +26,10 @@
"launchUrl": "v1/",
"environmentVariables": {
"Key": "Value",
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"COMBINE_JWT_SECRET_KEY": "0123456789abcdefghijklmnopqrstuvwxyz",
"TURNSTILE_SECRET_KEY": "1x0000000000000000000000000000000AA",
"TURNSTILE_VERIFY_URL": "https://challenges.cloudflare.com/turnstile/v0/siteverify"
},
"applicationUrl": "http://localhost:5000",
"hotReloadProfile": "aspnetcore"
Expand Down
36 changes: 36 additions & 0 deletions Backend/Services/TurnstileService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;
using BackendFramework.Interfaces;

namespace BackendFramework.Services
{
[ExcludeFromCodeCoverage]
public class TurnstileService : ITurnstileService
{
private readonly ITurnstileContext _turnstileContext;

public TurnstileService(ITurnstileContext turnstileContext)
{
_turnstileContext = turnstileContext;
}

public async Task<bool> VerifyToken(string token)
{
var secret = _turnstileContext.TurnstileSecretKey;
var verifyUrl = _turnstileContext.TurnstileVerifyUrl;
if (string.IsNullOrEmpty(secret) || string.IsNullOrEmpty(verifyUrl))
{
return false;
}
var httpContent = new FormUrlEncodedContent(new Dictionary<string, string>{
{"response", token},
{"secret", secret},
});
using var result = await new HttpClient().PostAsync(verifyUrl, httpContent);
var contentString = await result.Content.ReadAsStringAsync();
return contentString.Contains("\"success\":true");
}
}
}
17 changes: 17 additions & 0 deletions Backend/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public class Settings
public string? SmtpAddress { get; set; }
public string? SmtpFrom { get; set; }
public int PassResetExpireTime { get; set; }
public string? TurnstileSecretKey { get; set; }
public string? TurnstileVerifyUrl { get; set; }

public Settings()
{
Expand Down Expand Up @@ -178,10 +180,21 @@ public void ConfigureServices(IServiceCollection services)
"COMBINE_SMTP_FROM",
null,
emailServiceFailureMessage);

options.PassResetExpireTime = int.Parse(CheckedEnvironmentVariable(
"COMBINE_PASSWORD_RESET_EXPIRE_TIME",
Settings.DefaultPasswordResetExpireTime.ToString(),
$"Using default value: {Settings.DefaultPasswordResetExpireTime}")!);

const string turnstileFailureMessage = "Turnstile verification will not be available.";
options.TurnstileSecretKey = CheckedEnvironmentVariable(
"TURNSTILE_SECRET_KEY",
null,
turnstileFailureMessage);
options.TurnstileVerifyUrl = CheckedEnvironmentVariable(
"TURNSTILE_VERIFY_URL",
null,
turnstileFailureMessage);
});

// Register concrete types for dependency injection
Expand Down Expand Up @@ -228,6 +241,10 @@ public void ConfigureServices(IServiceCollection services)
// Statistics types
services.AddSingleton<IStatisticsService, StatisticsService>();

// Turnstile types
services.AddTransient<ITurnstileContext, TurnstileContext>();
services.AddTransient<ITurnstileService, TurnstileService>();

// User types
services.AddTransient<IUserContext, UserContext>();
services.AddTransient<IUserRepository, UserRepository>();
Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,9 @@ A rapid word collection tool. See the [User Guide](https://sillsdev.github.io/Th

### Prepare the Environment

1. Set the environment variable `COMBINE_JWT_SECRET_KEY` to a string **containing at least 32 characters**, such as
_This is a secret key that is longer_. Set it in your `.profile` (Linux or Mac 10.14-), your `.zprofile` (Mac
10.15+), or the _System_ app (Windows).
2. If you want the email services to work you will need to set the following environment variables. These values must be
kept secret, so ask your email administrator to supply them.
1. _(Optional)_ If you want the email services to work you will need to set the following environment variables. These
values must be kept secret, so ask your email administrator to supply them. Set them in your `.profile` (Linux or Mac
10.14-), your `.zprofile` (Mac 10.15+), or the _System_ app (Windows).

- `COMBINE_SMTP_SERVER`
- `COMBINE_SMTP_PORT`
Expand All @@ -155,16 +153,16 @@ A rapid word collection tool. See the [User Guide](https://sillsdev.github.io/Th
- `COMBINE_SMTP_ADDRESS`
- `COMBINE_SMTP_FROM`

3. _(Optional)_ To opt in to segment.com analytics to test the analytics during development:
2. _(Optional)_ To opt in to segment.com analytics to test the analytics during development:

```bash
# For Windows, use `copy`.
cp .env.local.template .env.local
```

4. Run `npm start` from the project directory to install dependencies and start the project.
3. Run `npm start` from the project directory to install dependencies and start the project.

5. Consult our [C#](docs/style_guide/c_sharp_style_guide.md) and [TypeScript](docs/style_guide/ts_style_guide.md) style
4. Consult our [C#](docs/style_guide/c_sharp_style_guide.md) and [TypeScript](docs/style_guide/ts_style_guide.md) style
guides for best coding practices in this project.

[chocolatey]: https://chocolatey.org/
Expand Down Expand Up @@ -687,7 +685,12 @@ Notes:

### Setup Environment Variables

_Note: This is optional for Development Environments._
Before installing _The Combine_ in Kubernetes, you need to set the following environment variables:
`COMBINE_JWT_SECRET_KEY`, `TURNSTILE_SECRET_KEY`. For development environments, you can use the values defined in
`Backend/Properties/launchSettings.json`. Set them in your `.profile` (Linux or Mac 10.14-), your `.zprofile` (Mac
10.15+), or the _System_ app (Windows).

_Note: The following is optional for Development Environments._

In addition to the environment variables defined in [Prepare the Environment](#prepare-the-environment), you may setup
the following environment variables:
Expand All @@ -697,15 +700,13 @@ the following environment variables:
- `AWS_ACCESS_KEY_ID`
- `AWS_SECRET_ACCESS_KEY`

These variables will allow the Combine to:
These variables will allow _The Combine_ to:

- pull released and QA software images from AWS Elastic Container Registry (ECR);
- create backups and push them to AWS S3 storage; and
- restore _The Combine's_ database and backend files from a backup stored in AWS S3 storage.

The Combine application will function in a local cluster without these variables set.

These can be set in your `.profile` (Linux or Mac 10.14-), your `.zprofile` (Mac 10.15+), or the _System_ app (Windows).
The Combine application will function in a local cluster without these `AWS_` variables set.

### Install/Update _The Combine_

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ data:
COMBINE_SMTP_FROM: {{ .Values.combineSmtpFrom | quote }}
COMBINE_SMTP_PORT: {{ .Values.combineSmtpPort | quote }}
COMBINE_SMTP_SERVER: {{ .Values.combineSmtpServer | quote }}
TURNSTILE_VERIFY_URL: {{ .Values.turnstileVerifyUrl | quote }}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ data:
COMBINE_JWT_SECRET_KEY: {{ .Values.global.combineJwtSecretKey | b64enc }}
COMBINE_SMTP_USERNAME: {{ .Values.global.combineSmtpUsername | b64enc }}
COMBINE_SMTP_PASSWORD: {{ .Values.global.combineSmtpPassword | b64enc }}
TURNSTILE_SECRET_KEY: {{ .Values.global.turnstileSecretKey | b64enc }}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ spec:
secretKeyRef:
key: COMBINE_SMTP_USERNAME
name: env-backend-secrets
- name: TURNSTILE_SECRET_KEY
valueFrom:
secretKeyRef:
key: TURNSTILE_SECRET_KEY
name: env-backend-secrets
- name: TURNSTILE_VERIFY_URL
valueFrom:
configMapKeyRef:
key: TURNSTILE_VERIFY_URL
name: env-backend
ports:
- containerPort: 5000
resources:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/thecombine/charts/backend/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ global:
imageTag: "latest"
# Define the image registry to use (may be blank for local images)
imageRegistry: ""
# Cloudflare Turnstile verification
turnstileSecretKey: "Override"

persistentVolumeSize: 32Gi
combinePasswordResetTime: 15
Expand All @@ -34,3 +36,4 @@ combineSmtpFrom: "The Combine"
combineSmtpPort: 587
combineSmtpServer: "email-smtp.us-east-1.amazonaws.com"
imageName: combine_backend
turnstileVerifyUrl: "https://challenges.cloudflare.com/turnstile/v0/siteverify"
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ spec:
configMapKeyRef:
key: CERT_ADDL_DOMAINS
name: env-frontend
- name: CONFIG_CAPTCHA_REQD
- name: CONFIG_TURNSTILE_REQUIRED
valueFrom:
configMapKeyRef:
key: CONFIG_CAPTCHA_REQD
key: CONFIG_TURNSTILE_REQUIRED
name: env-frontend
- name: CONFIG_CAPTCHA_SITE_KEY
- name: CONFIG_TURNSTILE_SITE_KEY
valueFrom:
configMapKeyRef:
key: CONFIG_CAPTCHA_SITE_KEY
key: CONFIG_TURNSTILE_SITE_KEY
name: env-frontend
- name: CONFIG_USE_CONNECTION_URL
valueFrom:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ data:
SERVER_NAME: {{ .Values.global.serverName }}
CERT_ADDL_DOMAINS: {{ .Values.combineAddlDomainList | quote }}
CONFIG_USE_CONNECTION_URL: "true"
CONFIG_CAPTCHA_REQD: {{ .Values.configCaptchaRequired | quote }}
CONFIG_CAPTCHA_SITE_KEY: {{ .Values.configCaptchaSiteKey | quote }}
CONFIG_TURNSTILE_REQUIRED: {{ .Values.configTurnstileRequired | quote }}
CONFIG_TURNSTILE_SITE_KEY: {{ .Values.configTurnstileSiteKey | quote }}
CONFIG_OFFLINE: {{ .Values.configOffline | quote }}
CONFIG_EMAIL_ENABLED: {{ and .Values.configEmailEnabled (empty .Values.global.combineSmtpUsername | not) | quote }}
CONFIG_SHOW_CERT_EXPIRATION: {{ .Values.configShowCertExpiration | quote }}
Expand Down
4 changes: 2 additions & 2 deletions deploy/helm/thecombine/charts/frontend/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ imageName: combine_frontend

# The additional domain list is a space-separated string list of domains
combineAddlDomainList: ""
configCaptchaRequired: "false"
configCaptchaSiteKey: "None - from frontend chart"
configTurnstileRequired: "false"
configTurnstileSiteKey: "None - from frontend chart"
configOffline: "false"
configEmailEnabled: "true"
configShowCertExpiration: "false"
Expand Down
6 changes: 4 additions & 2 deletions deploy/helm/thecombine/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ global:
pullSecretName: aws-login-credentials
# Update strategy should be "Recreate" or "Rolling Update"
updateStrategy: Recreate
# Cloudflare Turnstile verification
turnstileSecretKey: "Override"

includeResourceLimits: false

Expand All @@ -54,8 +56,8 @@ certManager:
frontend:
configShowCertExpiration: false
configAnalyticsWriteKey: ""
configCaptchaRequired: false
configCaptchaSiteKey: "None"
configTurnstileRequired: false
configTurnstileSiteKey: "None"

# Maintenance configuration items
maintenance:
Expand Down
Loading
Loading