/// <summary> /// Add a claim to the specified user /// </summary> public void AddClaim(string userName, Claim claim) { if (userName == null) { throw new ArgumentNullException(nameof(userName)); } else if (claim == null) { throw new ArgumentNullException(nameof(claim)); } try { using (var dataContext = this.m_configuration.Provider.GetWriteConnection()) { dataContext.Open(); var user = dataContext.FirstOrDefault <DbSecurityUser>(o => o.UserName.ToLower() == userName.ToLower()); if (user == null) { throw new KeyNotFoundException(userName); } lock (this.m_syncLock) { var existingClaim = dataContext.FirstOrDefault <DbUserClaim>(o => o.ClaimType == claim.Type && o.SourceKey == user.Key); // Set the secret if (existingClaim == null) { existingClaim = new DbUserClaim() { ClaimType = claim.Type, ClaimValue = claim.Value, SourceKey = user.Key }; dataContext.Insert(existingClaim); } else { existingClaim.ClaimValue = claim.Value; dataContext.Update(existingClaim); } } } } catch (Exception e) { this.m_traceSource.TraceEvent(TraceEventType.Error, e.HResult, e.ToString()); throw; } }
/// <summary> /// Add a claim to the specified user /// </summary> public void AddClaim(string userName, IClaim claim, IPrincipal principal, TimeSpan?expire = null) { if (userName == null) { throw new ArgumentNullException(nameof(userName)); } else if (claim == null) { throw new ArgumentNullException(nameof(claim)); } this.VerifyPrincipal(principal, PermissionPolicyIdentifiers.AlterIdentity); try { using (var dataContext = this.m_configuration.Provider.GetWriteConnection()) { dataContext.Open(); var user = dataContext.FirstOrDefault <DbSecurityUser>(o => o.UserName.ToLower() == userName.ToLower()); if (user == null) { throw new KeyNotFoundException(userName); } lock (this.m_syncLock) { var existingClaim = dataContext.FirstOrDefault <DbUserClaim>(o => o.ClaimType == claim.Type && o.SourceKey == user.Key); // Set the secret if (existingClaim == null) { existingClaim = new DbUserClaim() { ClaimType = claim.Type, ClaimValue = claim.Value, SourceKey = user.Key }; if (expire.HasValue) { existingClaim.ClaimExpiry = DateTime.Now.Add(expire.Value); } dataContext.Insert(existingClaim); } else { existingClaim.ClaimValue = claim.Value; if (expire.HasValue) { existingClaim.ClaimExpiry = DateTime.Now.Add(expire.Value); } dataContext.Update(existingClaim); } } } } catch (Exception e) { this.m_traceSource.TraceEvent(EventLevel.Error, e.ToString()); throw; } }
/// <summary> /// Authenticate the user using a TwoFactorAuthentication secret /// </summary> public IPrincipal Authenticate(string userName, string password, string tfaSecret) { // First, let's verify the TFA if (String.IsNullOrEmpty(userName)) { throw new ArgumentNullException(nameof(userName)); } else if (String.IsNullOrEmpty(tfaSecret)) { throw new ArgumentNullException(nameof(tfaSecret)); } // Authentication event args var evt = new AuthenticatingEventArgs(userName); this.Authenticating?.Invoke(this, evt); if (evt.Cancel) { throw new SecurityException("Authentication cancelled"); } // Password hasher var hashingService = ApplicationServiceContext.Current.GetService <IPasswordHashingService>(); tfaSecret = hashingService.ComputeHash(tfaSecret); // Try to authenticate try { using (var dataContext = this.m_configuration.Provider.GetWriteConnection()) { dataContext.Open(); using (var tx = dataContext.BeginTransaction()) { try { var user = dataContext.FirstOrDefault <DbSecurityUser>(o => o.UserName.ToLower() == userName.ToLower()); if (user == null) { throw new KeyNotFoundException(userName); } var claims = dataContext.Query <DbUserClaim>(o => o.SourceKey == user.Key && (o.ClaimType == SanteDBClaimTypes.SanteDBOTAuthCode || o.ClaimType == SanteDBClaimTypes.SanteDBCodeAuth) && (!o.ClaimExpiry.HasValue || o.ClaimExpiry > DateTime.Now)); DbUserClaim tfaClaim = claims.FirstOrDefault(o => o.ClaimType == SanteDBClaimTypes.SanteDBOTAuthCode), noPassword = claims.FirstOrDefault(o => o.ClaimType == SanteDBClaimTypes.SanteDBCodeAuth); if (tfaClaim == null) { throw new InvalidOperationException("Cannot find appropriate claims for TFA"); } // Expiry check IClaimsPrincipal retVal = null; if (String.IsNullOrEmpty(password) && Boolean.Parse(noPassword?.ClaimValue ?? "false") && tfaSecret == tfaClaim.ClaimValue) // TFA password hash sent as password, this is a password reset token - It will be set to expire ASAP { retVal = AdoClaimsIdentity.Create(user, true, "Tfa+LastPasswordHash").CreateClaimsPrincipal(); (retVal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.PurposeOfUse, PurposeOfUseKeys.SecurityAdmin.ToString())); (retVal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, PermissionPolicyIdentifiers.LoginPasswordOnly)); (retVal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, PermissionPolicyIdentifiers.ReadMetadata)); (retVal.Identity as IClaimsIdentity).RemoveClaim(retVal.FindFirst(SanteDBClaimTypes.Expiration)); (retVal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.Expiration, DateTime.Now.AddMinutes(5).ToString("o"))); // Special case, passwoird } else if (!String.IsNullOrEmpty(password)) { if (!user.TwoFactorEnabled || tfaSecret == tfaClaim.ClaimValue) { retVal = this.Authenticate(userName, password) as IClaimsPrincipal; } else { throw new AuthenticationException("TFA_MISMATCH"); } } else { throw new PolicyViolationException(new GenericPrincipal(new GenericIdentity(userName), new string[0]), PermissionPolicyIdentifiers.Login, PolicyGrantType.Deny); } // Now we want to fire authenticated this.Authenticated?.Invoke(this, new AuthenticatedEventArgs(userName, retVal, true)); tx.Commit(); return(retVal); } catch { tx.Rollback(); throw; } } } } catch (Exception e) { this.m_traceSource.TraceEvent(EventLevel.Verbose, "Invalid credentials : {0}/{1}", userName, password); this.m_traceSource.TraceEvent(EventLevel.Error, e.ToString()); this.Authenticated?.Invoke(this, new AuthenticatedEventArgs(userName, null, false)); throw; } }
/// <summary> /// Creates a principal based on username and password /// </summary> internal static AdoClaimsIdentity Create(byte[] refreshToken) { try { Guid?userId = Guid.Empty; using (var dataContext = s_configuration.Provider.GetWriteConnection()) { dataContext.Open(); // Attempt to get a user var cvalue = BitConverter.ToString(refreshToken).Replace("-", ""); DbUserClaim secretClaim = dataContext.FirstOrDefault <DbUserClaim>(o => o.ClaimType == AdoDataConstants.RefreshSecretClaimType && o.ClaimValue == cvalue); if (secretClaim == null) { throw new SecurityException("Invalid refresh token"); } DbUserClaim expiryClaim = dataContext.FirstOrDefault <DbUserClaim>(o => o.ClaimType == AdoDataConstants.RefreshExpiryClaimType && o.SourceKey == secretClaim.SourceKey); if (expiryClaim == null || DateTimeOffset.Parse(expiryClaim.ClaimValue) < DateTimeOffset.Now) { throw new SecurityException($"Token expired {expiryClaim?.ClaimValue}"); } // Grab the user and re-authenticate them var user = dataContext.SingleOrDefault <DbSecurityUser>(u => u.Key == secretClaim.SourceKey); var roles = dataContext.Query <DbSecurityRole>(dataContext.CreateSqlStatement <DbSecurityRole>().SelectFrom() .InnerJoin <DbSecurityUserRole>(o => o.Key, o => o.RoleKey) .Where <DbSecurityUserRole>(o => o.UserKey == user.Key)); if (user.ObsoletionTime.HasValue) { throw new AuthenticationException("Security user is not active"); } var userIdentity = new AdoClaimsIdentity(user, roles, true) { m_authenticationType = "Refresh" }; // Is user allowed to login? if (user.UserClass == UserClassKeys.HumanUser) { new PolicyPermission(System.Security.Permissions.PermissionState.Unrestricted, PermissionPolicyIdentifiers.Login, new GenericPrincipal(userIdentity, null)).Demand(); } else if (user.UserClass == UserClassKeys.ApplicationUser) { new PolicyPermission(System.Security.Permissions.PermissionState.Unrestricted, PermissionPolicyIdentifiers.LoginAsService, new GenericPrincipal(userIdentity, null)).Demand(); } return(userIdentity); } } catch (AuthenticationException) { // TODO: Audit this throw; } catch (SecurityException) { // TODO: Audit this throw; } catch (SqlException e) { switch (e.Number) { case 51900: throw new AuthenticationException("Account is locked"); case 51901: throw new AuthenticationException("Invalid username/password"); case 51902: throw new AuthenticationException("User requires two-factor authentication"); default: throw e; } } catch (Exception e) { s_traceSource.TraceEvent(TraceEventType.Error, e.HResult, e.ToString()); throw new Exception("Creating identity failed", e); } }
/// <summary> /// Authenticate the user using a TwoFactorAuthentication secret /// </summary> public IPrincipal Authenticate(string userName, string password, string tfaSecret) { // First, let's verify the TFA if (String.IsNullOrEmpty(userName)) { throw new ArgumentNullException(nameof(userName)); } else if (String.IsNullOrEmpty(tfaSecret)) { throw new ArgumentNullException(nameof(tfaSecret)); } // Authentication event args var evt = new AuthenticatingEventArgs(userName); this.Authenticating?.Invoke(this, evt); if (evt.Cancel) { throw new SecurityException("Authentication cancelled"); } // Password hasher var hashingService = ApplicationContext.Current.GetService <IPasswordHashingService>(); tfaSecret = hashingService.EncodePassword(tfaSecret); // Try to authenticate try { using (var dataContext = this.m_configuration.Provider.GetWriteConnection()) { dataContext.Open(); using (var tx = dataContext.BeginTransaction()) { try { var user = dataContext.FirstOrDefault <DbSecurityUser>(o => o.UserName.ToLower() == userName.ToLower()); if (user == null) { throw new KeyNotFoundException(userName); } var claims = dataContext.Query <DbUserClaim>(o => o.SourceKey == user.Key && (o.ClaimType == OpenIzClaimTypes.OpenIZTfaSecretClaim || o.ClaimType == OpenIzClaimTypes.OpenIZTfaSecretExpiry || o.ClaimType == OpenIzClaimTypes.OpenIZPasswordlessAuth)); DbUserClaim tfaClaim = claims.FirstOrDefault(o => o.ClaimType == OpenIzClaimTypes.OpenIZTfaSecretClaim), tfaExpiry = claims.FirstOrDefault(o => o.ClaimType == OpenIzClaimTypes.OpenIZTfaSecretExpiry), noPassword = claims.FirstOrDefault(o => o.ClaimType == OpenIzClaimTypes.OpenIZPasswordlessAuth); if (tfaClaim == null || tfaExpiry == null) { throw new InvalidOperationException("Cannot find appropriate claims for TFA"); } // Expiry check ClaimsPrincipal retVal = null; DateTime expiryDate = DateTime.Parse(tfaExpiry.ClaimValue); if (expiryDate < DateTime.Now) { throw new SecurityException("TFA secret expired"); } else if (String.IsNullOrEmpty(password) && Boolean.Parse(noPassword?.ClaimValue ?? "false") && tfaSecret == tfaClaim.ClaimValue) // Last known password hash sent as password, this is a password reset token - It will be set to expire ASAP { retVal = AdoClaimsIdentity.Create(user, true, "Tfa+LastPasswordHash").CreateClaimsPrincipal(); (retVal.Identity as ClaimsIdentity).AddClaim(new Claim(OpenIzClaimTypes.OpenIzGrantedPolicyClaim, PermissionPolicyIdentifiers.ChangePassword)); (retVal.Identity as ClaimsIdentity).RemoveClaim(retVal.FindFirst(ClaimTypes.Expiration)); // TODO: Add to configuration (retVal.Identity as ClaimsIdentity).AddClaim(new Claim(ClaimTypes.Expiration, DateTime.Now.AddMinutes(5).ToString("o"))); } else if (!String.IsNullOrEmpty(password)) { retVal = this.Authenticate(userName, password) as ClaimsPrincipal; } else { throw new PolicyViolationException(PermissionPolicyIdentifiers.Login, PolicyDecisionOutcomeType.Deny); } // Now we want to fire authenticated this.Authenticated?.Invoke(this, new AuthenticatedEventArgs(userName, retVal, true)); tx.Commit(); return(retVal); } catch { tx.Rollback(); throw; } } } } catch (Exception e) { this.m_traceSource.TraceEvent(TraceEventType.Verbose, e.HResult, "Invalid credentials : {0}/{1}", userName, password); this.m_traceSource.TraceEvent(TraceEventType.Error, e.HResult, e.ToString()); this.Authenticated?.Invoke(this, new AuthenticatedEventArgs(userName, null, false)); throw; } }
/// <summary> /// Authenticate a user using their challenge response /// </summary> public IPrincipal Authenticate(string userName, Guid challengeKey, string response, String tfa) { try { var authArgs = new AuthenticatingEventArgs(userName); this.Authenticating?.Invoke(this, authArgs); if (authArgs.Cancel) { throw new SecurityException("Authentication cancelled"); } var hashService = ApplicationServiceContext.Current.GetService <IPasswordHashingService>(); var responseHash = hashService.ComputeHash(response); // Connection to perform auth using (var context = this.m_configuration.Provider.GetWriteConnection()) { context.Open(); var query = context.CreateSqlStatement <DbSecurityUser>().SelectFrom(typeof(DbSecurityUser), typeof(DbSecurityUserChallengeAssoc)) .InnerJoin <DbSecurityUserChallengeAssoc>(o => o.Key, o => o.UserKey) .Where(o => o.UserName.ToLower() == userName.ToLower() && o.ObsoletionTime == null) .And <DbSecurityUserChallengeAssoc>(o => o.ExpiryTime > DateTimeOffset.Now); var dbUser = context.FirstOrDefault <CompositeResult <DbSecurityUser, DbSecurityUserChallengeAssoc> >(query); // User found? if (dbUser == null) { throw new SecurityException("AUTH_INV"); } // TFA? if (!String.IsNullOrEmpty(tfa)) { var tfaSecret = ApplicationServiceContext.Current.GetService <IPasswordHashingService>().ComputeHash(tfa); var claims = context.Query <DbUserClaim>(o => o.SourceKey == dbUser.Object1.Key && (!o.ClaimExpiry.HasValue || o.ClaimExpiry > DateTimeOffset.Now)).ToList(); DbUserClaim tfaClaim = claims.FirstOrDefault(o => o.ClaimType == SanteDBClaimTypes.SanteDBOTAuthCode); if (tfaClaim == null || !tfaSecret.Equals(tfaClaim.ClaimValue, StringComparison.OrdinalIgnoreCase)) { throw new SecurityException("TFA_MISMATCH"); } } if (dbUser.Object1.Lockout > DateTimeOffset.Now) { throw new SecurityException("AUTH_LCK"); } else if (dbUser.Object2.ChallengeResponse != responseHash || dbUser.Object1.Lockout.GetValueOrDefault() > DateTimeOffset.Now) // Increment invalid { dbUser.Object1.InvalidLoginAttempts++; if (dbUser.Object1.InvalidLoginAttempts > this.m_securityConfiguration.GetSecurityPolicy <Int32>(SecurityPolicyIdentification.MaxInvalidLogins, 5)) { dbUser.Object1.Lockout = DateTimeOffset.Now.Add(new TimeSpan(0, 0, dbUser.Object1.InvalidLoginAttempts.Value * 30)); } dbUser.Object1.UpdatedByKey = Guid.Parse(AuthenticationContext.SystemUserSid); dbUser.Object1.UpdatedTime = DateTimeOffset.Now; context.Update(dbUser.Object1); if (dbUser.Object1.Lockout > DateTimeOffset.Now) { throw new AuthenticationException("AUTH_LCK"); } else { throw new AuthenticationException("AUTH_INV"); } } else { var principal = AdoClaimsIdentity.Create(context, dbUser.Object1, true, "Secret=" + challengeKey.ToString()).CreateClaimsPrincipal(); this.m_policyEnforcementService.Demand(PermissionPolicyIdentifiers.Login, principal); // must still be allowed to login (principal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.PurposeOfUse, PurposeOfUseKeys.SecurityAdmin.ToString())); (principal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, PermissionPolicyIdentifiers.ReadMetadata)); (principal.Identity as IClaimsIdentity).AddClaim(new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, PermissionPolicyIdentifiers.LoginPasswordOnly)); this.Authenticated?.Invoke(this, new AuthenticatedEventArgs(userName, principal, true)); return(principal); } } } catch (Exception e) { this.m_tracer.TraceError("Challenge authentication failed: {0}", e); this.Authenticated?.Invoke(this, new AuthenticatedEventArgs(userName, null, false)); throw new AuthenticationException($"Challenge authentication failed"); } }
public UserClaimEntity(DbUserClaim claim) { _claim = claim; }