diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Auth/JwtAuthMessageHandler.cs b/DNN Platform/Dnn.AuthServices.Jwt/Auth/JwtAuthMessageHandler.cs index 9cb16b9dc78..f729218712f 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Auth/JwtAuthMessageHandler.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Auth/JwtAuthMessageHandler.cs @@ -2,74 +2,85 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Auth -{ - using System; - using System.Net.Http; - using System.Security.Principal; - using System.Threading; +namespace Dnn.AuthServices.Jwt.Auth +{ + using System; + using System.Net.Http; + using System.Security.Principal; + using System.Threading; - using Dnn.AuthServices.Jwt.Components.Common.Controllers; - using DotNetNuke.Instrumentation; - using DotNetNuke.Web.Api.Auth; - using DotNetNuke.Web.ConfigSection; + using Dnn.AuthServices.Jwt.Components.Common.Controllers; + using DotNetNuke.Instrumentation; + using DotNetNuke.Web.Api.Auth; + using DotNetNuke.Web.ConfigSection; - /// - /// This class implements Json Web Token (JWT) authentication scheme. - /// For detailed description of JWT refer to: - /// - JTW standard https://tools.ietf.org/html/rfc7519. - /// - Introduction to JSON Web Tokens http://jwt.io/introduction/. - /// - public class JwtAuthMessageHandler : AuthMessageHandlerBase - { - private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(JwtAuthMessageHandler)); - - private readonly IJwtController _jwtController = JwtController.Instance; - - public JwtAuthMessageHandler(bool includeByDefault, bool forceSsl) - : base(includeByDefault, forceSsl) - { - // Once an instance is enabled and gets registered in - // ServicesRoutingManager.RegisterAuthenticationHandlers() - // this scheme gets marked as enabled. - IsEnabled = true; - } - - public override string AuthScheme => this._jwtController.SchemeType; - - public override bool BypassAntiForgeryToken => true; - - internal static bool IsEnabled { get; set; } - - public override HttpResponseMessage OnInboundRequest(HttpRequestMessage request, CancellationToken cancellationToken) - { - if (this.NeedsAuthentication(request)) - { - this.TryToAuthenticate(request); - } - - return base.OnInboundRequest(request, cancellationToken); - } - - private void TryToAuthenticate(HttpRequestMessage request) - { - try - { - var username = this._jwtController.ValidateToken(request); - if (!string.IsNullOrEmpty(username)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace($"Authenticated user '{username}'"); - } - - SetCurrentPrincipal(new GenericPrincipal(new GenericIdentity(username, this.AuthScheme), null), request); - } - } - catch (Exception ex) - { - Logger.Error("Unexpected error in authenticating the user. " + ex); - } - } - } -} + /// + /// This class implements Json Web Token (JWT) authentication scheme. + /// For detailed description of JWT refer to: + /// - JTW standard https://tools.ietf.org/html/rfc7519. + /// - Introduction to JSON Web Tokens http://jwt.io/introduction/. + /// + public class JwtAuthMessageHandler : AuthMessageHandlerBase + { + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(JwtAuthMessageHandler)); + + private readonly IJwtController jwtController = JwtController.Instance; + + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether this handler should be inlcuded by default on all API endpoints. + /// A value indicating whether this handler should enforce SSL usage. + public JwtAuthMessageHandler(bool includeByDefault, bool forceSsl) + : base(includeByDefault, forceSsl) + { + // Once an instance is enabled and gets registered in + // ServicesRoutingManager.RegisterAuthenticationHandlers() + // this scheme gets marked as enabled. + IsEnabled = true; + } + + /// + public override string AuthScheme => this.jwtController.SchemeType; + + /// + public override bool BypassAntiForgeryToken => true; + + /// + /// Gets or sets a value indicating whether this handler is enabled. + /// + internal static bool IsEnabled { get; set; } + + /// + public override HttpResponseMessage OnInboundRequest(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (this.NeedsAuthentication(request)) + { + this.TryToAuthenticate(request); + } + + return base.OnInboundRequest(request, cancellationToken); + } + + private void TryToAuthenticate(HttpRequestMessage request) + { + try + { + var username = this.jwtController.ValidateToken(request); + if (!string.IsNullOrEmpty(username)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace($"Authenticated user '{username}'"); + } + + SetCurrentPrincipal(new GenericPrincipal(new GenericIdentity(username, this.AuthScheme), null), request); + } + } + catch (Exception ex) + { + Logger.Error("Unexpected error in authenticating the user. " + ex); + } + } + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/IJwtController.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/IJwtController.cs index cb7dcefed46..9c99f797b55 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/IJwtController.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/IJwtController.cs @@ -2,22 +2,50 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Components.Common.Controllers -{ - using System.Net.Http; +namespace Dnn.AuthServices.Jwt.Components.Common.Controllers +{ + using System.Net.Http; - using Dnn.AuthServices.Jwt.Components.Entity; + using Dnn.AuthServices.Jwt.Components.Entity; - public interface IJwtController - { - string SchemeType { get; } - - string ValidateToken(HttpRequestMessage request); - - bool LogoutUser(HttpRequestMessage request); - - LoginResultData LoginUser(HttpRequestMessage request, LoginData loginData); - - LoginResultData RenewToken(HttpRequestMessage request, string renewalToken); - } -} + /// + /// Controls JWT features. + /// + public interface IJwtController + { + /// + /// Gets the name of the authentication Scheme Type. + /// + string SchemeType { get; } + + /// + /// Validates the JWT token for the request. + /// + /// The current HTTP request. + /// Returns the UserName if the token is valid or null if not. + string ValidateToken(HttpRequestMessage request); + + /// + /// Logs the user out. + /// + /// The current HTTP request. + /// A value indicating whether the logout attempt succeeded. + bool LogoutUser(HttpRequestMessage request); + + /// + /// Logs the user in. + /// + /// The current HTTP request. + /// The login information, . + /// . + LoginResultData LoginUser(HttpRequestMessage request, LoginData loginData); + + /// + /// Attempts to renew a JWT token. + /// + /// The current HTTP request. + /// The JWT renewal token. + /// . + LoginResultData RenewToken(HttpRequestMessage request, string renewalToken); + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/JwtController.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/JwtController.cs index 52f03ba7ed4..30d81f039fe 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/JwtController.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Common/Controllers/JwtController.cs @@ -2,558 +2,602 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Components.Common.Controllers -{ - using System; - using System.Collections.Generic; - using System.IdentityModel.Tokens; - using System.Linq; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Security.Claims; - using System.Security.Cryptography; - using System.Text; - - using Dnn.AuthServices.Jwt.Auth; - using Dnn.AuthServices.Jwt.Components.Entity; - using Dnn.AuthServices.Jwt.Data; - using DotNetNuke.Entities.Portals; - using DotNetNuke.Entities.Users; - using DotNetNuke.Framework; - using DotNetNuke.Instrumentation; - using DotNetNuke.Security.Membership; - using DotNetNuke.Web.Api; - using Newtonsoft.Json; - - internal class JwtController : ServiceLocator, IJwtController - { - public const string AuthScheme = "Bearer"; - public readonly IDataService DataProvider = DataService.Instance; - - private const int ClockSkew = 5; // in minutes; default for clock skew - private const int SessionTokenTtl = 60; // in minutes = 1 hour - - private const int RenewalTokenTtl = 14; // in days = 2 weeks - private const string SessionClaimType = "sid"; - - private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(JwtController)); - private static readonly HashAlgorithm Hasher = SHA384.Create(); - private static readonly Encoding TextEncoder = Encoding.UTF8; - - public string SchemeType => "JWT"; - - private static string NewSessionId => DateTime.UtcNow.Ticks.ToString("x16") + Guid.NewGuid().ToString("N").Substring(16); - - /// - /// Validates the received JWT against the databas eand returns username when successful. - /// - /// - public string ValidateToken(HttpRequestMessage request) - { - if (!JwtAuthMessageHandler.IsEnabled) - { - Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); - return null; - } - - var authorization = this.ValidateAuthHeader(request?.Headers.Authorization); - return string.IsNullOrEmpty(authorization) ? null : this.ValidateAuthorizationValue(authorization); - } - - public bool LogoutUser(HttpRequestMessage request) - { - if (!JwtAuthMessageHandler.IsEnabled) - { - Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); - return false; - } - - var rawToken = this.ValidateAuthHeader(request?.Headers.Authorization); - if (string.IsNullOrEmpty(rawToken)) - { - return false; - } - - var jwt = new JwtSecurityToken(rawToken); - var sessionId = GetJwtSessionValue(jwt); - if (string.IsNullOrEmpty(sessionId)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Session ID not found in the claim"); - } - - return false; - } - - this.DataProvider.DeleteToken(sessionId); - return true; - } - - /// - /// Validates user login credentials and returns result when successful. - /// - /// - public LoginResultData LoginUser(HttpRequestMessage request, LoginData loginData) - { - if (!JwtAuthMessageHandler.IsEnabled) - { - Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); - return EmptyWithError("disabled"); - } - - var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); - if (portalSettings == null) - { - Logger.Trace("portalSettings = null"); - return EmptyWithError("no-portal"); - } - - var status = UserLoginStatus.LOGIN_FAILURE; - var ipAddress = request.GetIPAddress() ?? string.Empty; - var userInfo = UserController.ValidateUser( - portalSettings.PortalId, - loginData.Username, loginData.Password, "DNN", string.Empty, AuthScheme, ipAddress, ref status); - - if (userInfo == null) - { - Logger.Trace("user = null"); - return EmptyWithError("bad-credentials"); - } - - var valid = - status == UserLoginStatus.LOGIN_SUCCESS || - status == UserLoginStatus.LOGIN_SUPERUSER || - status == UserLoginStatus.LOGIN_INSECUREADMINPASSWORD || - status == UserLoginStatus.LOGIN_INSECUREHOSTPASSWORD; - - if (!valid) - { - Logger.Trace("login status = " + status); - return EmptyWithError("bad-credentials"); - } - - // save hash values in DB so no one with access can create JWT header from existing data - var sessionId = NewSessionId; - var now = DateTime.UtcNow; - var renewalToken = EncodeBase64(Hasher.ComputeHash(Guid.NewGuid().ToByteArray())); - var ptoken = new PersistedToken - { - TokenId = sessionId, - UserId = userInfo.UserID, - TokenExpiry = now.AddMinutes(SessionTokenTtl), - RenewalExpiry = now.AddDays(RenewalTokenTtl), - RenewalHash = GetHashedStr(renewalToken), - }; - - var secret = ObtainSecret(sessionId, portalSettings.GUID, userInfo.Membership.LastPasswordChangeDate); - var jwt = CreateJwtToken(secret, portalSettings.PortalAlias.HTTPAlias, ptoken, userInfo.Roles); - var accessToken = jwt.RawData; - - ptoken.TokenHash = GetHashedStr(accessToken); - this.DataProvider.AddToken(ptoken); - - return new LoginResultData - { - UserId = userInfo.UserID, - DisplayName = userInfo.DisplayName, - AccessToken = accessToken, - RenewalToken = renewalToken, - }; - } - - public LoginResultData RenewToken(HttpRequestMessage request, string renewalToken) - { - if (!JwtAuthMessageHandler.IsEnabled) - { - Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); - return EmptyWithError("disabled"); - } - - var rawToken = this.ValidateAuthHeader(request?.Headers.Authorization); - if (string.IsNullOrEmpty(rawToken)) - { - return EmptyWithError("bad-credentials"); - } - - var jwt = GetAndValidateJwt(rawToken, false); - if (jwt == null) - { - return EmptyWithError("bad-jwt"); - } - - var sessionId = GetJwtSessionValue(jwt); - if (string.IsNullOrEmpty(sessionId)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Session ID not found in the claim"); - } - - return EmptyWithError("bad-claims"); - } - - var ptoken = this.DataProvider.GetTokenById(sessionId); - if (ptoken == null) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Token not found in DB"); - } - - return EmptyWithError("not-found"); - } - - if (ptoken.RenewalExpiry <= DateTime.UtcNow) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Token can't bwe renewed anymore"); - } - - return EmptyWithError("not-more-renewal"); - } - - if (ptoken.RenewalHash != GetHashedStr(renewalToken)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Invalid renewal token"); - } - - return EmptyWithError("bad-token"); - } - - if (ptoken.TokenHash != GetHashedStr(rawToken)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Invalid access token"); - } - - return EmptyWithError("bad-token"); - } - - var userInfo = this.TryGetUser(jwt, false); - if (userInfo == null) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("User not found in DB"); - } - - return EmptyWithError("not-found"); - } - - if (ptoken.UserId != userInfo.UserID) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Mismatch token and user"); - } - - return EmptyWithError("bad-token"); - } - - return this.UpdateToken(renewalToken, ptoken, userInfo); - } - - protected override Func GetFactory() - { - return () => new JwtController(); - } - - private static LoginResultData EmptyWithError(string error) - { - return new LoginResultData { Error = error }; - } - - private static JwtSecurityToken CreateJwtToken(byte[] symmetricKey, string issuer, PersistedToken ptoken, IEnumerable roles) - { - // var key = Convert.FromBase64String(symmetricKey); - var credentials = new SigningCredentials( - new InMemorySymmetricSecurityKey(symmetricKey), - "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", - "http://www.w3.org/2001/04/xmlenc#sha256"); - - var claimsIdentity = new ClaimsIdentity(); - claimsIdentity.AddClaim(new Claim(SessionClaimType, ptoken.TokenId)); - claimsIdentity.AddClaims(roles.Select(r => new Claim(ClaimTypes.Role, r))); - - var notBefore = DateTime.UtcNow.AddMinutes(-ClockSkew); - var notAfter = ptoken.TokenExpiry; - var tokenHandler = new JwtSecurityTokenHandler(); - var token = tokenHandler.CreateToken(issuer, null, claimsIdentity, notBefore, notAfter, credentials); - return token; - } - - private static JwtSecurityToken GetAndValidateJwt(string rawToken, bool checkExpiry) - { - JwtSecurityToken jwt; - try - { - jwt = new JwtSecurityToken(rawToken); - } - catch (Exception ex) - { - Logger.Error("Unable to construct JWT object from authorization value. " + ex.Message); - return null; - } - - if (checkExpiry) - { - var now = DateTime.UtcNow; - if (now < jwt.ValidFrom || now > jwt.ValidTo) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Token is expired"); - } - - return null; - } - } - - var sessionId = GetJwtSessionValue(jwt); - if (string.IsNullOrEmpty(sessionId)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Invaid session ID claim"); - } - - return null; - } - - return jwt; - } - - private static string GetJwtSessionValue(JwtSecurityToken jwt) - { - var sessionClaim = jwt?.Claims?.FirstOrDefault(claim => SessionClaimType.Equals(claim.Type)); - return sessionClaim?.Value; - } - - private static byte[] ObtainSecret(string sessionId, Guid portalGuid, DateTime userCreationDate) - { - // The secret should contain unpredictable components that can't be inferred from the JWT string. - var stext = string.Join(".", sessionId, portalGuid.ToString("N"), userCreationDate.ToUniversalTime().ToString("O")); - return TextEncoder.GetBytes(stext); - } - - private static string DecodeBase64(string b64Str) - { - // fix Base64 string padding - var mod = b64Str.Length % 4; - if (mod != 0) - { - b64Str += new string('=', 4 - mod); - } - - return TextEncoder.GetString(Convert.FromBase64String(b64Str)); - } - - private static string EncodeBase64(byte[] data) - { - return Convert.ToBase64String(data).TrimEnd('='); - } - - private static string GetHashedStr(string data) - { - return EncodeBase64(Hasher.ComputeHash(TextEncoder.GetBytes(data))); - } - - private LoginResultData UpdateToken(string renewalToken, PersistedToken ptoken, UserInfo userInfo) - { - var expiry = DateTime.UtcNow.AddMinutes(SessionTokenTtl); - if (expiry > ptoken.RenewalExpiry) - { - // don't extend beyond renewal expiry and make sure it is marked in UTC - expiry = new DateTime(ptoken.RenewalExpiry.Ticks, DateTimeKind.Utc); - } - - ptoken.TokenExpiry = expiry; - - var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); - var secret = ObtainSecret(ptoken.TokenId, portalSettings.GUID, userInfo.Membership.LastPasswordChangeDate); - var jwt = CreateJwtToken(secret, portalSettings.PortalAlias.HTTPAlias, ptoken, userInfo.Roles); - var accessToken = jwt.RawData; - - // save hash values in DB so no one with access can create JWT header from existing data - ptoken.TokenHash = GetHashedStr(accessToken); - this.DataProvider.UpdateToken(ptoken); - - return new LoginResultData - { - UserId = userInfo.UserID, - DisplayName = userInfo.DisplayName, - AccessToken = accessToken, - RenewalToken = renewalToken, - }; - } - - /// - /// Checks for Authorization header and validates it is JWT scheme. If successful, it returns the token string. - /// - /// The request auhorization header. - /// The JWT passed in the request; otherwise, it returns null. - private string ValidateAuthHeader(AuthenticationHeaderValue authHdr) - { - if (authHdr == null) - { - // if (Logger.IsTraceEnabled) Logger.Trace("Authorization header not present in the request"); // too verbose; shows in all web requests - return null; - } - - if (!string.Equals(authHdr.Scheme, AuthScheme, StringComparison.CurrentCultureIgnoreCase)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Authorization header scheme in the request is not equal to " + this.SchemeType); - } - - return null; - } - - var authorization = authHdr.Parameter; - if (string.IsNullOrEmpty(authorization)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Missing authorization header value in the request"); - } - - return null; - } - - return authorization; - } - - private string ValidateAuthorizationValue(string authorization) - { - var parts = authorization.Split('.'); - if (parts.Length < 3) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Token must have [header:claims:signature] parts at least"); - } - - return null; - } - - var decoded = DecodeBase64(parts[0]); - if (decoded.IndexOf("\"" + this.SchemeType + "\"", StringComparison.InvariantCultureIgnoreCase) < 0) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace($"This is not a {this.SchemeType} autentication scheme."); - } - - return null; - } - - var header = JsonConvert.DeserializeObject(decoded); - if (!this.IsValidSchemeType(header)) - { - return null; - } - - var jwt = GetAndValidateJwt(authorization, true); - if (jwt == null) - { - return null; - } - - var userInfo = this.TryGetUser(jwt, true); - return userInfo?.Username; - } - - private bool IsValidSchemeType(JwtHeader header) - { - if (!this.SchemeType.Equals(header["typ"] as string, StringComparison.OrdinalIgnoreCase)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Unsupported authentication scheme type " + header.Typ); - } - - return false; - } - - return true; - } - - private UserInfo TryGetUser(JwtSecurityToken jwt, bool checkExpiry) - { - // validate against DB saved data - var sessionId = GetJwtSessionValue(jwt); - var ptoken = this.DataProvider.GetTokenById(sessionId); - if (ptoken == null) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Token not found in DB"); - } - - return null; - } - - if (checkExpiry) - { - var now = DateTime.UtcNow; - if (now > ptoken.TokenExpiry || now > ptoken.RenewalExpiry) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("DB Token is expired"); - } - - return null; - } - } - - if (ptoken.TokenHash != GetHashedStr(jwt.RawData)) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Mismatch data in received token"); - } - - return null; - } - - var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); - if (portalSettings == null) - { - Logger.Trace("Unable to retrieve portal settings"); - return null; - } - - var userInfo = UserController.GetUserById(portalSettings.PortalId, ptoken.UserId); - if (userInfo == null) - { - if (Logger.IsTraceEnabled) - { - Logger.Trace("Invalid user"); - } - - return null; - } - - var status = UserController.ValidateUser(userInfo, portalSettings.PortalId, false); - var valid = - status == UserValidStatus.VALID || - status == UserValidStatus.UPDATEPROFILE || - status == UserValidStatus.UPDATEPASSWORD; - - if (!valid && Logger.IsTraceEnabled) - { - Logger.Trace("Inactive user status: " + status); - return null; - } - - return userInfo; - } - } -} +namespace Dnn.AuthServices.Jwt.Components.Common.Controllers +{ + using System; + using System.Collections.Generic; + using System.IdentityModel.Tokens; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Security.Claims; + using System.Security.Cryptography; + using System.Text; + + using Dnn.AuthServices.Jwt.Auth; + using Dnn.AuthServices.Jwt.Components.Entity; + using Dnn.AuthServices.Jwt.Data; + using DotNetNuke.Abstractions.Portals; + using DotNetNuke.Common; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework; + using DotNetNuke.Instrumentation; + using DotNetNuke.Security.Membership; + using DotNetNuke.Web.Api; + using Newtonsoft.Json; + + /// + /// Controls JWT features. + /// + internal class JwtController : ServiceLocator, IJwtController + { + /// + /// The name of the authentication scheme header. + /// + public const string AuthScheme = "Bearer"; + + /// + /// A reference to the Dnn data provider. + /// + public readonly IDataService DataProvider = DataService.Instance; + + private const int ClockSkew = 5; // in minutes; default for clock skew + private const int SessionTokenTtl = 60; // in minutes = 1 hour + + private const int RenewalTokenTtl = 14; // in days = 2 weeks + private const string SessionClaimType = "sid"; + + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(JwtController)); + private static readonly HashAlgorithm Hasher = SHA384.Create(); + private static readonly Encoding TextEncoder = Encoding.UTF8; + + /// + public string SchemeType => "JWT"; + + private static string NewSessionId => DateTime.UtcNow.Ticks.ToString("x16") + Guid.NewGuid().ToString("N").Substring(16); + + /// + public string ValidateToken(HttpRequestMessage request) + { + if (!JwtAuthMessageHandler.IsEnabled) + { + Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); + return null; + } + + var authorization = this.ValidateAuthHeader(request?.Headers.Authorization); + return string.IsNullOrEmpty(authorization) ? null : this.ValidateAuthorizationValue(authorization); + } + + /// + public bool LogoutUser(HttpRequestMessage request) + { + if (!JwtAuthMessageHandler.IsEnabled) + { + Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); + return false; + } + + var rawToken = this.ValidateAuthHeader(request?.Headers.Authorization); + if (string.IsNullOrEmpty(rawToken)) + { + return false; + } + + var jwt = new JwtSecurityToken(rawToken); + var sessionId = GetJwtSessionValue(jwt); + if (string.IsNullOrEmpty(sessionId)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Session ID not found in the claim"); + } + + return false; + } + + this.DataProvider.DeleteToken(sessionId); + return true; + } + + /// + public LoginResultData LoginUser(HttpRequestMessage request, LoginData loginData) + { + if (!JwtAuthMessageHandler.IsEnabled) + { + Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); + return EmptyWithError("disabled"); + } + + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + if (portalSettings == null) + { + Logger.Trace("portalSettings = null"); + return EmptyWithError("no-portal"); + } + + var status = UserLoginStatus.LOGIN_FAILURE; + var ipAddress = request.GetIPAddress() ?? string.Empty; + var userInfo = UserController.ValidateUser( + portalSettings.PortalId, + loginData.Username, + loginData.Password, + "DNN", + string.Empty, + AuthScheme, + ipAddress, + ref status); + + if (userInfo == null) + { + Logger.Trace("user = null"); + return EmptyWithError("bad-credentials"); + } + + var valid = + status == UserLoginStatus.LOGIN_SUCCESS || + status == UserLoginStatus.LOGIN_SUPERUSER || + status == UserLoginStatus.LOGIN_INSECUREADMINPASSWORD || + status == UserLoginStatus.LOGIN_INSECUREHOSTPASSWORD; + + if (!valid) + { + Logger.Trace("login status = " + status); + return EmptyWithError("bad-credentials"); + } + + // save hash values in DB so no one with access can create JWT header from existing data + var sessionId = NewSessionId; + var now = DateTime.UtcNow; + var renewalToken = EncodeBase64(Hasher.ComputeHash(Guid.NewGuid().ToByteArray())); + var ptoken = new PersistedToken + { + TokenId = sessionId, + UserId = userInfo.UserID, + TokenExpiry = now.AddMinutes(SessionTokenTtl), + RenewalExpiry = now.AddDays(RenewalTokenTtl), + RenewalHash = GetHashedStr(renewalToken), + }; + + var secret = ObtainSecret(sessionId, portalSettings.GUID, userInfo.Membership.LastPasswordChangeDate); + var jwt = CreateJwtToken( + secret, + portalSettings.PortalAlias.HTTPAlias, + ptoken, + userInfo.Roles); + var accessToken = jwt.RawData; + + ptoken.TokenHash = GetHashedStr(accessToken); + this.DataProvider.AddToken(ptoken); + + return new LoginResultData + { + UserId = userInfo.UserID, + DisplayName = userInfo.DisplayName, + AccessToken = accessToken, + RenewalToken = renewalToken, + }; + } + + /// + public LoginResultData RenewToken(HttpRequestMessage request, string renewalToken) + { + if (!JwtAuthMessageHandler.IsEnabled) + { + Logger.Trace(this.SchemeType + " is not registered/enabled in web.config file"); + return EmptyWithError("disabled"); + } + + var rawToken = this.ValidateAuthHeader(request?.Headers.Authorization); + if (string.IsNullOrEmpty(rawToken)) + { + return EmptyWithError("bad-credentials"); + } + + var jwt = GetAndValidateJwt(rawToken, false); + if (jwt == null) + { + return EmptyWithError("bad-jwt"); + } + + var sessionId = GetJwtSessionValue(jwt); + if (string.IsNullOrEmpty(sessionId)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Session ID not found in the claim"); + } + + return EmptyWithError("bad-claims"); + } + + var ptoken = this.DataProvider.GetTokenById(sessionId); + if (ptoken == null) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Token not found in DB"); + } + + return EmptyWithError("not-found"); + } + + if (ptoken.RenewalExpiry <= DateTime.UtcNow) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Token can't bwe renewed anymore"); + } + + return EmptyWithError("not-more-renewal"); + } + + if (ptoken.RenewalHash != GetHashedStr(renewalToken)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Invalid renewal token"); + } + + return EmptyWithError("bad-token"); + } + + if (ptoken.TokenHash != GetHashedStr(rawToken)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Invalid access token"); + } + + return EmptyWithError("bad-token"); + } + + var userInfo = this.TryGetUser(jwt, false); + if (userInfo == null) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("User not found in DB"); + } + + return EmptyWithError("not-found"); + } + + if (ptoken.UserId != userInfo.UserID) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Mismatch token and user"); + } + + return EmptyWithError("bad-token"); + } + + return this.UpdateToken(renewalToken, ptoken, userInfo); + } + + /// + protected override Func GetFactory() + { + return () => new JwtController(); + } + + private static LoginResultData EmptyWithError(string error) + { + return new LoginResultData { Error = error }; + } + + private static JwtSecurityToken CreateJwtToken(byte[] symmetricKey, string issuer, PersistedToken ptoken, IEnumerable roles) + { + // var key = Convert.FromBase64String(symmetricKey); + var credentials = new SigningCredentials( + new InMemorySymmetricSecurityKey(symmetricKey), + "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", + "http://www.w3.org/2001/04/xmlenc#sha256"); + + var claimsIdentity = new ClaimsIdentity(); + claimsIdentity.AddClaim(new Claim(SessionClaimType, ptoken.TokenId)); + claimsIdentity.AddClaims(roles.Select(r => new Claim(ClaimTypes.Role, r))); + + var notBefore = DateTime.UtcNow.AddMinutes(-ClockSkew); + var notAfter = ptoken.TokenExpiry; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(issuer, null, claimsIdentity, notBefore, notAfter, credentials); + return token; + } + + private static JwtSecurityToken GetAndValidateJwt(string rawToken, bool checkExpiry) + { + JwtSecurityToken jwt; + try + { + jwt = new JwtSecurityToken(rawToken); + } + catch (Exception ex) + { + Logger.Error("Unable to construct JWT object from authorization value. " + ex.Message); + return null; + } + + if (checkExpiry) + { + var now = DateTime.UtcNow; + if (now < jwt.ValidFrom || now > jwt.ValidTo) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Token is expired"); + } + + return null; + } + } + + var sessionId = GetJwtSessionValue(jwt); + if (string.IsNullOrEmpty(sessionId)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Invaid session ID claim"); + } + + return null; + } + + return jwt; + } + + private static string GetJwtSessionValue(JwtSecurityToken jwt) + { + var sessionClaim = jwt?.Claims?.FirstOrDefault(claim => SessionClaimType.Equals(claim.Type)); + return sessionClaim?.Value; + } + + private static byte[] ObtainSecret(string sessionId, Guid portalGuid, DateTime userCreationDate) + { + // The secret should contain unpredictable components that can't be inferred from the JWT string. + var stext = string.Join(".", sessionId, portalGuid.ToString("N"), userCreationDate.ToUniversalTime().ToString("O")); + return TextEncoder.GetBytes(stext); + } + + private static string DecodeBase64(string b64Str) + { + // fix Base64 string padding + var mod = b64Str.Length % 4; + if (mod != 0) + { + b64Str += new string('=', 4 - mod); + } + + return TextEncoder.GetString(Convert.FromBase64String(b64Str)); + } + + private static string EncodeBase64(byte[] data) + { + return Convert.ToBase64String(data).TrimEnd('='); + } + + private static string GetHashedStr(string data) + { + return EncodeBase64(Hasher.ComputeHash(TextEncoder.GetBytes(data))); + } + + private LoginResultData UpdateToken(string renewalToken, PersistedToken ptoken, UserInfo userInfo) + { + var expiry = DateTime.UtcNow.AddMinutes(SessionTokenTtl); + if (expiry > ptoken.RenewalExpiry) + { + // don't extend beyond renewal expiry and make sure it is marked in UTC + expiry = new DateTime(ptoken.RenewalExpiry.Ticks, DateTimeKind.Utc); + } + + ptoken.TokenExpiry = expiry; + + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + var secret = ObtainSecret(ptoken.TokenId, portalSettings.GUID, userInfo.Membership.LastPasswordChangeDate); + var jwt = CreateJwtToken(secret, portalSettings.PortalAlias.HTTPAlias, ptoken, userInfo.Roles); + var accessToken = jwt.RawData; + + // save hash values in DB so no one with access can create JWT header from existing data + ptoken.TokenHash = GetHashedStr(accessToken); + this.DataProvider.UpdateToken(ptoken); + + return new LoginResultData + { + UserId = userInfo.UserID, + DisplayName = userInfo.DisplayName, + AccessToken = accessToken, + RenewalToken = renewalToken, + }; + } + + /// + /// Checks for Authorization header and validates it is JWT scheme. If successful, it returns the token string. + /// + /// The request auhorization header. + /// The JWT passed in the request; otherwise, it returns null. + private string ValidateAuthHeader(AuthenticationHeaderValue authHdr) + { + if (authHdr == null) + { + // if (Logger.IsTraceEnabled) Logger.Trace("Authorization header not present in the request"); // too verbose; shows in all web requests + return null; + } + + if (!string.Equals(authHdr.Scheme, AuthScheme, StringComparison.CurrentCultureIgnoreCase)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Authorization header scheme in the request is not equal to " + this.SchemeType); + } + + return null; + } + + var authorization = authHdr.Parameter; + if (string.IsNullOrEmpty(authorization)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Missing authorization header value in the request"); + } + + return null; + } + + return authorization; + } + + private string ValidateAuthorizationValue(string authorization) + { + var parts = authorization.Split('.'); + if (parts.Length < 3) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Token must have [header:claims:signature] parts at least"); + } + + return null; + } + + var decoded = DecodeBase64(parts[0]); + if (decoded.IndexOf("\"" + this.SchemeType + "\"", StringComparison.InvariantCultureIgnoreCase) < 0) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace($"This is not a {this.SchemeType} autentication scheme."); + } + + return null; + } + + var header = JsonConvert.DeserializeObject(decoded); + if (!this.IsValidSchemeType(header)) + { + return null; + } + + var jwt = GetAndValidateJwt(authorization, true); + if (jwt == null) + { + return null; + } + + var userInfo = this.TryGetUser(jwt, true); + return userInfo?.Username; + } + + private bool IsValidSchemeType(JwtHeader header) + { + if (!this.SchemeType.Equals(header["typ"] as string, StringComparison.OrdinalIgnoreCase)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Unsupported authentication scheme type " + header.Typ); + } + + return false; + } + + return true; + } + + private UserInfo TryGetUser(JwtSecurityToken jwt, bool checkExpiry) + { + // validate against DB saved data + var sessionId = GetJwtSessionValue(jwt); + var ptoken = this.DataProvider.GetTokenById(sessionId); + if (ptoken == null) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Token not found in DB"); + } + + return null; + } + + if (checkExpiry) + { + var now = DateTime.UtcNow; + if (now > ptoken.TokenExpiry || now > ptoken.RenewalExpiry) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("DB Token is expired"); + } + + return null; + } + } + + if (ptoken.TokenHash != GetHashedStr(jwt.RawData)) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Mismatch data in received token"); + } + + return null; + } + + var portalSettings = PortalController.Instance.GetCurrentSettings(); + if (portalSettings == null) + { + Logger.Trace("Unable to retrieve portal settings"); + return null; + } + + var userInfo = UserController.GetUserById(portalSettings.PortalId, ptoken.UserId); + if (userInfo == null) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Invalid user"); + } + + return null; + } + + var status = UserController.ValidateUser(userInfo, portalSettings.PortalId, false); + var valid = + status == UserValidStatus.VALID || + status == UserValidStatus.UPDATEPROFILE || + status == UserValidStatus.UPDATEPASSWORD; + + if (!valid) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Inactive user status: " + status); + } + + return null; + } + + if (!userInfo.Membership.Approved) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Non Approved user id: " + userInfo.UserID + " UserName: " + userInfo.Username); + } + + return null; + } + + if (userInfo.IsDeleted) + { + if (Logger.IsTraceEnabled) + { + Logger.Trace("Deleted user id: " + userInfo.UserID + " UserName: " + userInfo.Username); + } + + return null; + } + + return userInfo; + } + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginData.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginData.cs index 61bb48f3d24..a40bb397b38 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginData.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginData.cs @@ -2,20 +2,26 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Components.Entity -{ - using Newtonsoft.Json; +namespace Dnn.AuthServices.Jwt.Components.Entity +{ + using Newtonsoft.Json; - /// - /// Structure used for the Login to obtain a Json Web Token (JWT). - /// - [JsonObject] - public struct LoginData - { - [JsonProperty("u")] - public string Username; - - [JsonProperty("p")] - public string Password; - } -} + /// + /// Structure used for the Login to obtain a Json Web Token (JWT). + /// + [JsonObject] + public struct LoginData + { + /// + /// The authentication username. + /// + [JsonProperty("u")] + public string Username; + + /// + /// The authentication password. + /// + [JsonProperty("p")] + public string Password; + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginResultData.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginResultData.cs index 4a5a8353431..6d097e95dd7 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginResultData.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/LoginResultData.cs @@ -2,26 +2,44 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Components.Entity -{ - using Newtonsoft.Json; +namespace Dnn.AuthServices.Jwt.Components.Entity +{ + using Newtonsoft.Json; - [JsonObject] - public class LoginResultData - { - [JsonProperty("userId")] - public int UserId { get; set; } - - [JsonProperty("displayName")] - public string DisplayName { get; set; } - - [JsonProperty("accessToken")] - public string AccessToken { get; set; } - - [JsonProperty("renewalToken")] - public string RenewalToken { get; set; } - - [JsonIgnore] - public string Error { get; set; } - } -} + /// + /// Represents information about a login result. + /// + [JsonObject] + public class LoginResultData + { + /// + /// Gets or sets the id of the user. + /// + [JsonProperty("userId")] + public int UserId { get; set; } + + /// + /// Gets or sets the user display name. + /// + [JsonProperty("displayName")] + public string DisplayName { get; set; } + + /// + /// Gets or sets the access token. + /// + [JsonProperty("accessToken")] + public string AccessToken { get; set; } + + /// + /// Gets or sets the renewal token. + /// + [JsonProperty("renewalToken")] + public string RenewalToken { get; set; } + + /// + /// Gets or sets any error message. + /// + [JsonIgnore] + public string Error { get; set; } + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/PersistedToken.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/PersistedToken.cs index a2f14d5eec7..73f025904eb 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/PersistedToken.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/PersistedToken.cs @@ -2,25 +2,49 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Components.Entity -{ - using System; - - [Serializable] - public class PersistedToken - { - public string TokenId { get; set; } - - public int UserId { get; set; } - - public int RenewCount { get; set; } - - public DateTime TokenExpiry { get; set; } - - public DateTime RenewalExpiry { get; set; } - - public string TokenHash { get; set; } - - public string RenewalHash { get; set; } - } -} +namespace Dnn.AuthServices.Jwt.Components.Entity +{ + using System; + + /// + /// Represents a persisted token. + /// + [Serializable] + public class PersistedToken + { + /// + /// Gets or sets the ID for the token. + /// + public string TokenId { get; set; } + + /// + /// Gets or sets the id of the user. + /// + public int UserId { get; set; } + + /// + /// Gets or sets the renewal count. + /// + public int RenewCount { get; set; } + + /// + /// Gets or sets a value indicating when the token expires. + /// + public DateTime TokenExpiry { get; set; } + + /// + /// Gets or sets when the renewal token expires. + /// + public DateTime RenewalExpiry { get; set; } + + /// + /// Gets or sets the token hash value. + /// + public string TokenHash { get; set; } + + /// + /// Gets or sets the renewal token hash value. + /// + public string RenewalHash { get; set; } + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/RenewalDto.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/RenewalDto.cs index 5c64b7a6a57..cecd322ccd5 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/RenewalDto.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Entity/RenewalDto.cs @@ -2,14 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Components.Entity -{ - using Newtonsoft.Json; +namespace Dnn.AuthServices.Jwt.Components.Entity +{ + using Newtonsoft.Json; - [JsonObject] - public class RenewalDto - { - [JsonProperty("rtoken")] - public string RenewalToken; - } -} + /// + /// Renewal token data transfer object. + /// + [JsonObject] + public class RenewalDto + { + /// + /// A string representing the renewal token. + /// + [JsonProperty("rtoken")] + public string RenewalToken; + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Components/Schedule/PurgeExpiredTokensTask.cs b/DNN Platform/Dnn.AuthServices.Jwt/Components/Schedule/PurgeExpiredTokensTask.cs index 2c329d7a3e7..6640ec9c073 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Components/Schedule/PurgeExpiredTokensTask.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Components/Schedule/PurgeExpiredTokensTask.cs @@ -12,7 +12,7 @@ namespace Dnn.AuthServices.Jwt.Components.Schedule using DotNetNuke.Services.Scheduling; /// - /// Scheduled task to delete tokens that linger in the database after having expired + /// Scheduled task to delete tokens that linger in the database after having expired. /// public class PurgeExpiredTokensTask : SchedulerClient { @@ -21,7 +21,7 @@ public class PurgeExpiredTokensTask : SchedulerClient /// /// Initializes a new instance of the class. /// - /// The object used to record the results from this task + /// The object used to record the results from this task. public PurgeExpiredTokensTask(ScheduleHistoryItem objScheduleHistoryItem) { this.ScheduleHistoryItem = objScheduleHistoryItem; diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Data/DataService.cs b/DNN Platform/Dnn.AuthServices.Jwt/Data/DataService.cs index f42faaecd62..27ea2c4a515 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Data/DataService.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Data/DataService.cs @@ -2,83 +2,95 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Data -{ - using System; - using System.Collections.Generic; - using System.Web.Caching; +namespace Dnn.AuthServices.Jwt.Data +{ + using System; + using System.Collections.Generic; + using System.Web.Caching; - using Dnn.AuthServices.Jwt.Components.Entity; - using DotNetNuke.Common.Utilities; - using DotNetNuke.ComponentModel; - using DotNetNuke.Data; + using Dnn.AuthServices.Jwt.Components.Entity; + using DotNetNuke.Common.Utilities; + using DotNetNuke.ComponentModel; + using DotNetNuke.Data; - /// ----------------------------------------------------------------------------- - /// - /// This class provides the Data Access Layer for the JWT Authentication library. - /// - public class DataService : ComponentBase, IDataService - { - private readonly DataProvider _dataProvider = DataProvider.Instance(); - - public virtual PersistedToken GetTokenById(string tokenId) - { - try - { - return CBO.GetCachedObject( - new CacheItemArgs(GetCacheKey(tokenId), 60, CacheItemPriority.Default), - _ => CBO.FillObject(this._dataProvider.ExecuteReader("JsonWebTokens_GetById", tokenId))); - } - catch (InvalidCastException) - { - // occurs when no record found in th DB - return null; - } - } - - public virtual IList GetUserTokens(int userId) - { - return CBO.FillCollection(this._dataProvider.ExecuteReader("JsonWebTokens_GetByUserId", userId)); - } - - public virtual void AddToken(PersistedToken token) - { - this._dataProvider.ExecuteNonQuery("JsonWebTokens_Add", token.TokenId, token.UserId, - token.TokenExpiry, token.RenewalExpiry, token.TokenHash, token.RenewalHash); - DataCache.SetCache(GetCacheKey(token.TokenId), token, token.TokenExpiry.ToLocalTime()); - } - - public virtual void UpdateToken(PersistedToken token) - { - this._dataProvider.ExecuteNonQuery("JsonWebTokens_Update", token.TokenId, token.TokenExpiry, token.TokenHash); - token.RenewCount += 1; - DataCache.SetCache(GetCacheKey(token.TokenId), token, token.TokenExpiry.ToLocalTime()); - } - - public virtual void DeleteToken(string tokenId) - { - this._dataProvider.ExecuteNonQuery("JsonWebTokens_DeleteById", tokenId); - DataCache.RemoveCache(GetCacheKey(tokenId)); - } - - public virtual void DeleteUserTokens(int userId) - { - this._dataProvider.ExecuteNonQuery("JsonWebTokens_DeleteByUser", userId); - foreach (var token in this.GetUserTokens(userId)) - { - DataCache.RemoveCache(GetCacheKey(token.TokenId)); - } - } - - public virtual void DeleteExpiredTokens() - { - // don't worry about caching; these will already be invalidated by cache manager - this._dataProvider.ExecuteNonQuery("JsonWebTokens_DeleteExpired"); - } - - private static string GetCacheKey(string tokenId) - { - return string.Join(":", "JsonWebTokens", tokenId); - } - } -} + /// + /// This class provides the Data Access Layer for the JWT Authentication library. + /// + public class DataService : ComponentBase, IDataService + { + private readonly DataProvider dataProvider = DataProvider.Instance(); + + /// + public virtual PersistedToken GetTokenById(string tokenId) + { + try + { + return CBO.GetCachedObject( + new CacheItemArgs(GetCacheKey(tokenId), 60, CacheItemPriority.Default), + _ => CBO.FillObject(this.dataProvider.ExecuteReader("JsonWebTokens_GetById", tokenId))); + } + catch (InvalidCastException) + { + // occurs when no record found in th DB + return null; + } + } + + /// + public virtual IList GetUserTokens(int userId) + { + return CBO.FillCollection(this.dataProvider.ExecuteReader("JsonWebTokens_GetByUserId", userId)); + } + + /// + public virtual void AddToken(PersistedToken token) + { + this.dataProvider.ExecuteNonQuery( + "JsonWebTokens_Add", + token.TokenId, + token.UserId, + token.TokenExpiry, + token.RenewalExpiry, + token.TokenHash, + token.RenewalHash); + DataCache.SetCache(GetCacheKey(token.TokenId), token, token.TokenExpiry.ToLocalTime()); + } + + /// + public virtual void UpdateToken(PersistedToken token) + { + this.dataProvider.ExecuteNonQuery("JsonWebTokens_Update", token.TokenId, token.TokenExpiry, token.TokenHash); + token.RenewCount += 1; + DataCache.SetCache(GetCacheKey(token.TokenId), token, token.TokenExpiry.ToLocalTime()); + } + + /// + public virtual void DeleteToken(string tokenId) + { + this.dataProvider.ExecuteNonQuery("JsonWebTokens_DeleteById", tokenId); + DataCache.RemoveCache(GetCacheKey(tokenId)); + } + + /// + public virtual void DeleteUserTokens(int userId) + { + this.dataProvider.ExecuteNonQuery("JsonWebTokens_DeleteByUser", userId); + foreach (var token in this.GetUserTokens(userId)) + { + DataCache.RemoveCache(GetCacheKey(token.TokenId)); + } + } + + /// + public virtual void DeleteExpiredTokens() + { + // don't worry about caching; these will already be invalidated by cache manager + this.dataProvider.ExecuteNonQuery("JsonWebTokens_DeleteExpired"); + } + + private static string GetCacheKey(string tokenId) + { + return string.Join(":", "JsonWebTokens", tokenId); + } + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Data/IDataService.cs b/DNN Platform/Dnn.AuthServices.Jwt/Data/IDataService.cs index 440ea428650..8bdd7b7e109 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Data/IDataService.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Data/IDataService.cs @@ -8,20 +8,52 @@ namespace Dnn.AuthServices.Jwt.Data using Dnn.AuthServices.Jwt.Components.Entity; + /// + /// Provides data access services. + /// public interface IDataService - { + { + /// + /// Gets a token by a given token id. + /// + /// The token id. + /// . PersistedToken GetTokenById(string tokenId); - + + /// + /// Gets a user token by the user id. + /// + /// The id of the user. + /// A list of tokens. IList GetUserTokens(int userId); - + + /// + /// Adds (persists) a token. + /// + /// The token to persist. void AddToken(PersistedToken token); - + + /// + /// Updates an existing token. + /// + /// The token to persist. void UpdateToken(PersistedToken token); - + + /// + /// Deletes an existing token. + /// + /// The id of the token to delete. void DeleteToken(string tokenId); - + + /// + /// Deletes all tokens for a user. + /// + /// The id of user for which to delete the tokens. void DeleteUserTokens(int userId); - + + /// + /// Deletes the expired tokens. + /// void DeleteExpiredTokens(); } } diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Dnn.AuthServices.Jwt.csproj b/DNN Platform/Dnn.AuthServices.Jwt/Dnn.AuthServices.Jwt.csproj index b8b348b859c..c67c94d060b 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Dnn.AuthServices.Jwt.csproj +++ b/DNN Platform/Dnn.AuthServices.Jwt/Dnn.AuthServices.Jwt.csproj @@ -99,6 +99,10 @@ + + {6928A9B1-F88A-4581-A132-D3EB38669BB0} + DotNetNuke.Abstractions + {3cd5f6b8-8360-4862-80b6-f402892db7dd} DotNetNuke.Instrumentation diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Library.build b/DNN Platform/Dnn.AuthServices.Jwt/Library.build index e6bc4c9dcb0..e6450070b98 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Library.build +++ b/DNN Platform/Dnn.AuthServices.Jwt/Library.build @@ -4,7 +4,7 @@ - resources + zip Dnn.Jwt DnnJwtAuth $(WebsitePath)\DesktopModules\AuthenticationServices\JWTAuth diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Services/MobileController.cs b/DNN Platform/Dnn.AuthServices.Jwt/Services/MobileController.cs index 89e508a9958..f62c5800a46 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Services/MobileController.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Services/MobileController.cs @@ -2,104 +2,122 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Services -{ - using System.Net.Http.Headers; - using System.Web.Http; +namespace Dnn.AuthServices.Jwt.Services +{ + using System.Net.Http.Headers; + using System.Web.Http; - using Dnn.AuthServices.Jwt.Components.Common.Controllers; - using Dnn.AuthServices.Jwt.Components.Entity; - using DotNetNuke.Web.Api; - using Newtonsoft.Json; + using Dnn.AuthServices.Jwt.Components.Common.Controllers; + using Dnn.AuthServices.Jwt.Components.Entity; + using DotNetNuke.Web.Api; + using Newtonsoft.Json; - [DnnAuthorize(AuthTypes = "JWT")] - public class MobileController : DnnApiController - { - /// - /// Clients that used JWT login should use this API call to logout and invalidate the tokens. - /// - /// - [HttpGet] - public IHttpActionResult Logout() - { - return JwtController.Instance.LogoutUser(this.Request) ? (IHttpActionResult)this.Ok(new { success = true }) : this.Unauthorized(); - } - - /// - /// Clients that want to go cookie-less should call this API to login and receive - /// a Json Web Token (JWT) that allows them to authenticate the users to other - /// secure API endpoints afterwards. - /// - /// AllowAnonymous attribute must stay in this call even though the - /// DnnAuthorize attribute is present at a class level. - /// - [HttpPost] - [AllowAnonymous] - public IHttpActionResult Login(LoginData loginData) - { - var result = JwtController.Instance.LoginUser(this.Request, loginData); - return this.ReplyWith(result); - } - - /// - /// Extends the token expiry. A new JWT is returned to the caller which must be used in - /// new API requests. The caller must pass the renewal token received at the login time. - /// The header still needs to pass the current token for validation even when it is expired. - /// - /// The access token is allowed to get renewed one time only.
- /// AllowAnonymous attribute must stay in this call even though the - /// DnnAuthorize attribute is present at a class level. - ///
- /// - [HttpPost] - [AllowAnonymous] - public IHttpActionResult ExtendToken(RenewalDto rtoken) - { - var result = JwtController.Instance.RenewToken(this.Request, rtoken.RenewalToken); - return this.ReplyWith(result); - } - - // Test API Method 1 - [HttpGet] - public IHttpActionResult TestGet() - { - var identity = System.Threading.Thread.CurrentPrincipal.Identity; - var reply = $"Hello {identity.Name}! You are authenticated through {identity.AuthenticationType}."; - return this.Ok(new { reply }); - } - - // Test API Method 2 - [HttpPost] - [ValidateAntiForgeryToken] - public IHttpActionResult TestPost(TestPostData something) - { - var identity = System.Threading.Thread.CurrentPrincipal.Identity; - var reply = $"Hello {identity.Name}! You are authenticated through {identity.AuthenticationType}." + - $" You said: ({something.Text})"; - return this.Ok(new { reply }); - } - - private IHttpActionResult ReplyWith(LoginResultData result) - { - if (result == null) - { - return this.Unauthorized(); - } - - if (!string.IsNullOrEmpty(result.Error)) - { - // HACK: this will return the scheme with the error message as a challenge; non-standard method - return this.Unauthorized(new AuthenticationHeaderValue(JwtController.AuthScheme, result.Error)); - } - - return this.Ok(result); - } - - [JsonObject] - public class TestPostData - { - [JsonProperty("text")] - public string Text; - } - } -} + /// + /// API controller for JWT services (usually mobile). + /// + [DnnAuthorize(AuthTypes = "JWT")] + public class MobileController : DnnApiController + { + /// + /// Clients that used JWT login should use this API call to logout and invalidate the tokens. + /// + /// An asynchronous HTTP response. + [HttpGet] + public IHttpActionResult Logout() + { + return JwtController.Instance.LogoutUser(this.Request) ? (IHttpActionResult)this.Ok(new { success = true }) : this.Unauthorized(); + } + + /// + /// Clients that want to go cookie-less should call this API to login and receive + /// a Json Web Token (JWT) that allows them to authenticate the users to other + /// secure API endpoints afterwards. + /// + /// AllowAnonymous attribute must stay in this call even though the + /// DnnAuthorize attribute is present at a class level. + /// The information usd for login, . + /// An asynchronous HTTP response. + [HttpPost] + [AllowAnonymous] + public IHttpActionResult Login(LoginData loginData) + { + var result = JwtController.Instance.LoginUser(this.Request, loginData); + return this.ReplyWith(result); + } + + /// + /// Extends the token expiry. A new JWT is returned to the caller which must be used in + /// new API requests. The caller must pass the renewal token received at the login time. + /// The header still needs to pass the current token for validation even when it is expired. + /// + /// The access token is allowed to get renewed one time only.
+ /// AllowAnonymous attribute must stay in this call even though the + /// DnnAuthorize attribute is present at a class level. + ///
+ /// The renewal token information, . + /// An asynchronous HTTP response. + [HttpPost] + [AllowAnonymous] + public IHttpActionResult ExtendToken(RenewalDto rtoken) + { + var result = JwtController.Instance.RenewToken(this.Request, rtoken.RenewalToken); + return this.ReplyWith(result); + } + + /// + /// Tests a get HTTP request. + /// + /// Basic information about the identity. + [HttpGet] + public IHttpActionResult TestGet() + { + var identity = System.Threading.Thread.CurrentPrincipal.Identity; + var reply = $"Hello {identity.Name}! You are authenticated through {identity.AuthenticationType}."; + return this.Ok(new { reply }); + } + + /// + /// Tests a POST api method. + /// + /// . + /// Basic information about the identity and the text provided in the POST. + [HttpPost] + [ValidateAntiForgeryToken] + public IHttpActionResult TestPost(TestPostData something) + { + var identity = System.Threading.Thread.CurrentPrincipal.Identity; + var reply = $"Hello {identity.Name}! You are authenticated through {identity.AuthenticationType}." + + $" You said: ({something.Text})"; + return this.Ok(new { reply }); + } + + private IHttpActionResult ReplyWith(LoginResultData result) + { + if (result == null) + { + return this.Unauthorized(); + } + + if (!string.IsNullOrEmpty(result.Error)) + { + // HACK: this will return the scheme with the error message as a challenge; non-standard method + return this.Unauthorized(new AuthenticationHeaderValue(JwtController.AuthScheme, result.Error)); + } + + return this.Ok(result); + } + + /// + /// Represents the request data for a test POST. + /// + [JsonObject] + public class TestPostData + { + /// + /// The text used in the test. + /// + [JsonProperty("text")] + public string Text; + } + } +} diff --git a/DNN Platform/Dnn.AuthServices.Jwt/Services/ServiceRouteMapper.cs b/DNN Platform/Dnn.AuthServices.Jwt/Services/ServiceRouteMapper.cs index d94d2b82398..adf5959e8ca 100644 --- a/DNN Platform/Dnn.AuthServices.Jwt/Services/ServiceRouteMapper.cs +++ b/DNN Platform/Dnn.AuthServices.Jwt/Services/ServiceRouteMapper.cs @@ -2,16 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace Dnn.AuthServices.Jwt.Services -{ - using DotNetNuke.Web.Api; +namespace Dnn.AuthServices.Jwt.Services +{ + using DotNetNuke.Web.Api; - public class ServiceRouteMapper : IServiceRouteMapper - { - public void RegisterRoutes(IMapRoute mapRouteManager) - { - mapRouteManager.MapHttpRoute( - "JwtAuth", "default", "{controller}/{action}", new[] { this.GetType().Namespace }); - } - } -} + /// + /// Registers the API routes for this extension. + /// + public class ServiceRouteMapper : IServiceRouteMapper + { + /// + public void RegisterRoutes(IMapRoute mapRouteManager) + { + mapRouteManager.MapHttpRoute("JwtAuth", "default", "{controller}/{action}", new[] { this.GetType().Namespace }); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web/Api/Auth/AuthMessageHandlerBase.cs b/DNN Platform/DotNetNuke.Web/Api/Auth/AuthMessageHandlerBase.cs index 4abc246d033..c942c2e5c42 100644 --- a/DNN Platform/DotNetNuke.Web/Api/Auth/AuthMessageHandlerBase.cs +++ b/DNN Platform/DotNetNuke.Web/Api/Auth/AuthMessageHandlerBase.cs @@ -14,29 +14,49 @@ namespace DotNetNuke.Web.Api.Auth using DotNetNuke.Instrumentation; + /// + /// Base class for authentication providers message handlers. + /// public abstract class AuthMessageHandlerBase : DelegatingHandler { - private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(AuthMessageHandlerBase)); - + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(AuthMessageHandlerBase)); + + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether this handler should be included by default in all API endpoints. + /// A value indicating whether this handler should enforce SSL usage. protected AuthMessageHandlerBase(bool includeByDefault, bool forceSsl) { this.DefaultInclude = includeByDefault; this.ForceSsl = forceSsl; } - + + /// + /// Gets the name of the authentication scheme. + /// public abstract string AuthScheme { get; } - + + /// + /// Gets a value indicating whether this handler should bypass the anti-forgery token check. + /// public virtual bool BypassAntiForgeryToken => false; - + + /// + /// Gets a value indicating whether this handler should be included by default on all API endpoints. + /// public bool DefaultInclude { get; } - + + /// + /// Gets a value indicating whether this handler should enforce SSL usage on it's endpoints. + /// public bool ForceSsl { get; } /// /// A chance to process inbound requests. /// - /// the request message. - /// a cancellationtoken. + /// The request message. + /// A cancellationtoken. /// null normally, if a response is returned all inbound processing is terminated and the resposne is returned. public virtual HttpResponseMessage OnInboundRequest(HttpRequestMessage request, CancellationToken cancellationToken) { @@ -47,13 +67,18 @@ public virtual HttpResponseMessage OnInboundRequest(HttpRequestMessage request, /// A change to process outbound responses. /// /// The response message. - /// a cancellationtoken. - /// the responsemessage. + /// A cancellationtoken. + /// The responsemessage. public virtual HttpResponseMessage OnOutboundResponse(HttpResponseMessage response, CancellationToken cancellationToken) { return response; } - + + /// + /// Checks if the current request is an XmlHttpRequest. + /// + /// The HTTP Request. + /// A value indicating whether the request is an XmlHttpRequest. protected static bool IsXmlHttpRequest(HttpRequestMessage request) { string value = null; @@ -66,13 +91,24 @@ protected static bool IsXmlHttpRequest(HttpRequestMessage request) return !string.IsNullOrEmpty(value) && value.Equals("XmlHttpRequest", StringComparison.InvariantCultureIgnoreCase); } - + + /// + /// Sets the current principal for the request. + /// + /// The principal to set. + /// The current request. protected static void SetCurrentPrincipal(IPrincipal principal, HttpRequestMessage request) { Thread.CurrentPrincipal = principal; request.GetHttpContext().User = principal; } - + + /// + /// Asynchronously sends a response. + /// + /// The current request. + /// A cancellation token. + /// An HttpResponseMessage Task. protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = this.OnInboundRequest(request, cancellationToken); @@ -84,7 +120,12 @@ protected override Task SendAsync(HttpRequestMessage reques return base.SendAsync(request, cancellationToken).ContinueWith(x => this.OnOutboundResponse(x.Result, cancellationToken), cancellationToken); } - + + /// + /// Checks if the current request requires authentication. + /// + /// The current request. + /// A value indication whether the current request needs authentication. protected bool NeedsAuthentication(HttpRequestMessage request) { if (this.MustEnforceSslInRequest(request))