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

add Jwt authorization support for for API #2087

Merged
merged 1 commit into from
Mar 29, 2022
Merged
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
49 changes: 49 additions & 0 deletions Oqtane.Client/Modules/Admin/Users/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ else
</div>
</div>
</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>
<div class="col-sm-9">
<select id="cookietype" class="form-select" @bind="@_cookietype">
<option value="domain">@Localizer["Domain"]</option>
<option value="site">@Localizer["Site"]</option>
</select>
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
Expand Down Expand Up @@ -255,6 +266,23 @@ else
</div>
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="secret" HelpText="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." ResourceKey="Secret">Site Secret:</Label>
<div class="col-sm-9">
<input id="secret" class="form-control" @bind="@_secret" />
</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. The token will be valid for 1 year. 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>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
</div>
</div>
</div>
</Section>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
Expand All @@ -277,6 +305,8 @@ else
private string _maximumfailures;
private string _lockoutduration;

private string _cookietype;

private string _providertype;
private string _providername;
private string _authority;
Expand All @@ -294,6 +324,9 @@ else
private string _createusers;
private string _allowsitelogin;

private string _secret;
private string _token;

public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;

protected override async Task OnInitializedAsync()
Expand All @@ -311,9 +344,12 @@ else
_requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true");
_requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true");
_requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true");

_maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
_lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();

_cookietype = SettingService.GetSetting(settings, "CookieOptions:CookieType", "domain");

_providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", "");
_providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", "");
_authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
Expand All @@ -330,6 +366,8 @@ else
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_allowsitelogin = SettingService.GetSetting(settings, "ExternalLogin:AllowSiteLogin", "true");

_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
}

private List<UserRole> Search(string search)
Expand Down Expand Up @@ -406,9 +444,12 @@ else
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true);

settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true);

settings = SettingService.SetSetting(settings, "CookieOptions:CookieType", _cookietype, true);

settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true);
Expand All @@ -425,6 +466,9 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AllowSiteLogin", _allowsitelogin, false);

if (!string.IsNullOrEmpty(_secret) && _secret.Length < 16) _secret = (_secret + "????????????????").Substring(0, 16);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);

await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync(site.SiteId);

Expand All @@ -451,4 +495,9 @@ else
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged();
}

private async Task CreateToken()
{
_token = await UserService.GetTokenAsync();
}
}
5 changes: 5 additions & 0 deletions Oqtane.Client/Services/Interfaces/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,10 @@ public interface IUserService
/// <returns></returns>
Task<bool> ValidatePasswordAsync(string password);

/// <summary>
/// Get token for current user
/// </summary>
/// <returns></returns>
Task<string> GetTokenAsync();
}
}
5 changes: 5 additions & 0 deletions Oqtane.Client/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,10 @@ public async Task<bool> ValidatePasswordAsync(string password)
{
return await GetJsonAsync<bool>($"{Apiurl}/validate/{WebUtility.UrlEncode(password)}");
}

public async Task<string> GetTokenAsync()
{
return await GetStringAsync($"{Apiurl}/token");
}
}
}
3 changes: 3 additions & 0 deletions Oqtane.Server/Controllers/SettingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace Oqtane.Controllers
{
Expand Down Expand Up @@ -142,6 +143,8 @@ public void Delete(string entityName, int id)
[Authorize(Roles = RoleNames.Admin)]
public void Clear(int id)
{
var cookieAuthenticationOptionsCache = new SiteOptionsCache<CookieAuthenticationOptions>(_aliasAccessor);
cookieAuthenticationOptionsCache.Clear();
var openIdConnectOptionsCache = new SiteOptionsCache<OpenIdConnectOptions>(_aliasAccessor);
openIdConnectOptionsCache.Clear();
var oAuthOptionsCache = new SiteOptionsCache<OAuthOptions>(_aliasAccessor);
Expand Down
24 changes: 23 additions & 1 deletion Oqtane.Server/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Extensions;

namespace Oqtane.Controllers
{
Expand All @@ -30,9 +32,10 @@ public class UserController : Controller
private readonly IFolderRepository _folders;
private readonly ISyncManager _syncManager;
private readonly ISiteRepository _sites;
private readonly IJwtManager _jwtManager;
private readonly ILogManager _logger;

public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, ILogManager logger)
public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, IJwtManager jwtManager, ILogManager logger)
{
_users = users;
_roles = roles;
Expand All @@ -44,6 +47,7 @@ public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRep
_notifications = notifications;
_syncManager = syncManager;
_sites = sites;
_jwtManager = jwtManager;
_logger = logger;
}

Expand Down Expand Up @@ -516,6 +520,24 @@ public async Task<bool> Validate(string password)
return result.Succeeded;
}

// GET api/<controller>/token
[HttpGet("token")]
[Authorize(Roles = RoleNames.Admin)]
public string Token()
{
var token = "";
var user = _users.GetUser(User.Identity.Name);
if (user != null)
{
var secret = HttpContext.GetSiteSettings().GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
token = _jwtManager.GenerateToken(user, secret);
}
}
return token;
}

// GET api/<controller>/authenticate
[HttpGet("authenticate")]
public User Authenticate()
Expand Down
3 changes: 3 additions & 0 deletions Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ public static IApplicationBuilder UseOqtaneLocalization(this IApplicationBuilder

public static IApplicationBuilder UseTenantResolution(this IApplicationBuilder builder)
=> builder.UseMiddleware<TenantMiddleware>();

public static IApplicationBuilder UseJwtAuthorization(this IApplicationBuilder builder)
=> builder.UseMiddleware<JwtMiddleware>();
}
}
9 changes: 6 additions & 3 deletions Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ internal static IServiceCollection AddOqtaneTransientServices(this IServiceColle
{
services.AddTransient<ITenantManager, TenantManager>();
services.AddTransient<IAliasAccessor, AliasAccessor>();
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IUserPermissions, UserPermissions>();
services.AddTransient<ITenantResolver, TenantResolver>();
services.AddTransient<IJwtManager, JwtManager>();

services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IAliasRepository, AliasRepository>();
services.AddTransient<ITenantRepository, TenantRepository>();
services.AddTransient<ISiteRepository, SiteRepository>();
Expand All @@ -115,6 +117,7 @@ internal static IServiceCollection AddOqtaneTransientServices(this IServiceColle
services.AddTransient<ILanguageRepository, LanguageRepository>();
services.AddTransient<IVisitorRepository, VisitorRepository>();
services.AddTransient<IUrlMappingRepository, UrlMappingRepository>();

// obsolete - replaced by ITenantManager
services.AddTransient<ITenantResolver, TenantResolver>();

Expand Down Expand Up @@ -181,7 +184,7 @@ public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCol
options.SignIn.RequireConfirmedPhoneNumber = false;

// User settings
options.User.RequireUniqueEmail = false;
options.User.RequireUniqueEmail = true;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,43 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Oqtane.Repository;
using System.Collections.Generic;
using Oqtane.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OAuth;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace Oqtane.Extensions
{
public static class OqtaneSiteAuthenticationBuilderExtensions
{
public static OqtaneSiteOptionsBuilder WithSiteAuthentication(this OqtaneSiteOptionsBuilder builder)
{
// site OpenIdConnect options
// site cookie authentication options
builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("CookieOptions:CookieType", "domain") == "domain")
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
}
else
{
// use unique cookie name for site
options.Cookie.Name = ".AspNetCore.Identity.Application" + alias.SiteKey;
}
});

// site OpenId Connect options
builder.AddSiteOptions<OpenIdConnectOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OpenIDConnect)
{
// default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow
Expand Down Expand Up @@ -62,15 +76,15 @@ public static OqtaneSiteOptionsBuilder WithSiteAuthentication(this OqtaneSiteOpt
}
});

// site OAuth2.0 options
// site OAuth 2.0 options
builder.AddSiteOptions<OAuthOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OAuth2)
{
// default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OAuth2 : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OAuth2;
options.SaveTokens = true;
options.SaveTokens = false;

// site options
options.AuthorizationEndpoint = sitesettings.GetValue("ExternalLogin:AuthorizationUrl", "");
Expand Down Expand Up @@ -264,11 +278,9 @@ private static async Task LoginUser(string email, HttpContext httpContext, Claim
// add claims to principal
if (user != null)
{
// add Oqtane claims
var principal = (ClaimsIdentity)claimsPrincipal.Identity;
UserSecurity.ResetClaimsIdentity(principal);
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
var identity = UserSecurity.CreateClaimsIdentity(httpContext.GetAlias(), user, userroles);
var identity = UserSecurity.CreateClaimsIdentity(httpContext.GetAlias(), user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
principal.AddClaims(identity.Claims);

// update user
Expand All @@ -277,7 +289,7 @@ private static async Task LoginUser(string email, HttpContext httpContext, Claim
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerType);
}
else // user not logged in
else // user not valid
{
await httpContext.SignOutAsync();
}
Expand Down
57 changes: 57 additions & 0 deletions Oqtane.Server/Infrastructure/Middleware/JwtMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Extensions;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Shared;

namespace Oqtane.Infrastructure
{
internal class JwtMiddleware
{
private readonly RequestDelegate _next;

public JwtMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext context)
{
if (context.Request.Headers.ContainsKey("Authorization"))
{
var alias = context.GetAlias();
if (alias != null)
{
var secret = context.GetSiteSettings().GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
var logger = context.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
var jwtManager = context.RequestServices.GetService(typeof(IJwtManager)) as IJwtManager;

var token = context.Request.Headers["Authorization"].First().Split(" ").Last();
var user = jwtManager.ValidateToken(token, secret);
if (user != null)
{
// populate principal
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var principal = (ClaimsIdentity)context.User.Identity;
UserSecurity.ResetClaimsIdentity(principal);
var identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
principal.AddClaims(identity.Claims);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
}
else
{
logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validation Error");
}
}
}
}

await _next(context);
}
}
}
Loading