Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add per-page timeout for passwordless auth #492

Merged
merged 2 commits into from
Oct 23, 2024
Merged
Changes from all 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
102 changes: 60 additions & 42 deletions src/D2L.Bmx/OktaAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,51 +114,10 @@ private bool TryAuthenticateFromCache(
string user,
string browserPath
) {
await using var browser = await browserLauncher.LaunchAsync( browserPath );

using var cancellationTokenSource = new CancellationTokenSource( TimeSpan.FromSeconds( 15 ) );
var sessionIdTcs = new TaskCompletionSource<string?>( TaskCreationOptions.RunContinuationsAsynchronously );
cancellationTokenSource.Token.Register( () => sessionIdTcs.TrySetCanceled() );
string? sessionId = null;

try {
using var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token );
int attempt = 1;

page.Load += ( _, _ ) => _ = GetSessionCookieAsync();
await page.GoToAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token );
sessionId = await sessionIdTcs.Task;

async Task GetSessionCookieAsync() {
var url = new Uri( page.Url );
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( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token );
} else {
if( BmxEnvironment.IsDebug ) {
if( url.AbsolutePath == "/" ) {
consoleWriter.WriteWarning(
"Okta passwordless authentication is not available."
);
} else {
consoleWriter.WriteWarning( "Okta passwordless authentication failed" );
}
}
sessionIdTcs.SetResult( null );
}
return;
}
}
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 );
}
}
sessionId = await GetSessionIdFromBrowserAsync( browserPath, orgUrl );
} catch( TaskCanceledException ) {
if( BmxEnvironment.IsDebug ) {
consoleWriter.WriteWarning( "Okta passwordless authentication timed out." );
Expand Down Expand Up @@ -189,6 +148,65 @@ The provided Okta user '{providedLogin}' does not match the system configured pa
return oktaAuthenticatedClient;
}

private async Task<string?> GetSessionIdFromBrowserAsync( string browserPath, Uri orgUrl ) {
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 ) );
cancellationTokenSource.Token.Register( () => sessionIdTcs.TrySetCanceled() );

// cancel if we're stuck on a single page for 3 seconds
using var pageTimer = new System.Timers.Timer( TimeSpan.FromSeconds( 3 ) ) { AutoReset = false };
pageTimer.Elapsed += ( _, _ ) => cancellationTokenSource.Cancel();
pageTimer.Start();

using var page = await browser.NewPageAsync().WaitAsync( cancellationTokenSource.Token );
int attempt = 1;

page.Load += ( _, _ ) => _ = OnPageLoadAsync();
await page.GoToAsync( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token );
return await sessionIdTcs.Task;

async Task OnPageLoadAsync() {
// reset the 3-sec per-page timer on every page load
lock( pageTimer ) {
pageTimer.Stop();
pageTimer.Start();
Comment on lines +175 to +176
Copy link
Member

Choose a reason for hiding this comment

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

Don't follow why .stop needs to be called here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Calling .Start() on an already started timer does nothing - it will still fire at the originally scheduled time.
We want to restart the timer here, so .Stop() must be called first to reset it.

}

var url = new Uri( page.Url );
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( orgUrl.AbsoluteUri ).WaitAsync( cancellationTokenSource.Token );
} else {
if( BmxEnvironment.IsDebug ) {
if( url.AbsolutePath == "/" ) {
consoleWriter.WriteWarning(
"Okta passwordless authentication is not available."
);
} else {
consoleWriter.WriteWarning( "Okta passwordless authentication failed" );
}
}
sessionIdTcs.SetResult( null );
}
return;
}
}
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 );
}
}
}

private async Task<IOktaAuthenticatedClient> GetPasswordAuthenticatedClientAsync( Uri orgUrl, string user ) {
string password = consolePrompter.PromptPassword();

Expand Down
Loading