Merge pull request #2092 from sbwalker/dev

remote service support via Jwt
This commit is contained in:
Shaun Walker 2022-03-30 08:07:18 -04:00 committed by GitHub
commit 4f1ead116f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 272 additions and 80 deletions

View File

@ -45,6 +45,9 @@
[Parameter] [Parameter]
public string RemoteIPAddress { get; set; } public string RemoteIPAddress { get; set; }
[Parameter]
public string AuthorizationToken { get; set; }
private bool _initialized = false; private bool _initialized = false;
private string _display = "display: none;"; private string _display = "display: none;";
private Installation _installation = new Installation { Success = false, Message = "" }; private Installation _installation = new Installation { Success = false, Message = "" };
@ -55,7 +58,7 @@
{ {
SiteState.RemoteIPAddress = RemoteIPAddress; SiteState.RemoteIPAddress = RemoteIPAddress;
SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AntiForgeryToken = AntiForgeryToken;
InstallationService.SetAntiForgeryTokenHeader(AntiForgeryToken); SiteState.AuthorizationToken = AuthorizationToken;
_installation = await InstallationService.IsInstalled(); _installation = await InstallationService.IsInstalled();
if (_installation.Alias != null) if (_installation.Alias != null)

View File

@ -131,7 +131,7 @@ else
</Section> </Section>
<Section Name="Cookie" Heading="Cookie Settings" ResourceKey="CookieSettings"> <Section Name="Cookie" Heading="Cookie Settings" ResourceKey="CookieSettings">
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<select id="cookietype" class="form-select" @bind="@_cookietype"> <select id="cookietype" class="form-select" @bind="@_cookietype">
<option value="domain">@Localizer["Domain"]</option> <option value="domain">@Localizer["Domain"]</option>
@ -274,25 +274,25 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" /> <input id="issuer" class="form-control" @bind="@_issuer" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" /> <input id="audience" class="form-control" @bind="@_audience" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" /> <input id="lifetime" class="form-control" @bind="@_lifetime" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="token" class="form-control" @bind="@_token" /> <input id="token" class="form-control" @bind="@_token" />

View File

@ -26,6 +26,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" /> <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.Components.WebAssembly.Authentication" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <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="Microsoft.Extensions.Localization" Version="6.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -30,6 +30,7 @@ namespace Oqtane.Client
var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
builder.Services.AddSingleton(httpClient); builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient("Remote");
builder.Services.AddOptions(); builder.Services.AddOptions();
// Register localization services // Register localization services

View File

@ -127,10 +127,10 @@
<value>Delete User</value> <value>Delete User</value>
</data> </data>
<data name="AllowRegistration.HelpText" xml:space="preserve"> <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>
<data name="AllowRegistration.Text" xml:space="preserve"> <data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow User Registration? </value> <value>Allow Registration? </value>
</data> </data>
<data name="Error.SaveSiteSettings" xml:space="preserve"> <data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value> <value>Error Saving Settings</value>
@ -309,4 +309,49 @@
<data name="UserInfoUrl.Text" xml:space="preserve"> <data name="UserInfoUrl.Text" xml:space="preserve">
<value>User Info Url:</value> <value>User Info Url:</value>
</data> </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> </root>

View File

@ -50,14 +50,5 @@ namespace Oqtane.Services
{ {
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true); await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
} }
public void SetAntiForgeryTokenHeader(string antiforgerytokenvalue)
{
if (!string.IsNullOrEmpty(antiforgerytokenvalue))
{
AddRequestHeader(Constants.AntiForgeryTokenHeaderName, antiforgerytokenvalue);
}
}
} }
} }

View File

@ -42,10 +42,5 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task RegisterAsync(string email); 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);
} }
} }

View 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);
}
}
}

View File

@ -5,24 +5,31 @@ using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Services namespace Oqtane.Services
{ {
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ServiceBase public class ServiceBase
{ {
private readonly HttpClient _http; private readonly HttpClient _httpClient;
private readonly SiteState _siteState; private readonly SiteState _siteState;
protected ServiceBase(HttpClient client, SiteState siteState) protected ServiceBase(HttpClient httpClient, SiteState siteState)
{ {
_http = client; _httpClient = httpClient;
_siteState = siteState; _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 // should be used with new constructor
public string CreateApiUrl(string serviceName) 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) protected async Task GetAsync(string uri)
{ {
var response = await _http.GetAsync(uri); var response = await GetHttpClient().GetAsync(uri);
CheckResponse(response); CheckResponse(response);
} }
@ -120,7 +112,7 @@ namespace Oqtane.Services
{ {
try try
{ {
return await _http.GetStringAsync(uri); return await GetHttpClient().GetStringAsync(uri);
} }
catch (Exception e) catch (Exception e)
{ {
@ -134,7 +126,7 @@ namespace Oqtane.Services
{ {
try try
{ {
return await _http.GetByteArrayAsync(uri); return await GetHttpClient().GetByteArrayAsync(uri);
} }
catch (Exception e) catch (Exception e)
{ {
@ -146,7 +138,7 @@ namespace Oqtane.Services
protected async Task<T> GetJsonAsync<T>(string uri) 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)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
return await response.Content.ReadFromJsonAsync<T>(); return await response.Content.ReadFromJsonAsync<T>();
@ -157,7 +149,7 @@ namespace Oqtane.Services
protected async Task PutAsync(string uri) protected async Task PutAsync(string uri)
{ {
var response = await _http.PutAsync(uri, null); var response = await GetHttpClient().PutAsync(uri, null);
CheckResponse(response); CheckResponse(response);
} }
@ -168,7 +160,7 @@ namespace Oqtane.Services
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value) 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)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
var result = await response.Content.ReadFromJsonAsync<TResult>(); var result = await response.Content.ReadFromJsonAsync<TResult>();
@ -179,7 +171,7 @@ namespace Oqtane.Services
protected async Task PostAsync(string uri) protected async Task PostAsync(string uri)
{ {
var response = await _http.PostAsync(uri, null); var response = await GetHttpClient().PostAsync(uri, null);
CheckResponse(response); CheckResponse(response);
} }
@ -190,7 +182,7 @@ namespace Oqtane.Services
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value) 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)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
var result = await response.Content.ReadFromJsonAsync<TResult>(); var result = await response.Content.ReadFromJsonAsync<TResult>();
@ -202,7 +194,7 @@ namespace Oqtane.Services
protected async Task DeleteAsync(string uri) protected async Task DeleteAsync(string uri)
{ {
var response = await _http.DeleteAsync(uri); var response = await GetHttpClient().DeleteAsync(uri);
CheckResponse(response); CheckResponse(response);
} }
@ -228,7 +220,7 @@ namespace Oqtane.Services
// This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead. // This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead.
protected ServiceBase(HttpClient client) 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)] [Obsolete("This method is obsolete. Use CreateApiUrl(string serviceName, Alias alias) in conjunction with ControllerRoutes.ApiRoute in Controllers instead.", false)]

View File

@ -526,16 +526,12 @@ namespace Oqtane.Controllers
public string Token() public string Token()
{ {
var token = ""; var token = "";
var user = _users.GetUser(User.Identity.Name); var sitesettings = HttpContext.GetSiteSettings();
if (user != null) var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{ {
var sitesettings = HttpContext.GetSiteSettings(); var lifetime = 525600; // long-lived token set to 1 year
var secret = sitesettings.GetValue("JwtOptions:Secret", ""); token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), lifetime);
if (!string.IsNullOrEmpty(secret))
{
var lifetime = 525600; // long-lived token set to 1 year
token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), user, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), lifetime);
}
} }
return token; return token;
} }
@ -548,7 +544,10 @@ namespace Oqtane.Controllers
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
user.Username = User.Identity.Name; user.Username = User.Identity.Name;
user.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.PrimarySid).Value); if (User.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
{
user.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
string roles = ""; string roles = "";
foreach (var claim in User.Claims.Where(item => item.Type == ClaimTypes.Role)) foreach (var claim in User.Claims.Where(item => item.Type == ClaimTypes.Role))
{ {

View File

@ -194,7 +194,7 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
internal static IServiceCollection TryAddHttpClientWithAuthenticationCookie(this IServiceCollection services) internal static IServiceCollection AddHttpClients(this IServiceCollection services)
{ {
if (!services.Any(x => x.ServiceType == typeof(HttpClient))) if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{ {
@ -216,6 +216,8 @@ namespace Microsoft.Extensions.DependencyInjection
}); });
} }
services.AddHttpClient("External");
return services; return services;
} }

View File

@ -25,7 +25,7 @@
{ {
@(Html.AntiForgeryToken()) @(Html.AntiForgeryToken())
<component type="typeof(Oqtane.App)" render-mode="@Model.RenderMode" param-AntiForgeryToken="@Model.AntiForgeryToken" param-Runtime="@Model.Runtime" param-RenderMode="@Model.RenderMode.ToString()" param-VisitorId="@Model.VisitorId" param-RemoteIPAddress="@Model.RemoteIPAddress" /> <component type="typeof(Oqtane.App)" render-mode="@Model.RenderMode" param-AntiForgeryToken="@Model.AntiForgeryToken" param-Runtime="@Model.Runtime" param-RenderMode="@Model.RenderMode.ToString()" param-VisitorId="@Model.VisitorId" param-RemoteIPAddress="@Model.RemoteIPAddress" param-AuthorizationToken="@Model.AuthorizationToken" />
<div id="blazor-error-ui"> <div id="blazor-error-ui">
<environment include="Staging,Production"> <environment include="Staging,Production">

View File

@ -20,6 +20,8 @@ using System.Security.Claims;
using System.Net; using System.Net;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Security;
using Oqtane.Extensions;
namespace Oqtane.Pages namespace Oqtane.Pages
{ {
@ -30,6 +32,7 @@ namespace Oqtane.Pages
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly ILanguageRepository _languages; private readonly ILanguageRepository _languages;
private readonly IAntiforgery _antiforgery; private readonly IAntiforgery _antiforgery;
private readonly IJwtManager _jwtManager;
private readonly ISiteRepository _sites; private readonly ISiteRepository _sites;
private readonly IPageRepository _pages; private readonly IPageRepository _pages;
private readonly IUrlMappingRepository _urlMappings; private readonly IUrlMappingRepository _urlMappings;
@ -38,13 +41,14 @@ namespace Oqtane.Pages
private readonly ISettingRepository _settings; private readonly ISettingRepository _settings;
private readonly ILogManager _logger; private readonly ILogManager _logger;
public HostModel(IConfigManager configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors, IAliasRepository aliases, ISettingRepository settings, ILogManager logger) public HostModel(IConfigManager configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, IJwtManager jwtManager, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors, IAliasRepository aliases, ISettingRepository settings, ILogManager logger)
{ {
_configuration = configuration; _configuration = configuration;
_tenantManager = tenantManager; _tenantManager = tenantManager;
_localizationManager = localizationManager; _localizationManager = localizationManager;
_languages = languages; _languages = languages;
_antiforgery = antiforgery; _antiforgery = antiforgery;
_jwtManager = jwtManager;
_sites = sites; _sites = sites;
_pages = pages; _pages = pages;
_urlMappings = urlMappings; _urlMappings = urlMappings;
@ -56,6 +60,7 @@ namespace Oqtane.Pages
public string Language = "en"; public string Language = "en";
public string AntiForgeryToken = ""; public string AntiForgeryToken = "";
public string AuthorizationToken = "";
public string Runtime = "Server"; public string Runtime = "Server";
public RenderMode RenderMode = RenderMode.Server; public RenderMode RenderMode = RenderMode.Server;
public int VisitorId = -1; public int VisitorId = -1;
@ -133,6 +138,17 @@ namespace Oqtane.Pages
Title = site.Name; Title = site.Name;
ThemeType = site.DefaultThemeType; ThemeType = site.DefaultThemeType;
// get jwt token for downstream APIs
if (User.Identity.IsAuthenticated)
{
var sitesettings = HttpContext.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
AuthorizationToken = _jwtManager.GenerateToken(alias, (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
}
}
if (site.VisitorTracking) if (site.VisitorTracking)
{ {
TrackVisitor(site.SiteId); TrackVisitor(site.SiteId);
@ -247,9 +263,9 @@ namespace Oqtane.Pages
string url = Request.GetEncodedUrl(); string url = Request.GetEncodedUrl();
string referrer = (Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Request.Headers[HeaderNames.Referer] : ""; string referrer = (Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Request.Headers[HeaderNames.Referer] : "";
int? userid = null; int? userid = null;
if (User.HasClaim(item => item.Type == ClaimTypes.PrimarySid)) if (User.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
{ {
userid = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.PrimarySid).Value); userid = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
} }
// check if cookie already exists // check if cookie already exists

View File

@ -10,20 +10,19 @@ namespace Oqtane.Security
{ {
public interface IJwtManager public interface IJwtManager
{ {
string GenerateToken(Alias alias, User user, string secret, string issuer, string audience, int lifetime); string GenerateToken(Alias alias, ClaimsIdentity user, string secret, string issuer, string audience, int lifetime);
User ValidateToken(string token, string secret, string issuer, string audience); User ValidateToken(string token, string secret, string issuer, string audience);
} }
public class JwtManager : IJwtManager public class JwtManager : IJwtManager
{ {
public string GenerateToken(Alias alias, User user, string secret, string issuer, string audience, int lifetime) public string GenerateToken(Alias alias, ClaimsIdentity user, string secret, string issuer, string audience, int lifetime)
{ {
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret); var key = Encoding.ASCII.GetBytes(secret);
var identity = UserSecurity.CreateClaimsIdentity(alias, user);
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor
{ {
Subject = new ClaimsIdentity(identity), Subject = new ClaimsIdentity(user),
Issuer = issuer, Issuer = issuer,
Audience = audience, Audience = audience,
Expires = DateTime.UtcNow.AddMinutes(lifetime), Expires = DateTime.UtcNow.AddMinutes(lifetime),
@ -56,7 +55,7 @@ namespace Oqtane.Security
var jwtToken = (JwtSecurityToken)validatedToken; var jwtToken = (JwtSecurityToken)validatedToken;
var user = new User var user = new User
{ {
UserId = int.Parse(jwtToken.Claims.FirstOrDefault(item => item.Type == "id")?.Value), UserId = int.Parse(jwtToken.Claims.FirstOrDefault(item => item.Type == "nameid")?.Value),
Username = jwtToken.Claims.FirstOrDefault(item => item.Type == "name")?.Value Username = jwtToken.Claims.FirstOrDefault(item => item.Type == "name")?.Value
}; };
return user; return user;

View File

@ -28,7 +28,7 @@ namespace Oqtane.Security
var claims = context.Principal.Claims; var claims = context.Principal.Claims;
// check if principal has roles and matches current site // check if principal has roles and matches current site
if (!claims.Any(item => item.Type == ClaimTypes.Role) || claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.SiteKey) if (!claims.Any(item => item.Type == ClaimTypes.Role) || claims.FirstOrDefault(item => item.Type == "sitekey")?.Value != alias.SiteKey)
{ {
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository; var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;

View File

@ -49,9 +49,9 @@ namespace Oqtane.Security
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
user.Username = principal.Identity.Name; user.Username = principal.Identity.Name;
if (principal.Claims.Any(item => item.Type == ClaimTypes.PrimarySid)) if (principal.Claims.Any(item => item.Type == ClaimTypes.NameIdentifier))
{ {
user.UserId = int.Parse(principal.Claims.First(item => item.Type == ClaimTypes.PrimarySid).Value); user.UserId = int.Parse(principal.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
} }
foreach (var claim in principal.Claims.Where(item => item.Type == ClaimTypes.Role)) foreach (var claim in principal.Claims.Where(item => item.Type == ClaimTypes.Role))
{ {

View File

@ -70,7 +70,7 @@ namespace Oqtane
}); });
// setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) // setup HttpClient for server side in a client side compatible fashion ( with auth cookie )
services.TryAddHttpClientWithAuthenticationCookie(); services.AddHttpClients();
// register scoped core services // register scoped core services
services.AddScoped<IAuthorizationHandler, PermissionHandler>() services.AddScoped<IAuthorizationHandler, PermissionHandler>()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -133,8 +133,8 @@ namespace Oqtane.Security
if (alias != null && user != null && !user.IsDeleted) if (alias != null && user != null && !user.IsDeleted)
{ {
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username)); identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.PrimarySid, user.UserId.ToString())); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
identity.AddClaim(new Claim(ClaimTypes.GroupSid, alias.SiteKey)); identity.AddClaim(new Claim("sitekey", alias.SiteKey));
if (user.Roles.Contains(RoleNames.Host)) if (user.Roles.Contains(RoleNames.Host))
{ {
// host users are site admins by default // host users are site admins by default
@ -160,12 +160,12 @@ namespace Oqtane.Security
{ {
identity.RemoveClaim(claim); identity.RemoveClaim(claim);
} }
claim = identity.Claims.FirstOrDefault(item => item.Type == ClaimTypes.PrimarySid); claim = identity.Claims.FirstOrDefault(item => item.Type == ClaimTypes.NameIdentifier);
if (claim != null) if (claim != null)
{ {
identity.RemoveClaim(claim); identity.RemoveClaim(claim);
} }
claim = identity.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid); claim = identity.Claims.FirstOrDefault(item => item.Type == "sitekey");
if (claim != null) if (claim != null)
{ {
identity.RemoveClaim(claim); identity.RemoveClaim(claim);

View File

@ -7,6 +7,7 @@ namespace Oqtane.Shared
{ {
public Alias Alias { get; set; } public Alias Alias { get; set; }
public string AntiForgeryToken { get; set; } // passed from server for use in service calls on client public string AntiForgeryToken { get; set; } // passed from server for use in service calls on client
public string AuthorizationToken { get; set; } // passed from server for use in service calls on client
public string RemoteIPAddress { get; set; } // passed from server as cannot be reliable retrieved on client public string RemoteIPAddress { get; set; } // passed from server as cannot be reliable retrieved on client
} }
} }