CAPTCHA
A portal's account pages accept submissions from anybody: a script can create accounts, or make the portal mail password-reset and confirmation links to addresses it chooses. A CAPTCHA asks each visitor to show they are a person before the form is sent. Power Portals Pro supports Cloudflare Turnstile, Google reCAPTCHA (v2 and v3) and hCaptcha, and is off until you configure one.
Note
The server checks the challenge, not just the page. Once a provider is configured, the auth endpoints and the server-rendered account pages refuse a submission of a protected form without a solved challenge, whatever the browser sends.
Which forms are protected
CaptchaOptions.Forms chooses the forms, as a combination of CaptchaForms values. The default protects the forms an automated caller can abuse without knowing any password.
| CaptchaForms | Form | By default |
|---|---|---|
Register |
Creating a local account. | On |
ForgotPassword |
Asking for a password-reset link. | On |
ResendEmailConfirmation |
Asking for another email-confirmation link. | On |
Login |
Signing in with an email address and password. Turn it on to slow down password guessing; repeated wrong passwords also lock the account for a while. | Off |
ExternalLogin |
Completing a sign-in through Microsoft, Google or another provider with an email address the provider didn't verify, which the portal then mails a confirmation to. A sign-in whose address the provider verified completes without a challenge. | Off |
Choosing a provider
The three providers work the same way as far as the portal is concerned; they differ in what the visitor sees and in whose account you need. Each has a free tier.
| Provider | Account you need | What the visitor sees | Links |
|---|---|---|---|
| Cloudflare Turnstile | A free Cloudflare account. Your portal does not have to be hosted on Cloudflare or use its DNS. | Usually nothing to do: the check runs by itself and only occasionally asks for a click. | Dashboard · Documentation |
| Google reCAPTCHA v2 | A Google account. | The “I'm not a robot” checkbox, sometimes followed by an image puzzle. | Admin console · Documentation |
| hCaptcha | A free hCaptcha account. | A checkbox, sometimes followed by an image puzzle. | Dashboard · Documentation |
| Google reCAPTCHA v3 | A Google account. | Nothing at all: Google scores each submission, and Google's badge sits in the page's corner. A visitor scored too low is refused and can't prove otherwise. | Admin console · Documentation |
Whichever you choose, the provider gives you two keys for your site: a site key, which is public and is sent to every browser to draw the widget, and a secret key, which only the server uses to check a visitor's answer. Keep the secret key out of source control.
Setting up Cloudflare Turnstile
- Sign up for a free account at dash.cloudflare.com. You don't need to add a website or move any DNS.
- Open Turnstile from the dashboard's menu and choose Add widget.
- Name the widget and add every hostname the portal is served from — its production domain, any deployment slot's, and
localhostif you test with real keys. - Choose the Managed widget mode, which only asks for a click when Cloudflare isn't sure.
- Create the widget and copy its Site Key and Secret Key.
Setting up Google reCAPTCHA v2
- Sign in to the reCAPTCHA admin console with a Google account and create a new site.
- Choose Challenge (v2) and then “I'm not a robot” Checkbox.
- Add every domain the portal is served from, and
localhostif you test with real keys. - Leave Verify the origin of reCAPTCHA solutions on, so a token solved on another site is refused. Submit, and copy the site key and the secret key.
Important
Use provider
ReCaptchaV2and theCaptcha:ReCaptchaV2section only for a v2 “I'm not a robot” checkbox key — the widget shows “Invalid key type” for any other. A v3 key goes underReCaptchaV3, below; the invisible v2 badge isn't supported. Google may ask you to attach the key to a Google Cloud project; that doesn't change how the portal uses it.
Setting up Google reCAPTCHA v3
v3 shows no challenge. The page asks Google for a token when the form is submitted, tagged with the form it came from, and the server refuses a token scored below MinimumScore or tagged for another form. Use provider ReCaptchaV3 and its own section, Captcha:ReCaptchaV3: a v3 key is a different key from a v2 one.
- In the reCAPTCHA admin console, create a new site and choose Score based (v3).
- Add every domain the portal is served from, and
localhostif you test with real keys. Leave Verify the origin of reCAPTCHA solutions on. - Submit, and copy the site key and the secret key into
Captcha:ReCaptchaV3. - Set
MinimumScore, from 0.0 to 1.0. It defaults to 0.5, Google's suggested starting point; the admin console shows the scores your visitors get, to tune it by.
Important
A visitor scored below
MinimumScoreis refused with nothing to solve, and is told to try again in a moment. Raise it with care, and watch the scores in the admin console after you do. Google publishes no test keys for v3.
Setting up hCaptcha
- Sign up for a free account at dashboard.hcaptcha.com.
- Under Sites, add a new site and copy its Site Key.
- In the site's settings, add every hostname the portal is served from. hCaptcha doesn't accept
localhostor127.0.0.1: to try real keys on your machine, map a name such asdev.example.comto127.0.0.1in your hosts file and add that, or use the test keys below. - Copy the secret key from your account settings (or the site's own secret, where the dashboard offers one).
Test keys for local development
Each provider publishes a pair of test keys that work on any hostname, localhost included, and always pass — so you can try the portal's forms before creating an account. The widget shows that it is in test mode.
| Provider | Site key | Secret key |
|---|---|---|
Turnstile |
1x00000000000000000000AA |
1x0000000000000000000000000000000AA |
ReCaptchaV2 |
6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI |
6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe |
HCaptcha |
10000000-ffff-ffff-ffff-000000000001 |
0x0000000000000000000000000000000000000000 |
Important
Test keys accept every visitor, robots included. Never deploy them: replace them with your own keys before the portal goes live.
Configuring the portal
Projects generated from the templates bind CaptchaOptions from the Captcha section of appsettings.json, which starts with Provider set to None. Each provider has a section of its own for its keys — Turnstile, ReCaptchaV2, ReCaptchaV3 and HCaptcha — and Provider chooses which is used, so you can keep several configured and switch between them with one setting. Put each provider's site key in its section. Forms takes the form names separated by commas; leave it out to keep the default.
{
"Captcha": {
"Provider": "Turnstile",
"Forms": "Register, ForgotPassword, ResendEmailConfirmation",
"Turnstile": {
"SiteKey": "<Turnstile site key>"
},
"ReCaptchaV2": {
"SiteKey": "<reCAPTCHA v2 site key>"
},
"ReCaptchaV3": {
"SiteKey": "<reCAPTCHA v3 site key>",
"MinimumScore": 0.5
}
}
}
Keep the secret keys in user secrets while developing, and in Key Vault or the App Service's settings when deployed — never in appsettings.json. In Key Vault, name them Captcha--Turnstile--SecretKey and so on:
dotnet user-secrets set "Captcha:Turnstile:SecretKey" "<Turnstile secret key>"
dotnet user-secrets set "Captcha:ReCaptchaV2:SecretKey" "<reCAPTCHA v2 secret key>"
dotnet user-secrets set "Captcha:ReCaptchaV3:SecretKey" "<reCAPTCHA v3 secret key>"
dotnet user-secrets set "Captcha:HCaptcha:SecretKey" "<hCaptcha secret key>"
This is the line in Program.cs that reads the section. A portal that wasn't generated from a template adds it after AddPowerPortalsProWebServer():
builder.Services.Configure<CaptchaOptions>(builder.Configuration.GetSection("Captcha"));
Or set the options in code, like any other IOptions class:
builder.Services.Configure<CaptchaOptions>(options =>
{
options.Provider = CaptchaProvider.HCaptcha;
options.HCaptcha.SiteKey = "<site key>";
options.HCaptcha.SecretKey = builder.Configuration.GetRequiredValue("Captcha:HCaptcha:SecretKey");
options.Forms = CaptchaForms.Register | CaptchaForms.ForgotPassword | CaptchaForms.Login;
});
Tip
A provider set without both of its keys stops the portal at startup with a message naming what is missing, rather than starting with forms that would refuse every visitor.
Adding the challenge to your own forms
The template's account pages already carry it. On a form of your own, place the Captcha component — it renders nothing unless the form you name requires a challenge — and send the token it hands over when the form is submitted: TakeTokenAsync() in Blazor, takeToken() on the ref in React. A submission the server refused for the challenge raises CaptchaVerificationException in Blazor and CaptchaVerificationError in React.
const captcha = useRef<CaptchaHandle>(null);
const register = async () => {
try {
await auth.registerAsync({ email, password, captchaToken: (await captcha.current?.takeToken()) ?? null });
} catch (error) {
// Tells the visitor to solve the challenge again, and resets it.
if (error instanceof CaptchaVerificationError) captcha.current?.reportNotSolved();
}
};
<Captcha ref={captcha} form={CaptchaForms.Register} /><Captcha @ref="_captcha" Form="CaptchaForms.Register" />
@code {
private Captcha? _captcha;
private async Task RegisterAsync()
{
try
{
await AuthService.RegisterAsync(new RegisterRequest
{
Email = _email,
Password = _password,
CaptchaToken = await _captcha!.TakeTokenAsync(),
});
}
catch (CaptchaVerificationException)
{
// Tells the visitor to solve the challenge again, and resets it.
await _captcha!.ReportNotSolvedAsync();
}
}
}A token is good for one submission, so taking it also resets the challenge: whatever the outcome — a wrong password, an address already in use — the next attempt gets a new one without the page doing anything. Under reCAPTCHA v3 there is no token until it is taken, so don't hold a submit button back until TokenChanged (Blazor) or onTokenChange (React) reports one.
A server-rendered Blazor page that calls UserManager itself, rather than going through IAuthService, checks the token with ICaptchaVerifier before acting:
@inject ICaptchaVerifier CaptchaVerifier
if (!await CaptchaVerifier.VerifyAsync(CaptchaForms.Register, await _captcha!.TakeTokenAsync()))
{
await _captcha!.ReportNotSolvedAsync();
return;
}
Limiting attempts per address
Whether or not a CAPTCHA is configured, the portal limits how often one address submits each form anyone can submit without signing in — signing in, registering, resetting a password, resending a confirmation, accepting an invitation. By default an address may submit each 20 times a minute; past that, it is answered with 429 Too Many Requests until the minute is up. Each form is counted on its own. Change the limit under PowerPortalsPro:SignInRateLimit; a PermitLimit of 0 turns it off.
{
"PowerPortalsPro": {
"SignInRateLimit": {
"PermitLimit": 20,
"Window": "00:01:00"
}
}
}
Important
The limit counts the address a request comes from. Behind a proxy or gateway that doesn't pass the visitor's address on — Azure Front Door, Application Gateway, a load balancer of your own — every visitor arrives from the proxy's, and they share one count. Forward the visitor's address with
UseForwardedHeaders, naming the proxy inKnownProxiesorKnownNetworks.
Content Security Policy
The widget is loaded from the provider's site. If your portal sends a Content-Security-Policy header, allow the provider's hosts in script-src and frame-src (hCaptcha also needs style-src and connect-src):
| Provider | Hosts |
|---|---|
Turnstile |
https://challenges.cloudflare.com |
ReCaptchaV2 |
https://www.google.com/recaptcha/, https://www.gstatic.com/recaptcha/ |
HCaptcha |
https://hcaptcha.com, https://*.hcaptcha.com |
Troubleshooting
- Every submission is refused. The secret key is wrong, or belongs to another site key. The server logs an error naming the provider's complaint each time.
- The form says the challenge couldn't be loaded. The provider's script was blocked — by a browser extension, a network filter or a Content Security Policy — or the browser is offline.
- The widget shows an error about the domain. The hostname the portal is served from isn't in the provider's list for this site key.
- A solved challenge is refused. Tokens expire after a few minutes and can be used once. The widget usually renews itself; if a visitor leaves the form open, they solve it again.
- The portal won't start after setting a provider. That provider's
SiteKeyorSecretKeyis missing — underCaptcha:Turnstile,Captcha:ReCaptchaV2,Captcha:ReCaptchaV3orCaptcha:HCaptcha— or, for reCAPTCHA v3,MinimumScoreisn't between 0 and 1. The startup error says which options failed.
