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

De-serialize Security Question MFA to different type #459

Merged
merged 5 commits into from
Jul 8, 2024

Conversation

boarnoah
Copy link
Member

@boarnoah boarnoah commented Jul 8, 2024

Why

With .NET 9 we can use properties aside from the first in JSON to do type discrimination (in this case using factorType in the API).

So we can use it de-serialize the different properties in Factor's Profile section (in this case we only care about QuestionText for the Security Question factor).

Ticket

N/A

@boarnoah boarnoah self-assigned this Jul 8, 2024
@github-actions github-actions bot added language/csharp size/M A medium-sized PR. labels Jul 8, 2024
@@ -21,17 +21,68 @@ internal record AuthenticateResponseEmbedded(
OktaMfaFactor[]? Factors
);

[JsonPolymorphic(
TypeDiscriminatorPropertyName = "factorType",
Copy link
Member Author

@boarnoah boarnoah Jul 8, 2024

Choose a reason for hiding this comment

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

Unfortunately, STJ doesn't offer a way to include the property in JSON used for type discrimination in the de-serialized object.

I did find a issue asking for this capability (can't find it now). It makes sense since ideally if you are using type discrimination, then the rest of your code should use the C# type system to handle differences in the various types.

In this case, where we only want to treat one of several derived types differently (but include different names for each type), this is incredibly cumbersome.

@@ -57,18 +57,18 @@ bool ignoreCache
if( authnResponse is AuthenticateResponse.MfaRequired mfaInfo ) {
OktaMfaFactor mfaFactor = consolePrompter.SelectMfa( mfaInfo.Factors );

if( !IsMfaFactorTypeSupported( mfaFactor.FactorType ) ) {
if( mfaFactor.FactorName == "Unknown" ) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Not a fan of this at all

Copy link
Member

Choose a reason for hiding this comment

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

This can be a type check against the (maybe renamed) base type, or at least you can make the string a constant so there's no possibility of a typo.

Copy link
Member Author

Choose a reason for hiding this comment

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

Doesn't look great IMO. 19413d4

I think we should instead rely on something using UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization
& filtering out failures seems better although definitely more involved code wise (de-serialization call for this API response handling will need rework).

After all we aren't proceeding when unknown factor type is selected, might as well not populate the list with them (maybe a separate message for "x, y factors are not supported by BMX"

Copy link
Member

Choose a reason for hiding this comment

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

I was thinking of giving "unknown" a special constant, rather than reusing the "FactoryType" thingy we're using elsewhere. Reusing that would be confusing for sure.

What about doing a runtime type check though? We could call the base type something like OktaMfaFactorUnspecified, and check the exact type (rather than using is which considers child types a match).

More involved code change is precisely why I didn't suggest failing serialization directly, but that's an option for sure. We can even make the base type abstract.

Copy link
Member

Choose a reason for hiding this comment

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

actually, with a better base type name, the constant check wouldn't look so confusing either.

Copy link
Member Author

Choose a reason for hiding this comment

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

Think we should go with a standalone const + factor name check,

b57bf15 is confusing in it's own right (since we use the base type name in quite a few contexts, it is awkward to name it "unknown"/"unsupported".

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe BaseOktaMfaFactor or something might help, but it still feels wrong.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I see. Renaming the base type doesn't work. A distinctly named constant is good. The FactorType constant isn't in the type hierarchy anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to a better named const for this, should revisit if this ends up happening dotnet/runtime#93471

throw new BmxException( "Selected MFA not supported by BMX" );
}

// TODO: Handle retry
if( mfaFactor.FactorType is "sms" or "call" or "email" ) {
if( mfaFactor.RequireChallengeIssue ) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Alternatively if ( mfaFactor is OktaMfaEmailFactor ... ) { etc....?

Copy link
Member

Choose a reason for hiding this comment

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

as is seems good

@boarnoah boarnoah force-pushed the mfa-factors-polymorphic branch from 44dbb09 to 7e9e4db Compare July 8, 2024 14:26
@boarnoah boarnoah marked this pull request as ready for review July 8, 2024 14:26
@boarnoah boarnoah requested a review from a team as a code owner July 8, 2024 14:26
@boarnoah boarnoah requested review from cfbao, scowing, jharoutuniand2l, jkomonen and gord5500 and removed request for a team July 8, 2024 14:26
}

internal record OktaMfaQuestionFactor() : OktaMfaFactor() {
public override string FactorName => "Security Question";
Copy link
Member Author

@boarnoah boarnoah Jul 8, 2024

Choose a reason for hiding this comment

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

Going with this approach lets us customize this to be different from Okta's raw value in API response, there is a case to be made for leaving it as-is to reduce confusion*.

* See:
token vs token:hardware, I suppose we could map it better to what Okta actually calls it in the UI (Yubikey etc..., but thats awfully vendor specific)

Copy link
Member

Choose a reason for hiding this comment

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

Having control over a more user friendly name isn't a bad idea.

You can also do something like IMS does here
https://github.com/Brightspace/instance-metadata-store/blob/32251a9a9ff5c23c1fe7dea7faa2da39d02d8a5e/worker/src/D2L.InstanceMetadataStore.Worker/ConfigEtl/Extractor/ConfigEtlExtractorRequest.cs#L5-L10
So the discriminator value is defined in the child type itself to improve locality.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did play around with that, didn't find a great solution for keeping the discriminator as a static value but also to be able to use derived type's value for it without downcasting.

Copy link
Member

@cfbao cfbao Jul 8, 2024

Choose a reason for hiding this comment

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

I'm thinking of something like this

record OktaMfaFoo : OktaMfaFactor {
	public const string FactorType = "foo";
	public override string FactorName => FactorType;
}

We use OktaMfaFoo.FactorType for the compile time discriminator declarations, and factor.FactorName in our runtime logic to avoid casting.

Alternatively,

record OktaMfaFoo : OktaMfaFactor {
	public const string FactorType = "foo";
	public override string FactorName => "Foo;
}

purely for the locality benefit, to reduce possibility of a mismatch.

@boarnoah boarnoah force-pushed the mfa-factors-polymorphic branch from b57bf15 to 2dcb2b7 Compare July 8, 2024 18:42
@Brightspace Brightspace deleted a comment from github-actions bot Jul 8, 2024
@boarnoah boarnoah merged commit 643bd10 into aw/improve-mfa-prompt Jul 8, 2024
9 checks passed
@boarnoah boarnoah deleted the mfa-factors-polymorphic branch July 8, 2024 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language/csharp size/M A medium-sized PR.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants