Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/D2L.Bmx/BmxConfig.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
namespace D2L.Bmx;

internal static class PasswordlessTimeoutDefaults {
public const int Min = 5;
public const int Max = 30;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't necessarily cap max at 30. If people want to wait a minute I wouldn't stop them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeaaa I was wondering what to do for this. I'll bump it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you updated the default not the max?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, nit, and I'm not too sure either, but this seems more like "constants" rather than "config"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have a constants class

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was just looking at the config parsing logic - I'm not sure there's much value in setting a non-zero min either?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I'm leaning towards is no min or max.
We can keep the "0 = disable" behaviour (which is a logical conclusion from the config) and check timeout >=0 as a sanity check.
But I'm not seeing value in a non-zero min or any max.
Keep things simpler.

@gord5500 gord5500 Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can remove the limits. For me I just never saw it succeed below 5 seconds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I removed the limits and tried rewording the prompt for setting the value to 0 means disabling passwordless auth. Also the timing changes to always be timeout / 2.0

public const int Default = 30;
}

internal record BmxConfig(
string? Org,
string? User,
string? Account,
string? Role,
string? Profile,
int? Duration
int? Duration,
int? PasswordlessTimeout
);
20 changes: 19 additions & 1 deletion src/D2L.Bmx/BmxConfigProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,28 @@ public BmxConfig GetConfiguration() {
duration = configDuration;
}

int? passwordlessTimeout = null;
if( !string.IsNullOrEmpty( data.Global["passwordlessTimeout"] ) ) {
if( !int.TryParse( data.Global["passwordlessTimeout"], out int configTimeout )
|| configTimeout < 0
|| ( configTimeout > 0 && configTimeout < PasswordlessTimeoutDefaults.Min )

@cfbao cfbao Jun 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only need a single equality check with 0?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| configTimeout > PasswordlessTimeoutDefaults.Max ) {
throw new BmxException(
"Invalid passwordlessTimeout in config."
+ $" Must be 0 (disabled) or between {PasswordlessTimeoutDefaults.Min}"
+ $" and {PasswordlessTimeoutDefaults.Max} seconds." );
}
passwordlessTimeout = configTimeout;
}

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,
PasswordlessTimeout: passwordlessTimeout
);
}

Expand All @@ -75,6 +90,9 @@ public void SaveConfiguration( BmxConfig config ) {
if( config.Duration.HasValue ) {
data.Global["duration"] = $"{config.Duration}";
}
if( config.PasswordlessTimeout.HasValue ) {
data.Global["passwordlessTimeout"] = $"{config.PasswordlessTimeout}";
}

fs.Position = 0;
fs.SetLength( 0 );
Expand Down
8 changes: 7 additions & 1 deletion src/D2L.Bmx/ConfigureHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public void Handle(
string? org,
string? user,
int? duration,
int? passwordlessTimeout,
bool nonInteractive
) {

Expand All @@ -24,13 +25,18 @@ bool nonInteractive
duration = consolePrompter.PromptDuration();
}

if( passwordlessTimeout is null && !nonInteractive ) {
passwordlessTimeout = consolePrompter.PromptPasswordlessTimeout();
}

BmxConfig config = new(
Org: org,
User: user,
Account: null,
Role: null,
Profile: null,
Duration: duration
Duration: duration,
PasswordlessTimeout: passwordlessTimeout
);
configProvider.SaveConfiguration( config );
Console.WriteLine( "Your configuration has been created. Okta sessions will now also be cached." );
Expand Down
20 changes: 20 additions & 0 deletions src/D2L.Bmx/ConsolePrompter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal interface IConsolePrompter {
string PromptUser( bool allowEmptyInput );
string PromptPassword();
int? PromptDuration();
int? PromptPasswordlessTimeout();
string PromptAccount( string[] accounts );
string PromptRole( string[] roles );
OktaMfaFactor SelectMfa( OktaMfaFactor[] mfaOptions );
Expand Down Expand Up @@ -63,6 +64,25 @@ string IConsolePrompter.PromptPassword() {
return duration;
}

int? IConsolePrompter.PromptPasswordlessTimeout() {
Console.Error.Write(
"Okta passwordless (DSSO) timeout in seconds"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use ParameterDescriptions here?

+ " (optional, 0 to disable,"
+ $" {PasswordlessTimeoutDefaults.Min}-{PasswordlessTimeoutDefaults.Max},"
+ $" default: {PasswordlessTimeoutDefaults.Default}): " );
string? input = Console.ReadLine();
if( input is null || string.IsNullOrWhiteSpace( input ) ) {
return null;
}
if( int.TryParse( input, out int timeout )
&& ( timeout == 0
|| ( timeout >= PasswordlessTimeoutDefaults.Min
&& timeout <= PasswordlessTimeoutDefaults.Max ) ) ) {
return timeout;
}
return null;
}

string IConsolePrompter.PromptAccount( string[] accounts ) {
if( accounts.Length == 0 ) {
throw new BmxException( "No AWS account available" );
Expand Down
6 changes: 4 additions & 2 deletions src/D2L.Bmx/LoginHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ OktaAuthenticator oktaAuth
) {
public async Task HandleAsync(
string? org,
string? user
string? user,
int? passwordlessTimeout
) {
if( !File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) {
throw new BmxException(
Expand All @@ -16,7 +17,8 @@ await oktaAuth.AuthenticateAsync(
org,
user,
nonInteractive: false,
ignoreCache: true
ignoreCache: true,
passwordlessTimeout: passwordlessTimeout
);
Console.WriteLine( "Successfully logged in and Okta session has been cached." );
}
Expand Down
56 changes: 35 additions & 21 deletions src/D2L.Bmx/OktaAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public async Task<OktaAuthenticatedContext> AuthenticateAsync(
string? org,
string? user,
bool nonInteractive,
bool ignoreCache
bool ignoreCache,
int? passwordlessTimeout
) {
var orgSource = ParameterSource.CliArg;
if( string.IsNullOrEmpty( org ) && !string.IsNullOrEmpty( config.Org ) ) {
Expand Down Expand Up @@ -65,19 +66,30 @@ bool ignoreCache
OperatingSystem.IsWindows()
&& browserLauncher.TryGetPathToBrowser( out string? browserPath )
) {
if( !nonInteractive ) {
Console.Error.WriteLine( "Attempting Okta passwordless authentication..." );
}
oktaAuthenticated = await GetDssoAuthenticatedClientAsync(
orgUrl,
user,
browserPath
);
if( oktaAuthenticated is not null ) {
return new( Org: org, User: user, Client: oktaAuthenticated );
}
if( !nonInteractive ) {
Console.Error.WriteLine( "Falling back to Okta password authentication..." );
int resolvedTimeout = passwordlessTimeout
?? config.PasswordlessTimeout
?? PasswordlessTimeoutDefaults.Default;

if( resolvedTimeout == 0 ) {
if( BmxEnvironment.IsDebug ) {
messageWriter.WriteWarning( "Okta passwordless authentication disabled via configuration" );
}
} else {
if( !nonInteractive ) {
Console.Error.WriteLine( "Attempting Okta passwordless authentication..." );
}
oktaAuthenticated = await GetDssoAuthenticatedClientAsync(
orgUrl,
user,
browserPath,
resolvedTimeout
);
if( oktaAuthenticated is not null ) {
return new( Org: org, User: user, Client: oktaAuthenticated );
}
if( !nonInteractive ) {
Console.Error.WriteLine( "Falling back to Okta password authentication..." );
}
}
} else if( BmxEnvironment.IsDebug ) {
messageWriter.WriteWarning( "No suitable browser found for Okta passwordless authentication" );
Expand Down Expand Up @@ -115,12 +127,13 @@ private bool TryAuthenticateFromCache(
private async Task<IOktaAuthenticatedClient?> GetDssoAuthenticatedClientAsync(
Uri orgUrl,
string user,
string browserPath
string browserPath,
int timeoutSeconds
) {
string? sessionId = null;

try {
sessionId = await GetSessionIdFromBrowserAsync( browserPath, orgUrl );
sessionId = await GetSessionIdFromBrowserAsync( browserPath, orgUrl, timeoutSeconds );
} catch( TaskCanceledException ex ) {
if( BmxEnvironment.IsDebug ) {
messageWriter.WriteWarning( $"Okta passwordless authentication timed out. \n{ex}" );
Expand Down Expand Up @@ -158,20 +171,21 @@ The provided Okta user '{providedLogin}' does not match the system configured pa
return oktaAuthenticatedClient;
}

private async Task<string?> GetSessionIdFromBrowserAsync( string browserPath, Uri orgUrl ) {
private async Task<string?> GetSessionIdFromBrowserAsync( string browserPath, Uri orgUrl, int timeoutSeconds ) {
if( BmxEnvironment.IsDebug ) {
messageWriter.WriteWarning( $"Launching browser: {browserPath}" );
}
await using var browser = await browserLauncher.LaunchAsync( browserPath );

var sessionIdTcs = new TaskCompletionSource<string?>( TaskCreationOptions.RunContinuationsAsynchronously );

// cancel if the total time exceeds 15 seconds, including all page loads and retries
using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) );
// cancel if the total time exceeds the configured timeout, including all page loads and retries
using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( timeoutSeconds ) );
cancellationTokenSource.Token.Register( () => sessionIdTcs.TrySetCanceled() );

// cancel if we can't load the first page for 6 seconds
using var pageTimer = new System.Timers.Timer( TimeSpan.FromSeconds( 6 ) ) { AutoReset = false };
// cancel if we can't load the first page within a derived timeout
using var pageTimer = new System.Timers.Timer(
TimeSpan.FromSeconds( Math.Min( 6, timeoutSeconds / 2 ) ) ) { AutoReset = false };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 is the min we should wait. Here should be Math.Max

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's also a 3 sec wait somewhere below that need to be bumped up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe the timeout that people actually hit is the 15 total timeout, but rather the 6 and 3 page load timeout.

pageTimer.Elapsed += ( _, _ ) => cancellationTokenSource.Cancel();
pageTimer.Start();

Expand Down
4 changes: 4 additions & 0 deletions src/D2L.Bmx/ParameterDescriptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ internal static class ParameterDescriptions {
public const string NonInteractive = "Run non-interactively without showing any prompts";
public const string CacheAwsCredentials =
"Enables Cache for AWS tokens. Implied if '--use-credential-process' is supplied";
public static readonly string PasswordlessTimeout =
"Timeout for Okta passwordless (DSSO) authentication in seconds"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever say "DSSO" in user facing messages?

+ $" (0 to disable, {PasswordlessTimeoutDefaults.Min}-{PasswordlessTimeoutDefaults.Max},"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message not clear to me: 0 to disable the timeout control or to disable passwordless auth?

+ $" default: {PasswordlessTimeoutDefaults.Default})";
public const string UseCredentialProcess = """
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.
Expand Down
6 changes: 4 additions & 2 deletions src/D2L.Bmx/PrintHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ public async Task HandleAsync(
int? duration,
bool nonInteractive,
string? format,
bool cacheAwsCredentials
bool cacheAwsCredentials,
int? passwordlessTimeout
) {
var oktaContext = await oktaAuth.AuthenticateAsync(
org: org,
user: user,
nonInteractive: nonInteractive,
ignoreCache: false
ignoreCache: false,
passwordlessTimeout: passwordlessTimeout
);
var awsCreds = ( await awsCredsCreator.CreateAwsCredsAsync(
okta: oktaContext,
Expand Down
37 changes: 34 additions & 3 deletions src/D2L.Bmx/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,34 @@
name: "--user",
description: ParameterDescriptions.User );

var passwordlessTimeoutOption = new Option<int?>(
name: "--passwordless-timeout",
description: ParameterDescriptions.PasswordlessTimeout );

passwordlessTimeoutOption.AddValidator( result => {
if( result.Tokens is [Token token, ..]
&& int.TryParse( token.Value, out int timeout ) ) {
if( timeout != 0
&& ( timeout < PasswordlessTimeoutDefaults.Min
|| timeout > PasswordlessTimeoutDefaults.Max ) ) {
result.ErrorMessage =
"Passwordless timeout must be 0 (disabled)"
+ $" or between {PasswordlessTimeoutDefaults.Min}"
+ $" and {PasswordlessTimeoutDefaults.Max} seconds";
}
} else if( result.Tokens.Count > 0 ) {
result.ErrorMessage =
"Passwordless timeout must be 0 (disabled)"
+ $" or between {PasswordlessTimeoutDefaults.Min}"
+ $" and {PasswordlessTimeoutDefaults.Max} seconds";
}
} );

// bmx login
var loginCommand = new Command( "login", "Log into Okta and save an Okta session" ) {
orgOption,
userOption,
passwordlessTimeoutOption,
};
loginCommand.SetHandler( ( InvocationContext context ) => {
var messageWriter = new MessageWriter();
Expand All @@ -36,7 +60,8 @@
) );
return handler.HandleAsync(
org: context.ParseResult.GetValueForOption( orgOption ),
user: context.ParseResult.GetValueForOption( userOption )
user: context.ParseResult.GetValueForOption( userOption ),
passwordlessTimeout: context.ParseResult.GetValueForOption( passwordlessTimeoutOption )
);
} );

Expand Down Expand Up @@ -64,6 +89,7 @@
orgOption,
userOption,
durationOption,
passwordlessTimeoutOption,
nonInteractiveOption,
};

Expand All @@ -75,6 +101,7 @@
org: context.ParseResult.GetValueForOption( orgOption ),
user: context.ParseResult.GetValueForOption( userOption ),
duration: context.ParseResult.GetValueForOption( durationOption ),
passwordlessTimeout: context.ParseResult.GetValueForOption( passwordlessTimeoutOption ),
nonInteractive: context.ParseResult.GetValueForOption( nonInteractiveOption )
);
return Task.CompletedTask;
Expand Down Expand Up @@ -120,6 +147,7 @@
userOption,
nonInteractiveOption,
cacheAwsCredentialsOption,
passwordlessTimeoutOption,
};

printCommand.SetHandler( ( InvocationContext context ) => {
Expand Down Expand Up @@ -149,7 +177,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 ),
passwordlessTimeout: context.ParseResult.GetValueForOption( passwordlessTimeoutOption )
);
} );

Expand All @@ -175,6 +204,7 @@
nonInteractiveOption,
cacheAwsCredentialsOption,
useCredentialProcessOption,
passwordlessTimeoutOption,
};

writeCommand.SetHandler( ( InvocationContext context ) => {
Expand Down Expand Up @@ -210,7 +240,8 @@
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 ),
passwordlessTimeout: context.ParseResult.GetValueForOption( passwordlessTimeoutOption )
);
} );

Expand Down
6 changes: 4 additions & 2 deletions src/D2L.Bmx/WriteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ public async Task HandleAsync(
string? output,
string? profile,
bool cacheAwsCredentials,
bool useCredentialProcess
bool useCredentialProcess,
int? passwordlessTimeout
) {
cacheAwsCredentials = cacheAwsCredentials || useCredentialProcess;

var oktaContext = await oktaAuth.AuthenticateAsync(
org: org,
user: user,
nonInteractive: nonInteractive,
ignoreCache: false
ignoreCache: false,
passwordlessTimeout: passwordlessTimeout
);
var awsCredsInfo = await awsCredsCreator.CreateAwsCredsAsync(
okta: oktaContext,
Expand Down
Loading