remote service support via Jwt
This commit is contained in:
@ -45,6 +45,9 @@
|
||||
[Parameter]
|
||||
public string RemoteIPAddress { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string AuthorizationToken { get; set; }
|
||||
|
||||
private bool _initialized = false;
|
||||
private string _display = "display: none;";
|
||||
private Installation _installation = new Installation { Success = false, Message = "" };
|
||||
@ -55,7 +58,7 @@
|
||||
{
|
||||
SiteState.RemoteIPAddress = RemoteIPAddress;
|
||||
SiteState.AntiForgeryToken = AntiForgeryToken;
|
||||
InstallationService.SetAntiForgeryTokenHeader(AntiForgeryToken);
|
||||
SiteState.AuthorizationToken = AuthorizationToken;
|
||||
|
||||
_installation = await InstallationService.IsInstalled();
|
||||
if (_installation.Alias != null)
|
||||
|
@ -131,7 +131,7 @@ else
|
||||
</Section>
|
||||
<Section Name="Cookie" Heading="Cookie Settings" ResourceKey="CookieSettings">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="cookietype" HelpText="Cookies are managed per domain by default. However you can also choose to have distinct cookies for each site." ResourceKey="CookieType">Cookie Type:</Label>
|
||||
<Label Class="col-sm-3" For="cookietype" HelpText="Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site." ResourceKey="CookieType">Cookie Type:</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="cookietype" class="form-select" @bind="@_cookietype">
|
||||
<option value="domain">@Localizer["Domain"]</option>
|
||||
@ -274,25 +274,25 @@ else
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Secret">Issuer:</Label>
|
||||
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="issuer" class="form-control" @bind="@_issuer" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Secret">Audience:</Label>
|
||||
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="audience" class="form-control" @bind="@_audience" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Secret">Lifetime:</Label>
|
||||
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="lifetime" class="form-control" @bind="@_lifetime" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate an access token. Be sure to save this token in a safe place as you will not be able to view it in the future." ResourceKey="Token">Access Token:</Label>
|
||||
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input id="token" class="form-control" @bind="@_token" />
|
||||
|
@ -26,6 +26,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.3" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
@ -30,6 +30,7 @@ namespace Oqtane.Client
|
||||
var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
|
||||
|
||||
builder.Services.AddSingleton(httpClient);
|
||||
builder.Services.AddHttpClient("Remote");
|
||||
builder.Services.AddOptions();
|
||||
|
||||
// Register localization services
|
||||
|
@ -127,10 +127,10 @@
|
||||
<value>Delete User</value>
|
||||
</data>
|
||||
<data name="AllowRegistration.HelpText" xml:space="preserve">
|
||||
<value>Do you want the users to be able to register for an account on the site</value>
|
||||
<value>Do you want anonymous visitors to be able to register for an account on the site</value>
|
||||
</data>
|
||||
<data name="AllowRegistration.Text" xml:space="preserve">
|
||||
<value>Allow User Registration? </value>
|
||||
<value>Allow Registration? </value>
|
||||
</data>
|
||||
<data name="Error.SaveSiteSettings" xml:space="preserve">
|
||||
<value>Error Saving Settings</value>
|
||||
@ -309,4 +309,49 @@
|
||||
<data name="UserInfoUrl.Text" xml:space="preserve">
|
||||
<value>User Info Url:</value>
|
||||
</data>
|
||||
<data name="Audience.HelpText" xml:space="preserve">
|
||||
<value>Optionally provide the audience for the token</value>
|
||||
</data>
|
||||
<data name="Audience.Text" xml:space="preserve">
|
||||
<value>Audience:</value>
|
||||
</data>
|
||||
<data name="CookieSettings.Heading" xml:space="preserve">
|
||||
<value>Cookie Settings</value>
|
||||
</data>
|
||||
<data name="CookieType.HelpText" xml:space="preserve">
|
||||
<value>Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site.</value>
|
||||
</data>
|
||||
<data name="CookieType.Text" xml:space="preserve">
|
||||
<value>Cookie Type:</value>
|
||||
</data>
|
||||
<data name="CreateToken" xml:space="preserve">
|
||||
<value>Create Token</value>
|
||||
</data>
|
||||
<data name="Issuer.HelpText" xml:space="preserve">
|
||||
<value>Optionally provide the issuer of the token</value>
|
||||
</data>
|
||||
<data name="Issuer.Text" xml:space="preserve">
|
||||
<value>Issuer:</value>
|
||||
</data>
|
||||
<data name="Lifetime.HelpText" xml:space="preserve">
|
||||
<value>The number of minutes for which a token should be valid</value>
|
||||
</data>
|
||||
<data name="Lifetime.Text" xml:space="preserve">
|
||||
<value>Lifetime:</value>
|
||||
</data>
|
||||
<data name="Secret.HelpText" xml:space="preserve">
|
||||
<value>If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated.</value>
|
||||
</data>
|
||||
<data name="Secret.Text" xml:space="preserve">
|
||||
<value>Site Secret:</value>
|
||||
</data>
|
||||
<data name="Token.HelpText" xml:space="preserve">
|
||||
<value>Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future.</value>
|
||||
</data>
|
||||
<data name="Token.Text" xml:space="preserve">
|
||||
<value>Token:</value>
|
||||
</data>
|
||||
<data name="TokenSettings.Heading" xml:space="preserve">
|
||||
<value>Token Settings</value>
|
||||
</data>
|
||||
</root>
|
@ -50,14 +50,5 @@ namespace Oqtane.Services
|
||||
{
|
||||
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
|
||||
}
|
||||
|
||||
public void SetAntiForgeryTokenHeader(string antiforgerytokenvalue)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(antiforgerytokenvalue))
|
||||
{
|
||||
AddRequestHeader(Constants.AntiForgeryTokenHeaderName, antiforgerytokenvalue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,5 @@ namespace Oqtane.Services
|
||||
/// <returns></returns>
|
||||
Task RegisterAsync(string email);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the antiforgerytoken header so that it is included on all HttpClient calls for the lifetime of the app
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
void SetAntiForgeryTokenHeader(string antiforgerytokenvalue);
|
||||
}
|
||||
}
|
||||
|
147
Oqtane.Client/Services/RemoteServiceBase.cs
Normal file
147
Oqtane.Client/Services/RemoteServiceBase.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public class RemoteServiceBase
|
||||
{
|
||||
private readonly SiteState _siteState;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
protected RemoteServiceBase(IHttpClientFactory httpClientFactory, SiteState siteState)
|
||||
{
|
||||
_siteState = siteState;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
private HttpClient GetHttpClient()
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient("Remote");
|
||||
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && _siteState != null && !string.IsNullOrEmpty(_siteState.AuthorizationToken))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + _siteState.AuthorizationToken);
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
protected async Task GetAsync(string uri)
|
||||
{
|
||||
var response = await GetHttpClient().GetAsync(uri);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
protected async Task<string> GetStringAsync(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetHttpClient().GetStringAsync(uri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected async Task<byte[]> GetByteArrayAsync(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetHttpClient().GetByteArrayAsync(uri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected async Task<T> GetJsonAsync<T>(string uri)
|
||||
{
|
||||
var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
|
||||
if (CheckResponse(response) && ValidateJsonContent(response.Content))
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<T>();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected async Task PutAsync(string uri)
|
||||
{
|
||||
var response = await GetHttpClient().PutAsync(uri, null);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
protected async Task<T> PutJsonAsync<T>(string uri, T value)
|
||||
{
|
||||
return await PutJsonAsync<T, T>(uri, value);
|
||||
}
|
||||
|
||||
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
|
||||
{
|
||||
var response = await GetHttpClient().PutAsJsonAsync(uri, value);
|
||||
if (CheckResponse(response) && ValidateJsonContent(response.Content))
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<TResult>();
|
||||
return result;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
protected async Task PostAsync(string uri)
|
||||
{
|
||||
var response = await GetHttpClient().PostAsync(uri, null);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
protected async Task<T> PostJsonAsync<T>(string uri, T value)
|
||||
{
|
||||
return await PostJsonAsync<T, T>(uri, value);
|
||||
}
|
||||
|
||||
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
|
||||
{
|
||||
var response = await GetHttpClient().PostAsJsonAsync(uri, value);
|
||||
if (CheckResponse(response) && ValidateJsonContent(response.Content))
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<TResult>();
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected async Task DeleteAsync(string uri)
|
||||
{
|
||||
var response = await GetHttpClient().DeleteAsync(uri);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
private bool CheckResponse(HttpResponseMessage response)
|
||||
{
|
||||
if (response.IsSuccessStatusCode) return true;
|
||||
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
|
||||
{
|
||||
Console.WriteLine($"Request: {response.RequestMessage.RequestUri}");
|
||||
Console.WriteLine($"Response status: {response.StatusCode} {response.ReasonPhrase}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ValidateJsonContent(HttpContent content)
|
||||
{
|
||||
var mediaType = content?.Headers.ContentType?.MediaType;
|
||||
return mediaType != null && mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,24 +5,31 @@ using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Documentation;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
|
||||
public class ServiceBase
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly SiteState _siteState;
|
||||
|
||||
protected ServiceBase(HttpClient client, SiteState siteState)
|
||||
protected ServiceBase(HttpClient httpClient, SiteState siteState)
|
||||
{
|
||||
_http = client;
|
||||
_httpClient = httpClient;
|
||||
_siteState = siteState;
|
||||
}
|
||||
|
||||
private HttpClient GetHttpClient()
|
||||
{
|
||||
if (!_httpClient.DefaultRequestHeaders.Contains(Constants.AntiForgeryTokenHeaderName) && _siteState != null && !string.IsNullOrEmpty(_siteState.AntiForgeryToken))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Add(Constants.AntiForgeryTokenHeaderName, _siteState.AntiForgeryToken);
|
||||
}
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
// should be used with new constructor
|
||||
public string CreateApiUrl(string serviceName)
|
||||
{
|
||||
@ -95,24 +102,9 @@ namespace Oqtane.Services
|
||||
}
|
||||
}
|
||||
|
||||
// note that HttpClient is registered as a Scoped(shared) service and therefore you should not use request headers whose value can vary over the lifetime of the service
|
||||
protected void AddRequestHeader(string name, string value)
|
||||
{
|
||||
RemoveRequestHeader(name);
|
||||
_http.DefaultRequestHeaders.Add(name, value);
|
||||
}
|
||||
|
||||
protected void RemoveRequestHeader(string name)
|
||||
{
|
||||
if (_http.DefaultRequestHeaders.Contains(name))
|
||||
{
|
||||
_http.DefaultRequestHeaders.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task GetAsync(string uri)
|
||||
{
|
||||
var response = await _http.GetAsync(uri);
|
||||
var response = await GetHttpClient().GetAsync(uri);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
@ -120,7 +112,7 @@ namespace Oqtane.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _http.GetStringAsync(uri);
|
||||
return await GetHttpClient().GetStringAsync(uri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -134,7 +126,7 @@ namespace Oqtane.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _http.GetByteArrayAsync(uri);
|
||||
return await GetHttpClient().GetByteArrayAsync(uri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -146,7 +138,7 @@ namespace Oqtane.Services
|
||||
|
||||
protected async Task<T> GetJsonAsync<T>(string uri)
|
||||
{
|
||||
var response = await _http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
|
||||
var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
|
||||
if (CheckResponse(response) && ValidateJsonContent(response.Content))
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<T>();
|
||||
@ -157,7 +149,7 @@ namespace Oqtane.Services
|
||||
|
||||
protected async Task PutAsync(string uri)
|
||||
{
|
||||
var response = await _http.PutAsync(uri, null);
|
||||
var response = await GetHttpClient().PutAsync(uri, null);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
@ -168,7 +160,7 @@ namespace Oqtane.Services
|
||||
|
||||
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync(uri, value);
|
||||
var response = await GetHttpClient().PutAsJsonAsync(uri, value);
|
||||
if (CheckResponse(response) && ValidateJsonContent(response.Content))
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<TResult>();
|
||||
@ -179,7 +171,7 @@ namespace Oqtane.Services
|
||||
|
||||
protected async Task PostAsync(string uri)
|
||||
{
|
||||
var response = await _http.PostAsync(uri, null);
|
||||
var response = await GetHttpClient().PostAsync(uri, null);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
@ -190,7 +182,7 @@ namespace Oqtane.Services
|
||||
|
||||
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync(uri, value);
|
||||
var response = await GetHttpClient().PostAsJsonAsync(uri, value);
|
||||
if (CheckResponse(response) && ValidateJsonContent(response.Content))
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<TResult>();
|
||||
@ -202,7 +194,7 @@ namespace Oqtane.Services
|
||||
|
||||
protected async Task DeleteAsync(string uri)
|
||||
{
|
||||
var response = await _http.DeleteAsync(uri);
|
||||
var response = await GetHttpClient().DeleteAsync(uri);
|
||||
CheckResponse(response);
|
||||
}
|
||||
|
||||
@ -228,7 +220,7 @@ namespace Oqtane.Services
|
||||
// This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead.
|
||||
protected ServiceBase(HttpClient client)
|
||||
{
|
||||
_http = client;
|
||||
_httpClient = client;
|
||||
}
|
||||
|
||||
[Obsolete("This method is obsolete. Use CreateApiUrl(string serviceName, Alias alias) in conjunction with ControllerRoutes.ApiRoute in Controllers instead.", false)]
|
||||
|
Reference in New Issue
Block a user