/// <summary> /// Extend the session /// </summary> /// <param name="refreshToken">The signed session token to be refreshed</param> /// <returns>The session that was extended</returns> public ISession Extend(byte[] refreshToken) { // Validate the parameters if (refreshToken == null) { throw new ArgumentNullException(nameof(refreshToken)); } IDbTransaction tx = null; using (var context = this.m_configuration.Provider.GetWriteConnection()) { try { context.Open(); tx = context.BeginTransaction(); var signingService = ApplicationServiceContext.Current.GetService <IDataSigningService>(); if (signingService == null) { this.m_traceSource.TraceWarning("No IDataSigningService provided. Digital signatures will not be verified"); } else if (!signingService.Verify(refreshToken.Take(16).ToArray(), refreshToken.Skip(16).ToArray())) { throw new SecurityException("Refresh token appears to have been tampered with"); } // Get the session to be extended var qToken = BitConverter.ToString(refreshToken.Take(16).ToArray()).Replace("-", ""); qToken = ApplicationServiceContext.Current.GetService <IPasswordHashingService>().ComputeHash(qToken); var dbSession = context.SingleOrDefault <DbSession>(o => o.RefreshToken == qToken && o.RefreshExpiration > DateTimeOffset.Now); if (dbSession == null) { throw new SecurityTokenValidationException(BitConverter.ToString(refreshToken)); } var claims = context.Query <DbSessionClaim>(o => o.SessionKey == dbSession.Key).ToArray(); // Get rid of the old session context.Delete <DbSessionClaim>(o => o.SessionKey == dbSession.Key); context.Delete(dbSession); // Generate a new session for this user dbSession.Key = Guid.Empty; refreshToken = this.CreateRefreshToken(); dbSession.RefreshToken = ApplicationServiceContext.Current.GetService <IPasswordHashingService>().ComputeHash(BitConverter.ToString(refreshToken).Replace("-", "")); dbSession.NotAfter = DateTimeOffset.Now + (dbSession.NotAfter - dbSession.NotBefore); // Extend for original time dbSession.NotBefore = DateTimeOffset.Now; dbSession.RefreshExpiration = dbSession.NotAfter.AddMinutes(10); // Save dbSession = context.Insert(dbSession); foreach (var clm in claims) { context.Insert(new DbSessionClaim() { SessionKey = dbSession.Key, ClaimType = clm.ClaimType, ClaimValue = clm.ClaimValue }); } tx.Commit(); if (signingService == null) { this.m_traceSource.TraceWarning("No IDataSigningService provided. Session data will be unsigned!"); var session = new AdoSecuritySession(dbSession.Key, dbSession.Key.ToByteArray(), refreshToken, dbSession.NotBefore, dbSession.NotAfter, claims.Select(o => new SanteDBClaim(o.ClaimType, o.ClaimValue)).ToArray()); this.m_adhocCache?.Add($"ses.{session.Key}", session); return(session); } else { var signedToken = dbSession.Key.ToByteArray().Concat(signingService.SignData(dbSession.Key.ToByteArray())).ToArray(); var signedRefresh = refreshToken.Concat(signingService.SignData(refreshToken)).ToArray(); var session = new AdoSecuritySession(dbSession.Key, signedToken, signedRefresh, dbSession.NotBefore, dbSession.NotAfter, claims.Select(o => new SanteDBClaim(o.ClaimType, o.ClaimValue)).ToArray()); this.m_adhocCache?.Add($"ses.{session.Key}", session); return(session); } } catch (Exception e) { tx?.Rollback(); this.m_traceSource.TraceError("Error getting session: {0}", e.Message); throw new SecurityException("Error getting session", e); } } }
/// <summary> /// Gets the specified session if valid from a signed session token /// </summary> /// <param name="sessionToken">The session token to retrieve the session for</param> /// <param name="allowExpired">When true, instructs the method to fetch a session even if it is expired</param> /// <returns>The fetched session token</returns> public ISession Get(byte[] sessionToken, bool allowExpired = false) { // Validate the parameters if (sessionToken == null) { throw new ArgumentNullException(nameof(sessionToken)); } try { var signingService = ApplicationServiceContext.Current.GetService <IDataSigningService>(); var sessionId = new Guid(sessionToken.Take(16).ToArray()); if (signingService == null) { this.m_traceSource.TraceWarning("No IDataSigingService registered. Session data will not be verified"); } else if (sessionToken.Length == 16) { this.m_traceSource.TraceWarning("Will not verify signature for session {0}", sessionId); } else if (!signingService.Verify(sessionToken.Take(16).ToArray(), sessionToken.Skip(16).ToArray())) { throw new SecurityException("Session token appears to have been tampered with"); } var session = this.m_adhocCache?.Get <AdoSecuritySession>($"ses.{sessionId}"); if (session != null) { if (session.Key == Guid.Empty) { var exp = this.m_adhocCache.Get <dynamic>($"ses.{sessionId}"); return(new GenericSession(Convert.FromBase64String((String)exp.Id), null, DateTime.Parse((String)exp.NotBefore), DateTime.Parse((String)exp.NotAfter), (exp.Claims as IEnumerable <dynamic>).Select(o => new SanteDBClaim((String)o.Type, (String)o.Value)).ToArray())); } else { return(session); } } else { using (var context = this.m_configuration.Provider.GetReadonlyConnection()) { context.Open(); // Check the cache var dbSession = context.SingleOrDefault <DbSession>(o => o.Key == sessionId); if (dbSession == null) { throw new SecuritySessionException(SessionExceptionType.NotEstablished, $"Session {BitConverter.ToString(sessionToken)} not found", null); } else if (dbSession.NotAfter < DateTimeOffset.Now) { throw new SecuritySessionException(SessionExceptionType.Expired, new AdoSecuritySession(dbSession), $"Session {BitConverter.ToString(sessionToken)} is expired", null); } else if (dbSession.NotBefore > DateTimeOffset.Now) { throw new SecuritySessionException(SessionExceptionType.NotYetValid, new AdoSecuritySession(dbSession), $"Session {BitConverter.ToString(sessionToken)} is expired", null); } else { var sessionInfo = new KeyValuePair <DbSession, DbSessionClaim[]>(dbSession, context.Query <DbSessionClaim>(o => o.SessionKey == dbSession.Key).ToArray()); session = new AdoSecuritySession(sessionInfo.Key.Key, sessionToken, null, sessionInfo.Key.NotBefore, sessionInfo.Key.NotAfter, sessionInfo.Value.Select(o => new SanteDBClaim(o.ClaimType, o.ClaimValue)).ToArray()); this.m_adhocCache?.Add($"ses.{sessionId}", session); } } } return(session); } catch (Exception e) { this.m_traceSource.TraceError("Error getting session: {0}", e.Message); throw new SecuritySessionException(SessionExceptionType.Other, null, $"Could not get session token {BitConverter.ToString(sessionToken)}", e); } }
/// <summary> /// Establish the session /// </summary> /// <param name="principal">The security principal for which the session is being created</param> /// <param name="expiry">The expiration of the session</param> /// <param name="aud">The audience of the session</param> /// <param name="remoteEp">The remote endpoint from which the session is created</param> /// <param name="policyDemands">The policies which are being demanded for this session</param> /// <param name="purposeOfUse">The purpose of this session</param> /// <returns>A constructed <see cref="global::ThisAssembly:AdoSession"/></returns> public ISession Establish(IPrincipal principal, String remoteEp, bool isOverride, String purpose, String[] policyDemands, string lang) { // Validate the parameters if (principal == null) { throw new ArgumentNullException(nameof(principal)); } else if (!principal.Identity.IsAuthenticated) { throw new InvalidOperationException("Cannot create a session for a non-authenticated principal"); } else if (!(principal is IClaimsPrincipal)) { throw new ArgumentException("Principal must be ClaimsPrincipal", nameof(principal)); } else if (isOverride && (String.IsNullOrEmpty(purpose) || policyDemands == null || policyDemands.Length == 0)) { throw new InvalidOperationException("Override requests require policy demands and a purpose of use"); } var cprincipal = principal as IClaimsPrincipal; try { using (var context = this.m_configuration.Provider.GetWriteConnection()) { context.Open(); using (var tx = context.BeginTransaction()) { var refreshToken = this.CreateRefreshToken(); var applicationKey = cprincipal.FindFirst(SanteDBClaimTypes.SanteDBApplicationIdentifierClaim)?.Value; var deviceKey = cprincipal.FindFirst(SanteDBClaimTypes.SanteDBDeviceIdentifierClaim)?.Value; var userKey = cprincipal.FindFirst(SanteDBClaimTypes.Sid).Value; var expiration = DateTimeOffset.Now.Add(this.m_securityConfiguration.GetSecurityPolicy <TimeSpan>(SecurityPolicyIdentification.SessionLength, new TimeSpan(0, 5, 0))); if ((purpose == PurposeOfUseKeys.SecurityAdmin.ToString() || cprincipal.Claims.Any(o => o.Type == SanteDBClaimTypes.PurposeOfUse && o.Value == PurposeOfUseKeys.SecurityAdmin.ToString())) && policyDemands?.Contains(PermissionPolicyIdentifiers.LoginPasswordOnly) == true) // TODO: Make purpose of use menmonic check instead { expiration = DateTimeOffset.Now.Add(new TimeSpan(0, 2, 0)); } var dbSession = new DbSession() { DeviceKey = deviceKey != null ? (Guid?)Guid.Parse(deviceKey) : null, ApplicationKey = Guid.Parse(applicationKey), UserKey = userKey != null && userKey != deviceKey ? (Guid?)Guid.Parse(userKey) : null, NotBefore = DateTimeOffset.Now, NotAfter = expiration, RefreshExpiration = DateTimeOffset.Now.Add(this.m_securityConfiguration.GetSecurityPolicy <TimeSpan>(SecurityPolicyIdentification.RefreshLength, new TimeSpan(0, 10, 0))), RemoteEndpoint = remoteEp, RefreshToken = ApplicationServiceContext.Current.GetService <IPasswordHashingService>().ComputeHash(BitConverter.ToString(refreshToken).Replace("-", "")) }; if (dbSession.ApplicationKey == dbSession.UserKey) // SID == Application = Application Grant { dbSession.UserKey = null; } dbSession = context.Insert(dbSession); // Setup claims var claims = cprincipal.Claims.ToList(); // Did the caller explicitly set policies? var pdp = ApplicationServiceContext.Current.GetService <IPolicyDecisionService>(); // Is the principal only valid for pwd reset? if (cprincipal.HasClaim(o => o.Type == SanteDBClaimTypes.SanteDBScopeClaim)) // Allow the createor to specify { ; } else if (policyDemands?.Length > 0) { if (isOverride) { claims.Add(new SanteDBClaim(SanteDBClaimTypes.SanteDBOverrideClaim, "true")); } if (!String.IsNullOrEmpty(purpose)) { claims.Add(new SanteDBClaim(SanteDBClaimTypes.PurposeOfUse, purpose)); } foreach (var pol in policyDemands) { // Get grant var grant = pdp.GetPolicyOutcome(cprincipal, pol); if (isOverride && grant == PolicyGrantType.Elevate && (pol.StartsWith(PermissionPolicyIdentifiers.SecurityElevations) || // Special security elevations don't require override permission pdp.GetPolicyOutcome(cprincipal, PermissionPolicyIdentifiers.OverridePolicyPermission) == PolicyGrantType.Grant )) // We are attempting to override { claims.Add(new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, pol)); } else if (grant == PolicyGrantType.Grant) { claims.Add(new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, pol)); } else { throw new PolicyViolationException(cprincipal, pol, grant); } } } claims.Add(new SanteDBClaim(SanteDBClaimTypes.SanteDBSessionIdClaim, dbSession.Key.ToString())); (cprincipal.Identity as IClaimsIdentity).AddClaim(claims.Last()); // Add default policies var oizPrincipalPolicies = pdp.GetEffectivePolicySet(cprincipal); // Scopes user is allowed to access claims.AddRange(oizPrincipalPolicies.Where(o => o.Rule == PolicyGrantType.Grant).Select(o => new SanteDBClaim(SanteDBClaimTypes.SanteDBScopeClaim, o.Policy.Oid))); if (!String.IsNullOrEmpty(lang)) // set language { claims.Add(new SanteDBClaim(SanteDBClaimTypes.Language, lang)); } // Claims? foreach (var clm in claims.Where(o => !m_nonSessionClaims.Contains(o.Type))) { context.Insert(new DbSessionClaim() { SessionKey = dbSession.Key, ClaimType = clm.Type, ClaimValue = clm.Value, }); } tx.Commit(); var signingService = ApplicationServiceContext.Current.GetService <IDataSigningService>(); if (signingService == null) { this.m_traceSource.TraceWarning("No IDataSigningService provided. Session data will be unsigned!"); var session = new AdoSecuritySession(dbSession.Key, dbSession.Key.ToByteArray(), refreshToken, dbSession.NotBefore, dbSession.NotAfter, claims.ToArray()); this.Established?.Invoke(this, new SessionEstablishedEventArgs(principal, session, true, isOverride, purpose, policyDemands)); this.m_adhocCache?.Add($"ses.{session.Key}", session); return(session); } else { var signedToken = dbSession.Key.ToByteArray().Concat(signingService.SignData(dbSession.Key.ToByteArray())).ToArray(); var signedRefresh = refreshToken.Concat(signingService.SignData(refreshToken)).ToArray(); var session = new AdoSecuritySession(dbSession.Key, signedToken, signedRefresh, dbSession.NotBefore, dbSession.NotAfter, claims.ToArray()); this.Established?.Invoke(this, new SessionEstablishedEventArgs(principal, session, true, isOverride, purpose, policyDemands)); this.m_adhocCache?.Add($"ses.{session.Key}", session); return(session); } } } } catch (Exception e) { this.m_traceSource.TraceError("Error establishing session: {0}", e.Message); this.Established?.Invoke(this, new SessionEstablishedEventArgs(principal, null, false, isOverride, purpose, policyDemands)); throw new SecurityException("Error establishing session", e); } }