Skip to content

Commit

Permalink
Merge pull request #56 from cesmii/cesmii/develop
Browse files Browse the repository at this point in the history
API Key auth, Captcha
  • Loading branch information
MarkusHorstmann authored Nov 7, 2023
2 parents 09c6e59 + 531ff54 commit 5560594
Show file tree
Hide file tree
Showing 33 changed files with 1,605 additions and 120 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
@page
@model ManageApiKeysModel
@{
ViewData["Title"] = "Manage API Keys";
ViewData["ActivePage"] = ManageNavPages.ManageApiKeys;
}

<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<script type="module" src="~/lib/clipboard-copy-element/dist/index.js"></script>
<script>
document.addEventListener('clipboard-copy', function (event) {
const notice = event.target.querySelector('.notice')
notice.hidden = false
const copyButton = event.target.querySelector('.copyButton')
copyButton.hidden = true
setTimeout(function () {
notice.hidden = true
copyButton.hidden = false
}, 1000)
})
</script>
<div class="row">
@if (!string.IsNullOrEmpty(Model.GeneratedApiKey))
{
<div class="col-md-12 pt-2">
<h2>
Generated Key
</h2>
<table class="table">
<tbody>
<tr>
<td>
<div>Name:</div>
</td>
<td>
<div>@Model.GeneratedApiKeyName</div>
</td>
</tr>
<tr>
<td>
<div>Key:</div>
</td>
<td>
<div>
@Model.GeneratedApiKey
<clipboard-copy value="@Model.GeneratedApiKey" title="Copy to clipboard" class="btn btn-sm" style="line-height: 1;vertical-align: top;padding-top: 0px;padding-bottom: 0px">
<span class="copyButton">
<svg height="16" width="16">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
</svg>
</span>
<span class="notice" hidden>
<svg height="16" width="16">
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
</svg>
</span>
</clipboard-copy>
</div>
</td>
</tr>
</tbody>
</table>
<div class="alert">
Usage: Add an HTTP Header X-Api-key with the API key as the header value.
Note that the key name does not need to be provided separately for authentication.
</div>
</div>
}
<div class="col-md-6 pt-2">
<h2>
Generate a new API key
</h2>
<div>
API keys are intended for non-interactive services that need to call the REST and GraphQL APIs. They have all the permissions of the user who created them, including any Administrator role membership.
</div>
<form id="create-api-key-form" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-floating">
<input asp-for="Input.NewApiKeyName" class="form-control" aria-required="true" />
<label asp-for="Input.NewApiKeyName" class="form-label"></label>
<span asp-validation-for="Input.NewApiKeyName" class="text-danger"></span>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Generate API Key</button>
</form>
</div>
@if (Model.ApiKeysAndNames?.Any() == true)
{
<div class="col-md-6 pt-2">
<h2>
Existing API keys
</h2>
<table class="table">
<tbody>
@foreach (var apiKeyAndName in Model.ApiKeysAndNames)
{
<tr>
<td>@apiKeyAndName.KeyName</td>
<td>@apiKeyAndName.KeyPrefix...</td>
<td>
<form asp-page-handler="DeleteApiKey" method="post" id="@($"remove-login-{apiKeyAndName.KeyName}")">
<div>
<input asp-for="@apiKeyAndName.KeyName" name="ApiKeyToDelete" type="hidden" />
<button type="submit" class="btn btn-primary" title="Remove API key @apiKeyAndName.KeyName">Delete</button>
</div>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>

@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Opc.Ua.Cloud.Library.Authentication;

namespace Opc.Ua.Cloud.Library.Areas.Identity.Pages.Account.Manage
{
public class ManageApiKeysModel : PageModel
{
private readonly UserManager<IdentityUser> _userManager;
private readonly ApiKeyTokenProvider _apiKeyTokenProvider;

public ManageApiKeysModel(
UserManager<IdentityUser> userManager,
ApiKeyTokenProvider apiKeyTokenProvider)
{
_userManager = userManager;
_apiKeyTokenProvider = apiKeyTokenProvider;
}

/// <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.
/// </summary>
[BindProperty]
public InputModel Input { 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.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
public List<(string KeyName, string KeyPrefix)> ApiKeysAndNames { get; private set; }

[TempData]
public string GeneratedApiKeyName { get; set; }
[TempData]
public string GeneratedApiKey { 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.
/// </summary>
public class InputModel
{
/// <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.
/// </summary>
[Required]
[DataType(DataType.Text)]
[Display(Name = "API Key Name")]
public string NewApiKeyName { get; set; }
}

public async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User).ConfigureAwait(false);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

ApiKeysAndNames = await _apiKeyTokenProvider.GetUserApiKeysAsync(user).ConfigureAwait(false);
return Page();
}

public async Task<IActionResult> OnPostDeleteApiKeyAsync(string apiKeyToDelete)
{
var user = await _userManager.GetUserAsync(User).ConfigureAwait(false);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var removeResult = await _userManager.RemoveAuthenticationTokenAsync(user, ApiKeyTokenProvider.ApiKeyProviderName, apiKeyToDelete).ConfigureAwait(false);
if (!removeResult.Succeeded)
{
foreach (var error in removeResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
StatusMessage = $"Deleted API key {apiKeyToDelete}.";
return RedirectToPage();
}

public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

try
{
var user = await _userManager.GetUserAsync(User).ConfigureAwait(false);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var newApiKeyName = Input.NewApiKeyName;

try
{
var newApiKey = await ApiKeyTokenProvider.GenerateAndSetAuthenticationTokenAsync(_userManager, user, newApiKeyName).ConfigureAwait(false);
if (string.IsNullOrEmpty(newApiKey))
{
ModelState.AddModelError(string.Empty, "A key with this name already exists.");
ApiKeysAndNames = await _apiKeyTokenProvider.GetUserApiKeysAsync(user).ConfigureAwait(false);
return Page();
}
GeneratedApiKeyName = newApiKeyName;
GeneratedApiKey = newApiKey;
StatusMessage = $"Be sure to save the API key before you leave this page. You will not be able to retrieve it later.";
}
catch (ApiKeyGenerationException ex)
{
foreach (var error in ex.Errors)
{
ModelState.AddModelError(string.Empty, error);
}
return Page();
}

}
catch
{
ModelState.AddModelError(string.Empty, "Error generating key.");
}

return RedirectToPage();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public static class ManageNavPages
/// </summary>
public static string ChangePassword => "ChangePassword";

/// <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.
/// </summary>
public static string ManageApiKeys => "ManageApiKeys";
/// <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 @@ -79,6 +84,12 @@ public static class ManageNavPages
/// </summary>
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);

/// <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.
/// </summary>
public static string ManageApiKeysNavClass(ViewContext viewContext) => PageNavClass(viewContext, ManageApiKeys);

/// <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
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
@inject SignInManager<IdentityUser> SignInManager
@{
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync().ConfigureAwait(false)).Any();
#if APIKEY_AUTH
bool apiKeyAuth = true;
#else
bool apiKeyAuth = false;
#endif
}
<ul class="nav nav-pills flex-column">
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
Expand All @@ -10,5 +15,9 @@
{
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
}
@if (apiKeyAuth)
{
<li class="nav-item"><a class="nav-link @ManageNavPages.ManageApiKeysNavClass(ViewContext)" id="manage-api-keys" asp-page="./ManageApiKeys">API Keys</a></li>
}
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
</ul>
Loading

0 comments on commit 5560594

Please sign in to comment.