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

extract password auth into a dedicated method #486

Merged
merged 1 commit into from
Oct 17, 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
109 changes: 57 additions & 52 deletions src/D2L.Bmx/OktaAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,67 +55,27 @@ bool bypassBrowserSecurity
}

var orgUrl = GetOrgBaseAddress( org );
var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( orgUrl );
Copy link
Member Author

@cfbao cfbao Oct 17, 2024

Choose a reason for hiding this comment

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

a side benefit of wrapping password auth in a dedicated private method is that it exposes and prevents when we declare/define variables too early, like here


if( !ignoreCache && TryAuthenticateFromCache( orgUrl, user, oktaClientFactory, out var oktaAuthenticated ) ) {
return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated );
if( !ignoreCache && TryAuthenticateFromCache( orgUrl, user, out var oktaAuthenticated ) ) {
return new( Org: org, User: user, Client: oktaAuthenticated );
}
if( await GetDssoAuthenticatedClientAsync(

oktaAuthenticated = await GetDssoAuthenticatedClientAsync(
orgUrl,
user,
nonInteractive,
bypassBrowserSecurity ) is { } oktaDssoAuthenticated
) {
return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaDssoAuthenticated );
bypassBrowserSecurity
);
if( oktaAuthenticated is not null ) {
return new( Org: org, User: user, Client: oktaAuthenticated );
}

if( nonInteractive ) {
throw new BmxException( "Okta authentication failed. Please run `bmx login` first." );
}

string password = consolePrompter.PromptPassword();

var authnResponse = await oktaAnonymous.AuthenticateAsync( user, password );

if( authnResponse is AuthenticateResponse.Failure failure ) {
throw new BmxException( $"""
Okta authentication for user '{user}' in org '{org}' failed ({failure.StatusCode}).
Check if org, user, and password is correct.
""" );
}

if( authnResponse is AuthenticateResponse.MfaRequired mfaInfo ) {
OktaMfaFactor mfaFactor = consolePrompter.SelectMfa( mfaInfo.Factors );

if( mfaFactor.FactorName == OktaMfaFactor.UnsupportedMfaFactor ) {
throw new BmxException( "Selected MFA not supported by BMX" );
}

// TODO: Handle retry
if( mfaFactor.RequireChallengeIssue ) {
await oktaAnonymous.IssueMfaChallengeAsync( mfaInfo.StateToken, mfaFactor.Id );
}

string mfaResponse = consolePrompter.GetMfaResponse(
mfaFactor is OktaMfaQuestionFactor questionFactor ? questionFactor.Profile.QuestionText : "PassCode",
mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value
);

authnResponse = await oktaAnonymous.VerifyMfaChallengeResponseAsync( mfaInfo.StateToken, mfaFactor.Id, mfaResponse );
}

if( authnResponse is AuthenticateResponse.Success successInfo ) {
var sessionResp = await oktaAnonymous.CreateSessionAsync( successInfo.SessionToken );

oktaAuthenticated = oktaClientFactory.CreateAuthenticatedClient( orgUrl, sessionResp.Id );
TryCacheOktaSession( user, orgUrl.Host, sessionResp.Id, sessionResp.ExpiresAt );
return new OktaAuthenticatedContext( Org: org, User: user, Client: oktaAuthenticated );
}

if( authnResponse is AuthenticateResponse.Failure failure2 ) {
throw new BmxException( $"Error verifying MFA with Okta ({failure2.StatusCode})." );
}

throw new UnreachableException( $"Unexpected response type: {authnResponse.GetType()}" );
oktaAuthenticated = await GetPasswordAuthenticatedClientAsync( orgUrl, user );
return new( Org: org, User: user, Client: oktaAuthenticated );
}

private static Uri GetOrgBaseAddress( string org ) {
Expand All @@ -127,7 +87,6 @@ private static Uri GetOrgBaseAddress( string org ) {
private bool TryAuthenticateFromCache(
Uri orgBaseAddress,
string user,
IOktaClientFactory oktaClientFactory,
[NotNullWhen( true )] out IOktaAuthenticatedClient? oktaAuthenticated
) {
string? sessionId = GetCachedOktaSessionId( user, orgBaseAddress.Host );
Expand Down Expand Up @@ -230,6 +189,52 @@ The provided Okta user '{providedLogin}' does not match the system configured pa
return oktaAuthenticatedClient;
}

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

var oktaAnonymous = oktaClientFactory.CreateAnonymousClient( orgUrl );
var authnResponse = await oktaAnonymous.AuthenticateAsync( user, password );

if( authnResponse is AuthenticateResponse.Failure failure ) {
throw new BmxException( $"""
Okta authentication for user '{user}' in org '{orgUrl.Host}' failed ({failure.StatusCode}).
Check if org, user, and password is correct.
""" );
}

if( authnResponse is AuthenticateResponse.MfaRequired mfaInfo ) {
OktaMfaFactor mfaFactor = consolePrompter.SelectMfa( mfaInfo.Factors );

if( mfaFactor.FactorName == OktaMfaFactor.UnsupportedMfaFactor ) {
throw new BmxException( "Selected MFA not supported by BMX" );
}

// TODO: Handle retry
if( mfaFactor.RequireChallengeIssue ) {
await oktaAnonymous.IssueMfaChallengeAsync( mfaInfo.StateToken, mfaFactor.Id );
}

string mfaResponse = consolePrompter.GetMfaResponse(
mfaFactor is OktaMfaQuestionFactor questionFactor ? questionFactor.Profile.QuestionText : "PassCode",
mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value
);

authnResponse = await oktaAnonymous.VerifyMfaChallengeResponseAsync( mfaInfo.StateToken, mfaFactor.Id, mfaResponse );
}

if( authnResponse is AuthenticateResponse.Success successInfo ) {
var sessionResp = await oktaAnonymous.CreateSessionAsync( successInfo.SessionToken );
TryCacheOktaSession( user, orgUrl.Host, sessionResp.Id, sessionResp.ExpiresAt );
return oktaClientFactory.CreateAuthenticatedClient( orgUrl, sessionResp.Id );
}

if( authnResponse is AuthenticateResponse.Failure failure2 ) {
throw new BmxException( $"Error verifying MFA with Okta ({failure2.StatusCode})." );
}

throw new UnreachableException( $"Unexpected response type: {authnResponse.GetType()}" );
}

private bool TryCacheOktaSession( string userId, string org, string sessionId, DateTimeOffset expiresAt ) {
if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) {
CacheOktaSession( userId, org, sessionId, expiresAt );
Expand Down
Loading