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

Recaptcha on password reset and email verification #61

Open
wants to merge 4 commits into
base: cesmii/develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# UA Cloud Library

The reference implementation of the UA Cloud Library. The UA Cloud Library enables the storage in and querying of OPC UA Information Models from anywhere in the world.
The reference implementation of the UA Cloud Library. The UA Cloud Library enables the storage in and querying of OPC UA Information Models from anywhere in the world.

## Features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,55 @@
@model ForgotPasswordModel
@{
ViewData["Title"] = "Forgot your password?";
var _reCaptchaUrl = $"{@Model.CaptchaSettings.ClientApiUrl}{@Model.CaptchaSettings.SiteKey}";
}

<h1>@ViewData["Title"]</h1>
<h2>Enter your email.</h2>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<form id="resetForm" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<span asp-validation-for="CaptchaResponseToken" class="text-danger"></span>
<div class="form-floating">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
<label asp-for="Input.Email" class="form-label"></label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset Password</button>
<input type="hidden" asp-for="CaptchaResponseToken" id="CaptchaResponseToken" />
@if (!Model.CaptchaSettings.Enabled)
{
<button id="resetSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Reset Password</button>
}
else
{
<button id="resetSubmit" type="button" class="w-100 btn btn-lg btn-primary">Reset Password</button>
}
</form>
</div>
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />
@if (Model.CaptchaSettings.Enabled)
{
<script src='@_reCaptchaUrl'></script>
<script>

function reCaptchaExecute() {
grecaptcha.execute('@Model.CaptchaSettings.SiteKey', { action: 'register' }).then(function (token) {
//populate token value in hidden field
document.getElementById("CaptchaResponseToken").value = token;
//submit form
$('#resetForm').submit();
});
}

//wire up reset button to execute reCaptcha before submitting
const btnResetSubmit = document.getElementById("resetSubmit");
btnResetSubmit.addEventListener('click', reCaptchaExecute, false);
//Note - if run on load, the token expires after 2 mins
</script>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,43 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Opc.Ua.Cloud.Library.Authentication;
using Opc.Ua.Cloud.Library.Interfaces;

namespace Opc.Ua.Cloud.Library.Areas.Identity.Pages.Account
{
public class ForgotPasswordModel : PageModel
{
private readonly UserManager<IdentityUser> _userManager;
private readonly IEmailSender _emailSender;
private readonly Interfaces.ICaptchaValidation _captchaValidation;
private readonly CaptchaSettings _captchaSettings;

public ForgotPasswordModel(UserManager<IdentityUser> userManager, IEmailSender emailSender)
public ForgotPasswordModel(
UserManager<IdentityUser> userManager,
IEmailSender emailSender,
IConfiguration configuration,
Interfaces.ICaptchaValidation captchaValidation)
{
_userManager = userManager;
_emailSender = emailSender;
_captchaValidation = captchaValidation;

_captchaSettings = new CaptchaSettings();
configuration.GetSection("CaptchaSettings").Bind(_captchaSettings);
}

/// Populate values for cshtml to use
/// </summary>
public CaptchaSettings CaptchaSettings { get { return _captchaSettings; } }

/// <summary>
/// Populate a token returned from client side call to Google Captcha
/// </summary>
[BindProperty]
public string CaptchaResponseToken { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
Expand All @@ -52,6 +74,10 @@ public class InputModel

public async Task<IActionResult> OnPostAsync()
{
//Captcha validate
var captchaResult = await _captchaValidation.ValidateCaptcha(CaptchaResponseToken);
if (!string.IsNullOrEmpty(captchaResult)) ModelState.AddModelError("CaptchaResponseToken", captchaResult);

if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(Input.Email).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
@{
ViewData["Title"] = "Manage Email";
ViewData["ActivePage"] = ManageNavPages.Email;
var _reCaptchaUrl = $"{@Model.CaptchaSettings.ClientApiUrl}{@Model.CaptchaSettings.SiteKey}";
}

<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="email-form" method="post">
<form id="emailForm" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<span asp-validation-for="CaptchaResponseToken" class="text-danger"></span>
<input type="hidden" asp-for="CaptchaResponseToken" id="CaptchaResponseToken" />
@if (Model.IsEmailConfirmed)
{
<div class="form-floating input-group">
Expand All @@ -26,19 +29,53 @@
<div class="form-floating">
<input asp-for="Email" class="form-control" disabled />
<label asp-for="Email" class="form-label"></label>
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
<button id="emailVerificationSubmit" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
</div>
}
<div class="form-floating">
<input asp-for="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" />
<label asp-for="Input.NewEmail" class="form-label"></label>
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
</div>
<button id="change-email-button" type="submit" asp-page-handler="ChangeEmail" class="w-100 btn btn-lg btn-primary">Change email</button>
<button id="changeEmailSubmit" type="submit" asp-page-handler="ChangeEmail" class="w-100 btn btn-lg btn-primary">Change email</button>
</form>
</div>
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />
@if (Model.CaptchaSettings.Enabled)
{
<script src='@_reCaptchaUrl'></script>
<script>
function reCaptchaExecuteVerify() {
grecaptcha.execute('@Model.CaptchaSettings.SiteKey', { action: 'reverifyEmail' }).then(function (token) {
//populate token value in hidden field
document.getElementById("CaptchaResponseToken").value = token;
//submit form
$('#emailForm').submit();
});
}

//wire up register button to execute reCaptcha before submitting
const btnVerifySubmit = document.getElementById("emailVerificationSubmit");
btnVerifySubmit.addEventListener('click', reCaptchaExecuteVerify, false);
//Note - if run on load, the token expires after 2 mins

function reCaptchaExecute() {
grecaptcha.execute('@Model.CaptchaSettings.SiteKey', { action: 'reverifyEmail' }).then(function (token) {
//populate token value in hidden field
document.getElementById("CaptchaResponseToken").value = token;
//submit form
$('#emailForm').submit();
});
}

//wire up register button to execute reCaptcha before submitting
const btnChangeEmail = document.getElementById("changeEmailSubmit");
btnChangeEmail.addEventListener('click', reCaptchaExecute, false);
//Note - if run on load, the token expires after 2 mins
</script>
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Opc.Ua.Cloud.Library.Authentication;

namespace Opc.Ua.Cloud.Library.Areas.Identity.Pages.Account.Manage
Expand All @@ -20,15 +21,23 @@ public class EmailModel : PageModel
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly Interfaces.ICaptchaValidation _captchaValidation;
private readonly CaptchaSettings _captchaSettings;

public EmailModel(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IEmailSender emailSender)
IEmailSender emailSender,
IConfiguration configuration,
Interfaces.ICaptchaValidation captchaValidation)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_captchaValidation = captchaValidation;

_captchaSettings = new CaptchaSettings();
configuration.GetSection("CaptchaSettings").Bind(_captchaSettings);
}

/// <summary>
Expand All @@ -43,6 +52,17 @@ public EmailModel(
/// </summary>
public bool IsEmailConfirmed { get; set; }

/// <summary>
/// Populate values for cshtml to use
/// </summary>
public CaptchaSettings CaptchaSettings { get { return _captchaSettings; } }

/// <summary>
/// Populate a token returned from client side call to Google Captcha
/// </summary>
[BindProperty]
public string CaptchaResponseToken { get; set; }

/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
Expand Down Expand Up @@ -105,6 +125,9 @@ public async Task<IActionResult> OnPostChangeEmailAsync()
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var captchaResult = await _captchaValidation.ValidateCaptcha(CaptchaResponseToken);
if (!string.IsNullOrEmpty(captchaResult)) ModelState.AddModelError("CaptchaResponseToken", captchaResult);

if (!ModelState.IsValid)
{
await LoadAsync(user);
Expand Down Expand Up @@ -145,6 +168,10 @@ public async Task<IActionResult> OnPostSendVerificationEmailAsync()
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

//Captcha validate
var captchaResult = await _captchaValidation.ValidateCaptcha(CaptchaResponseToken);
if (!string.IsNullOrEmpty(captchaResult)) ModelState.AddModelError("CaptchaResponseToken", captchaResult);

if (!ModelState.IsValid)
{
await LoadAsync(user).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,8 @@
{
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">
article
about setting up this ASP.NET application to support logging in via external services
</a>.
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
</p>
</div>
}
Expand Down
8 changes: 0 additions & 8 deletions UACloudLibraryServer/CaptchaValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,6 @@ public async Task<string> ValidateCaptcha(string responseToken)
return "Error(s) occurred validating the Captcha response. Please contact your system administrator.";
}

// check reCaptcha response action
//if (recaptchaResponse.action.ToUpper() != expected_action.ToUpper())
//{
// //Logging.Log(new Logging.LogItem { Msg = $"Google RecCaptcha action doesn't match:\nExpected action: {expected_action} Given action: {recaptcha_response.action}" }, DefaultLogValues);
// return (recaptchaResponse, false);
//}

// anything less than 0.5 is a bot
if (recaptchaResponse.score < _captchaSettings.BotThreshold)
{
Expand All @@ -191,7 +184,6 @@ public async Task<string> ValidateCaptcha(string responseToken)
}
finally
{
// Dispose once all HttpClient calls are complete. This is not necessary if the containing object will be disposed of; for example in this case the HttpClient instance will be disposed automatically when the application terminates so the following call is superfluous.
client.Dispose();
}
}
Expand Down
33 changes: 33 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pool:
vmImage: 'windows-latest'

variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
inputs:
restoreSolution: '$(solution)'

- task: VSBuild@1
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'

- task: VSTest@2
inputs:
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'