-
Notifications
You must be signed in to change notification settings - Fork 9
[ CDM-243 ] [ CDM-245 ] Orcid Provider MFA Support & Token response mfa
key
#471
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
base: develop
Are you sure you want to change the base?
Conversation
src/main/java/us/kbase/auth2/lib/identity/RemoteIdentityDetails.java
Outdated
Show resolved
Hide resolved
src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java
Outdated
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
Update tests to handle MFA field additions and ORCID OpenID Connect changes: - Fix hash codes in RemoteIdentityTest for new MFA field - Update ORCID provider tests for openid scope instead of /authenticate - Add mfaAuthenticated field to API response expectations - Fix non-ORCID provider test to use null MFA status - Update identity ordering in user endpoint tests
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #471 +/- ##
=============================================
- Coverage 93.37% 93.35% -0.02%
- Complexity 2151 2171 +20
=============================================
Files 126 127 +1
Lines 7558 7634 +76
Branches 1184 1207 +23
=============================================
+ Hits 7057 7127 +70
- Misses 458 459 +1
- Partials 43 48 +5 🚀 New features to boost your workflow:
|
mfaAuthenticated
key
mfaAuthenticated
keymfaAuthenticated
key
Move MFA status from being computed from user identities to being stored as a boolean field on tokens themselves. This provides per-token MFA tracking and eliminates the need for complex identity lookups.
src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java
Outdated
Show resolved
Hide resolved
mfaAuthenticated
keymfa
key
See I need another test or two to have complete coverage, will add. |
- Add getMfaStatus helper method to handle null MFA fields in database - Update token and identity deserialization to use helper method - Add tests for backward compatibility with existing database records - Prevents NullPointerException when MFA field is missing from stored data
545f705
to
e8fdd6a
Compare
- Update scope in login URL tests from 'openid' to 'openid+%2Fauthenticate' - Add JWT tokens to tests that were missing them after OpenID Connect changes - Update getIdentityWithNoJWT test to expect new error when JWT is missing - Fix error condition tests to provide valid JWTs so they test intended errors
- Add MFA field to ExternalToken base class with UNKNOWN default - Remove duplicate MFA handling from APIToken (now inherited) - Ensures all token endpoints return MFA status for UI compatibility - Non-ORCID providers default to MFA status UNKNOWN
…lsverifier contract test. this resolves the hardcoded hashcodes causing stochastic test failures depending on execution order/enviroment, test cleanup to follow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review is already huge so skipping tests until next time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to codecov there's still some uncovered code, which should be coverable since the orcid interactions are mocked and everything else can be unit tested
private NewToken login(final UserName userName, final TokenCreationContext tokenCtx, | ||
final MfaStatus mfa) throws AuthStorageException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private NewToken login(final UserName userName, final TokenCreationContext tokenCtx, | |
final MfaStatus mfa) throws AuthStorageException { | |
private NewToken login( | |
final UserName userName, | |
final TokenCreationContext tokenCtx, | |
final MfaStatus mfa | |
) throws AuthStorageException { |
My rule is either all args on 1 line or each arg on its own line. More readable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to rework this in a similar way to TokenType so that we can change the enum name without breaking things later:
} else { | ||
this.email = email.trim(); | ||
} | ||
this.mfa = mfa; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.mfa = mfa; | |
this.mfa = requireNonNull(mfa, "mfa"); |
int result = 1; | ||
result = prime * result + ((email == null) ? 0 : email.hashCode()); | ||
result = prime * result + ((fullname == null) ? 0 : fullname.hashCode()); | ||
result = prime * result + ((mfa == null) ? 0 : mfa.name().hashCode()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
result = prime * result + ((mfa == null) ? 0 : mfa.name().hashCode()); | |
result = prime * result + ((mfa == null) ? 0 : mfa.hashCode()); |
Getting the hash directly from the enum class is fine, it's done this way in other places in the codebase.
There's a couple other places in the PR where this should be fixed
.append(pre + Fields.IDENTITIES_EMAIL, rid.getEmail()) | ||
.append(pre + Fields.IDENTITIES_NAME, rid.getFullname())); | ||
.append(pre + Fields.IDENTITIES_NAME, rid.getFullname()) | ||
.append(pre + Fields.IDENTITIES_MFA, rid.getMfa().name())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is updating the user document, which I don't think you want to do
|
||
@Override | ||
public OptionalsStep withMfa(final MfaStatus mfa) { | ||
this.mfa = mfa; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.mfa = mfa; | |
this.mfa = requireNonNull(mfa, "mfa"); |
} | ||
|
||
|
||
/** Get the external configuration without providing any credentials. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
/** Get the external configuration without providing any credentials. | |
} | |
/** Get the external configuration without providing any credentials. |
created = storedToken.getCreationDate().toEpochMilli(); | ||
custom = storedToken.getContext().getCustomContext(); | ||
// For tokens from non-ORCID providers, MFA status defaults to UNKNOWN | ||
mfa = storedToken.getMfa() != null ? storedToken.getMfa() : MfaStatus.UNKNOWN; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mfa = storedToken.getMfa() != null ? storedToken.getMfa() : MfaStatus.UNKNOWN; | |
mfa = storedToken.getMfa() |
Can't be null with the other changes in this review
} | ||
|
||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
} | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, hit the submit button too early
private static final Client CLI = ClientBuilder.newClient(); | ||
|
||
private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(OrcIDIdentityProviderFactory.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you handle logging like this:
auth2/src/main/java/us/kbase/auth2/lib/Authentication.java
Lines 325 to 327 in 47f6a01
private void logInfo(final String format, final Object... params) { | |
LoggerFactory.getLogger(getClass()).info(format, params); | |
} |
There's some weirdness I've encountered in the past where a static logger could cause (I think) permgen OOMs. Probably won't happen here but just to be safe
(String) m.get("access_token"), | ||
(String) m.get("name"), | ||
(String) m.get("orcid")); | ||
fullName != null ? fullName : "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change? It's backwards incompatible and all the other providers return null if there's no full name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A mistake, artifact from when I was trying to use only one auth call
* to determine if multi-factor authentication was used. | ||
* | ||
* @param jwt the JWT ID token from ORCID (may be null/empty for non-member accounts) | ||
* @return MfaStatus indicating whether MFA was used, UNKNOWN if JWT is missing or unparseable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @return MfaStatus indicating whether MFA was used, UNKNOWN if JWT is missing or unparseable | |
* @return MfaStatus indicating whether MFA was used, UNKNOWN if JWT is missing |
although from a comment from review # 1, it sounds like missing should throw an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't figure out how to make the error informative to the user and not just crap-out without a lot of changes to other parts of the code. Perhaps an mfa:ERROR state? Right now I'm just printing to logs that the error occurred
} catch (IllegalArgumentException e) { | ||
// Base64 decoding failed - invalid JWT format | ||
LOGGER.warn("Unable to decode JWT from ORCID: {}", e.getMessage()); | ||
throw new IdentityRetrievalException("Unable to decode JWT from ORCID: " + e.getMessage(), e); | ||
} catch (IOException e) { | ||
// JSON parsing failed - malformed payload | ||
LOGGER.warn("Unable to parse JWT payload from ORCID: {}", e.getMessage()); | ||
throw new IdentityRetrievalException("Unable to parse JWT payload from ORCID: " + e.getMessage(), e); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move these up to specifically catch errors from the lines that throw them? Ensures we don't accidentally mask unexpected errors
It's been a while since the first time I reviewed it but I get the feeling the updated implementation is much more straightforward |
Adds MFA boolean to RemoteIdentity and StoredToken classes, which get stored in Mongo. Adds JWT parsing to extract the AMR claim from the Orcid login token during login. Modifies the token to include MFA status.
A nice summary of all the changes from claude:
Key Changes
Core Implementation:
mfa
field to StoredToken (stored as mfa in MongoDB)ORCID Provider Enhancement:
Authentication Flow:
API Response
The /api/V2/token endpoint now returns
mfa
with tri-state semantics:USED
: User authenticated with MFA during token creation (e.g. Orcid with MFA)NOT_USED
: User explicitly chose not to use MFA when available (e.g. Orcid without MFA)UNKNOWN
: MFA status unknown or not applicable to authentication method (e.g. Google (mfa inspection not supported), a non-member Orcid API client account (openid token not supported), or a dev token)Database Schema