public void TestInitialize() { var storageBuilder = new StorageCreationPropertiesBuilder( Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath)); storageBuilder = storageBuilder.WithMacKeyChain( serviceName: "Microsoft.Developer.IdentityService.Test", accountName: "MSALCacheTest"); // unit tests run on Linux boxes without LibSecret storageBuilder.WithLinuxUnprotectedFile(); // 1. Use MSAL to create an instance of the Public Client Application var app = PublicClientApplicationBuilder.Create(ClientId) .Build(); // 3. Create the high level MsalCacheHelper based on properties and a logger _cacheHelper = MsalCacheHelper.CreateAsync( storageBuilder.Build(), new TraceSource("MSAL.CacheExtension.Test")) .GetAwaiter().GetResult(); // 4. Let the cache helper handle MSAL's cache _cacheHelper.RegisterCache(app.UserTokenCache); }
public void UnprotectedOptionMutuallyExclusiveWithOtherOptions() { var builder = new StorageCreationPropertiesBuilder( Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath)); builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); builder.WithUnprotectedFile(); AssertException.Throws <ArgumentException>(() => builder.Build()); builder = new StorageCreationPropertiesBuilder( Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath)); builder = builder.WithLinuxKeyring( schemaName: "msal.cache", collection: "default", secretLabel: "MSALCache", attribute1: new KeyValuePair <string, string>("MsalClientID", "Microsoft.Developer.IdentityService"), attribute2: new KeyValuePair <string, string>("MsalClientVersion", "1.0.0.0")); builder.WithUnprotectedFile(); AssertException.Throws <ArgumentException>(() => builder.Build()); builder = new StorageCreationPropertiesBuilder( Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath)); builder.WithLinuxUnprotectedFile(); builder.WithUnprotectedFile(); AssertException.Throws <ArgumentException>(() => builder.Build()); }
/// <inheritdoc /> public SharedTokenCacheProvider(IConfiguration config = null, ILogger logger = null) { _logger = logger; _config = config ?? new ConfigurationBuilder().AddEnvironmentVariables().Build(); const string serviceName = "Microsoft.Developer.IdentityService"; const string clientId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"; var storageCreationPropertiesBuilder = new StorageCreationPropertiesBuilder( Path.GetFileName(s_cacheFilePath), Path.GetDirectoryName(s_cacheFilePath), clientId) .WithMacKeyChain(serviceName: serviceName, accountName: "MSALCache") .WithLinuxKeyring( schemaName: "msal.cache", collection: "default", secretLabel: "MSALCache", attribute1: new KeyValuePair <string, string>("MsalClientID", serviceName), attribute2: new KeyValuePair <string, string>("MsalClientVersion", "1.0.0.0")); var authority = string.Format(CultureInfo.InvariantCulture, AadAuthority.AadCanonicalAuthorityTemplate, AadAuthority.DefaultTrustedHost, "common"); _app = PublicClientApplicationBuilder .Create(clientId) .WithAuthority(new Uri(authority)) .Build(); var cacheStore = new MsalCacheStorage(storageCreationPropertiesBuilder.Build()); _cacheHelper = new MsalCacheHelper(_app.UserTokenCache, cacheStore); _cacheHelper.RegisterCache(_app.UserTokenCache); }
public static void ClassInitialize(TestContext _) { var builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath), "ClientIDGoesHere"); builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); // Tests run on machines without Libsecret builder = builder.WithLinuxUnprotectedFile(); s_storageCreationProperties = builder.Build(); }
public static void ClassInitialize(TestContext _) { var builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath), "ClientIDGoesHere"); builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); builder = builder.WithLinuxKeyring( schemaName: "msal.cache", collection: "default", secretLabel: "MSALCache", attribute1: new KeyValuePair<string, string>("MsalClientID", "Microsoft.Developer.IdentityService"), attribute2: new KeyValuePair<string, string>("MsalClientVersion", "1.0.0.0")); s_storageCreationProperties = builder.Build(); }
/// <summary> /// Gets an aptly configured instance of the <see cref="MsalCacheStorage" /> class. /// </summary> /// <returns>An aptly configured instance of the <see cref="MsalCacheStorage" /> class.</returns> private MsalCacheHelper GetMsalCacheStorage() { StorageCreationPropertiesBuilder builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath), ClientId); builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); builder = builder.WithLinuxKeyring( schemaName: "msal.cache", collection: "default", secretLabel: "MSALCache", attribute1: new KeyValuePair <string, string>("MsalClientID", "Microsoft.Developer.IdentityService"), attribute2: new KeyValuePair <string, string>("MsalClientVersion", "1.0.0.0")); return(MsalCacheHelper.CreateAsync(builder.Build(), new TraceSource("Partner Center PowerShell")).ConfigureAwait(false).GetAwaiter().GetResult()); }
/// <summary> /// Gets an aptly configured instance of the <see cref="MsalCacheStorage" /> class. /// </summary> /// <returns>An aptly configured instance of the <see cref="MsalCacheStorage" /> class.</returns> private MsalCacheStorage GetMsalCacheStorage() { StorageCreationPropertiesBuilder builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath), ClientId); builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); builder = builder.WithLinuxKeyring( schemaName: "msal.cache", collection: "default", secretLabel: "MSALCache", attribute1: new KeyValuePair <string, string>("MsalClientID", "Microsoft.Developer.IdentityService"), attribute2: new KeyValuePair <string, string>("MsalClientVersion", "1.0.0.0")); return(new MsalCacheStorage(builder.Build())); }
public void ImportExport() { // Arrange var cacheAccessor = NSubstitute.Substitute.For <ICacheAccessor>(); var cache = new MockTokenCache(); var storage = new Storage( _storageCreationPropertiesBuilder.Build(), cacheAccessor, new TraceSourceLogger(new TraceSource("ts"))); var helper = new MsalCacheHelper(cache, storage, _logger); byte[] dataToSave = Encoding.UTF8.GetBytes("Hello World 2"); cacheAccessor.Read().Returns(Encoding.UTF8.GetBytes("Hello World")); // Act byte[] actualData = helper.LoadUnencryptedTokenCache(); helper.SaveUnencryptedTokenCache(dataToSave); // Assert Assert.AreEqual("Hello World", Encoding.UTF8.GetString(actualData)); cacheAccessor.Received().Write(dataToSave); }
private async Task <MsalCacheHelper> GetMsalCacheHelperAsync() { // There are options to set up the cache correctly using StorageCreationProperties on other OS's but that will need to be tested // for now only support windows if (helper == null && this.cacheEnabled && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var fileName = Path.GetFileName(cacheLocation); var directory = Path.GetDirectoryName(cacheLocation); var builder = new StorageCreationPropertiesBuilder(fileName, directory, this.clientId); StorageCreationProperties creationProps = builder.Build(); helper = await MsalCacheHelper.CreateAsync(creationProps); } return(helper); }
private static MsalCacheHelper InitializeCacheHelper(string clientId) { StorageCreationPropertiesBuilder builder = new StorageCreationPropertiesBuilder(Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath), clientId); builder = builder.WithMacKeyChain(serviceName: "Microsoft.Developer.IdentityService", accountName: "MSALCache"); builder = builder.WithLinuxKeyring( schemaName: "msal.cache", collection: "default", secretLabel: "MSALCache", attribute1: new KeyValuePair <string, string>("MsalClientID", "Microsoft.Developer.IdentityService"), attribute2: new KeyValuePair <string, string>("MsalClientVersion", "1.0.0.0")); StorageCreationProperties storageCreationProperties = builder.Build(); return(MsalCacheHelper.CreateAsync(storageCreationProperties).ConfigureAwait(false).GetAwaiter().GetResult()); }
internal static (string[], IPublicClientApplication, MsalCacheHelper) GetPublicClient( string resource, string tenant, Uri baseAuthority, bool validateAuthority, string clientId, string cacheFilename, string cacheDirectory, string serviceName, string accountName) { // tenant can be null resource = resource ?? throw new ArgumentNullException(nameof(resource)); Console.WriteLine($"Using resource: '{resource}', tenant:'{tenant}'"); var scopes = new string[] { resource + "/.default" }; Console.WriteLine($"Using scopes: '{string.Join(",", scopes)}'"); var authority = $"{baseAuthority.AbsoluteUri}{tenant}"; Console.WriteLine($"GetPublicClient for authority: '{authority}' ValidateAuthority: '{validateAuthority}'"); Uri authorityUri = new Uri(authority); var appBuilder = PublicClientApplicationBuilder.Create(clientId).WithAuthority(authorityUri, validateAuthority); var app = appBuilder.Build(); Console.WriteLine($"Built public client"); var storageCreationPropsBuilder = new StorageCreationPropertiesBuilder(cacheFilename, cacheDirectory); storageCreationPropsBuilder = storageCreationPropsBuilder.WithMacKeyChain(serviceName, accountName); var storageCreationProps = storageCreationPropsBuilder.Build(); // This hooks up our custom cache onto the one used by MSAL var cacheHelper = new MsalCacheHelper(storageCreationProps); cacheHelper.RegisterCache(app.UserTokenCache); Console.WriteLine($"Cache registered"); return(scopes, app, cacheHelper); }
internal SharedTokenCacheProvider(StorageCreationPropertiesBuilder builder, IConfiguration config = null, ILogger logger = null) { _logger = logger; _config = config ?? new ConfigurationBuilder().AddEnvironmentVariables().Build(); var authority = _config.GetValue <string>(Constants.AadAuthorityEnvName) ?? string.Format(CultureInfo.InvariantCulture, AadAuthority.AadCanonicalAuthorityTemplate, AadAuthority.DefaultTrustedHost, "common"); _app = PublicClientApplicationBuilder .Create(AzureCliClientId) .WithAuthority(new Uri(authority)) .Build(); var cacheStore = new MsalCacheStorage(builder.Build()); _cacheHelper = new MsalCacheHelper(_app.UserTokenCache, cacheStore); _cacheHelper.RegisterCache(_app.UserTokenCache); }
public void ImportExport() { var storageBuilder = new StorageCreationPropertiesBuilder( Path.GetFileName(CacheFilePath), Path.GetDirectoryName(CacheFilePath), ClientId); storageBuilder = storageBuilder.WithMacKeyChain( serviceName: "Microsoft.Developer.IdentityService.Test", accountName: "MSALCacheTest"); // unit tests run on Linux boxes without LibSecret storageBuilder.WithLinuxUnprotectedFile(); // 1. Use MSAL to create an instance of the Public Client Application var app = PublicClientApplicationBuilder.Create(ClientId) .Build(); // 3. Create the high level MsalCacheHelper based on properties and a logger _cacheHelper = MsalCacheHelper.CreateAsync( storageBuilder.Build(), new TraceSource("MSAL.CacheExtension.Test")) .GetAwaiter().GetResult(); // 4. Let the cache helper handle MSAL's cache _cacheHelper.RegisterCache(app.UserTokenCache); // Act string dataString = "Hello World"; byte[] dataBytes = Encoding.UTF8.GetBytes(dataString); var result = _cacheHelper.LoadUnencryptedTokenCache(); Assert.AreEqual(0, result.Length); _cacheHelper.SaveUnencryptedTokenCache(dataBytes); byte[] actualData = _cacheHelper.LoadUnencryptedTokenCache(); Assert.AreEqual(dataString, Encoding.UTF8.GetString(actualData)); }
private StorageCreationProperties CreateTokenCacheProps(string clientId, bool useLinuxFallback) { const string cacheFileName = "msal.cache"; string cacheDirectory; if (PlatformUtils.IsWindows()) { // The shared MSAL cache is located at "%LocalAppData%\.IdentityService\msal.cache" on Windows. cacheDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".IdentityService" ); } else { // The shared MSAL cache metadata is located at "~/.local/.IdentityService/msal.cache" on UNIX. cacheDirectory = Path.Combine(Context.FileSystem.UserHomePath, ".local", ".IdentityService"); } // The keychain is used on macOS with the following service & account names var builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, clientId) .WithMacKeyChain("Microsoft.Developer.IdentityService", "MSALCache"); if (useLinuxFallback) { builder.WithLinuxUnprotectedFile(); } else { // The SecretService/keyring is used on Linux with the following collection name and attributes builder.WithLinuxKeyring(cacheFileName, "default", "MSALCache", new KeyValuePair <string, string>("MsalClientID", "Microsoft.Developer.IdentityService"), new KeyValuePair <string, string>("Microsoft.Developer.IdentityService", "1.0.0.0")); } return(builder.Build()); }
private static void StoreOtherSecret() { var storageProperties = new StorageCreationPropertiesBuilder( Config.CacheFileName + ".other_secrets", Config.CacheDir) .WithMacKeyChain( Config.KeyChainServiceName + ".other_secrets", Config.KeyChainAccountName) .WithLinuxKeyring( Config.LinuxKeyRingSchema, Config.LinuxKeyRingCollection, Config.LinuxKeyRingLabel, Config.LinuxKeyRingAttr1, new KeyValuePair <string, string>("other_secrets", "secret_description")); Storage storage = Storage.Create(storageProperties.Build()); byte[] secretBytes = Encoding.UTF8.GetBytes("secret"); using (new CrossPlatLock(Config.CacheFileName + ".other_secrets.lock")) { Console.WriteLine("Writing..."); storage.WriteData(secretBytes); Console.WriteLine("Writing again..."); storage.WriteData(secretBytes); Console.WriteLine("Reading..."); var data = storage.ReadData(); Console.WriteLine("Read: " + Encoding.UTF8.GetString(data)); Console.WriteLine("Deleting..."); storage.Clear(); } }
public void MultiAccessSerializationAsync() { var cache1 = new MockTokenCache(); var helper1 = new MsalCacheHelper( cache1, new MsalCacheStorage(_storageCreationPropertiesBuilder.Build(), _logger), _logger); var cache2 = new MockTokenCache(); var helper2 = new MsalCacheHelper( cache2, new MsalCacheStorage(_storageCreationPropertiesBuilder.Build(), _logger), _logger); //Test signalling thread 1 var resetEvent1 = new ManualResetEventSlim(initialState: false); //Test signalling thread 2 var resetEvent2 = new ManualResetEventSlim(initialState: false); //Thread 1 signalling test var resetEvent3 = new ManualResetEventSlim(initialState: false); // Thread 2 signalling test var resetEvent4 = new ManualResetEventSlim(initialState: false); var thread1 = new Thread(() => { var args = new TokenCacheNotificationArgs(cache1, string.Empty, null, false); helper1.BeforeAccessNotification(args); resetEvent3.Set(); resetEvent1.Wait(); helper1.AfterAccessNotification(args); }); var thread2 = new Thread(() => { var args = new TokenCacheNotificationArgs(cache2, string.Empty, null, false); helper2.BeforeAccessNotification(args); resetEvent4.Set(); resetEvent2.Wait(); helper2.AfterAccessNotification(args); resetEvent4.Set(); }); // Let thread 1 start and get the lock thread1.Start(); resetEvent3.Wait(); // Start thread 2 and give it enough time to get blocked on the lock thread2.Start(); Thread.Sleep(5000); // Make sure helper1 has the lock still, and helper2 doesn't Assert.IsNotNull(helper1.CacheLock); Assert.IsNull(helper2.CacheLock); // Allow thread1 to give up the lock, and wait for helper2 to get it resetEvent1.Set(); resetEvent4.Wait(); resetEvent4.Reset(); // Make sure helper1 gave it up properly, and helper2 now owns the lock Assert.IsNull(helper1.CacheLock); Assert.IsNotNull(helper2.CacheLock); // Allow thread2 to give up the lock, and wait for it to complete resetEvent2.Set(); resetEvent4.Wait(); // Make sure thread2 cleaned up after itself as well Assert.IsNull(helper2.CacheLock); }
#pragma warning disable UseAsyncSuffix // Use Async suffix static async Task Main(string[] args) #pragma warning restore UseAsyncSuffix // Use Async suffix { // It's recommended to create a separate PublicClient Application for each tenant // but only one CacheHelper object var pca = CreatePublicClient("https://login.microsoftonline.com/organizations"); var cacheHelper = await CreateCacheHelperAsync().ConfigureAwait(false); cacheHelper.RegisterCache(pca.UserTokenCache); // Advanced scenario for when 2 or more apps share the same cache cacheHelper.CacheChanged += (s, e) => // this event is very expensive perf wise { Console.BackgroundColor = ConsoleColor.DarkCyan; Console.WriteLine($"Cache Changed, Added: {e.AccountsAdded.Count()} Removed: {e.AccountsRemoved.Count()}"); Console.ResetColor(); }; AuthenticationResult result; while (true) { // Display menu Console.WriteLine($@" 1. Acquire Token using Username and Password - for TEST only, do not use in production! 2. Acquire Token using Device Code Flow 3. Acquire Token Interactive 4. Acquire Token Silent 5. Display Accounts (reads the cache) 6. Acquire Token U/P and Silent in a loop 7. Use persistence layer to read / write any data 8. Use persistence layer to read / write any data with process-level lock c. Clear cache e. Expire Access Tokens (TEST only!) x. Exit app Enter your selection: "); char.TryParse(Console.ReadLine(), out var selection); try { switch (selection) { case '1': // Acquire Token using Username and Password (requires config) // IMPORTANT: you should ALWAYS try to get a token silently first result = await AcquireTokenROPCAsync(pca).ConfigureAwait(false); DisplayResult(result); break; case '2': // Device Code Flow // IMPORTANT: you should ALWAYS try to get a token silently first result = await pca.AcquireTokenWithDeviceCode(Config.Scopes, (dcr) => { Console.BackgroundColor = ConsoleColor.DarkCyan; Console.WriteLine(dcr.Message); Console.ResetColor(); return(Task.FromResult(1)); }).ExecuteAsync().ConfigureAwait(false); DisplayResult(result); break; case '3': // Interactive // IMPORTANT: you should ALWAYS try to get a token silently first result = await pca.AcquireTokenInteractive(Config.Scopes) .ExecuteAsync() .ConfigureAwait(false); DisplayResult(result); break; case '4': // Silent Console.WriteLine("Getting all the accounts. This reads the cache"); var accounts = await pca.GetAccountsAsync().ConfigureAwait(false); var firstAccount = accounts.FirstOrDefault(); // this is expected to fail when account is null result = await pca.AcquireTokenSilent(Config.Scopes, firstAccount) .ExecuteAsync() .ConfigureAwait(false); DisplayResult(result); break; case '5': // Display Accounts Console.Clear(); var accounts2 = await pca.GetAccountsAsync().ConfigureAwait(false); if (!accounts2.Any()) { Console.WriteLine("No accounts were found in the cache."); } foreach (var acc in accounts2) { Console.WriteLine($"Account for {acc.Username}"); } break; case '6': // U/P and Silent in a loop Console.WriteLine("CTRL-C to stop..."); #pragma warning disable CS0618 // Type or member is obsolete cacheHelper.Clear(); #pragma warning restore CS0618 // Type or member is obsolete var pca2 = CreatePublicClient("https://login.microsoftonline.com/organizations"); var pca3 = CreatePublicClient("https://login.microsoftonline.com/organizations"); cacheHelper.RegisterCache(pca2.UserTokenCache); cacheHelper.RegisterCache(pca3.UserTokenCache); while (true) { await Task.WhenAll( RunRopcAndSilentAsync("PCA_1", pca), RunRopcAndSilentAsync("PCA_2", pca2), RunRopcAndSilentAsync("PCA_3", pca3) ).ConfigureAwait(false); Trace.Flush(); await Task.Delay(2000).ConfigureAwait(false); } case '7': var storageProperties = new StorageCreationPropertiesBuilder( Config.CacheFileName + ".other_secrets", Config.CacheDir) .WithMacKeyChain( Config.KeyChainServiceName + ".other_secrets", Config.KeyChainAccountName); Storage storage = Storage.Create(storageProperties.Build()); //string lockFilePath = Path.Combine(Config.CacheDir, Config.CacheFileName + ".other_secrets.lockfile"); byte[] secretBytes = Encoding.UTF8.GetBytes("secret"); Console.WriteLine("Writing..."); storage.WriteData(secretBytes); Console.WriteLine("Writing again..."); storage.WriteData(secretBytes); Console.WriteLine("Reading..."); var data = storage.ReadData(); Console.WriteLine("Read: " + Encoding.UTF8.GetString(data)); Console.WriteLine("Deleting..."); storage.Clear(); break; case '8': storageProperties = new StorageCreationPropertiesBuilder( Config.CacheFileName + ".other_secrets2", Config.CacheDir) .WithMacKeyChain( Config.KeyChainServiceName + ".other_secrets2", Config.KeyChainAccountName); storage = Storage.Create(storageProperties.Build()); string lockFilePath = Path.Combine(Config.CacheDir, Config.CacheFileName + ".lockfile"); using (new CrossPlatLock(lockFilePath)) // cross-process only { secretBytes = Encoding.UTF8.GetBytes("secret"); Console.WriteLine("Writing..."); storage.WriteData(secretBytes); Console.WriteLine("Writing again..."); storage.WriteData(secretBytes); // if another process (not thread!) attempts to read / write this secret // and uses the CrossPlatLock mechanism, it will wait for the lock to be released first await Task.Delay(1000).ConfigureAwait(false); Console.WriteLine("Reading..."); data = storage.ReadData(); Console.WriteLine("Read: " + Encoding.UTF8.GetString(data)); Console.WriteLine("Deleting..."); storage.Clear(); } // lock released break; case 'c': var accounts4 = await pca.GetAccountsAsync().ConfigureAwait(false); foreach (var acc in accounts4) { Console.WriteLine($"Removing account for {acc.Username}"); await pca.RemoveAsync(acc).ConfigureAwait(false); } Console.Clear(); break; case 'e': // This is only meant for testing purposes // do smth that loads the cache first await pca.GetAccountsAsync().ConfigureAwait(false); DateTimeOffset expiredValue = DateTimeOffset.UtcNow.AddMonths(-1); var accessor = pca.UserTokenCache.GetType() .GetRuntimeProperties() .Single(p => p.Name == "Microsoft.Identity.Client.ITokenCacheInternal.Accessor") .GetValue(pca.UserTokenCache); var internalAccessTokens = accessor.GetType().GetMethod("GetAllAccessTokens").Invoke(accessor, new object[] { null }) as IEnumerable <object>; foreach (var internalAt in internalAccessTokens) { internalAt.GetType().GetRuntimeMethods().Single(m => m.Name == "set_ExpiresOn").Invoke(internalAt, new object[] { expiredValue }); accessor.GetType().GetMethod("SaveAccessToken").Invoke(accessor, new[] { internalAt }); } var ctor = typeof(TokenCacheNotificationArgs).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single(); var notificationArgs = ctor.Invoke(new object[] { pca.UserTokenCache, Config.ClientId, null, true, false, true, null, null, null }); var task = pca.UserTokenCache.GetType().GetRuntimeMethods() .Single(m => m.Name == "Microsoft.Identity.Client.ITokenCacheInternal.OnAfterAccessAsync") .Invoke(pca.UserTokenCache, new[] { notificationArgs }); await(task as Task).ConfigureAwait(false); break; case 'x': return; } } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Exception : " + ex); Console.ResetColor(); Console.WriteLine("Hit Enter to continue"); Console.Read(); } } }