Skip to content

Commit a4a0262

Browse files
committed
Improve ORCID JWT error handling and test robustness
1 parent 164e3cb commit a4a0262

File tree

8 files changed

+47
-47
lines changed

8 files changed

+47
-47
lines changed

src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ public IdentityProvider configure(final IdentityProviderConfig cfg) {
5151
}
5252

5353
/** An identity provider for OrcID accounts.
54+
*
55+
* Multi-Factor Authentication (MFA) Status Handling:
56+
* - Uses OpenID Connect JWT tokens to determine MFA status via AMR claims
57+
* - Missing JWT (non-member accounts): defaults to MfaStatus.UNKNOWN
58+
* - Malformed JWT during login: logs warning and defaults to MfaStatus.UNKNOWN for graceful degradation
59+
* - Valid JWT with AMR claim: returns MfaStatus.USED or MfaStatus.NOT_USED based on "mfa" presence
60+
*
5461
5562
*
5663
*/
@@ -72,6 +79,7 @@ public static class OrcIDIdentityProvider implements IdentityProvider {
7279
private static final Client CLI = ClientBuilder.newClient();
7380

7481
private static final ObjectMapper MAPPER = new ObjectMapper();
82+
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(OrcIDIdentityProviderFactory.class);
7583

7684
private final IdentityProviderConfig cfg;
7785

@@ -214,13 +222,15 @@ public void handleError(final Response r, final Map<String, Object> response)
214222
/**
215223
* Parses the Authentication Method Reference (AMR) claim from an OpenID Connect ID token
216224
* to determine if multi-factor authentication was used.
217-
*
218-
* @param jwt the JWT ID token from ORCID
219-
* @return MfaStatus indicating whether MFA was used
225+
*
226+
* @param jwt the JWT ID token from ORCID (may be null/empty for non-member accounts)
227+
* @return MfaStatus indicating whether MFA was used, UNKNOWN if JWT is missing or unparseable
220228
*/
221229
private static MfaStatus parseAmrClaim(final String jwt) throws IdentityRetrievalException {
222230
if (jwt == null || jwt.trim().isEmpty()) {
223-
throw new IdentityRetrievalException("No JWT token provided by ORCID despite requesting OpenID scope");
231+
// Missing JWT is expected for non-member ORCID accounts without OpenID Connect scope
232+
LOGGER.debug("No JWT token provided by ORCID - defaulting MFA status to UNKNOWN");
233+
return MfaStatus.UNKNOWN;
224234
}
225235

226236
try {
@@ -256,10 +266,12 @@ private static MfaStatus parseAmrClaim(final String jwt) throws IdentityRetrieva
256266
throw new IdentityRetrievalException("AMR claim from ORCID in unexpected format: " + amrClaim);
257267

258268
} catch (IllegalArgumentException e) {
259-
// Base64 decoding failed - invalid JWT
269+
// Base64 decoding failed - invalid JWT format
270+
LOGGER.warn("Unable to decode JWT from ORCID: {}", e.getMessage());
260271
throw new IdentityRetrievalException("Unable to decode JWT from ORCID: " + e.getMessage(), e);
261272
} catch (IOException e) {
262273
// JSON parsing failed - malformed payload
274+
LOGGER.warn("Unable to parse JWT payload from ORCID: {}", e.getMessage());
263275
throw new IdentityRetrievalException("Unable to parse JWT payload from ORCID: " + e.getMessage(), e);
264276
}
265277
}

src/test/java/us/kbase/test/auth2/lib/EmailAddressTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ public class EmailAddressTest {
1818
@Test
1919
public void construct() throws Exception {
2020
final EmailAddress ea = new EmailAddress(" [email protected] \n");
21-
assertThat("incorrect email", ea.getAddress(), is("[email protected]"));
22-
assertThat("incorrect equality", ea, is(new EmailAddress("[email protected]")));
21+
assertThat("incorrect email content, failed equality", ea, is(new EmailAddress("[email protected]")));
2322
assertThat("incorrect toString", ea.toString(), is("EmailAddress [[email protected]]"));
2423
}
2524

src/test/java/us/kbase/test/auth2/lib/UserNameTest.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,22 @@ public class UserNameTest {
2121
@Test
2222
public void root() throws Exception {
2323
final UserName un = new UserName("***ROOT***");
24-
assertThat("incorrect username", un.getName(), is("***ROOT***"));
24+
assertThat("incorrect root content, failed equality", un, is(UserName.ROOT));
2525
assertThat("incorrect is root", un.isRoot(), is(true));
2626
assertThat("incorrect toString", un.toString(), is("UserName [getName()=***ROOT***]"));
27-
assertThat("incorrect equality", un, is(UserName.ROOT));
2827

2928
final UserName un2 = UserName.ROOT;
30-
assertThat("incorrect username", un2.getName(), is("***ROOT***"));
29+
assertThat("incorrect root content, failed equality", un2, is(new UserName("***ROOT***")));
3130
assertThat("incorrect is root", un2.isRoot(), is(true));
3231
assertThat("incorrect toString", un2.toString(), is("UserName [getName()=***ROOT***]"));
33-
assertThat("incorrect equality", un2, is(new UserName("***ROOT***")));
3432
}
3533

3634
@Test
3735
public void construct() throws Exception {
3836
final UserName un = new UserName("a8nba9");
39-
assertThat("incorrect username", un.getName(), is("a8nba9"));
37+
assertThat("incorrect username content, failed equality", un, is(new UserName("a8nba9")));
4038
assertThat("incorrect is root", un.isRoot(), is(false));
4139
assertThat("incorrect toString", un.toString(), is("UserName [getName()=a8nba9]"));
42-
assertThat("incorrect equality", un, is(new UserName("a8nba9")));
4340
}
4441

4542
@Test

src/test/java/us/kbase/test/auth2/lib/identity/RemoteIdentityTest.java

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,19 @@ public class RemoteIdentityTest {
1818
@Test
1919
public void remoteDetailsWithAllFields() throws Exception {
2020
final RemoteIdentityDetails dets = new RemoteIdentityDetails("user ", " full", "\temail");
21-
assertThat("incorrect username", dets.getUsername(), is("user"));
22-
assertThat("incorrect fullname", dets.getFullname(), is("full"));
23-
assertThat("incorrect email", dets.getEmail(), is("email"));
24-
assertThat("incorrect mfa authenticated", dets.getMfa(), is(MfaStatus.UNKNOWN));
25-
assertThat("incorrect equality", dets, is(new RemoteIdentityDetails("user", "full", "email")));
21+
assertThat("incorrect details content, failed equality", dets, is(new RemoteIdentityDetails("user", "full", "email")));
2622
assertThat("incorrect toString()", dets.toString(),
2723
is("RemoteIdentityDetails [username=user, fullname=full, email=email, mfa=UNKNOWN]"));
2824
}
2925

3026
@Test
3127
public void remoteDetailsWithEmptyFields() throws Exception {
3228
final RemoteIdentityDetails dets = new RemoteIdentityDetails("user", "\t ", " \n");
33-
assertThat("incorrect username", dets.getUsername(), is("user"));
34-
assertThat("incorrect fullname", dets.getFullname(), is((String) null));
35-
assertThat("incorrect email", dets.getEmail(), is((String) null));
36-
assertThat("incorrect mfa authenticated", dets.getMfa(), is(MfaStatus.UNKNOWN));
3729
assertThat("incorrect toString()", dets.toString(),
3830
is("RemoteIdentityDetails [username=user, fullname=null, email=null, mfa=UNKNOWN]"));
3931

4032
final RemoteIdentityDetails dets2 = new RemoteIdentityDetails("user", null, null);
41-
assertThat("incorrect username", dets2.getUsername(), is("user"));
42-
assertThat("incorrect fullname", dets2.getFullname(), is((String) null));
43-
assertThat("incorrect email", dets2.getEmail(), is((String) null));
44-
assertThat("incorrect mfa authenticated", dets2.getMfa(), is(MfaStatus.UNKNOWN));
45-
assertThat("incorrect equality", dets2, is(dets));
33+
assertThat("incorrect empty field content, failed equality", dets2, is(dets));
4634
assertThat("incorrect toString()", dets2.toString(),
4735
is("RemoteIdentityDetails [username=user, fullname=null, email=null, mfa=UNKNOWN]"));
4836
}
@@ -71,12 +59,10 @@ public void remoteDetailsEquals() throws Exception {
7159
@Test
7260
public void remoteId() throws Exception {
7361
final RemoteIdentityID id = new RemoteIdentityID("foo", "bar");
74-
assertThat("incorrect provider name", id.getProviderName(), is("foo"));
75-
assertThat("incorrect provider id", id.getProviderIdentityId(), is("bar"));
62+
assertThat("incorrect id content, failed equality", id, is(new RemoteIdentityID("foo", "bar")));
7663
assertThat("incorrect unique id", id.getID(), is("5c7d96a3dd7a87850a2ef34087565a6e"));
7764
// check unique id again to check memoization doesn't change result
7865
assertThat("incorrect unique id", id.getID(), is("5c7d96a3dd7a87850a2ef34087565a6e"));
79-
assertThat("incorrect equality", id, is(new RemoteIdentityID("foo", "bar")));
8066
assertThat("incorrect toString()", id.toString(),
8167
is("RemoteIdentityID [provider=foo, id=bar]"));
8268
}
@@ -113,9 +99,7 @@ public void identity() throws Exception {
11399
final RemoteIdentityID id = new RemoteIdentityID("p", "i");
114100
final RemoteIdentityDetails dets = new RemoteIdentityDetails("u", "f", "e");
115101
final RemoteIdentity ri = new RemoteIdentity(id, dets);
116-
assertThat("incorrect id", ri.getRemoteID(), is(id));
117-
assertThat("incorrect details", ri.getDetails(), is(dets));
118-
assertThat("incorrect equality", ri, is(new RemoteIdentity(
102+
assertThat("incorrect identity content, failed equality", ri, is(new RemoteIdentity(
119103
new RemoteIdentityID("p", "i"), new RemoteIdentityDetails("u", "f", "e"))));
120104
assertThat("incorrect toString()", ri.toString(),
121105
is("RemoteIdentity [remoteID=RemoteIdentityID [provider=p, id=i], " +

src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTokensTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ TokenType.LOGIN, id, new UserName("bar"))
4242
.withCustomContext("k1", "v1")
4343
.withCustomContext("k2", "v2")
4444
.build())
45+
.withMfa(us.kbase.auth2.lib.identity.MfaStatus.NOT_USED)
4546
.withTokenName(new TokenName("foo")).build();
4647
storage.storeToken(store, "nJKFR6Xc4vzCeI3jT+FjlC9k5Q/qVw0zd0gi1erL8ew=");
47-
48+
4849
final StoredToken expected = StoredToken.getBuilder(
4950
TokenType.LOGIN, id, new UserName("bar"))
5051
.withLifeTime(now, now.plusSeconds(20))
@@ -56,6 +57,7 @@ TokenType.LOGIN, id, new UserName("bar"))
5657
.withCustomContext("k1", "v1")
5758
.withCustomContext("k2", "v2")
5859
.build())
60+
.withMfa(us.kbase.auth2.lib.identity.MfaStatus.NOT_USED)
5961
.withTokenName(new TokenName("foo")).build();
6062
final StoredToken st = storage.getToken(new IncomingToken("sometoken").getHashedToken());
6163
assertThat("incorrect token", st, is(expected));

src/test/java/us/kbase/test/auth2/lib/token/TokenTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ private void failCreateTemporaryToken(
161161

162162
@Test
163163
public void storedTokenMSLifetime() throws Exception {
164-
164+
165165
final UUID id = UUID.randomUUID();
166166
final StoredToken ht = StoredToken.getBuilder(TokenType.LOGIN, id, new UserName("whee"))
167167
.withLifeTime(Instant.ofEpochMilli(1000), 4000).build();
@@ -175,6 +175,7 @@ public void storedTokenMSLifetime() throws Exception {
175175
is(Instant.ofEpochMilli(5000)));
176176
assertThat("incorrect context", ht.getContext(),
177177
is(TokenCreationContext.getBuilder().build()));
178+
assertThat("incorrect default mfa status", ht.getMfa(), is(us.kbase.auth2.lib.identity.MfaStatus.UNKNOWN));
178179
}
179180

180181
@Test
@@ -183,6 +184,7 @@ public void storedTokenExpDateAndNameAndContext() throws Exception {
183184
final StoredToken ht2 = StoredToken.getBuilder(TokenType.DEV, id2, new UserName("whee2"))
184185
.withLifeTime(Instant.ofEpochMilli(27000), Instant.ofEpochMilli(42000))
185186
.withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build())
187+
.withMfa(us.kbase.auth2.lib.identity.MfaStatus.USED)
186188
.withTokenName(new TokenName("ugh")).build();
187189
assertThat("incorrect token type", ht2.getTokenType(), is(TokenType.DEV));
188190
assertThat("incorrect token name", ht2.getTokenName(),
@@ -195,6 +197,7 @@ public void storedTokenExpDateAndNameAndContext() throws Exception {
195197
is(Instant.ofEpochMilli(42000)));
196198
assertThat("incorrect context", ht2.getContext(),
197199
is(TokenCreationContext.getBuilder().withNullableDevice("d").build()));
200+
assertThat("incorrect mfa status", ht2.getMfa(), is(us.kbase.auth2.lib.identity.MfaStatus.USED));
198201
}
199202

200203
@Test

src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -542,22 +542,21 @@ public void getIdentityWithNoJWT() throws Exception {
542542
final IdentityProviderConfig idconfig = getTestIDConfig();
543543
final IdentityProvider idp = new OrcIDIdentityProvider(idconfig);
544544
final String orcID = "0000-0001-1234-5678";
545-
545+
546546
setUpCallAuthToken(authCode, "footoken5", "https://ologinredir.com",
547547
idconfig.getClientID(), idconfig.getClientSecret(), " My name ", orcID);
548548
setupCallID("footoken5", orcID, APP_JSON, 200, MAPPER.writeValueAsString(
549-
map("email", Arrays.asList(map("email", "[email protected]")))));
550-
551-
// Now that JWT is required, this should fail with missing JWT error
552-
try {
553-
idp.getIdentities(authCode, "pkce", false, null);
554-
fail("Expected IdentityRetrievalException");
555-
} catch (IdentityRetrievalException e) {
556-
assertThat("incorrect exception message", e.getMessage(),
557-
is("10030 Identity retrieval failed: No JWT token provided by ORCID despite requesting OpenID scope"));
558-
}
549+
map("email", Arrays.asList(map("email", "[email protected]")))));
550+
551+
// Missing JWT from non-member ORCID accounts should default to UNKNOWN MFA status
552+
final Set<RemoteIdentity> rids = idp.getIdentities(authCode, "pkce", false, null);
553+
assertThat("incorrect number of idents", rids.size(), is(1));
554+
final Set<RemoteIdentity> expected = new HashSet<>();
555+
expected.add(new RemoteIdentity(new RemoteIdentityID(ORCID, orcID),
556+
new RemoteIdentityDetails(orcID, "My name", "[email protected]", MfaStatus.UNKNOWN)));
557+
assertThat("incorrect ident set", rids, is(expected));
559558
}
560-
559+
561560
@Test
562561
public void getIdentityWithInvalidJWT() throws Exception {
563562
final String authCode = "authcodeInvalidJWT";

src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ public void getTokensMinimalInput() throws Exception {
158158
.with("name", null)
159159
.with("user", "whoo")
160160
.with("custom", Collections.emptyMap())
161+
.with("mfa", "UNKNOWN")
161162
.with("os", null)
162163
.with("osver", null)
163164
.with("agent", null)
@@ -260,6 +261,7 @@ public void getTokensMaximalInput() throws Exception {
260261
.with("name", "wugga")
261262
.with("user", "whoo")
262263
.with("custom", ImmutableMap.of("foo", "bar"))
264+
.with("mfa", "UNKNOWN")
263265
.with("os", "o")
264266
.with("osver", "osv")
265267
.with("agent", "ag")
@@ -276,6 +278,7 @@ public void getTokensMaximalInput() throws Exception {
276278
.with("name", "whee")
277279
.with("user", "whoo")
278280
.with("custom", Collections.emptyMap())
281+
.with("mfa", "UNKNOWN")
279282
.with("os", null)
280283
.with("osver", null)
281284
.with("agent", null)
@@ -291,6 +294,7 @@ public void getTokensMaximalInput() throws Exception {
291294
.with("name", null)
292295
.with("user", "whoo")
293296
.with("custom", ImmutableMap.of("baz", "bat"))
297+
.with("mfa", "UNKNOWN")
294298
.with("os", null)
295299
.with("osver", null)
296300
.with("agent", "ag2")

0 commit comments

Comments
 (0)