From e9f18ac7aaffd4a1f412a9535a61792d12cec9a8 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 09:16:14 -0400 Subject: [PATCH 01/49] okta: add dsso authentication --- src/D2L.Bmx/Browser.cs | 64 +++++++++++ src/D2L.Bmx/D2L.Bmx.csproj | 1 + src/D2L.Bmx/JsonSerializerContext.cs | 1 + src/D2L.Bmx/LoginHandler.cs | 5 +- src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs | 5 + src/D2L.Bmx/Okta/OktaClient.cs | 14 +++ src/D2L.Bmx/OktaAuthenticator.cs | 111 +++++++++++++++++++- src/D2L.Bmx/ParameterDescriptions.cs | 2 + src/D2L.Bmx/PrintHandler.cs | 6 +- src/D2L.Bmx/Program.cs | 19 +++- src/D2L.Bmx/WriteHandler.cs | 6 +- 11 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 src/D2L.Bmx/Browser.cs create mode 100644 src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs new file mode 100644 index 00000000..18072ccc --- /dev/null +++ b/src/D2L.Bmx/Browser.cs @@ -0,0 +1,64 @@ +using PuppeteerSharp; + +namespace D2L.Bmx; + +public class Browser { + + // https://github.com/microsoft/playwright/blob/6763d5ab6bd20f1f0fc879537855a26c7644a496/packages/playwright-core/src/server/registry/index.ts#L630 + private static readonly string[] WindowsEnvironmentVariables = [ + "LOCALAPPDATA", + "PROGRAMFILES", + "PROGRAMFILES(X86)", + ]; + + // https://github.com/microsoft/playwright/blob/6763d5ab6bd20f1f0fc879537855a26c7644a496/packages/playwright-core/src/server/registry/index.ts#L457-L459 + private static readonly string[] WindowsPartialPaths = [ + "\\Google\\Chrome\\Application\\chrome.exe", + "\\Microsoft\\Edge\\Application\\msedge.exe" + ]; + private static readonly string[] MacPaths = [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" + ]; + private static readonly string[] LinuxPaths = [ + "/opt/google/chrome/chrome", + "/opt/microsoft/msedge/msedge" + ]; + + public static async Task LaunchBrowserAsync( bool noSandbox = false ) { + string? browserPath = GetPathToBrowser(); + if( browserPath is null ) { + return null; + } + + var launchOptions = new LaunchOptions { + Headless = true, + ExecutablePath = browserPath, + Args = noSandbox ? ["--no-sandbox"] : [] + }; + + return await Puppeteer.LaunchAsync( launchOptions ); + } + + private static string? GetPathToBrowser() { + string? browser = null; + if( OperatingSystem.IsWindows() ) { + foreach( string windowsPartialPath in WindowsPartialPaths ) { + foreach( string environmentVariable in WindowsEnvironmentVariables ) { + string? prefix = Environment.GetEnvironmentVariable( environmentVariable ); + if( prefix is not null ) { + string path = prefix + windowsPartialPath; + if( File.Exists( path ) ) { + return path; + } + } + } + } + } else if( OperatingSystem.IsMacOS() ) { + return MacPaths.First( File.Exists ); + } else if( OperatingSystem.IsLinux() ) { + return LinuxPaths.First( File.Exists ); + } + return browser; + } +} diff --git a/src/D2L.Bmx/D2L.Bmx.csproj b/src/D2L.Bmx/D2L.Bmx.csproj index f1b65770..ae048996 100644 --- a/src/D2L.Bmx/D2L.Bmx.csproj +++ b/src/D2L.Bmx/D2L.Bmx.csproj @@ -14,6 +14,7 @@ + diff --git a/src/D2L.Bmx/JsonSerializerContext.cs b/src/D2L.Bmx/JsonSerializerContext.cs index 4466f472..dbd8316f 100644 --- a/src/D2L.Bmx/JsonSerializerContext.cs +++ b/src/D2L.Bmx/JsonSerializerContext.cs @@ -20,6 +20,7 @@ namespace D2L.Bmx; [JsonSerializable( typeof( List ) )] [JsonSerializable( typeof( UpdateCheckCache ) )] [JsonSerializable( typeof( List ) )] +[JsonSerializable( typeof( OktaHomeResponse ) )] internal partial class JsonCamelCaseContext : JsonSerializerContext { } diff --git a/src/D2L.Bmx/LoginHandler.cs b/src/D2L.Bmx/LoginHandler.cs index 079286a8..104d87b1 100644 --- a/src/D2L.Bmx/LoginHandler.cs +++ b/src/D2L.Bmx/LoginHandler.cs @@ -5,14 +5,15 @@ OktaAuthenticator oktaAuth ) { public async Task HandleAsync( string? org, - string? user + string? user, + bool experimental ) { if( !File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { throw new BmxException( "BMX global config file not found. Okta sessions will not be saved. Please run `bmx configure` first." ); } - await oktaAuth.AuthenticateAsync( org, user, nonInteractive: false, ignoreCache: true ); + await oktaAuth.AuthenticateAsync( org, user, nonInteractive: false, ignoreCache: true, experimental: experimental ); Console.WriteLine( "Successfully logged in and Okta session has been cached." ); } } diff --git a/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs b/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs new file mode 100644 index 00000000..4ecdf309 --- /dev/null +++ b/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs @@ -0,0 +1,5 @@ +namespace D2L.Bmx.Okta.Models; + +internal record OktaHomeResponse( + string Login +); diff --git a/src/D2L.Bmx/Okta/OktaClient.cs b/src/D2L.Bmx/Okta/OktaClient.cs index e7fbd28e..618569e2 100644 --- a/src/D2L.Bmx/Okta/OktaClient.cs +++ b/src/D2L.Bmx/Okta/OktaClient.cs @@ -23,6 +23,7 @@ string challengeResponse internal interface IOktaAuthenticatedClient { Task GetAwsAccountAppsAsync(); + Task GetSessionExpiryAsync(); Task GetPageAsync( string url ); } @@ -187,6 +188,19 @@ async Task IOktaAuthenticatedClient.GetAwsAccountAppsAsync() { ?? throw new BmxException( "Error retrieving AWS accounts from Okta." ); } + async Task IOktaAuthenticatedClient.GetSessionExpiryAsync() { + OktaSession? session; + try { + session = await httpClient.GetFromJsonAsync( + "sessions/me", + JsonCamelCaseContext.Default.OktaSession ); + } catch( Exception ex ) { + throw new BmxException( "Request to retrieve session expiry from Okta failed.", ex ); + } + + return session?.ExpiresAt ?? throw new BmxException( "Error retrieving session expiry from Okta." ); + } + async Task IOktaAuthenticatedClient.GetPageAsync( string url ) { return await httpClient.GetStringAsync( url ); } diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index cf16dde0..2a740888 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -1,7 +1,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using D2L.Bmx.Okta; using D2L.Bmx.Okta.Models; +using PuppeteerSharp; namespace D2L.Bmx; @@ -22,7 +24,8 @@ public async Task AuthenticateAsync( string? org, string? user, bool nonInteractive, - bool ignoreCache + bool ignoreCache, + bool experimental ) { var orgSource = ParameterSource.CliArg; if( string.IsNullOrEmpty( org ) && !string.IsNullOrEmpty( config.Org ) ) { @@ -57,6 +60,9 @@ bool ignoreCache if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } + if( await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } dssoclient ) { + return new OktaAuthenticatedContext( Org: org, User: user, Client: dssoclient ); + } if( nonInteractive ) { throw new BmxException( "Okta authentication failed. Please run `bmx login` first." ); } @@ -130,6 +136,109 @@ private bool TryAuthenticateFromCache( return true; } + private async Task TryAuthenticateWithDSSOAsync( + string org, + string user, + IOktaClientFactory oktaClientFactory, + bool experimental + ) { + await using IBrowser? browser = await Browser.LaunchBrowserAsync( experimental ); + if( browser is null ) { + return null; + } + + Console.WriteLine( "Attempting to automatically login using DSSO." ); + var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 10 ) ); + var sessionIdTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); + var userEmailTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); + string? sessionId; + string? userEmail; + + try { + var page = await browser.NewPageAsync(); + string baseAddress = $"https://{org}.okta.com/"; + int attempt = 1; + + page.Load += ( _, _ ) => _ = GetSessionCookieAsync( cancellationTokenSource.Token ); + page.Response += ( _, responseCreatedEventArgs ) => _ = GetOktaUserEmailAsync( + responseCreatedEventArgs.Response + ); + await page.GoToAsync( baseAddress, timeout: 10000 ); + sessionId = await sessionIdTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); + userEmail = await userEmailTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); + + async Task GetSessionCookieAsync( CancellationToken cancellationToken ) { + var url = new Uri( page.Url ); + if( url.Host == $"{org}.okta.com" ) { + string title = await page.GetTitleAsync(); + if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { + if( attempt < 3 && url.AbsolutePath != "/" ) { + attempt++; + await page.GoToAsync( baseAddress ); + } else { + sessionIdTaskProducer.SetResult( null ); + } + return; + } + } + var cookies = await page.GetCookiesAsync( baseAddress ); + if( Array.Find( cookies, c => c.Name == "sid" )?.Value is string sid ) { + sessionIdTaskProducer.SetResult( sid ); + } + } + + async Task GetOktaUserEmailAsync( + IResponse response + ) { + if( response.Url.Contains( $"{baseAddress}enduser/api/v1/home" ) ) { + string content = await response.TextAsync(); + var home = JsonSerializer.Deserialize( content, JsonCamelCaseContext.Default.OktaHomeResponse ); + if( home is not null ) { + userEmailTaskProducer.SetResult( home.Login ); + } + } + } + + } catch( TaskCanceledException ) { + consoleWriter.WriteWarning( + $"WARNING: Failed to create {org} Okta session through DSSO. Check if org is correct." + ); + return null; + } catch( TargetClosedException ) { + consoleWriter.WriteWarning( + "WARNING: Failed to create Okta session through DSSO. If running BMX with admin privileges, rerun the command with the '--experimental' flag." + ); + return null; + } catch( Exception e ) { + consoleWriter.WriteWarning( "Error while trying to authenticate with Okta using DSSO." ); + consoleWriter.WriteError( e.GetType().ToString() ); + consoleWriter.WriteError( e.Message ); + return null; + } + + if( sessionId is null || userEmail is null ) { + return null; + } else if( !OktaUserMatchesProvided( userEmail, user ) ) { + consoleWriter.WriteWarning( + "WARNING: Could not create Okta session using DSSO as " + + $"provided Okta user '{user}' does not match user '{userEmail}'." ); + return null; + } + + var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( org, sessionId ); + var sessionExpiry = await oktaAuthenticatedClient.GetSessionExpiryAsync(); + CacheOktaSession( user, org, sessionId, sessionExpiry ); + return oktaAuthenticatedClient; + } + + private static bool OktaUserMatchesProvided( string oktaLogin, string providedUser ) { + string adName = oktaLogin.Split( '@' )[0]; + string normalizedUser = providedUser.Contains( '@' ) + ? providedUser.Split( '@' )[0] + : providedUser; + return adName.Equals( normalizedUser, StringComparison.OrdinalIgnoreCase ); + } + private void CacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { var session = new OktaSessionCache( userId, org, sessionId, expiresAt ); var sessionsToCache = ReadOktaSessionCacheFile(); diff --git a/src/D2L.Bmx/ParameterDescriptions.cs b/src/D2L.Bmx/ParameterDescriptions.cs index be7f4bc9..5d00fb50 100644 --- a/src/D2L.Bmx/ParameterDescriptions.cs +++ b/src/D2L.Bmx/ParameterDescriptions.cs @@ -18,4 +18,6 @@ internal static class ParameterDescriptions { Write BMX command to AWS profile, so that AWS tools & SDKs using the profile will source credentials from BMX. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html. """; + + public const string Experimental = "Enables experimental features"; } diff --git a/src/D2L.Bmx/PrintHandler.cs b/src/D2L.Bmx/PrintHandler.cs index 15f736fc..97791561 100644 --- a/src/D2L.Bmx/PrintHandler.cs +++ b/src/D2L.Bmx/PrintHandler.cs @@ -14,13 +14,15 @@ public async Task HandleAsync( int? duration, bool nonInteractive, string? format, - bool cacheAwsCredentials + bool cacheAwsCredentials, + bool experimental ) { var oktaContext = await oktaAuth.AuthenticateAsync( org: org, user: user, nonInteractive: nonInteractive, - ignoreCache: false + ignoreCache: false, + experimental: experimental ); var awsCreds = ( await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index bd136e9e..8af8897c 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -18,10 +18,16 @@ name: "--user", description: ParameterDescriptions.User ); +// allow no-sandbox argument for DSSO and future experimental features +var experimentalOption = new Option( + name: "--experimental", + description: ParameterDescriptions.Experimental ); + // bmx login var loginCommand = new Command( "login", "Log into Okta and save an Okta session" ){ orgOption, userOption, + experimentalOption, }; loginCommand.SetHandler( ( InvocationContext context ) => { var consoleWriter = new ConsoleWriter(); @@ -35,7 +41,8 @@ ) ); return handler.HandleAsync( org: context.ParseResult.GetValueForOption( orgOption ), - user: context.ParseResult.GetValueForOption( userOption ) + user: context.ParseResult.GetValueForOption( userOption ), + experimental: context.ParseResult.GetValueForOption( experimentalOption ) ); } ); @@ -119,6 +126,7 @@ userOption, nonInteractiveOption, cacheAwsCredentialsOption, + experimentalOption, }; printCommand.SetHandler( ( InvocationContext context ) => { @@ -147,7 +155,8 @@ duration: context.ParseResult.GetValueForOption( durationOption ), nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), format: context.ParseResult.GetValueForOption( formatOption ), - cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ) + cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), + experimental: context.ParseResult.GetValueForOption( experimentalOption ) ); } ); @@ -173,6 +182,7 @@ nonInteractiveOption, cacheAwsCredentialsOption, useCredentialProcessOption, + experimentalOption }; writeCommand.SetHandler( ( InvocationContext context ) => { @@ -207,8 +217,9 @@ output: context.ParseResult.GetValueForOption( outputOption ), profile: context.ParseResult.GetValueForOption( profileOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), - useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ) - ); + useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ), + experimental: context.ParseResult.GetValueForOption( experimentalOption ) + ); } ); var updateCommand = new Command( "update", "Updates BMX to the latest version" ); diff --git a/src/D2L.Bmx/WriteHandler.cs b/src/D2L.Bmx/WriteHandler.cs index 037e322b..88e73229 100644 --- a/src/D2L.Bmx/WriteHandler.cs +++ b/src/D2L.Bmx/WriteHandler.cs @@ -27,7 +27,8 @@ public async Task HandleAsync( string? output, string? profile, bool cacheAwsCredentials, - bool useCredentialProcess + bool useCredentialProcess, + bool experimental ) { cacheAwsCredentials = cacheAwsCredentials || useCredentialProcess; @@ -35,7 +36,8 @@ bool useCredentialProcess org: org, user: user, nonInteractive: nonInteractive, - ignoreCache: false + ignoreCache: false, + experimental: experimental ); var awsCredsInfo = await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, From a16a9e95c291c6492cfd994e1711aa228fe19823 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 09:45:16 -0400 Subject: [PATCH 02/49] add cancellation token to newpageasync --- src/D2L.Bmx/OktaAuthenticator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 2a740888..bc2691c7 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -155,7 +155,7 @@ bool experimental string? userEmail; try { - var page = await browser.NewPageAsync(); + var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); string baseAddress = $"https://{org}.okta.com/"; int attempt = 1; @@ -214,6 +214,9 @@ IResponse response consoleWriter.WriteError( e.GetType().ToString() ); consoleWriter.WriteError( e.Message ); return null; + } finally { + cancellationTokenSource.Dispose(); + browser.Dispose(); } if( sessionId is null || userEmail is null ) { From 6fc7baab1da88b033da60a039d5f98bb8dc3a515 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 09:54:27 -0400 Subject: [PATCH 03/49] tweak warning messages --- src/D2L.Bmx/OktaAuthenticator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index bc2691c7..7ee6cd8b 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -202,17 +202,17 @@ IResponse response } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $"WARNING: Failed to create {org} Okta session through DSSO. Check if org is correct." + + " If running BMX with admin privileges, rerun the command with the '--experimental' flag." ); return null; } catch( TargetClosedException ) { consoleWriter.WriteWarning( - "WARNING: Failed to create Okta session through DSSO. If running BMX with admin privileges, rerun the command with the '--experimental' flag." + $"WARNING: Failed to create {org} Okta session through DSSO as BMX is likely being run with elevated privilieges." + + " Rerun the command with the '--experimental' flag." ); return null; - } catch( Exception e ) { - consoleWriter.WriteWarning( "Error while trying to authenticate with Okta using DSSO." ); - consoleWriter.WriteError( e.GetType().ToString() ); - consoleWriter.WriteError( e.Message ); + } catch( Exception ) { + consoleWriter.WriteWarning( "WARNING: Unknown error while trying to authenticate with Okta using DSSO." ); return null; } finally { cancellationTokenSource.Dispose(); From 73d0cddddc56382673581d33038f40fde1f38355 Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:58:16 -0400 Subject: [PATCH 04/49] Update src/D2L.Bmx/OktaAuthenticator.cs --- src/D2L.Bmx/OktaAuthenticator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 7ee6cd8b..0c3b418d 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -60,8 +60,8 @@ bool experimental if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } - if( await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } dssoclient ) { - return new OktaAuthenticatedContext( Org: org, User: user, Client: dssoclient ); + if( await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } oktaAuthenticated ) { + return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } if( nonInteractive ) { throw new BmxException( "Okta authentication failed. Please run `bmx login` first." ); From c2f13bd2ef582d2cd758c19f0a7162a6bc690486 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 10:06:44 -0400 Subject: [PATCH 05/49] var name tweak --- src/D2L.Bmx/OktaAuthenticator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 0c3b418d..71a4ff65 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -60,8 +60,8 @@ bool experimental if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } - if( await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } oktaAuthenticated ) { - return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); + if( await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } oktaDSSOAuthenticated ) { + return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDSSOAuthenticated ); } if( nonInteractive ) { throw new BmxException( "Okta authentication failed. Please run `bmx login` first." ); From 19fa544c049dabc78627636da08a371884a7eeb5 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 10:44:58 -0400 Subject: [PATCH 06/49] tweak warning message for non matching user --- src/D2L.Bmx/OktaAuthenticator.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 71a4ff65..f9e16268 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -224,7 +224,7 @@ IResponse response } else if( !OktaUserMatchesProvided( userEmail, user ) ) { consoleWriter.WriteWarning( "WARNING: Could not create Okta session using DSSO as " - + $"provided Okta user '{user}' does not match user '{userEmail}'." ); + + $"provided Okta user '{StripLoginDomain( user )}' does not match user '{StripLoginDomain( userEmail )}'." ); return null; } @@ -234,11 +234,13 @@ IResponse response return oktaAuthenticatedClient; } + private static string StripLoginDomain( string email ) { + return email.Contains( '@' ) ? email.Split( '@' )[0] : email; + } + private static bool OktaUserMatchesProvided( string oktaLogin, string providedUser ) { - string adName = oktaLogin.Split( '@' )[0]; - string normalizedUser = providedUser.Contains( '@' ) - ? providedUser.Split( '@' )[0] - : providedUser; + string adName = StripLoginDomain( oktaLogin ); + string normalizedUser = StripLoginDomain( providedUser ); return adName.Equals( normalizedUser, StringComparison.OrdinalIgnoreCase ); } From 4f177b7d83e1cd69464282d7416ffcb2f7b2148a Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 10:46:48 -0400 Subject: [PATCH 07/49] mend --- src/D2L.Bmx/OktaAuthenticator.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index f9e16268..98222b46 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -224,7 +224,7 @@ IResponse response } else if( !OktaUserMatchesProvided( userEmail, user ) ) { consoleWriter.WriteWarning( "WARNING: Could not create Okta session using DSSO as " - + $"provided Okta user '{StripLoginDomain( user )}' does not match user '{StripLoginDomain( userEmail )}'." ); + + $"provided Okta user '{StripUserDomain( user )}' does not match user '{StripUserDomain( userEmail )}'." ); return null; } @@ -234,16 +234,16 @@ IResponse response return oktaAuthenticatedClient; } - private static string StripLoginDomain( string email ) { - return email.Contains( '@' ) ? email.Split( '@' )[0] : email; - } - private static bool OktaUserMatchesProvided( string oktaLogin, string providedUser ) { - string adName = StripLoginDomain( oktaLogin ); - string normalizedUser = StripLoginDomain( providedUser ); + string adName = StripUserDomain( oktaLogin ); + string normalizedUser = StripUserDomain( providedUser ); return adName.Equals( normalizedUser, StringComparison.OrdinalIgnoreCase ); } + private static string StripUserDomain( string user ) { + return user.Contains( '@' ) ? user.Split( '@' )[0] : user; + } + private void CacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { var session = new OktaSessionCache( userId, org, sessionId, expiresAt ); var sessionsToCache = ReadOktaSessionCacheFile(); From aedb8350c48d41623de88742d492974f5f0ac34c Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 11:04:01 -0400 Subject: [PATCH 08/49] change reload signin page --- src/D2L.Bmx/OktaAuthenticator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 98222b46..56df46e1 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -159,20 +159,20 @@ bool experimental string baseAddress = $"https://{org}.okta.com/"; int attempt = 1; - page.Load += ( _, _ ) => _ = GetSessionCookieAsync( cancellationTokenSource.Token ); + page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); page.Response += ( _, responseCreatedEventArgs ) => _ = GetOktaUserEmailAsync( responseCreatedEventArgs.Response ); - await page.GoToAsync( baseAddress, timeout: 10000 ); + await page.GoToAsync( baseAddress ); sessionId = await sessionIdTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); userEmail = await userEmailTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); - async Task GetSessionCookieAsync( CancellationToken cancellationToken ) { + async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); if( url.Host == $"{org}.okta.com" ) { string title = await page.GetTitleAsync(); if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { - if( attempt < 3 && url.AbsolutePath != "/" ) { + if( attempt < 3 && url.AbsolutePath == "/" ) { attempt++; await page.GoToAsync( baseAddress ); } else { From c14de8eb71ac577227e2fee5eb26a875b2ccc51c Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 11:51:29 -0400 Subject: [PATCH 09/49] add passwordless option that defaults to false --- src/D2L.Bmx/BmxConfig.cs | 3 ++- src/D2L.Bmx/BmxConfigProvider.cs | 14 +++++++++++++- src/D2L.Bmx/Browser.cs | 2 +- src/D2L.Bmx/ConfigureHandler.cs | 10 ++++++++-- src/D2L.Bmx/ConsolePrompter.cs | 10 ++++++++++ src/D2L.Bmx/LoginHandler.cs | 12 ++++++++++-- src/D2L.Bmx/OktaAuthenticator.cs | 20 +++++++++++++++++--- src/D2L.Bmx/ParameterDescriptions.cs | 1 + src/D2L.Bmx/PrintHandler.cs | 6 ++++-- src/D2L.Bmx/Program.cs | 22 +++++++++++++++++----- src/D2L.Bmx/WriteHandler.cs | 6 ++++-- 11 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/D2L.Bmx/BmxConfig.cs b/src/D2L.Bmx/BmxConfig.cs index c4b310b9..9ea021e2 100644 --- a/src/D2L.Bmx/BmxConfig.cs +++ b/src/D2L.Bmx/BmxConfig.cs @@ -6,5 +6,6 @@ internal record BmxConfig( string? Account, string? Role, string? Profile, - int? Duration + int? Duration, + bool? Passwordless ); diff --git a/src/D2L.Bmx/BmxConfigProvider.cs b/src/D2L.Bmx/BmxConfigProvider.cs index af8949bc..775f5cb3 100644 --- a/src/D2L.Bmx/BmxConfigProvider.cs +++ b/src/D2L.Bmx/BmxConfigProvider.cs @@ -44,13 +44,22 @@ public BmxConfig GetConfiguration() { duration = configDuration; } + bool? passwordless = null; + if( !string.IsNullOrEmpty( data.Global["passwordless"] ) ) { + if( !bool.TryParse( data.Global["passwordless"], out bool configPasswordless ) ) { + throw new BmxException( "Invalid passwordless in config" ); + } + passwordless = configPasswordless; + } + return new BmxConfig( Org: data.Global["org"], User: data.Global["user"], Account: data.Global["account"], Role: data.Global["role"], Profile: data.Global["profile"], - Duration: duration + Duration: duration, + Passwordless: passwordless ); } @@ -75,6 +84,9 @@ public void SaveConfiguration( BmxConfig config ) { if( config.Duration.HasValue ) { data.Global["duration"] = $"{config.Duration}"; } + if( config.Passwordless.HasValue ) { + data.Global["passwordless"] = $"{config.Passwordless}"; + } fs.Position = 0; fs.SetLength( 0 ); diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index 18072ccc..e0a0a72e 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -32,7 +32,7 @@ public class Browser { } var launchOptions = new LaunchOptions { - Headless = true, + Headless = false, ExecutablePath = browserPath, Args = noSandbox ? ["--no-sandbox"] : [] }; diff --git a/src/D2L.Bmx/ConfigureHandler.cs b/src/D2L.Bmx/ConfigureHandler.cs index c57ad806..a550693c 100644 --- a/src/D2L.Bmx/ConfigureHandler.cs +++ b/src/D2L.Bmx/ConfigureHandler.cs @@ -9,7 +9,8 @@ public void Handle( string? org, string? user, int? duration, - bool nonInteractive + bool nonInteractive, + bool? passwordless ) { if( string.IsNullOrEmpty( org ) && !nonInteractive ) { @@ -24,13 +25,18 @@ bool nonInteractive duration = consolePrompter.PromptDuration(); } + if( passwordless is null && !nonInteractive ) { + passwordless = consolePrompter.PromptPasswordless(); + } + BmxConfig config = new( Org: org, User: user, Account: null, Role: null, Profile: null, - Duration: duration + Duration: duration, + Passwordless: passwordless ); configProvider.SaveConfiguration( config ); Console.WriteLine( "Your configuration has been created. Okta sessions will now also be cached." ); diff --git a/src/D2L.Bmx/ConsolePrompter.cs b/src/D2L.Bmx/ConsolePrompter.cs index 070301a4..f9d8cf68 100644 --- a/src/D2L.Bmx/ConsolePrompter.cs +++ b/src/D2L.Bmx/ConsolePrompter.cs @@ -13,6 +13,7 @@ internal interface IConsolePrompter { string PromptRole( string[] roles ); OktaMfaFactor SelectMfa( OktaMfaFactor[] mfaOptions ); string GetMfaResponse( string mfaInputPrompt, bool maskInput ); + bool PromptPasswordless(); } internal class ConsolePrompter : IConsolePrompter { @@ -107,6 +108,15 @@ string IConsolePrompter.PromptRole( string[] roles ) { return roles[index - 1]; } + bool IConsolePrompter.PromptPasswordless() { + Console.Error.Write( $"{ParameterDescriptions.Passwordless} (y/n): " ); + string? input = Console.ReadLine(); + if( input is null || input.Length != 1 || ( input[0] != 'y' && input[0] != 'n' ) ) { + throw new BmxException( "Invalid passwordless input" ); + } + return input[0] == 'y'; + } + OktaMfaFactor IConsolePrompter.SelectMfa( OktaMfaFactor[] mfaOptions ) { Console.Error.WriteLine( "MFA Required" ); diff --git a/src/D2L.Bmx/LoginHandler.cs b/src/D2L.Bmx/LoginHandler.cs index 104d87b1..4ca846f9 100644 --- a/src/D2L.Bmx/LoginHandler.cs +++ b/src/D2L.Bmx/LoginHandler.cs @@ -6,14 +6,22 @@ OktaAuthenticator oktaAuth public async Task HandleAsync( string? org, string? user, - bool experimental + bool experimental, + bool? passwordless ) { if( !File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { throw new BmxException( "BMX global config file not found. Okta sessions will not be saved. Please run `bmx configure` first." ); } - await oktaAuth.AuthenticateAsync( org, user, nonInteractive: false, ignoreCache: true, experimental: experimental ); + await oktaAuth.AuthenticateAsync( + org, + user, + nonInteractive: false, + ignoreCache: true, + experimental: experimental, + passwordless: passwordless + ); Console.WriteLine( "Successfully logged in and Okta session has been cached." ); } } diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 56df46e1..ac0b5d0f 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -25,7 +25,8 @@ public async Task AuthenticateAsync( string? user, bool nonInteractive, bool ignoreCache, - bool experimental + bool experimental, + bool? passwordless ) { var orgSource = ParameterSource.CliArg; if( string.IsNullOrEmpty( org ) && !string.IsNullOrEmpty( config.Org ) ) { @@ -55,12 +56,18 @@ bool experimental consoleWriter.WriteParameter( ParameterDescriptions.User, user, userSource ); } + if( passwordless is null && config.Passwordless is not null ) { + passwordless = config.Passwordless; + } + var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( org ); if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } - if( await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } oktaDSSOAuthenticated ) { + if( passwordless == true + && await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } oktaDSSOAuthenticated + ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDSSOAuthenticated ); } if( nonInteractive ) { @@ -230,7 +237,14 @@ IResponse response var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( org, sessionId ); var sessionExpiry = await oktaAuthenticatedClient.GetSessionExpiryAsync(); - CacheOktaSession( user, org, sessionId, sessionExpiry ); + if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { + CacheOktaSession( user, org, sessionId, sessionExpiry ); + } else { + consoleWriter.WriteWarning( """ + No config file found. Your Okta session will not be cached. + Consider running `bmx configure` if you own this machine. + """ ); + } return oktaAuthenticatedClient; } diff --git a/src/D2L.Bmx/ParameterDescriptions.cs b/src/D2L.Bmx/ParameterDescriptions.cs index 5d00fb50..0f31a93d 100644 --- a/src/D2L.Bmx/ParameterDescriptions.cs +++ b/src/D2L.Bmx/ParameterDescriptions.cs @@ -20,4 +20,5 @@ internal static class ParameterDescriptions { """; public const string Experimental = "Enables experimental features"; + public const string Passwordless = "Use Okta DSSO to attempt to authenticate without providing a password"; } diff --git a/src/D2L.Bmx/PrintHandler.cs b/src/D2L.Bmx/PrintHandler.cs index 97791561..a104efe5 100644 --- a/src/D2L.Bmx/PrintHandler.cs +++ b/src/D2L.Bmx/PrintHandler.cs @@ -15,14 +15,16 @@ public async Task HandleAsync( bool nonInteractive, string? format, bool cacheAwsCredentials, - bool experimental + bool experimental, + bool? passwordless ) { var oktaContext = await oktaAuth.AuthenticateAsync( org: org, user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimental: experimental + experimental: experimental, + passwordless: passwordless ); var awsCreds = ( await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 8af8897c..29145123 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -17,6 +17,10 @@ var userOption = new Option( name: "--user", description: ParameterDescriptions.User ); +var passwordlessOption = new Option( + name: "--passwordless", + description: ParameterDescriptions.Passwordless +); // allow no-sandbox argument for DSSO and future experimental features var experimentalOption = new Option( @@ -28,6 +32,7 @@ orgOption, userOption, experimentalOption, + passwordlessOption }; loginCommand.SetHandler( ( InvocationContext context ) => { var consoleWriter = new ConsoleWriter(); @@ -42,7 +47,8 @@ return handler.HandleAsync( org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), - experimental: context.ParseResult.GetValueForOption( experimentalOption ) + experimental: context.ParseResult.GetValueForOption( experimentalOption ), + passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); } ); @@ -71,6 +77,7 @@ userOption, durationOption, nonInteractiveOption, + passwordlessOption, }; configureCommand.SetHandler( ( InvocationContext context ) => { @@ -81,7 +88,8 @@ org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), duration: context.ParseResult.GetValueForOption( durationOption ), - nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ) + nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), + passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); return Task.CompletedTask; } ); @@ -127,6 +135,7 @@ nonInteractiveOption, cacheAwsCredentialsOption, experimentalOption, + passwordlessOption, }; printCommand.SetHandler( ( InvocationContext context ) => { @@ -156,7 +165,8 @@ nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), format: context.ParseResult.GetValueForOption( formatOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), - experimental: context.ParseResult.GetValueForOption( experimentalOption ) + experimental: context.ParseResult.GetValueForOption( experimentalOption ), + passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); } ); @@ -182,7 +192,8 @@ nonInteractiveOption, cacheAwsCredentialsOption, useCredentialProcessOption, - experimentalOption + experimentalOption, + passwordlessOption, }; writeCommand.SetHandler( ( InvocationContext context ) => { @@ -218,7 +229,8 @@ profile: context.ParseResult.GetValueForOption( profileOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ), - experimental: context.ParseResult.GetValueForOption( experimentalOption ) + experimental: context.ParseResult.GetValueForOption( experimentalOption ), + passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); } ); diff --git a/src/D2L.Bmx/WriteHandler.cs b/src/D2L.Bmx/WriteHandler.cs index 88e73229..261b7754 100644 --- a/src/D2L.Bmx/WriteHandler.cs +++ b/src/D2L.Bmx/WriteHandler.cs @@ -28,7 +28,8 @@ public async Task HandleAsync( string? profile, bool cacheAwsCredentials, bool useCredentialProcess, - bool experimental + bool experimental, + bool? passwordless ) { cacheAwsCredentials = cacheAwsCredentials || useCredentialProcess; @@ -37,7 +38,8 @@ bool experimental user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimental: experimental + experimental: experimental, + passwordless: passwordless ); var awsCredsInfo = await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, From 77a91c47eafef6ad7f944ef14eb1c74769d3409f Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 11:52:55 -0400 Subject: [PATCH 10/49] mend --- src/D2L.Bmx/BmxConfigProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/BmxConfigProvider.cs b/src/D2L.Bmx/BmxConfigProvider.cs index 775f5cb3..8123f183 100644 --- a/src/D2L.Bmx/BmxConfigProvider.cs +++ b/src/D2L.Bmx/BmxConfigProvider.cs @@ -47,7 +47,7 @@ public BmxConfig GetConfiguration() { bool? passwordless = null; if( !string.IsNullOrEmpty( data.Global["passwordless"] ) ) { if( !bool.TryParse( data.Global["passwordless"], out bool configPasswordless ) ) { - throw new BmxException( "Invalid passwordless in config" ); + throw new BmxException( "Invalid passwordless value in config" ); } passwordless = configPasswordless; } From 1b8cfd24bc7246cf1f0c068dd1eeff2d58ce6d1d Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 12:06:33 -0400 Subject: [PATCH 11/49] tweak warning message --- src/D2L.Bmx/OktaAuthenticator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index ac0b5d0f..e754379c 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -208,7 +208,7 @@ IResponse response } catch( TaskCanceledException ) { consoleWriter.WriteWarning( - $"WARNING: Failed to create {org} Okta session through DSSO. Check if org is correct." + $"WARNING: Timed out when trying to create {org} Okta session through DSSO. Check if org is correct." + " If running BMX with admin privileges, rerun the command with the '--experimental' flag." ); return null; From 1a8ba2e599b130e1dfe3099fa36497d9763cba7d Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 12:10:25 -0400 Subject: [PATCH 12/49] set browser as headless based on bmx_debug env variable --- src/D2L.Bmx/Browser.cs | 2 +- src/D2L.Bmx/OktaAuthenticator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index e0a0a72e..caad08db 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -32,7 +32,7 @@ public class Browser { } var launchOptions = new LaunchOptions { - Headless = false, + Headless = Environment.GetEnvironmentVariable( "BMX_DEBUG" ) != "1", ExecutablePath = browserPath, Args = noSandbox ? ["--no-sandbox"] : [] }; diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index e754379c..2b6fab8d 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -155,7 +155,7 @@ bool experimental } Console.WriteLine( "Attempting to automatically login using DSSO." ); - var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 10 ) ); + var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); var sessionIdTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); var userEmailTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); string? sessionId; @@ -179,7 +179,7 @@ async Task GetSessionCookieAsync() { if( url.Host == $"{org}.okta.com" ) { string title = await page.GetTitleAsync(); if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { - if( attempt < 3 && url.AbsolutePath == "/" ) { + if( attempt < 3 ) { attempt++; await page.GoToAsync( baseAddress ); } else { From b526dbeab899efd892307633c7bf8e0ec8d0181d Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 12:22:27 -0400 Subject: [PATCH 13/49] readd path check --- src/D2L.Bmx/OktaAuthenticator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 2b6fab8d..df2a5151 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -178,7 +178,7 @@ async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); if( url.Host == $"{org}.okta.com" ) { string title = await page.GetTitleAsync(); - if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { + if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) && url.AbsolutePath != "/" ) { if( attempt < 3 ) { attempt++; await page.GoToAsync( baseAddress ); From d502f1d0d687cc1f13959fe422e56727770a76ff Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 14:35:52 -0400 Subject: [PATCH 14/49] default browser to edge for windows --- src/D2L.Bmx/Browser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index caad08db..eee7dd3e 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -13,8 +13,8 @@ public class Browser { // https://github.com/microsoft/playwright/blob/6763d5ab6bd20f1f0fc879537855a26c7644a496/packages/playwright-core/src/server/registry/index.ts#L457-L459 private static readonly string[] WindowsPartialPaths = [ - "\\Google\\Chrome\\Application\\chrome.exe", - "\\Microsoft\\Edge\\Application\\msedge.exe" + "\\Microsoft\\Edge\\Application\\msedge.exe", + "\\Google\\Chrome\\Application\\chrome.exe" ]; private static readonly string[] MacPaths = [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", From 28b320a81e61500431681e470ef87a70c5a203ae Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 14:36:17 -0400 Subject: [PATCH 15/49] headless is always true regardless of bmx_debug --- src/D2L.Bmx/Browser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index eee7dd3e..e5ed1d20 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -32,7 +32,7 @@ public class Browser { } var launchOptions = new LaunchOptions { - Headless = Environment.GetEnvironmentVariable( "BMX_DEBUG" ) != "1", + Headless = true, ExecutablePath = browserPath, Args = noSandbox ? ["--no-sandbox"] : [] }; From 7633b0f803549bd79f25f133ca2214df49b2b892 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 14:38:44 -0400 Subject: [PATCH 16/49] make no-sandbox option scarier --- src/D2L.Bmx/ParameterDescriptions.cs | 2 +- src/D2L.Bmx/Program.cs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/D2L.Bmx/ParameterDescriptions.cs b/src/D2L.Bmx/ParameterDescriptions.cs index 0f31a93d..e1b8f9ab 100644 --- a/src/D2L.Bmx/ParameterDescriptions.cs +++ b/src/D2L.Bmx/ParameterDescriptions.cs @@ -19,6 +19,6 @@ internal static class ParameterDescriptions { See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html. """; - public const string Experimental = "Enables experimental features"; + public const string ExperimentalBypassBrowserSecurity = "Enables experimental features"; public const string Passwordless = "Use Okta DSSO to attempt to authenticate without providing a password"; } diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 29145123..06fbfd37 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -22,16 +22,16 @@ description: ParameterDescriptions.Passwordless ); -// allow no-sandbox argument for DSSO and future experimental features -var experimentalOption = new Option( - name: "--experimental", - description: ParameterDescriptions.Experimental ); +// allow no-sandbox argument for chromium to for passwordless auth with elevated permissions +var experimentalBypassBrowserSecurityOption = new Option( + name: "--experimental-bypass-browser-security", + description: ParameterDescriptions.ExperimentalBypassBrowserSecurity ); // bmx login var loginCommand = new Command( "login", "Log into Okta and save an Okta session" ){ orgOption, userOption, - experimentalOption, + experimentalBypassBrowserSecurityOption, passwordlessOption }; loginCommand.SetHandler( ( InvocationContext context ) => { @@ -47,7 +47,7 @@ return handler.HandleAsync( org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), - experimental: context.ParseResult.GetValueForOption( experimentalOption ), + experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ), passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); } ); @@ -134,7 +134,7 @@ userOption, nonInteractiveOption, cacheAwsCredentialsOption, - experimentalOption, + experimentalBypassBrowserSecurityOption, passwordlessOption, }; @@ -165,7 +165,7 @@ nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), format: context.ParseResult.GetValueForOption( formatOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), - experimental: context.ParseResult.GetValueForOption( experimentalOption ), + experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ), passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); } ); @@ -192,7 +192,7 @@ nonInteractiveOption, cacheAwsCredentialsOption, useCredentialProcessOption, - experimentalOption, + experimentalBypassBrowserSecurityOption, passwordlessOption, }; @@ -229,7 +229,7 @@ profile: context.ParseResult.GetValueForOption( profileOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ), - experimental: context.ParseResult.GetValueForOption( experimentalOption ), + experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ), passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) ); } ); From 185b8ff93e5ab2d8f1726058a8fd19a693588bb0 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 14:39:11 -0400 Subject: [PATCH 17/49] abort if not on vpn --- src/D2L.Bmx/OktaAuthenticator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index df2a5151..c40ee021 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -178,12 +178,15 @@ async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); if( url.Host == $"{org}.okta.com" ) { string title = await page.GetTitleAsync(); - if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) && url.AbsolutePath != "/" ) { - if( attempt < 3 ) { + // DSSO can sometimes takes more than one attempt. + // If the path is '/', it means DSSO is not available and we should stop retrying. + if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { + if( attempt < 3 && url.AbsolutePath != "/" ) { attempt++; await page.GoToAsync( baseAddress ); } else { sessionIdTaskProducer.SetResult( null ); + userEmailTaskProducer.SetResult( null ); } return; } From d9fe294aae6fb29a7e4c8259e5618df2e1ad44e0 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 15:08:50 -0400 Subject: [PATCH 18/49] normalize okta org and check users route --- src/D2L.Bmx/JsonSerializerContext.cs | 1 - src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs | 5 -- src/D2L.Bmx/Okta/OktaClient.cs | 6 +- src/D2L.Bmx/OktaAuthenticator.cs | 68 +++++++-------------- 4 files changed, 25 insertions(+), 55 deletions(-) delete mode 100644 src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs diff --git a/src/D2L.Bmx/JsonSerializerContext.cs b/src/D2L.Bmx/JsonSerializerContext.cs index dbd8316f..4466f472 100644 --- a/src/D2L.Bmx/JsonSerializerContext.cs +++ b/src/D2L.Bmx/JsonSerializerContext.cs @@ -20,7 +20,6 @@ namespace D2L.Bmx; [JsonSerializable( typeof( List ) )] [JsonSerializable( typeof( UpdateCheckCache ) )] [JsonSerializable( typeof( List ) )] -[JsonSerializable( typeof( OktaHomeResponse ) )] internal partial class JsonCamelCaseContext : JsonSerializerContext { } diff --git a/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs b/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs deleted file mode 100644 index 4ecdf309..00000000 --- a/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace D2L.Bmx.Okta.Models; - -internal record OktaHomeResponse( - string Login -); diff --git a/src/D2L.Bmx/Okta/OktaClient.cs b/src/D2L.Bmx/Okta/OktaClient.cs index 618569e2..c5c5b9bd 100644 --- a/src/D2L.Bmx/Okta/OktaClient.cs +++ b/src/D2L.Bmx/Okta/OktaClient.cs @@ -23,7 +23,7 @@ string challengeResponse internal interface IOktaAuthenticatedClient { Task GetAwsAccountAppsAsync(); - Task GetSessionExpiryAsync(); + Task GetSessionExpiryAsync(); Task GetPageAsync( string url ); } @@ -188,7 +188,7 @@ async Task IOktaAuthenticatedClient.GetAwsAccountAppsAsync() { ?? throw new BmxException( "Error retrieving AWS accounts from Okta." ); } - async Task IOktaAuthenticatedClient.GetSessionExpiryAsync() { + async Task IOktaAuthenticatedClient.GetSessionExpiryAsync() { OktaSession? session; try { session = await httpClient.GetFromJsonAsync( @@ -198,7 +198,7 @@ async Task IOktaAuthenticatedClient.GetSessionExpiryAsync() { throw new BmxException( "Request to retrieve session expiry from Okta failed.", ex ); } - return session?.ExpiresAt ?? throw new BmxException( "Error retrieving session expiry from Okta." ); + return session ?? throw new BmxException( "Error retrieving session expiry from Okta." ); } async Task IOktaAuthenticatedClient.GetPageAsync( string url ) { diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index c40ee021..85a45c8f 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using D2L.Bmx.Okta; using D2L.Bmx.Okta.Models; using PuppeteerSharp; @@ -155,28 +154,23 @@ bool experimental } Console.WriteLine( "Attempting to automatically login using DSSO." ); + string normalizedOrg = org.Replace( ".okta.com", "" ); var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); var sessionIdTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); - var userEmailTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); string? sessionId; - string? userEmail; try { var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); - string baseAddress = $"https://{org}.okta.com/"; + string baseAddress = $"https://{normalizedOrg}.okta.com/"; int attempt = 1; page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); - page.Response += ( _, responseCreatedEventArgs ) => _ = GetOktaUserEmailAsync( - responseCreatedEventArgs.Response - ); await page.GoToAsync( baseAddress ); sessionId = await sessionIdTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); - userEmail = await userEmailTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); - if( url.Host == $"{org}.okta.com" ) { + if( url.Host == $"{normalizedOrg}.okta.com" ) { string title = await page.GetTitleAsync(); // DSSO can sometimes takes more than one attempt. // If the path is '/', it means DSSO is not available and we should stop retrying. @@ -186,7 +180,6 @@ async Task GetSessionCookieAsync() { await page.GoToAsync( baseAddress ); } else { sessionIdTaskProducer.SetResult( null ); - userEmailTaskProducer.SetResult( null ); } return; } @@ -196,29 +189,19 @@ async Task GetSessionCookieAsync() { sessionIdTaskProducer.SetResult( sid ); } } - - async Task GetOktaUserEmailAsync( - IResponse response - ) { - if( response.Url.Contains( $"{baseAddress}enduser/api/v1/home" ) ) { - string content = await response.TextAsync(); - var home = JsonSerializer.Deserialize( content, JsonCamelCaseContext.Default.OktaHomeResponse ); - if( home is not null ) { - userEmailTaskProducer.SetResult( home.Login ); - } - } - } - } catch( TaskCanceledException ) { - consoleWriter.WriteWarning( - $"WARNING: Timed out when trying to create {org} Okta session through DSSO. Check if org is correct." - + " If running BMX with admin privileges, rerun the command with the '--experimental' flag." + consoleWriter.WriteWarning( $""" + WARNING: Timed out when trying to create {normalizedOrg} Okta session through DSSO. + Check if the org is correct. If running BMX with elevated privileges, + rerun the command with the '--experimental-bypass-browser-security' flag + """ ); return null; } catch( TargetClosedException ) { - consoleWriter.WriteWarning( - $"WARNING: Failed to create {org} Okta session through DSSO as BMX is likely being run with elevated privilieges." + - " Rerun the command with the '--experimental' flag." + consoleWriter.WriteWarning( $""" + WARNING: Failed to create {normalizedOrg} Okta session through DSSO as BMX is likely being run + with elevated privileges. Rerun the command with the '--experimental-bypass-browser-security' flag. + """ ); return null; } catch( Exception ) { @@ -229,17 +212,20 @@ IResponse response browser.Dispose(); } - if( sessionId is null || userEmail is null ) { - return null; - } else if( !OktaUserMatchesProvided( userEmail, user ) ) { - consoleWriter.WriteWarning( - "WARNING: Could not create Okta session using DSSO as " - + $"provided Okta user '{StripUserDomain( user )}' does not match user '{StripUserDomain( userEmail )}'." ); + if( sessionId is null ) { return null; } var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( org, sessionId ); - var sessionExpiry = await oktaAuthenticatedClient.GetSessionExpiryAsync(); + var sessionExpiry = ( await oktaAuthenticatedClient.GetSessionExpiryAsync() ).ExpiresAt; + // We can expect a 404 if the session does not belong to the user which will throw an exception + try { + string userResponse = await oktaAuthenticatedClient.GetPageAsync( $"users/{user}" ); + } catch( Exception ) { + consoleWriter.WriteWarning( + $"WARNING: Failed to create {org} Okta session through DSSO as created session does not belong to {user}." ); + return null; + } if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { CacheOktaSession( user, org, sessionId, sessionExpiry ); } else { @@ -251,16 +237,6 @@ No config file found. Your Okta session will not be cached. return oktaAuthenticatedClient; } - private static bool OktaUserMatchesProvided( string oktaLogin, string providedUser ) { - string adName = StripUserDomain( oktaLogin ); - string normalizedUser = StripUserDomain( providedUser ); - return adName.Equals( normalizedUser, StringComparison.OrdinalIgnoreCase ); - } - - private static string StripUserDomain( string user ) { - return user.Contains( '@' ) ? user.Split( '@' )[0] : user; - } - private void CacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { var session = new OktaSessionCache( userId, org, sessionId, expiresAt ); var sessionsToCache = ReadOktaSessionCacheFile(); From 00800632b23088b6c4ac300ce66f9c639491c599 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 15:12:06 -0400 Subject: [PATCH 19/49] remove passwordless flag --- src/D2L.Bmx/BmxConfig.cs | 3 +-- src/D2L.Bmx/BmxConfigProvider.cs | 14 +------------- src/D2L.Bmx/ConfigureHandler.cs | 10 ++-------- src/D2L.Bmx/LoginHandler.cs | 6 ++---- src/D2L.Bmx/OktaAuthenticator.cs | 18 ++++++++---------- src/D2L.Bmx/PrintHandler.cs | 6 ++---- src/D2L.Bmx/Program.cs | 20 ++++---------------- src/D2L.Bmx/WriteHandler.cs | 6 ++---- 8 files changed, 22 insertions(+), 61 deletions(-) diff --git a/src/D2L.Bmx/BmxConfig.cs b/src/D2L.Bmx/BmxConfig.cs index 9ea021e2..c4b310b9 100644 --- a/src/D2L.Bmx/BmxConfig.cs +++ b/src/D2L.Bmx/BmxConfig.cs @@ -6,6 +6,5 @@ internal record BmxConfig( string? Account, string? Role, string? Profile, - int? Duration, - bool? Passwordless + int? Duration ); diff --git a/src/D2L.Bmx/BmxConfigProvider.cs b/src/D2L.Bmx/BmxConfigProvider.cs index 8123f183..af8949bc 100644 --- a/src/D2L.Bmx/BmxConfigProvider.cs +++ b/src/D2L.Bmx/BmxConfigProvider.cs @@ -44,22 +44,13 @@ public BmxConfig GetConfiguration() { duration = configDuration; } - bool? passwordless = null; - if( !string.IsNullOrEmpty( data.Global["passwordless"] ) ) { - if( !bool.TryParse( data.Global["passwordless"], out bool configPasswordless ) ) { - throw new BmxException( "Invalid passwordless value in config" ); - } - passwordless = configPasswordless; - } - return new BmxConfig( Org: data.Global["org"], User: data.Global["user"], Account: data.Global["account"], Role: data.Global["role"], Profile: data.Global["profile"], - Duration: duration, - Passwordless: passwordless + Duration: duration ); } @@ -84,9 +75,6 @@ public void SaveConfiguration( BmxConfig config ) { if( config.Duration.HasValue ) { data.Global["duration"] = $"{config.Duration}"; } - if( config.Passwordless.HasValue ) { - data.Global["passwordless"] = $"{config.Passwordless}"; - } fs.Position = 0; fs.SetLength( 0 ); diff --git a/src/D2L.Bmx/ConfigureHandler.cs b/src/D2L.Bmx/ConfigureHandler.cs index a550693c..c57ad806 100644 --- a/src/D2L.Bmx/ConfigureHandler.cs +++ b/src/D2L.Bmx/ConfigureHandler.cs @@ -9,8 +9,7 @@ public void Handle( string? org, string? user, int? duration, - bool nonInteractive, - bool? passwordless + bool nonInteractive ) { if( string.IsNullOrEmpty( org ) && !nonInteractive ) { @@ -25,18 +24,13 @@ public void Handle( duration = consolePrompter.PromptDuration(); } - if( passwordless is null && !nonInteractive ) { - passwordless = consolePrompter.PromptPasswordless(); - } - BmxConfig config = new( Org: org, User: user, Account: null, Role: null, Profile: null, - Duration: duration, - Passwordless: passwordless + Duration: duration ); configProvider.SaveConfiguration( config ); Console.WriteLine( "Your configuration has been created. Okta sessions will now also be cached." ); diff --git a/src/D2L.Bmx/LoginHandler.cs b/src/D2L.Bmx/LoginHandler.cs index 4ca846f9..22352dd6 100644 --- a/src/D2L.Bmx/LoginHandler.cs +++ b/src/D2L.Bmx/LoginHandler.cs @@ -6,8 +6,7 @@ OktaAuthenticator oktaAuth public async Task HandleAsync( string? org, string? user, - bool experimental, - bool? passwordless + bool experimental ) { if( !File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { throw new BmxException( @@ -19,8 +18,7 @@ await oktaAuth.AuthenticateAsync( user, nonInteractive: false, ignoreCache: true, - experimental: experimental, - passwordless: passwordless + experimentalBypassBrowserSecurity: experimental ); Console.WriteLine( "Successfully logged in and Okta session has been cached." ); } diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 85a45c8f..1c1e5004 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -24,8 +24,7 @@ public async Task AuthenticateAsync( string? user, bool nonInteractive, bool ignoreCache, - bool experimental, - bool? passwordless + bool experimentalBypassBrowserSecurity ) { var orgSource = ParameterSource.CliArg; if( string.IsNullOrEmpty( org ) && !string.IsNullOrEmpty( config.Org ) ) { @@ -55,17 +54,16 @@ public async Task AuthenticateAsync( consoleWriter.WriteParameter( ParameterDescriptions.User, user, userSource ); } - if( passwordless is null && config.Passwordless is not null ) { - passwordless = config.Passwordless; - } - var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( org ); if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } - if( passwordless == true - && await TryAuthenticateWithDSSOAsync( org, user, oktaClientFactory, experimental ) is { } oktaDSSOAuthenticated + if( await TryAuthenticateWithDSSOAsync( + org, + user, + oktaClientFactory, + experimentalBypassBrowserSecurity ) is { } oktaDSSOAuthenticated ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDSSOAuthenticated ); } @@ -146,9 +144,9 @@ private bool TryAuthenticateFromCache( string org, string user, IOktaClientFactory oktaClientFactory, - bool experimental + bool experimentalBypassBrowserSecurity ) { - await using IBrowser? browser = await Browser.LaunchBrowserAsync( experimental ); + await using IBrowser? browser = await Browser.LaunchBrowserAsync( experimentalBypassBrowserSecurity ); if( browser is null ) { return null; } diff --git a/src/D2L.Bmx/PrintHandler.cs b/src/D2L.Bmx/PrintHandler.cs index a104efe5..0bcad1ca 100644 --- a/src/D2L.Bmx/PrintHandler.cs +++ b/src/D2L.Bmx/PrintHandler.cs @@ -15,16 +15,14 @@ public async Task HandleAsync( bool nonInteractive, string? format, bool cacheAwsCredentials, - bool experimental, - bool? passwordless + bool experimental ) { var oktaContext = await oktaAuth.AuthenticateAsync( org: org, user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimental: experimental, - passwordless: passwordless + experimentalBypassBrowserSecurity: experimental ); var awsCreds = ( await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 06fbfd37..d1cc2513 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -17,10 +17,6 @@ var userOption = new Option( name: "--user", description: ParameterDescriptions.User ); -var passwordlessOption = new Option( - name: "--passwordless", - description: ParameterDescriptions.Passwordless -); // allow no-sandbox argument for chromium to for passwordless auth with elevated permissions var experimentalBypassBrowserSecurityOption = new Option( @@ -32,7 +28,6 @@ orgOption, userOption, experimentalBypassBrowserSecurityOption, - passwordlessOption }; loginCommand.SetHandler( ( InvocationContext context ) => { var consoleWriter = new ConsoleWriter(); @@ -47,8 +42,7 @@ return handler.HandleAsync( org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), - experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ), - passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) + experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) ); } ); @@ -77,7 +71,6 @@ userOption, durationOption, nonInteractiveOption, - passwordlessOption, }; configureCommand.SetHandler( ( InvocationContext context ) => { @@ -88,8 +81,7 @@ org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), duration: context.ParseResult.GetValueForOption( durationOption ), - nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), - passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) + nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ) ); return Task.CompletedTask; } ); @@ -135,7 +127,6 @@ nonInteractiveOption, cacheAwsCredentialsOption, experimentalBypassBrowserSecurityOption, - passwordlessOption, }; printCommand.SetHandler( ( InvocationContext context ) => { @@ -165,8 +156,7 @@ nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), format: context.ParseResult.GetValueForOption( formatOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), - experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ), - passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) + experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) ); } ); @@ -193,7 +183,6 @@ cacheAwsCredentialsOption, useCredentialProcessOption, experimentalBypassBrowserSecurityOption, - passwordlessOption, }; writeCommand.SetHandler( ( InvocationContext context ) => { @@ -229,8 +218,7 @@ profile: context.ParseResult.GetValueForOption( profileOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ), - experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ), - passwordless: context.ParseResult.GetValueForOption( passwordlessOption ) + experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) ); } ); diff --git a/src/D2L.Bmx/WriteHandler.cs b/src/D2L.Bmx/WriteHandler.cs index 261b7754..ac027dbf 100644 --- a/src/D2L.Bmx/WriteHandler.cs +++ b/src/D2L.Bmx/WriteHandler.cs @@ -28,8 +28,7 @@ public async Task HandleAsync( string? profile, bool cacheAwsCredentials, bool useCredentialProcess, - bool experimental, - bool? passwordless + bool experimental ) { cacheAwsCredentials = cacheAwsCredentials || useCredentialProcess; @@ -38,8 +37,7 @@ public async Task HandleAsync( user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimental: experimental, - passwordless: passwordless + experimentalBypassBrowserSecurity: experimental ); var awsCredsInfo = await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, From ac51994b9e9d89b90d1bc9811c79b20e18680ec7 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 15:13:15 -0400 Subject: [PATCH 20/49] mend --- src/D2L.Bmx/ConsolePrompter.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/D2L.Bmx/ConsolePrompter.cs b/src/D2L.Bmx/ConsolePrompter.cs index f9d8cf68..070301a4 100644 --- a/src/D2L.Bmx/ConsolePrompter.cs +++ b/src/D2L.Bmx/ConsolePrompter.cs @@ -13,7 +13,6 @@ internal interface IConsolePrompter { string PromptRole( string[] roles ); OktaMfaFactor SelectMfa( OktaMfaFactor[] mfaOptions ); string GetMfaResponse( string mfaInputPrompt, bool maskInput ); - bool PromptPasswordless(); } internal class ConsolePrompter : IConsolePrompter { @@ -108,15 +107,6 @@ string IConsolePrompter.PromptRole( string[] roles ) { return roles[index - 1]; } - bool IConsolePrompter.PromptPasswordless() { - Console.Error.Write( $"{ParameterDescriptions.Passwordless} (y/n): " ); - string? input = Console.ReadLine(); - if( input is null || input.Length != 1 || ( input[0] != 'y' && input[0] != 'n' ) ) { - throw new BmxException( "Invalid passwordless input" ); - } - return input[0] == 'y'; - } - OktaMfaFactor IConsolePrompter.SelectMfa( OktaMfaFactor[] mfaOptions ) { Console.Error.WriteLine( "MFA Required" ); From ff3f8e10c7f4a4290eca5f21b520769d1aa58c8e Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 15:15:31 -0400 Subject: [PATCH 21/49] adjust parameter name for experimental --- src/D2L.Bmx/LoginHandler.cs | 4 ++-- src/D2L.Bmx/PrintHandler.cs | 4 ++-- src/D2L.Bmx/Program.cs | 6 +++--- src/D2L.Bmx/WriteHandler.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/D2L.Bmx/LoginHandler.cs b/src/D2L.Bmx/LoginHandler.cs index 22352dd6..5e8d574c 100644 --- a/src/D2L.Bmx/LoginHandler.cs +++ b/src/D2L.Bmx/LoginHandler.cs @@ -6,7 +6,7 @@ OktaAuthenticator oktaAuth public async Task HandleAsync( string? org, string? user, - bool experimental + bool experimentalBypassBrowserSecurity ) { if( !File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { throw new BmxException( @@ -18,7 +18,7 @@ await oktaAuth.AuthenticateAsync( user, nonInteractive: false, ignoreCache: true, - experimentalBypassBrowserSecurity: experimental + experimentalBypassBrowserSecurity: experimentalBypassBrowserSecurity ); Console.WriteLine( "Successfully logged in and Okta session has been cached." ); } diff --git a/src/D2L.Bmx/PrintHandler.cs b/src/D2L.Bmx/PrintHandler.cs index 0bcad1ca..62ddeeb9 100644 --- a/src/D2L.Bmx/PrintHandler.cs +++ b/src/D2L.Bmx/PrintHandler.cs @@ -15,14 +15,14 @@ public async Task HandleAsync( bool nonInteractive, string? format, bool cacheAwsCredentials, - bool experimental + bool experimentalBypassBrowserSecurity ) { var oktaContext = await oktaAuth.AuthenticateAsync( org: org, user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimentalBypassBrowserSecurity: experimental + experimentalBypassBrowserSecurity: experimentalBypassBrowserSecurity ); var awsCreds = ( await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index d1cc2513..dfb59d9c 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -42,7 +42,7 @@ return handler.HandleAsync( org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), - experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) + experimentalBypassBrowserSecurity: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) ); } ); @@ -156,7 +156,7 @@ nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), format: context.ParseResult.GetValueForOption( formatOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), - experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) + experimentalBypassBrowserSecurity: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) ); } ); @@ -218,7 +218,7 @@ profile: context.ParseResult.GetValueForOption( profileOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ), - experimental: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) + experimentalBypassBrowserSecurity: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) ); } ); diff --git a/src/D2L.Bmx/WriteHandler.cs b/src/D2L.Bmx/WriteHandler.cs index ac027dbf..585ce993 100644 --- a/src/D2L.Bmx/WriteHandler.cs +++ b/src/D2L.Bmx/WriteHandler.cs @@ -28,7 +28,7 @@ public async Task HandleAsync( string? profile, bool cacheAwsCredentials, bool useCredentialProcess, - bool experimental + bool experimentalBypassBrowserSecurity ) { cacheAwsCredentials = cacheAwsCredentials || useCredentialProcess; @@ -37,7 +37,7 @@ bool experimental user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimentalBypassBrowserSecurity: experimental + experimentalBypassBrowserSecurity: experimentalBypassBrowserSecurity ); var awsCredsInfo = await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, From 551f6cf7b1e0a3bdce7671706ceb102b848a5ed1 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 15:16:45 -0400 Subject: [PATCH 22/49] rename okta session function --- src/D2L.Bmx/Okta/OktaClient.cs | 4 ++-- src/D2L.Bmx/OktaAuthenticator.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/D2L.Bmx/Okta/OktaClient.cs b/src/D2L.Bmx/Okta/OktaClient.cs index c5c5b9bd..e8e36da3 100644 --- a/src/D2L.Bmx/Okta/OktaClient.cs +++ b/src/D2L.Bmx/Okta/OktaClient.cs @@ -23,7 +23,7 @@ string challengeResponse internal interface IOktaAuthenticatedClient { Task GetAwsAccountAppsAsync(); - Task GetSessionExpiryAsync(); + Task GetCurrentOktaSessionAsync(); Task GetPageAsync( string url ); } @@ -188,7 +188,7 @@ async Task IOktaAuthenticatedClient.GetAwsAccountAppsAsync() { ?? throw new BmxException( "Error retrieving AWS accounts from Okta." ); } - async Task IOktaAuthenticatedClient.GetSessionExpiryAsync() { + async Task IOktaAuthenticatedClient.GetCurrentOktaSessionAsync() { OktaSession? session; try { session = await httpClient.GetFromJsonAsync( diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 1c1e5004..57fd3343 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -215,7 +215,7 @@ with elevated privileges. Rerun the command with the '--experimental-bypass-brow } var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( org, sessionId ); - var sessionExpiry = ( await oktaAuthenticatedClient.GetSessionExpiryAsync() ).ExpiresAt; + var sessionExpiry = ( await oktaAuthenticatedClient.GetCurrentOktaSessionAsync() ).ExpiresAt; // We can expect a 404 if the session does not belong to the user which will throw an exception try { string userResponse = await oktaAuthenticatedClient.GetPageAsync( $"users/{user}" ); From 64f5efbaf562f5f2c066639713a8f7f31cdc5b24 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Wed, 18 Sep 2024 07:52:40 -0400 Subject: [PATCH 23/49] redo org check --- src/D2L.Bmx/OktaAuthenticator.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 57fd3343..4c20df75 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -152,14 +152,16 @@ bool experimentalBypassBrowserSecurity } Console.WriteLine( "Attempting to automatically login using DSSO." ); - string normalizedOrg = org.Replace( ".okta.com", "" ); var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); var sessionIdTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); string? sessionId; try { var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); - string baseAddress = $"https://{normalizedOrg}.okta.com/"; + string baseAddress = org.Contains( '.' ) + ? $"https://{org}/" + : $"https://{org}.okta.com/"; + var baseUrl = new Uri( baseAddress ); int attempt = 1; page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); @@ -168,7 +170,7 @@ bool experimentalBypassBrowserSecurity async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); - if( url.Host == $"{normalizedOrg}.okta.com" ) { + if( url.Host == baseUrl.Host ) { string title = await page.GetTitleAsync(); // DSSO can sometimes takes more than one attempt. // If the path is '/', it means DSSO is not available and we should stop retrying. @@ -189,15 +191,15 @@ async Task GetSessionCookieAsync() { } } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" - WARNING: Timed out when trying to create {normalizedOrg} Okta session through DSSO. - Check if the org is correct. If running BMX with elevated privileges, + WARNING: Timed out when trying to create Okta session through DSSO. + Check if the org '{org}' is correct. If running BMX with elevated privileges, rerun the command with the '--experimental-bypass-browser-security' flag """ ); return null; } catch( TargetClosedException ) { - consoleWriter.WriteWarning( $""" - WARNING: Failed to create {normalizedOrg} Okta session through DSSO as BMX is likely being run + consoleWriter.WriteWarning( """ + WARNING: Failed to create Okta session through DSSO as BMX is likely being run with elevated privileges. Rerun the command with the '--experimental-bypass-browser-security' flag. """ ); @@ -221,7 +223,7 @@ with elevated privileges. Rerun the command with the '--experimental-bypass-brow string userResponse = await oktaAuthenticatedClient.GetPageAsync( $"users/{user}" ); } catch( Exception ) { consoleWriter.WriteWarning( - $"WARNING: Failed to create {org} Okta session through DSSO as created session does not belong to {user}." ); + $"WARNING: Failed to create Okta session through DSSO as created session does not belong to {user}." ); return null; } if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { From 40171e4acf81ad2da538ffe6a1e6f396a8e3186f Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:08:15 -0400 Subject: [PATCH 24/49] Update src/D2L.Bmx/Browser.cs Co-authored-by: Adipa Wijayathilaka --- src/D2L.Bmx/Browser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index e5ed1d20..349a0deb 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -2,7 +2,7 @@ namespace D2L.Bmx; -public class Browser { +public static class Browser { // https://github.com/microsoft/playwright/blob/6763d5ab6bd20f1f0fc879537855a26c7644a496/packages/playwright-core/src/server/registry/index.ts#L630 private static readonly string[] WindowsEnvironmentVariables = [ From a2f903aca5eea03d02b374641110d4eb412eb9b6 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Wed, 18 Sep 2024 14:26:18 -0400 Subject: [PATCH 25/49] nits --- src/D2L.Bmx/Browser.cs | 10 ++++------ src/D2L.Bmx/LoginHandler.cs | 4 ++-- src/D2L.Bmx/Okta/OktaClient.cs | 4 ++-- src/D2L.Bmx/OktaAuthenticator.cs | 4 ++-- src/D2L.Bmx/PrintHandler.cs | 4 ++-- src/D2L.Bmx/Program.cs | 14 +++++++------- src/D2L.Bmx/WriteHandler.cs | 4 ++-- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index 349a0deb..7403a0fd 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -14,15 +14,15 @@ public static class Browser { // https://github.com/microsoft/playwright/blob/6763d5ab6bd20f1f0fc879537855a26c7644a496/packages/playwright-core/src/server/registry/index.ts#L457-L459 private static readonly string[] WindowsPartialPaths = [ "\\Microsoft\\Edge\\Application\\msedge.exe", - "\\Google\\Chrome\\Application\\chrome.exe" + "\\Google\\Chrome\\Application\\chrome.exe", ]; private static readonly string[] MacPaths = [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", ]; private static readonly string[] LinuxPaths = [ "/opt/google/chrome/chrome", - "/opt/microsoft/msedge/msedge" + "/opt/microsoft/msedge/msedge", ]; public static async Task LaunchBrowserAsync( bool noSandbox = false ) { @@ -32,7 +32,6 @@ public static class Browser { } var launchOptions = new LaunchOptions { - Headless = true, ExecutablePath = browserPath, Args = noSandbox ? ["--no-sandbox"] : [] }; @@ -41,7 +40,6 @@ public static class Browser { } private static string? GetPathToBrowser() { - string? browser = null; if( OperatingSystem.IsWindows() ) { foreach( string windowsPartialPath in WindowsPartialPaths ) { foreach( string environmentVariable in WindowsEnvironmentVariables ) { @@ -59,6 +57,6 @@ public static class Browser { } else if( OperatingSystem.IsLinux() ) { return LinuxPaths.First( File.Exists ); } - return browser; + return null; } } diff --git a/src/D2L.Bmx/LoginHandler.cs b/src/D2L.Bmx/LoginHandler.cs index 5e8d574c..b0f4b969 100644 --- a/src/D2L.Bmx/LoginHandler.cs +++ b/src/D2L.Bmx/LoginHandler.cs @@ -6,7 +6,7 @@ OktaAuthenticator oktaAuth public async Task HandleAsync( string? org, string? user, - bool experimentalBypassBrowserSecurity + bool bypassBrowserSecurity ) { if( !File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { throw new BmxException( @@ -18,7 +18,7 @@ await oktaAuth.AuthenticateAsync( user, nonInteractive: false, ignoreCache: true, - experimentalBypassBrowserSecurity: experimentalBypassBrowserSecurity + bypassBrowserSecurity: bypassBrowserSecurity ); Console.WriteLine( "Successfully logged in and Okta session has been cached." ); } diff --git a/src/D2L.Bmx/Okta/OktaClient.cs b/src/D2L.Bmx/Okta/OktaClient.cs index e8e36da3..101a1763 100644 --- a/src/D2L.Bmx/Okta/OktaClient.cs +++ b/src/D2L.Bmx/Okta/OktaClient.cs @@ -195,10 +195,10 @@ async Task IOktaAuthenticatedClient.GetCurrentOktaSessionAsync() { "sessions/me", JsonCamelCaseContext.Default.OktaSession ); } catch( Exception ex ) { - throw new BmxException( "Request to retrieve session expiry from Okta failed.", ex ); + throw new BmxException( "Request to retrieve session from Okta failed.", ex ); } - return session ?? throw new BmxException( "Error retrieving session expiry from Okta." ); + return session ?? throw new BmxException( "Error retrieving session from Okta." ); } async Task IOktaAuthenticatedClient.GetPageAsync( string url ) { diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 4c20df75..b2929365 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -24,7 +24,7 @@ public async Task AuthenticateAsync( string? user, bool nonInteractive, bool ignoreCache, - bool experimentalBypassBrowserSecurity + bool bypassBrowserSecurity ) { var orgSource = ParameterSource.CliArg; if( string.IsNullOrEmpty( org ) && !string.IsNullOrEmpty( config.Org ) ) { @@ -63,7 +63,7 @@ bool experimentalBypassBrowserSecurity org, user, oktaClientFactory, - experimentalBypassBrowserSecurity ) is { } oktaDSSOAuthenticated + bypassBrowserSecurity ) is { } oktaDSSOAuthenticated ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDSSOAuthenticated ); } diff --git a/src/D2L.Bmx/PrintHandler.cs b/src/D2L.Bmx/PrintHandler.cs index 62ddeeb9..ce677eb4 100644 --- a/src/D2L.Bmx/PrintHandler.cs +++ b/src/D2L.Bmx/PrintHandler.cs @@ -15,14 +15,14 @@ public async Task HandleAsync( bool nonInteractive, string? format, bool cacheAwsCredentials, - bool experimentalBypassBrowserSecurity + bool bypassBrowserSecurity ) { var oktaContext = await oktaAuth.AuthenticateAsync( org: org, user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimentalBypassBrowserSecurity: experimentalBypassBrowserSecurity + bypassBrowserSecurity: bypassBrowserSecurity ); var awsCreds = ( await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index dfb59d9c..f3d20ed7 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -19,7 +19,7 @@ description: ParameterDescriptions.User ); // allow no-sandbox argument for chromium to for passwordless auth with elevated permissions -var experimentalBypassBrowserSecurityOption = new Option( +var bypassBrowserSecurityOption = new Option( name: "--experimental-bypass-browser-security", description: ParameterDescriptions.ExperimentalBypassBrowserSecurity ); @@ -27,7 +27,7 @@ var loginCommand = new Command( "login", "Log into Okta and save an Okta session" ){ orgOption, userOption, - experimentalBypassBrowserSecurityOption, + bypassBrowserSecurityOption, }; loginCommand.SetHandler( ( InvocationContext context ) => { var consoleWriter = new ConsoleWriter(); @@ -42,7 +42,7 @@ return handler.HandleAsync( org: context.ParseResult.GetValueForOption( orgOption ), user: context.ParseResult.GetValueForOption( userOption ), - experimentalBypassBrowserSecurity: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) + bypassBrowserSecurity: context.ParseResult.GetValueForOption( bypassBrowserSecurityOption ) ); } ); @@ -126,7 +126,7 @@ userOption, nonInteractiveOption, cacheAwsCredentialsOption, - experimentalBypassBrowserSecurityOption, + bypassBrowserSecurityOption, }; printCommand.SetHandler( ( InvocationContext context ) => { @@ -156,7 +156,7 @@ nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption ), format: context.ParseResult.GetValueForOption( formatOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), - experimentalBypassBrowserSecurity: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) + bypassBrowserSecurity: context.ParseResult.GetValueForOption( bypassBrowserSecurityOption ) ); } ); @@ -182,7 +182,7 @@ nonInteractiveOption, cacheAwsCredentialsOption, useCredentialProcessOption, - experimentalBypassBrowserSecurityOption, + bypassBrowserSecurityOption, }; writeCommand.SetHandler( ( InvocationContext context ) => { @@ -218,7 +218,7 @@ profile: context.ParseResult.GetValueForOption( profileOption ), cacheAwsCredentials: context.ParseResult.GetValueForOption( cacheAwsCredentialsOption ), useCredentialProcess: context.ParseResult.GetValueForOption( useCredentialProcessOption ), - experimentalBypassBrowserSecurity: context.ParseResult.GetValueForOption( experimentalBypassBrowserSecurityOption ) + bypassBrowserSecurity: context.ParseResult.GetValueForOption( bypassBrowserSecurityOption ) ); } ); diff --git a/src/D2L.Bmx/WriteHandler.cs b/src/D2L.Bmx/WriteHandler.cs index 585ce993..1adf1171 100644 --- a/src/D2L.Bmx/WriteHandler.cs +++ b/src/D2L.Bmx/WriteHandler.cs @@ -28,7 +28,7 @@ public async Task HandleAsync( string? profile, bool cacheAwsCredentials, bool useCredentialProcess, - bool experimentalBypassBrowserSecurity + bool bypassBrowserSecurity ) { cacheAwsCredentials = cacheAwsCredentials || useCredentialProcess; @@ -37,7 +37,7 @@ bool experimentalBypassBrowserSecurity user: user, nonInteractive: nonInteractive, ignoreCache: false, - experimentalBypassBrowserSecurity: experimentalBypassBrowserSecurity + bypassBrowserSecurity: bypassBrowserSecurity ); var awsCredsInfo = await awsCredsCreator.CreateAwsCredsAsync( okta: oktaContext, From 88f7517291a9090c310741b09d5eeb735a3ec428 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 08:34:39 -0400 Subject: [PATCH 26/49] more nits --- src/D2L.Bmx/Okta/OktaClient.cs | 10 ++- src/D2L.Bmx/OktaAuthenticator.cs | 97 ++++++++++++++++------------ src/D2L.Bmx/ParameterDescriptions.cs | 4 +- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/D2L.Bmx/Okta/OktaClient.cs b/src/D2L.Bmx/Okta/OktaClient.cs index 101a1763..a731bb34 100644 --- a/src/D2L.Bmx/Okta/OktaClient.cs +++ b/src/D2L.Bmx/Okta/OktaClient.cs @@ -31,13 +31,13 @@ internal class OktaClientFactory : IOktaClientFactory { IOktaAnonymousClient IOktaClientFactory.CreateAnonymousClient( string org ) { var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds( 30 ), - BaseAddress = GetBaseAddress( org ), + BaseAddress = GetApiBaseAddress( org ), }; return new OktaAnonymousClient( httpClient ); } IOktaAuthenticatedClient IOktaClientFactory.CreateAuthenticatedClient( string org, string sessionId ) { - var baseAddress = GetBaseAddress( org ); + var baseAddress = GetApiBaseAddress( org ); var cookieContainer = new CookieContainer(); cookieContainer.Add( new Cookie( "sid", sessionId, "/", baseAddress.Host ) ); @@ -52,10 +52,8 @@ IOktaAuthenticatedClient IOktaClientFactory.CreateAuthenticatedClient( string or return new OktaAuthenticatedClient( httpClient ); } - private static Uri GetBaseAddress( string org ) { - return org.Contains( '.' ) - ? new Uri( $"https://{org}/api/v1/" ) - : new Uri( $"https://{org}.okta.com/api/v1/" ); + private static Uri GetApiBaseAddress( string org ) { + return new Uri( $"{org}api/v1/" ); } } diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index b2929365..efa25b95 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -1,8 +1,10 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net; using D2L.Bmx.Okta; using D2L.Bmx.Okta.Models; using PuppeteerSharp; +using PuppeteerSharp.Helpers; namespace D2L.Bmx; @@ -54,18 +56,21 @@ bool bypassBrowserSecurity consoleWriter.WriteParameter( ParameterDescriptions.User, user, userSource ); } - var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( org ); + string orgBaseAddress = GetOrgBaseAddress( org ); + var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( orgBaseAddress ); if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } - if( await TryAuthenticateWithDSSOAsync( + if( await GetDssoAuthenticatedClientAsync( + orgBaseAddress, org, user, oktaClientFactory, - bypassBrowserSecurity ) is { } oktaDSSOAuthenticated + nonInteractive, + bypassBrowserSecurity ) is { } oktaDssoAuthenticated ) { - return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDSSOAuthenticated ); + return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDssoAuthenticated ); } if( nonInteractive ) { throw new BmxException( "Okta authentication failed. Please run `bmx login` first." ); @@ -106,14 +111,7 @@ mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value var sessionResp = await oktaAnonymous.CreateSessionAsync( successInfo.SessionToken ); oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( org, sessionResp.Id ); - if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { - CacheOktaSession( user, org, sessionResp.Id, sessionResp.ExpiresAt ); - } else { - consoleWriter.WriteWarning( """ - No config file found. Your Okta session will not be cached. - Consider running `bmx configure` if you own this machine. - """ ); - } + TryCacheOktaSession( user, org, sessionResp.Id, sessionResp.ExpiresAt ); return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } @@ -124,6 +122,12 @@ No config file found. Your Okta session will not be cached. throw new UnreachableException( $"Unexpected response type: {authnResponse.GetType()}" ); } + private static string GetOrgBaseAddress( string org ) { + return org.Contains( '.' ) + ? $"https://{org}/" + : $"https://{org}.okta.com/"; + } + private bool TryAuthenticateFromCache( string org, string user, @@ -140,10 +144,12 @@ private bool TryAuthenticateFromCache( return true; } - private async Task TryAuthenticateWithDSSOAsync( + private async Task GetDssoAuthenticatedClientAsync( + string orgBaseAddress, string org, string user, IOktaClientFactory oktaClientFactory, + bool nonInteractive, bool experimentalBypassBrowserSecurity ) { await using IBrowser? browser = await Browser.LaunchBrowserAsync( experimentalBypassBrowserSecurity ); @@ -151,90 +157,95 @@ bool experimentalBypassBrowserSecurity return null; } - Console.WriteLine( "Attempting to automatically login using DSSO." ); - var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); - var sessionIdTaskProducer = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); + if( !nonInteractive ) { + Console.Error.WriteLine( "Attempting to automatically login using Okta Desktop Single Sign-On." ); + } + using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); + var sessionIdTcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); string? sessionId; try { - var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); - string baseAddress = org.Contains( '.' ) - ? $"https://{org}/" - : $"https://{org}.okta.com/"; - var baseUrl = new Uri( baseAddress ); + using var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); + var baseUrl = new Uri( orgBaseAddress ); int attempt = 1; page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); - await page.GoToAsync( baseAddress ); - sessionId = await sessionIdTaskProducer.Task.WaitAsync( cancellationTokenSource.Token ); + await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); + sessionId = await sessionIdTcs.Task; async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); if( url.Host == baseUrl.Host ) { string title = await page.GetTitleAsync(); // DSSO can sometimes takes more than one attempt. - // If the path is '/', it means DSSO is not available and we should stop retrying. + // If the path is '/' with 'sign in' in the title, it means DSSO is not available and we should stop retrying. if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { if( attempt < 3 && url.AbsolutePath != "/" ) { attempt++; - await page.GoToAsync( baseAddress ); + await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); } else { - sessionIdTaskProducer.SetResult( null ); + consoleWriter.WriteWarning( + "WARNING: Could not authenticate with Okta using Desktop Single Sign-On." ); + sessionIdTcs.SetResult( null ); } return; } } - var cookies = await page.GetCookiesAsync( baseAddress ); + var cookies = await page.GetCookiesAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); if( Array.Find( cookies, c => c.Name == "sid" )?.Value is string sid ) { - sessionIdTaskProducer.SetResult( sid ); + sessionIdTcs.SetResult( sid ); } } } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" - WARNING: Timed out when trying to create Okta session through DSSO. - Check if the org '{org}' is correct. If running BMX with elevated privileges, + WARNING: Timed out when trying to create Okta session through Desktop Single Sign-On. + Check if the org '{orgBaseAddress}' is correct. If running BMX with elevated privileges, rerun the command with the '--experimental-bypass-browser-security' flag """ ); return null; } catch( TargetClosedException ) { consoleWriter.WriteWarning( """ - WARNING: Failed to create Okta session through DSSO as BMX is likely being run + WARNING: Failed to create Okta session through Desktop Single Sign-On as BMX is likely being run with elevated privileges. Rerun the command with the '--experimental-bypass-browser-security' flag. """ ); return null; } catch( Exception ) { - consoleWriter.WriteWarning( "WARNING: Unknown error while trying to authenticate with Okta using DSSO." ); + consoleWriter.WriteWarning( + "WARNING: Unknown error while trying to authenticate with Okta using Desktop Single Sign-On." ); return null; - } finally { - cancellationTokenSource.Dispose(); - browser.Dispose(); } if( sessionId is null ) { return null; } - var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( org, sessionId ); - var sessionExpiry = ( await oktaAuthenticatedClient.GetCurrentOktaSessionAsync() ).ExpiresAt; + var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( orgBaseAddress, sessionId ); + var expiresAt = ( await oktaAuthenticatedClient.GetCurrentOktaSessionAsync() ).ExpiresAt; // We can expect a 404 if the session does not belong to the user which will throw an exception try { string userResponse = await oktaAuthenticatedClient.GetPageAsync( $"users/{user}" ); } catch( Exception ) { consoleWriter.WriteWarning( - $"WARNING: Failed to create Okta session through DSSO as created session does not belong to {user}." ); + "WARNING: Failed to create Okta session with Desktop Single Sign-On" + + $" as created session does not belong to {user}." ); return null; } + TryCacheOktaSession( user, org, sessionId, expiresAt ); + return oktaAuthenticatedClient; + } + + private bool TryCacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { - CacheOktaSession( user, org, sessionId, sessionExpiry ); - } else { - consoleWriter.WriteWarning( """ + CacheOktaSession( userId, org, sessionId, expiresAt ); + return true; + } + consoleWriter.WriteWarning( """ No config file found. Your Okta session will not be cached. Consider running `bmx configure` if you own this machine. """ ); - } - return oktaAuthenticatedClient; + return false; } private void CacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { diff --git a/src/D2L.Bmx/ParameterDescriptions.cs b/src/D2L.Bmx/ParameterDescriptions.cs index e1b8f9ab..aca37bbe 100644 --- a/src/D2L.Bmx/ParameterDescriptions.cs +++ b/src/D2L.Bmx/ParameterDescriptions.cs @@ -19,6 +19,6 @@ internal static class ParameterDescriptions { See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html. """; - public const string ExperimentalBypassBrowserSecurity = "Enables experimental features"; - public const string Passwordless = "Use Okta DSSO to attempt to authenticate without providing a password"; + public const string ExperimentalBypassBrowserSecurity + = "Disable chromium sandbox when running with elevated permissions for Okta Desktop Single Sign-On"; } From 91d52b67944a2463b2751b7af1d2d7a666c405e1 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 08:46:13 -0400 Subject: [PATCH 27/49] readd user email strip check --- src/D2L.Bmx/JsonSerializerContext.cs | 1 + src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs | 5 +++ src/D2L.Bmx/OktaAuthenticator.cs | 50 ++++++++++++++------- 3 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs diff --git a/src/D2L.Bmx/JsonSerializerContext.cs b/src/D2L.Bmx/JsonSerializerContext.cs index 4466f472..dbd8316f 100644 --- a/src/D2L.Bmx/JsonSerializerContext.cs +++ b/src/D2L.Bmx/JsonSerializerContext.cs @@ -20,6 +20,7 @@ namespace D2L.Bmx; [JsonSerializable( typeof( List ) )] [JsonSerializable( typeof( UpdateCheckCache ) )] [JsonSerializable( typeof( List ) )] +[JsonSerializable( typeof( OktaHomeResponse ) )] internal partial class JsonCamelCaseContext : JsonSerializerContext { } diff --git a/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs b/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs new file mode 100644 index 00000000..4ecdf309 --- /dev/null +++ b/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs @@ -0,0 +1,5 @@ +namespace D2L.Bmx.Okta.Models; + +internal record OktaHomeResponse( + string Login +); diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index efa25b95..d035ce12 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -1,10 +1,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Net; +using System.Text.Json; using D2L.Bmx.Okta; using D2L.Bmx.Okta.Models; using PuppeteerSharp; -using PuppeteerSharp.Helpers; namespace D2L.Bmx; @@ -157,21 +156,27 @@ bool experimentalBypassBrowserSecurity return null; } + Console.WriteLine( DateTimeOffset.Now ); if( !nonInteractive ) { Console.Error.WriteLine( "Attempting to automatically login using Okta Desktop Single Sign-On." ); } using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); var sessionIdTcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); + var userEmailTcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); string? sessionId; + string? userEmail; try { using var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); var baseUrl = new Uri( orgBaseAddress ); int attempt = 1; - page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); + page.Load += ( _, _ ) => _ = GetSessionCookieAsync().WaitAsync( cancellationTokenSource.Token ); + page.Response += ( _, response ) => _ = GetOktaUserEmailAsync( response.Response ).WaitAsync( + cancellationTokenSource.Token ); await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); sessionId = await sessionIdTcs.Task; + userEmail = await userEmailTcs.Task; async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); @@ -182,7 +187,7 @@ async Task GetSessionCookieAsync() { if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { if( attempt < 3 && url.AbsolutePath != "/" ) { attempt++; - await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); + await page.GoToAsync( orgBaseAddress ); } else { consoleWriter.WriteWarning( "WARNING: Could not authenticate with Okta using Desktop Single Sign-On." ); @@ -191,11 +196,20 @@ async Task GetSessionCookieAsync() { return; } } - var cookies = await page.GetCookiesAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); + var cookies = await page.GetCookiesAsync( orgBaseAddress ); if( Array.Find( cookies, c => c.Name == "sid" )?.Value is string sid ) { sessionIdTcs.SetResult( sid ); } } + async Task GetOktaUserEmailAsync( IResponse response ) { + if( response.Url.Contains( $"{orgBaseAddress}enduser/api/v1/home" ) ) { + string content = await response.TextAsync(); + var home = JsonSerializer.Deserialize( content, JsonCamelCaseContext.Default.OktaHomeResponse ); + if( home is not null ) { + userEmailTcs.SetResult( home.Login ); + } + } + } } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" WARNING: Timed out when trying to create Okta session through Desktop Single Sign-On. @@ -217,25 +231,31 @@ with elevated privileges. Rerun the command with the '--experimental-bypass-brow return null; } - if( sessionId is null ) { + if( sessionId is null || userEmail is null ) { + return null; + } else if( !OktaUserMatchesProvided( userEmail, user ) ) { + consoleWriter.WriteWarning( + "WARNING: Could not create Okta session using Desktop Single Sign-On as " + + $"provided Okta user '{StripLoginDomain( user )}' does not match user '{StripLoginDomain( userEmail )}'." ); return null; } var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( orgBaseAddress, sessionId ); var expiresAt = ( await oktaAuthenticatedClient.GetCurrentOktaSessionAsync() ).ExpiresAt; - // We can expect a 404 if the session does not belong to the user which will throw an exception - try { - string userResponse = await oktaAuthenticatedClient.GetPageAsync( $"users/{user}" ); - } catch( Exception ) { - consoleWriter.WriteWarning( - "WARNING: Failed to create Okta session with Desktop Single Sign-On" - + $" as created session does not belong to {user}." ); - return null; - } TryCacheOktaSession( user, org, sessionId, expiresAt ); return oktaAuthenticatedClient; } + private static string StripLoginDomain( string email ) { + return email.Contains( '@' ) ? email.Split( '@' )[0] : email; + } + + private static bool OktaUserMatchesProvided( string oktaLogin, string providedUser ) { + string adName = StripLoginDomain( oktaLogin ); + string normalizedUser = StripLoginDomain( providedUser ); + return adName.Equals( normalizedUser, StringComparison.OrdinalIgnoreCase ); + } + private bool TryCacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { CacheOktaSession( userId, org, sessionId, expiresAt ); From 684f1bdec54b37c032ac9fddd6d7c2c5d975cec6 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 08:49:13 -0400 Subject: [PATCH 28/49] mend --- .github/workflows/publish.yml | 6 ++++++ src/D2L.Bmx/OktaAuthenticator.cs | 14 ++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 11e0690c..f6a15a47 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,9 +62,15 @@ jobs: include: - machine: windows-latest platform: win + architecture: x64 + file_name: bmx.exe + - machine: windows-latest + platform: win + architecture: x64 file_name: bmx.exe - machine: macos-latest platform: osx + architecture: x64 file_name: bmx runs-on: ${{ matrix.machine }} timeout-minutes: 10 diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index d035ce12..211f76a6 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -156,7 +156,6 @@ bool experimentalBypassBrowserSecurity return null; } - Console.WriteLine( DateTimeOffset.Now ); if( !nonInteractive ) { Console.Error.WriteLine( "Attempting to automatically login using Okta Desktop Single Sign-On." ); } @@ -171,9 +170,8 @@ bool experimentalBypassBrowserSecurity var baseUrl = new Uri( orgBaseAddress ); int attempt = 1; - page.Load += ( _, _ ) => _ = GetSessionCookieAsync().WaitAsync( cancellationTokenSource.Token ); - page.Response += ( _, response ) => _ = GetOktaUserEmailAsync( response.Response ).WaitAsync( - cancellationTokenSource.Token ); + page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); + page.Response += ( _, response ) => _ = GetOktaUserEmailAsync( response.Response ); await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); sessionId = await sessionIdTcs.Task; userEmail = await userEmailTcs.Task; @@ -181,13 +179,13 @@ bool experimentalBypassBrowserSecurity async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); if( url.Host == baseUrl.Host ) { - string title = await page.GetTitleAsync(); + string title = await page.GetTitleAsync().WaitAsync( cancellationTokenSource.Token ); // DSSO can sometimes takes more than one attempt. // If the path is '/' with 'sign in' in the title, it means DSSO is not available and we should stop retrying. if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { if( attempt < 3 && url.AbsolutePath != "/" ) { attempt++; - await page.GoToAsync( orgBaseAddress ); + await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); } else { consoleWriter.WriteWarning( "WARNING: Could not authenticate with Okta using Desktop Single Sign-On." ); @@ -196,14 +194,14 @@ async Task GetSessionCookieAsync() { return; } } - var cookies = await page.GetCookiesAsync( orgBaseAddress ); + var cookies = await page.GetCookiesAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); if( Array.Find( cookies, c => c.Name == "sid" )?.Value is string sid ) { sessionIdTcs.SetResult( sid ); } } async Task GetOktaUserEmailAsync( IResponse response ) { if( response.Url.Contains( $"{orgBaseAddress}enduser/api/v1/home" ) ) { - string content = await response.TextAsync(); + string content = await response.TextAsync().WaitAsync( cancellationTokenSource.Token ); var home = JsonSerializer.Deserialize( content, JsonCamelCaseContext.Default.OktaHomeResponse ); if( home is not null ) { userEmailTcs.SetResult( home.Login ); From 81a37143e643fd32b478ce6bc9a568ec22f06aaa Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 08:50:04 -0400 Subject: [PATCH 29/49] mend --- .github/workflows/publish.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f6a15a47..11e0690c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,15 +62,9 @@ jobs: include: - machine: windows-latest platform: win - architecture: x64 - file_name: bmx.exe - - machine: windows-latest - platform: win - architecture: x64 file_name: bmx.exe - machine: macos-latest platform: osx - architecture: x64 file_name: bmx runs-on: ${{ matrix.machine }} timeout-minutes: 10 From d48c7db07b54adae853a8d52f068fd24d546e991 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 16:13:21 -0400 Subject: [PATCH 30/49] deal in uris instread of string for orgs --- src/D2L.Bmx/Okta/Models/OktaSession.cs | 1 + src/D2L.Bmx/Okta/OktaClient.cs | 16 +++--- src/D2L.Bmx/OktaAuthenticator.cs | 67 ++++++++++---------------- 3 files changed, 35 insertions(+), 49 deletions(-) diff --git a/src/D2L.Bmx/Okta/Models/OktaSession.cs b/src/D2L.Bmx/Okta/Models/OktaSession.cs index 81019d9e..876a7cc8 100644 --- a/src/D2L.Bmx/Okta/Models/OktaSession.cs +++ b/src/D2L.Bmx/Okta/Models/OktaSession.cs @@ -2,6 +2,7 @@ namespace D2L.Bmx.Okta.Models; internal record OktaSession( string Id, + string Login, string UserId, DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt diff --git a/src/D2L.Bmx/Okta/OktaClient.cs b/src/D2L.Bmx/Okta/OktaClient.cs index a731bb34..18c068c8 100644 --- a/src/D2L.Bmx/Okta/OktaClient.cs +++ b/src/D2L.Bmx/Okta/OktaClient.cs @@ -6,8 +6,8 @@ namespace D2L.Bmx.Okta; internal interface IOktaClientFactory { - IOktaAnonymousClient CreateAnonymousClient( string org ); - IOktaAuthenticatedClient CreateAuthenticatedClient( string org, string sessionId ); + IOktaAnonymousClient CreateAnonymousClient( Uri orgUrl ); + IOktaAuthenticatedClient CreateAuthenticatedClient( Uri orgUrl, string sessionId ); } internal interface IOktaAnonymousClient { @@ -28,16 +28,16 @@ internal interface IOktaAuthenticatedClient { } internal class OktaClientFactory : IOktaClientFactory { - IOktaAnonymousClient IOktaClientFactory.CreateAnonymousClient( string org ) { + IOktaAnonymousClient IOktaClientFactory.CreateAnonymousClient( Uri orgUrl ) { var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds( 30 ), - BaseAddress = GetApiBaseAddress( org ), + BaseAddress = GetApiBaseAddress( orgUrl ), }; return new OktaAnonymousClient( httpClient ); } - IOktaAuthenticatedClient IOktaClientFactory.CreateAuthenticatedClient( string org, string sessionId ) { - var baseAddress = GetApiBaseAddress( org ); + IOktaAuthenticatedClient IOktaClientFactory.CreateAuthenticatedClient( Uri orgUrl, string sessionId ) { + var baseAddress = GetApiBaseAddress( orgUrl ); var cookieContainer = new CookieContainer(); cookieContainer.Add( new Cookie( "sid", sessionId, "/", baseAddress.Host ) ); @@ -52,8 +52,8 @@ IOktaAuthenticatedClient IOktaClientFactory.CreateAuthenticatedClient( string or return new OktaAuthenticatedClient( httpClient ); } - private static Uri GetApiBaseAddress( string org ) { - return new Uri( $"{org}api/v1/" ); + private static Uri GetApiBaseAddress( Uri orgBaseAddresss ) { + return new Uri( orgBaseAddresss, "api/v1/" ); } } diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 211f76a6..4eda4087 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using D2L.Bmx.Okta; using D2L.Bmx.Okta.Models; using PuppeteerSharp; @@ -55,15 +54,14 @@ bool bypassBrowserSecurity consoleWriter.WriteParameter( ParameterDescriptions.User, user, userSource ); } - string orgBaseAddress = GetOrgBaseAddress( org ); + var orgBaseAddress = GetOrgBaseAddress( org ); var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( orgBaseAddress ); - if( !ignoreCache && TryAuthenticateFromCache( org, user, oktaClientFactory, out var oktaAuthenticated ) ) { + if( !ignoreCache && TryAuthenticateFromCache( orgBaseAddress, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } if( await GetDssoAuthenticatedClientAsync( orgBaseAddress, - org, user, oktaClientFactory, nonInteractive, @@ -109,8 +107,8 @@ mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value if( authnResponse is AuthenticateResponse.Success successInfo ) { var sessionResp = await oktaAnonymous.CreateSessionAsync( successInfo.SessionToken ); - oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( org, sessionResp.Id ); - TryCacheOktaSession( user, org, sessionResp.Id, sessionResp.ExpiresAt ); + oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( orgBaseAddress, sessionResp.Id ); + TryCacheOktaSession( user, orgBaseAddress.Host, sessionResp.Id, sessionResp.ExpiresAt ); return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } @@ -121,31 +119,30 @@ mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value throw new UnreachableException( $"Unexpected response type: {authnResponse.GetType()}" ); } - private static string GetOrgBaseAddress( string org ) { + private static Uri GetOrgBaseAddress( string org ) { return org.Contains( '.' ) - ? $"https://{org}/" - : $"https://{org}.okta.com/"; + ? new Uri( $"https://{org}/" ) + : new Uri( $"https://{org}.okta.com/" ); } private bool TryAuthenticateFromCache( - string org, + Uri orgBaseAddress, string user, IOktaClientFactory oktaClientFactory, [NotNullWhen( true )] out IOktaAuthenticatedClient? oktaAuthenticated ) { - string? sessionId = GetCachedOktaSessionId( user, org ); + string? sessionId = GetCachedOktaSessionId( user, orgBaseAddress.Host ); if( string.IsNullOrEmpty( sessionId ) ) { oktaAuthenticated = null; return false; } - oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( org, sessionId ); + oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( orgBaseAddress, sessionId ); return true; } private async Task GetDssoAuthenticatedClientAsync( - string orgBaseAddress, - string org, + Uri orgUrl, string user, IOktaClientFactory oktaClientFactory, bool nonInteractive, @@ -161,31 +158,26 @@ bool experimentalBypassBrowserSecurity } using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); var sessionIdTcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); - var userEmailTcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); string? sessionId; - string? userEmail; try { using var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); - var baseUrl = new Uri( orgBaseAddress ); int attempt = 1; page.Load += ( _, _ ) => _ = GetSessionCookieAsync(); - page.Response += ( _, response ) => _ = GetOktaUserEmailAsync( response.Response ); - await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); + await page.GoToAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token ); sessionId = await sessionIdTcs.Task; - userEmail = await userEmailTcs.Task; async Task GetSessionCookieAsync() { var url = new Uri( page.Url ); - if( url.Host == baseUrl.Host ) { + if( url.Host == orgUrl.Host ) { string title = await page.GetTitleAsync().WaitAsync( cancellationTokenSource.Token ); // DSSO can sometimes takes more than one attempt. // If the path is '/' with 'sign in' in the title, it means DSSO is not available and we should stop retrying. if( title.Contains( "sign in", StringComparison.OrdinalIgnoreCase ) ) { if( attempt < 3 && url.AbsolutePath != "/" ) { attempt++; - await page.GoToAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); + await page.GoToAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token ); } else { consoleWriter.WriteWarning( "WARNING: Could not authenticate with Okta using Desktop Single Sign-On." ); @@ -194,24 +186,15 @@ async Task GetSessionCookieAsync() { return; } } - var cookies = await page.GetCookiesAsync( orgBaseAddress ).WaitAsync( cancellationTokenSource.Token ); + var cookies = await page.GetCookiesAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token ); if( Array.Find( cookies, c => c.Name == "sid" )?.Value is string sid ) { sessionIdTcs.SetResult( sid ); } } - async Task GetOktaUserEmailAsync( IResponse response ) { - if( response.Url.Contains( $"{orgBaseAddress}enduser/api/v1/home" ) ) { - string content = await response.TextAsync().WaitAsync( cancellationTokenSource.Token ); - var home = JsonSerializer.Deserialize( content, JsonCamelCaseContext.Default.OktaHomeResponse ); - if( home is not null ) { - userEmailTcs.SetResult( home.Login ); - } - } - } } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" WARNING: Timed out when trying to create Okta session through Desktop Single Sign-On. - Check if the org '{orgBaseAddress}' is correct. If running BMX with elevated privileges, + Check if the org '{orgUrl}' is correct. If running BMX with elevated privileges, rerun the command with the '--experimental-bypass-browser-security' flag """ ); @@ -229,18 +212,20 @@ with elevated privileges. Rerun the command with the '--experimental-bypass-brow return null; } - if( sessionId is null || userEmail is null ) { + if( sessionId is null ) { return null; - } else if( !OktaUserMatchesProvided( userEmail, user ) ) { + } + + var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( orgUrl, sessionId ); + var oktaSession = await oktaAuthenticatedClient.GetCurrentOktaSessionAsync(); + if( !OktaUserMatchesProvided( oktaSession.Login, user ) ) { consoleWriter.WriteWarning( - "WARNING: Could not create Okta session using Desktop Single Sign-On as " - + $"provided Okta user '{StripLoginDomain( user )}' does not match user '{StripLoginDomain( userEmail )}'." ); + "WARNING: Could not create Okta session using Desktop Single Sign-On as provided Okta user " + + $"'{StripLoginDomain( user )}' does not match user '{StripLoginDomain( oktaSession.Login )}'." ); return null; } - var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( orgBaseAddress, sessionId ); - var expiresAt = ( await oktaAuthenticatedClient.GetCurrentOktaSessionAsync() ).ExpiresAt; - TryCacheOktaSession( user, org, sessionId, expiresAt ); + TryCacheOktaSession( user, orgUrl.Host, sessionId, oktaSession.ExpiresAt ); return oktaAuthenticatedClient; } @@ -269,7 +254,7 @@ No config file found. Your Okta session will not be cached. private void CacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { var session = new OktaSessionCache( userId, org, sessionId, expiresAt ); var sessionsToCache = ReadOktaSessionCacheFile(); - sessionsToCache = sessionsToCache.Where( session => session.UserId != userId && session.Org != org ) + sessionsToCache = sessionsToCache.Where( session => session.UserId != userId || session.Org != org ) .ToList(); sessionsToCache.Add( session ); From 7a68852f84a854888c15fdc0fd1fe520ee71c552 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 16:14:12 -0400 Subject: [PATCH 31/49] remove OktaHomeResponse model --- src/D2L.Bmx/JsonSerializerContext.cs | 1 - src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs | 5 ----- 2 files changed, 6 deletions(-) delete mode 100644 src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs diff --git a/src/D2L.Bmx/JsonSerializerContext.cs b/src/D2L.Bmx/JsonSerializerContext.cs index dbd8316f..4466f472 100644 --- a/src/D2L.Bmx/JsonSerializerContext.cs +++ b/src/D2L.Bmx/JsonSerializerContext.cs @@ -20,7 +20,6 @@ namespace D2L.Bmx; [JsonSerializable( typeof( List ) )] [JsonSerializable( typeof( UpdateCheckCache ) )] [JsonSerializable( typeof( List ) )] -[JsonSerializable( typeof( OktaHomeResponse ) )] internal partial class JsonCamelCaseContext : JsonSerializerContext { } diff --git a/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs b/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs deleted file mode 100644 index 4ecdf309..00000000 --- a/src/D2L.Bmx/Okta/Models/OktaHomeResponse.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace D2L.Bmx.Okta.Models; - -internal record OktaHomeResponse( - string Login -); From 6dba5c1d010e49606dd1e90fde747d1c1c241687 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 19 Sep 2024 16:15:55 -0400 Subject: [PATCH 32/49] rename to orgUrl --- src/D2L.Bmx/OktaAuthenticator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 4eda4087..f19945ce 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -54,14 +54,14 @@ bool bypassBrowserSecurity consoleWriter.WriteParameter( ParameterDescriptions.User, user, userSource ); } - var orgBaseAddress = GetOrgBaseAddress( org ); - var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( orgBaseAddress ); + var orgUrl = GetOrgBaseAddress( org ); + var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( orgUrl ); - if( !ignoreCache && TryAuthenticateFromCache( orgBaseAddress, user, oktaClientFactory, out var oktaAuthenticated ) ) { + if( !ignoreCache && TryAuthenticateFromCache( orgUrl, user, oktaClientFactory, out var oktaAuthenticated ) ) { return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } if( await GetDssoAuthenticatedClientAsync( - orgBaseAddress, + orgUrl, user, oktaClientFactory, nonInteractive, @@ -107,8 +107,8 @@ mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value if( authnResponse is AuthenticateResponse.Success successInfo ) { var sessionResp = await oktaAnonymous.CreateSessionAsync( successInfo.SessionToken ); - oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( orgBaseAddress, sessionResp.Id ); - TryCacheOktaSession( user, orgBaseAddress.Host, sessionResp.Id, sessionResp.ExpiresAt ); + oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( orgUrl, sessionResp.Id ); + TryCacheOktaSession( user, orgUrl.Host, sessionResp.Id, sessionResp.ExpiresAt ); return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated ); } From f46d80035d92ed37804dc4c72c5ee56ab9032a80 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Fri, 20 Sep 2024 12:19:42 -0400 Subject: [PATCH 33/49] dont mention sso --- src/D2L.Bmx/OktaAuthenticator.cs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index f19945ce..a4212b36 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -154,11 +154,11 @@ bool experimentalBypassBrowserSecurity } if( !nonInteractive ) { - Console.Error.WriteLine( "Attempting to automatically login using Okta Desktop Single Sign-On." ); + Console.Error.WriteLine( "Attempting to automatically sign in to Okta." ); } using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) ); var sessionIdTcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously ); - string? sessionId; + string? sessionId = null; try { using var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token ); @@ -180,7 +180,7 @@ async Task GetSessionCookieAsync() { await page.GoToAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token ); } else { consoleWriter.WriteWarning( - "WARNING: Could not authenticate with Okta using Desktop Single Sign-On." ); + "WARNING: Failed to authenticate with Okta when trying to automatically sign in" ); sessionIdTcs.SetResult( null ); } return; @@ -193,23 +193,22 @@ async Task GetSessionCookieAsync() { } } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" - WARNING: Timed out when trying to create Okta session through Desktop Single Sign-On. - Check if the org '{orgUrl}' is correct. If running BMX with elevated privileges, - rerun the command with the '--experimental-bypass-browser-security' flag + WARNING: Timed out when trying to automatically sign in to Okta. Check if the org '{orgUrl}' is correct. + if you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, + consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); - return null; } catch( TargetClosedException ) { consoleWriter.WriteWarning( """ - WARNING: Failed to create Okta session through Desktop Single Sign-On as BMX is likely being run - with elevated privileges. Rerun the command with the '--experimental-bypass-browser-security' flag. + WARNING: Failed to automatically sign in to Okta as BMX is likely being run with elevated privileges. + if you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, + consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); return null; } catch( Exception ) { consoleWriter.WriteWarning( - "WARNING: Unknown error while trying to authenticate with Okta using Desktop Single Sign-On." ); - return null; + "WARNING: Unknown error occurred while trying to automatically sign in with Okta." ); } if( sessionId is null ) { @@ -220,7 +219,7 @@ with elevated privileges. Rerun the command with the '--experimental-bypass-brow var oktaSession = await oktaAuthenticatedClient.GetCurrentOktaSessionAsync(); if( !OktaUserMatchesProvided( oktaSession.Login, user ) ) { consoleWriter.WriteWarning( - "WARNING: Could not create Okta session using Desktop Single Sign-On as provided Okta user " + "WARNING: Could not automatically sign in to Okta as provided Okta user " + $"'{StripLoginDomain( user )}' does not match user '{StripLoginDomain( oktaSession.Login )}'." ); return null; } From fa1fd6d2ea49db296b4520acf16743d90608cd95 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Fri, 20 Sep 2024 12:25:01 -0400 Subject: [PATCH 34/49] simplify login name check --- src/D2L.Bmx/OktaAuthenticator.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index a4212b36..2180a347 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -217,10 +217,12 @@ consider running the command again with the '--experimental-bypass-browser-secur var oktaAuthenticatedClient = oktaClientFactory.CreateAuthenticatedClient( orgUrl, sessionId ); var oktaSession = await oktaAuthenticatedClient.GetCurrentOktaSessionAsync(); - if( !OktaUserMatchesProvided( oktaSession.Login, user ) ) { + string sessionLogin = oktaSession.Login.Split( "@" )[0]; + string providedLogin = user.Split( "@" )[0]; + if( !sessionLogin.Equals( providedLogin, StringComparison.OrdinalIgnoreCase ) ) { consoleWriter.WriteWarning( "WARNING: Could not automatically sign in to Okta as provided Okta user " - + $"'{StripLoginDomain( user )}' does not match user '{StripLoginDomain( oktaSession.Login )}'." ); + + $"'{sessionLogin}' does not match user '{providedLogin}'." ); return null; } @@ -228,16 +230,6 @@ consider running the command again with the '--experimental-bypass-browser-secur return oktaAuthenticatedClient; } - private static string StripLoginDomain( string email ) { - return email.Contains( '@' ) ? email.Split( '@' )[0] : email; - } - - private static bool OktaUserMatchesProvided( string oktaLogin, string providedUser ) { - string adName = StripLoginDomain( oktaLogin ); - string normalizedUser = StripLoginDomain( providedUser ); - return adName.Equals( normalizedUser, StringComparison.OrdinalIgnoreCase ); - } - private bool TryCacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) { if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { CacheOktaSession( userId, org, sessionId, expiresAt ); From fc17c78996a1747e2a05f8b3abe08650d5503b22 Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:15:45 -0400 Subject: [PATCH 35/49] Update src/D2L.Bmx/ParameterDescriptions.cs Co-authored-by: Chenfeng Bao --- src/D2L.Bmx/ParameterDescriptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/ParameterDescriptions.cs b/src/D2L.Bmx/ParameterDescriptions.cs index aca37bbe..4891014b 100644 --- a/src/D2L.Bmx/ParameterDescriptions.cs +++ b/src/D2L.Bmx/ParameterDescriptions.cs @@ -20,5 +20,5 @@ internal static class ParameterDescriptions { """; public const string ExperimentalBypassBrowserSecurity - = "Disable chromium sandbox when running with elevated permissions for Okta Desktop Single Sign-On"; + = "Disable Chromium sandbox when automatically signing into Okta"; } From e479bc88bab9c613c5ce4a61e3e4b3a087c24941 Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:20:48 -0400 Subject: [PATCH 36/49] Update src/D2L.Bmx/OktaAuthenticator.cs Co-authored-by: Chenfeng Bao --- src/D2L.Bmx/OktaAuthenticator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 2180a347..712bbc4e 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -236,9 +236,9 @@ private bool TryCacheOktaSession( string userId, string org, string sessionId, D return true; } consoleWriter.WriteWarning( """ - No config file found. Your Okta session will not be cached. - Consider running `bmx configure` if you own this machine. - """ ); + No config file found. Your Okta session will not be cached. + Consider running `bmx configure` if you own this machine. + """ ); return false; } From d87d349295fc113d14335b322c39d3e4bbf27843 Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:21:06 -0400 Subject: [PATCH 37/49] Update src/D2L.Bmx/OktaAuthenticator.cs Co-authored-by: Chenfeng Bao --- src/D2L.Bmx/OktaAuthenticator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 712bbc4e..f53c2e30 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -194,7 +194,7 @@ async Task GetSessionCookieAsync() { } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" WARNING: Timed out when trying to automatically sign in to Okta. Check if the org '{orgUrl}' is correct. - if you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, + If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); From 754699ecbce20813eea3cabe86cecd7c84abe46b Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:21:13 -0400 Subject: [PATCH 38/49] Update src/D2L.Bmx/OktaAuthenticator.cs Co-authored-by: Chenfeng Bao --- src/D2L.Bmx/OktaAuthenticator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index f53c2e30..2ca2b80b 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -201,7 +201,7 @@ consider running the command again with the '--experimental-bypass-browser-secur } catch( TargetClosedException ) { consoleWriter.WriteWarning( """ WARNING: Failed to automatically sign in to Okta as BMX is likely being run with elevated privileges. - if you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, + If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); From 1054c064ba3522b83a81fb789c72d8cf781bab4e Mon Sep 17 00:00:00 2001 From: gord5500 <90227099+gord5500@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:21:21 -0400 Subject: [PATCH 39/49] Update src/D2L.Bmx/OktaAuthenticator.cs Co-authored-by: Chenfeng Bao --- src/D2L.Bmx/OktaAuthenticator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 2ca2b80b..aaabd456 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -205,7 +205,6 @@ consider running the command again with the '--experimental-bypass-browser-secur consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); - return null; } catch( Exception ) { consoleWriter.WriteWarning( "WARNING: Unknown error occurred while trying to automatically sign in with Okta." ); From 7ec383300e62d9b8449d3fd3c96a3c87f1701f1b Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 07:24:08 -0400 Subject: [PATCH 40/49] don't pass the client factory --- src/D2L.Bmx/OktaAuthenticator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index aaabd456..a88dc266 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -63,7 +63,6 @@ bool bypassBrowserSecurity if( await GetDssoAuthenticatedClientAsync( orgUrl, user, - oktaClientFactory, nonInteractive, bypassBrowserSecurity ) is { } oktaDssoAuthenticated ) { @@ -144,7 +143,6 @@ private bool TryAuthenticateFromCache( private async Task GetDssoAuthenticatedClientAsync( Uri orgUrl, string user, - IOktaClientFactory oktaClientFactory, bool nonInteractive, bool experimentalBypassBrowserSecurity ) { From de1e41f6807a7c26cdf40c17fdcfe0bf07080c3e Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 08:47:19 -0400 Subject: [PATCH 41/49] check elevated permissions on experimental flag --- src/D2L.Bmx/Program.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index f3d20ed7..6452f459 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -2,6 +2,8 @@ using System.CommandLine.Builder; using System.CommandLine.Invocation; using System.CommandLine.Parsing; +using System.Runtime.InteropServices; +using System.Security.Principal; using Amazon.Runtime; using Amazon.SecurityToken; using D2L.Bmx; @@ -21,8 +23,29 @@ // allow no-sandbox argument for chromium to for passwordless auth with elevated permissions var bypassBrowserSecurityOption = new Option( name: "--experimental-bypass-browser-security", + getDefaultValue: () => false, description: ParameterDescriptions.ExperimentalBypassBrowserSecurity ); +[LibraryImport( "libc", EntryPoint = "geteuid" )] +static extern uint GetPosixEuid(); + +bypassBrowserSecurityOption.AddValidator( result => { + bool bypass = result.GetValueForOption( bypassBrowserSecurityOption ); + bool isElevated = false; + if( OperatingSystem.IsWindows() ) { + isElevated = new WindowsPrincipal( WindowsIdentity.GetCurrent() ).IsInRole( WindowsBuiltInRole.Administrator ); + } else if( OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ) { + isElevated = GetPosixEuid() == 0; + } + if( isElevated && !bypass ) { + result.ErrorMessage + = "When running BMX with elevated permissions, --experimental-bypass-browser-security must be set to true"; + } else if( !isElevated && bypass ) { + result.ErrorMessage + = "BMX is not running with elevated permissions, so --experimental-bypass-browser-security must be set to false"; + } +} ); + // bmx login var loginCommand = new Command( "login", "Log into Okta and save an Okta session" ){ orgOption, From 96a9e7d58fbb8b36eede4f639a9665cb56b84829 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 09:02:19 -0400 Subject: [PATCH 42/49] scarier message --- src/D2L.Bmx/Program.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 6452f459..a8e1dbec 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -38,8 +38,10 @@ isElevated = GetPosixEuid() == 0; } if( isElevated && !bypass ) { - result.ErrorMessage - = "When running BMX with elevated permissions, --experimental-bypass-browser-security must be set to true"; + result.ErrorMessage = """ + Running BMX with elevated permissions is only allowed with the '--experimental-bypass-browser-security' flag. + Only include this if you aren't concerned with the security of the host for your Okta organization. + """; } else if( !isElevated && bypass ) { result.ErrorMessage = "BMX is not running with elevated permissions, so --experimental-bypass-browser-security must be set to false"; From 96177a7646037e4c80666a6a562c5da56178c020 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 24 Sep 2024 09:34:15 -0400 Subject: [PATCH 43/49] fix libc call --- src/D2L.Bmx/Program.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index a8e1dbec..828ca03f 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -26,16 +26,13 @@ getDefaultValue: () => false, description: ParameterDescriptions.ExperimentalBypassBrowserSecurity ); -[LibraryImport( "libc", EntryPoint = "geteuid" )] -static extern uint GetPosixEuid(); - bypassBrowserSecurityOption.AddValidator( result => { bool bypass = result.GetValueForOption( bypassBrowserSecurityOption ); bool isElevated = false; if( OperatingSystem.IsWindows() ) { isElevated = new WindowsPrincipal( WindowsIdentity.GetCurrent() ).IsInRole( WindowsBuiltInRole.Administrator ); } else if( OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ) { - isElevated = GetPosixEuid() == 0; + isElevated = NativeMethods.GetPosixEuid() == 0; } if( isElevated && !bypass ) { result.ErrorMessage = """ @@ -307,3 +304,8 @@ Running BMX with elevated permissions is only allowed with the '--experimental-b } ) .Build() .InvokeAsync( args ); + +static partial class NativeMethods { + [LibraryImport("libc", EntryPoint = "geteuid")] + public static partial uint GetPosixEuid(); +} From 5b05ca9e933df13754fddbfd50a96f908b395fc3 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 09:38:05 -0400 Subject: [PATCH 44/49] format --- src/D2L.Bmx/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 828ca03f..b5d11e6c 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -305,7 +305,7 @@ Running BMX with elevated permissions is only allowed with the '--experimental-b .Build() .InvokeAsync( args ); -static partial class NativeMethods { - [LibraryImport("libc", EntryPoint = "geteuid")] +internal static partial class NativeMethods { + [LibraryImport( "libc", EntryPoint = "geteuid" )] public static partial uint GetPosixEuid(); } From afef1842a8be2f37128ccfd3e7931c53772dbe90 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 09:52:39 -0400 Subject: [PATCH 45/49] wording --- src/D2L.Bmx/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index b5d11e6c..293a8d40 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -41,7 +41,7 @@ Running BMX with elevated permissions is only allowed with the '--experimental-b """; } else if( !isElevated && bypass ) { result.ErrorMessage - = "BMX is not running with elevated permissions, so --experimental-bypass-browser-security must be set to false"; + = "BMX is not running with elevated permissions, so don't provide the '--experimental-bypass-browser-security' flag"; } } ); From c956f2a39152292b12c28c0e87e7dc92de542080 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 11:04:05 -0400 Subject: [PATCH 46/49] don't kill on bad setup --- src/D2L.Bmx/OktaAuthenticator.cs | 19 +++++++++++++++++-- src/D2L.Bmx/Program.cs | 26 -------------------------- src/D2L.Bmx/UserPrivilieges.cs | 20 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 28 deletions(-) create mode 100644 src/D2L.Bmx/UserPrivilieges.cs diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index a88dc266..2cbec7ac 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -144,9 +144,24 @@ private bool TryAuthenticateFromCache( Uri orgUrl, string user, bool nonInteractive, - bool experimentalBypassBrowserSecurity + bool bypassBrowserSecurity ) { - await using IBrowser? browser = await Browser.LaunchBrowserAsync( experimentalBypassBrowserSecurity ); + + bool isAdmin = UserPrivileges.HasElevatedPermissions(); + if( isAdmin && !bypassBrowserSecurity ) { + consoleWriter.WriteWarning( $""" + BMX is being run with elevated privileges and is unable to automatically sign in to Okta. + If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, + consider running the command again with the '--experimental-bypass-browser-security' flag. + """ + ); + return null; + } else if( !isAdmin && bypassBrowserSecurity ) { + // We want to avoid providing '--no-sandbox' to chromium unless absolutely neccessary. + bypassBrowserSecurity = false; + } + + await using IBrowser? browser = await Browser.LaunchBrowserAsync( bypassBrowserSecurity ); if( browser is null ) { return null; } diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 293a8d40..01a89706 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -2,8 +2,6 @@ using System.CommandLine.Builder; using System.CommandLine.Invocation; using System.CommandLine.Parsing; -using System.Runtime.InteropServices; -using System.Security.Principal; using Amazon.Runtime; using Amazon.SecurityToken; using D2L.Bmx; @@ -26,25 +24,6 @@ getDefaultValue: () => false, description: ParameterDescriptions.ExperimentalBypassBrowserSecurity ); -bypassBrowserSecurityOption.AddValidator( result => { - bool bypass = result.GetValueForOption( bypassBrowserSecurityOption ); - bool isElevated = false; - if( OperatingSystem.IsWindows() ) { - isElevated = new WindowsPrincipal( WindowsIdentity.GetCurrent() ).IsInRole( WindowsBuiltInRole.Administrator ); - } else if( OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ) { - isElevated = NativeMethods.GetPosixEuid() == 0; - } - if( isElevated && !bypass ) { - result.ErrorMessage = """ - Running BMX with elevated permissions is only allowed with the '--experimental-bypass-browser-security' flag. - Only include this if you aren't concerned with the security of the host for your Okta organization. - """; - } else if( !isElevated && bypass ) { - result.ErrorMessage - = "BMX is not running with elevated permissions, so don't provide the '--experimental-bypass-browser-security' flag"; - } -} ); - // bmx login var loginCommand = new Command( "login", "Log into Okta and save an Okta session" ){ orgOption, @@ -304,8 +283,3 @@ Running BMX with elevated permissions is only allowed with the '--experimental-b } ) .Build() .InvokeAsync( args ); - -internal static partial class NativeMethods { - [LibraryImport( "libc", EntryPoint = "geteuid" )] - public static partial uint GetPosixEuid(); -} diff --git a/src/D2L.Bmx/UserPrivilieges.cs b/src/D2L.Bmx/UserPrivilieges.cs new file mode 100644 index 00000000..64fa98d5 --- /dev/null +++ b/src/D2L.Bmx/UserPrivilieges.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace D2L.Bmx; + +internal static partial class UserPrivileges { + + [LibraryImport( "libc", EntryPoint = "geteuid" )] + internal static partial uint GetPosixEuid(); + + internal static bool HasElevatedPermissions() { + bool isElevated = false; + if( OperatingSystem.IsWindows() ) { + isElevated = new WindowsPrincipal( WindowsIdentity.GetCurrent() ).IsInRole( WindowsBuiltInRole.Administrator ); + } else if( OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ) { + isElevated = GetPosixEuid() == 0; + } + return isElevated; + } +} From 9d717e8b082e54d31b67ac174d8cdede938afde7 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 11:06:04 -0400 Subject: [PATCH 47/49] mend --- src/D2L.Bmx/OktaAuthenticator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 2cbec7ac..812ed068 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -147,8 +147,8 @@ private bool TryAuthenticateFromCache( bool bypassBrowserSecurity ) { - bool isAdmin = UserPrivileges.HasElevatedPermissions(); - if( isAdmin && !bypassBrowserSecurity ) { + bool hasElevatedPermissions = UserPrivileges.HasElevatedPermissions(); + if( hasElevatedPermissions && !bypassBrowserSecurity ) { consoleWriter.WriteWarning( $""" BMX is being run with elevated privileges and is unable to automatically sign in to Okta. If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, @@ -156,7 +156,7 @@ consider running the command again with the '--experimental-bypass-browser-secur """ ); return null; - } else if( !isAdmin && bypassBrowserSecurity ) { + } else if( !hasElevatedPermissions && bypassBrowserSecurity ) { // We want to avoid providing '--no-sandbox' to chromium unless absolutely neccessary. bypassBrowserSecurity = false; } From e47e88bd14d1d372980632d8227f443412b4a486 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 11:06:42 -0400 Subject: [PATCH 48/49] mend --- src/D2L.Bmx/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 01a89706..f3d20ed7 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -21,7 +21,6 @@ // allow no-sandbox argument for chromium to for passwordless auth with elevated permissions var bypassBrowserSecurityOption = new Option( name: "--experimental-bypass-browser-security", - getDefaultValue: () => false, description: ParameterDescriptions.ExperimentalBypassBrowserSecurity ); // bmx login From 521a53f4146171c78f046b9dcd0f2be5e09afd14 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 13:42:31 -0400 Subject: [PATCH 49/49] reword the warning message --- src/D2L.Bmx/OktaAuthenticator.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index 812ed068..68a3cb09 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -151,8 +151,8 @@ bool bypassBrowserSecurity if( hasElevatedPermissions && !bypassBrowserSecurity ) { consoleWriter.WriteWarning( $""" BMX is being run with elevated privileges and is unable to automatically sign in to Okta. - If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, - consider running the command again with the '--experimental-bypass-browser-security' flag. + If you want to automatically sign in, and aren't concerned with the security of {orgUrl.Host}, + consider using '--experimental-bypass-browser-security' flag. """ ); return null; @@ -193,7 +193,7 @@ async Task GetSessionCookieAsync() { await page.GoToAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token ); } else { consoleWriter.WriteWarning( - "WARNING: Failed to authenticate with Okta when trying to automatically sign in" ); + "Failed to authenticate with Okta when trying to automatically sign in" ); sessionIdTcs.SetResult( null ); } return; @@ -206,21 +206,21 @@ async Task GetSessionCookieAsync() { } } catch( TaskCanceledException ) { consoleWriter.WriteWarning( $""" - WARNING: Timed out when trying to automatically sign in to Okta. Check if the org '{orgUrl}' is correct. + Timed out when trying to automatically sign in to Okta. Check if the org '{orgUrl}' is correct. If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); } catch( TargetClosedException ) { consoleWriter.WriteWarning( """ - WARNING: Failed to automatically sign in to Okta as BMX is likely being run with elevated privileges. + Failed to automatically sign in to Okta as BMX is likely being run with elevated privileges. If you have to run BMX with elevated privileges, and aren't concerned with the security of {orgUrl.Host}, consider running the command again with the '--experimental-bypass-browser-security' flag. """ ); } catch( Exception ) { consoleWriter.WriteWarning( - "WARNING: Unknown error occurred while trying to automatically sign in with Okta." ); + "Unknown error occurred while trying to automatically sign in with Okta." ); } if( sessionId is null ) { @@ -233,7 +233,7 @@ consider running the command again with the '--experimental-bypass-browser-secur string providedLogin = user.Split( "@" )[0]; if( !sessionLogin.Equals( providedLogin, StringComparison.OrdinalIgnoreCase ) ) { consoleWriter.WriteWarning( - "WARNING: Could not automatically sign in to Okta as provided Okta user " + "Could not automatically sign in to Okta as provided Okta user " + $"'{sessionLogin}' does not match user '{providedLogin}'." ); return null; }