forked from OPCFoundation/UA-CloudLibrary
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from cesmii/cesmii/develop
API Key auth, Captcha
- Loading branch information
Showing
33 changed files
with
1,605 additions
and
120 deletions.
There are no files selected for viewing
119 changes: 119 additions & 0 deletions
119
UACloudLibraryServer/Areas/Identity/Pages/Account/Manage/ManageApiKeys.cshtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" /> | ||
} |
145 changes: 145 additions & 0 deletions
145
UACloudLibraryServer/Areas/Identity/Pages/Account/Manage/ManageApiKeys.cshtml.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.