From e9f18ac7aaffd4a1f412a9535a61792d12cec9a8 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 17 Sep 2024 09:16:14 -0400 Subject: [PATCH 01/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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 e07c26db5308aa447e4e8ca0c8295f4f7b761f95 Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 09:44:01 -0400 Subject: [PATCH 41/42] path join and don't throw if no browser found on linux / mac --- src/D2L.Bmx/ParentProcess.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/D2L.Bmx/ParentProcess.cs b/src/D2L.Bmx/ParentProcess.cs index c3f794b0..8d421b03 100644 --- a/src/D2L.Bmx/ParentProcess.cs +++ b/src/D2L.Bmx/ParentProcess.cs @@ -25,7 +25,6 @@ internal partial class ParentProcess { [LibraryImport( "libc", EntryPoint = "getppid" )] private static partial int GetPosixParentProcessId(); - // Uses the same approach of calling NtQueryInformationProcess as in the PowerShell library // https://github.com/PowerShell/PowerShell/blob/26f621952910e33840efb0c539fbef1e2a467a0d/src/System.Management.Automation/engine/ProcessCodeMethods.cs private static int GetWindowsParentProcessId() { From 5bf12bdc43c54889444a62e72771bfb25ab55e0e Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Tue, 24 Sep 2024 09:45:14 -0400 Subject: [PATCH 42/42] mend --- src/D2L.Bmx/Browser.cs | 10 +++++----- src/D2L.Bmx/ParentProcess.cs | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/D2L.Bmx/Browser.cs b/src/D2L.Bmx/Browser.cs index 7403a0fd..af86cdc2 100644 --- a/src/D2L.Bmx/Browser.cs +++ b/src/D2L.Bmx/Browser.cs @@ -13,8 +13,8 @@ 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", + "Microsoft\\Edge\\Application\\msedge.exe", + "Google\\Chrome\\Application\\chrome.exe", ]; private static readonly string[] MacPaths = [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", @@ -45,7 +45,7 @@ public static class Browser { foreach( string environmentVariable in WindowsEnvironmentVariables ) { string? prefix = Environment.GetEnvironmentVariable( environmentVariable ); if( prefix is not null ) { - string path = prefix + windowsPartialPath; + string path = Path.Join( prefix, windowsPartialPath ); if( File.Exists( path ) ) { return path; } @@ -53,9 +53,9 @@ public static class Browser { } } } else if( OperatingSystem.IsMacOS() ) { - return MacPaths.First( File.Exists ); + return Array.Find( MacPaths, File.Exists ); } else if( OperatingSystem.IsLinux() ) { - return LinuxPaths.First( File.Exists ); + return Array.Find( LinuxPaths, File.Exists ); } return null; } diff --git a/src/D2L.Bmx/ParentProcess.cs b/src/D2L.Bmx/ParentProcess.cs index 8d421b03..c3f794b0 100644 --- a/src/D2L.Bmx/ParentProcess.cs +++ b/src/D2L.Bmx/ParentProcess.cs @@ -25,6 +25,7 @@ internal partial class ParentProcess { [LibraryImport( "libc", EntryPoint = "getppid" )] private static partial int GetPosixParentProcessId(); + // Uses the same approach of calling NtQueryInformationProcess as in the PowerShell library // https://github.com/PowerShell/PowerShell/blob/26f621952910e33840efb0c539fbef1e2a467a0d/src/System.Management.Automation/engine/ProcessCodeMethods.cs private static int GetWindowsParentProcessId() {