730 lines · 31.6 KB · C#
RawDownload
//******************************************************************************************************//  SessionHandler.cs - Gbtc////  Copyright © 2017, Grid Protection Alliance.  All Rights Reserved.////  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See//  the NOTICE file distributed with this work for additional information regarding copyright ownership.//  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may//  not use this file except in compliance with the License. You may obtain a copy of the License at:////      http://opensource.org/licenses/MIT////  Unless agreed to in writing, the subject software distributed under the License is distributed on an//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the//  License for the specific language governing permissions and limitations.////  Code Modification History://  ----------------------------------------------------------------------------------------------------//  08/24/2017 - J. Ritchie Carroll//       Generated original version of source code.////****************************************************************************************************** using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.Diagnostics.CodeAnalysis;using System.IO;using System.Linq;using System.Net.Http;using System.Net.Http.Headers;using System.Security;using System.Security.Cryptography;using System.Text;using System.Threading;using System.Threading.Tasks;using System.Timers;using System.Web.Http;using GSF.Collections;using GSF.Configuration;using GSF.Diagnostics;using GSF.IO;using GSF.Security;using Microsoft.AspNet.SignalR;using Microsoft.Owin;using RazorEngine.Templating;using Timer = System.Timers.Timer;using Random = GSF.Security.Cryptography.Random; #pragma warning disable SG0015 // Validated - no hard-coded password present namespace GSF.Web.Security{    /// <summary>    /// Represents an HTTP messaging handler that can inject a session ID that can be used for security and user state.    /// </summary>    public class SessionHandler : DelegatingHandler    {        #region [ Members ]         // Nested Types        private class Session        {            public readonly DynamicViewBag State = new();            public DateTime LastAccess;        }         private class Credential        {            #region [ Constructors ]             public Credential()            {            }             // Note: Usage is dynamically called via FileBackedDictionary            // ReSharper disable once UnusedMember.Local            [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]            public Credential(Stream stream)            {                try                {                    using BinaryReader reader = new(stream, Encoding.Unicode, true);                    int length = reader.ReadInt32();                    byte[] protectedCredentials = reader.ReadBytes(length);                    byte[] credentials = ProtectedData.Unprotect(protectedCredentials, null, DataProtectionScope.CurrentUser);                     using MemoryStream memStream = new(credentials);                    using BinaryReader memReader = new(memStream, Encoding.Unicode, true);                     Validator = memReader.ReadString();                    Username = memReader.ReadString();                    Password = memReader.ReadString();                    Expiration = new DateTime(memReader.ReadInt64());                }                catch (Exception ex)                {                    // Invalidate cache entry for decrypt failures (service could be running as different user)                    Validator = Username = Password = null;                    Expiration = DateTime.MinValue;                    Logger.SwallowException(ex);                }            }             #endregion             #region [ Properties ]             public string Validator { get; set; }             public string Username { get; set; }             public string Password { get; set; }             public DateTime Expiration { get; set; }             #endregion             #region [ Methods ]             // Note: Usage is dynamically called via FileBackedDictionary            // ReSharper disable once UnusedMember.Local            [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]            public void WriteTo(Stream stream)            {                using MemoryStream memStream = new();                using BinaryWriter memWriter = new(memStream, Encoding.Unicode, true);                                memWriter.Write(Validator);                memWriter.Write(Username);                memWriter.Write(Password);                memWriter.Write(Expiration.Ticks);                 byte[] credentials = memStream.ToArray();                byte[] protectedCredentials = ProtectedData.Protect(credentials, null, DataProtectionScope.CurrentUser);                 using BinaryWriter writer = new(stream, Encoding.Unicode, true);                writer.Write(protectedCredentials.Length);            }             #endregion        }         // Constants         /// <summary>        /// Default value for <see cref="SessionTimeout"/>.        /// </summary>        public const double DefaultSessionTimeout = 20.0D;         /// <summary>        /// Default value for <see cref="SessionMonitorInterval"/>.        /// </summary>        public const double DefaultSessionMonitorInterval = 60000.0D;         /// <summary>        /// Default value for a cached session expiration, in days.        /// </summary>        public const double DefaultSessionExpirationDays = 30.0D;         /// <summary>        /// Default value for <see cref="AuthenticationToken"/>;        /// </summary>        public const string DefaultAuthenticationToken = "x-gsf-auth";         /// <summary>        /// Default value for <see cref="SessionToken"/>;        /// </summary>        public const string DefaultSessionToken = "x-gsf-session";         #endregion         #region [ Constructors ]         /// <summary>        /// Creates a new <see cref="SessionHandler"/>.        /// </summary>        /// <param name="authenticationToken">Token used for identifying the authentication token in cookie headers.</param>        /// <param name="sessionToken">Token used for identifying the session ID in cookie headers.</param>        public SessionHandler(string authenticationToken = DefaultAuthenticationToken, string sessionToken = DefaultSessionToken)        {            AuthenticationToken = authenticationToken;            SessionToken = sessionToken;        }         #endregion         #region [ Properties ]         /// <summary>        /// Gets the token used for identifying the authentication token in cookie headers.        /// </summary>        public string AuthenticationToken { get; }         /// <summary>        /// Gets the token used for identifying the session ID in cookie headers.        /// </summary>        public string SessionToken { get; }         #endregion         #region [ Methods ]         /// <summary>        /// Modifies the HTTP request message pipeline to include a session identifier.        /// </summary>        /// <param name="request">The HTTP request message to send to the server.</param>        /// <param name="cancellationToken">A cancellation token to cancel operation.</param>        /// <returns>HTTP response message.</returns>        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)        {            // Get existing session data from the request, if any            CookieHeaderValue cookie = request.Headers.GetCookies(SessionToken).FirstOrDefault();            string sessionCookieValue = cookie?[SessionToken].Value;             // If session ID format is invalid, create a new one            if (!Guid.TryParse(sessionCookieValue, out Guid sessionID))                sessionID = Guid.NewGuid();             sessionCookieValue = sessionID.ToString();             // Save session ID (as Guid) in the request properties            request.Properties[SessionToken] = sessionID;             // Continue processing the HTTP request            HttpResponseMessage response = await base.SendAsync(request, cancellationToken);             // Store session ID in response message cookie            response.Headers.AddCookies(new[] { new CookieHeaderValue(SessionToken, sessionCookieValue) { Path = "/" } });             // Get authentication options associated with this request            ReadonlyAuthenticationOptions options = request.GetAuthenticationOptions();             // If requesting the AuthTest page using BASIC authentication, reissue the client's authentication token            string authTestPage = options.AuthTestPage;             if (request.RequestUri.LocalPath == authTestPage)            {                if (request.Method != HttpMethod.Post)                    throw new SecurityException("Authorization test only operates with validated HTTP post operation");                 SecurityPrincipal securityPrincipal = request.GetRequestContext().Principal as SecurityPrincipal;                SecurityIdentity securityIdentity = securityPrincipal?.Identity;                ISecurityProvider securityProvider = securityIdentity?.Provider;                 string username = securityIdentity?.Name;                string password = securityProvider?.Password;                 if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))                {                    string authenticationToken = IssueAuthenticationToken(username, password);                     InvalidateAuthenticationToken(request);                     response.Headers.AddCookies(new[]                    {                        new CookieHeaderValue(AuthenticationToken, authenticationToken)                        {                            Path = request.RequestUri.LocalPath,                            MaxAge = TimeSpan.FromDays(s_sessionExpirationDays)                        }                    });                }                 // AuthTest page should always have a valid request verification token                request.ValidateRequestVerificationToken(options);            }             return response;        }         private string IssueAuthenticationToken(string username, string password)        {            byte[] buffer = new byte[9];             // Generate the selector for the token            Random.GetBytes(buffer);            string selector = Convert.ToBase64String(buffer);             // Generate the validator for the token            Random.GetBytes(buffer);            string validator = Convert.ToBase64String(buffer);             if (s_sessionCredentialCache is not null)            {                // Open the credential cache                lock (s_sessionCredentialCacheLock)                {                    using FileBackedDictionary<string, Credential> credentialCache = new(s_sessionCredentialCache);                     // Clean out expired credentials before issuing a new one                    DateTime now = DateTime.UtcNow;                     List<string> expiredSelectors = credentialCache                        .Where(kvp => now >= kvp.Value.Expiration)                        .Select(kvp => kvp.Key)                        .ToList();                     if (expiredSelectors.Count > 0)                    {                        foreach (string expiredSelector in expiredSelectors)                            credentialCache.Remove(expiredSelector);                         credentialCache.Compact();                    }                     // Enter the new token into the credential cache                    credentialCache[selector] = new Credential                    {                        Validator = validator,                        Username = username,                        Password = password,                        Expiration = DateTime.UtcNow.AddDays(s_sessionExpirationDays)                    };                }            }             return $"{selector}:{validator}";        }         private void InvalidateAuthenticationToken(HttpRequestMessage request)        {            // Get the authentication token provided by the client in the request message            string authenticationToken = GetAuthenticationTokenFromCookie(request, AuthenticationToken);             if (authenticationToken is null)                return;             string selector = authenticationToken.Split(':')[0];             if (s_sessionCredentialCache is null)                return;             // Open the credential cache            lock (s_sessionCredentialCacheLock)            {                using FileBackedDictionary<string, Credential> credentialCache = new(s_sessionCredentialCache);                credentialCache.Remove(selector);            }        }         #endregion         #region [ Static ]         // Static Events         /// <summary>        /// Raised when a client session is being expired.        /// </summary>        public static event EventHandler<EventArgs<Guid, DynamicViewBag>> SessionExpired;         /// <summary>        /// Occurs when a static method encounters an exception.        /// </summary>        public static event EventHandler<EventArgs<Exception>> ProcessException;         // Static Fields        private static readonly Timer s_sessionCacheMonitor;        private static readonly ConcurrentDictionary<Guid, Session> s_sessionCache;        private static readonly string s_sessionCredentialCache;        private static readonly object s_sessionCredentialCacheLock;        private static readonly double s_sessionExpirationDays;         // Static Constructor        static SessionHandler()        {            CategorizedSettingsElementCollection settings = ConfigurationFile.Current.Settings["systemSettings"];             settings.Add("SessionTimeout", DefaultSessionTimeout, "The timeout, in minutes, for which inactive client sessions will be expired and removed from the cache.");            settings.Add("SessionMonitorInterval", DefaultSessionMonitorInterval, "The interval, in milliseconds, over which the client session cache will be evaluated for expired sessions.");            settings.Add("ConfigurationCachePath", $"{FilePath.GetAbsolutePath("")}{Path.DirectorySeparatorChar}ConfigurationCache{Path.DirectorySeparatorChar}", "Defines the path used to cache serialized phasor protocol configurations");            settings.Add("SessionCredentialCacheEnabled", true, "Defines flag that determines if session credentials cache is enabled");            settings.Add("SessionCredentialCache", Path.Combine(settings["ConfigurationCachePath"].Value, "CredentialCache.bin"), "Path and file name of session credentials cache");            settings.Add("SessionExpirationDays", s_sessionExpirationDays, "Default number of days for session expirations");             SessionTimeout = settings["SessionTimeout"].ValueAs(DefaultSessionTimeout);            SessionMonitorInterval = settings["SessionMonitorInterval"].ValueAs(DefaultSessionMonitorInterval);             s_sessionCacheMonitor = new Timer(SessionMonitorInterval);            s_sessionCacheMonitor.Elapsed += s_sessionCacheMonitor_Elapsed;            s_sessionCacheMonitor.Enabled = false;            s_sessionCache = new ConcurrentDictionary<Guid, Session>();             if (settings["SessionCredentialCacheEnabled"].ValueAs(true))            {                s_sessionCredentialCache = settings["SessionCredentialCache"].Value;                s_sessionCredentialCacheLock = new object();            }             s_sessionExpirationDays = settings["SessionExpirationDays"].ValueAs(DefaultSessionExpirationDays);        }         // Static Properties         /// <summary>        /// Gets timeout, in minutes, for which inactive client sessions will be expired and removed from the cache.        /// </summary>        public static double SessionTimeout { get; }         /// <summary>        /// Gets interval, in milliseconds, over which the client session cache will be evaluated for expired sessions.        /// </summary>        public static double SessionMonitorInterval { get; }         // Static Methods         /// <summary>        /// Clears any cached session for the specified <paramref name="sessionID"/>.        /// </summary>        /// <param name="sessionID">Identifier of session to clear.</param>        /// <returns><c>true</c> if session was found and cleared; otherwise, <c>false</c>.</returns>        public static bool ClearSessionCache(Guid sessionID)        {            if (!s_sessionCache.TryRemove(sessionID, out Session session))                return false;             OnSessionExpired(sessionID, session.State);            return true;        }         /// <summary>        /// Attempts to use the authentication token to retrieve the user's credentials from the credential cache.        /// </summary>        /// <param name="authenticationToken">The token used to retrieve the user's credentials.</param>        /// <param name="username">The username of the user.</param>        /// <param name="password">THe user's password.</param>        /// <returns>True if the user's credentials were successfully retrieved; false otherwise.</returns>        public static bool TryGetCachedCredentials(string authenticationToken, out string username, out string password)        {            try            {                if (s_sessionCredentialCache is not null)                {                    // Parse the selector and validator from the authentication token                    string[] authenticationTokenParts = authenticationToken.Split(':');                     if (authenticationTokenParts.Length != 2)                    {                        username = null;                        password = null;                        return false;                    }                     string selector = authenticationTokenParts[0];                    string validator = authenticationTokenParts[1];                     // Read the credential cache to retrieve the user's                    // credentials that were mapped to this authentication token                    lock (s_sessionCredentialCacheLock)                    {                        using FileBackedDictionary<string, Credential> credentialCache = new(s_sessionCredentialCache);                         if (credentialCache.TryGetValue(selector, out Credential credential) && validator == credential.Validator && DateTime.UtcNow < credential.Expiration)                        {                            username = credential.Username;                            password = credential.Password;                            return username is not null;                        }                    }                }            }            catch (Exception ex)            {                OnProcessException(ex);            }             username = null;            password = null;            return false;        }         /// <summary>        /// Tries to get the session ID Guid as defined in the request properties dictionary.        /// </summary>        /// <param name="request">The target HTTP request message.</param>        /// <param name="sessionToken">Token used for identifying the session ID in properties dictionary.</param>        /// <param name="sessionID">Validated session ID to return.</param>        /// <returns><c>true</c> if session ID was successfully accessed; otherwise, <c>false</c>.</returns>        public static bool TryGetSessionID(HttpRequestMessage request, string sessionToken, out Guid sessionID)        {            if (request.Properties.TryGetValue(sessionToken ?? DefaultSessionToken, out object result) && result is Guid guid)            {                sessionID = guid;                return true;            }             sessionID = Guid.Empty;            return false;        }         /// <summary>        /// Tries to get the session state as defined in the request properties dictionary.        /// </summary>        /// <param name="request">The target HTTP request message.</param>        /// <param name="sessionToken">Token used for identifying the session ID in properties dictionary.</param>        /// <param name="sessionState">Session state to return.</param>        /// <returns><c>true</c> if session state was successfully accessed; otherwise, <c>false</c>.</returns>        public static bool TryGetSessionState(HttpRequestMessage request, string sessionToken, out DynamicViewBag sessionState)        {

Lines 1–500 of 730.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t