From 55273290c2845d9c632fc28ec0d1003e071c4dd0 Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 3 Jun 2024 14:22:29 -0700 Subject: [PATCH 01/24] Fix user search bugs re underscores + mongo errors * Fixed a bug where if a user name contained an underscore and the search term included the underscore followed by at least one letter the search wouldn't match * Fixed a bug where if the search terms were all illegal characters such that the search list was empty for a requested search a mongo error would be thrown --- RELEASE_NOTES.md | 8 ++ src/main/java/us/kbase/auth2/Version.java | 2 +- .../us/kbase/auth2/lib/Authentication.java | 15 +- .../java/us/kbase/auth2/lib/UserName.java | 34 ++++- .../us/kbase/auth2/lib/UserSearchSpec.java | 127 ++++++++++++----- .../auth2/lib/storage/mongo/MongoStorage.java | 20 +-- .../us/kbase/test/auth2/lib/UserNameTest.java | 33 ++++- .../test/auth2/lib/UserSearchSpecTest.java | 129 ++++++++++++++---- .../MongoStorageGetDisplayNamesTest.java | 21 +++ .../auth2/service/api/UserEndpointTest.java | 93 ++++++++----- .../service/common/ServiceCommonTest.java | 2 +- 11 files changed, 370 insertions(+), 114 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4dfc4809..ba235ddd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,13 @@ # Authentication Service MKII release notes +## 0.7.2 + +* Fixed a bug where usernames with underscores would not be matched in username searches if an + underscore was an interior character of a search prefix. +* Fixed a bug where a MongoDB error would be thrown if a user search prefix resulted in no search + terms if it had no valid characters for the requested search, whether user name or display + name. Now a service error is thrown. + ## 0.7.1 * Publishes a shadow jar on jitpack.io for supporting tests in other repos. diff --git a/src/main/java/us/kbase/auth2/Version.java b/src/main/java/us/kbase/auth2/Version.java index 30993a69..bd75936f 100644 --- a/src/main/java/us/kbase/auth2/Version.java +++ b/src/main/java/us/kbase/auth2/Version.java @@ -5,6 +5,6 @@ public class Version { /** The version of the KBase Auth2 service. */ - public static final String VERSION = "0.7.1"; + public static final String VERSION = "0.7.2"; } diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index 93ba621d..72fc0b7d 100644 --- a/src/main/java/us/kbase/auth2/lib/Authentication.java +++ b/src/main/java/us/kbase/auth2/lib/Authentication.java @@ -1181,10 +1181,17 @@ private Optional getAvailableUserName( * problems. Make this smarter if necessary. E.g. could store username and numeric suffix * db side and search and sort db side. */ - final UserSearchSpec spec = UserSearchSpec.getBuilder() - // checked that this does indeed use an index for the mongo implementation - .withSearchRegex("^" + Pattern.quote(sugStrip) + "\\d*$") - .withSearchOnUserName(true).withIncludeDisabled(true).build(); + final UserSearchSpec spec; + try { + spec = UserSearchSpec.getBuilder() + // checked that this does indeed use an index for the mongo implementation + .withSearchRegex("^" + Pattern.quote(sugStrip) + "\\d*$") + .withSearchOnUserName(true) + .withIncludeDisabled(true) + .build(); + } catch (IllegalParameterException e) { + throw new RuntimeException("this is impossible", e); + } final Map users = storage.getUserDisplayNames(spec, -1); final boolean match = users.containsKey(suggestedUserName); final boolean hasNumSuffix = sugStrip.length() != sugName.length(); diff --git a/src/main/java/us/kbase/auth2/lib/UserName.java b/src/main/java/us/kbase/auth2/lib/UserName.java index 97af8559..7b94c51d 100644 --- a/src/main/java/us/kbase/auth2/lib/UserName.java +++ b/src/main/java/us/kbase/auth2/lib/UserName.java @@ -1,10 +1,14 @@ package us.kbase.auth2.lib; import static java.util.Objects.requireNonNull; +import static us.kbase.auth2.lib.Utils.checkStringNoCheckedException; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import us.kbase.auth2.lib.exceptions.ErrorType; import us.kbase.auth2.lib.exceptions.IllegalParameterException; @@ -35,8 +39,8 @@ public class UserName extends Name { } } - private static final String INVALID_CHARS_REGEX = "[^a-z\\d_]+"; - private final static Pattern INVALID_CHARS = Pattern.compile(INVALID_CHARS_REGEX); + private static final Pattern FORCE_ALPHA_FIRST_CHAR = Pattern.compile("^[^a-z]+"); + private final static Pattern INVALID_CHARS = Pattern.compile("[^a-z\\d_]+"); public final static int MAX_NAME_LENGTH = 100; /** Create a new user name. @@ -75,8 +79,7 @@ public boolean isRoot() { */ public static Optional sanitizeName(final String suggestedUserName) { requireNonNull(suggestedUserName, "suggestedUserName"); - final String s = suggestedUserName.toLowerCase().replaceAll(INVALID_CHARS_REGEX, "") - .replaceAll("^[^a-z]+", ""); + final String s = cleanUserName(suggestedUserName); try { return s.isEmpty() ? Optional.empty() : Optional.of(new UserName(s)); } catch (IllegalParameterException | MissingParameterException e) { @@ -84,6 +87,29 @@ public static Optional sanitizeName(final String suggestedUserName) { } } + private static String cleanUserName(final String putativeName) { + return FORCE_ALPHA_FIRST_CHAR.matcher( + INVALID_CHARS.matcher( + putativeName.toLowerCase()) + .replaceAll("")) + .replaceAll(""); + } + + /** Given a string, splits the string by whitespace, strips all illegal + * characters from the tokens, and returns the resulting strings, + * discarding repeats. + * @param names the names string to process. + * @return the list of canonicalized names. + */ + public static List getCanonicalNames(final String names) { + checkStringNoCheckedException(names, "names"); + return Arrays.asList(names.toLowerCase().split("\\s+")).stream() + .map(u -> cleanUserName(u)) + .filter(u -> !u.isEmpty()) + .distinct() + .collect(Collectors.toList()); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/us/kbase/auth2/lib/UserSearchSpec.java b/src/main/java/us/kbase/auth2/lib/UserSearchSpec.java index 613fe483..b51c2675 100644 --- a/src/main/java/us/kbase/auth2/lib/UserSearchSpec.java +++ b/src/main/java/us/kbase/auth2/lib/UserSearchSpec.java @@ -10,6 +10,8 @@ import java.util.Optional; import java.util.Set; +import us.kbase.auth2.lib.exceptions.IllegalParameterException; + /** A specification for how a user search should be conducted. * * If a search prefix or regex is supplied and neither withSearchOnUserName() nor @@ -24,7 +26,8 @@ public class UserSearchSpec { //TODO ZLATER CODE don't expose regex externally. Not sure how best to do this without duplicating a lot of the class. For now setting regex is default access (package only). - private final List prefixes; + private final List userNamePrefixes; + private final List displayPrefixes; private final String regex; private final boolean searchUser; private final boolean searchDisplayName; @@ -34,7 +37,8 @@ public class UserSearchSpec { private final boolean includeDisabled; private UserSearchSpec( - final List prefixes, + final List userNamePrefixes, + final List displayPrefixes, final String regex, final boolean searchUser, final boolean searchDisplayName, @@ -42,7 +46,10 @@ private UserSearchSpec( final Set searchCustomRoles, final boolean includeRoot, final boolean includeDisabled) { - this.prefixes = prefixes == null ? null : Collections.unmodifiableList(prefixes); + this.userNamePrefixes = userNamePrefixes == null ? null : + Collections.unmodifiableList(userNamePrefixes); + this.displayPrefixes = displayPrefixes == null ? null : + Collections.unmodifiableList(displayPrefixes); this.regex = regex; this.searchUser = searchUser; this.searchDisplayName = searchDisplayName; @@ -52,13 +59,21 @@ private UserSearchSpec( this.includeDisabled = includeDisabled; } - /** Returns the user and/or display name prefixes for the search, if any. - * The prefixes match the start of the username or the start of any part of the whitespace + /** Returns the user name prefixes for the search, if any. + * The prefixes match the start of the user name. + * @return the search prefix. + */ + public List getSearchUserNamePrefixes() { + return userNamePrefixes == null ? Collections.emptyList() : userNamePrefixes; + } + + /** Returns the display name prefixes for the search, if any. + * The prefixes match the start of any part of the whitespace * tokenized display name. * @return the search prefix. */ - public List getSearchPrefixes() { - return prefixes == null ? Collections.emptyList() : prefixes; + public List getSearchDisplayPrefixes() { + return displayPrefixes == null ? Collections.emptyList() : displayPrefixes; } /** Returns the user and/or display name regex for the search, if any. @@ -80,35 +95,33 @@ public boolean hasSearchRegex() { * @return true if the search prefixes are set. */ public boolean hasSearchPrefixes() { - return prefixes != null; + return displayPrefixes != null; } - private boolean hasSearchString() { - return prefixes != null || regex != null; - } - /** Returns true if a search should occur on the user's user name. * - * True when a) a prefix or regex is provided and b) withSearchOnUserName() was called with a - * true argument or neither withSearchOnUserName() nor withSearchOnDisplayName() were called - * with a true argument. + * True when + * a) a prefix with a valid format for a username or regex is provided and + * b) withSearchOnUserName() was called with a true argument or neither or both of + * withSearchOnUserName() and withSearchOnDisplayName() were called with a true argument. * @return whether the search should occur on the user's user name with the provided prefix or * regex. */ public boolean isUserNameSearch() { - return searchUser || (hasSearchString() && !searchDisplayName); + return (regex != null || userNamePrefixes != null) && (searchUser || !searchDisplayName); } /** Returns true if a search should occur on the user's tokenized display name. * - * True when a) a prefix or regex is provided and b) withSearchOnDisplayName() was called with - * a true argument or neither withSearchOnUserName() nor withSearchOnDisplayName() were - * called with a true argument. + * True when + * a) a prefix or regex is provided and + * b) withSearchOnDisplayName() was called with a true argument or neither or both of + * withSearchOnUserName() and withSearchOnDisplayName() were called with a true argument. * @return whether the search should occur on the users's display name with the provided * prefix or regex. */ public boolean isDisplayNameSearch() { - return searchDisplayName || (hasSearchString() && !searchUser); + return (regex != null || displayPrefixes != null) && (searchDisplayName || !searchUser); } /** Returns true if a search should occur on the user's roles. @@ -202,8 +215,8 @@ public static Builder getBuilder() { @Override public int hashCode() { - return Objects.hash(includeDisabled, includeRoot, prefixes, regex, - searchCustomRoles, searchDisplayName, searchRoles, searchUser); + return Objects.hash(displayPrefixes, includeDisabled, includeRoot, regex, + searchCustomRoles, searchDisplayName, searchRoles, searchUser, userNamePrefixes); } @Override @@ -218,14 +231,15 @@ public boolean equals(Object obj) { return false; } UserSearchSpec other = (UserSearchSpec) obj; - return includeDisabled == other.includeDisabled + return Objects.equals(displayPrefixes, other.displayPrefixes) + && includeDisabled == other.includeDisabled && includeRoot == other.includeRoot - && Objects.equals(prefixes, other.prefixes) && Objects.equals(regex, other.regex) && Objects.equals(searchCustomRoles, other.searchCustomRoles) && searchDisplayName == other.searchDisplayName && Objects.equals(searchRoles, other.searchRoles) - && searchUser == other.searchUser; + && searchUser == other.searchUser + && Objects.equals(userNamePrefixes, other.userNamePrefixes); } /** A builder for a UserSearchSpec. @@ -234,7 +248,7 @@ public boolean equals(Object obj) { */ public static class Builder { - private List prefixes = null; + private String prefix; private String regex = null; private boolean searchUser = false; private boolean searchDisplayName = false; @@ -249,15 +263,16 @@ private Builder() {} * The prefix will replace the search regex, if any. * The prefix matches the start of the username or the start of any part of the whitespace * and hyphen tokenized display name. - * The prefix is always split by whitespace and hyphens, punctuation removed, and - * converted to lower case. + * The user name prefix is split by whitespace and all illegal characters removed. + * The display name prefix is split by whitespace and hyphens, punctuation removed, + * and converted to lower case. * Once the prefix or search regex is set in this builder it cannot be removed. * @param prefix the prefix. * @return this builder. */ public Builder withSearchPrefix(final String prefix) { checkStringNoCheckedException(prefix, "prefix"); - this.prefixes = DisplayName.getCanonicalDisplayName(prefix); + this.prefix = prefix; this.regex = null; return this; } @@ -273,12 +288,14 @@ public Builder withSearchPrefix(final String prefix) { */ Builder withSearchRegex(final String regex) { this.regex = checkStringNoCheckedException(regex, "regex"); - this.prefixes = null; + this.prefix = null; return this; } /** Specify whether a search on a users's user name should occur. * A prefix must be set prior to calling this method. + * If neither a user nor a display search is set (the default) and a prefix is set, then + * the search occurs on both fields. * @param search whether the search should occur on the user's user name. * @return this builder. */ @@ -290,6 +307,8 @@ public Builder withSearchOnUserName(final boolean search) { /** Specify whether a search on a users's display name should occur. * A prefix must be set prior to calling this method. + * If neither a user nor a display search is set (the default) and a prefix is set, then + * the search occurs on both fields. * @param search whether the search should occur on the user's display name. * @return this builder. */ @@ -300,7 +319,7 @@ public Builder withSearchOnDisplayName(final boolean search) { } private void checkSearchPrefix(final boolean search) { - if (search && prefixes == null && regex == null) { + if (search && prefix == null && regex == null) { throw new IllegalStateException( "Must provide a prefix or regex if a name search is to occur"); } @@ -353,10 +372,50 @@ public Builder withIncludeDisabled(final boolean include) { /** Build a UserSearchSpec instance. * @return a UserSearchSpec. + * @throws IllegalParameterException if a prefix is set that, after normalizing, contains + * no characters for the requested search(es). */ - public UserSearchSpec build() { - return new UserSearchSpec(prefixes, regex, searchUser, searchDisplayName, searchRoles, - searchCustomRoles, includeRoot, includeDisabled); + public UserSearchSpec build() throws IllegalParameterException { + List userNamePrefixes = null; + List displayPrefixes = null; + if (this.prefix != null) { + /* UsrSrch DisSrch UsrOK DisOK Throw exception? + * T T Y implies Y + * T T No Y No, just go with display search + * T T No No Display or user exception + * + * T F Y implies Y + * T F No Y User exception + * T F No No User exception + * + * F T Y implies Y + * F T No Y + * F T No No Display exception + * + * Note that: + * * If the user search is ok (UsrOK) the display search must be ok since the + * user search has at least one a-z char. + * * The first block where UsrSrch and DisSrch are all true is equivalent + * to a block where they're all false, and so that block is omitted. + */ + userNamePrefixes = UserName.getCanonicalNames(prefix); + userNamePrefixes = userNamePrefixes.isEmpty() ? null : userNamePrefixes; + displayPrefixes = DisplayName.getCanonicalDisplayName(prefix); + displayPrefixes = displayPrefixes.isEmpty() ? null : displayPrefixes; + if (searchUser && !searchDisplayName && userNamePrefixes == null) { + throw new IllegalParameterException(String.format( + "The search prefix %s contains no valid username prefix " + + "and a user name search was requested", this.prefix)); + } + if (displayPrefixes == null) { + throw new IllegalParameterException(String.format( + "The search prefix %s contains only punctuation and a " + + "display name search was requested", this.prefix)); + } + } + return new UserSearchSpec(userNamePrefixes, displayPrefixes, regex, searchUser, + searchDisplayName, searchRoles, searchCustomRoles, + includeRoot, includeDisabled); } } } diff --git a/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java b/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java index e1c2a287..f2e520f6 100644 --- a/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java +++ b/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java @@ -1131,6 +1131,11 @@ public Map testModeGetUserDisplayNames(final Set getDisplayNames( final String collection, final Document query, @@ -1156,6 +1161,7 @@ private Map getDisplayNames( } } + // Sort on a field we're querying otherwise a table scan could occur private static final Map SEARCHFIELD_TO_FIELD; static { final Map m = new HashMap<>(); @@ -1166,9 +1172,9 @@ private Map getDisplayNames( SEARCHFIELD_TO_FIELD = m; } - private Document andRegexes(final String field, final List regexes) { - return new Document("$and", regexes.stream() - .map(regex -> new Document(field, regex)) + private Document andRegexes(final String field, final List prefixes) { + return new Document("$and", prefixes.stream() + .map(t -> new Document(field, new Document("$regex", "^" + Pattern.quote(t)))) .collect(Collectors.toList())); } @@ -1180,16 +1186,14 @@ public Map getUserDisplayNames( requireNonNull(spec, "spec"); final Document query = new Document(); if (spec.hasSearchPrefixes()) { - final List regexes = spec.getSearchPrefixes().stream() - .map(token -> new Document("$regex", "^" + Pattern.quote(token))) - .collect(Collectors.toList()); final List queries = new LinkedList<>(); if (spec.isDisplayNameSearch()) { - queries.add(andRegexes(Fields.USER_DISPLAY_NAME_CANONICAL, regexes)); + queries.add(andRegexes( + Fields.USER_DISPLAY_NAME_CANONICAL, spec.getSearchDisplayPrefixes())); } if (spec.isUserNameSearch() ) { // this means if there's > 1 token nothing will match, but that seems right - queries.add(andRegexes(Fields.USER_NAME, regexes)); + queries.add(andRegexes(Fields.USER_NAME, spec.getSearchUserNamePrefixes())); } query.put("$or", queries); } diff --git a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java index 6a99f5e0..d5085a63 100644 --- a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static us.kbase.test.auth2.TestCommon.list; import org.junit.Test; @@ -104,8 +105,8 @@ public void equals() throws Exception { @Test public void sanitize() throws Exception { - assertThat("incorrect santize", UserName.sanitizeName(" 999aFA8 ea6t \t ѱ ** J(())"), - is(Optional.of(new UserName("afa8ea6tj")))); + assertThat("incorrect santize", UserName.sanitizeName(" 999aF_A8 ea6t \t ѱ ** J(())"), + is(Optional.of(new UserName("af_a8ea6tj")))); assertThat("incorrect santize", UserName.sanitizeName("999 8 6 \t ѱ ** (())"), is(Optional.empty())); } @@ -120,4 +121,32 @@ public void failSanitize() { } } + @Test + public void getCanonicalNames() throws Exception { + assertThat("incorrect canonicalize", + UserName.getCanonicalNames( + " 999aF_A8 ea6t \t foo _ѱaatѱ(*) 891**ѱ \n fo-o x\n"), + is(list("af_a8", "ea6t", "foo", "aat", "x"))); + assertThat("incorrect canonicalize", + UserName.getCanonicalNames( + " 999_8 6 \t _ѱѱ(*) 891**ѱ \n \n"), + is(list())); + } + + @Test + public void getCanonicalNamesFail() throws Exception { + failGetCanonicalNames(null); + failGetCanonicalNames(" \t "); + } + + private void failGetCanonicalNames(final String names) { + try { + UserName.getCanonicalNames(names); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, new IllegalArgumentException( + "names cannot be null or whitespace only")); + } + } + } diff --git a/src/test/java/us/kbase/test/auth2/lib/UserSearchSpecTest.java b/src/test/java/us/kbase/test/auth2/lib/UserSearchSpecTest.java index b5c738a5..3a1e1137 100644 --- a/src/test/java/us/kbase/test/auth2/lib/UserSearchSpecTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/UserSearchSpecTest.java @@ -20,6 +20,7 @@ import us.kbase.auth2.lib.UserSearchSpec; import us.kbase.auth2.lib.UserSearchSpec.Builder; import us.kbase.auth2.lib.UserSearchSpec.SearchField; +import us.kbase.auth2.lib.exceptions.IllegalParameterException; import us.kbase.test.auth2.TestCommon; public class UserSearchSpecTest { @@ -32,9 +33,9 @@ public void equals() { } @Test - public void buildWithEverything() { + public void buildWithEverything() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() - .withSearchPrefix("F*oo bar *()") + .withSearchPrefix("F*oo bar *() baz_bat") .withSearchOnUserName(true) .withSearchOnDisplayName(true) .withSearchOnRole(Role.ADMIN) @@ -45,7 +46,10 @@ public void buildWithEverything() { .withIncludeDisabled(true) .build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list("foo", "bar"))); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), + is(list("foo", "bar", "baz_bat"))); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), + is(list("foo", "bar", "bazbat"))); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(true)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -60,13 +64,13 @@ public void buildWithEverything() { assertThat("incorrect orderby", uss.orderBy(), is(SearchField.USERNAME)); assertThat("incorrect include root", uss.isRootIncluded(), is(true)); assertThat("incorrect include disabled", uss.isDisabledIncluded(), is(true)); - } @Test - public void buildWithNothing() { + public void buildWithNothing() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder().build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list())); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list())); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list())); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(false)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -83,10 +87,11 @@ public void buildWithNothing() { } @Test - public void buildWithPrefixOnly() { + public void buildWithPrefixOnly() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchPrefix("foO").build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list("foo"))); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list("foo"))); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list("foo"))); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(true)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -103,11 +108,12 @@ public void buildWithPrefixOnly() { } @Test - public void buildUserSearch() { + public void buildUserSearch() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchPrefix("foo") .withSearchOnUserName(true).build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list("foo"))); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list("foo"))); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list("foo"))); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(true)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -124,13 +130,14 @@ public void buildUserSearch() { } @Test - public void buildDisplaySearch() { + public void buildDisplaySearch() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchPrefix("foo") .withSearchOnDisplayName(true) .withSearchOnCustomRole("bar") .withSearchOnRole(Role.SERV_TOKEN).build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list("foo"))); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list("foo"))); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list("foo"))); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(true)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -146,11 +153,12 @@ public void buildDisplaySearch() { } @Test - public void buildCustomRoleSearch() { + public void buildCustomRoleSearch() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchOnCustomRole("foo") .withSearchOnRole(Role.DEV_TOKEN).build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list())); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list())); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list())); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(false)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -166,10 +174,11 @@ public void buildCustomRoleSearch() { } @Test - public void buildRoleSearch() { + public void buildRoleSearch() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchOnRole(Role.DEV_TOKEN).build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list())); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list())); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list())); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(false)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -186,10 +195,11 @@ public void buildRoleSearch() { } @Test - public void resetSearch() { + public void resetSearch() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder().withSearchPrefix("foo") .withSearchOnUserName(false).withSearchOnDisplayName(false).build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list("foo"))); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list("foo"))); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list("foo"))); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(true)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -210,7 +220,8 @@ public void regex() throws Exception { final Builder b = UserSearchSpec.getBuilder(); setRegex(b, "\\Qfoo.bar\\E"); final UserSearchSpec uss = b.build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list())); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list())); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list())); assertThat("incorrect regex", uss.getSearchRegex(), is(opt("\\Qfoo.bar\\E"))); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(false)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(true)); @@ -238,7 +249,8 @@ public void prefixToRegex() throws Exception { final Builder b = UserSearchSpec.getBuilder().withSearchPrefix("foo"); setRegex(b, "\\Qfoo.bar\\E"); final UserSearchSpec uss = b.build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list())); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list())); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list())); assertThat("incorrect regex", uss.getSearchRegex(), is(opt("\\Qfoo.bar\\E"))); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(false)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(true)); @@ -259,7 +271,8 @@ public void regexToPrefix() throws Exception { final Builder b = UserSearchSpec.getBuilder(); setRegex(b, "\\Qfoo.bar\\E"); final UserSearchSpec uss = b.withSearchPrefix("foo").build(); - assertThat("incorrect prefix", uss.getSearchPrefixes(), is(list("foo"))); + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list("foo"))); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list("foo"))); assertThat("incorrect regex", uss.getSearchRegex(), is(MT)); assertThat("incorrect has prefixes", uss.hasSearchPrefixes(), is(true)); assertThat("incorrect has regex", uss.hasSearchRegex(), is(false)); @@ -276,10 +289,16 @@ public void regexToPrefix() throws Exception { } @Test - public void immutablePrefixes() { + public void immutablePrefixes() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder().withSearchPrefix("foo bar").build(); try { - uss.getSearchPrefixes().add("baz"); + uss.getSearchUserNamePrefixes().add("baz"); + fail("expected exception"); + } catch (UnsupportedOperationException e) { + //test passed + } + try { + uss.getSearchDisplayPrefixes().add("baz"); fail("expected exception"); } catch (UnsupportedOperationException e) { //test passed @@ -287,7 +306,7 @@ public void immutablePrefixes() { } @Test - public void immutableRoles() { + public void immutableRoles() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchOnRole(Role.DEV_TOKEN).build(); try { @@ -299,7 +318,7 @@ public void immutableRoles() { } @Test - public void immutableCustomRoles() { + public void immutableCustomRoles() throws Exception { final UserSearchSpec uss = UserSearchSpec.getBuilder() .withSearchOnCustomRole("foo").build(); try { @@ -310,6 +329,66 @@ public void immutableCustomRoles() { } } + private static final String ERR_USER_SEARCH = "The search prefix %s contains no valid " + + "username prefix and a user name search was requested"; + + @Test + public void buildUserSearchWithInvalidAndValidPrefixes() throws Exception { + // if the user search spec is good, the display spec must be good. The reverse is not true. + final Exception e = new IllegalParameterException(String.format(ERR_USER_SEARCH, "98_7")); + final Builder b = UserSearchSpec.getBuilder() + .withSearchPrefix("98_7"); // valid display spec, not user spec + + // test that no exception is thrown + UserSearchSpec uss = b.build(); + buildUserSearchWithInvalidAndValidPrefixesAssertOnPass(uss); + + buildFail(b.withSearchOnUserName(true), e); + + // test that no exception is thrown + uss = b.withSearchOnDisplayName(true).build(); + buildUserSearchWithInvalidAndValidPrefixesAssertOnPass(uss); + + // test that no exception is thrown + uss = b.withSearchOnUserName(false).build(); + buildUserSearchWithInvalidAndValidPrefixesAssertOnPass(uss); + } + + private void buildUserSearchWithInvalidAndValidPrefixesAssertOnPass(final UserSearchSpec uss) { + assertThat("incorrect user prefix", uss.getSearchUserNamePrefixes(), is(list())); + assertThat("incorrect display prefix", uss.getSearchDisplayPrefixes(), is(list("987"))); + assertThat("incorrect user search", uss.isUserNameSearch(), is(false)); + assertThat("incorrect display name search", uss.isDisplayNameSearch(), is(true)); + } + + private static final String ERR_DISPLAY_SEARCH = "The search prefix &*^(%(^*&) contains only " + + "punctuation and a display name search was requested"; + + @Test + public void buildDisplaySearchFail() throws Exception { + // if the user search spec is good, the display spec must be good. The reverse is not true. + final Exception e = new IllegalParameterException(ERR_DISPLAY_SEARCH); + final Exception euser = new IllegalParameterException( + String.format(ERR_USER_SEARCH, ("&*^(%(^*&)"))); + final Builder b = UserSearchSpec.getBuilder() + .withSearchPrefix("&*^(%(^*&)"); + buildFail(b, e); + + buildFail(b.withSearchOnDisplayName(true), e); + buildFail(b.withSearchOnUserName(true), e); + buildFail(b.withSearchOnDisplayName(false), euser); + } + + + private void buildFail(final Builder b, final Exception expected) { + try { + b.build(); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, expected); + } + } + @Test public void addPrefixFail() { failAddPrefix(null, new IllegalArgumentException( diff --git a/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageGetDisplayNamesTest.java b/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageGetDisplayNamesTest.java index cb7c6d39..3703d207 100644 --- a/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageGetDisplayNamesTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageGetDisplayNamesTest.java @@ -17,6 +17,8 @@ import org.junit.Test; +import com.google.common.collect.ImmutableMap; + import us.kbase.auth2.lib.CustomRole; import us.kbase.auth2.lib.DisplayName; import us.kbase.auth2.lib.Role; @@ -655,6 +657,25 @@ public void canonicalSearchPunctuation4() throws Exception { is(Collections.emptyMap())); } + @Test + public void userSearchUnderscore() throws Exception { + storage.createUser(NewUser.getBuilder( + new UserName("foo_bar"), UID1, new DisplayName("1"), + NOW, REMOTE1) + .build()); + storage.createUser(NewUser.getBuilder( + new UserName("fo_obar"), UID2, new DisplayName("2"), NOW, REMOTE2) + .build()); + + assertThat("incorrect users found", storage.getUserDisplayNames(UserSearchSpec.getBuilder() + .withSearchPrefix("fo_").build(), -1), + is(ImmutableMap.of(new UserName("fo_obar"), new DisplayName("2")))); + + assertThat("incorrect users found", storage.getUserDisplayNames(UserSearchSpec.getBuilder() + .withSearchPrefix("foo_b").build(), -1), + is(ImmutableMap.of(new UserName("foo_bar"), new DisplayName("1")))); + } + @Test public void canonicalSearch1() throws Exception { createUsersForCanonicalSearch(); diff --git a/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java b/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java index 200c866c..08e79047 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java @@ -3,12 +3,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestJSON; +import static us.kbase.test.auth2.TestCommon.inst; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.nio.file.Path; -import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -154,12 +154,12 @@ public void testModeFail() throws Exception { public void getMeMinimalInput() throws Exception { final UUID uid = UUID.randomUUID(); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foobar"), - uid, new DisplayName("bleah"), Instant.ofEpochMilli(20000)).build(), + uid, new DisplayName("bleah"), inst(20000)).build(), new PasswordHashAndSalt("foobarbazbing".getBytes(), "aa".getBytes())); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("foobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("foobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/V2/me").build(); @@ -197,25 +197,25 @@ public void getMeMaximalInput() throws Exception { manager.storage.setCustomRole(new CustomRole("whoo", "a")); manager.storage.setCustomRole(new CustomRole("whee", "b")); manager.storage.createUser(NewUser.getBuilder(new UserName("foobar"), UID, - new DisplayName("bleah"), Instant.ofEpochMilli(20000), + new DisplayName("bleah"), inst(20000), new RemoteIdentity(new RemoteIdentityID("prov", "id"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withCustomRole("whoo") .withCustomRole("whee") .withEmailAddress(new EmailAddress("a@g.com")) - .withLastLogin(Instant.ofEpochMilli(30000)) + .withLastLogin(inst(30000)) .withRole(Role.ADMIN) .withRole(Role.DEV_TOKEN) - .withPolicyID(new PolicyID("wugga"), Instant.ofEpochMilli(40000)) - .withPolicyID(new PolicyID("wubba"), Instant.ofEpochMilli(50000)) + .withPolicyID(new PolicyID("wugga"), inst(40000)) + .withPolicyID(new PolicyID("wubba"), inst(50000)) .build()); manager.storage.link(new UserName("foobar"), new RemoteIdentity( new RemoteIdentityID("prov2", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com"))); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("foobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("foobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/V2/me").build(); @@ -295,13 +295,13 @@ public void getMeFailBadToken() throws Exception { @Test public void putMeNoUpdate() throws Exception { manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foobar"), - UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) + UID, new DisplayName("bleah"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), new PasswordHashAndSalt("foobarbazbing".getBytes(), "aa".getBytes())); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("foobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("foobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/V2/me").build(); @@ -316,7 +316,7 @@ UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) assertThat("user modified unexpectedly", manager.storage.getUser(new UserName("foobar")), is(AuthUser.getBuilder(new UserName("foobar"), UID, new DisplayName("bleah"), - Instant.ofEpochMilli(20000)) + inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")) .build())); } @@ -324,13 +324,13 @@ UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) @Test public void putMeFullUpdate() throws Exception { manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foobar"), - UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) + UID, new DisplayName("bleah"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), new PasswordHashAndSalt("foobarbazbing".getBytes(), "aa".getBytes())); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("foobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("foobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/V2/me").build(); @@ -346,7 +346,7 @@ UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) assertThat("user not modified", manager.storage.getUser(new UserName("foobar")), is(AuthUser.getBuilder(new UserName("foobar"), UID, new DisplayName("whee"), - Instant.ofEpochMilli(20000)) + inst(20000)) .withEmailAddress(new EmailAddress("x@g.com")) .build())); } @@ -421,13 +421,13 @@ public void getGlobusUserSelfWithGlobusHeader() throws Exception { final PasswordHashAndSalt creds = new PasswordHashAndSalt( "foobarbazbing".getBytes(), "aa".getBytes()); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foobar"), - UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) + UID, new DisplayName("bleah"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), creds); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("foobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("foobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/legacy/globus/users/foobar") @@ -465,17 +465,17 @@ public void getGlobusUserOtherWithAuthHeader() throws Exception { final PasswordHashAndSalt creds = new PasswordHashAndSalt( "foobarbazbing".getBytes(), "aa".getBytes()); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foobar"), - UID, new DisplayName("bleah"), Instant.ofEpochMilli(20000)) + UID, new DisplayName("bleah"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), creds); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foobaz"), - UUID.randomUUID(),new DisplayName("bleah2"), Instant.ofEpochMilli(20000)) + UUID.randomUUID(),new DisplayName("bleah2"), inst(20000)) .withEmailAddress(new EmailAddress("f2@g.com")).build(), creds); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("foobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("foobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/legacy/globus/users/foobaz") @@ -604,31 +604,35 @@ private IncomingToken setUpUsersForTesting() throws Exception { "foobarbazbing".getBytes(), "aa".getBytes()); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("foo"), - uuid(), new DisplayName("bar *thing*"), Instant.ofEpochMilli(20000)) + uuid(), new DisplayName("bar *thing*"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), creds); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("baz"), - uuid(), new DisplayName("fuz"), Instant.ofEpochMilli(20000)) + uuid(), new DisplayName("fuz"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), creds); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("puz"), - uuid(), new DisplayName("mup"), Instant.ofEpochMilli(20000)) + uuid(), new DisplayName("mup"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), creds); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("mua"), - uuid(), new DisplayName("paz"), Instant.ofEpochMilli(20000)) + uuid(), new DisplayName("paz"), inst(20000)) + .withEmailAddress(new EmailAddress("f@h.com")).build(), + creds); + manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("under_score"), + uuid(), new DisplayName("zzznevermind"), inst(20000)) .withEmailAddress(new EmailAddress("f@h.com")).build(), creds); manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("toobar"), - uuid(), new DisplayName("bleah2"), Instant.ofEpochMilli(20000)) + uuid(), new DisplayName("bleah2"), inst(20000)) .withEmailAddress(new EmailAddress("f2@g.com")).build(), creds); final IncomingToken token = new IncomingToken("whee"); manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(), - new UserName("toobar")).withLifeTime(Instant.ofEpochMilli(10000), - Instant.ofEpochMilli(1000000000000000L)).build(), + new UserName("toobar")).withLifeTime(inst(10000), + inst(1000000000000000L)).build(), token.getHashedToken().getTokenHash()); return token; } @@ -682,6 +686,15 @@ public void searchUsersBlankFields() throws Exception { searchUsers("f", " \t , ", ImmutableMap.of("foo", "bar *thing*", "baz", "fuz")); } + @Test + public void searchUsersUnderscore() throws Exception { + // The display name canonicalization previously applied to the user name as well, which + // caused a bug since the username in the database is not canonicalized. This would cause + // searches to fail when the one allowed punctuation symbol `_`, was included in + // the search term. + searchUsers("9un$der_s", "", ImmutableMap.of("under_score", "zzznevermind")); + } + @Test public void searchUsersUserName() throws Exception { searchUsers("f", " username , \t ", ImmutableMap.of("foo", "bar *thing*")); @@ -742,17 +755,27 @@ private void searchUsers( @Test public void searchUsersFailBadToken() throws Exception { - failSearchUsers(null, 400, "Bad Request", + failSearchUsers("f", null, 400, "Bad Request", new NoTokenProvidedException("No user token provided")); - failSearchUsers("foobar", 401, "Unauthorized", new InvalidTokenException()); + failSearchUsers("f", "foobar", 401, "Unauthorized", new InvalidTokenException()); + } + + @Test + public void searchUsersFailBadInput() throws Exception { + final IncomingToken token = setUpUsersForTesting(); + failSearchUsers("*^&)*^)", token.getToken(), 400, "Bad Request", + new IllegalParameterException( + "The search prefix *^&)*^) contains only " + + "punctuation and a display name search was requested")); } private void failSearchUsers( + final String prefix, final String token, final int code, final String error, final AuthException e) throws Exception { - final URI target = UriBuilder.fromUri(host).path("/api/V2/users/search/f").build(); + final URI target = UriBuilder.fromUri(host).path("/api/V2/users/search/" + prefix).build(); final WebTarget wt = CLI.target(target); final Builder req = wt.request() diff --git a/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java b/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java index bf662eb9..5f4983dc 100644 --- a/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java +++ b/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java @@ -47,7 +47,7 @@ public class ServiceCommonTest { public static final String SERVICE_NAME = "Authentication Service"; - public static final String SERVER_VER = "0.7.1"; + public static final String SERVER_VER = "0.7.2"; public static final String GIT_ERR = "Missing git commit file gitcommit, should be in us.kbase.auth2"; From d0ef8c94e12f50023c94e312f1cb09699f86898a Mon Sep 17 00:00:00 2001 From: Sijie Date: Fri, 13 Jun 2025 17:07:13 -0700 Subject: [PATCH 02/24] add gradle to dependabot file --- .github/dependabot.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f506fa4b..9a383169 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,26 @@ version: 2 updates: - # Docker + # Docker base images - package-ecosystem: docker directory: "/" schedule: interval: weekly - time: '11:00' + time: "11:00" open-pull-requests-limit: 25 - # GitHub Actions - - package-ecosystem: "github-actions" + # GitHub Actions workflows + - package-ecosystem: github-actions directory: ".github/workflows" schedule: - interval: "monthly" + interval: monthly + time: "11:00" + open-pull-requests-limit: 25 + + # Gradle (Java dependencies) + - package-ecosystem: gradle + directory: "/" + schedule: + interval: monthly + time: "11:00" open-pull-requests-limit: 25 From f49190b8695317632b4ba81724e50c698c4276b8 Mon Sep 17 00:00:00 2001 From: Sijie Date: Mon, 16 Jun 2025 13:05:14 -0700 Subject: [PATCH 03/24] change schedule.interval to monthly --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9a383169..d2a5af66 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ updates: - package-ecosystem: docker directory: "/" schedule: - interval: weekly + interval: monthly time: "11:00" open-pull-requests-limit: 25 From cbbb3b58d8f86fa12fe13b51e0502a79e821acfe Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Thu, 2 Oct 2025 14:44:12 -0700 Subject: [PATCH 04/24] Move LoginTest -> LoginIntegrationTest ...because that's what it is and I want to create a non-integration LoginTest. --- .../service/ui/{LoginTest.java => LoginIntegrationTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/java/us/kbase/test/auth2/service/ui/{LoginTest.java => LoginIntegrationTest.java} (99%) diff --git a/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java similarity index 99% rename from src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java rename to src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java index 97c18ae9..47395fd2 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java @@ -88,7 +88,7 @@ import us.kbase.test.auth2.service.ServiceTestUtils; import us.kbase.testutils.RegexMatcher; -public class LoginTest { +public class LoginIntegrationTest { //TODO TEST convert most of these to unit tests, but keep enough for integration tests From 11012d2d16a9bf34c8c2981a6a25717f4a8a4ea6 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Thu, 2 Oct 2025 15:16:55 -0700 Subject: [PATCH 05/24] Move login integration test files to correct locations doh --- ...inIntegrationTest_loginChoice2CreateAndLoginDisabled.testdata} | 0 ...ginIntegrationTest_loginChoice2CreateWithRedirectURL.testdata} | 0 ...onTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata} | 0 ...ata => LoginIntegrationTest_loginChoice3Create2Login.testdata} | 0 ...ta => LoginIntegrationTest_startDisplayLoginDisabled.testdata} | 0 ... => LoginIntegrationTest_startDisplayWithOneProvider.testdata} | 0 ...=> LoginIntegrationTest_startDisplayWithTwoProviders.testdata} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_loginChoice2CreateAndLoginDisabled.testdata => LoginIntegrationTest_loginChoice2CreateAndLoginDisabled.testdata} (100%) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_loginChoice2CreateWithRedirectURL.testdata => LoginIntegrationTest_loginChoice2CreateWithRedirectURL.testdata} (100%) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata => LoginIntegrationTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata} (100%) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_loginChoice3Create2Login.testdata => LoginIntegrationTest_loginChoice3Create2Login.testdata} (100%) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_startDisplayLoginDisabled.testdata => LoginIntegrationTest_startDisplayLoginDisabled.testdata} (100%) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_startDisplayWithOneProvider.testdata => LoginIntegrationTest_startDisplayWithOneProvider.testdata} (100%) rename src/test/resources/us/kbase/test/auth2/service/ui/{LoginTest_startDisplayWithTwoProviders.testdata => LoginIntegrationTest_startDisplayWithTwoProviders.testdata} (100%) diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice2CreateAndLoginDisabled.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice2CreateAndLoginDisabled.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice2CreateAndLoginDisabled.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice2CreateAndLoginDisabled.testdata diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice2CreateWithRedirectURL.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice2CreateWithRedirectURL.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice2CreateWithRedirectURL.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice2CreateWithRedirectURL.testdata diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice2LoginWithRedirectAndLoginDisabled.testdata diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice3Create2Login.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice3Create2Login.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_loginChoice3Create2Login.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_loginChoice3Create2Login.testdata diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_startDisplayLoginDisabled.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_startDisplayLoginDisabled.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_startDisplayLoginDisabled.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_startDisplayLoginDisabled.testdata diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_startDisplayWithOneProvider.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_startDisplayWithOneProvider.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_startDisplayWithOneProvider.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_startDisplayWithOneProvider.testdata diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_startDisplayWithTwoProviders.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_startDisplayWithTwoProviders.testdata similarity index 100% rename from src/test/resources/us/kbase/test/auth2/service/ui/LoginTest_startDisplayWithTwoProviders.testdata rename to src/test/resources/us/kbase/test/auth2/service/ui/LoginIntegrationTest_startDisplayWithTwoProviders.testdata From 457ebb4063d0b276a5fbbefb5a59c437f0dc2603 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Thu, 2 Oct 2025 16:11:11 -0700 Subject: [PATCH 06/24] Disallow repeating and trailing underscores in new user names Current users should be unaffected. < 1% of KBase users have these features in their usernames. Makes it easier to integrate with other systems that have stringent character requirements where we want to insert a username into a field, as now a double underscore can be used to separate the username from the rest of the field. --- .gitignore | 35 +++++ RELEASE_NOTES.md | 2 + .../us/kbase/auth2/lib/Authentication.java | 8 +- .../java/us/kbase/auth2/lib/NewUserName.java | 55 +++++++ .../java/us/kbase/auth2/lib/UserName.java | 3 +- .../us/kbase/auth2/service/api/TestMode.java | 3 +- .../java/us/kbase/auth2/service/ui/Admin.java | 3 +- .../java/us/kbase/auth2/service/ui/Login.java | 30 ++-- .../AuthenticationCreateLocalUserTest.java | 24 +-- .../lib/AuthenticationImportUserTest.java | 51 ++++--- .../auth2/lib/AuthenticationLoginTest.java | 83 ++++++----- .../lib/AuthenticationTestModeUserTest.java | 15 +- .../us/kbase/test/auth2/lib/UserNameTest.java | 47 +++++- .../test/auth2/service/ServiceTestUtils.java | 3 +- .../test/auth2/service/api/TestModeTest.java | 23 ++- .../test/auth2/service/ui/AdminTest.java | 49 ++++++ .../test/auth2/service/ui/LoginTest.java | 140 ++++++++++++++++++ 17 files changed, 466 insertions(+), 108 deletions(-) create mode 100644 .gitignore create mode 100644 src/main/java/us/kbase/auth2/lib/NewUserName.java create mode 100644 src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..862fdce9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +########################################## +# READ BEFORE ALTERING THIS FILE +# +# Only files specific to this repo or that will be generated as part of using this repo should +# be ignored here. Files that are specific to particular development environments or users +# should be ignored in the global gitignore to ignore for all repos, or in .git/info/exclude +# to ignore for just this repo. +# +# Examples of appropriate files for each location: +# This file: +# junit files +# gradle build files +# test configuration and output, including coverage data +# +# Global gitignore +# Eclipse .settings, .project, and .pyproject files +# Mac .DS_store files +# VSCode .vscode directory +# +# .git/info/exclude +# Temporary code / notes while exploring new repo features +# Personal variations of test.cfg files, e.g. my-test.cfg, etc. +# Personal data used for manual testing +# +########################################## + +/bin +/test.cfg +/junit*.properties + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ba235ddd..27e72f44 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,8 @@ ## 0.7.2 +* BACKWARDS INCOMPATIBILITY: Multiple underscores in series or trailing underscores are + no longer allowed in usernames. Existing usernames are unaffected. * Fixed a bug where usernames with underscores would not be matched in username searches if an underscore was an interior character of a search prefix. * Fixed a bug where a MongoDB error would be thrown if a user search prefix resulted in no search diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index 72fc0b7d..7d079ba5 100644 --- a/src/main/java/us/kbase/auth2/lib/Authentication.java +++ b/src/main/java/us/kbase/auth2/lib/Authentication.java @@ -436,7 +436,7 @@ public void createRoot(final Password pwd) */ public Password createLocalUser( final IncomingToken adminToken, - final UserName userName, + final NewUserName userName, final DisplayName displayName, final EmailAddress email) throws AuthStorageException, UserExistsException, UnauthorizedException, @@ -1960,7 +1960,7 @@ private TemporarySessionData getTemporarySessionData( public NewToken createUser( final IncomingToken token, final String identityID, - final UserName userName, + final NewUserName userName, final DisplayName displayName, final EmailAddress email, final Set policyIDs, @@ -2082,7 +2082,7 @@ public StoredToken testModeGetToken(final IncomingToken token) * @throws UnauthorizedException the user name is the root user name. * @throws TestModeException if test mode is not enabled. */ - public void testModeCreateUser(final UserName userName, final DisplayName displayName) + public void testModeCreateUser(final NewUserName userName, final DisplayName displayName) throws UserExistsException, AuthStorageException, UnauthorizedException, TestModeException { ensureTestMode(); @@ -3172,7 +3172,7 @@ public T getExternalConfig( * @throws AuthStorageException if an error occurred accessing the storage system. * @throws IdentityLinkedException if the identity is already linked to a user. */ - public void importUser(final UserName userName, final RemoteIdentity remoteIdentity) + public void importUser(final NewUserName userName, final RemoteIdentity remoteIdentity) throws UserExistsException, AuthStorageException, IdentityLinkedException { requireNonNull(userName, "userName"); requireNonNull(remoteIdentity, "remoteIdentity"); diff --git a/src/main/java/us/kbase/auth2/lib/NewUserName.java b/src/main/java/us/kbase/auth2/lib/NewUserName.java new file mode 100644 index 00000000..0c93b70f --- /dev/null +++ b/src/main/java/us/kbase/auth2/lib/NewUserName.java @@ -0,0 +1,55 @@ +package us.kbase.auth2.lib; + +import us.kbase.auth2.lib.exceptions.ErrorType; +import us.kbase.auth2.lib.exceptions.IllegalParameterException; +import us.kbase.auth2.lib.exceptions.MissingParameterException; + +/** A user name for a new user. + * + * Valid user names are strings of up to 100 characters consisting of lowercase ASCII letters, + * digits, and the underscore. The first character must be a letter. + * + * Unlike existing users, new users may also not have more than 1 underscore in a row and may not + * have trailing underscores. + * + * The only exception is the user name ***ROOT***, which represents the root user. + * + */ +public class NewUserName extends UserName { + + /** The username for the root user. */ + public final static NewUserName ROOT; + static { + try { + ROOT = new NewUserName(ROOT_NAME); + } catch (IllegalParameterException | MissingParameterException e) { + throw new RuntimeException("Programming error: " + e.getMessage(), e); + } + } + + /** Create a user name for a new, to be created, user. + * @param name the user name. + * @throws MissingParameterException if the name supplied is null or empty. + * @throws IllegalParameterException if the name supplied has illegal characters or is too + * long. + */ + public NewUserName(final String name) + throws MissingParameterException, IllegalParameterException { + super(name); + if (name.contains("__") || name.endsWith("_")) { + throw new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, + "New usernames cannot contain repeating underscores or " + + "trailing underscores" + ); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("NewUserName [getName()="); + builder.append(getName()); + builder.append("]"); + return builder.toString(); + } +} diff --git a/src/main/java/us/kbase/auth2/lib/UserName.java b/src/main/java/us/kbase/auth2/lib/UserName.java index 7b94c51d..09fc260f 100644 --- a/src/main/java/us/kbase/auth2/lib/UserName.java +++ b/src/main/java/us/kbase/auth2/lib/UserName.java @@ -20,13 +20,12 @@ * digits, and the underscore. The first character must be a letter. * * The only exception is the user name ***ROOT***, which represents the root user. - * @author gaprice@lbl.gov * */ public class UserName extends Name { // this must never be a valid username - private final static String ROOT_NAME = "***ROOT***"; + final static String ROOT_NAME = "***ROOT***"; /** The username for the root user. */ public final static UserName ROOT; diff --git a/src/main/java/us/kbase/auth2/service/api/TestMode.java b/src/main/java/us/kbase/auth2/service/api/TestMode.java index 1e054d6f..73c46d1b 100644 --- a/src/main/java/us/kbase/auth2/service/api/TestMode.java +++ b/src/main/java/us/kbase/auth2/service/api/TestMode.java @@ -35,6 +35,7 @@ import us.kbase.auth2.lib.Authentication; import us.kbase.auth2.lib.CustomRole; import us.kbase.auth2.lib.DisplayName; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.Role; import us.kbase.auth2.lib.UserName; import us.kbase.auth2.lib.exceptions.AuthException; @@ -99,7 +100,7 @@ public Map createTestUser(final CreateTestUser create) throw new MissingParameterException("JSON body missing"); } create.exceptOnAdditionalProperties(); - final UserName user = new UserName(create.userName); + final NewUserName user = new NewUserName(create.userName); auth.testModeCreateUser(user, new DisplayName(create.displayName)); try { return Me.toUserMap(auth.testModeGetUser(user)); diff --git a/src/main/java/us/kbase/auth2/service/ui/Admin.java b/src/main/java/us/kbase/auth2/service/ui/Admin.java index 12ba3b2f..1ca55b28 100644 --- a/src/main/java/us/kbase/auth2/service/ui/Admin.java +++ b/src/main/java/us/kbase/auth2/service/ui/Admin.java @@ -50,6 +50,7 @@ import us.kbase.auth2.lib.CustomRole; import us.kbase.auth2.lib.DisplayName; import us.kbase.auth2.lib.EmailAddress; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.Password; import us.kbase.auth2.lib.PolicyID; import us.kbase.auth2.lib.Role; @@ -255,7 +256,7 @@ public Map createLocalAccountComplete( NoTokenProvidedException { final Password pwd = auth.createLocalUser( getTokenFromCookie(headers, cfg.getTokenCookieName()), - new UserName(userName), new DisplayName(displayName), new EmailAddress(email)); + new NewUserName(userName), new DisplayName(displayName), new EmailAddress(email)); final Map ret = ImmutableMap.of( Fields.USER, userName, Fields.DISPLAY, displayName, diff --git a/src/main/java/us/kbase/auth2/service/ui/Login.java b/src/main/java/us/kbase/auth2/service/ui/Login.java index b16bdcca..95db9638 100644 --- a/src/main/java/us/kbase/auth2/service/ui/Login.java +++ b/src/main/java/us/kbase/auth2/service/ui/Login.java @@ -62,6 +62,7 @@ import us.kbase.auth2.lib.EmailAddress; import us.kbase.auth2.lib.LoginState; import us.kbase.auth2.lib.LoginToken; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.OAuth2StartData; import us.kbase.auth2.lib.PolicyID; import us.kbase.auth2.lib.TokenCreationContext; @@ -106,14 +107,25 @@ public class Login { private static final String TRUE = "true"; private static final String FALSE = "false"; - @Inject - private Authentication auth; - - @Inject - private AuthAPIStaticConfig cfg; + private final Authentication auth; + private final AuthAPIStaticConfig cfg; + private final UserAgentParser userAgentParser; + /** Construct the login endpoint handler. This is typically done by the Jersey framework. + * @param auth an instance of the core authentication class. + * @param cfg the static configuration for the authentication service. + * @param userAgentParser a user agent parser instance. + */ @Inject - private UserAgentParser userAgentParser; + public Login( + final Authentication auth, + final AuthAPIStaticConfig cfg, + final UserAgentParser userAgentParser + ) { + this.auth = auth; + this.cfg = cfg; + this.userAgentParser = userAgentParser; + } @GET @Template(name = "/loginstart") @@ -625,7 +637,7 @@ public Response createUser( final NewToken newtoken = auth.createUser( getLoginInProcessToken(token), CreateChoice.getString(identityID, Fields.ID), - new UserName(userName), + new NewUserName(userName), new DisplayName(displayName), new EmailAddress(email), CreateChoice.getPolicyIDs(policyIDs), @@ -634,7 +646,7 @@ public Response createUser( return createLoginResponse(redirectURI, newtoken, !FALSE.equals(session)); } - private static class CreateChoice extends PickChoice { + public static class CreateChoice extends PickChoice { public final String user; public final String displayName; @@ -683,7 +695,7 @@ public Response createUser( final NewToken newtoken = auth.createUser( getLoginInProcessToken(token), create.getIdentityID(), - new UserName(create.user), + new NewUserName(create.user), new DisplayName(create.displayName), new EmailAddress(create.email), create.getPolicyIDs(), diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationCreateLocalUserTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationCreateLocalUserTest.java index 52fde487..fa48042e 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationCreateLocalUserTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationCreateLocalUserTest.java @@ -29,6 +29,7 @@ import us.kbase.auth2.lib.Authentication; import us.kbase.auth2.lib.DisplayName; import us.kbase.auth2.lib.EmailAddress; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.Password; import us.kbase.auth2.lib.PasswordHashAndSalt; import us.kbase.auth2.lib.Role; @@ -134,7 +135,7 @@ private void create(final AuthUser adminUser) throws Exception { when(clock.instant()).thenReturn(create); final LocalUser expected = LocalUser.getLocalUserBuilder( - new UserName("foo"), uid, new DisplayName("bar"), create) + new NewUserName("foo"), uid, new DisplayName("bar"), create) .withEmailAddress(new EmailAddress("f@h.com")) .withForceReset(true).build(); @@ -145,7 +146,8 @@ private void create(final AuthUser adminUser) throws Exception { any(LocalUser.class), any(PasswordHashAndSalt.class)); final Password pwd = auth.createLocalUser( - token, new UserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com")); + token, new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com") + ); assertThat("incorrect pwd", pwd.getPassword(), is(pwdChar)); assertClear(matcher.savedSalt); assertClear(matcher.savedHash); @@ -198,7 +200,7 @@ public void createFailUserExists() throws Exception { doThrow(new UserExistsException("foo")).when(storage) .createLocalUser(any(LocalUser.class), any(PasswordHashAndSalt.class)); - failCreateLocalUser(auth, token, new UserName("foo"), new DisplayName("bar"), + failCreateLocalUser(auth, token, new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), new UserExistsException("foo")); } @@ -238,7 +240,7 @@ public void createFailIllegalRole() throws Exception { doThrow(new NoSuchRoleException("foo")).when(storage) .createLocalUser(any(LocalUser.class), any(PasswordHashAndSalt.class)); - failCreateLocalUser(auth, token, new UserName("foo"), new DisplayName("bar"), + failCreateLocalUser(auth, token, new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), new RuntimeException("didn't supply any roles")); } @@ -266,7 +268,7 @@ public void createFailRuntimeOnGetPwd() throws Exception { when(rand.getTemporaryPassword(10)).thenThrow(new RuntimeException("booga")); - failCreateLocalUser(auth, token, new UserName("foo"), new DisplayName("bar"), + failCreateLocalUser(auth, token, new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), new RuntimeException("booga")); } @@ -282,7 +284,7 @@ public IncomingToken getIncomingToken() { @Override public void execute(final Authentication auth) throws Exception { - auth.createLocalUser(token, new UserName("whee"), new DisplayName("bar"), + auth.createLocalUser(token, new NewUserName("whee"), new DisplayName("bar"), new EmailAddress("f@h.com")); } @@ -303,18 +305,18 @@ public void createUserFailNulls() throws Exception { final TestMocks testauth = initTestMocks(); final Authentication auth = testauth.auth; - failCreateLocalUser(auth, null, new UserName("foo"), new DisplayName("bar"), + failCreateLocalUser(auth, null, new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), new NullPointerException("token")); failCreateLocalUser(auth, new IncomingToken("whee"), null, new DisplayName("bar"), new EmailAddress("f@h.com"), new NullPointerException("userName")); - failCreateLocalUser(auth, new IncomingToken("whee"), new UserName("foo"), + failCreateLocalUser(auth, new IncomingToken("whee"), new NewUserName("foo"), null, new EmailAddress("f@h.com"), new NullPointerException("displayName")); - failCreateLocalUser(auth, new IncomingToken("whee"), new UserName("foo"), + failCreateLocalUser(auth, new IncomingToken("whee"), new NewUserName("foo"), new DisplayName("bar"), null, new NullPointerException("email")); } @@ -340,7 +342,7 @@ UserName.ROOT, UID, new DisplayName("foo"), NOW) when(storage.getUser(new UserName("admin"))).thenReturn(admin); - failCreateLocalUser(auth, token, UserName.ROOT, new DisplayName("bar"), + failCreateLocalUser(auth, token, NewUserName.ROOT, new DisplayName("bar"), new EmailAddress("f@h.com"), new UnauthorizedException(ErrorType.UNAUTHORIZED, "Cannot create ROOT user")); @@ -353,7 +355,7 @@ UserName.ROOT, UID, new DisplayName("foo"), NOW) public void failCreateLocalUser( final Authentication auth, final IncomingToken token, - final UserName userName, + final NewUserName userName, final DisplayName display, final EmailAddress email, final Exception e) { diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationImportUserTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationImportUserTest.java index 72b41cad..3cf83ded 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationImportUserTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationImportUserTest.java @@ -22,7 +22,7 @@ import us.kbase.auth2.lib.Authentication; import us.kbase.auth2.lib.DisplayName; import us.kbase.auth2.lib.EmailAddress; -import us.kbase.auth2.lib.UserName; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.exceptions.IdentityLinkedException; import us.kbase.auth2.lib.exceptions.NoSuchRoleException; import us.kbase.auth2.lib.exceptions.UserExistsException; @@ -66,11 +66,14 @@ public void importUser() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.importUser(new UserName("foo"), new RemoteIdentity(new RemoteIdentityID("prov", "id"), - new RemoteIdentityDetails("user", "full", "f@h.com"))); + auth.importUser( + new NewUserName("foo"), + new RemoteIdentity(new RemoteIdentityID("prov", "id"), + new RemoteIdentityDetails("user", "full", "f@h.com")) + ); verify(storage).createUser( - NewUser.getBuilder(new UserName("foo"), UID, new DisplayName("full"), + NewUser.getBuilder(new NewUserName("foo"), UID, new DisplayName("full"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id"), new RemoteIdentityDetails("user", "full", "f@h.com"))) @@ -97,10 +100,13 @@ private void importUserBadDisplayName(final String fullname) throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.importUser(new UserName("foo"), new RemoteIdentity(new RemoteIdentityID("prov", "id"), - new RemoteIdentityDetails("user", fullname, "f@h.com"))); + auth.importUser( + new NewUserName("foo"), + new RemoteIdentity(new RemoteIdentityID("prov", "id"), + new RemoteIdentityDetails("user", fullname, "f@h.com")) + ); - verify(storage).createUser(NewUser.getBuilder(new UserName("foo"), UID, + verify(storage).createUser(NewUser.getBuilder(new NewUserName("foo"), UID, new DisplayName("unknown"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id"), new RemoteIdentityDetails("user", fullname, "f@h.com"))) @@ -124,10 +130,13 @@ private void importUserBadEmail(final String email) throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.importUser(new UserName("foo"), new RemoteIdentity(new RemoteIdentityID("prov", "id"), - new RemoteIdentityDetails("user", "full", email))); + auth.importUser( + new NewUserName("foo"), + new RemoteIdentity(new RemoteIdentityID("prov", "id"), + new RemoteIdentityDetails("user", "full", email)) + ); - verify(storage).createUser(NewUser.getBuilder(new UserName("foo"), UID, + verify(storage).createUser(NewUser.getBuilder(new NewUserName("foo"), UID, new DisplayName("full"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id"), new RemoteIdentityDetails("user", "full", email))) @@ -143,7 +152,7 @@ public void importUserFailNulls() throws Exception { new RemoteIdentityID("prov", "id"), new RemoteIdentityDetails("user", "full", "email")), new NullPointerException("userName")); - failImportUser(auth, new UserName("foo"), null, + failImportUser(auth, new NewUserName("foo"), null, new NullPointerException("remoteIdentity")); } @@ -158,15 +167,15 @@ public void importUserFailUserExists() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.importUser(new UserName("foo"), REMOTE_ID); + auth.importUser(new NewUserName("foo"), REMOTE_ID); doThrow(new UserExistsException("foo")).when(storage).createUser( - NewUser.getBuilder(new UserName("foo"), UID, new DisplayName("full"), + NewUser.getBuilder(new NewUserName("foo"), UID, new DisplayName("full"), Instant.ofEpochMilli(10000), REMOTE_ID) .withEmailAddress(new EmailAddress("e@g.com")) .build()); - failImportUser(auth, new UserName("foo"), REMOTE_ID, new UserExistsException("foo")); + failImportUser(auth, new NewUserName("foo"), REMOTE_ID, new UserExistsException("foo")); } @Test @@ -180,15 +189,15 @@ public void importUserFailAlreadyLinked() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.importUser(new UserName("foo"), REMOTE_ID); + auth.importUser(new NewUserName("foo"), REMOTE_ID); doThrow(new IdentityLinkedException("linked")).when(storage).createUser( - NewUser.getBuilder(new UserName("foo2"), UID, new DisplayName("full"), + NewUser.getBuilder(new NewUserName("foo2"), UID, new DisplayName("full"), Instant.ofEpochMilli(10000), REMOTE_ID) .withEmailAddress(new EmailAddress("e@g.com")) .build()); - failImportUser(auth, new UserName("foo2"), REMOTE_ID, + failImportUser(auth, new NewUserName("foo2"), REMOTE_ID, new IdentityLinkedException("linked")); } @@ -203,21 +212,21 @@ public void importUserFailNoSuchRole() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.importUser(new UserName("foo2"), REMOTE_ID); + auth.importUser(new NewUserName("foo2"), REMOTE_ID); doThrow(new NoSuchRoleException("foo")).when(storage).createUser( - NewUser.getBuilder(new UserName("foo"), UID, new DisplayName("full"), + NewUser.getBuilder(new NewUserName("foo"), UID, new DisplayName("full"), Instant.ofEpochMilli(10000), REMOTE_ID) .withEmailAddress(new EmailAddress("e@g.com")) .build()); - failImportUser(auth, new UserName("foo"), REMOTE_ID, + failImportUser(auth, new NewUserName("foo"), REMOTE_ID, new RuntimeException("didn't supply any roles")); } private void failImportUser( final Authentication auth, - final UserName userName, + final NewUserName userName, final RemoteIdentity remoteIdentity, final Exception e) { try { diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java index 11f4bbc4..55e569a1 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java @@ -43,6 +43,7 @@ import us.kbase.auth2.lib.EmailAddress; import us.kbase.auth2.lib.LoginState; import us.kbase.auth2.lib.LoginToken; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.OAuth2StartData; import us.kbase.auth2.lib.PolicyID; import us.kbase.auth2.lib.Role; @@ -1408,12 +1409,12 @@ public void createUser() throws Exception { when(rand.getToken()).thenReturn("mfingtoken"); final NewToken nt = auth.createUser(token, "ef0518c79af70ed979907969c6d0a0f7", - new UserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), + new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), set(new PolicyID("pid1"), new PolicyID("pid2")), TokenCreationContext.getBuilder().withNullableDevice("d").build(), false); verify(storage).createUser(NewUser.getBuilder( - new UserName("foo"), UID, new DisplayName("bar"), Instant.ofEpochMilli(10000), + new NewUserName("foo"), UID, new DisplayName("bar"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withEmailAddress(new EmailAddress("f@h.com")) @@ -1423,17 +1424,17 @@ public void createUser() throws Exception { verify(storage, never()).link(any(), any()); verify(storage).storeToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) + TokenType.LOGIN, tokenID, new NewUserName("foo")) .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) .build(), "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); - verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(30000)); + verify(storage).setLastLogin(new NewUserName("foo"), Instant.ofEpochMilli(30000)); verify(storage).deleteTemporarySessionData(token.getHashedToken()); assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) + TokenType.LOGIN, tokenID, new NewUserName("foo")) .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) .build(), @@ -1478,12 +1479,12 @@ public void createUserAlternateTokenLifeTimeAndEmptyLinks() throws Exception { when(rand.getToken()).thenReturn("mfingtoken"); final NewToken nt = auth.createUser(token, "ef0518c79af70ed979907969c6d0a0f7", - new UserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), + new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), set(new PolicyID("pid1"), new PolicyID("pid2")), TokenCreationContext.getBuilder().withNullableDevice("d").build(), true); verify(storage).createUser(NewUser.getBuilder( - new UserName("foo"), UID, new DisplayName("bar"), Instant.ofEpochMilli(10000), + new NewUserName("foo"), UID, new DisplayName("bar"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withEmailAddress(new EmailAddress("f@h.com")) @@ -1493,17 +1494,17 @@ public void createUserAlternateTokenLifeTimeAndEmptyLinks() throws Exception { verify(storage, never()).link(any(), any()); verify(storage).storeToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) + TokenType.LOGIN, tokenID, new NewUserName("foo")) .withLifeTime(Instant.ofEpochMilli(20000), 100000) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) .build(), "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); - verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(30000)); + verify(storage).setLastLogin(new NewUserName("foo"), Instant.ofEpochMilli(30000)); verify(storage).deleteTemporarySessionData(token.getHashedToken()); assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) + TokenType.LOGIN, tokenID, new NewUserName("foo")) .withLifeTime(Instant.ofEpochMilli(20000), 100000) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) .build(), @@ -1575,16 +1576,16 @@ public void createUserAndLinkAll() throws Exception { //the identity was linked after identity filtering. Code should just ignore this. when(storage.link( - new UserName("foo"), new RemoteIdentity(new RemoteIdentityID("prov", "id3"), + new NewUserName("foo"), new RemoteIdentity(new RemoteIdentityID("prov", "id3"), new RemoteIdentityDetails("user3", "full3", "d@g.com")))) .thenThrow(new IdentityLinkedException("foo")); - when(storage.link(new UserName("foo"), new RemoteIdentity( + when(storage.link(new NewUserName("foo"), new RemoteIdentity( new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "e@g.com")))) .thenReturn(true); - when(storage.link(new UserName("foo"), new RemoteIdentity( + when(storage.link(new NewUserName("foo"), new RemoteIdentity( new RemoteIdentityID("prov", "id4"), new RemoteIdentityDetails("user4", "full4", "c@g.com")))) .thenReturn(true); @@ -1595,36 +1596,36 @@ public void createUserAndLinkAll() throws Exception { when(rand.getToken()).thenReturn("mfingtoken"); final NewToken nt = auth.createUser(token, "ef0518c79af70ed979907969c6d0a0f7", - new UserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), + new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), Collections.emptySet(), TokenCreationContext.getBuilder().withNullableDevice("d").build(), true); verify(storage).createUser(NewUser.getBuilder( - new UserName("foo"), UID2, new DisplayName("bar"), Instant.ofEpochMilli(10000), + new NewUserName("foo"), UID2, new DisplayName("bar"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withEmailAddress(new EmailAddress("f@h.com")).build()); - verify(storage, never()).link(new UserName("foo"), new RemoteIdentity( + verify(storage, never()).link(new NewUserName("foo"), new RemoteIdentity( new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))); - verify(storage, never()).link(new UserName("foo"), new RemoteIdentity( + verify(storage, never()).link(new NewUserName("foo"), new RemoteIdentity( new RemoteIdentityID("prov", "id5"), new RemoteIdentityDetails("user5", "full5", "b@g.com"))); verify(storage).storeToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) + TokenType.LOGIN, tokenID, new NewUserName("foo")) .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) .build(), "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); - verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(30000)); + verify(storage).setLastLogin(new NewUserName("foo"), Instant.ofEpochMilli(30000)); verify(storage).deleteTemporarySessionData(token.getHashedToken()); assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) + TokenType.LOGIN, tokenID, new NewUserName("foo")) .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) .build(), @@ -1659,7 +1660,7 @@ public void createUserFailNullsAndEmpties() throws Exception { .login(set(REMOTE))); final String id = "bar"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1687,7 +1688,7 @@ public void createUserFailRoot() throws Exception { final IncomingToken t = new IncomingToken("foo"); final String id = "bar"; - final UserName u = UserName.ROOT; + final NewUserName u = NewUserName.ROOT; final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1712,7 +1713,7 @@ public void createUserFailLoginNotAllowed() throws Exception { final IncomingToken t = new IncomingToken("foo"); final String id = "bar"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1741,7 +1742,7 @@ public void createUserFailBadToken() throws Exception { .thenThrow(new NoSuchTokenException("foo")); final String id = "bar"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1772,7 +1773,7 @@ public void createUserFailProviderError() throws Exception { .thenReturn(null); final String id = "bar"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1803,7 +1804,7 @@ public void createUserFailUnexpectedError() throws Exception { .thenReturn(null); final String id = "bar"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1835,7 +1836,7 @@ public void createUserFailBadTokenOp() throws Exception { .thenReturn(null); final String id = "bar"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1872,7 +1873,7 @@ public void createUserFailNoMatchingIdentities() throws Exception { .thenReturn(null); final String id = "bar"; //yep, that won't match - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1909,14 +1910,14 @@ public void createUserFailUserExists() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L)).thenReturn(null); doThrow(new UserExistsException("baz")).when(storage).createUser( - NewUser.getBuilder(new UserName("baz"), UID, new DisplayName("bat"), + NewUser.getBuilder(new NewUserName("baz"), UID, new DisplayName("bat"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withEmailAddress(new EmailAddress("e@g.com")).build()); final String id = "ef0518c79af70ed979907969c6d0a0f7"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1952,14 +1953,14 @@ public void createUserFailIdentityLinked() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L)).thenReturn(null); doThrow(new IdentityLinkedException("ef0518c79af70ed979907969c6d0a0f7")).when(storage) - .createUser(NewUser.getBuilder(new UserName("baz"), UID, new DisplayName("bat"), + .createUser(NewUser.getBuilder(new NewUserName("baz"), UID, new DisplayName("bat"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withEmailAddress(new EmailAddress("e@g.com")).build()); final String id = "ef0518c79af70ed979907969c6d0a0f7"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -1995,14 +1996,14 @@ public void createUserFailNoRole() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L)).thenReturn(null); doThrow(new NoSuchRoleException("foobar")).when(storage) - .createUser(NewUser.getBuilder(new UserName("baz"), UID, new DisplayName("bat"), + .createUser(NewUser.getBuilder(new NewUserName("baz"), UID, new DisplayName("bat"), Instant.ofEpochMilli(10000), new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com"))) .withEmailAddress(new EmailAddress("e@g.com")).build()); final String id = "ef0518c79af70ed979907969c6d0a0f7"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -2048,11 +2049,11 @@ public void createUserFailLinkAllNoSuchUser() throws Exception { .thenReturn(Optional.empty()); doThrow(new NoSuchUserException("baz")).when(storage).link( - new UserName("baz"), new RemoteIdentity(new RemoteIdentityID("prov", "id2"), + new NewUserName("baz"), new RemoteIdentity(new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "e@g.com"))); final String id = "ef0518c79af70ed979907969c6d0a0f7"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -2099,11 +2100,11 @@ public void createUserFailLinkFailed() throws Exception { .thenReturn(Optional.empty()); doThrow(new LinkFailedException("local")).when(storage).link( - new UserName("baz"), new RemoteIdentity(new RemoteIdentityID("prov", "id2"), + new NewUserName("baz"), new RemoteIdentity(new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "e@g.com"))); final String id = "ef0518c79af70ed979907969c6d0a0f7"; - final UserName u = new UserName("baz"); + final NewUserName u = new NewUserName("baz"); final DisplayName d = new DisplayName("bat"); final EmailAddress e = new EmailAddress("e@g.com"); final Set pids = Collections.emptySet(); @@ -2145,10 +2146,10 @@ public void createUserFailNoSuchUserOnSetLastLogin() throws Exception { when(rand.getToken()).thenReturn("mfingtoken"); doThrow(new NoSuchUserException("foo")).when(storage).setLastLogin( - new UserName("foo"), Instant.ofEpochMilli(30000)); + new NewUserName("foo"), Instant.ofEpochMilli(30000)); failCreateUser(auth, token, "ef0518c79af70ed979907969c6d0a0f7", - new UserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), + new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), Collections.emptySet(), CTX, false, new AuthStorageException( "Something is very broken. User should exist but doesn't: " + "50000 No such user: foo")); @@ -2158,7 +2159,7 @@ private void failCreateUser( final Authentication auth, final IncomingToken token, final String identityID, - final UserName userName, + final NewUserName userName, final DisplayName displayName, final EmailAddress email, final Set pids, diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeUserTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeUserTest.java index cf5bbfa6..94b32161 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeUserTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeUserTest.java @@ -24,6 +24,7 @@ import us.kbase.auth2.lib.Authentication; import us.kbase.auth2.lib.DisplayName; import us.kbase.auth2.lib.EmailAddress; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.UserName; import us.kbase.auth2.lib.ViewableUser; import us.kbase.auth2.lib.exceptions.ErrorType; @@ -82,9 +83,9 @@ public void createUser() throws Exception { when(testauth.randGenMock.randomUUID()).thenReturn(UID, (UUID) null); when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); - auth.testModeCreateUser(new UserName("foo"), new DisplayName("whee")); + auth.testModeCreateUser(new NewUserName("foo"), new DisplayName("whee")); - verify(storage).testModeCreateUser(new UserName("foo"), UID, new DisplayName("whee"), + verify(storage).testModeCreateUser(new NewUserName("foo"), UID, new DisplayName("whee"), Instant.ofEpochMilli(10000), Instant.ofEpochMilli(3610000)); assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, "Created test mode user foo", @@ -96,12 +97,12 @@ public void createUserFailInputs() throws Exception { final TestMocks testauth = initTestMocks(true); final Authentication auth = testauth.auth; - final UserName u = new UserName("foo"); + final NewUserName u = new NewUserName("foo"); final DisplayName d = new DisplayName("bar"); failCreateUser(auth, null, d, new NullPointerException("userName")); failCreateUser(auth, u, null, new NullPointerException("displayName")); - failCreateUser(auth, UserName.ROOT, d, + failCreateUser(auth, NewUserName.ROOT, d, new UnauthorizedException("Cannot create root user")); } @@ -112,7 +113,7 @@ public void createUserFailUserExists() throws Exception { final AuthStorage storage = testauth.storageMock; final Clock clock = testauth.clockMock; - final UserName u = new UserName("foo"); + final NewUserName u = new NewUserName("foo"); final DisplayName d = new DisplayName("bar"); when(testauth.randGenMock.randomUUID()).thenReturn(UID, (UUID) null); @@ -126,13 +127,13 @@ public void createUserFailUserExists() throws Exception { @Test public void createUserFailNoTestMode() throws Exception { - failCreateUser(initTestMocks(false).auth, new UserName("u"), new DisplayName("d"), + failCreateUser(initTestMocks(false).auth, new NewUserName("u"), new DisplayName("d"), new TestModeException(ErrorType.UNSUPPORTED_OP, "Test mode is not enabled")); } private void failCreateUser( final Authentication auth, - final UserName userName, + final NewUserName userName, final DisplayName displayName, final Exception expected) { try { diff --git a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java index d5085a63..73cbe5f7 100644 --- a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java @@ -10,6 +10,7 @@ import java.util.Optional; import nl.jqno.equalsverifier.EqualsVerifier; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.UserName; import us.kbase.auth2.lib.exceptions.ErrorType; import us.kbase.auth2.lib.exceptions.IllegalParameterException; @@ -24,22 +25,32 @@ public void root() throws Exception { assertThat("incorrect username", un.getName(), is("***ROOT***")); assertThat("incorrect is root", un.isRoot(), is(true)); assertThat("incorrect toString", un.toString(), is("UserName [getName()=***ROOT***]")); - assertThat("incorrect hashCode" , un.hashCode(), is(-280622915)); final UserName un2 = UserName.ROOT; assertThat("incorrect username", un2.getName(), is("***ROOT***")); assertThat("incorrect is root", un2.isRoot(), is(true)); assertThat("incorrect toString", un2.toString(), is("UserName [getName()=***ROOT***]")); - assertThat("incorrect hashCode" , un2.hashCode(), is(-280622915)); + + final NewUserName nun = NewUserName.ROOT; + assertThat("incorrect username", nun.getName(), is("***ROOT***")); + assertThat("incorrect is root", nun.isRoot(), is(true)); + assertThat("incorrect toString", nun.toString(), is("NewUserName [getName()=***ROOT***]")); } @Test public void construct() throws Exception { - final UserName un = new UserName("a8nba9"); - assertThat("incorrect username", un.getName(), is("a8nba9")); + final UserName un = new UserName("a8___nba9__"); + assertThat("incorrect username", un.getName(), is("a8___nba9__")); + assertThat("incorrect is root", un.isRoot(), is(false)); + assertThat("incorrect toString", un.toString(), is("UserName [getName()=a8___nba9__]")); + } + + @Test + public void constructNewUser() throws Exception { + final NewUserName un = new NewUserName("a8_nba9"); + assertThat("incorrect username", un.getName(), is("a8_nba9")); assertThat("incorrect is root", un.isRoot(), is(false)); - assertThat("incorrect toString", un.toString(), is("UserName [getName()=a8nba9]")); - assertThat("incorrect hashCode" , un.hashCode(), is(-1462848190)); + assertThat("incorrect toString", un.toString(), is("NewUserName [getName()=a8_nba9]")); } @Test @@ -58,6 +69,18 @@ public void constructFail() throws Exception { ErrorType.ILLEGAL_PARAMETER, "user name size greater than limit 100")); } + + @Test + public void constructFailNewUser() throws Exception { + failConstructNewUser(null, new MissingParameterException("user name")); + failConstructNewUser(" \t \n ", new MissingParameterException("user name")); + failConstructNewUser( + "xaa__baea", new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, + "New usernames cannot contain repeating underscores or trailing underscores") + ); + failConstructNewUser("xaabaea_", new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, + "New usernames cannot contain repeating underscores or trailing underscores")); + } private void failConstruct( final String name, @@ -70,6 +93,17 @@ private void failConstruct( } } + private void failConstructNewUser( + final String name, + final Exception exception) { + try { + new NewUserName(name); + fail("constructed bad name"); + } catch (Exception e) { + TestCommon.assertExceptionCorrect(e, exception); + } + } + @Test public void compareLessThan() throws Exception { assertThat("incorrect compare", @@ -101,6 +135,7 @@ public void compareFail() throws Exception { @Test public void equals() throws Exception { EqualsVerifier.forClass(UserName.class).usingGetClass().verify(); + EqualsVerifier.forClass(NewUserName.class).usingGetClass().verify(); } @Test diff --git a/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java b/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java index 80b24f60..4469941a 100644 --- a/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java +++ b/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java @@ -46,6 +46,7 @@ import us.kbase.auth2.lib.Authentication; import us.kbase.auth2.lib.DisplayName; import us.kbase.auth2.lib.EmailAddress; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.Password; import us.kbase.auth2.lib.Role; import us.kbase.auth2.lib.TokenCreationContext; @@ -83,7 +84,7 @@ public static IncomingToken getAdminToken(final MongoStorageTestManager manager) new Password(rootpwd.toCharArray()), TokenCreationContext.getBuilder().build()).getToken().get().getToken(); final Password admintemppwd = auth.createLocalUser( - new IncomingToken(roottoken), new UserName("admin"), new DisplayName("a"), + new IncomingToken(roottoken), new NewUserName("admin"), new DisplayName("a"), new EmailAddress("f@h.com")); auth.updateRoles(new IncomingToken(roottoken), new UserName("admin"), set(Role.CREATE_ADMIN), set()); diff --git a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java index 1657dd2a..a0e1e272 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java @@ -32,6 +32,7 @@ import us.kbase.auth2.lib.Authentication; import us.kbase.auth2.lib.CustomRole; import us.kbase.auth2.lib.DisplayName; +import us.kbase.auth2.lib.NewUserName; import us.kbase.auth2.lib.Role; import us.kbase.auth2.lib.UserName; import us.kbase.auth2.lib.ViewableUser; @@ -85,7 +86,7 @@ public void createUser() throws Exception { final Authentication auth = mock(Authentication.class); final TestMode tm = new TestMode(auth); - when(auth.testModeGetUser(new UserName("foobar"))).thenReturn(AuthUser.getBuilder( + when(auth.testModeGetUser(new NewUserName("foobar"))).thenReturn(AuthUser.getBuilder( new UserName("foobar"), UID, new DisplayName("foo bar"), inst(10000)) .build()); @@ -107,7 +108,7 @@ public void createUser() throws Exception { assertThat("incorrect user", au, is(expected)); - verify(auth).testModeCreateUser(new UserName("foobar"), new DisplayName("foo bar")); + verify(auth).testModeCreateUser(new NewUserName("foobar"), new DisplayName("foo bar")); } @Test @@ -132,7 +133,7 @@ public void createUserFailRootUser() throws Exception { doThrow(new UnauthorizedException("Cannot create root user")) .when(auth).testModeCreateUser( - new UserName("***ROOT***"), new DisplayName("root baby")); + new NewUserName("***ROOT***"), new DisplayName("root baby")); final TestMode tm = new TestMode(auth); final CreateTestUser ctu = new CreateTestUser("***ROOT***", "root baby"); @@ -146,7 +147,7 @@ public void createUserFailNoSuchUser() throws Exception { final TestMode tm = new TestMode(auth); - when(auth.testModeGetUser(new UserName("foobar"))) + when(auth.testModeGetUser(new NewUserName("foobar"))) .thenThrow(new NoSuchUserException("foobar")); final CreateTestUser ctu = new CreateTestUser("foobar", "foo bar"); @@ -154,6 +155,20 @@ public void createUserFailNoSuchUser() throws Exception { "Neat, user creation is totally busted: 50000 No such user: foobar")); } + @Test + public void createUserFailUnderscores() throws Exception { + final Authentication auth = mock(Authentication.class); + + final TestMode tm = new TestMode(auth); + final String err = "New usernames cannot contain repeating underscores or trailing " + + "underscores"; + + final CreateTestUser ctu = new CreateTestUser("foo__bar", "foo bar"); + failCreateUser(tm, ctu, new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, err)); + final CreateTestUser ctu2 = new CreateTestUser("foobar__", "foo bar"); + failCreateUser(tm, ctu2, new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, err)); + } + private void failCreateUser( final TestMode tm, final CreateTestUser create, diff --git a/src/test/java/us/kbase/test/auth2/service/ui/AdminTest.java b/src/test/java/us/kbase/test/auth2/service/ui/AdminTest.java index b8261ff0..f7d7ae2e 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/AdminTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/AdminTest.java @@ -41,6 +41,7 @@ import us.kbase.auth2.lib.config.AuthConfigSetWithUpdateTime; import us.kbase.auth2.lib.config.AuthConfigUpdate; import us.kbase.auth2.lib.config.ConfigAction.State; +import us.kbase.auth2.lib.exceptions.ErrorType; import us.kbase.auth2.lib.exceptions.ExternalConfigMappingException; import us.kbase.auth2.lib.exceptions.IllegalParameterException; import us.kbase.auth2.lib.exceptions.InvalidTokenException; @@ -72,7 +73,55 @@ public class AdminTest { * - but keep the integration tests as simple as possible. On the order of 1 happy path, * 1 unhappy path per method. Also need to test mustache templates */ + + // TODO TEST need to add unit tests for happy path createLocalUser (and a lot of other stuff) + @Test + public void createLocalUserFailUnderscores() throws Exception { + final Authentication auth = mock(Authentication.class); + final AuthAPIStaticConfig cfg = new AuthAPIStaticConfig("kbcookie", "fake"); + final HttpHeaders headers = mock(HttpHeaders.class); + + final Admin admin = new Admin(auth, cfg); + + when(headers.getCookies()).thenReturn( + ImmutableMap.of("kbcookie", new Cookie("kbcookie", "token"))); + + final String err = "New usernames cannot contain repeating underscores or trailing " + + "underscores"; + failCreateLocalUser( + admin, + headers, + "under__score", + "foo", + "foo@example.com", + new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, err) + ); + failCreateLocalUser( + admin, + headers, + "underscore_", + "foo", + "foo@example.com", + new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, err) + ); + } + private void failCreateLocalUser( + final Admin admin, + final HttpHeaders headers, + final String userName, + final String displayName, + final String email, + final Exception expected + ) throws Exception { + try { + admin.createLocalAccountComplete(headers, userName, displayName, email); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, expected); + } + } + @Test public void getConfigMinimal() throws Exception { final Authentication auth = mock(Authentication.class); diff --git a/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java b/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java new file mode 100644 index 00000000..73c7446b --- /dev/null +++ b/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java @@ -0,0 +1,140 @@ +package us.kbase.test.auth2.service.ui; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.junit.Assert.fail; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; + +import us.kbase.auth2.lib.Authentication; +import us.kbase.auth2.lib.TokenCreationContext; +import us.kbase.auth2.lib.config.ConfigItem; +import us.kbase.auth2.lib.exceptions.ErrorType; +import us.kbase.auth2.lib.exceptions.IllegalParameterException; +import us.kbase.auth2.service.AuthAPIStaticConfig; +import us.kbase.auth2.service.AuthExternalConfig; +import us.kbase.auth2.service.UserAgentParser; +import us.kbase.auth2.service.AuthExternalConfig.URLSet; +import us.kbase.auth2.service.ui.Login; +import us.kbase.testutils.TestCommon; + +public class LoginTest { + + // these are unit tests, not integration tests. + + // TODO TEST finish unit tests + + // TODO TEST need to add unit tests for happy path createUser (and a lot of other stuff) + @Test + public void createUserFailUnderscores() throws Exception { + final Authentication auth = mock(Authentication.class); + final AuthAPIStaticConfig cfg = new AuthAPIStaticConfig("kbcookie", "fake"); + final HttpServletRequest hsr = mock(HttpServletRequest.class); + final UserAgentParser uap = mock(UserAgentParser.class); // looong startup + + when(hsr.getHeader("user-agent")).thenReturn("foo"); + when(uap.getTokenContextFromUserAgent("foo")).thenReturn( + TokenCreationContext.getBuilder() + ); + when(auth.getExternalConfig(isA(AuthExternalConfig.AuthExternalConfigMapper.class))) + .thenReturn(AuthExternalConfig.getBuilder( + new URLSet<>( + ConfigItem.emptyState(), + ConfigItem.emptyState(), + ConfigItem.emptyState(), + ConfigItem.emptyState()), + ConfigItem.state(true), // ignore ip headers + ConfigItem.state(false)) + .build()); + when(hsr.getRemoteAddr()).thenReturn(""); // causes system to ignore IP + + final Login login = new Login(auth, cfg, uap); + + final String err = "New usernames cannot contain repeating underscores or trailing " + + "underscores"; + + createLocalUserFail( + login, + hsr, + "tok", + null, + null, + null, + "ident", + "foo__bar", + "display", + "foo@example.com", + new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, err) + ); + createLocalUserFail( + login, + hsr, + "tok", + null, + null, + null, + "ident", + "foobar_", + "display", + "foo@example.com", + new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, err) + ); + } + + private void createLocalUserFail( + final Login login, + final HttpServletRequest req, + final String token, + final String redirect, + final String session, + final String environment, + final String identityID, + final String userName, + final String displayName, + final String email, + final Exception expected + ) throws Exception { + try { + login.createUser( // form based + req, + token, + redirect, + session, + environment, + identityID, + userName, + displayName, + email, + null, + null, + null + ); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, expected); + } + try { + login.createUser( // json based + req, + token, + redirect, + environment, + new Login.CreateChoice( + identityID, + userName, + displayName, + email, + null, + null, + false + ) + ); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, expected); + } + } + +} From d655e8bb35e5277265d870eef71c1747f0518795 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 7 Oct 2025 12:37:31 -0700 Subject: [PATCH 07/24] Update username suggestion endpoint for underscore changes --- RELEASE_NOTES.md | 2 +- .../us/kbase/auth2/lib/Authentication.java | 2 +- .../java/us/kbase/auth2/lib/NewUserName.java | 28 ++++++++++++++++ .../java/us/kbase/auth2/lib/UserName.java | 32 ++++--------------- ...uthenticationGetAvailableUserNameTest.java | 14 ++++++++ .../us/kbase/test/auth2/lib/UserNameTest.java | 16 +++++++--- 6 files changed, 62 insertions(+), 32 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 27e72f44..1ecd3193 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,7 +2,7 @@ ## 0.7.2 -* BACKWARDS INCOMPATIBILITY: Multiple underscores in series or trailing underscores are +* BACKWARDS INCOMPATIBILITY: Repeated or trailing underscores are no longer allowed in usernames. Existing usernames are unaffected. * Fixed a bug where usernames with underscores would not be matched in username searches if an underscore was an interior character of a search prefix. diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index 7d079ba5..24948f70 100644 --- a/src/main/java/us/kbase/auth2/lib/Authentication.java +++ b/src/main/java/us/kbase/auth2/lib/Authentication.java @@ -1156,7 +1156,7 @@ public Map getUserDisplayNames( public Optional getAvailableUserName(final String suggestedUserName) throws AuthStorageException { requireNonNull(suggestedUserName, "suggestedUserName"); - final Optional target = UserName.sanitizeName(suggestedUserName); + final Optional target = NewUserName.sanitizeName(suggestedUserName); Optional availableUserName = Optional.empty(); if (target.isPresent()) { availableUserName = getAvailableUserName(target.get(), false, true); diff --git a/src/main/java/us/kbase/auth2/lib/NewUserName.java b/src/main/java/us/kbase/auth2/lib/NewUserName.java index 0c93b70f..8c220923 100644 --- a/src/main/java/us/kbase/auth2/lib/NewUserName.java +++ b/src/main/java/us/kbase/auth2/lib/NewUserName.java @@ -1,5 +1,10 @@ package us.kbase.auth2.lib; +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import java.util.regex.Pattern; + import us.kbase.auth2.lib.exceptions.ErrorType; import us.kbase.auth2.lib.exceptions.IllegalParameterException; import us.kbase.auth2.lib.exceptions.MissingParameterException; @@ -26,6 +31,10 @@ public class NewUserName extends UserName { throw new RuntimeException("Programming error: " + e.getMessage(), e); } } + + private final static Pattern REPEATING_UNDERSCORES = Pattern.compile("_+"); + // just need to match one since the repeating underscores will have removed any more + private final static Pattern TRAILING_UNDERSCORE = Pattern.compile("_$"); /** Create a user name for a new, to be created, user. * @param name the user name. @@ -44,6 +53,25 @@ public NewUserName(final String name) } } + /** Given a string, returns a new name based on that string that is a legal user name. If + * it is not possible construct a valid user name, empty() is returned. + * @param suggestedUserName the user name to mutate into a legal user name. + * @return the new user name, or empty() if mutation proved impossible. + */ + public static Optional sanitizeName(final String suggestedUserName) { + requireNonNull(suggestedUserName, "suggestedUserName"); + String cleaned = suggestedUserName.toLowerCase(); + cleaned = INVALID_CHARS.matcher(cleaned).replaceAll(""); + cleaned = FORCE_ALPHA_FIRST_CHAR.matcher(cleaned).replaceAll(""); + cleaned = REPEATING_UNDERSCORES.matcher(cleaned).replaceAll("_"); + cleaned = TRAILING_UNDERSCORE.matcher(cleaned).replaceAll(""); + try { + return cleaned.isEmpty() ? Optional.empty() : Optional.of(new UserName(cleaned)); + } catch (IllegalParameterException | MissingParameterException e) { + throw new RuntimeException("This should be impossible", e); + } + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/us/kbase/auth2/lib/UserName.java b/src/main/java/us/kbase/auth2/lib/UserName.java index 09fc260f..64422918 100644 --- a/src/main/java/us/kbase/auth2/lib/UserName.java +++ b/src/main/java/us/kbase/auth2/lib/UserName.java @@ -1,11 +1,9 @@ package us.kbase.auth2.lib; -import static java.util.Objects.requireNonNull; import static us.kbase.auth2.lib.Utils.checkStringNoCheckedException; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -25,7 +23,7 @@ public class UserName extends Name { // this must never be a valid username - final static String ROOT_NAME = "***ROOT***"; + protected final static String ROOT_NAME = "***ROOT***"; /** The username for the root user. */ public final static UserName ROOT; @@ -38,8 +36,8 @@ public class UserName extends Name { } } - private static final Pattern FORCE_ALPHA_FIRST_CHAR = Pattern.compile("^[^a-z]+"); - private final static Pattern INVALID_CHARS = Pattern.compile("[^a-z\\d_]+"); + protected static final Pattern FORCE_ALPHA_FIRST_CHAR = Pattern.compile("^[^a-z]+"); + protected final static Pattern INVALID_CHARS = Pattern.compile("[^a-z\\d_]+"); public final static int MAX_NAME_LENGTH = 100; /** Create a new user name. @@ -71,27 +69,11 @@ public boolean isRoot() { return getName().equals(ROOT_NAME); } - /** Given a string, returns a new name based on that string that is a legal user name. If - * it is not possible construct a valid user name, absent is returned. - * @param suggestedUserName the user name to mutate into a legal user name. - * @return the new user name, or absent if mutation proved impossible. - */ - public static Optional sanitizeName(final String suggestedUserName) { - requireNonNull(suggestedUserName, "suggestedUserName"); - final String s = cleanUserName(suggestedUserName); - try { - return s.isEmpty() ? Optional.empty() : Optional.of(new UserName(s)); - } catch (IllegalParameterException | MissingParameterException e) { - throw new RuntimeException("This should be impossible", e); - } - } - private static String cleanUserName(final String putativeName) { - return FORCE_ALPHA_FIRST_CHAR.matcher( - INVALID_CHARS.matcher( - putativeName.toLowerCase()) - .replaceAll("")) - .replaceAll(""); + String cleaned = putativeName.toLowerCase(); + cleaned = INVALID_CHARS.matcher(cleaned).replaceAll(""); + cleaned = FORCE_ALPHA_FIRST_CHAR.matcher(cleaned).replaceAll(""); + return cleaned; } /** Given a string, splits the string by whitespace, strips all illegal diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationGetAvailableUserNameTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationGetAvailableUserNameTest.java index 6cd572e0..4e4797c0 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationGetAvailableUserNameTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationGetAvailableUserNameTest.java @@ -59,6 +59,20 @@ public void failGetAvailableUserName() throws Exception { } } + @Test + public void getAvailableUserNameNoMatchUnderscores() throws Exception { + + final String suggestedUserName = " !# 999 45F___OO___*(^"; + final String searchName = "f_oo"; + final Map names = new HashMap<>(); + names.put(new UserName("f_oo1"), DISPNAME); + names.put(new UserName("f_oo2"), DISPNAME); + names.put(new UserName("f_oo26"), DISPNAME); + final Optional expected = Optional.of(new UserName("f_oo")); + + getAvailableUserName(suggestedUserName, searchName, expected, names); + } + @Test public void getAvailableUserNameNoMatchNum0() throws Exception { diff --git a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java index 73cbe5f7..7f3334cc 100644 --- a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java @@ -140,16 +140,22 @@ public void equals() throws Exception { @Test public void sanitize() throws Exception { - assertThat("incorrect santize", UserName.sanitizeName(" 999aF_A8 ea6t \t ѱ ** J(())"), - is(Optional.of(new UserName("af_a8ea6tj")))); - assertThat("incorrect santize", UserName.sanitizeName("999 8 6 \t ѱ ** (())"), - is(Optional.empty())); + assertThat( + "incorrect santize", + NewUserName.sanitizeName(" 999aF____A8 ea6t \t ѱ ** J___(())___"), + is(Optional.of(new UserName("af_a8ea6tj"))) + ); + assertThat( + "incorrect santize", + NewUserName.sanitizeName("999 8 6 \t ѱ ** (())"), + is(Optional.empty()) + ); } @Test public void failSanitize() { try { - UserName.sanitizeName(null); + NewUserName.sanitizeName(null); fail("expected exception"); } catch (Exception got) { TestCommon.assertExceptionCorrect(got, new NullPointerException("suggestedUserName")); From 8ee390b8c87b7c68cfff062d837c2e9f313c83a9 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 7 Oct 2025 12:58:29 -0700 Subject: [PATCH 08/24] santize -> sanitize --- src/test/java/us/kbase/test/auth2/lib/UserNameTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java index 7f3334cc..93875268 100644 --- a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java @@ -141,12 +141,12 @@ public void equals() throws Exception { @Test public void sanitize() throws Exception { assertThat( - "incorrect santize", + "incorrect sanitize", NewUserName.sanitizeName(" 999aF____A8 ea6t \t ѱ ** J___(())___"), is(Optional.of(new UserName("af_a8ea6tj"))) ); assertThat( - "incorrect santize", + "incorrect sanitize", NewUserName.sanitizeName("999 8 6 \t ѱ ** (())"), is(Optional.empty()) ); From b077abd9185035ce2080e0de3a089ba125263216 Mon Sep 17 00:00:00 2001 From: David Lyon <5115845+dauglyon@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:21:40 -0800 Subject: [PATCH 09/24] Fix flaky test by ensuring deterministic identity ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MeTest.getMeMaximalInput test was failing in CI but passing locally due to non-deterministic ordering of identities. The issue was that u.getIdentities() returns a Set, and Sets don't guarantee iteration order. This caused the identities array in the response to have different ordering across different environments and test runs. Fixed by making RemoteIdentity implement Comparable interface, sorting identities by provider name then provider username. This provides a clean, OOP solution where RemoteIdentity owns its own comparison logic. Changes: - Made RemoteIdentity implement Comparable - Added compareTo() method sorting by provider name → provider username - Added comprehensive unit tests for compareTo() functionality - Simplified Me endpoints to use .sorted() instead of inline lambda Fixes the failing test: - us.kbase.test.auth2.service.ui.MeTest.getMeMaximalInput --- .../auth2/lib/identity/RemoteIdentity.java | 13 +++- .../java/us/kbase/auth2/service/api/Me.java | 5 +- .../java/us/kbase/auth2/service/ui/Me.java | 5 +- .../lib/identity/RemoteIdentityTest.java | 66 +++++++++++++++++++ .../auth2/service/api/UserEndpointTest.java | 10 +-- .../kbase/test/auth2/service/ui/MeTest.java | 10 +-- .../ui/MeTest_getMeMaximalInput.testdata | 10 +-- 7 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/identity/RemoteIdentity.java b/src/main/java/us/kbase/auth2/lib/identity/RemoteIdentity.java index 9fe4418d..aa435303 100644 --- a/src/main/java/us/kbase/auth2/lib/identity/RemoteIdentity.java +++ b/src/main/java/us/kbase/auth2/lib/identity/RemoteIdentity.java @@ -6,7 +6,7 @@ * @author gaprice@lbl.gov * */ -public class RemoteIdentity { +public class RemoteIdentity implements Comparable { private final RemoteIdentityID remoteID; private final RemoteIdentityDetails details; @@ -78,6 +78,17 @@ public boolean equals(Object obj) { return true; } + @Override + public int compareTo(final RemoteIdentity other) { + requireNonNull(other, "other"); + // Sort by provider name first, then by provider username + int cmp = this.remoteID.getProviderName().compareTo(other.remoteID.getProviderName()); + if (cmp != 0) { + return cmp; + } + return this.details.getUsername().compareTo(other.details.getUsername()); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/us/kbase/auth2/service/api/Me.java b/src/main/java/us/kbase/auth2/service/api/Me.java index f2a85837..d4c92c54 100644 --- a/src/main/java/us/kbase/auth2/service/api/Me.java +++ b/src/main/java/us/kbase/auth2/service/api/Me.java @@ -77,7 +77,10 @@ static Map toUserMap(final AuthUser u) { ret.put(Fields.ROLES, roles); final List> idents = new LinkedList<>(); ret.put(Fields.IDENTITIES, idents); - for (final RemoteIdentity ri: u.getIdentities()) { + // Sort identities for deterministic ordering + for (final RemoteIdentity ri: u.getIdentities().stream() + .sorted() + .collect(Collectors.toList())) { final Map i = new HashMap<>(); i.put(Fields.PROVIDER, ri.getRemoteID().getProviderName()); i.put(Fields.PROV_USER, ri.getDetails().getUsername()); diff --git a/src/main/java/us/kbase/auth2/service/ui/Me.java b/src/main/java/us/kbase/auth2/service/ui/Me.java index 2da3da3e..54f4cbb9 100644 --- a/src/main/java/us/kbase/auth2/service/ui/Me.java +++ b/src/main/java/us/kbase/auth2/service/ui/Me.java @@ -120,7 +120,10 @@ private Map me(final IncomingToken token, final UriInfo uriInfo) ret.put(Fields.HAS_ROLES, !roles.isEmpty()); final List> idents = new LinkedList<>(); ret.put(Fields.IDENTITIES, idents); - for (final RemoteIdentity ri: u.getIdentities()) { + // Sort identities for deterministic ordering + for (final RemoteIdentity ri: u.getIdentities().stream() + .sorted() + .collect(Collectors.toList())) { final Map i = new HashMap<>(); i.put(Fields.PROVIDER, ri.getRemoteID().getProviderName()); i.put(Fields.PROV_USER, ri.getDetails().getUsername()); diff --git a/src/test/java/us/kbase/test/auth2/lib/identity/RemoteIdentityTest.java b/src/test/java/us/kbase/test/auth2/lib/identity/RemoteIdentityTest.java index a8b48f3d..91ba1601 100644 --- a/src/test/java/us/kbase/test/auth2/lib/identity/RemoteIdentityTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/identity/RemoteIdentityTest.java @@ -140,4 +140,70 @@ private void failCreateIdentity( assertThat("incorrect exception message", e.getMessage(), is(exception)); } } + + @Test + public void compareToSameProviderSameUsername() throws Exception { + final RemoteIdentity id1 = new RemoteIdentity( + new RemoteIdentityID("google", "123"), + new RemoteIdentityDetails("alice", "Alice", "alice@example.com")); + final RemoteIdentity id2 = new RemoteIdentity( + new RemoteIdentityID("google", "456"), + new RemoteIdentityDetails("alice", "Alice Smith", "alice@gmail.com")); + + assertThat("should be equal when provider and username match", id1.compareTo(id2), is(0)); + } + + @Test + public void compareToSameProviderDifferentUsername() throws Exception { + final RemoteIdentity id1 = new RemoteIdentity( + new RemoteIdentityID("google", "123"), + new RemoteIdentityDetails("alice", "Alice", "alice@example.com")); + final RemoteIdentity id2 = new RemoteIdentity( + new RemoteIdentityID("google", "456"), + new RemoteIdentityDetails("bob", "Bob", "bob@example.com")); + + assertThat("alice should come before bob", id1.compareTo(id2) < 0, is(true)); + assertThat("bob should come after alice", id2.compareTo(id1) > 0, is(true)); + } + + @Test + public void compareToDifferentProviderSameUsername() throws Exception { + final RemoteIdentity id1 = new RemoteIdentity( + new RemoteIdentityID("globus", "123"), + new RemoteIdentityDetails("alice", "Alice", "alice@example.com")); + final RemoteIdentity id2 = new RemoteIdentity( + new RemoteIdentityID("google", "456"), + new RemoteIdentityDetails("alice", "Alice", "alice@example.com")); + + assertThat("globus should come before google", id1.compareTo(id2) < 0, is(true)); + assertThat("google should come after globus", id2.compareTo(id1) > 0, is(true)); + } + + @Test + public void compareToDifferentProviderDifferentUsername() throws Exception { + final RemoteIdentity id1 = new RemoteIdentity( + new RemoteIdentityID("globus", "123"), + new RemoteIdentityDetails("zoe", "Zoe", "zoe@example.com")); + final RemoteIdentity id2 = new RemoteIdentity( + new RemoteIdentityID("google", "456"), + new RemoteIdentityDetails("alice", "Alice", "alice@example.com")); + + // Provider takes precedence: globus < google, regardless of username + assertThat("globus should come before google", id1.compareTo(id2) < 0, is(true)); + assertThat("google should come after globus", id2.compareTo(id1) > 0, is(true)); + } + + @Test + public void compareToNullFails() throws Exception { + final RemoteIdentity id = new RemoteIdentity( + new RemoteIdentityID("google", "123"), + new RemoteIdentityDetails("alice", "Alice", "alice@example.com")); + + try { + id.compareTo(null); + fail("expected NullPointerException"); + } catch (NullPointerException e) { + assertThat("incorrect exception msg", e.getMessage(), is("other")); + } + } } diff --git a/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java b/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java index 08e79047..0ff18ddd 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java @@ -244,14 +244,14 @@ public void getMeMaximalInput() throws Exception { ImmutableMap.of("id", "Admin", "desc", "Administrator"), ImmutableMap.of("id", "DevToken", "desc", "Create developer tokens"))) .with("idents", Arrays.asList( - ImmutableMap.of( - "provider", "prov2", - "provusername", "user2", - "id", "57980b7a3440a4342567e060c3e47666"), ImmutableMap.of( "provider", "prov", "provusername", "user1", - "id", "c20a5e632833ab26d99906fc9cb07d6b"))) + "id", "c20a5e632833ab26d99906fc9cb07d6b"), + ImmutableMap.of( + "provider", "prov2", + "provusername", "user2", + "id", "57980b7a3440a4342567e060c3e47666"))) .with("policyids", Arrays.asList( ImmutableMap.of("id", "wubba", "agreedon", 50000), ImmutableMap.of("id", "wugga", "agreedon", 40000))) diff --git a/src/test/java/us/kbase/test/auth2/service/ui/MeTest.java b/src/test/java/us/kbase/test/auth2/service/ui/MeTest.java index 4cf85a6d..3c00823d 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/MeTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/MeTest.java @@ -200,14 +200,14 @@ public void getMeMaximalInput() throws Exception { ImmutableMap.of("id", "Admin", "desc", "Administrator"), ImmutableMap.of("id", "DevToken", "desc", "Create developer tokens"))) .with("idents", Arrays.asList( - ImmutableMap.of( - "provider", "prov2", - "provusername", "user2", - "id", "57980b7a3440a4342567e060c3e47666"), ImmutableMap.of( "provider", "prov", "provusername", "user1", - "id", "c20a5e632833ab26d99906fc9cb07d6b"))) + "id", "c20a5e632833ab26d99906fc9cb07d6b"), + ImmutableMap.of( + "provider", "prov2", + "provusername", "user2", + "id", "57980b7a3440a4342567e060c3e47666"))) .with("policyids", Arrays.asList( ImmutableMap.of("id", "wubba", "agreedon", 50000), ImmutableMap.of("id", "wugga", "agreedon", 40000))) diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/MeTest_getMeMaximalInput.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/MeTest_getMeMaximalInput.testdata index c218326a..00acc4ab 100644 --- a/src/test/resources/us/kbase/test/auth2/service/ui/MeTest_getMeMaximalInput.testdata +++ b/src/test/resources/us/kbase/test/auth2/service/ui/MeTest_getMeMaximalInput.testdata @@ -28,16 +28,16 @@ Email is only visible to you, software acting on your behalf, and system adminis

Identities:

-Provider: prov2
-User id: user2
-
- -
Provider: prov
User id: user1
+Provider: prov2
+User id: user2
+
+ +
\ No newline at end of file From 5760026af127ecb929904df48a32816c6046aa3f Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Sun, 16 Nov 2025 14:47:26 -0800 Subject: [PATCH 10/24] Add MFAStatus class Self explanatory. Original version written by David Lyon here: https://github.com/kbase/auth2/pull/471 --- .../us/kbase/auth2/lib/token/MFAStatus.java | 59 +++++++++++++++++ .../test/auth2/lib/token/MFAStatusTest.java | 64 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/main/java/us/kbase/auth2/lib/token/MFAStatus.java create mode 100644 src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java diff --git a/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java new file mode 100644 index 00000000..d7f1dab4 --- /dev/null +++ b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java @@ -0,0 +1,59 @@ +package us.kbase.auth2.lib.token; + +import java.util.HashMap; +import java.util.Map; + +/** An enumeration representing the multi-factor authentication status of a user's login. */ +public enum MFAStatus { + + /* first arg is ID, second arg is description. ID CANNOT change + * since that field is stored in the DB. + */ + /** User authenticated with MFA during token creation. */ + USED ("Used", "MFA used"), + /** User explicitly chose not to use MFA when available. */ + NOT_USED ("NotUsed", "MFA not used"), + /** MFA status unknown or not applicable to authentication method. */ + UNKNOWN ("Unknown", "MFA status unknown"); + + private static final Map STATUS_MAP = new HashMap<>(); + static { + for (final MFAStatus status: MFAStatus.values()) { + STATUS_MAP.put(status.getID(), status); + } + } + + private final String id; + private final String description; + + private MFAStatus(final String id, final String description) { + this.id = id; + this.description = description; + } + + /** Get the ID of this MFA status. + * @return the ID. + */ + public String getID() { + return id; + } + + /** Get the description of this MFA status. + * @return the description. + */ + public String getDescription() { + return description; + } + + /** Get an MFA status from its ID. + * @param id the ID of the MFA status. + * @return the MFA status. + * @throws IllegalArgumentException if there is no MFA status matching the ID. + */ + public static MFAStatus fromID(final String id) { + if (!STATUS_MAP.containsKey(id)) { + throw new IllegalArgumentException("Invalid MFA status: " + id); + } + return STATUS_MAP.get(id); + } +} diff --git a/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java b/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java new file mode 100644 index 00000000..e1eb73c9 --- /dev/null +++ b/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java @@ -0,0 +1,64 @@ +package us.kbase.test.auth2.lib.token; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import us.kbase.auth2.lib.token.MFAStatus; + +public class MFAStatusTest { + + @Test + public void testValues() { + final MFAStatus[] expected = {MFAStatus.USED, MFAStatus.NOT_USED, MFAStatus.UNKNOWN}; + assertThat("incorrect values", MFAStatus.values(), is(expected)); + } + + @Test + public void testMFAStatusGetDescription() throws Exception { + assertThat("incorrect Used description", MFAStatus.USED.getDescription(), + is("MFA used")); + assertThat("incorrect NotUsed description", MFAStatus.NOT_USED.getDescription(), + is("MFA not used")); + assertThat("incorrect Unknown description", MFAStatus.UNKNOWN.getDescription(), + is("MFA status unknown")); + } + + @Test + public void testMFAStatusIDsAreStableForSerialization() throws Exception { + // These IDs are persisted to database via JSON serialization and must never change. + // Changing these values would break backwards compatibility with existing tokens + // and user data stored in MongoDB. + assertThat("USED ID must be stable", MFAStatus.USED.getID(), is("Used")); + assertThat("NOT_USED ID must be stable", MFAStatus.NOT_USED.getID(), is("NotUsed")); + assertThat("UNKNOWN ID must be stable", MFAStatus.UNKNOWN.getID(), is("Unknown")); + } + + @Test + public void testMFAStatusFromIDValidValues() throws Exception { + assertThat("incorrect fromID for Used", MFAStatus.fromID("Used"), is(MFAStatus.USED)); + assertThat("incorrect fromID for NotUsed", MFAStatus.fromID("NotUsed"), + is(MFAStatus.NOT_USED)); + assertThat("incorrect fromID for Unknown", MFAStatus.fromID("Unknown"), + is(MFAStatus.UNKNOWN)); + } + + @Test + public void testFromIDFail() throws Exception { + failFromId(null, "Invalid MFA status: null"); + failFromId(" \t ", "Invalid MFA status: \t "); + failFromId("INVALID", "Invalid MFA status: INVALID"); + failFromId("used", "Invalid MFA status: used"); + } + + private void failFromId(final String id, final String exception) { + try { + MFAStatus.fromID(id); + fail("expected exception"); + } catch (IllegalArgumentException e) { + assertThat("correct exception message", e.getMessage(), is(exception)); + } + } +} From e1c53a9c14eb6b359ed9e23b721e874c6af55380 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Sun, 16 Nov 2025 15:36:13 -0800 Subject: [PATCH 11/24] Add identity provider response class --- .../identity/IdentityProviderResponse.java | 101 ++++++++++ .../IdentityProviderResponseTest.java | 186 ++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 src/main/java/us/kbase/auth2/lib/identity/IdentityProviderResponse.java create mode 100644 src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderResponseTest.java diff --git a/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderResponse.java b/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderResponse.java new file mode 100644 index 00000000..ece8cf6b --- /dev/null +++ b/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderResponse.java @@ -0,0 +1,101 @@ +package us.kbase.auth2.lib.identity; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import us.kbase.auth2.lib.token.MFAStatus; + +/** Response data from a 3rd party identity provider. */ +public class IdentityProviderResponse { + + private final Set idents; + private final MFAStatus mfa; + + private IdentityProviderResponse(final Set idents, final MFAStatus mfa) { + // ensure class contents are immutable + this.idents = Collections.unmodifiableSet(new HashSet<>(idents)); + this.mfa = mfa; + } + + /** Get the identities from the remote identity response. + * @return the identities. + */ + public Set getIdentities() { + return idents; + } + + /** Get the multifactor authentication status from the user login. + * @return the MFA status. + */ + public MFAStatus getMFA() { + return mfa; + } + + @Override + public int hashCode() { + return Objects.hash(idents, mfa); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + IdentityProviderResponse other = (IdentityProviderResponse) obj; + return Objects.equals(idents, other.idents) && mfa == other.mfa; + } + + /** Create the response. + * @param identity the identity the provider returned post user login. + * @return the response. + */ + public static IdentityProviderResponse from(final RemoteIdentity identity) { + return from(Collections.singleton(requireNonNull(identity, "identity"))); + } + + + /** Create the response. + * @param identities the identities the provider returned post user login. + * @return the response. + */ + public static IdentityProviderResponse from(final Set identities) { + return from(identities, MFAStatus.UNKNOWN); + } + + /** Create the response. + * @param identity the identity the provider returned post user login. + * @param mfa the multifactor authentication status of the response. + * @return the response. + */ + public static IdentityProviderResponse from( + final RemoteIdentity identity, + final MFAStatus mfa + ) { + return from(Collections.singleton(requireNonNull(identity, "identity")), mfa); + } + + + /** Create the response. + * @param identities the identities the provider returned post user login. + * @param mfa the multifactor authentication status of the response. + * @return the response. + */ + public static IdentityProviderResponse from( + final Set identities, + final MFAStatus mfa + ) { + requireNonNull(identities, "identities"); + if (identities.size() < 1) { + throw new IllegalArgumentException("Must provide at least one identity"); + } + return new IdentityProviderResponse(identities, requireNonNull(mfa, "mfa")); + } + +} diff --git a/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderResponseTest.java b/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderResponseTest.java new file mode 100644 index 00000000..124b17df --- /dev/null +++ b/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderResponseTest.java @@ -0,0 +1,186 @@ +package us.kbase.test.auth2.lib.identity; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; +import us.kbase.auth2.lib.identity.RemoteIdentity; +import us.kbase.auth2.lib.identity.RemoteIdentityDetails; +import us.kbase.auth2.lib.identity.RemoteIdentityID; +import us.kbase.auth2.lib.token.MFAStatus; +import us.kbase.test.auth2.TestCommon; + +public class IdentityProviderResponseTest { + + private static final RemoteIdentity IDENT1 = new RemoteIdentity( + new RemoteIdentityID("p", "i1"), + new RemoteIdentityDetails("u1", "f", "e") + ); + private static final RemoteIdentity IDENT2 = new RemoteIdentity( + new RemoteIdentityID("p", "i2"), + new RemoteIdentityDetails("u2", "f", "e") + ); + private static final RemoteIdentity IDENT3 = new RemoteIdentity( + new RemoteIdentityID("p", "i3"), + new RemoteIdentityDetails("u3", "f", "e") + ); + + @Test + public void testEquals() throws Exception { + EqualsVerifier.forClass(IdentityProviderResponse.class).usingGetClass().verify(); + } + + @Test + public void testConstructWithIdentity() { + final IdentityProviderResponse response = IdentityProviderResponse.from(IDENT1); + + assertThat(response.getIdentities(), is(Collections.singleton(IDENT1))); + assertThat(response.getMFA(), is(MFAStatus.UNKNOWN)); + } + + @Test + public void testConstructWithIdentityAndMFA() { + final IdentityProviderResponse response = IdentityProviderResponse.from( + IDENT1, MFAStatus.USED); + + assertThat(response.getIdentities(), is(Collections.singleton(IDENT1))); + assertThat(response.getMFA(), is(MFAStatus.USED)); + } + + @Test + public void testConstructWithMultipleIdentities() { + final Set idents = new HashSet<>(Arrays.asList(IDENT1, IDENT2)); + final IdentityProviderResponse response = IdentityProviderResponse.from(idents); + + assertThat(response.getIdentities(), is(new HashSet<>(Arrays.asList(IDENT1, IDENT2)))); + assertThat(response.getMFA(), is(MFAStatus.UNKNOWN)); + } + + @Test + public void testConstructWithMultipleIdentitiesAndMFA() { + final Set idents = new HashSet<>(Arrays.asList(IDENT1, IDENT2, IDENT3)); + final IdentityProviderResponse response = IdentityProviderResponse.from( + idents, MFAStatus.NOT_USED); + + assertThat(response.getIdentities(), + is(new HashSet<>(Arrays.asList(IDENT1, IDENT2, IDENT3)))); + assertThat(response.getMFA(), is(MFAStatus.NOT_USED)); + } + + @Test + public void testAllMFAStatuses() { + for (final MFAStatus status : MFAStatus.values()) { + final IdentityProviderResponse r1 = IdentityProviderResponse.from(IDENT1, status); + assertThat(r1.getMFA(), is(status)); + final IdentityProviderResponse r2 = IdentityProviderResponse.from( + Collections.singleton(IDENT1), status); + assertThat(r2.getMFA(), is(status)); + } + } + + @Test + public void testImmutableIdentities() { + final Set idents = new HashSet<>(Arrays.asList(IDENT1, IDENT2)); + final IdentityProviderResponse response = IdentityProviderResponse.from(idents); + + // Verify returned set is unmodifiable + try { + response.getIdentities().add(IDENT3); + fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + + // Verify original set modification doesn't affect response + idents.add(IDENT3); + assertThat(response.getIdentities(), is(new HashSet<>(Arrays.asList(IDENT1, IDENT2)))); + } + + @Test + public void testFailConstructWithIdentity() { + try { + IdentityProviderResponse.from((RemoteIdentity) null); + fail("Expected NullPointerException"); + } catch (Exception e) { + TestCommon.assertExceptionCorrect(e, new NullPointerException("identity")); + } + } + + @Test + public void testFailConstructWithIdentitySet() { + failConstructWithIdentitySet(null, new NullPointerException("identities")); + failConstructWithIdentitySet( + Collections.emptySet(), + new IllegalArgumentException("Must provide at least one identity") + ); + } + + private void failConstructWithIdentitySet( + final Set ris, + final Exception expected + ) { + try { + IdentityProviderResponse.from(ris); + fail("Expected exception"); + } catch (Exception e) { + TestCommon.assertExceptionCorrect(e, expected); + } + } + + @Test + public void testFailConstructWithIdentityAndMFA() { + failConstructWithIdentityAndMFA( + null, MFAStatus.UNKNOWN, new NullPointerException("identity") + ); + failConstructWithIdentityAndMFA(IDENT1, null, new NullPointerException("mfa")); + } + + private void failConstructWithIdentityAndMFA( + final RemoteIdentity ri, + final MFAStatus mfa, + final Exception expected + ) { + try { + IdentityProviderResponse.from(ri, mfa); + fail("Expected exception"); + } catch (Exception e) { + TestCommon.assertExceptionCorrect(e, expected); + } + } + + @Test + public void testFailConstructWithIdentitySetAndMFA() { + failConstructWithIdentitySetAndMFA( + null, MFAStatus.UNKNOWN, new NullPointerException("identities") + ); + failConstructWithIdentitySetAndMFA(Collections.emptySet(), MFAStatus.UNKNOWN, + new IllegalArgumentException("Must provide at least one identity") + ); + failConstructWithIdentitySetAndMFA(Collections.singleton(IDENT1), null, + new NullPointerException("mfa") + ); + } + + private void failConstructWithIdentitySetAndMFA( + final Set ris, + final MFAStatus mfa, + final Exception expected + ) { + try { + IdentityProviderResponse.from(ris, mfa); + fail("Expected exception"); + } catch (Exception e) { + TestCommon.assertExceptionCorrect(e, expected); + } + } + +} From 509b8cd022d8be12bfdb8e46946e177204a5d211 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Sun, 16 Nov 2025 16:21:42 -0800 Subject: [PATCH 12/24] Use IdentityProviderResponse class in auth For now no MFA --- .../us/kbase/auth2/lib/Authentication.java | 15 +++--- .../auth2/lib/identity/IdentityProvider.java | 2 +- .../GlobusIdentityProviderFactory.java | 5 +- .../GoogleIdentityProviderFactory.java | 7 ++- .../OrcIDIdentityProviderFactory.java | 7 ++- .../lib/AuthenticationConstructorTest.java | 9 +++- .../auth2/lib/AuthenticationLinkTest.java | 22 +++++---- .../auth2/lib/AuthenticationLoginTest.java | 43 +++++++++-------- .../providers/GlobusIdentityProviderTest.java | 25 +++++----- .../providers/GoogleIdentityProviderTest.java | 27 +++++------ .../providers/OrcIDIdentityProviderTest.java | 47 +++++++++---------- .../kbase/test/auth2/service/ui/LinkTest.java | 9 ++-- .../service/ui/LoginIntegrationTest.java | 3 +- 13 files changed, 116 insertions(+), 105 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index 24948f70..52d613f9 100644 --- a/src/main/java/us/kbase/auth2/lib/Authentication.java +++ b/src/main/java/us/kbase/auth2/lib/Authentication.java @@ -76,6 +76,7 @@ import us.kbase.auth2.lib.exceptions.UnauthorizedException; import us.kbase.auth2.lib.exceptions.UserExistsException; import us.kbase.auth2.lib.identity.IdentityProvider; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityID; import us.kbase.auth2.lib.storage.AuthStorage; @@ -905,8 +906,10 @@ public NewToken createToken( final NewToken nt = new NewToken(StoredToken.getBuilder(tokenType, id, au.getUserName()) .withLifeTime(clock.instant(), life) .withContext(tokenCtx) - .withTokenName(tokenName).build(), - randGen.getToken()); + .withTokenName(tokenName) + .build(), + randGen.getToken() + ); storage.storeToken(nt.getStoredToken(), nt.getTokenHash()); logInfo("User {} created {} token {}", au.getUserName().getName(), tokenType, id); return nt; @@ -1787,9 +1790,9 @@ public LoginToken login( // enough args here to start considering a builder Optional.empty(), Operation.LOGINSTART, token); checkState(tids, oauth2State); storage.deleteTemporarySessionData(token.getHashedToken()); - final Set ris = idp.getIdentities( + final IdentityProviderResponse ipr = idp.getIdentities( authcode, tids.getPKCECodeVerifier().get(), false, environment); - final LoginState lstate = getLoginState(ris, Instant.MIN); + final LoginState lstate = getLoginState(ipr.getIdentities(), Instant.MIN); final ProviderConfig pc = cfg.getAppConfig().getProviderConfig(idp.getProviderName()); final LoginToken loginToken; if (lstate.getUsers().size() == 1 && @@ -2527,8 +2530,8 @@ public LinkToken link( // enough args here to start considering a builder throw new LinkFailedException("Cannot link identities to local account " + u.getUserName().getName()); } - final Set ids = idp.getIdentities( - authcode, tids.getPKCECodeVerifier().get(), true, environment); + final Set ids = idp.getIdentities( // don't care about MFA here (yet) + authcode, tids.getPKCECodeVerifier().get(), true, environment).getIdentities(); final Set filtered = new HashSet<>(ids); filterLinkCandidates(filtered); /* Don't throw an error if ids are empty since an auth UI is not controlling the call in diff --git a/src/main/java/us/kbase/auth2/lib/identity/IdentityProvider.java b/src/main/java/us/kbase/auth2/lib/identity/IdentityProvider.java index 59d34eec..ae96defb 100644 --- a/src/main/java/us/kbase/auth2/lib/identity/IdentityProvider.java +++ b/src/main/java/us/kbase/auth2/lib/identity/IdentityProvider.java @@ -49,7 +49,7 @@ URI getLoginURI(String state, String pkceCodeChallenge, boolean link, String env * @throws IdentityRetrievalException if getting the idenities failed. * @throws NoSuchEnvironmentException if there is no such environment configured. */ - Set getIdentities( + IdentityProviderResponse getIdentities( String authcode, String pkceCodeVerifier, boolean link, String environment) throws IdentityRetrievalException, NoSuchEnvironmentException; diff --git a/src/main/java/us/kbase/auth2/providers/GlobusIdentityProviderFactory.java b/src/main/java/us/kbase/auth2/providers/GlobusIdentityProviderFactory.java index a1d6d224..d1e0a679 100644 --- a/src/main/java/us/kbase/auth2/providers/GlobusIdentityProviderFactory.java +++ b/src/main/java/us/kbase/auth2/providers/GlobusIdentityProviderFactory.java @@ -35,6 +35,7 @@ import us.kbase.auth2.lib.identity.IdentityProvider; import us.kbase.auth2.lib.identity.IdentityProviderConfig; import us.kbase.auth2.lib.identity.IdentityProviderFactory; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -152,7 +153,7 @@ public Idents(RemoteIdentity primary, Set secondaryIDs) { } @Override - public Set getIdentities( + public IdentityProviderResponse getIdentities( final String authcode, final String pkceCodeVerifier, final boolean link, @@ -169,7 +170,7 @@ public Set getIdentities( final Set secondaries = getSecondaryIdentities( accessToken, idents.secondaryIDs); secondaries.add(idents.primary); - return secondaries; + return IdentityProviderResponse.from(secondaries); } private Set getSecondaryIdentities( diff --git a/src/main/java/us/kbase/auth2/providers/GoogleIdentityProviderFactory.java b/src/main/java/us/kbase/auth2/providers/GoogleIdentityProviderFactory.java index 17954b56..1e45b16e 100644 --- a/src/main/java/us/kbase/auth2/providers/GoogleIdentityProviderFactory.java +++ b/src/main/java/us/kbase/auth2/providers/GoogleIdentityProviderFactory.java @@ -7,9 +7,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.Arrays; import java.util.Base64; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -32,6 +30,7 @@ import us.kbase.auth2.lib.identity.IdentityProvider; import us.kbase.auth2.lib.identity.IdentityProviderConfig; import us.kbase.auth2.lib.identity.IdentityProviderFactory; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -141,7 +140,7 @@ private URI toURI(final URL loginURL) { } @Override - public Set getIdentities( + public IdentityProviderResponse getIdentities( final String authcode, final String pkceCodeVerifier, final boolean link, @@ -150,7 +149,7 @@ public Set getIdentities( checkStringNoCheckedException(authcode, "authcode"); checkStringNoCheckedException(pkceCodeVerifier, "pkceCodeVerifier"); final RemoteIdentity ri = getIdentity(authcode, pkceCodeVerifier, link, environment); - return new HashSet<>(Arrays.asList(ri)); + return IdentityProviderResponse.from(ri); } private RemoteIdentity getIdentity( diff --git a/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java b/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java index e93a357b..da0645a6 100644 --- a/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java +++ b/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java @@ -6,8 +6,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -31,6 +29,7 @@ import us.kbase.auth2.lib.identity.IdentityProvider; import us.kbase.auth2.lib.identity.IdentityProviderConfig; import us.kbase.auth2.lib.identity.IdentityProviderFactory; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -134,7 +133,7 @@ private URI toURI(final URL loginURL) { } @Override - public Set getIdentities( + public IdentityProviderResponse getIdentities( final String authcode, final String pkceCodeVerifier, final boolean link, @@ -148,7 +147,7 @@ public Set getIdentities( final OrcIDAccessTokenResponse accessToken = getAccessToken( authcode, link, environment); final RemoteIdentity ri = getIdentity(accessToken); - return new HashSet<>(Arrays.asList(ri)); + return IdentityProviderResponse.from(ri); } private RemoteIdentity getIdentity(final OrcIDAccessTokenResponse accessToken) diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationConstructorTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationConstructorTest.java index 279aea99..5a05e057 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationConstructorTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationConstructorTest.java @@ -39,7 +39,10 @@ import us.kbase.auth2.lib.exceptions.IdentityRetrievalException; import us.kbase.auth2.lib.identity.IdentityProvider; import us.kbase.auth2.lib.identity.IdentityProviderConfig; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; +import us.kbase.auth2.lib.identity.RemoteIdentityDetails; +import us.kbase.auth2.lib.identity.RemoteIdentityID; import us.kbase.auth2.lib.storage.AuthStorage; import us.kbase.auth2.lib.storage.exceptions.AuthStorageException; import us.kbase.auth2.lib.storage.exceptions.StorageInitException; @@ -158,13 +161,15 @@ public URI getLoginURI( } @Override - public Set getIdentities( + public IdentityProviderResponse getIdentities( final String authcode, final String pkceVerifier, final boolean link, final String environment) throws IdentityRetrievalException { - return Collections.emptySet(); + return IdentityProviderResponse.from(new RemoteIdentity( + new RemoteIdentityID("p", "i"), new RemoteIdentityDetails("u", "f", "e")) + ); } @Override diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java index 9802ff3a..2053bc4f 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java @@ -65,6 +65,7 @@ import us.kbase.auth2.lib.exceptions.UnLinkFailedException; import us.kbase.auth2.lib.exceptions.UnauthorizedException; import us.kbase.auth2.lib.identity.IdentityProvider; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -323,7 +324,7 @@ public void linkWithTokenImmediately() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkceverifiedforyourcomfort", true, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("Prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")))) .thenReturn(null); @@ -383,7 +384,7 @@ public void linkWithTokenImmediatelyUpdateRemoteIdentity() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkcecuresacne", true, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("Prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")))) .thenReturn(null); @@ -441,7 +442,7 @@ public void linkWithTokenRaceConditionAndIDLinked() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkceambrosiaofthegods", true, null)) - .thenReturn(set( + .thenReturn(IdentityProviderResponse.from(set( new RemoteIdentity( new RemoteIdentityID("Prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com") @@ -449,7 +450,7 @@ public void linkWithTokenRaceConditionAndIDLinked() throws Exception { new RemoteIdentity( new RemoteIdentityID("Prov", "id3"), new RemoteIdentityDetails("user3", "full3", "f3@g.com")) - )) + ))) .thenReturn(null); final RemoteIdentity storageRemoteID2 = new RemoteIdentity( @@ -524,7 +525,7 @@ public void linkWithTokenForceChoiceWithEnvironment() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkcehasgreatretirementbenefits", true, "myenv")) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")))) .thenReturn(null); @@ -591,7 +592,7 @@ public void linkWithTokenNoAvailableIDsDueToFilter() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkceisnotsnakeoilatall", true, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")))) .thenReturn(null); @@ -663,13 +664,14 @@ public void linkWithTokenWith2IDs1Filtered() throws Exception { when(idp.getIdentities( "authcode", "pkcemakesanexcellentbodywashandenginegrease", true, null)) - .thenReturn(set( + .thenReturn(IdentityProviderResponse.from(set( new RemoteIdentity(new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")), new RemoteIdentity(new RemoteIdentityID("prov", "id3"), new RemoteIdentityDetails("user3", "full3", "f3@g.com")), new RemoteIdentity(new RemoteIdentityID("prov", "id4"), - new RemoteIdentityDetails("user4", "full4", "f4@g.com")))) + new RemoteIdentityDetails("user4", "full4", "f4@g.com"))) + )) .thenReturn(null); final RemoteIdentity storageRemoteID2 = new RemoteIdentity( @@ -1157,7 +1159,7 @@ public void linkWithTokenFailNoSuchUserOnLink() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkceimkindofgettingboredwiththis", true, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("Prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")))) .thenReturn(null); @@ -1207,7 +1209,7 @@ public void linkWithTokenFailLinkFailedOnLink() throws Exception { .withIdentity(REMOTE).build()).thenReturn(null); when(idp.getIdentities("authcode", "pkceohwhocares", true, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("Prov", "id2"), new RemoteIdentityDetails("user2", "full2", "f2@g.com")))) .thenReturn(null); diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java index 55e569a1..baff074b 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java @@ -74,6 +74,7 @@ import us.kbase.auth2.lib.exceptions.UnauthorizedException; import us.kbase.auth2.lib.exceptions.UserExistsException; import us.kbase.auth2.lib.identity.IdentityProvider; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -270,7 +271,7 @@ private void loginContinueImmediately( .login("suporstate", "pkceughherewegoagain")); when(idp.getIdentities("foobar", "pkceughherewegoagain", false, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com")))) .thenReturn(null); @@ -372,7 +373,7 @@ private void loginContinueStoreSingleLinkedIdentity( .login("suporstate2", "pkceisathingiguess")); when(idp.getIdentities("foobar", "pkceisathingiguess", false, null)) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com")))) .thenReturn(null); @@ -447,7 +448,7 @@ public void loginContinueStoreUnlinkedIdentityWithEnvironment() throws Exception .login("veryneatstate", "pkcewhoopdefndoo")); when(idp.getIdentities("foobar", "pkcewhoopdefndoo", false, "env2")) - .thenReturn(set(new RemoteIdentity( + .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com")))) .thenReturn(null); @@ -513,13 +514,13 @@ public void loginContinueStoreLinkedAndUnlinkedIdentity() throws Exception { .login("somestate", "pkceverifierlalalalala")); when(idp.getIdentities("foobar", "pkceverifierlalalalala", false, null)) - .thenReturn(set( + .thenReturn(IdentityProviderResponse.from(set( new RemoteIdentity( new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com")), new RemoteIdentity(new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "e@g.com")) - )) + ))) .thenReturn(null); final RemoteIdentity storageRemoteID1 = new RemoteIdentity( @@ -593,13 +594,15 @@ public void loginContinueStoreMultipleLinkedIdentities() throws Exception { TemporarySessionData.create(UUID.randomUUID(), now(), now().plusSeconds(10)) .login("suporstateystate", "pkceohgodpleasestop")); - when(idp.getIdentities("foobar", "pkceohgodpleasestop", false, null)).thenReturn(set( - new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id3"), - new RemoteIdentityDetails("user3", "full3", "d@g.com")))) + when(idp.getIdentities("foobar", "pkceohgodpleasestop", false, null)).thenReturn( + IdentityProviderResponse.from(set( + new RemoteIdentity(new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com")), + new RemoteIdentity(new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com")), + new RemoteIdentity(new RemoteIdentityID("prov", "id3"), + new RemoteIdentityDetails("user3", "full3", "d@g.com")) + ))) .thenReturn(null); final RemoteIdentity storageRemoteID1 = new RemoteIdentity( @@ -687,13 +690,15 @@ public void loginContinueStoreMultipleUnLinkedIdentities() throws Exception { TemporarySessionData.create(UUID.randomUUID(), now(), now().plusSeconds(10)) .login("state.thatisall", "pkceithinkimightgomad")); - when(idp.getIdentities("foobar", "pkceithinkimightgomad", false, null)).thenReturn(set( - new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id3"), - new RemoteIdentityDetails("user3", "full3", "d@g.com")))) + when(idp.getIdentities("foobar", "pkceithinkimightgomad", false, null)).thenReturn( + IdentityProviderResponse.from(set( + new RemoteIdentity(new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com")), + new RemoteIdentity(new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com")), + new RemoteIdentity(new RemoteIdentityID("prov", "id3"), + new RemoteIdentityDetails("user3", "full3", "d@g.com")) + ))) .thenReturn(null); final RemoteIdentity storageRemoteID1 = new RemoteIdentity( diff --git a/src/test/java/us/kbase/test/auth2/providers/GlobusIdentityProviderTest.java b/src/test/java/us/kbase/test/auth2/providers/GlobusIdentityProviderTest.java index 3c5ac05b..57d951d9 100644 --- a/src/test/java/us/kbase/test/auth2/providers/GlobusIdentityProviderTest.java +++ b/src/test/java/us/kbase/test/auth2/providers/GlobusIdentityProviderTest.java @@ -41,6 +41,7 @@ import us.kbase.auth2.lib.identity.IdentityProviderConfig; import us.kbase.auth2.lib.identity.IdentityProviderConfig.Builder; import us.kbase.auth2.lib.identity.IdentityProviderConfig.IdentityProviderConfigurationException; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.providers.GlobusIdentityProviderFactory; import us.kbase.auth2.providers.GlobusIdentityProviderFactory.GlobusIdentityProvider; import us.kbase.auth2.lib.identity.RemoteIdentity; @@ -701,7 +702,7 @@ public void getIdentityWithSecondariesAndLoginURLAndEnvironment() throws Excepti MAPPER.writeValueAsString(ImmutableMap.of("identities", idents))); - final Set rids = idp.getIdentities(authCode, "pixypixy", false, "myenv"); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pixypixy", false, "myenv"); final Set expected = new HashSet<>(); expected.add(new RemoteIdentity(new RemoteIdentityID(GLOBUS, "anID"), new RemoteIdentityDetails("aUsername", "fullname", "anEmail"))); @@ -709,7 +710,7 @@ public void getIdentityWithSecondariesAndLoginURLAndEnvironment() throws Excepti new RemoteIdentityDetails("user1", "name1", null))); expected.add(new RemoteIdentity(new RemoteIdentityID(GLOBUS, "id2"), new RemoteIdentityDetails("user2", null, "email2"))); - assertThat("incorrect ident set", rids, is(expected)); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from(expected))); } @Test @@ -741,11 +742,11 @@ public void getIdentityWithSecondariesDisabledAndLoginURL() throws Exception { MAPPER.writeValueAsString(ImmutableMap.of("identities", idents))); - final Set rids = idp.getIdentities(authCode, "pkce", false, null); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(GLOBUS, "anID"), - new RemoteIdentityDetails("aUsername", "fullname", "anEmail"))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pkce", false, null); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(GLOBUS, "anID"), + new RemoteIdentityDetails("aUsername", "fullname", "anEmail")) + ))); } private void setupCallSecondaryID( @@ -805,12 +806,12 @@ private void getIdentityWithoutSecondariesAndLinkURL(final String env, final Str "name", null, "email", null, "identities_set", Arrays.asList("anID2 \n")))); - final Set rids = idp.getIdentities( + final IdentityProviderResponse ipr = idp.getIdentities( authCode, "pkcepkcepkcepkcepkcepkce", true, env); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(GLOBUS, "anID2"), - new RemoteIdentityDetails("aUsername2", null, null))); - assertThat("incorrect ident set", rids, is(expected)); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(GLOBUS, "anID2"), + new RemoteIdentityDetails("aUsername2", null, null)) + ))); } private Map map(final Object... entries) { diff --git a/src/test/java/us/kbase/test/auth2/providers/GoogleIdentityProviderTest.java b/src/test/java/us/kbase/test/auth2/providers/GoogleIdentityProviderTest.java index 85540a26..783a7fc0 100644 --- a/src/test/java/us/kbase/test/auth2/providers/GoogleIdentityProviderTest.java +++ b/src/test/java/us/kbase/test/auth2/providers/GoogleIdentityProviderTest.java @@ -14,9 +14,7 @@ import java.net.URL; import java.util.Base64; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.After; @@ -40,6 +38,7 @@ import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; import us.kbase.auth2.lib.identity.IdentityProviderConfig.IdentityProviderConfigurationException; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.providers.GoogleIdentityProviderFactory; import us.kbase.auth2.providers.GoogleIdentityProviderFactory.GoogleIdentityProvider; import us.kbase.test.auth2.TestCommon; @@ -425,12 +424,13 @@ private void getIdentityWithLoginURL(final String env, final String url) throws setUpOAuthCall(authCode, "pkcewithstuff", "foo." + b64json(payload) + ".bar", url, idconfig.getClientID(), idconfig.getClientSecret()); - final Set rids = idp.getIdentities(authCode, "pkcewithstuff", false, env); - assertThat("incorrect number of idents", rids.size(), is(1)); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(GOOGLE, "id7"), - new RemoteIdentityDetails("email3", null, "email3"))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities( + authCode, "pkcewithstuff", false, env + ); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(GOOGLE, "id7"), + new RemoteIdentityDetails("email3", null, "email3")) + ))); } @Test @@ -466,12 +466,11 @@ private void getIdentityWithLinkURL(final String env, final String url) throws E setUpOAuthCall(authCode, "pixy", "foo." + b64json(payload) + ".bar", url, idconfig.getClientID(), idconfig.getClientSecret()); - final Set rids = idp.getIdentities(authCode, "pixy", true, env); - assertThat("incorrect number of idents", rids.size(), is(1)); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(GOOGLE, "id1"), - new RemoteIdentityDetails("email1", "dispname1", "email1"))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pixy", true, env); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(GOOGLE, "id1"), + new RemoteIdentityDetails("email1", "dispname1", "email1")) + ))); } private void setUpOAuthCall( diff --git a/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java b/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java index f061dbda..651f489d 100644 --- a/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java +++ b/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java @@ -12,9 +12,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import org.junit.After; import org.junit.BeforeClass; @@ -37,6 +35,7 @@ import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; import us.kbase.auth2.lib.identity.IdentityProviderConfig.IdentityProviderConfigurationException; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.providers.OrcIDIdentityProviderFactory; import us.kbase.auth2.providers.OrcIDIdentityProviderFactory.OrcIDIdentityProvider; import us.kbase.test.auth2.TestCommon; @@ -399,12 +398,11 @@ private void getIdentityWithLoginURL(final String email, final Map rids = idp.getIdentities(authCode, "pkce", false, null); - assertThat("incorrect number of idents", rids.size(), is(1)); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), - new RemoteIdentityDetails(orcID, "My name", email))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pkce", false, null); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), + new RemoteIdentityDetails(orcID, "My name", email)) + ))); } @Test @@ -418,12 +416,11 @@ public void getIdentityWithLoginURLAndEnvironment() throws Exception { idconfig.getClientID(), idconfig.getClientSecret(), " My name ", orcID); setupCallID("footoken3", orcID, APP_JSON, 200, MAPPER.writeValueAsString( map("email", Arrays.asList(map("email", "email7"))))); - final Set rids = idp.getIdentities(authCode, "pkce", false, "e3"); - assertThat("incorrect number of idents", rids.size(), is(1)); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), - new RemoteIdentityDetails(orcID, "My name", "email7"))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pkce", false, "e3"); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), + new RemoteIdentityDetails(orcID, "My name", "email7")) + ))); } @Test @@ -457,12 +454,11 @@ private void getIdentityWithLinkURL(final String email, final Map rids = idp.getIdentities(authCode, "pkce", true, null); - assertThat("incorrect number of idents", rids.size(), is(1)); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), - new RemoteIdentityDetails(orcID, null, email))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pkce", true, null); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), + new RemoteIdentityDetails(orcID, null, email)) + ))); } @Test @@ -486,12 +482,11 @@ public void getIdentityWithLinkURLAndEnvironment() throws Exception { null, orcID); setupCallID("footoken2", orcID, APP_JSON, 200, MAPPER.writeValueAsString( map("email", Arrays.asList(map("email", "email4"))))); - final Set rids = idp.getIdentities(authCode, "pkce", true, "e3"); - assertThat("incorrect number of idents", rids.size(), is(1)); - final Set expected = new HashSet<>(); - expected.add(new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), - new RemoteIdentityDetails(orcID, null, "email4"))); - assertThat("incorrect ident set", rids, is(expected)); + final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pkce", true, "e3"); + assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( + new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), + new RemoteIdentityDetails(orcID, null, "email4")) + ))); } private void setUpCallAuthToken( diff --git a/src/test/java/us/kbase/test/auth2/service/ui/LinkTest.java b/src/test/java/us/kbase/test/auth2/service/ui/LinkTest.java index c7d1b0f4..12c983fb 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/LinkTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/LinkTest.java @@ -61,6 +61,7 @@ import us.kbase.auth2.lib.exceptions.NoSuchTokenException; import us.kbase.auth2.lib.exceptions.NoTokenProvidedException; import us.kbase.auth2.lib.identity.IdentityProvider; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -499,7 +500,7 @@ private void linkCompleteImmediateLinkDefaultRedirect(final String env) throws E final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); when(provmock.getIdentities(authcode, "pkceisgoodfordiptheria", true, env)) - .thenReturn(set(REMOTE1, REMOTE2)); + .thenReturn(IdentityProviderResponse.from(set(REMOTE1, REMOTE2))); final WebTarget wt = linkCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -549,7 +550,7 @@ private void linkCompleteImmediateLinkCustomRedirect(final String env, final Str final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); when(provmock.getIdentities(authcode, "pkcebludgeonsjoyintoyoursoul", true, env)) - .thenReturn(set(REMOTE1, REMOTE3)); + .thenReturn(IdentityProviderResponse.from(set(REMOTE1, REMOTE3))); final WebTarget wt = linkCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -597,7 +598,7 @@ private void linkCompleteDelayedDefaultRedirect(final String env) throws Excepti final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); when(provmock.getIdentities(authcode, "pkcewhateverfeckit", true, env)) - .thenReturn(set(REMOTE1)); + .thenReturn(IdentityProviderResponse.from(REMOTE1)); final WebTarget wt = linkCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -669,7 +670,7 @@ private void linkCompleteDelayedMultipleIdentsAndCustomRedirect( final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); when(provmock.getIdentities(authcode, "pkcewowbaggerismyhomie", true, env)).thenReturn( - set(REMOTE1, REMOTE2, REMOTE3)); + IdentityProviderResponse.from(set(REMOTE1, REMOTE2, REMOTE3))); final WebTarget wt = linkCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() diff --git a/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java index 47395fd2..6f1db05b 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java @@ -69,6 +69,7 @@ import us.kbase.auth2.lib.exceptions.NoSuchTokenException; import us.kbase.auth2.lib.exceptions.NoTokenProvidedException; import us.kbase.auth2.lib.identity.IdentityProvider; +import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; @@ -937,7 +938,7 @@ private RemoteIdentity loginCompleteSetUpProviderMock( new RemoteIdentityID("prov1", "prov1id"), new RemoteIdentityDetails("user", "full", "email@email.com")); when(provmock.getIdentities(authcode, pkce, false, environment)) - .thenReturn(set(remoteIdentity)); + .thenReturn(IdentityProviderResponse.from(remoteIdentity)); return remoteIdentity; } From d48dd969b18815e340b6fcabdac4944bbb5510a9 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Sun, 16 Nov 2025 17:32:01 -0800 Subject: [PATCH 13/24] Fix immutability bug in identity provider config Also, one of the URLs was hanging for 10s or so, so changed the target --- .../lib/identity/IdentityProviderConfig.java | 6 +++--- .../identity/IdentityProviderConfigTest.java | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderConfig.java b/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderConfig.java index 89c41817..3f8247d0 100644 --- a/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderConfig.java +++ b/src/main/java/us/kbase/auth2/lib/identity/IdentityProviderConfig.java @@ -46,9 +46,9 @@ private IdentityProviderConfig( this.apiURL = apiURL; this.defaultLoginRedirectURL = defaultLoginRedirectURL; this.defaultLinkRedirectURL = defaultLinkRedirectURL; - this.customConfig = Collections.unmodifiableMap(customConfig); - this.envLoginRedirectURL = Collections.unmodifiableMap(envLoginRedirectURL); - this.envLinkRedirectURL = Collections.unmodifiableMap(envLinkRedirectURL); + this.customConfig = Collections.unmodifiableMap(new HashMap<>(customConfig)); + this.envLoginRedirectURL = Collections.unmodifiableMap(new HashMap<>(envLoginRedirectURL)); + this.envLinkRedirectURL = Collections.unmodifiableMap(new HashMap<>(envLinkRedirectURL)); } /** Get the class name of the identity provider factory for this configuration. diff --git a/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderConfigTest.java b/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderConfigTest.java index ca30c53d..82642a91 100644 --- a/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderConfigTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/identity/IdentityProviderConfigTest.java @@ -37,7 +37,7 @@ public void goodInput() throws Exception { new URL("http://api.com"), "foo", "bar", - new URL("https://loginredirect.com"), + new URL("https://fakeloginredirect.com"), new URL("https://linkredirect.com")) .withCustomConfiguration("foo", "bar") .withCustomConfiguration("baz", "bat") @@ -61,7 +61,7 @@ public void goodInput() throws Exception { assertThat("incorrect link redirect URL", c.getLinkRedirectURL("env2"), is(new URL("https://linkredirect2.com"))); assertThat("incorrect login redirect URL", c.getLoginRedirectURL(), - is(new URL("https://loginredirect.com"))); + is(new URL("https://fakeloginredirect.com"))); assertThat("incorrect login redirect URL", c.getLoginRedirectURL("env1"), is(new URL("https://loginredirect1.com"))); assertThat("incorrect login redirect URL", c.getLoginRedirectURL("env2"), @@ -265,7 +265,7 @@ private void failAddEnvironment( @Test public void immutable() throws Exception { - final IdentityProviderConfig c = IdentityProviderConfig.getBuilder( + Builder b = IdentityProviderConfig.getBuilder( "MyProv", new URL("http://login.com"), new URL("http://api.com"), @@ -275,8 +275,8 @@ public void immutable() throws Exception { new URL("https://linkredirect.com")) .withCustomConfiguration("foo", "bar") .withCustomConfiguration("baz", "bat") - .withEnvironment("e", new URL("http://foo.com"), new URL("http://foo.com")) - .build(); + .withEnvironment("e", new URL("http://foo.com"), new URL("http://foo.com")); + final IdentityProviderConfig c = b.build(); try { c.getCustomConfiguation().put("foo", "bar"); @@ -291,6 +291,14 @@ public void immutable() throws Exception { } catch (UnsupportedOperationException e) { // test passed } + + // bugfix check - ensure modifying the builders maps doesn't modify old builds + b.withCustomConfiguration("whee", "whoo") + .withEnvironment("e1", new URL("http://whoo.com"), new URL("http://whee.com")); + assertThat("incorrect custom config", c.getCustomConfiguation(), is(ImmutableMap.of( + "foo", "bar", "baz", "bat" + ))); + assertThat("incorrect envs", c.getEnvironments(), is(set("e"))); } } From aa9b52a63269e480f00cee188b5321f65dff70e5 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Mon, 17 Nov 2025 14:02:10 -0800 Subject: [PATCH 14/24] Add MFA state determination code to OrcID provider Based on code originally written by David Lyon here: https://github.com/kbase/auth2/pull/471 --- deploy.cfg.example | 2 + .../OrcIDIdentityProviderFactory.java | 108 +++++++- .../providers/OrcIDIdentityProviderTest.java | 249 ++++++++++++++---- 3 files changed, 307 insertions(+), 52 deletions(-) diff --git a/deploy.cfg.example b/deploy.cfg.example index c41f6350..ce7f7da1 100644 --- a/deploy.cfg.example +++ b/deploy.cfg.example @@ -98,3 +98,5 @@ identity-provider-OrcID-client-id = identity-provider-OrcID-client-secret = identity-provider-OrcID-login-redirect-url = https://kbase.us/services/auth/login/complete/orcid identity-provider-OrcID-link-redirect-url = https://kbase.us/services/auth/link/complete/orcid +# uncomment to disable using MFA. Required if OrcID plan doesn't support MFA +#identity-provider-OrcID-custom-disable-mfa = true diff --git a/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java b/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java index da0645a6..391051f8 100644 --- a/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java +++ b/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java @@ -6,6 +6,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,6 +34,7 @@ import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; +import us.kbase.auth2.lib.token.MFAStatus; /** A factory for a OrcID identity provider. * @author gaprice@lbl.gov @@ -46,7 +48,14 @@ public IdentityProvider configure(final IdentityProviderConfig cfg) { } /** An identity provider for OrcID accounts. - * @author gaprice@lbl.gov + * + * Multi-Factor Authentication (MFA) Status Handling: + * - Uses OpenID Connect JWT tokens to determine MFA status via AMR claims + * - Configuration option "disable-mfa" (default: false): + * - false: Requires OpenID scope, throws error on missing or malformed JWT + * - true: Skips MFA check, returns MFAStatus.UNKNOWN (for non-member API apps) + * - Valid JWT with AMR claim: returns MFAStatus.USED or MFAStatus.NOT_USED based on + * "mfa" presence * */ public static class OrcIDIdentityProvider implements IdentityProvider { @@ -57,8 +66,10 @@ public static class OrcIDIdentityProvider implements IdentityProvider { /* Get creds: https://sandbox.orcid.org/developer-tools */ + private static final String DISABLE_MFA = "disable-mfa"; private static final String NAME = "OrcID"; - private static final String SCOPE = "/authenticate"; + private static final String SCOPE_OPENID = "openid /authenticate"; + private static final String SCOPE_NO_OPENID = "/authenticate"; private static final String LOGIN_PATH = "/oauth/authorize"; private static final String TOKEN_PATH = "/oauth/token"; private static final String RECORD_PATH = "/v2.1"; @@ -69,6 +80,7 @@ public static class OrcIDIdentityProvider implements IdentityProvider { private static final ObjectMapper MAPPER = new ObjectMapper(); private final IdentityProviderConfig cfg; + private final boolean skipMFA; /** Create an identity provider for OrcID. * @param idc the configuration for this provider. @@ -82,6 +94,7 @@ public OrcIDIdentityProvider(final IdentityProviderConfig idc) { idc.getIdentityProviderFactoryClassName()); } this.cfg = idc; + skipMFA = "true".equals(idc.getCustomConfiguation().get(DISABLE_MFA)); } @Override @@ -102,11 +115,12 @@ public URI getLoginURI( final boolean link, final String environment) throws NoSuchEnvironmentException { + final String scope = skipMFA ? SCOPE_NO_OPENID : SCOPE_OPENID; // note that OrcID does not currently implement PKCE so we ignore the code // challenge: https://github.com/ORCID/ORCID-Source/issues/5977 return UriBuilder.fromUri(toURI(cfg.getLoginURL())) .path(LOGIN_PATH) - .queryParam("scope", SCOPE) + .queryParam("scope", scope) .queryParam("state", state) .queryParam("redirect_uri", getRedirectURL(link, environment)) .queryParam("response_type", "code") @@ -147,7 +161,7 @@ public IdentityProviderResponse getIdentities( final OrcIDAccessTokenResponse accessToken = getAccessToken( authcode, link, environment); final RemoteIdentity ri = getIdentity(accessToken); - return IdentityProviderResponse.from(ri); + return IdentityProviderResponse.from(ri, accessToken.mfa); } private RemoteIdentity getIdentity(final OrcIDAccessTokenResponse accessToken) @@ -210,11 +224,14 @@ private static class OrcIDAccessTokenResponse { private final String accessToken; private final String fullName; private final String orcID; + private final MFAStatus mfa; private OrcIDAccessTokenResponse( final String accessToken, final String fullName, - final String orcID) + final String orcID, + final MFAStatus mfaStatus + ) throws IdentityRetrievalException { if (accessToken == null || accessToken.trim().isEmpty()) { throw new IdentityRetrievalException( @@ -227,6 +244,7 @@ private OrcIDAccessTokenResponse( this.accessToken = accessToken.trim(); this.fullName = fullName == null ? null : fullName.trim(); this.orcID = orcID.trim(); + this.mfa = mfaStatus; } } @@ -255,10 +273,88 @@ private OrcIDAccessTokenResponse getAccessToken( throw new IdentityRetrievalException("Authtoken retrieval failed: " + msg[msg.length - 1].trim()); } + + // Determine MFA status based on configuration + final MFAStatus mfaStatus; + if (skipMFA) { + // MFA checking disabled - no OpenID scope, so no id_token expected + mfaStatus = MFAStatus.UNKNOWN; + } else { + // MFA checking enabled - parse JWT from id_token + final String idToken = (String) m.get("id_token"); + mfaStatus = parseAmrClaim(idToken); + } + return new OrcIDAccessTokenResponse( (String) m.get("access_token"), (String) m.get("name"), - (String) m.get("orcid")); + (String) m.get("orcid"), + mfaStatus + ); + } + + /** + * Parses the Authentication Method Reference (AMR) claim from an OpenID Connect ID token + * to determine if multi-factor authentication was used. + * + * @param jwt the JWT ID token from ORCID + * @return MFAStatus indicating whether MFA was used + * @throws IdentityRetrievalException if JWT is missing, malformed, or unparseable + */ + private MFAStatus parseAmrClaim(final String jwt) throws IdentityRetrievalException { + if (jwt == null || jwt.trim().isEmpty()) { + throw new IdentityRetrievalException( + "No JWT token provided by ORCID. For non-member API applications, " + + "set disable-mfa to true in provider configuration"); + } + + // JWT format: header.payload.signature + final String[] parts = jwt.split("\\."); + if (parts.length != 3) { + // Invalid JWT format + throw new IdentityRetrievalException( + "Invalid JWT format from ORCID: expected 3 parts, got " + parts.length + ); + } + + // Decode the payload (second part) - URL-safe base64 + final String payload; + try { + payload = new String(Base64.getUrlDecoder().decode(parts[1])); + } catch (IllegalArgumentException e) { + // Base64 decoding failed - invalid JWT format + throw new IdentityRetrievalException("Unable to decode JWT from ORCID", e); + } + + // Parse JSON payload to extract claims + final Map claims; + try { + @SuppressWarnings("unchecked") + final Map parsedClaims = MAPPER.readValue(payload, Map.class); + claims = parsedClaims; + } catch (IOException e) { + // JSON parsing failed - malformed payload + throw new IdentityRetrievalException("Unable to parse JWT payload from ORCID", e); + } + + final Object amrClaim = claims.get("amr"); + if (amrClaim == null) { + // No AMR claim present - MFA status unknown + return MFAStatus.UNKNOWN; + } else if (amrClaim instanceof List) { + // OpenID Connect spec: AMR should be an array of strings + @SuppressWarnings("unchecked") + final List amrList = (List) amrClaim; + return amrList.contains("mfa") ? MFAStatus.USED : MFAStatus.NOT_USED; + } else if (amrClaim instanceof String) { + // ORCID may return single string - handle as fallback + return "mfa".equals(amrClaim) ? MFAStatus.USED : MFAStatus.NOT_USED; + } + + // AMR claim present but in unexpected format + throw new IdentityRetrievalException( + "AMR claim from ORCID in unexpected format: " + amrClaim + ); } private Map orcIDPostRequest( diff --git a/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java b/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java index 651f489d..9c530d86 100644 --- a/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java +++ b/src/test/java/us/kbase/test/auth2/providers/OrcIDIdentityProviderTest.java @@ -3,11 +3,11 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static us.kbase.test.auth2.TestCommon.list; import static us.kbase.test.auth2.TestCommon.set; import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Collections; @@ -34,6 +34,8 @@ import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; +import us.kbase.auth2.lib.token.MFAStatus; +import us.kbase.auth2.lib.identity.IdentityProviderConfig.Builder; import us.kbase.auth2.lib.identity.IdentityProviderConfig.IdentityProviderConfigurationException; import us.kbase.auth2.lib.identity.IdentityProviderResponse; import us.kbase.auth2.providers.OrcIDIdentityProviderFactory; @@ -90,10 +92,11 @@ public void tearDownTest() { mockClientAndServer.reset(); } - private static final IdentityProviderConfig CFG; + private static final IdentityProviderConfig CFG_NO_MFA; + private static final IdentityProviderConfig CFG_MFA; static { try { - CFG = IdentityProviderConfig.getBuilder( + Builder base = IdentityProviderConfig.getBuilder( OrcIDIdentityProviderFactory.class.getName(), new URL("https://ologin.com"), new URL("https://osetapiurl.com"), @@ -102,47 +105,48 @@ public void tearDownTest() { new URL("https://ologinredir.com"), new URL("https://olinkredir.com")) .withEnvironment("myenv", - new URL("https://myologinred.com"), new URL("https://myolinkred.com")) - .build(); + new URL("https://myologinred.com"), new URL("https://myolinkred.com")); + CFG_MFA = base.build(); + CFG_NO_MFA = base.withCustomConfiguration("disable-mfa", "true").build(); } catch (IdentityProviderConfigurationException | MalformedURLException e) { throw new RuntimeException("Fix yer tests newb", e); } } @Test - public void simpleOperationsWithConfigurator() throws Exception { + public void simpleOperationsWithConfiguratorWithMFA() throws Exception { final OrcIDIdentityProviderFactory gc = new OrcIDIdentityProviderFactory(); - final IdentityProvider oip = gc.configure(CFG); + final IdentityProvider oip = gc.configure(CFG_MFA); assertThat("incorrect provider name", oip.getProviderName(), is("OrcID")); assertThat("incorrect environments", oip.getEnvironments(), is(set("myenv"))); assertThat("incorrect login url", oip.getLoginURI("foo3", "pkce", false, null), is(new URI("https://ologin.com/oauth/authorize?" + - "scope=%2Fauthenticate" + + "scope=openid+%2Fauthenticate" + "&state=foo3&redirect_uri=https%3A%2F%2Fologinredir.com" + "&response_type=code&client_id=ofoo"))); assertThat("incorrect link url", oip.getLoginURI("foo4", "pkce", true, null), is(new URI("https://ologin.com/oauth/authorize?" + - "scope=%2Fauthenticate" + + "scope=openid+%2Fauthenticate" + "&state=foo4&redirect_uri=https%3A%2F%2Folinkredir.com" + "&response_type=code&client_id=ofoo"))); assertThat("incorrect login url", oip.getLoginURI("foo3", "pkce", false, "myenv"), is(new URI("https://ologin.com/oauth/authorize?" + - "scope=%2Fauthenticate" + + "scope=openid+%2Fauthenticate" + "&state=foo3&redirect_uri=https%3A%2F%2Fmyologinred.com" + "&response_type=code&client_id=ofoo"))); assertThat("incorrect link url", oip.getLoginURI("foo4", "pkce", true, "myenv"), is(new URI("https://ologin.com/oauth/authorize?" + - "scope=%2Fauthenticate" + + "scope=openid+%2Fauthenticate" + "&state=foo4&redirect_uri=https%3A%2F%2Fmyolinkred.com" + "&response_type=code&client_id=ofoo"))); } @Test - public void simpleOperationsWithoutConfigurator() throws Exception { + public void simpleOperationsWithoutConfiguratorWithoutMFA() throws Exception { - final IdentityProvider oip = new OrcIDIdentityProvider(CFG); + final IdentityProvider oip = new OrcIDIdentityProvider(CFG_NO_MFA); assertThat("incorrect provider name", oip.getProviderName(), is("OrcID")); assertThat("incorrect environments", oip.getEnvironments(), is(set("myenv"))); assertThat("incorrect login url", oip.getLoginURI("foo5", "pkce", false, null), @@ -174,12 +178,12 @@ public void createFail() throws Exception { failCreate(null, new NullPointerException("idc")); failCreate(IdentityProviderConfig.getBuilder( "foo", - CFG.getLoginURL(), - CFG.getApiURL(), - CFG.getClientID(), - CFG.getClientSecret(), - CFG.getLoginRedirectURL(), - CFG.getLinkRedirectURL()) + CFG_NO_MFA.getLoginURL(), + CFG_NO_MFA.getApiURL(), + CFG_NO_MFA.getClientID(), + CFG_NO_MFA.getClientSecret(), + CFG_NO_MFA.getLoginRedirectURL(), + CFG_NO_MFA.getLinkRedirectURL()) .build(), new IllegalArgumentException( "Configuration class name doesn't match factory class name: foo")); @@ -196,7 +200,7 @@ private void failCreate(final IdentityProviderConfig cfg, final Exception except @Test public void illegalAuthcode() throws Exception { - final IdentityProvider idp = new OrcIDIdentityProvider(CFG); + final IdentityProvider idp = new OrcIDIdentityProvider(CFG_NO_MFA); failGetIdentities(idp, null, "pkce", true, new IllegalArgumentException( "authcode cannot be null or empty")); failGetIdentities(idp, " \t \n ", "pkce", true, new IllegalArgumentException( @@ -205,7 +209,7 @@ public void illegalAuthcode() throws Exception { @Test public void noSuchEnvironment() throws Exception { - final IdentityProvider idp = new OrcIDIdentityProvider(CFG); + final IdentityProvider idp = new OrcIDIdentityProvider(CFG_NO_MFA); failGetIdentities(idp, "foo", "pkce", true, "myenv1", new NoSuchEnvironmentException("myenv1")); @@ -238,10 +242,12 @@ private void failGetIdentities( } } - private IdentityProviderConfig getTestIDConfig() - throws IdentityProviderConfigurationException, MalformedURLException, - URISyntaxException { - return IdentityProviderConfig.getBuilder( + private IdentityProviderConfig getTestIDConfig() throws Exception { + return getTestIDConfig(false); + } + + private IdentityProviderConfig getTestIDConfig(final boolean withMFA) throws Exception { + final Builder b = IdentityProviderConfig.getBuilder( OrcIDIdentityProviderFactory.class.getName(), new URL("http://localhost:" + mockClientAndServer.getPort()), new URL("http://localhost:" + mockClientAndServer.getPort()), @@ -249,8 +255,12 @@ private IdentityProviderConfig getTestIDConfig() "obar", new URL("https://ologinredir.com"), new URL("https://olinkredir.com")) - .withEnvironment("e3", new URL("https://lo.com"), new URL("https://li.com")) - .build(); + .withEnvironment( + "e3", new URL("https://lo.com"), new URL("https://li.com")); + if (!withMFA) { + b.withCustomConfiguration("disable-mfa", "true"); + } + return b.build(); } @Test @@ -322,6 +332,68 @@ public void returnsBadResponseAuthToken() throws Exception { "Error: whee!. Error description: whoo!")); } + @Test + public void returnsBadResponseJWT() throws Exception { + final String orcID = "0000-0001-1234-5678"; + + failParseJWT(null, new IdentityRetrievalException( + "No JWT token provided by ORCID. For non-member API applications, " + + "set disable-mfa to true in provider configuration" + )); + failParseJWT(" \t ", new IdentityRetrievalException( + "No JWT token provided by ORCID. For non-member API applications, " + + "set disable-mfa to true in provider configuration" + )); + failParseJWT("who wrote this bloody token", new IdentityRetrievalException( + "Invalid JWT format from ORCID: expected 3 parts, got 1") + ); + failParseJWT("header.payload", new IdentityRetrievalException( + "Invalid JWT format from ORCID: expected 3 parts, got 2")); + failParseJWT("invalid.jwt.token", new IdentityRetrievalException( + "Unable to parse JWT payload from ORCID" + )); + failParseJWT( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid==base64!!.signature", + new IdentityRetrievalException("Unable to decode JWT from ORCID") + ); + final String invalidJSON = "{\"sub\":\"" + orcID + "\",\"amr\":}"; + final String encodedPayload = java.util.Base64.getUrlEncoder().withoutPadding() + .encodeToString(invalidJSON.getBytes()); + final String invalidJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + encodedPayload + ".signature"; + failParseJWT(invalidJWT, new IdentityRetrievalException( + "Unable to parse JWT payload from ORCID") + ); + failParseJWT( + jwt(orcID, map("amr", Collections.emptyMap())), new IdentityRetrievalException( + "AMR claim from ORCID in unexpected format: {}")); + } + + private void failParseJWT(final String jwt, final Exception expected) throws Exception { + final String authCode = "authcode2"; + final IdentityProviderConfig idconfig = getTestIDConfig(true); + final IdentityProvider idp = new OrcIDIdentityProvider(idconfig); + final String orcID = "0000-0001-1234-5678"; + + setUpCallAuthToken( + authCode, + "footoken3", + "https://ologinredir.com", + idconfig.getClientID(), + idconfig.getClientSecret(), + " My name ", + orcID, + jwt + ); + failGetIdentities( + idp, + authCode, + "pkce", + false, + expected + ); + } + @Test public void returnsBadResponseIdentity() throws Exception { final IdentityProviderConfig cfg = getTestIDConfig(); @@ -379,29 +451,80 @@ public void returnsBadResponseIdentity() throws Exception { @Test public void getIdentityWithLoginURL() throws Exception { - getIdentityWithLoginURL(null, map()); - getIdentityWithLoginURL(null, map("email", null)); - getIdentityWithLoginURL(null, map("email", Collections.emptyList())); - getIdentityWithLoginURL(null, map("email", Arrays.asList(map()))); - getIdentityWithLoginURL(null, map("email", Arrays.asList(map("email", null)))); - getIdentityWithLoginURL(null, map("email", Arrays.asList(map("email", " \t \n ")))); - getIdentityWithLoginURL("email3", map("email", Arrays.asList(map("email", "email3")))); + /* For now we only test MFA with login since it's only relevant there and the code + * path is identical to linking. The only difference is the redirect url transmitted to + * OrcID + */ + final String orcID = "0000-0001-1234-5678"; + // MFA checking is skipped + getIdentityWithLoginURL(orcID, null, map(), null, MFAStatus.UNKNOWN); + getIdentityWithLoginURL( + orcID, + null, + map("email", null), + jwt(orcID, map("iss", "https://orcid.org")), + MFAStatus.UNKNOWN + ); + getIdentityWithLoginURL( + orcID, + null, + map("email", Collections.emptyList()), + jwt(orcID, map("amr", "pwd")), + MFAStatus.NOT_USED + ); + getIdentityWithLoginURL( + orcID, + null, + map("email", Arrays.asList(map())), + jwt(orcID, map("amr", "mfa")), + MFAStatus.USED + ); + getIdentityWithLoginURL( + orcID, + null, + map("email", Arrays.asList(map("email", null))), + jwt(orcID, map("amr", list())), + MFAStatus.NOT_USED + ); + getIdentityWithLoginURL( + orcID, + null, + map("email", Arrays.asList(map("email", " \t \n "))), + jwt(orcID, map("amr", list("pwd"))), + MFAStatus.NOT_USED + ); + getIdentityWithLoginURL( + orcID, + "email3", + map("email", Arrays.asList(map("email", "email3"))), + jwt(orcID, map("amr", list("pwd", "mfa"))), + MFAStatus.USED + ); } - private void getIdentityWithLoginURL(final String email, final Map response) - throws Exception { + private void getIdentityWithLoginURL( + final String orcID, + final String email, + final Map identityResponse, + final String jwt, + final MFAStatus mfa + ) throws Exception { final String authCode = "authcode2"; - final IdentityProviderConfig idconfig = getTestIDConfig(); + final IdentityProviderConfig idconfig = getTestIDConfig(jwt != null); final IdentityProvider idp = new OrcIDIdentityProvider(idconfig); - final String orcID = "0000-0001-1234-5678"; setUpCallAuthToken(authCode, "footoken3", "https://ologinredir.com", - idconfig.getClientID(), idconfig.getClientSecret(), " My name ", orcID); - setupCallID("footoken3", orcID, APP_JSON, 200, MAPPER.writeValueAsString(response)); + idconfig.getClientID(), idconfig.getClientSecret(), " My name ", orcID, jwt + ); + setupCallID( + "footoken3", orcID, APP_JSON, 200, MAPPER.writeValueAsString(identityResponse) + ); final IdentityProviderResponse ipr = idp.getIdentities(authCode, "pkce", false, null); assertThat("incorrect ident set", ipr, is(IdentityProviderResponse.from( new RemoteIdentity(new RemoteIdentityID(ORCID, orcID), - new RemoteIdentityDetails(orcID, "My name", email)) + new RemoteIdentityDetails(orcID, "My name", email) + ), + mfa ))); } @@ -445,6 +568,7 @@ private void getIdentityWithLinkURL(final String email, final Map resp = map( + "access_token", authtoken, + "name", name, + "orcid", orcID + ); + if (idToken != null) { + resp.put("id_token", idToken); + } mockClientAndServer.when( new HttpRequest() .withMethod("POST") @@ -515,11 +664,7 @@ private void setUpCallAuthToken( new HttpResponse() .withStatusCode(200) .withHeader(CONTENT_TYPE, APP_JSON) - .withBody(MAPPER.writeValueAsString(map( - "access_token", authtoken, - "name", name, - "orcid", orcID - ))) + .withBody(MAPPER.writeValueAsString(resp)) ); } @@ -576,6 +721,18 @@ private void setupCallID( ); } + private String jwt(final String orcID, final Map payload) throws Exception { + final String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; + payload.put("sub", orcID); + final String paystr = MAPPER.writeValueAsString(payload); + + final String encodedHeader = java.util.Base64.getUrlEncoder().withoutPadding() + .encodeToString(header.getBytes()); + final String encodedPayload = java.util.Base64.getUrlEncoder().withoutPadding() + .encodeToString(paystr.getBytes()); + return encodedHeader + "." + encodedPayload + ".signature"; + } + private Map map(final Object... entries) { if (entries.length % 2 != 0) { throw new IllegalArgumentException(); From 487555b3a2c8b0c697b4517f69bac1eb37984967 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Mon, 17 Nov 2025 14:53:42 -0800 Subject: [PATCH 15/24] Add MFAStatus to Stored Token --- .../us/kbase/auth2/lib/token/StoredToken.java | 125 +++++++----------- .../kbase/test/auth2/lib/token/TokenTest.java | 115 +++++++++------- 2 files changed, 115 insertions(+), 125 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/token/StoredToken.java b/src/main/java/us/kbase/auth2/lib/token/StoredToken.java index cdedf822..ed04428b 100644 --- a/src/main/java/us/kbase/auth2/lib/token/StoredToken.java +++ b/src/main/java/us/kbase/auth2/lib/token/StoredToken.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import java.time.Instant; +import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -23,6 +24,7 @@ public class StoredToken { private final UserName userName; private final Instant creationDate; private final Instant expirationDate; + private final MFAStatus mfa; private StoredToken( final UUID id, @@ -31,7 +33,9 @@ private StoredToken( final UserName userName, final TokenCreationContext context, final Instant creationDate, - final Instant expirationDate) { + final Instant expirationDate, + final MFAStatus mfa + ) { // this stuff is here just in case naughty users use casting to skip a builder step requireNonNull(creationDate, "created"); // no way to test this one @@ -43,6 +47,7 @@ private StoredToken( this.expirationDate = expirationDate; this.creationDate = creationDate; this.id = id; + this.mfa = mfa; } /** Get the type of the token. @@ -94,78 +99,37 @@ public Instant getExpirationDate() { return expirationDate; } + /** Get the MFA status of the token. + * @return the MFA status. + */ + public MFAStatus getMFA() { + return mfa; + } + @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((context == null) ? 0 : context.hashCode()); - result = prime * result + ((creationDate == null) ? 0 : creationDate.hashCode()); - result = prime * result + ((expirationDate == null) ? 0 : expirationDate.hashCode()); - result = prime * result + ((id == null) ? 0 : id.hashCode()); - result = prime * result + ((tokenName == null) ? 0 : tokenName.hashCode()); - result = prime * result + ((type == null) ? 0 : type.hashCode()); - result = prime * result + ((userName == null) ? 0 : userName.hashCode()); - return result; + return Objects.hash( + context, creationDate, expirationDate, id, mfa, tokenName, type, userName + ); } @Override public boolean equals(Object obj) { - if (this == obj) { + if (this == obj) return true; - } - if (obj == null) { + if (obj == null) return false; - } - if (getClass() != obj.getClass()) { + if (getClass() != obj.getClass()) return false; - } StoredToken other = (StoredToken) obj; - if (context == null) { - if (other.context != null) { - return false; - } - } else if (!context.equals(other.context)) { - return false; - } - if (creationDate == null) { - if (other.creationDate != null) { - return false; - } - } else if (!creationDate.equals(other.creationDate)) { - return false; - } - if (expirationDate == null) { - if (other.expirationDate != null) { - return false; - } - } else if (!expirationDate.equals(other.expirationDate)) { - return false; - } - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - if (tokenName == null) { - if (other.tokenName != null) { - return false; - } - } else if (!tokenName.equals(other.tokenName)) { - return false; - } - if (type != other.type) { - return false; - } - if (userName == null) { - if (other.userName != null) { - return false; - } - } else if (!userName.equals(other.userName)) { - return false; - } - return true; + return Objects.equals(context, other.context) + && Objects.equals(creationDate, other.creationDate) + && Objects.equals(expirationDate, other.expirationDate) + && Objects.equals(id, other.id) + && mfa == other.mfa + && Objects.equals(tokenName, other.tokenName) + && type == other.type + && Objects.equals(userName, other.userName); } /** Get a builder for a StoredToken. @@ -224,6 +188,12 @@ public interface OptionalsStep { */ OptionalsStep withContext(TokenCreationContext context); + /** Specify the MFA status; default is {@link MFAStatus#UNKNOWN}. + * @param context the MFA status. + * @return this builder. + */ + OptionalsStep withMFA(MFAStatus mfa); + /** Build the token. * @return a new StoredToken. */ @@ -239,20 +209,17 @@ private static class Builder implements LifeStep, OptionalsStep { private final UserName userName; private Instant creationDate; private Instant expirationDate; + private MFAStatus mfa = MFAStatus.UNKNOWN; private Builder(final TokenType type, final UUID id, final UserName userName) { - requireNonNull(type, "type"); - requireNonNull(id, "id"); - requireNonNull(userName, "userName"); - this.id = id; - this.type = type; - this.userName = userName; + this.id = requireNonNull(id, "id"); + this.type = requireNonNull(type, "type"); + this.userName = requireNonNull(userName, "userName");; } @Override public OptionalsStep withTokenName(final TokenName tokenName) { - requireNonNull(tokenName, "tokenName"); - this.tokenName = Optional.of(tokenName); + this.tokenName = Optional.of(requireNonNull(tokenName, "tokenName")); return this; } @@ -264,15 +231,20 @@ public OptionalsStep withNullableTokenName(final TokenName tokenName) { @Override public OptionalsStep withContext(final TokenCreationContext context) { - requireNonNull(context, "context"); - this.context = context; + this.context = requireNonNull(context, "context"); + return this; + } + + @Override + public OptionalsStep withMFA(final MFAStatus mfa) { + this.mfa = requireNonNull(mfa, "mfa"); return this; } @Override public StoredToken build() { return new StoredToken(id, type, tokenName, userName, context, - creationDate, expirationDate); + creationDate, expirationDate, mfa); } @Override @@ -290,9 +262,8 @@ public OptionalsStep withLifeTime(final Instant created, final Instant expires) @Override public OptionalsStep withLifeTime( final Instant created, - final long lifeTimeInMilliseconds) { - requireNonNull(created, "created"); - this.creationDate = created; + final long lifeTimeInMilliseconds) { // TODO CODE check > 0 + this.creationDate = requireNonNull(created, "created"); this.expirationDate = created.plusMillis(lifeTimeInMilliseconds); return this; } diff --git a/src/test/java/us/kbase/test/auth2/lib/token/TokenTest.java b/src/test/java/us/kbase/test/auth2/lib/token/TokenTest.java index 035fb0dd..7527d299 100644 --- a/src/test/java/us/kbase/test/auth2/lib/token/TokenTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/token/TokenTest.java @@ -24,6 +24,7 @@ import us.kbase.auth2.lib.exceptions.MissingParameterException; import us.kbase.auth2.lib.token.IncomingHashedToken; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.NewToken; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.StoredToken.OptionalsStep; @@ -163,75 +164,85 @@ private void failCreateTemporaryToken( public void storedTokenMSLifetime() throws Exception { final UUID id = UUID.randomUUID(); - final StoredToken ht = StoredToken.getBuilder(TokenType.LOGIN, id, new UserName("whee")) + final StoredToken st = StoredToken.getBuilder(TokenType.LOGIN, id, new UserName("whee")) .withLifeTime(Instant.ofEpochMilli(1000), 4000).build(); - assertThat("incorrect token type", ht.getTokenType(), is(TokenType.LOGIN)); - assertThat("incorrect token name", ht.getTokenName(), is(Optional.empty())); - assertThat("incorrect token id", ht.getId(), is(id)); - assertThat("incorrect user", ht.getUserName(), is(new UserName("whee"))); - assertThat("incorrect creation date", ht.getCreationDate(), + assertThat("incorrect token type", st.getTokenType(), is(TokenType.LOGIN)); + assertThat("incorrect token name", st.getTokenName(), is(Optional.empty())); + assertThat("incorrect token id", st.getId(), is(id)); + assertThat("incorrect user", st.getUserName(), is(new UserName("whee"))); + assertThat("incorrect creation date", st.getCreationDate(), is(Instant.ofEpochMilli(1000))); - assertThat("incorrect expiration date", ht.getExpirationDate(), + assertThat("incorrect expiration date", st.getExpirationDate(), is(Instant.ofEpochMilli(5000))); - assertThat("incorrect context", ht.getContext(), + assertThat("incorrect context", st.getContext(), is(TokenCreationContext.getBuilder().build())); + assertThat("incorrect MFA", st.getMFA(), is(MFAStatus.UNKNOWN)); } @Test public void storedTokenExpDateAndNameAndContext() throws Exception { - final UUID id2 = UUID.randomUUID(); - final StoredToken ht2 = StoredToken.getBuilder(TokenType.DEV, id2, new UserName("whee2")) + final UUID id = UUID.randomUUID(); + final StoredToken st = StoredToken.getBuilder(TokenType.DEV, id, new UserName("whee2")) .withLifeTime(Instant.ofEpochMilli(27000), Instant.ofEpochMilli(42000)) .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) - .withTokenName(new TokenName("ugh")).build(); - assertThat("incorrect token type", ht2.getTokenType(), is(TokenType.DEV)); - assertThat("incorrect token name", ht2.getTokenName(), + .withTokenName(new TokenName("ugh")) + .withMFA(MFAStatus.USED) + .build(); + assertThat("incorrect token type", st.getTokenType(), is(TokenType.DEV)); + assertThat("incorrect token name", st.getTokenName(), is(Optional.of(new TokenName("ugh")))); - assertThat("incorrect token id", ht2.getId(), is(id2)); - assertThat("incorrect user", ht2.getUserName(), is(new UserName("whee2"))); - assertThat("incorrect creation date", ht2.getCreationDate(), + assertThat("incorrect token id", st.getId(), is(id)); + assertThat("incorrect user", st.getUserName(), is(new UserName("whee2"))); + assertThat("incorrect creation date", st.getCreationDate(), is(Instant.ofEpochMilli(27000))); - assertThat("incorrect expiration date", ht2.getExpirationDate(), + assertThat("incorrect expiration date", st.getExpirationDate(), is(Instant.ofEpochMilli(42000))); - assertThat("incorrect context", ht2.getContext(), + assertThat("incorrect context", st.getContext(), is(TokenCreationContext.getBuilder().withNullableDevice("d").build())); + assertThat("incorrect MFA", st.getMFA(), is(MFAStatus.USED)); } @Test public void storedTokenNullableName() throws Exception { - final UUID id2 = UUID.randomUUID(); - final StoredToken ht2 = StoredToken.getBuilder(TokenType.DEV, id2, new UserName("whee2")) + final UUID id = UUID.randomUUID(); + final StoredToken st = StoredToken.getBuilder(TokenType.DEV, id, new UserName("whee2")) .withLifeTime(Instant.ofEpochMilli(27000), Instant.ofEpochMilli(42000)) - .withNullableTokenName(new TokenName("ugh")).build(); - assertThat("incorrect token type", ht2.getTokenType(), is(TokenType.DEV)); - assertThat("incorrect token name", ht2.getTokenName(), + .withNullableTokenName(new TokenName("ugh")) + .withMFA(MFAStatus.NOT_USED) + .build(); + assertThat("incorrect token type", st.getTokenType(), is(TokenType.DEV)); + assertThat("incorrect token name", st.getTokenName(), is(Optional.of(new TokenName("ugh")))); - assertThat("incorrect token id", ht2.getId(), is(id2)); - assertThat("incorrect user", ht2.getUserName(), is(new UserName("whee2"))); - assertThat("incorrect creation date", ht2.getCreationDate(), + assertThat("incorrect token id", st.getId(), is(id)); + assertThat("incorrect user", st.getUserName(), is(new UserName("whee2"))); + assertThat("incorrect creation date", st.getCreationDate(), is(Instant.ofEpochMilli(27000))); - assertThat("incorrect expiration date", ht2.getExpirationDate(), + assertThat("incorrect expiration date", st.getExpirationDate(), is(Instant.ofEpochMilli(42000))); - assertThat("incorrect context", ht2.getContext(), + assertThat("incorrect context", st.getContext(), is(TokenCreationContext.getBuilder().build())); + assertThat("incorrect MFA", st.getMFA(), is(MFAStatus.NOT_USED)); } @Test public void storedTokenEmptyNullableName() throws Exception { - final UUID id2 = UUID.randomUUID(); - final StoredToken ht2 = StoredToken.getBuilder(TokenType.DEV, id2, new UserName("whee2")) + final UUID id = UUID.randomUUID(); + final StoredToken st = StoredToken.getBuilder(TokenType.DEV, id, new UserName("whee2")) .withLifeTime(Instant.ofEpochMilli(27000), Instant.ofEpochMilli(42000)) - .withNullableTokenName(null).build(); - assertThat("incorrect token type", ht2.getTokenType(), is(TokenType.DEV)); - assertThat("incorrect token name", ht2.getTokenName(), is(Optional.empty())); - assertThat("incorrect token id", ht2.getId(), is(id2)); - assertThat("incorrect user", ht2.getUserName(), is(new UserName("whee2"))); - assertThat("incorrect creation date", ht2.getCreationDate(), + .withNullableTokenName(null) + .withMFA(MFAStatus.UNKNOWN) + .build(); + assertThat("incorrect token type", st.getTokenType(), is(TokenType.DEV)); + assertThat("incorrect token name", st.getTokenName(), is(Optional.empty())); + assertThat("incorrect token id", st.getId(), is(id)); + assertThat("incorrect user", st.getUserName(), is(new UserName("whee2"))); + assertThat("incorrect creation date", st.getCreationDate(), is(Instant.ofEpochMilli(27000))); - assertThat("incorrect expiration date", ht2.getExpirationDate(), + assertThat("incorrect expiration date", st.getExpirationDate(), is(Instant.ofEpochMilli(42000))); - assertThat("incorrect context", ht2.getContext(), + assertThat("incorrect context", st.getContext(), is(TokenCreationContext.getBuilder().build())); + assertThat("incorrect MFA", st.getMFA(), is(MFAStatus.UNKNOWN)); } @Test @@ -255,21 +266,24 @@ public void storedTokenCreateFail() throws Exception { final Instant e = Instant.ofEpochMilli(2); final TokenName tn = new TokenName("ugh"); final TokenCreationContext ctx = TokenCreationContext.getBuilder().build(); - failCreateStoredToken(null, tn, id, u, c, e, ctx, new NullPointerException("type")); - failCreateStoredToken(TokenType.LOGIN, null, id, u, c, e, ctx, + final MFAStatus m = MFAStatus.UNKNOWN; + failCreateStoredToken(null, tn, id, u, c, e, ctx, m, new NullPointerException("type")); + failCreateStoredToken(TokenType.LOGIN, null, id, u, c, e, ctx, m, new NullPointerException("tokenName")); - failCreateStoredToken(TokenType.LOGIN, tn, null, u, c, e, ctx, + failCreateStoredToken(TokenType.LOGIN, tn, null, u, c, e, ctx, m, new NullPointerException("id")); - failCreateStoredToken(TokenType.LOGIN, tn, id, null, c, e, ctx, + failCreateStoredToken(TokenType.LOGIN, tn, id, null, c, e, ctx, m, new NullPointerException("userName")); - failCreateStoredToken(TokenType.LOGIN, tn, id, u, null, e, ctx, + failCreateStoredToken(TokenType.LOGIN, tn, id, u, null, e, ctx, m, new NullPointerException("created")); - failCreateStoredToken(TokenType.LOGIN, tn, id, u, c, null, ctx, + failCreateStoredToken(TokenType.LOGIN, tn, id, u, c, null, ctx, m, new NullPointerException("expires")); - failCreateStoredToken(TokenType.LOGIN, tn, id, u, e, c, ctx, + failCreateStoredToken(TokenType.LOGIN, tn, id, u, e, c, ctx, m, new IllegalArgumentException("expires must be > created")); - failCreateStoredToken(TokenType.LOGIN, tn, id, u, c, e, null, + failCreateStoredToken(TokenType.LOGIN, tn, id, u, c, e, null, m, new NullPointerException("context")); + failCreateStoredToken(TokenType.LOGIN, tn, id, u, c, e, ctx, null, + new NullPointerException("mfa")); } private void failCreateStoredToken( @@ -280,10 +294,15 @@ private void failCreateStoredToken( final Instant creationDate, final Instant expirationDate, final TokenCreationContext ctx, + final MFAStatus mfa, final Exception exception) { try { - StoredToken.getBuilder(type, id, userName).withLifeTime(creationDate, expirationDate) - .withTokenName(tokenName).withContext(ctx).build(); + StoredToken.getBuilder(type, id, userName) + .withLifeTime(creationDate, expirationDate) + .withTokenName(tokenName) + .withContext(ctx) + .withMFA(mfa) + .build(); fail("made bad hashed token"); } catch (Exception e) { TestCommon.assertExceptionCorrect(e, exception); From 2b7fc4b5b6140307238b7e58a286f6d25e120a61 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Mon, 17 Nov 2025 15:30:08 -0800 Subject: [PATCH 16/24] Store token MFA status in Mongo Based on code originally written by David Lyon here: https://github.com/kbase/auth2/pull/471 --- .../kbase/auth2/lib/storage/mongo/Fields.java | 2 + .../auth2/lib/storage/mongo/MongoStorage.java | 11 ++++- .../storage/mongo/MongoStorageTokensTest.java | 48 +++++++++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java b/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java index b05aba6d..b1ff8517 100644 --- a/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java +++ b/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java @@ -113,6 +113,8 @@ public class Fields { public static final String TOKEN_EXPIRY = "expires"; /** The ID of the token. */ public static final String TOKEN_ID = "id"; + /** The MFA status for the token. */ + public static final String TOKEN_MFA = "mfa"; /** The name of the token, if any. */ public static final String TOKEN_NAME = "name"; /** The date the token was created. */ diff --git a/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java b/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java index f2e520f6..c0d75ac1 100644 --- a/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java +++ b/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java @@ -95,6 +95,7 @@ import us.kbase.auth2.lib.storage.exceptions.AuthStorageException; import us.kbase.auth2.lib.storage.exceptions.StorageInitException; import us.kbase.auth2.lib.token.IncomingHashedToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; import us.kbase.auth2.lib.token.TokenType; @@ -871,7 +872,9 @@ private void storeToken(final String collection, final StoredToken token, final .append(Fields.TOKEN_DEVICE, ctx.getDevice().orElse(null)) .append(Fields.TOKEN_IP, ctx.getIpAddress().isPresent() ? ctx.getIpAddress().get().getHostAddress() : null) - .append(Fields.TOKEN_CUSTOM_CONTEXT, toCustomContextList(ctx.getCustomContext())); + .append(Fields.TOKEN_CUSTOM_CONTEXT, toCustomContextList(ctx.getCustomContext())) + .append(Fields.TOKEN_MFA, token.getMFA().getID()) + ; try { db.getCollection(collection).insertOne(td); } catch (MongoWriteException mwe) { @@ -959,6 +962,11 @@ private StoredToken getToken(final String collection, final IncomingHashedToken return htoken; } + private MFAStatus getMFA(final String mfa) { + // compatibility with auth versions 0.7.1 and earlier, which don't have the MFA field + return mfa == null ? MFAStatus.UNKNOWN : MFAStatus.fromID(mfa); + } + private StoredToken getToken(final Document t) throws AuthStorageException { return StoredToken.getBuilder( TokenType.getType(t.getString(Fields.TOKEN_TYPE)), @@ -969,6 +977,7 @@ private StoredToken getToken(final Document t) throws AuthStorageException { t.getDate(Fields.TOKEN_EXPIRY).toInstant()) .withNullableTokenName(getTokenName(t.getString(Fields.TOKEN_NAME))) .withContext(toTokenCreationContext(t)) + .withMFA(getMFA(t.getString(Fields.TOKEN_MFA))) .build(); } diff --git a/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTokensTest.java b/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTokensTest.java index 4bfc6268..5ec60de7 100644 --- a/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTokensTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTokensTest.java @@ -20,6 +20,7 @@ import us.kbase.auth2.lib.exceptions.NoSuchTokenException; import us.kbase.auth2.lib.token.IncomingHashedToken; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; import us.kbase.auth2.lib.token.TokenType; @@ -71,7 +72,9 @@ TokenType.LOGIN, id, new UserName("bar")) .withContext(TokenCreationContext.getBuilder() .withIpAddress(InetAddress.getByName("localhost")) .build()) - .withTokenName(new TokenName("foo")).build(); + .withTokenName(new TokenName("foo")) + .withMFA(MFAStatus.USED) + .build(); storage.storeToken(store, "nJKFR6Xc4vzCeI3jT+FjlC9k5Q/qVw0zd0gi1erL8ew="); final StoredToken expected = StoredToken.getBuilder( @@ -80,7 +83,9 @@ TokenType.LOGIN, id, new UserName("bar")) .withContext(TokenCreationContext.getBuilder() .withIpAddress(InetAddress.getByName("127.0.0.1")) .build()) - .withTokenName(new TokenName("foo")).build(); + .withTokenName(new TokenName("foo")) + .withMFA(MFAStatus.USED) + .build(); final StoredToken st = storage.getToken(new IncomingToken("sometoken").getHashedToken()); assertThat("incorrect token", st, is(expected)); } @@ -91,13 +96,16 @@ public void storeAndGetNoNameNoContext() throws Exception { final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); // mongo truncates final StoredToken ht = StoredToken.getBuilder( TokenType.LOGIN, id, new UserName("bar")) - .withLifeTime(now, now.plusSeconds(10)).build(); + .withLifeTime(now, now.plusSeconds(10)) + .withMFA(MFAStatus.UNKNOWN) + .build(); storage.storeToken(ht, "nJKFR6Xc4vzCeI3jT+FjlC9k5Q/qVw0zd0gi1erL8ew="); final StoredToken expected = StoredToken.getBuilder( TokenType.LOGIN, id, new UserName("bar")) - .withLifeTime(now, now.plusSeconds(10)).build(); + .withLifeTime(now, now.plusSeconds(10)) + .build(); final StoredToken st = storage.getToken(new IncomingToken("sometoken").getHashedToken()); assertThat("incorrect token", st, is(expected)); @@ -112,12 +120,42 @@ public void getWithNullCustomContext() throws Exception { final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); // mongo truncates final StoredToken ht = StoredToken.getBuilder( TokenType.LOGIN, id, new UserName("bar")) - .withLifeTime(now, now.plusSeconds(10)).build(); + .withLifeTime(now, now.plusSeconds(10)) + .withMFA(MFAStatus.NOT_USED) + .build(); storage.storeToken(ht, "nJKFR6Xc4vzCeI3jT+FjlC9k5Q/qVw0zd0gi1erL8ew="); db.getCollection("tokens").updateOne(new Document("id", id.toString()), new Document("$set", new Document("custctx", null))); + final StoredToken expected = StoredToken.getBuilder( + TokenType.LOGIN, id, new UserName("bar")) + .withLifeTime(now, now.plusSeconds(10)) + .withMFA(MFAStatus.NOT_USED) + .build(); + + final StoredToken st = storage.getToken(new IncomingToken("sometoken").getHashedToken()); + assertThat("incorrect token", st, is(expected)); + } + + @Test + public void getWithNullMfaBackwardsCompatibility() throws Exception { + /* + * Tests backwards compatibility with tokens created before the MFA field was added. + */ + final UUID id = UUID.randomUUID(); + final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); // mongo truncates + final StoredToken ht = StoredToken.getBuilder( + TokenType.LOGIN, id, new UserName("bar")) + .withLifeTime(now, now.plusSeconds(10)) + .withMFA(MFAStatus.USED) + .build(); + storage.storeToken(ht, "nJKFR6Xc4vzCeI3jT+FjlC9k5Q/qVw0zd0gi1erL8ew="); + + // Remove the MFA field to simulate old database records + db.getCollection("tokens").updateOne(new Document("id", id.toString()), + new Document("$unset", new Document("mfa", ""))); + final StoredToken expected = StoredToken.getBuilder( TokenType.LOGIN, id, new UserName("bar")) .withLifeTime(now, now.plusSeconds(10)).build(); From 3000b4ac4613edb2abc80c2dc05d6f3d3830b800 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 09:06:46 -0800 Subject: [PATCH 17/24] Make token type case insenstive when creating serv / dev tokens Also clarify MFAStatus.UNKNOWN docs --- .../us/kbase/auth2/lib/token/MFAStatus.java | 9 +- .../us/kbase/auth2/service/ui/Tokens.java | 6 +- .../test/auth2/service/ui/TokensTest.java | 113 +++++++++--------- 3 files changed, 70 insertions(+), 58 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java index d7f1dab4..3d4c7143 100644 --- a/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java +++ b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java @@ -9,11 +9,18 @@ public enum MFAStatus { /* first arg is ID, second arg is description. ID CANNOT change * since that field is stored in the DB. */ + /** User authenticated with MFA during token creation. */ USED ("Used", "MFA used"), + /** User explicitly chose not to use MFA when available. */ NOT_USED ("NotUsed", "MFA not used"), - /** MFA status unknown or not applicable to authentication method. */ + + /** MFA status catch all. Covers + * - source did not provide enough information to determine MFA status + * - source does not support MFA + * - MFA is not applicable to the data (e.g. token types other than Login) + */ UNKNOWN ("Unknown", "MFA status unknown"); private static final Map STATUS_MAP = new HashMap<>(); diff --git a/src/main/java/us/kbase/auth2/service/ui/Tokens.java b/src/main/java/us/kbase/auth2/service/ui/Tokens.java index 7508d712..baa6109e 100644 --- a/src/main/java/us/kbase/auth2/service/ui/Tokens.java +++ b/src/main/java/us/kbase/auth2/service/ui/Tokens.java @@ -60,7 +60,8 @@ @Path(UIPaths.TOKENS_ROOT) public class Tokens { - //TODO JAVADOC or swagger + // TODO JAVADOC or swagger + // TODO TEST unit tests @Inject private Authentication auth; @@ -196,7 +197,7 @@ public void revokeAll( private NewUIToken createtoken( final HttpServletRequest req, final String tokenName, - final String tokenType, + String tokenType, final IncomingToken userToken, final Map customContext) throws AuthStorageException, MissingParameterException, @@ -204,6 +205,7 @@ private NewUIToken createtoken( UnauthorizedException, IllegalParameterException { final TokenCreationContext tcc = getTokenContext( userAgentParser, req, isIgnoreIPsInHeaders(auth), customContext); + tokenType = tokenType == null ? null : tokenType.toLowerCase(); return new NewUIToken(auth.createToken(userToken, new TokenName(tokenName), Fields.TOKEN_SERVICE.equals(tokenType) ? TokenType.SERV : TokenType.DEV, tcc)); } diff --git a/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java b/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java index dbcb507a..ecf2a3b6 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java @@ -6,6 +6,7 @@ import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestHTML; import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestJSON; import static us.kbase.test.auth2.TestCommon.inst; +import static us.kbase.test.auth2.TestCommon.list; import static us.kbase.test.auth2.TestCommon.set; import java.net.InetAddress; @@ -421,62 +422,64 @@ public void createTokenMaximalInput() throws Exception { .build(), token.getHashedToken().getTokenHash()); - final URI target = UriBuilder.fromUri(host).path("/tokens").build(); - final WebTarget wt = CLI.target(target); - - final Builder req = wt.request() - .cookie(COOKIE_NAME, token.getToken()); - - final Form form = new Form(); - form.param("name", "foo"); - form.param("type", "service"); - form.param("customcontext", "foo, bar ; baz, bat"); - - final Response res = req.post(Entity.form(form)); - final String html = res.readEntity(String.class); - - assertThat("incorrect response code", res.getStatus(), is(200)); - - final String regex = String.format(TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName()), "whoo", "foo"); - - final Pattern p = Pattern.compile(regex); - - final Matcher m = p.matcher(html); - if (!m.matches()) { - fail("pattern did not match token page"); - } - final String id = m.group(1); - final String newtoken = m.group(2); - final long created = Long.parseLong(m.group(3)); - final long expires = Long.parseLong(m.group(4)); - - UUID.fromString(id); // ensures the id is a valid uuid - TestCommon.assertCloseToNow(created); - assertThat("incorrect expires", expires, is(created + 100_000_000L * 24 * 3600 * 1000L)); - - ServiceTestUtils.checkStoredToken(manager, newtoken, id, created, - ImmutableMap.of("foo", "bar", "baz", "bat"), - new UserName("whoo"), TokenType.SERV, "foo", 100_000_000L * 24 * 3600 * 1000L); + for (final String tokenType: list("Service", "service")) { + final URI target = UriBuilder.fromUri(host).path("/tokens").build(); + final WebTarget wt = CLI.target(target); + + final Builder req = wt.request() + .cookie(COOKIE_NAME, token.getToken()); - - final Builder req2 = wt.request() - .header("authorization", token.getToken()) - .header("accept", MediaType.APPLICATION_JSON); - - final Response jsonresp = req2.post(Entity.json(ImmutableMap.of( - "name", "foo", - "type", "service", - "customcontext", ImmutableMap.of("foo", "bar", "baz", "bat")))); - @SuppressWarnings("unchecked") - final Map json = jsonresp.readEntity(Map.class); - - assertThat("incorrect response code", res.getStatus(), is(200)); - - ServiceTestUtils.checkReturnedToken(manager, json, - ImmutableMap.of("foo", "bar", "baz", "bat"), - new UserName("whoo"), TokenType.SERV, "foo", - 100_000_000L * 24 * 3600 * 1000L, true); + final Form form = new Form(); + form.param("name", "foo"); + form.param("type", tokenType); + form.param("customcontext", "foo, bar ; baz, bat"); + + final Response res = req.post(Entity.form(form)); + final String html = res.readEntity(String.class); + + assertThat("incorrect response code", res.getStatus(), is(200)); + + final String regex = String.format(TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName()), "whoo", "foo"); + + final Pattern p = Pattern.compile(regex); + + final Matcher m = p.matcher(html); + if (!m.matches()) { + fail("pattern did not match token page"); + } + final String id = m.group(1); + final String newtoken = m.group(2); + final long created = Long.parseLong(m.group(3)); + final long expires = Long.parseLong(m.group(4)); + + UUID.fromString(id); // ensures the id is a valid uuid + TestCommon.assertCloseToNow(created); + assertThat("incorrect expires", expires, is(created + 100_000_000L * 24 * 3600 * 1000L)); + + ServiceTestUtils.checkStoredToken(manager, newtoken, id, created, + ImmutableMap.of("foo", "bar", "baz", "bat"), + new UserName("whoo"), TokenType.SERV, "foo", 100_000_000L * 24 * 3600 * 1000L); + + + final Builder req2 = wt.request() + .header("authorization", token.getToken()) + .header("accept", MediaType.APPLICATION_JSON); + + final Response jsonresp = req2.post(Entity.json(ImmutableMap.of( + "name", "foo", + "type", tokenType, + "customcontext", ImmutableMap.of("foo", "bar", "baz", "bat")))); + @SuppressWarnings("unchecked") + final Map json = jsonresp.readEntity(Map.class); + + assertThat("incorrect response code", res.getStatus(), is(200)); + + ServiceTestUtils.checkReturnedToken(manager, json, + ImmutableMap.of("foo", "bar", "baz", "bat"), + new UserName("whoo"), TokenType.SERV, "foo", + 100_000_000L * 24 * 3600 * 1000L, true); + } } @Test From 4b0e4d2a2524fb3b2221ad819921f240becc3a9b Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 09:54:35 -0800 Subject: [PATCH 18/24] Update MFA descriptions Will be exposed in the serivce API / UI --- src/main/java/us/kbase/auth2/lib/token/MFAStatus.java | 10 ++++++---- .../us/kbase/test/auth2/lib/token/MFAStatusTest.java | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java index 3d4c7143..d7487ec5 100644 --- a/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java +++ b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java @@ -7,21 +7,23 @@ public enum MFAStatus { /* first arg is ID, second arg is description. ID CANNOT change - * since that field is stored in the DB. + * since that field is stored in the DB. Description is exposed in the service API / UI. + * This allows for changing the variable name or API name without breaking the database + * records. */ /** User authenticated with MFA during token creation. */ - USED ("Used", "MFA used"), + USED ("Used", "Used"), /** User explicitly chose not to use MFA when available. */ - NOT_USED ("NotUsed", "MFA not used"), + NOT_USED ("NotUsed", "NotUsed"), /** MFA status catch all. Covers * - source did not provide enough information to determine MFA status * - source does not support MFA * - MFA is not applicable to the data (e.g. token types other than Login) */ - UNKNOWN ("Unknown", "MFA status unknown"); + UNKNOWN ("Unknown", "Unknown"); private static final Map STATUS_MAP = new HashMap<>(); static { diff --git a/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java b/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java index e1eb73c9..a3cba428 100644 --- a/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/token/MFAStatusTest.java @@ -19,11 +19,11 @@ public void testValues() { @Test public void testMFAStatusGetDescription() throws Exception { assertThat("incorrect Used description", MFAStatus.USED.getDescription(), - is("MFA used")); + is("Used")); assertThat("incorrect NotUsed description", MFAStatus.NOT_USED.getDescription(), - is("MFA not used")); + is("NotUsed")); assertThat("incorrect Unknown description", MFAStatus.UNKNOWN.getDescription(), - is("MFA status unknown")); + is("Unknown")); } @Test From d2a68a6c104d0e1e5e8bcd8a402b8bc7c2a717e0 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 13:07:38 -0800 Subject: [PATCH 19/24] Add MFA to TemporarySessionData LOGINIDENTS operation All the other changes in this PR flow out of that single change. eesh. TODO: test the UITokens for MFA status once it's exposed in the API layer. --- .../us/kbase/auth2/lib/Authentication.java | 50 +- .../kbase/auth2/lib/TemporarySessionData.java | 72 +- .../kbase/auth2/lib/storage/mongo/Fields.java | 2 + .../auth2/lib/storage/mongo/MongoStorage.java | 12 +- .../java/us/kbase/test/auth2/TestCommon.java | 4 + .../auth2/lib/AuthenticationLinkTest.java | 9 +- .../auth2/lib/AuthenticationLoginTest.java | 1062 ++++++++++------- .../auth2/lib/TemporarySessionDataTest.java | 58 +- .../MongoStorageTempSessionDataTest.java | 5 +- .../test/auth2/service/ServiceTestUtils.java | 16 +- .../auth2/service/api/TokenEndpointTest.java | 7 +- .../service/ui/LoginIntegrationTest.java | 164 +-- .../auth2/service/ui/SimpleEndpointsTest.java | 7 +- .../test/auth2/service/ui/TokensTest.java | 12 +- 14 files changed, 915 insertions(+), 565 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index 52d613f9..aeff80b8 100644 --- a/src/main/java/us/kbase/auth2/lib/Authentication.java +++ b/src/main/java/us/kbase/auth2/lib/Authentication.java @@ -92,6 +92,7 @@ import us.kbase.auth2.lib.user.LocalUser; import us.kbase.auth2.lib.user.NewUser; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; /** The main class for the Authentication application. * @@ -512,7 +513,7 @@ public LocalLoginResult localLogin( userName.getName()); return new LocalLoginResult(u.getUserName()); } - return new LocalLoginResult(login(u.getUserName(), tokenCtx)); + return new LocalLoginResult(login(u.getUserName(), tokenCtx, MFAStatus.UNKNOWN)); } private LocalUser getLocalUser(final UserName userName, final Password password) @@ -744,13 +745,15 @@ public void forceResetAllPasswords(final IncomingToken token) admin.getUserName().getName()); } - private NewToken login(final UserName userName, final TokenCreationContext tokenCtx) + private NewToken login( + final UserName userName, final TokenCreationContext tokenCtx, final MFAStatus mfa) throws AuthStorageException { final NewToken nt = new NewToken(StoredToken.getBuilder( TokenType.LOGIN, randGen.randomUUID(), userName) .withLifeTime(clock.instant(), cfg.getAppConfig().getTokenLifetimeMS(TokenLifetimeType.LOGIN)) .withContext(tokenCtx) + .withMFA(mfa) .build(), randGen.getToken()); storage.storeToken(nt.getStoredToken(), nt.getTokenHash()); @@ -1795,9 +1798,11 @@ public LoginToken login( // enough args here to start considering a builder final LoginState lstate = getLoginState(ipr.getIdentities(), Instant.MIN); final ProviderConfig pc = cfg.getAppConfig().getProviderConfig(idp.getProviderName()); final LoginToken loginToken; - if (lstate.getUsers().size() == 1 && - lstate.getIdentities().isEmpty() && - !pc.isForceLoginChoice()) { + if ( + lstate.getUsers().size() == 1 + && lstate.getIdentities().isEmpty() + && !pc.isForceLoginChoice()) + { final UserName userName = lstate.getUsers().iterator().next(); final AuthUser user = lstate.getUser(userName); /* Don't throw an error here since an auth UI may not be controlling the call - @@ -1811,16 +1816,16 @@ public LoginToken login( // enough args here to start considering a builder * so who cares. */ if (!cfg.getAppConfig().isLoginAllowed() && !Role.isAdmin(user.getRoles())) { - loginToken = storeIdentitiesTemporarily(lstate); + loginToken = storeIdentitiesTemporarily(lstate, ipr.getMFA()); } else if (user.isDisabled()) { - loginToken = storeIdentitiesTemporarily(lstate); + loginToken = storeIdentitiesTemporarily(lstate, ipr.getMFA()); } else { - loginToken = new LoginToken(login(user.getUserName(), tokenCtx)); + loginToken = new LoginToken(login(user.getUserName(), tokenCtx, ipr.getMFA())); } } else { // store the identities so the user can create an account or choose from more than one // account - loginToken = storeIdentitiesTemporarily(lstate); + loginToken = storeIdentitiesTemporarily(lstate, ipr.getMFA()); } return loginToken; } @@ -1834,13 +1839,13 @@ private void checkState(final TemporarySessionData tids, final String state) } // ignores expiration date of login state - private LoginToken storeIdentitiesTemporarily(final LoginState ls) + private LoginToken storeIdentitiesTemporarily(final LoginState ls, final MFAStatus mfa) throws AuthStorageException { final Set store = new HashSet<>(ls.getIdentities()); ls.getUsers().stream().forEach(u -> store.addAll(ls.getIdentities(u))); final TemporarySessionData data = TemporarySessionData.create( randGen.randomUUID(), clock.instant(), LOGIN_TOKEN_LIFETIME_MS) - .login(store); + .login(store, mfa); final TemporaryToken tt = storeTemporarySessionData(data); logInfo("Stored temporary token {} with {} login identities", tt.getId(), store.size()); return new LoginToken(tt); @@ -1871,6 +1876,7 @@ private TemporaryToken storeTemporarySessionData(final TemporarySessionData data public LoginState getLoginState(final IncomingToken token) throws AuthStorageException, InvalidTokenException, IdentityProviderErrorException, UnauthorizedException { + // TODO CODE this ignores the MFA state. May want to add it to LoginState in the future final TemporarySessionData ids = getTemporarySessionData( Optional.empty(), Operation.LOGINIDENTS, token); logInfo("Accessed temporary login token {} with {} identities", ids.getId(), @@ -1984,11 +1990,12 @@ public NewToken createUser( if (!cfg.getAppConfig().isLoginAllowed()) { throw new UnauthorizedException("Account creation is disabled"); } - // allow mutation of the identity set - final Set ids = new HashSet<>( - getTemporarySessionData(Optional.empty(), Operation.LOGINIDENTS, token) - .getIdentities().get()); + final TemporarySessionData tsd = getTemporarySessionData( + Optional.empty(), Operation.LOGINIDENTS, token + ); storage.deleteTemporarySessionData(token.getHashedToken()); + // allow mutation of the identity set + final Set ids = new HashSet<>(tsd.getIdentities().get()); final Optional match = getIdentity(identityID, ids); if (!match.isPresent()) { throw new UnauthorizedException(String.format( @@ -2020,7 +2027,7 @@ public NewToken createUser( linked, userName.getName()); } } - return login(userName, tokenCtx); + return login(userName, tokenCtx, tsd.getMFA().get()); } /** Create a test token. The token is entirely separate from standard tokens and is @@ -2308,11 +2315,12 @@ public NewToken login( requireNonNull(policyIDs, "policyIDs"); requireNonNull(tokenCtx, "tokenCtx"); noNulls(policyIDs, "null item in policyIDs"); - // allow mutation of the identity set - final Set ids = new HashSet<>( - getTemporarySessionData(Optional.empty(), Operation.LOGINIDENTS, token) - .getIdentities().get()); + final TemporarySessionData tsd = getTemporarySessionData( + Optional.empty(), Operation.LOGINIDENTS, token + ); storage.deleteTemporarySessionData(token.getHashedToken()); + // allow mutation of the identity set + final Set ids = new HashSet<>(tsd.getIdentities().get()); final Optional ri = getIdentity(identityID, ids); if (!ri.isPresent()) { throw new UnauthorizedException(String.format( @@ -2342,7 +2350,7 @@ public NewToken login( linked, u.get().getUserName().getName()); } } - return login(u.get().getUserName(), tokenCtx); + return login(u.get().getUserName(), tokenCtx, tsd.getMFA().get()); } private Optional getIdentity( diff --git a/src/main/java/us/kbase/auth2/lib/TemporarySessionData.java b/src/main/java/us/kbase/auth2/lib/TemporarySessionData.java index a99d384d..f7bb6790 100644 --- a/src/main/java/us/kbase/auth2/lib/TemporarySessionData.java +++ b/src/main/java/us/kbase/auth2/lib/TemporarySessionData.java @@ -15,6 +15,7 @@ import us.kbase.auth2.lib.exceptions.ErrorType; import us.kbase.auth2.lib.identity.RemoteIdentity; +import us.kbase.auth2.lib.token.MFAStatus; /** Temporary session data that may include a set of temporary identities and / or an associated * user, or an error that was stored instead of the identities. @@ -33,6 +34,7 @@ public class TemporarySessionData { private final String error; private final ErrorType errorType; private final UserName user; + private final MFAStatus mfa; private TemporarySessionData( final Operation op, @@ -44,7 +46,9 @@ private TemporarySessionData( final Set identities, final UserName user, final String error, - final ErrorType errorType) { + final ErrorType errorType, + final MFAStatus mfa + ) { this.op = op; this.id = id; this.created = created; @@ -55,6 +59,7 @@ private TemporarySessionData( this.user = user; this.error = error; this.errorType = errorType; + this.mfa = mfa; } /** Get the operation this temporary session data supports. @@ -77,6 +82,13 @@ public UUID getId() { public Optional> getIdentities() { return Optional.ofNullable(identities); } + + /** Get the MFA status, if any. + * @return the MFA status. + */ + public Optional getMFA() { + return Optional.ofNullable(mfa); + } /** Get the date of creation for the session data. * @return the creation date. @@ -139,27 +151,31 @@ public boolean hasError() { @Override public int hashCode() { - return Objects.hash(created, error, errorType, expires, id, identities, oauth2State, op, pkceCodeVerifier, - user); + return Objects.hash(created, error, errorType, expires, id, identities, mfa, oauth2State, + op, pkceCodeVerifier, user + ); } @Override public boolean equals(Object obj) { - if (this == obj) { + if (this == obj) return true; - } - if (obj == null) { + if (obj == null) return false; - } - if (getClass() != obj.getClass()) { + if (getClass() != obj.getClass()) return false; - } TemporarySessionData other = (TemporarySessionData) obj; - return Objects.equals(created, other.created) && Objects.equals(error, other.error) - && errorType == other.errorType && Objects.equals(expires, other.expires) - && Objects.equals(id, other.id) && Objects.equals(identities, other.identities) - && Objects.equals(oauth2State, other.oauth2State) && op == other.op - && Objects.equals(pkceCodeVerifier, other.pkceCodeVerifier) && Objects.equals(user, other.user); + return Objects.equals(created, other.created) + && Objects.equals(error, other.error) + && errorType == other.errorType + && Objects.equals(expires, other.expires) + && Objects.equals(id, other.id) + && Objects.equals(identities, other.identities) + && mfa == other.mfa + && Objects.equals(oauth2State, other.oauth2State) + && op == other.op + && Objects.equals(pkceCodeVerifier, other.pkceCodeVerifier) + && Objects.equals(user, other.user); } /** The operation this session data is associated with. @@ -242,7 +258,7 @@ public TemporarySessionData error(final String error, final ErrorType errorType) requireNonNull(errorType, "errorType"); return new TemporarySessionData( Operation.ERROR, id, created, expires, - null, null, null, null, error, errorType); + null, null, null, null, error, errorType, null); } /** Create temporary session data for the start of a login operation. @@ -258,18 +274,32 @@ public TemporarySessionData login( checkStringNoCheckedException(pkceCodeVerifier, "pkceCodeVerifier"); return new TemporarySessionData( Operation.LOGINSTART, id, created, expires, - oauth2State, pkceCodeVerifier, null, null, null, null); + oauth2State, pkceCodeVerifier, null, null, null, null, null); } /** Create temporary session data for a login operation where remote identities are * involved. * @param identities the remote identities involved in the login. + * @param mfa the MFA state from the login. * @return the temporary session data. */ - public TemporarySessionData login(final Set identities) { + public TemporarySessionData login( + final Set identities, + final MFAStatus mfa + ) { return new TemporarySessionData( - Operation.LOGINIDENTS, id, created, expires, - null, null, checkIdents(identities), null, null, null); + Operation.LOGINIDENTS, + id, + created, + expires, + null, + null, + checkIdents(identities), + null, + null, + null, + requireNonNull(mfa, "mfa") + ); } private Set checkIdents(final Set identities) { @@ -299,7 +329,7 @@ public TemporarySessionData link( requireNonNull(userName, "userName"); return new TemporarySessionData( Operation.LINKSTART, id, created, expires, - oauth2State, pkceCodeVerifier, null, userName, null, null); + oauth2State, pkceCodeVerifier, null, userName, null, null, null); } /** Create temporary session data for a linking operation when remote identities are @@ -314,7 +344,7 @@ public TemporarySessionData link( requireNonNull(userName, "userName"); return new TemporarySessionData( Operation.LINKIDENTS, id, created, expires, - null, null, checkIdents(identities), userName, null, null); + null, null, checkIdents(identities), userName, null, null, null); } } } diff --git a/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java b/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java index b1ff8517..5c8ebe08 100644 --- a/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java +++ b/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java @@ -161,6 +161,8 @@ public class Fields { public static final String TEMP_SESSION_USER = "user"; /** The remote identities associated with the temporary token. */ public static final String TEMP_SESSION_IDENTITIES = "idents"; + /** The MFA status associated with the temporary token. */ + public static final String TEMP_SESSION_MFA = "mfa"; /** The error associated with the temporary token. */ public static final String TEMP_SESSION_ERROR = "err"; /** The type of the error associated with the temporary token. */ diff --git a/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java b/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java index c0d75ac1..72c6d2d7 100644 --- a/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java +++ b/src/main/java/us/kbase/auth2/lib/storage/mongo/MongoStorage.java @@ -1698,13 +1698,14 @@ public void storeTemporarySessionData(final TemporarySessionData data, final Str .append(Fields.TEMP_SESSION_OAUTH2STATE, data.getOAuth2State().orElse(null)) .append(Fields.TEMP_SESSION_PKCE_CODE_VERIFIER, data.getPKCECodeVerifier().orElse(null)) - .append(Fields.TEMP_SESSION_ERROR, - data.getError().isPresent() ? data.getError().get() : null) + .append(Fields.TEMP_SESSION_ERROR, data.getError().orElse(null)) .append(Fields.TEMP_SESSION_ERROR_TYPE, data.getErrorType().isPresent() ? data.getErrorType().get().getErrorCode() : null) .append(Fields.TEMP_SESSION_IDENTITIES, ids) - .append(Fields.TEMP_SESSION_USER, - data.getUser().isPresent() ? data.getUser().get().getName() : null); + .append(Fields.TEMP_SESSION_USER, data.getUser().isPresent() ? + data.getUser().get().getName() : null) + .append(Fields.TEMP_SESSION_MFA, data.getMFA().isPresent() ? + data.getMFA().get().getID() : null); storeTemporarySessionData(td); } @@ -1772,7 +1773,8 @@ public TemporarySessionData getTemporarySessionData( d.getString(Fields.TEMP_SESSION_PKCE_CODE_VERIFIER) ); } else if (op.equals(Operation.LOGINIDENTS)) { - tis = b.login(toIdentities(ids)); + final MFAStatus mfa = MFAStatus.fromID(d.getString(Fields.TEMP_SESSION_MFA)); + tis = b.login(toIdentities(ids), mfa); } else if (op.equals(Operation.LINKSTART)) { tis = b.link( d.getString(Fields.TEMP_SESSION_OAUTH2STATE), diff --git a/src/test/java/us/kbase/test/auth2/TestCommon.java b/src/test/java/us/kbase/test/auth2/TestCommon.java index f98b0db0..49128ecf 100644 --- a/src/test/java/us/kbase/test/auth2/TestCommon.java +++ b/src/test/java/us/kbase/test/auth2/TestCommon.java @@ -169,6 +169,10 @@ public static List list(T... objects) { public static final Optional ES = Optional.empty(); + public static Optional opt() { + return Optional.empty(); + } + public static Optional opt(final T obj) { return Optional.of(obj); } diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java index 2053bc4f..9817d098 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java @@ -72,6 +72,7 @@ import us.kbase.auth2.lib.storage.AuthStorage; import us.kbase.auth2.lib.storage.exceptions.AuthStorageException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TemporaryToken; import us.kbase.auth2.lib.token.TokenType; @@ -885,7 +886,7 @@ public void linkWithTokenFailBadTokenOp() throws Exception { final UUID tid = UUID.randomUUID(); when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( TemporarySessionData.create(tid, Instant.now(), Instant.now()) - .login(set(REMOTE))) + .login(set(REMOTE), MFAStatus.UNKNOWN)) .thenReturn(null); failLinkWithToken(auth, token, "prov", "foo", null, "state", new InvalidTokenException( @@ -1573,7 +1574,7 @@ public void getLinkStateFailBadTokenOp() throws Exception { when(storage.getTemporarySessionData(tempToken.getHashedToken())).thenReturn( TemporarySessionData.create(tempTokenID, NOW, NOW) - .login(set(REMOTE))) + .login(set(REMOTE), MFAStatus.UNKNOWN)) .thenReturn(null); failGetLinkState(auth, userToken, tempToken, new InvalidTokenException( @@ -1867,7 +1868,7 @@ public void linkIdentityFailBadTokenOp() throws Exception { final UUID id = UUID.randomUUID(); when(storage.getTemporarySessionData(tempToken.getHashedToken())).thenReturn( TemporarySessionData.create(id, NOW, NOW) - .login(set(REMOTE))) + .login(set(REMOTE), MFAStatus.UNKNOWN)) .thenReturn(null); failLinkIdentity(auth, userToken, tempToken, "fakeid", new InvalidTokenException( @@ -2384,7 +2385,7 @@ public void linkAllFailLinkFailBadTokenOp() throws Exception { final UUID id = UUID.randomUUID(); when(storage.getTemporarySessionData(tempToken.getHashedToken())).thenReturn( TemporarySessionData.create(id, NOW, NOW) - .login(set(REMOTE))) + .login(set(REMOTE), MFAStatus.UNKNOWN)) .thenReturn(null); failLinkAll(auth, userToken, tempToken, new InvalidTokenException( diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java index baff074b..eb3f4bc1 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLoginTest.java @@ -81,6 +81,7 @@ import us.kbase.auth2.lib.storage.AuthStorage; import us.kbase.auth2.lib.storage.exceptions.AuthStorageException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.NewToken; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenType; @@ -90,6 +91,8 @@ import us.kbase.test.auth2.lib.AuthenticationTester.LogEvent; import us.kbase.test.auth2.lib.AuthenticationTester.TestMocks; +// TODO CODE it seems like there's a lot of repetition in these tests, maybe could consolidate + public class AuthenticationLoginTest { private static final Instant SMALL = Instant.ofEpochMilli(1); @@ -242,93 +245,106 @@ private void loginContinueImmediately( final Role userRole, final boolean allowLogin) throws Exception { - logEvents.clear(); - - final IdentityProvider idp = mock(IdentityProvider.class); - - when(idp.getProviderName()).thenReturn("prov"); - - final TestMocks testauth = initTestMocks(set(idp)); - final AuthStorage storage = testauth.storageMock; - final RandomDataGenerator rand = testauth.randGenMock; - final Clock clock = testauth.clockMock; - final Authentication auth = testauth.auth; - - AuthenticationTester.setConfigUpdateInterval(auth, -1); - - final Map providers = ImmutableMap.of( - "prov", new ProviderConfig(true, false, false)); - - when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) - .thenReturn(new AuthConfigSet( - new AuthConfig(allowLogin, providers, null), - new CollectingExternalConfig(Collections.emptyMap()))); - - final IncomingToken token = new IncomingToken("inctoken"); - - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), now(), now().plusSeconds(10)) - .login("suporstate", "pkceughherewegoagain")); - - when(idp.getIdentities("foobar", "pkceughherewegoagain", false, null)) - .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( - new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")))) - .thenReturn(null); - - final RemoteIdentity storageRemoteID = new RemoteIdentity( - new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")); - - final AuthUser user = AuthUser.getBuilder(new UserName("foo"), UID, new DisplayName("bar"), - Instant.ofEpochMilli(10000L)) - .withRole(userRole) - .withIdentity(storageRemoteID).build(); - - when(storage.getUser(storageRemoteID)).thenReturn(Optional.of(user)).thenReturn(null); - - final UUID tokenID = UUID.randomUUID(); - - when(rand.randomUUID()).thenReturn(tokenID).thenReturn(null); - when(rand.getToken()).thenReturn("thisisatoken").thenReturn(null); - when(clock.instant()).thenReturn(Instant.ofEpochMilli(20000)) - .thenReturn(Instant.ofEpochMilli(30000)).thenReturn(null); - - final LoginToken lt = auth.login( - token, - "prov", - "foobar", - null, - TokenCreationContext.getBuilder().withNullableAgent("a", "v").build(), - "suporstate"); - - verify(storage).deleteTemporarySessionData(token.getHashedToken()); - - verify(storage).storeToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) - .withContext(TokenCreationContext.getBuilder() - .withNullableAgent("a", "v").build()).build(), - "rIWdQ6H23g7MLjLjJTz8k7A6zEbn6+Cnwm5anDwasLc="); - - verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(30000)); - - final LoginToken expected = new LoginToken( - new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) - .withContext(TokenCreationContext.getBuilder() - .withNullableAgent("a", "v").build()).build(), - "thisisatoken")); - - assertThat("incorrect login token", lt, is(expected)); - - assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, - "Logged in user foo with token " + tokenID, Authentication.class)); + for (final MFAStatus mfa: MFAStatus.values()) { + logEvents.clear(); + + final IdentityProvider idp = mock(IdentityProvider.class); + + when(idp.getProviderName()).thenReturn("prov"); + + final TestMocks testauth = initTestMocks(set(idp)); + final AuthStorage storage = testauth.storageMock; + final RandomDataGenerator rand = testauth.randGenMock; + final Clock clock = testauth.clockMock; + final Authentication auth = testauth.auth; + + AuthenticationTester.setConfigUpdateInterval(auth, -1); + + final Map providers = ImmutableMap.of( + "prov", new ProviderConfig(true, false, false)); + + when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) + .thenReturn(new AuthConfigSet( + new AuthConfig(allowLogin, providers, null), + new CollectingExternalConfig(Collections.emptyMap()))); + + final IncomingToken token = new IncomingToken("inctoken"); + + when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( + TemporarySessionData.create(UUID.randomUUID(), now(), now().plusSeconds(10)) + .login("suporstate", "pkceughherewegoagain")); + + when(idp.getIdentities("foobar", "pkceughherewegoagain", false, null)) + .thenReturn(IdentityProviderResponse.from( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + mfa + )) + .thenReturn(null); + + final RemoteIdentity storageRemoteID = new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com")); + + final AuthUser user = AuthUser.getBuilder( + new UserName("foo"), UID, new DisplayName("bar"), + Instant.ofEpochMilli(10000L)) + .withRole(userRole) + .withIdentity(storageRemoteID).build(); + + when(storage.getUser(storageRemoteID)).thenReturn(Optional.of(user)).thenReturn(null); + + final UUID tokenID = UUID.randomUUID(); + + when(rand.randomUUID()).thenReturn(tokenID).thenReturn(null); + when(rand.getToken()).thenReturn("thisisatoken").thenReturn(null); + when(clock.instant()).thenReturn(Instant.ofEpochMilli(20000)) + .thenReturn(Instant.ofEpochMilli(30000)).thenReturn(null); + + final LoginToken lt = auth.login( + token, + "prov", + "foobar", + null, + TokenCreationContext.getBuilder().withNullableAgent("a", "v").build(), + "suporstate"); + + verify(storage).deleteTemporarySessionData(token.getHashedToken()); + + verify(storage).storeToken(StoredToken.getBuilder( + TokenType.LOGIN, tokenID, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) + .withContext(TokenCreationContext.getBuilder() + .withNullableAgent("a", "v").build()) + .withMFA(mfa) + .build(), + "rIWdQ6H23g7MLjLjJTz8k7A6zEbn6+Cnwm5anDwasLc="); + + verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(30000)); + + final LoginToken expected = new LoginToken( + new NewToken(StoredToken.getBuilder( + TokenType.LOGIN, tokenID, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) + .withContext(TokenCreationContext.getBuilder() + .withNullableAgent("a", "v").build()) + .withMFA(mfa) + .build(), + "thisisatoken")); + + assertThat("incorrect login token", lt, is(expected)); + + assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, + "Logged in user foo with token " + tokenID, Authentication.class)); + } } @Test public void loginContinueStoreSingleIdentity() throws Exception { + // this covers all the paths through the login continue method other than immediate + // login, so we test all MFA types here loginContinueStoreSingleLinkedIdentity(Role.DEV_TOKEN, false, false, false); loginContinueStoreSingleLinkedIdentity(Role.DEV_TOKEN, true, true, false); loginContinueStoreSingleLinkedIdentity(Role.ADMIN, true, false, false); @@ -344,78 +360,85 @@ private void loginContinueStoreSingleLinkedIdentity( final boolean allowLogin, final boolean forceLoginChoice) throws Exception { - logEvents.clear(); - - final IdentityProvider idp = mock(IdentityProvider.class); - - when(idp.getProviderName()).thenReturn("prov"); - - final TestMocks testauth = initTestMocks(set(idp)); - final AuthStorage storage = testauth.storageMock; - final RandomDataGenerator rand = testauth.randGenMock; - final Clock clock = testauth.clockMock; - final Authentication auth = testauth.auth; - - AuthenticationTester.setConfigUpdateInterval(auth, -1); - - final Map providers = ImmutableMap.of( - "prov", new ProviderConfig(true, forceLoginChoice, false)); - - when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) - .thenReturn(new AuthConfigSet( - new AuthConfig(allowLogin, providers, null), - new CollectingExternalConfig(Collections.emptyMap()))); - - final IncomingToken token = new IncomingToken("inctoken"); - - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), now(), now().plusSeconds(10)) - .login("suporstate2", "pkceisathingiguess")); - - when(idp.getIdentities("foobar", "pkceisathingiguess", false, null)) - .thenReturn(IdentityProviderResponse.from(new RemoteIdentity( - new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")))) + for (final MFAStatus mfa: MFAStatus.values()) { + logEvents.clear(); + + final IdentityProvider idp = mock(IdentityProvider.class); + + when(idp.getProviderName()).thenReturn("prov"); + + final TestMocks testauth = initTestMocks(set(idp)); + final AuthStorage storage = testauth.storageMock; + final RandomDataGenerator rand = testauth.randGenMock; + final Clock clock = testauth.clockMock; + final Authentication auth = testauth.auth; + + AuthenticationTester.setConfigUpdateInterval(auth, -1); + + final Map providers = ImmutableMap.of( + "prov", new ProviderConfig(true, forceLoginChoice, false)); + + when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) + .thenReturn(new AuthConfigSet( + new AuthConfig(allowLogin, providers, null), + new CollectingExternalConfig(Collections.emptyMap()))); + + final IncomingToken token = new IncomingToken("inctoken"); + + when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( + TemporarySessionData.create(UUID.randomUUID(), now(), now().plusSeconds(10)) + .login("suporstate2", "pkceisathingiguess")); + + when(idp.getIdentities("foobar", "pkceisathingiguess", false, null)) + .thenReturn(IdentityProviderResponse.from( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + mfa + )) + .thenReturn(null); + + final RemoteIdentity storageRemoteID = new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com")); + + final AuthUser.Builder user = AuthUser.getBuilder(new UserName("foo"), UID, + new DisplayName("bar"), Instant.ofEpochMilli(10000L)) + .withRole(userRole) + .withIdentity(storageRemoteID); + if (disabled) { + user.withUserDisabledState(new UserDisabledState( + "d", new UserName("baz"), Instant.ofEpochMilli(5000))); + } + when(storage.getUser(storageRemoteID)).thenReturn(Optional.of(user.build())) + .thenReturn(null); + + final UUID tokenID = UUID.randomUUID(); + + when(rand.randomUUID()).thenReturn(tokenID).thenReturn(null); + when(rand.getToken()).thenReturn("thisisatoken").thenReturn(null); + when(clock.instant()).thenReturn(Instant.ofEpochMilli(20000)) .thenReturn(null); - - final RemoteIdentity storageRemoteID = new RemoteIdentity( - new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")); - - final AuthUser.Builder user = AuthUser.getBuilder(new UserName("foo"), UID, - new DisplayName("bar"), Instant.ofEpochMilli(10000L)) - .withRole(userRole) - .withIdentity(storageRemoteID); - if (disabled) { - user.withUserDisabledState(new UserDisabledState( - "d", new UserName("baz"), Instant.ofEpochMilli(5000))); + + final LoginToken lt = auth.login(token, "prov", "foobar", null, CTX, "suporstate2"); + + verify(storage).deleteTemporarySessionData(token.getHashedToken()); + + verify(storage).storeTemporarySessionData(TemporarySessionData + .create(tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000) + .login(set(storageRemoteID), mfa), + IncomingToken.hash("thisisatoken")); + + final LoginToken expected = new LoginToken(tempToken( + tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000, "thisisatoken")); + + assertThat("incorrect login token", lt, is(expected)); + + assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, String.format( + "Stored temporary token %s with 1 login identities", tokenID), + Authentication.class)); } - when(storage.getUser(storageRemoteID)).thenReturn(Optional.of(user.build())) - .thenReturn(null); - - final UUID tokenID = UUID.randomUUID(); - - when(rand.randomUUID()).thenReturn(tokenID).thenReturn(null); - when(rand.getToken()).thenReturn("thisisatoken").thenReturn(null); - when(clock.instant()).thenReturn(Instant.ofEpochMilli(20000)) - .thenReturn(null); - - final LoginToken lt = auth.login(token, "prov", "foobar", null, CTX, "suporstate2"); - - verify(storage).deleteTemporarySessionData(token.getHashedToken()); - - verify(storage).storeTemporarySessionData(TemporarySessionData.create( - tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000).login(set(storageRemoteID)), - IncomingToken.hash("thisisatoken")); - - final LoginToken expected = new LoginToken(tempToken( - tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000, "thisisatoken")); - - assertThat("incorrect login token", lt, is(expected)); - - assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, String.format( - "Stored temporary token %s with 1 login identities", tokenID), - Authentication.class)); } @Test @@ -471,8 +494,9 @@ public void loginContinueStoreUnlinkedIdentityWithEnvironment() throws Exception verify(storage).deleteTemporarySessionData(token.getHashedToken()); - verify(storage).storeTemporarySessionData(TemporarySessionData.create( - tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000).login(set(storageRemoteID)), + verify(storage).storeTemporarySessionData(TemporarySessionData + .create(tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000) + .login(set(storageRemoteID), MFAStatus.UNKNOWN), IncomingToken.hash("thisisatoken")); final LoginToken expected = new LoginToken(tempToken( @@ -553,7 +577,7 @@ public void loginContinueStoreLinkedAndUnlinkedIdentity() throws Exception { verify(storage).storeTemporarySessionData(TemporarySessionData.create( tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000) - .login(set(storageRemoteID1, storageRemoteID2)), + .login(set(storageRemoteID1, storageRemoteID2), MFAStatus.UNKNOWN), IncomingToken.hash("thisisatoken")); final LoginToken expected = new LoginToken(tempToken( @@ -649,7 +673,10 @@ public void loginContinueStoreMultipleLinkedIdentities() throws Exception { verify(storage).storeTemporarySessionData(TemporarySessionData.create( tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000) - .login(set(storageRemoteID1, storageRemoteID2, storageRemoteID3)), + .login( + set(storageRemoteID1, storageRemoteID2, storageRemoteID3), + MFAStatus.UNKNOWN + ), IncomingToken.hash("thisisatoken")); final LoginToken expected = new LoginToken(tempToken( @@ -734,7 +761,10 @@ public void loginContinueStoreMultipleUnLinkedIdentities() throws Exception { verify(storage).storeTemporarySessionData(TemporarySessionData.create( tokenID, Instant.ofEpochMilli(20000), 30 * 60 * 1000) - .login(set(storageRemoteID1, storageRemoteID2, storageRemoteID3)), + .login( + set(storageRemoteID1, storageRemoteID2, storageRemoteID3), + MFAStatus.UNKNOWN + ), IncomingToken.hash("thisisatoken")); final LoginToken expected = new LoginToken(tempToken( @@ -829,7 +859,7 @@ public void loginContinueFailBadTokenOp() throws Exception { final UUID tid = UUID.randomUUID(); when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( TemporarySessionData.create(tid, Instant.now(), Instant.now()) - .login(set(REMOTE))) + .login(set(REMOTE), MFAStatus.UNKNOWN)) .thenReturn(null); failLoginContinue(auth, token, "ip2", "foo", null, CTX, "state", @@ -1035,8 +1065,13 @@ public void getLoginStateOneUnlinkedID() throws Exception { final UUID id = UUID.randomUUID(); when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( TemporarySessionData.create(id, SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + .login( + set(new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + )), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) @@ -1073,13 +1108,21 @@ public void getLoginStateTwoUnlinkedIDsAndNoLoginAllowed() throws Exception { final IncomingToken token = new IncomingToken("foobar"); final UUID id = UUID.randomUUID(); - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(id, SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) - .thenReturn(null); + final TemporarySessionData tsd = TemporarySessionData.create(id, SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + ); + when(storage.getTemporarySessionData(token.getHashedToken())) + .thenReturn(tsd).thenReturn(null); when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) .thenReturn(new AuthConfigSet( @@ -1124,8 +1167,13 @@ public void getLoginStateOneLinkedID() throws Exception { when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( TemporarySessionData.create(id, SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + .login( + set(new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + )), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) @@ -1170,13 +1218,21 @@ public void getLoginStateTwoLinkedIDs() throws Exception { final IncomingToken token = new IncomingToken("foobar"); final UUID id = UUID.randomUUID(); - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(id, SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) - .thenReturn(null); + final TemporarySessionData tsd = TemporarySessionData.create(id, SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + ); + when(storage.getTemporarySessionData(token.getHashedToken())) + .thenReturn(tsd).thenReturn(null); when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) .thenReturn(new AuthConfigSet( @@ -1240,13 +1296,21 @@ public void getLoginStateOneLinkedOneUnlinkedID() throws Exception { final IncomingToken token = new IncomingToken("foobar"); final UUID id = UUID.randomUUID(); - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(id, SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) - .thenReturn(null); + final TemporarySessionData tsd = TemporarySessionData.create(id, SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + ); + when(storage.getTemporarySessionData(token.getHashedToken())) + .thenReturn(tsd).thenReturn(null); when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) .thenReturn(new AuthConfigSet( @@ -1384,72 +1448,93 @@ private void failGetLoginState( @Test public void createUser() throws Exception { - final TestMocks testauth = initTestMocks(); - final AuthStorage storage = testauth.storageMock; - final RandomDataGenerator rand = testauth.randGenMock; - final Clock clock = testauth.clockMock; - final Authentication auth = testauth.auth; - - AuthenticationTester.setConfigUpdateInterval(auth, -1); - - final IncomingToken token = new IncomingToken("foobar"); - final UUID tokenID = UUID.randomUUID(); - - when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) - .thenReturn(new AuthConfigSet( - new AuthConfig(true, null, null), - new CollectingExternalConfig(Collections.emptyMap()))); - - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) - .thenReturn(null); - - when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L), - Instant.ofEpochMilli(20000L), Instant.ofEpochMilli(30000L), null); - when(rand.randomUUID()).thenReturn(UID).thenReturn(tokenID).thenReturn(null); - when(rand.getToken()).thenReturn("mfingtoken"); - - final NewToken nt = auth.createUser(token, "ef0518c79af70ed979907969c6d0a0f7", - new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), - set(new PolicyID("pid1"), new PolicyID("pid2")), - TokenCreationContext.getBuilder().withNullableDevice("d").build(), false); - - verify(storage).createUser(NewUser.getBuilder( - new NewUserName("foo"), UID, new DisplayName("bar"), Instant.ofEpochMilli(10000), - new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))) - .withEmailAddress(new EmailAddress("f@h.com")) - .withPolicyID(new PolicyID("pid1"), Instant.ofEpochMilli(10000)) - .withPolicyID(new PolicyID("pid2"), Instant.ofEpochMilli(10000)).build()); - - verify(storage, never()).link(any(), any()); - - verify(storage).storeToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new NewUserName("foo")) - .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) - .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) - .build(), - "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); - - verify(storage).setLastLogin(new NewUserName("foo"), Instant.ofEpochMilli(30000)); - verify(storage).deleteTemporarySessionData(token.getHashedToken()); - - assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new NewUserName("foo")) - .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) - .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) - .build(), - "mfingtoken"))); - - assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, - "Created user foo linked to remote identity " + - "ef0518c79af70ed979907969c6d0a0f7 prov id1 user1", Authentication.class), - new LogEvent(Level.INFO, "Logged in user foo with token " + tokenID, - Authentication.class)); + // There's only one happy path through createUser wrt MFA so we just test here + for (final MFAStatus mfa: MFAStatus.values()) { + logEvents.clear(); + final TestMocks testauth = initTestMocks(); + final AuthStorage storage = testauth.storageMock; + final RandomDataGenerator rand = testauth.randGenMock; + final Clock clock = testauth.clockMock; + final Authentication auth = testauth.auth; + + AuthenticationTester.setConfigUpdateInterval(auth, -1); + + final IncomingToken token = new IncomingToken("foobar"); + final UUID tokenID = UUID.randomUUID(); + + when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) + .thenReturn(new AuthConfigSet( + new AuthConfig(true, null, null), + new CollectingExternalConfig(Collections.emptyMap()))); + + final TemporarySessionData tsd = TemporarySessionData.create( + UUID.randomUUID(), SMALL, 10000) + .login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + mfa + ); + when(storage.getTemporarySessionData(token.getHashedToken())) + .thenReturn(tsd).thenReturn(null); + + when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L), + Instant.ofEpochMilli(20000L), Instant.ofEpochMilli(30000L), null); + when(rand.randomUUID()).thenReturn(UID).thenReturn(tokenID).thenReturn(null); + when(rand.getToken()).thenReturn("mfingtoken"); + + final NewToken nt = auth.createUser(token, "ef0518c79af70ed979907969c6d0a0f7", + new NewUserName("foo"), new DisplayName("bar"), new EmailAddress("f@h.com"), + set(new PolicyID("pid1"), new PolicyID("pid2")), + TokenCreationContext.getBuilder().withNullableDevice("d").build(), false); + + verify(storage).createUser(NewUser.getBuilder( + new NewUserName("foo"), + UID, + new DisplayName("bar"), + Instant.ofEpochMilli(10000), + new RemoteIdentity(new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ) + .withEmailAddress(new EmailAddress("f@h.com")) + .withPolicyID(new PolicyID("pid1"), Instant.ofEpochMilli(10000)) + .withPolicyID(new PolicyID("pid2"), Instant.ofEpochMilli(10000)).build()); + + verify(storage, never()).link(any(), any()); + + verify(storage).storeToken(StoredToken.getBuilder( + TokenType.LOGIN, tokenID, new NewUserName("foo")) + .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) + .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) + .withMFA(mfa) + .build(), + "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); + + verify(storage).setLastLogin(new NewUserName("foo"), Instant.ofEpochMilli(30000)); + verify(storage).deleteTemporarySessionData(token.getHashedToken()); + + assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( + TokenType.LOGIN, tokenID, new NewUserName("foo")) + .withLifeTime(Instant.ofEpochMilli(20000), 14 * 24 * 3600 * 1000) + .withContext(TokenCreationContext.getBuilder().withNullableDevice("d").build()) + .withMFA(mfa) + .build(), + "mfingtoken"))); + + assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, + "Created user foo linked to remote identity " + + "ef0518c79af70ed979907969c6d0a0f7 prov id1 user1", Authentication.class), + new LogEvent(Level.INFO, "Logged in user foo with token " + tokenID, + Authentication.class)); + } } @Test @@ -1473,10 +1558,14 @@ public void createUserAlternateTokenLifeTimeAndEmptyLinks() throws Exception { new CollectingExternalConfig(Collections.emptyMap()))); when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) - .thenReturn(null); + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set(new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + )), + MFAStatus.UNKNOWN + ) + ).thenReturn(null); when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L), Instant.ofEpochMilli(20000L), Instant.ofEpochMilli(30000L), null); @@ -1546,19 +1635,34 @@ public void createUserAndLinkAll() throws Exception { new AuthConfig(true, null, null), new CollectingExternalConfig(Collections.emptyMap()))); - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id3"), - new RemoteIdentityDetails("user3", "full3", "d@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id4"), - new RemoteIdentityDetails("user4", "full4", "c@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id5"), - new RemoteIdentityDetails("user5", "full5", "b@g.com"))))) - .thenReturn(null); + TemporarySessionData tsd = TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) + .login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id3"), + new RemoteIdentityDetails("user3", "full3", "d@g.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id4"), + new RemoteIdentityDetails("user4", "full4", "c@g.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id5"), + new RemoteIdentityDetails("user5", "full5", "b@g.com") + ) + ), + MFAStatus.UNKNOWN + ); + when(storage.getTemporarySessionData(token.getHashedToken())) + .thenReturn(tsd).thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id2"), new RemoteIdentityDetails("user2", "full2", "e@g.com")))) @@ -1662,7 +1766,7 @@ public void createUserFailNullsAndEmpties() throws Exception { when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( TemporarySessionData.create(UUID.randomUUID(), SMALL, SMALL) - .login(set(REMOTE))); + .login(set(REMOTE), MFAStatus.UNKNOWN)); final String id = "bar"; final NewUserName u = new NewUserName("baz"); @@ -1873,8 +1977,14 @@ public void createUserFailNoMatchingIdentities() throws Exception { when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + .login( + set(new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + )), + MFAStatus.UNKNOWN + ) + ) .thenReturn(null); final String id = "bar"; //yep, that won't match @@ -1906,9 +2016,15 @@ public void createUserFailUserExists() throws Exception { final IncomingToken t = new IncomingToken("foo"); when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(testauth.randGenMock.randomUUID()).thenReturn(UID).thenReturn(null); @@ -1948,9 +2064,15 @@ public void createUserFailIdentityLinked() throws Exception { final IncomingToken t = new IncomingToken("foo"); when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(testauth.randGenMock.randomUUID()).thenReturn(UID).thenReturn(null); @@ -1992,9 +2114,15 @@ public void createUserFailNoRole() throws Exception { final IncomingToken t = new IncomingToken("foo"); when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(testauth.randGenMock.randomUUID()).thenReturn(UID).thenReturn(null); @@ -2035,11 +2163,19 @@ public void createUserFailLinkAllNoSuchUser() throws Exception { final IncomingToken t = new IncomingToken("foo"); when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(testauth.randGenMock.randomUUID()).thenReturn(UID).thenReturn(null); @@ -2085,11 +2221,19 @@ public void createUserFailLinkFailed() throws Exception { final IncomingToken t = new IncomingToken("foo"); when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(testauth.randGenMock.randomUUID()).thenReturn(UID).thenReturn(null); @@ -2138,11 +2282,19 @@ public void createUserFailNoSuchUserOnSetLastLogin() throws Exception { new CollectingExternalConfig(Collections.emptyMap()))); when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L), @@ -2181,81 +2333,100 @@ private void failCreateUser( @Test public void completeLogin() throws Exception { + // There's only one happy path through the final login method wrt MFA so we just test here completeLogin(Role.DEV_TOKEN, true); completeLogin(Role.ADMIN, false); completeLogin(Role.CREATE_ADMIN, false); } - private void completeLogin(final Role userRole, final boolean allowLogin) - throws Exception { - logEvents.clear(); - - final TestMocks testauth = initTestMocks(); - final AuthStorage storage = testauth.storageMock; - final RandomDataGenerator rand = testauth.randGenMock; - final Clock clock = testauth.clockMock; - final Authentication auth = testauth.auth; - - AuthenticationTester.setConfigUpdateInterval(auth, -1); - - final IncomingToken token = new IncomingToken("foobar"); - final UUID tokenID = UUID.randomUUID(); - - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) - .thenReturn(null); - - when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")))).thenReturn(Optional.of( - AuthUser.getBuilder(new UserName("foo"), UID, new DisplayName("bar"), - Instant.ofEpochMilli(70000)) - .withRole(userRole) - .withIdentity(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))) - .build())); - - when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) - .thenReturn(new AuthConfigSet( - new AuthConfig(allowLogin, null, null), - new CollectingExternalConfig(Collections.emptyMap()))); - - when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L), - Instant.ofEpochMilli(20000L), null); - when(rand.randomUUID()).thenReturn(tokenID).thenReturn(null); - when(rand.getToken()).thenReturn("mfingtoken"); - - final NewToken nt = auth.login(token, "ef0518c79af70ed979907969c6d0a0f7", - set(new PolicyID("pid1"), new PolicyID("pid2")), - TokenCreationContext.getBuilder().withNullableDevice("dev").build(), false); - - verify(storage).addPolicyIDs(new UserName("foo"), - set(new PolicyID("pid1"), new PolicyID("pid2"))); - - verify(storage, never()).link(any(), any()); - - verify(storage).storeToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(10000), 14 * 24 * 3600 * 1000) - .withContext(TokenCreationContext.getBuilder().withNullableDevice("dev").build()) - .build(), - "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); - - verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(20000)); - verify(storage).deleteTemporarySessionData(token.getHashedToken()); - - assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(10000), 14 * 24 * 3600 * 1000) - .withContext(TokenCreationContext.getBuilder().withNullableDevice("dev").build()) - .build(), - "mfingtoken"))); - - assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, - "Logged in user foo with token " + tokenID, Authentication.class)); + private void completeLogin(final Role userRole, final boolean allowLogin) throws Exception { + for (final MFAStatus mfa: MFAStatus.values()) { + logEvents.clear(); + + final TestMocks testauth = initTestMocks(); + final AuthStorage storage = testauth.storageMock; + final RandomDataGenerator rand = testauth.randGenMock; + final Clock clock = testauth.clockMock; + final Authentication auth = testauth.auth; + + AuthenticationTester.setConfigUpdateInterval(auth, -1); + + final IncomingToken token = new IncomingToken("foobar"); + final UUID tokenID = UUID.randomUUID(); + + when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + mfa + )) + .thenReturn(null); + + when(storage.getUser( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + )).thenReturn(Optional.of( + AuthUser.getBuilder(new UserName("foo"), UID, new DisplayName("bar"), + Instant.ofEpochMilli(70000)) + .withRole(userRole) + .withIdentity(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com"))) + .build() + )); + + when(storage.getConfig(isA(CollectingExternalConfigMapper.class))) + .thenReturn(new AuthConfigSet( + new AuthConfig(allowLogin, null, null), + new CollectingExternalConfig(Collections.emptyMap()))); + + when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000L), + Instant.ofEpochMilli(20000L), null); + when(rand.randomUUID()).thenReturn(tokenID).thenReturn(null); + when(rand.getToken()).thenReturn("mfingtoken"); + + final NewToken nt = auth.login(token, "ef0518c79af70ed979907969c6d0a0f7", + set(new PolicyID("pid1"), new PolicyID("pid2")), + TokenCreationContext.getBuilder().withNullableDevice("dev").build(), false); + + verify(storage).addPolicyIDs(new UserName("foo"), + set(new PolicyID("pid1"), new PolicyID("pid2"))); + + verify(storage, never()).link(any(), any()); + + verify(storage).storeToken(StoredToken.getBuilder( + TokenType.LOGIN, tokenID, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(10000), 14 * 24 * 3600 * 1000) + .withContext(TokenCreationContext.getBuilder().withNullableDevice("dev") + .build()) + .withMFA(mfa) + .build(), + "hQ9Z3p0WaYunsmIBRUcJgBn5Pd4BCYhOEQCE3enFOzA="); + + verify(storage).setLastLogin(new UserName("foo"), Instant.ofEpochMilli(20000)); + verify(storage).deleteTemporarySessionData(token.getHashedToken()); + + assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( + TokenType.LOGIN, tokenID, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(10000), 14 * 24 * 3600 * 1000) + .withContext(TokenCreationContext.getBuilder().withNullableDevice("dev") + .build()) + .withMFA(mfa) + .build(), + "mfingtoken"))); + + assertLogEventsCorrect(logEvents, new LogEvent(Level.INFO, + "Logged in user foo with token " + tokenID, Authentication.class)); + } } @Test @@ -2273,9 +2444,15 @@ public void completeLoginWithAlternateTokenLifetimeAndEmptyLinks() throws Except final UUID tokenID = UUID.randomUUID(); when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2346,19 +2523,34 @@ public void completeLoginAndLinkAll() throws Exception { final IncomingToken token = new IncomingToken("foobar"); final UUID tokenID = UUID.randomUUID(); - when(storage.getTemporarySessionData(token.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id3"), - new RemoteIdentityDetails("user3", "full3", "d@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id4"), - new RemoteIdentityDetails("user4", "full4", "c@g.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id5"), - new RemoteIdentityDetails("user5", "full5", "b@g.com"))))) - .thenReturn(null); + TemporarySessionData tsd = TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) + .login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id3"), + new RemoteIdentityDetails("user3", "full3", "d@g.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id4"), + new RemoteIdentityDetails("user4", "full4", "c@g.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id5"), + new RemoteIdentityDetails("user5", "full5", "b@g.com") + ) + ), + MFAStatus.UNKNOWN + ); + when(storage.getTemporarySessionData(token.getHashedToken())) + .thenReturn(tsd).thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), new RemoteIdentityDetails("user1", "full1", "f@h.com")))).thenReturn(Optional.of( @@ -2466,7 +2658,7 @@ public void completeLoginFailNullsAndEmpties() throws Exception { when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( TemporarySessionData.create(UUID.randomUUID(), SMALL, SMALL) - .login(set(REMOTE))); + .login(set(REMOTE), MFAStatus.UNKNOWN)); failCompleteLogin(auth, null, id, pids, CTX, l, new NullPointerException("Temporary token")); @@ -2574,9 +2766,15 @@ public void completeLoginFailBadId() throws Exception { final boolean l = false; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); failCompleteLogin(auth, t, id, pids, CTX, l, @@ -2596,9 +2794,15 @@ public void completeLoginFailNoUser() throws Exception { final boolean l = false; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2622,9 +2826,15 @@ public void completeLoginFailLoginDisabled() throws Exception { final boolean l = false; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2658,9 +2868,15 @@ public void completeLoginFailDisabledAccount() throws Exception { final boolean l = false; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2696,9 +2912,15 @@ public void completeLoginFailNoSuchUserOnPolicyID() throws Exception { final boolean l = false; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2736,11 +2958,19 @@ public void completeLoginFailNoSuchUserOnLink() throws Exception { final boolean l = true; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2782,11 +3012,19 @@ public void completeLoginFailLinkFailOnLink() throws Exception { final boolean l = true; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com")), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e@g.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ), + new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e@g.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), @@ -2830,9 +3068,15 @@ public void completeLoginFailNoSuchUserOnSetLastLogin() throws Exception { final boolean l = false; when(storage.getTemporarySessionData(t.getHashedToken())).thenReturn( - TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "f@h.com"))))) + TemporarySessionData.create(UUID.randomUUID(), SMALL, 10000).login( + set( + new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "f@h.com") + ) + ), + MFAStatus.UNKNOWN + )) .thenReturn(null); when(storage.getUser(new RemoteIdentity(new RemoteIdentityID("prov", "id1"), diff --git a/src/test/java/us/kbase/test/auth2/lib/TemporarySessionDataTest.java b/src/test/java/us/kbase/test/auth2/lib/TemporarySessionDataTest.java index c11b6513..aec90adf 100644 --- a/src/test/java/us/kbase/test/auth2/lib/TemporarySessionDataTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/TemporarySessionDataTest.java @@ -23,6 +23,7 @@ import us.kbase.auth2.lib.identity.RemoteIdentity; import us.kbase.auth2.lib.identity.RemoteIdentityDetails; import us.kbase.auth2.lib.identity.RemoteIdentityID; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.test.auth2.TestCommon; public class TemporarySessionDataTest { @@ -52,11 +53,12 @@ public void constructLoginStart() throws Exception { assertThat("incorrect expires", ti.getExpires(), is(inst(20000))); assertThat("incorrect state", ti.getOAuth2State(), is(opt("stategoeshere"))); assertThat("incorrect pkce", ti.getPKCECodeVerifier(), is(opt("pkcegoeshere"))); - assertThat("incorrect user", ti.getUser(), is(Optional.empty())); - assertThat("incorrect idents", ti.getIdentities(), is(Optional.empty())); - assertThat("incorrect error", ti.getError(), is(Optional.empty())); - assertThat("incorrect error type", ti.getErrorType(), is(Optional.empty())); + assertThat("incorrect user", ti.getUser(), is(opt())); + assertThat("incorrect idents", ti.getIdentities(), is(opt())); + assertThat("incorrect error", ti.getError(), is(opt())); + assertThat("incorrect error type", ti.getErrorType(), is(opt())); assertThat("incorrect has error", ti.hasError(), is(false)); + assertThat("incorrect mfa", ti.getMFA(), is(opt())); } @Test @@ -64,7 +66,7 @@ public void constructLoginIdents() throws Exception { final UUID id = UUID.randomUUID(); final Instant now = Instant.now(); final TemporarySessionData ti = TemporarySessionData.create( - id, now, now.plusMillis(100000)).login(set(REMOTE1, REMOTE2)); + id, now, now.plusMillis(100000)).login(set(REMOTE1, REMOTE2), MFAStatus.USED); assertThat("incorrect op", ti.getOperation(), is(Operation.LOGINIDENTS)); assertThat("incorrect id", ti.getId(), is(id)); @@ -72,11 +74,12 @@ public void constructLoginIdents() throws Exception { assertThat("incorrect expires", ti.getExpires(), is(now.plusMillis(100000))); assertThat("incorrect state", ti.getOAuth2State(), is(ES)); assertThat("incorrect pkce", ti.getPKCECodeVerifier(), is(ES)); - assertThat("incorrect user", ti.getUser(), is(Optional.empty())); + assertThat("incorrect user", ti.getUser(), is(opt())); assertThat("incorrect idents", ti.getIdentities(), is(Optional.of(set(REMOTE2, REMOTE1)))); - assertThat("incorrect error", ti.getError(), is(Optional.empty())); - assertThat("incorrect error type", ti.getErrorType(), is(Optional.empty())); + assertThat("incorrect error", ti.getError(), is(opt())); + assertThat("incorrect error type", ti.getErrorType(), is(opt())); assertThat("incorrect has error", ti.hasError(), is(false)); + assertThat("incorrect mfa", ti.getMFA(), is(opt(MFAStatus.USED))); assertImmutable(ti); } @@ -95,11 +98,12 @@ public void constructWithError() throws Exception { assertThat("incorrect expires", ti.getExpires(), is(now.plusMillis(10000))); assertThat("incorrect state", ti.getOAuth2State(), is(ES)); assertThat("incorrect pkce", ti.getPKCECodeVerifier(), is(ES)); - assertThat("incorrect idents", ti.getIdentities(), is(Optional.empty())); - assertThat("incorrect user", ti.getUser(), is(Optional.empty())); + assertThat("incorrect idents", ti.getIdentities(), is(opt())); + assertThat("incorrect user", ti.getUser(), is(opt())); assertThat("incorrect error", ti.getError(), is(Optional.of("foo"))); assertThat("incorrect error type", ti.getErrorType(), is(Optional.of(ErrorType.DISABLED))); assertThat("incorrect has error", ti.hasError(), is(true)); + assertThat("incorrect mfa", ti.getMFA(), is(opt())); } @Test @@ -115,11 +119,12 @@ public void constructLinkStart() throws Exception { assertThat("incorrect expires", ti.getExpires(), is(now.plusMillis(10000))); assertThat("incorrect state", ti.getOAuth2State(), is(opt("somestate"))); assertThat("incorrect pkce", ti.getPKCECodeVerifier(), is(opt("pkce"))); - assertThat("incorrect idents", ti.getIdentities(), is(Optional.empty())); + assertThat("incorrect idents", ti.getIdentities(), is(opt())); assertThat("incorrect user", ti.getUser(), is(Optional.of(new UserName("bar")))); - assertThat("incorrect error", ti.getError(), is(Optional.empty())); - assertThat("incorrect error type", ti.getErrorType(), is(Optional.empty())); + assertThat("incorrect error", ti.getError(), is(opt())); + assertThat("incorrect error type", ti.getErrorType(), is(opt())); assertThat("incorrect has error", ti.hasError(), is(false)); + assertThat("incorrect mfa", ti.getMFA(), is(opt())); } @@ -139,9 +144,10 @@ public void constructLinkIdents() throws Exception { assertThat("incorrect pkce", ti.getPKCECodeVerifier(), is(ES)); assertThat("incorrect idents", ti.getIdentities(), is(Optional.of(set(REMOTE1, REMOTE2)))); assertThat("incorrect user", ti.getUser(), is(Optional.of(new UserName("bar")))); - assertThat("incorrect error", ti.getError(), is(Optional.empty())); - assertThat("incorrect error type", ti.getErrorType(), is(Optional.empty())); + assertThat("incorrect error", ti.getError(), is(opt())); + assertThat("incorrect error type", ti.getErrorType(), is(opt())); assertThat("incorrect has error", ti.hasError(), is(false)); + assertThat("incorrect mfa", ti.getMFA(), is(opt())); assertImmutable(ti); } @@ -161,9 +167,10 @@ public void constructExpireOverflows() throws Exception { assertThat("incorrect pkce", ti.getPKCECodeVerifier(), is(ES)); assertThat("incorrect idents", ti.getIdentities(), is(Optional.of(set(REMOTE1, REMOTE2)))); assertThat("incorrect user", ti.getUser(), is(Optional.of(new UserName("bar")))); - assertThat("incorrect error", ti.getError(), is(Optional.empty())); - assertThat("incorrect error type", ti.getErrorType(), is(Optional.empty())); + assertThat("incorrect error", ti.getError(), is(opt())); + assertThat("incorrect error type", ti.getErrorType(), is(opt())); assertThat("incorrect has error", ti.hasError(), is(false)); + assertThat("incorrect mfa", ti.getMFA(), is(opt())); assertImmutable(ti); } @@ -245,16 +252,23 @@ private void failConstructLoginStart( @Test public void constructLoginIdentsFailNulls() throws Exception { - failConstructLoginIdents(null, new NullPointerException("identities")); + final MFAStatus m = MFAStatus.UNKNOWN; + failConstructLoginIdents(null, m, new NullPointerException("identities")); + failConstructLoginIdents(set(REMOTE1), null, new NullPointerException("mfa")); failConstructLoginIdents( - set(REMOTE1, null), new NullPointerException("null item in identities")); - failConstructLoginIdents(set(), new IllegalArgumentException("empty identities")); + set(REMOTE1, null), m, new NullPointerException("null item in identities") + ); + failConstructLoginIdents(set(), m, new IllegalArgumentException("empty identities")); } - private void failConstructLoginIdents(final Set idents, final Exception e) { + private void failConstructLoginIdents( + final Set idents, + final MFAStatus mfa, + final Exception e + ) { try { TemporarySessionData.create(UUID.randomUUID(), Instant.now(), Instant.now()) - .login(idents); + .login(idents, mfa); fail("expected exception"); } catch (Exception got) { TestCommon.assertExceptionCorrect(got, e); diff --git a/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTempSessionDataTest.java b/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTempSessionDataTest.java index 6941afbf..6eb46091 100644 --- a/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTempSessionDataTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/storage/mongo/MongoStorageTempSessionDataTest.java @@ -23,6 +23,7 @@ import us.kbase.auth2.lib.UserName; import us.kbase.auth2.lib.token.IncomingHashedToken; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.test.auth2.TestCommon; public class MongoStorageTempSessionDataTest extends MongoStorageTester { @@ -54,13 +55,13 @@ public void storeAndGetLoginIdents() throws Exception { final UUID id = UUID.randomUUID(); final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); // mongo truncates final TemporarySessionData tsd = TemporarySessionData.create(id, now, now.plusSeconds(10)) - .login(set(REMOTE2)); + .login(set(REMOTE2), MFAStatus.USED); storage.storeTemporarySessionData(tsd, IncomingToken.hash("foobar")); assertThat("incorrect session data", storage.getTemporarySessionData( new IncomingToken("foobar").getHashedToken()), is( TemporarySessionData.create(id, now, now.plusSeconds(10)) - .login(set(REMOTE2)))); + .login(set(REMOTE2), MFAStatus.USED))); } @Test diff --git a/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java b/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java index 4469941a..3ffc9c4c 100644 --- a/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java +++ b/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java @@ -54,6 +54,7 @@ import us.kbase.auth2.lib.exceptions.AuthException; import us.kbase.auth2.lib.identity.IdentityProvider; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; import us.kbase.auth2.lib.token.TokenType; @@ -224,6 +225,7 @@ public static void checkReturnedToken( final Map customContext, final UserName userName, final TokenType type, + final MFAStatus mfa, final String name, final long lifetime, final boolean checkAgentContext) @@ -248,7 +250,7 @@ public static void checkReturnedToken( } checkStoredToken(manager, (String) uitoken.get("token"), id, created, customContext, - userName, type, name, lifetime); + userName, type, mfa, name, lifetime); } public static void checkStoredToken( @@ -259,9 +261,10 @@ public static void checkStoredToken( final Map customContext, final UserName userName, final TokenType type, + final MFAStatus mfa, final String name, - final long lifetime) - throws Exception { + final long lifetime + ) throws Exception { assertThat("incorrect token", token, is(RegexMatcher.matches("[A-Z2-7]{32}"))); @@ -291,6 +294,7 @@ public static void checkStoredToken( assertThat("incorrect id", st.getId(), is(UUID.fromString(id))); assertThat("incorrect name", st.getTokenName(), is(tn)); assertThat("incorrect user", st.getUserName(), is(userName)); + assertThat("incorrect mfa", st.getMFA(), is(mfa)); } // combine with above somehow? @@ -300,9 +304,10 @@ public static void checkStoredToken( final Map customContext, final UserName userName, final TokenType type, + final MFAStatus mfa, final String name, - final long lifetime) - throws Exception { + final long lifetime + ) throws Exception { assertThat("incorrect token", token, is(RegexMatcher.matches("[A-Z2-7]{32}"))); @@ -332,6 +337,7 @@ public static void checkStoredToken( assertThat("incorrect id", st.getId(), isA(UUID.class)); assertThat("incorrect name", st.getTokenName(), is(tn)); assertThat("incorrect user", st.getUserName(), is(userName)); + assertThat("incorrect mfa", st.getMFA(), is(mfa)); } public static void resetServer( diff --git a/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java b/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java index 4bdb2a34..0e449f99 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java @@ -44,6 +44,7 @@ import us.kbase.auth2.lib.exceptions.TestModeException; import us.kbase.auth2.lib.exceptions.UnauthorizedException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.NewToken; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; @@ -226,7 +227,8 @@ public void createTokenNoCustomContext() throws Exception { @SuppressWarnings("unchecked") final Map response = res.readEntity(Map.class); ServiceTestUtils.checkReturnedToken(manager, response, Collections.emptyMap(), - new UserName("foo"), TokenType.AGENT, "whee", 7 * 24 * 3600 * 1000, false); + new UserName("foo"), TokenType.AGENT, MFAStatus.UNKNOWN, "whee", + 7 * 24 * 3600 * 1000, false); } @Test @@ -246,7 +248,8 @@ public void createTokenWithCustomContext() throws Exception { @SuppressWarnings("unchecked") final Map response = res.readEntity(Map.class); ServiceTestUtils.checkReturnedToken(manager, response, ImmutableMap.of("foo", "bar"), - new UserName("foo"), TokenType.AGENT, "whee", 7 * 24 * 3600 * 1000, false); + new UserName("foo"), TokenType.AGENT, MFAStatus.UNKNOWN, "whee", + 7 * 24 * 3600 * 1000, false); } @Test diff --git a/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java index 6f1db05b..0b5c6d86 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java @@ -75,6 +75,7 @@ import us.kbase.auth2.lib.identity.RemoteIdentityID; import us.kbase.auth2.lib.storage.exceptions.AuthStorageException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.TemporaryToken; import us.kbase.auth2.lib.token.TokenType; import us.kbase.auth2.lib.user.AuthUser; @@ -429,7 +430,7 @@ private void loginCompleteImmediateLoginMinimalInput(final String env) throws Ex saveTemporarySessionData(state, "pkceohgodohgod", "foobartoken"); - loginCompleteImmediateLoginStoreUser(authcode, "pkceohgodohgod", env); + loginCompleteImmediateLoginStoreUser(authcode, "pkceohgodohgod", env, MFAStatus.UNKNOWN); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -450,7 +451,7 @@ private void loginCompleteImmediateLoginMinimalInput(final String env) throws Ex assertThat("incorrect auth cookie less token", token, is(expectedtoken)); assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - loginCompleteImmediateLoginCheckToken(token); + loginCompleteImmediateLoginCheckToken(token, MFAStatus.UNKNOWN); } @Test @@ -468,7 +469,9 @@ public void loginCompleteImmediateLoginEmptyStringInput() throws Exception { saveTemporarySessionData(state, "pkcethisisinhumane", "foobartoken"); - loginCompleteImmediateLoginStoreUser(authcode, "pkcethisisinhumane", null); + loginCompleteImmediateLoginStoreUser( + authcode, "pkcethisisinhumane", null, MFAStatus.USED + ); final WebTarget wt = loginCompleteSetUpWebTargetEmptyError(authcode, state); final Response res = wt.request() @@ -488,7 +491,7 @@ public void loginCompleteImmediateLoginEmptyStringInput() throws Exception { assertThat("incorrect auth cookie less token", token, is(expectedtoken)); assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - loginCompleteImmediateLoginCheckToken(token); + loginCompleteImmediateLoginCheckToken(token, MFAStatus.USED); } @Test @@ -519,7 +522,7 @@ private void loginCompleteImmediateLoginRedirectAndTrueSession( saveTemporarySessionData(state, "pkcepkcepkcepkce", "foobartoken"); - loginCompleteImmediateLoginStoreUser(authcode, "pkcepkcepkcepkce", env); + loginCompleteImmediateLoginStoreUser(authcode, "pkcepkcepkcepkce", env, MFAStatus.NOT_USED); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -542,7 +545,7 @@ private void loginCompleteImmediateLoginRedirectAndTrueSession( assertThat("incorrect auth cookie less token", token, is(expectedtoken)); assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - loginCompleteImmediateLoginCheckToken(token); + loginCompleteImmediateLoginCheckToken(token, MFAStatus.NOT_USED); } @Test @@ -559,7 +562,7 @@ public void loginCompleteImmediateLoginRedirectAndFalseSession() throws Exceptio saveTemporarySessionData(state, "pkceoohhooohhhmm", "foobartoken"); - loginCompleteImmediateLoginStoreUser(authcode, "pkceoohhooohhhmm", null); + loginCompleteImmediateLoginStoreUser(authcode, "pkceoohhooohhhmm", null, MFAStatus.USED); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Response res = wt.request() @@ -581,16 +584,17 @@ public void loginCompleteImmediateLoginRedirectAndFalseSession() throws Exceptio assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); TestCommon.assertCloseTo(token.getMaxAge(), 14 * 24 * 3600, 10); - loginCompleteImmediateLoginCheckToken(token); + loginCompleteImmediateLoginCheckToken(token, MFAStatus.USED); } private void loginCompleteImmediateLoginStoreUser( final String authcode, final String pkce, - final String environment) + final String environment, + final MFAStatus mfa) throws Exception { final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, pkce, environment); + authcode, pkce, environment, mfa); manager.storage.createUser(NewUser.getBuilder( new UserName("whee"), UID, new DisplayName("dn"), Instant.ofEpochMilli(20000), @@ -598,27 +602,30 @@ private void loginCompleteImmediateLoginStoreUser( .build()); } - private void loginCompleteImmediateLoginCheckToken(final NewCookie token) throws Exception { - checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("whee")); + private void loginCompleteImmediateLoginCheckToken(final NewCookie token, final MFAStatus mfa + ) throws Exception { + checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("whee"), mfa); } private void checkLoginToken( final Map uitoken, final Map customContext, - final UserName userName) + final UserName userName, + final MFAStatus mfa) throws Exception { ServiceTestUtils.checkReturnedToken(manager, uitoken, customContext, userName, - TokenType.LOGIN, null, 14 * 24 * 3600 * 1000, true); + TokenType.LOGIN, mfa, null, 14 * 24 * 3600 * 1000, true); } private void checkLoginToken( final String token, final Map customContext, - final UserName userName) + final UserName userName, + final MFAStatus mfa) throws Exception { ServiceTestUtils.checkStoredToken(manager, token, customContext, userName, TokenType.LOGIN, - null, 14 * 24 * 3600 * 1000); + mfa, null, 14 * 24 * 3600 * 1000); } private void assertLoginProcessTokensRemoved(final Response res) { @@ -685,7 +692,7 @@ private void loginCompleteDelayedMinimalInput(final String env) throws Exception saveTemporarySessionData(state, "pkceopraisethedarkgodsbelow", "foobartoken"); final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkceopraisethedarkgodsbelow", env); + authcode, "pkceopraisethedarkgodsbelow", env, MFAStatus.USED); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -710,7 +717,7 @@ private void loginCompleteDelayedMinimalInput(final String env) throws Exception assertEnvironmentCookieCorrect(res, env, 30 * 60); - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.USED, res); } @Test @@ -744,7 +751,7 @@ private void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( saveTemporarySessionData(state, "pkceisinmybrainicanseeall", "foobartoken"); final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkceisinmybrainicanseeall", env); + authcode, "pkceisinmybrainicanseeall", env, MFAStatus.NOT_USED); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -771,7 +778,7 @@ private void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( assertEnvironmentCookieCorrect(res, env, 30 * 60); - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.NOT_USED, res); } @Test @@ -810,7 +817,7 @@ private void loginCompleteDelayedLoginRedirectAndTrueSession( saveTemporarySessionData(state, "pkcuwgahngalftaghn", "foobartoken"); final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkcuwgahngalftaghn", env); + authcode, "pkcuwgahngalftaghn", env, MFAStatus.UNKNOWN); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Builder b = wt.request() @@ -840,7 +847,7 @@ private void loginCompleteDelayedLoginRedirectAndTrueSession( assertEnvironmentCookieCorrect(res, env, 30 * 60); - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.UNKNOWN, res); } @Test @@ -859,7 +866,7 @@ public void loginCompleteDelayedLoginRedirectAndFalseSession() throws Exception saveTemporarySessionData(state, "pkceifeelmoistandsprightly", "foobartoken"); final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkceifeelmoistandsprightly", null); + authcode, "pkceifeelmoistandsprightly", null, MFAStatus.NOT_USED); final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); final Response res = wt.request() @@ -885,11 +892,12 @@ public void loginCompleteDelayedLoginRedirectAndFalseSession() throws Exception assertThat("incorrect session cookie less max age", session, is(expectedsession)); TestCommon.assertCloseTo(session.getMaxAge(), 30 * 60, 10); - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.NOT_USED, res); } private void loginCompleteDelayedCheckTempAndStateCookies( final RemoteIdentity remoteIdentity, + final MFAStatus mfa, final Response res) throws Exception { @@ -903,6 +911,7 @@ private void loginCompleteDelayedCheckTempAndStateCookies( new IncomingToken(tempCookie.getValue()).getHashedToken()); assertThat("incorrect stored ids", tis.getIdentities().get(), is(set(remoteIdentity))); + assertThat("incorrect mfa", tis.getMFA().get(), is(mfa)); } private WebTarget loginCompleteSetUpWebTarget(final String authcode, final String state) { @@ -930,7 +939,8 @@ private WebTarget loginCompleteSetUpWebTargetEmptyError( private RemoteIdentity loginCompleteSetUpProviderMock( final String authcode, final String pkce, - final String environment) + final String environment, + final MFAStatus mfa) throws Exception { final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); @@ -938,7 +948,7 @@ private RemoteIdentity loginCompleteSetUpProviderMock( new RemoteIdentityID("prov1", "prov1id"), new RemoteIdentityDetails("user", "full", "email@email.com")); when(provmock.getIdentities(authcode, pkce, false, environment)) - .thenReturn(IdentityProviderResponse.from(remoteIdentity)); + .thenReturn(IdentityProviderResponse.from(remoteIdentity, mfa)); return remoteIdentity; } @@ -1154,7 +1164,7 @@ public void loginChoice3Create2Login() throws Exception { final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); + .login(idents, MFAStatus.USED); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -1280,7 +1290,7 @@ public void loginChoice2LoginWithRedirectAndLoginDisabled() throws Exception { final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); + .login(idents, MFAStatus.NOT_USED); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -1378,7 +1388,7 @@ private void loginChoice2CreateAndLoginDisabled(final String env) throws Excepti } final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); + .login(idents, MFAStatus.UNKNOWN); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -1464,7 +1474,7 @@ private void loginChoice2CreateWithRedirectURL(final String env, final String ur } final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); + .login(idents, MFAStatus.USED); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -1685,8 +1695,13 @@ public void loginChoiceFailBadRedirect() throws Exception { public void loginCancelPOST() throws Exception { final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id"), - new RemoteIdentityDetails("user", "full", "e@g.com")))); + .login( + set(new RemoteIdentity( + new RemoteIdentityID("prov", "id"), + new RemoteIdentityDetails("user", "full", "e@g.com") + )), + MFAStatus.UNKNOWN + ); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -1710,8 +1725,13 @@ public void loginCancelPOST() throws Exception { public void loginCancelDELETE() throws Exception { final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(set(new RemoteIdentity(new RemoteIdentityID("prov", "id"), - new RemoteIdentityDetails("user", "full", "e@g.com")))); + .login( + set(new RemoteIdentity( + new RemoteIdentityID("prov", "id"), + new RemoteIdentityDetails("user", "full", "e@g.com") + )), + MFAStatus.UNKNOWN + ); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -1769,7 +1789,7 @@ public void loginPickFormMinimalInputWithEnvironment() throws Exception { } private void loginPickFormMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginPickSetup(); + final TemporaryToken tt = loginPickSetup(MFAStatus.UNKNOWN); final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); @@ -1785,7 +1805,7 @@ private void loginPickFormMinimalInput(final String env) throws Exception { assertThat("incorrect response code", res.getStatus(), is(303)); assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - loginPickOrCreateCheckSessionToken(res); + loginPickOrCreateCheckSessionToken(res, MFAStatus.UNKNOWN); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -1806,7 +1826,7 @@ public void loginPickJSONMinimalInputWithException() throws Exception { } private void loginPickJSONMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginPickSetup(); + final TemporaryToken tt = loginPickSetup(MFAStatus.USED); final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); @@ -1826,7 +1846,7 @@ private void loginPickJSONMinimalInput(final String env) throws Exception { @SuppressWarnings("unchecked") final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.USED); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -1847,7 +1867,7 @@ public void loginPickFormMaximalInputWithEnvironment() throws Exception { } private void loginPickFormMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginPickSetup(); + final TemporaryToken tt = loginPickSetup(MFAStatus.NOT_USED); final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); @@ -1869,7 +1889,9 @@ private void loginPickFormMaximalInput(final String env, final String url) throw assertThat("incorrect response code", res.getStatus(), is(303)); assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); - loginPickOrCreateCheckExtendedToken(res, ImmutableMap.of("a", "1", "b", "2")); + loginPickOrCreateCheckExtendedToken( + res, ImmutableMap.of("a", "1", "b", "2"), MFAStatus.NOT_USED + ); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -1893,7 +1915,7 @@ public void loginPickJsonMaximalInputWithEnvironment() throws Exception { } private void loginPickJsonMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginPickSetup(); + final TemporaryToken tt = loginPickSetup(MFAStatus.UNKNOWN); final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); @@ -1918,7 +1940,9 @@ private void loginPickJsonMaximalInput(final String env, final String url) throw @SuppressWarnings("unchecked") final Map token = (Map) response.get("token"); - checkLoginToken(token, ImmutableMap.of("a", "1", "b", "2"), new UserName("u1")); + checkLoginToken( + token, ImmutableMap.of("a", "1", "b", "2"), new UserName("u1"), MFAStatus.UNKNOWN + ); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -1934,7 +1958,7 @@ private void loginPickJsonMaximalInput(final String env, final String url) throw @Test public void loginPickFormEmptyStrings() throws Exception { - final TemporaryToken tt = loginPickSetup(); + final TemporaryToken tt = loginPickSetup(MFAStatus.USED); final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); @@ -1953,7 +1977,7 @@ public void loginPickFormEmptyStrings() throws Exception { assertThat("incorrect response code", res.getStatus(), is(303)); assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - loginPickOrCreateCheckSessionToken(res); + loginPickOrCreateCheckSessionToken(res, MFAStatus.USED); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -1966,7 +1990,7 @@ public void loginPickFormEmptyStrings() throws Exception { @Test public void loginPickJsonEmptyData() throws Exception { - final TemporaryToken tt = loginPickSetup(); + final TemporaryToken tt = loginPickSetup(MFAStatus.NOT_USED); final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); @@ -1991,7 +2015,7 @@ public void loginPickJsonEmptyData() throws Exception { @SuppressWarnings("unchecked") final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.NOT_USED); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2306,7 +2330,8 @@ public void loginPickFailEmptyPolicyID() throws Exception { private void loginPickOrCreateCheckExtendedToken( final Response res, - final Map customContext) + final Map customContext, + final MFAStatus mfa) throws Exception { assertLoginProcessTokensRemoved(res); @@ -2316,10 +2341,10 @@ private void loginPickOrCreateCheckExtendedToken( assertThat("incorrect auth cookie less token", token, is(expectedtoken)); TestCommon.assertCloseTo(token.getMaxAge(), 14 * 24 * 3600, 10); - checkLoginToken(token.getValue(), customContext, new UserName("u1")); + checkLoginToken(token.getValue(), customContext, new UserName("u1"), mfa); } - private void loginPickOrCreateCheckSessionToken(final Response res) + private void loginPickOrCreateCheckSessionToken(final Response res, final MFAStatus mfa) throws Exception, MissingParameterException, IllegalParameterException { assertLoginProcessTokensRemoved(res); @@ -2328,7 +2353,7 @@ private void loginPickOrCreateCheckSessionToken(final Response res) "/", null, "authtoken", -1, false); assertThat("incorrect auth cookie less token", token, is(expectedtoken)); - checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("u1")); + checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("u1"), mfa); } private Builder loginPickOrCreateRequestBuilder( @@ -2344,7 +2369,7 @@ private Builder loginPickOrCreateRequestBuilder( return req; } - private TemporaryToken loginPickSetup() throws Exception { + private TemporaryToken loginPickSetup(final MFAStatus mfa) throws Exception { final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); @@ -2354,7 +2379,7 @@ private TemporaryToken loginPickSetup() throws Exception { final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(set(REMOTE1, REMOTE2, REMOTE3)); + .login(set(REMOTE1, REMOTE2, REMOTE3), mfa); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); @@ -2380,7 +2405,7 @@ public void loginCreateFormMinimalInputWithEnvironment() throws Exception { } private void loginCreateFormMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); + final TemporaryToken tt = loginCreateSetup(MFAStatus.UNKNOWN); final URI target = UriBuilder.fromUri(host).path("/login/create").build(); @@ -2399,7 +2424,7 @@ private void loginCreateFormMinimalInput(final String env) throws Exception { assertThat("incorrect response code", res.getStatus(), is(303)); assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - loginPickOrCreateCheckSessionToken(res); + loginPickOrCreateCheckSessionToken(res, MFAStatus.UNKNOWN); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2423,7 +2448,7 @@ public void loginCreateJSONMinimalInputWithEnvironment() throws Exception { private void loginCreateJSONMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); + final TemporaryToken tt = loginCreateSetup(MFAStatus.USED); final URI target = UriBuilder.fromUri(host).path("/login/create").build(); @@ -2448,7 +2473,7 @@ private void loginCreateJSONMinimalInput(final String env) throws Exception { @SuppressWarnings("unchecked") final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.USED); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2471,7 +2496,7 @@ public void loginCreateFormMaximalInputFromEnvironment() throws Exception { } private void loginCreateFormMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); + final TemporaryToken tt = loginCreateSetup(MFAStatus.NOT_USED); final URI target = UriBuilder.fromUri(host).path("/login/create").build(); @@ -2496,7 +2521,9 @@ private void loginCreateFormMaximalInput(final String env, final String url) thr assertThat("incorrect response code", res.getStatus(), is(303)); assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); - loginPickOrCreateCheckExtendedToken(res, ImmutableMap.of("a", "1", "b", "2")); + loginPickOrCreateCheckExtendedToken( + res, ImmutableMap.of("a", "1", "b", "2"), MFAStatus.NOT_USED + ); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2511,7 +2538,6 @@ private void loginCreateFormMaximalInput(final String env, final String url) thr assertNoTempToken(tt); } - @Test public void loginCreateJSONMaximalInput() throws Exception { loginCreateJSONMaximalInput(null, "https://foo.com/baz/bat"); @@ -2523,7 +2549,7 @@ public void loginCreateJSONMaximalInputWithEnvironment() throws Exception { } private void loginCreateJSONMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); + final TemporaryToken tt = loginCreateSetup(MFAStatus.UNKNOWN); final URI target = UriBuilder.fromUri(host).path("/login/create").build(); @@ -2555,7 +2581,9 @@ private void loginCreateJSONMaximalInput(final String env, final String url) thr @SuppressWarnings("unchecked") final Map token = (Map) response.get("token"); - checkLoginToken(token, ImmutableMap.of("a", "1", "b", "2"), new UserName("u1")); + checkLoginToken( + token, ImmutableMap.of("a", "1", "b", "2"), new UserName("u1"), MFAStatus.UNKNOWN + ); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2573,7 +2601,7 @@ private void loginCreateJSONMaximalInput(final String env, final String url) thr @Test public void loginCreateFormEmptyStrings() throws Exception { - final TemporaryToken tt = loginChoiceSetup(); + final TemporaryToken tt = loginCreateSetup(MFAStatus.USED); final URI target = UriBuilder.fromUri(host).path("/login/create").build(); @@ -2597,7 +2625,7 @@ public void loginCreateFormEmptyStrings() throws Exception { assertThat("incorrect response code", res.getStatus(), is(303)); assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - loginPickOrCreateCheckSessionToken(res); + loginPickOrCreateCheckSessionToken(res, MFAStatus.USED); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2612,7 +2640,7 @@ public void loginCreateFormEmptyStrings() throws Exception { @Test public void loginCreateJSONEmptyInput() throws Exception { - final TemporaryToken tt = loginChoiceSetup(); + final TemporaryToken tt = loginCreateSetup(MFAStatus.NOT_USED); final URI target = UriBuilder.fromUri(host).path("/login/create").build(); @@ -2645,7 +2673,7 @@ public void loginCreateJSONEmptyInput() throws Exception { @SuppressWarnings("unchecked") final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.NOT_USED); final AuthUser u = manager.storage.getUser(new UserName("u1")); TestCommon.assertCloseToNow(u.getLastLogin().get()); @@ -2689,7 +2717,7 @@ public void loginCreateFailEmptyID() throws Exception { @Test public void loginCreateFailBadToken() throws Exception { - loginChoiceSetup(); + loginCreateSetup(MFAStatus.UNKNOWN); final URI target = UriBuilder.fromUri(host) .path("/login/create") @@ -2884,7 +2912,7 @@ public void loginCreateFailBadEmail() throws Exception { "e1@g.@com")); } - private TemporaryToken loginChoiceSetup() throws Exception { + private TemporaryToken loginCreateSetup(final MFAStatus mfa) throws Exception { final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); enableLogin(host, admintoken); @@ -2893,7 +2921,7 @@ private TemporaryToken loginChoiceSetup() throws Exception { final TemporarySessionData data = TemporarySessionData.create( UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(set(REMOTE1, REMOTE2)); + .login(set(REMOTE1, REMOTE2), mfa); final TemporaryToken tt = new TemporaryToken(data, "this is a token"); diff --git a/src/test/java/us/kbase/test/auth2/service/ui/SimpleEndpointsTest.java b/src/test/java/us/kbase/test/auth2/service/ui/SimpleEndpointsTest.java index 12b3b0ba..d863cfa5 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/SimpleEndpointsTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/SimpleEndpointsTest.java @@ -55,6 +55,7 @@ import us.kbase.auth2.lib.exceptions.NoTokenProvidedException; import us.kbase.auth2.lib.exceptions.PasswordMismatchException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenType; import us.kbase.auth2.lib.user.LocalUser; @@ -521,7 +522,8 @@ public void localLoginSuccessMinimalInput() throws Exception { assertThat("incorrect auth cookie less token", token, is(expectedtoken)); ServiceTestUtils.checkStoredToken(manager, token.getValue(), Collections.emptyMap(), - new UserName("whoo"), TokenType.LOGIN, null, 14 * 24 * 3600 * 1000); + new UserName("whoo"), TokenType.LOGIN, MFAStatus.UNKNOWN, null, + 14 * 24 * 3600 * 1000); } @Test @@ -556,7 +558,8 @@ public void localLoginSuccessMaximalInput() throws Exception { ServiceTestUtils.checkStoredToken(manager, token.getValue(), ImmutableMap.of("foo", "bar", "baz", "bat"), - new UserName("whoo"), TokenType.LOGIN, null, 14 * 24 * 3600 * 1000); + new UserName("whoo"), TokenType.LOGIN, MFAStatus.UNKNOWN, null, + 14 * 24 * 3600 * 1000); } @Test diff --git a/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java b/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java index ecf2a3b6..5f64fa01 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java @@ -51,6 +51,7 @@ import us.kbase.auth2.lib.exceptions.NoSuchTokenException; import us.kbase.auth2.lib.exceptions.NoTokenProvidedException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; import us.kbase.auth2.lib.token.TokenType; @@ -389,7 +390,8 @@ public void createTokenMinimalInput() throws Exception { assertThat("incorrect expires", expires, is(created + 90 * 24 * 3600 * 1000L)); ServiceTestUtils.checkStoredToken(manager, newtoken, id, created, Collections.emptyMap(), - new UserName("whoo"), TokenType.DEV, "foo", 90 * 24 * 3600 * 1000L); + new UserName("whoo"), TokenType.DEV, MFAStatus.UNKNOWN, "foo", + 90 * 24 * 3600 * 1000L); final Builder req2 = wt.request() @@ -403,7 +405,8 @@ public void createTokenMinimalInput() throws Exception { assertThat("incorrect response code", res.getStatus(), is(200)); ServiceTestUtils.checkReturnedToken(manager, json, Collections.emptyMap(), - new UserName("whoo"), TokenType.DEV, "foo", 90 * 24 * 3600 * 1000L, true); + new UserName("whoo"), TokenType.DEV, MFAStatus.UNKNOWN, "foo", + 90 * 24 * 3600 * 1000L, true); } @Test @@ -459,7 +462,8 @@ public void createTokenMaximalInput() throws Exception { ServiceTestUtils.checkStoredToken(manager, newtoken, id, created, ImmutableMap.of("foo", "bar", "baz", "bat"), - new UserName("whoo"), TokenType.SERV, "foo", 100_000_000L * 24 * 3600 * 1000L); + new UserName("whoo"), TokenType.SERV, MFAStatus.UNKNOWN, "foo", + 100_000_000L * 24 * 3600 * 1000L); final Builder req2 = wt.request() @@ -477,7 +481,7 @@ public void createTokenMaximalInput() throws Exception { ServiceTestUtils.checkReturnedToken(manager, json, ImmutableMap.of("foo", "bar", "baz", "bat"), - new UserName("whoo"), TokenType.SERV, "foo", + new UserName("whoo"), TokenType.SERV, MFAStatus.UNKNOWN, "foo", 100_000_000L * 24 * 3600 * 1000L, true); } } From 801888b5c4c31546bf0589a9476ff8f5994e22f5 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 13:57:02 -0800 Subject: [PATCH 20/24] Allow specifying the MFA status for test mode tokens Currently the tests other than the Authentication unit tests can't see the MFA status because it's not yet in the ExternalToken. Once that happends there will be changes all over so I'll do that later. --- .../us/kbase/auth2/lib/Authentication.java | 5 +- .../us/kbase/auth2/service/api/TestMode.java | 57 ++++++++++++------- .../us/kbase/auth2/service/common/Fields.java | 2 + .../lib/AuthenticationTestModeTokenTest.java | 29 +++++++--- .../service/api/TestModeIntegrationTest.java | 19 +++++-- .../test/auth2/service/api/TestModeTest.java | 52 +++++++++++------ 6 files changed, 115 insertions(+), 49 deletions(-) diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index aeff80b8..0c1662da 100644 --- a/src/main/java/us/kbase/auth2/lib/Authentication.java +++ b/src/main/java/us/kbase/auth2/lib/Authentication.java @@ -2043,16 +2043,19 @@ public NewToken createUser( public NewToken testModeCreateToken( final UserName userName, final TokenName tokenName, - final TokenType tokenType) + final TokenType tokenType, + final MFAStatus mfa) throws TestModeException, AuthStorageException, NoSuchUserException { ensureTestMode(); requireNonNull(userName, "userName"); requireNonNull(tokenType, "tokenType"); + requireNonNull(mfa, "mfa"); storage.testModeGetUser(userName); // ensure user exists final UUID id = randGen.randomUUID(); final NewToken nt = new NewToken(StoredToken.getBuilder(tokenType, id, userName) .withLifeTime(clock.instant(), TEST_MODE_DATA_LIFETIME_MS) .withNullableTokenName(tokenName) + .withMFA(mfa) .build(), randGen.getToken()); storage.testModeStoreToken(nt.getStoredToken(), nt.getTokenHash()); diff --git a/src/main/java/us/kbase/auth2/service/api/TestMode.java b/src/main/java/us/kbase/auth2/service/api/TestMode.java index 73c46d1b..35589b61 100644 --- a/src/main/java/us/kbase/auth2/service/api/TestMode.java +++ b/src/main/java/us/kbase/auth2/service/api/TestMode.java @@ -50,6 +50,7 @@ import us.kbase.auth2.lib.exceptions.UnauthorizedException; import us.kbase.auth2.lib.exceptions.UserExistsException; import us.kbase.auth2.lib.storage.exceptions.AuthStorageException; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; import us.kbase.auth2.lib.token.TokenType; @@ -119,22 +120,6 @@ public Map getTestUser(@PathParam(APIPaths.USERNAME) final Strin return Me.toUserMap(auth.testModeGetUser(new UserName(userName))); } - public static class CreateTestToken extends IncomingJSON { - public final String userName; - public final String tokenName; - public final String tokenType; - - @JsonCreator - public CreateTestToken( - @JsonProperty(Fields.USER) final String userName, - @JsonProperty(Fields.TOKEN_NAME) final String tokenName, - @JsonProperty(Fields.TOKEN_TYPE) final String tokenType) { - this.userName = userName; - this.tokenName = tokenName; - this.tokenType = tokenType; - } - } - @GET @Path(APIPaths.TESTMODE_USER_DISPLAY) @Produces(MediaType.APPLICATION_JSON) @@ -150,6 +135,26 @@ public Map getUsers( Collectors.toMap(e -> e.getKey().getName(), e -> e.getValue().getName())); } + public static class CreateTestToken extends IncomingJSON { + public final String userName; + public final String tokenName; + public final String tokenType; + public final String mfa; + + @JsonCreator + public CreateTestToken( + @JsonProperty(Fields.USER) final String userName, + @JsonProperty(Fields.TOKEN_NAME) final String tokenName, + @JsonProperty(Fields.TOKEN_TYPE) final String tokenType, + @JsonProperty(Fields.TOKEN_MFA) final String mfa + ) { + this.userName = userName; + this.tokenName = tokenName; + this.tokenType = tokenType; + this.mfa = mfa; + } + } + @POST @Path(APIPaths.TESTMODE_TOKEN_CREATE) @Consumes(MediaType.APPLICATION_JSON) @@ -161,10 +166,24 @@ public NewAPIToken createTestToken(final CreateTestToken create) throw new MissingParameterException("JSON body missing"); } create.exceptOnAdditionalProperties(); + final MFAStatus mfa; + if (create.mfa == null || create.mfa.trim().isEmpty()) { + mfa = MFAStatus.UNKNOWN; + } else { + try { + // TODO CODE this should really be from description, but they're identical + // currently so we just use fromID + mfa = MFAStatus.fromID(create.mfa.trim()); + } catch (IllegalArgumentException e) { + throw new IllegalParameterException("Unknown MFA state: " + create.mfa); + } + } return new NewAPIToken(auth.testModeCreateToken( - new UserName(create.userName), - create.tokenName == null ? null : new TokenName(create.tokenName), - getTokenType(create.tokenType)), + new UserName(create.userName), + create.tokenName == null ? null : new TokenName(create.tokenName), + getTokenType(create.tokenType), + mfa + ), auth.getSuggestedTokenCacheTime()); } diff --git a/src/main/java/us/kbase/auth2/service/common/Fields.java b/src/main/java/us/kbase/auth2/service/common/Fields.java index 6663671a..8bb8a3f2 100644 --- a/src/main/java/us/kbase/auth2/service/common/Fields.java +++ b/src/main/java/us/kbase/auth2/service/common/Fields.java @@ -174,6 +174,8 @@ public class Fields { public static final String TOKEN_NAME = "name"; /** The type of a token. */ public static final String TOKEN_TYPE = "type"; + /** The mfa status of a token. */ + public static final String TOKEN_MFA = "mfa"; /** Whether the user can create developer tokens. */ public static final String TOKEN_DEV = "dev"; /** Whether the user can create service tokens. */ diff --git a/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeTokenTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeTokenTest.java index 84a1f0ec..d7b227c4 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeTokenTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationTestModeTokenTest.java @@ -30,6 +30,7 @@ import us.kbase.auth2.lib.exceptions.TestModeException; import us.kbase.auth2.lib.storage.AuthStorage; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.NewToken; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; @@ -71,17 +72,21 @@ public void createTokenWithoutName() throws Exception { when(clock.instant()).thenReturn(Instant.ofEpochMilli(10000)); when(rand.getToken()).thenReturn("whee"); - final NewToken nt = auth.testModeCreateToken(new UserName("foo"), null, TokenType.AGENT); + final NewToken nt = auth.testModeCreateToken( + new UserName("foo"), null, TokenType.AGENT, MFAStatus.USED + ); assertThat("incorrect token", nt, is(new NewToken(StoredToken.getBuilder( TokenType.AGENT, id, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(3610000)) + .withMFA(MFAStatus.USED) .build(), "whee"))); verify(storage).testModeStoreToken(StoredToken.getBuilder( TokenType.AGENT, id, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(3610000)) + .withMFA(MFAStatus.USED) .build(), IncomingToken.hash("whee")); @@ -107,12 +112,13 @@ public void createTokenWithName() throws Exception { when(rand.getToken()).thenReturn("whee"); final NewToken nt = auth.testModeCreateToken( - new UserName("foo"), new TokenName("tok"), TokenType.SERV); + new UserName("foo"), new TokenName("tok"), TokenType.SERV, MFAStatus.NOT_USED); assertThat("incorrect token", nt, is(new NewToken(StoredToken.getBuilder( TokenType.SERV, id, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(3610000)) .withTokenName(new TokenName("tok")) + .withMFA(MFAStatus.NOT_USED) .build(), "whee"))); @@ -120,6 +126,7 @@ TokenType.SERV, id, new UserName("foo")) TokenType.SERV, id, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(3610000)) .withTokenName(new TokenName("tok")) + .withMFA(MFAStatus.NOT_USED) .build(), IncomingToken.hash("whee")); @@ -131,14 +138,19 @@ TokenType.SERV, id, new UserName("foo")) @Test public void createTokenFailNulls() throws Exception { final Authentication auth = initTestMocks(true).auth; - failCreateToken(auth, null, TokenType.DEV, new NullPointerException("userName")); - failCreateToken(auth, new UserName("u"), null, new NullPointerException("tokenType")); + final TokenType tt = TokenType.DEV; + final MFAStatus m = MFAStatus.NOT_USED; + failCreateToken(auth, null, tt, m, new NullPointerException("userName")); + failCreateToken(auth, new UserName("u"), null, m, new NullPointerException("tokenType")); + failCreateToken(auth, new UserName("u"), tt, null, new NullPointerException("mfa")); } @Test public void createTokenFailNoTestMode() throws Exception { - failCreateToken(initTestMocks(false).auth, new UserName("u"), TokenType.DEV, - new TestModeException(ErrorType.UNSUPPORTED_OP, "Test mode is not enabled")); + failCreateToken( + initTestMocks(false).auth, new UserName("u"), TokenType.DEV, MFAStatus.UNKNOWN, + new TestModeException(ErrorType.UNSUPPORTED_OP, "Test mode is not enabled") + ); } @Test @@ -150,7 +162,7 @@ public void createTokenFailNoUser() throws Exception { when(storage.testModeGetUser(new UserName("foo"))) .thenThrow(new NoSuchUserException("foo")); - failCreateToken(auth, new UserName("foo"), TokenType.AGENT, + failCreateToken(auth, new UserName("foo"), TokenType.AGENT, MFAStatus.UNKNOWN, new NoSuchUserException("foo")); } @@ -158,9 +170,10 @@ private void failCreateToken( final Authentication auth, final UserName userName, final TokenType tokenType, + final MFAStatus mfa, final Exception expected) { try { - auth.testModeCreateToken(userName, null, tokenType); + auth.testModeCreateToken(userName, null, tokenType, mfa); fail("expected exception"); } catch (Exception got) { TestCommon.assertExceptionCorrect(got, expected); diff --git a/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java index 748a9566..f2a61abf 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java @@ -190,7 +190,7 @@ public void createAndGetToken() { ImmutableMap.of("user", "whee", "display", "whoo"))); assertThat("user create failed", ures.getStatus(), is(200)); - final Map response = createToken("whee", "Login", "foo"); + final Map response = createToken("whee", "Login", "foo", "Used"); final long created = (long) response.get("created"); response.remove("created"); @@ -240,19 +240,30 @@ private Map getToken(final String token, final int code) { final Map response2 = res2.readEntity(Map.class); return response2; } - private Map createToken( final String user, final String type, final String name) { + return createToken(user, type, name, null); + } + + private Map createToken( + final String user, + final String type, + final String name, + final String mfa) { final URI target = UriBuilder.fromUri(host).path("/testmode/api/V2/testmodeonly/token/") .build(); final WebTarget wt = CLI.target(target); final Builder req = wt.request(); + final Map json = new HashMap<>(); + json.put("user", user); + json.put("type", type); + json.put("name", name); + json.put("mfa", mfa); - final Response res = req.post(Entity.json( - ImmutableMap.of("user", user, "type", type, "name", name))); + final Response res = req.post(Entity.json(json)); assertThat("incorrect response code", res.getStatus(), is(200)); diff --git a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java index a0e1e272..40947ba3 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java @@ -46,6 +46,7 @@ import us.kbase.auth2.lib.exceptions.TestModeException; import us.kbase.auth2.lib.exceptions.UnauthorizedException; import us.kbase.auth2.lib.token.IncomingToken; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.NewToken; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; @@ -314,24 +315,30 @@ public void createTokenNoName() throws Exception { final UUID uuid = UUID.randomUUID(); - when(auth.testModeCreateToken(new UserName("foo"), null, TokenType.DEV)) + when(auth.testModeCreateToken(new UserName("foo"), null, TokenType.DEV, MFAStatus.UNKNOWN)) .thenReturn(new NewToken(StoredToken.getBuilder( TokenType.DEV, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) + .withMFA(MFAStatus.UNKNOWN) .build(), "a token")); when(auth.getSuggestedTokenCacheTime()).thenReturn(30000L); - final NewAPIToken token = tm.createTestToken(new CreateTestToken("foo", null, "Dev")); - - final NewAPIToken expected = new NewAPIToken(new NewToken(StoredToken.getBuilder( - TokenType.DEV, uuid, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) - .build(), - "a token"), 30000L); - - assertThat("incorrect token", token, is(expected)); + for (final String mfa: Arrays.asList(null, "", " \t ")) { + final NewAPIToken token = tm.createTestToken( + new CreateTestToken("foo", null, "Dev", mfa + )); + + final NewAPIToken expected = new NewAPIToken(new NewToken(StoredToken.getBuilder( + TokenType.DEV, uuid, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) + .withMFA(MFAStatus.USED) + .build(), + "a token"), 30000L); + + assertThat("incorrect token", token, is(expected)); + } } @Test @@ -341,17 +348,20 @@ public void createTokenWithName() throws Exception { final UUID uuid = UUID.randomUUID(); - when(auth.testModeCreateToken(new UserName("foo"), new TokenName("whee"), TokenType.AGENT)) + when(auth.testModeCreateToken( + new UserName("foo"), new TokenName("whee"), TokenType.AGENT, MFAStatus.USED)) .thenReturn(new NewToken(StoredToken.getBuilder( TokenType.AGENT, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) .withTokenName(new TokenName("whee")) + .withMFA(MFAStatus.USED) .build(), "a token")); when(auth.getSuggestedTokenCacheTime()).thenReturn(30000L); - final NewAPIToken token = tm.createTestToken(new CreateTestToken("foo", "whee", "Agent")); + final NewAPIToken token = tm.createTestToken( + new CreateTestToken("foo", "whee", "Agent", " Used \t ")); final NewAPIToken expected = new NewAPIToken(new NewToken(StoredToken.getBuilder( TokenType.AGENT, uuid, new UserName("foo")) @@ -373,11 +383,11 @@ public void createTokenFailNoJson() { public void createTokenFailNulls() { final TestMode tm = new TestMode(mock(Authentication.class)); - failCreateToken(tm, new CreateTestToken(null, "foo", "Dev"), + failCreateToken(tm, new CreateTestToken(null, "foo", "Dev", null), new MissingParameterException("user name")); - failCreateToken(tm, new CreateTestToken("foo", " \t \n ", "Dev"), + failCreateToken(tm, new CreateTestToken("foo", " \t \n ", "Dev", null), new MissingParameterException("token name")); - failCreateToken(tm, new CreateTestToken("whee", "foo", null), + failCreateToken(tm, new CreateTestToken("whee", "foo", null, null), new IllegalParameterException("Invalid token type: null")); } @@ -385,14 +395,22 @@ public void createTokenFailNulls() { public void createTokenFailBadTokenType() { final TestMode tm = new TestMode(mock(Authentication.class)); - failCreateToken(tm, new CreateTestToken("whee", "foo", "Devv"), + failCreateToken(tm, new CreateTestToken("whee", "foo", "Devv", null), new IllegalParameterException("Invalid token type: Devv")); } + @Test + public void createTokenFailBadMFAType() { + final TestMode tm = new TestMode(mock(Authentication.class)); + + failCreateToken(tm, new CreateTestToken("whee", "foo", "Dev", "SlightlyUsed"), + new IllegalParameterException("Unknown MFA state: SlightlyUsed")); + } + @Test public void createTokenFailAddlProps() { final TestMode tm = new TestMode(mock(Authentication.class)); - final CreateTestToken create = new CreateTestToken("foo", "bar", "baz"); + final CreateTestToken create = new CreateTestToken("foo", "bar", "baz", null); create.setAdditionalProperties("whee", "whoo"); failCreateToken(tm, create, new IllegalParameterException( "Unexpected parameters in request: whee")); From e28ad11a540a4010659dd1b51d16e6201fbffaee Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 16:54:38 -0800 Subject: [PATCH 21/24] Add mfa to external endpoints Admin template endpoints were tested manually --- docker-compose.yml | 1 - .../auth2/service/common/ExternalToken.java | 77 +++++-------------- .../test/auth2/service/ServiceTestUtils.java | 1 + .../service/api/TestModeIntegrationTest.java | 3 +- .../test/auth2/service/api/TestModeTest.java | 6 +- .../auth2/service/api/TokenEndpointTest.java | 2 + .../service/common/ExternalTokenTest.java | 4 + .../test/auth2/service/ui/TokensTest.java | 6 ++ .../TokensTest_getTokensMaximalInput.testdata | 3 + .../TokensTest_getTokensMinimalInput.testdata | 1 + templates/admintoken.mustache | 1 + templates/adminusertokens.mustache | 1 + templates/tokens.mustache | 2 + 13 files changed, 46 insertions(+), 62 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1e3187a1..353b6072 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.1" # This is just an example that shows the relationships between the auth2 image # and other services. Many of these things would be overidden in the actual # deployment docker-compose file - for example, the name of the mongodb instance diff --git a/src/main/java/us/kbase/auth2/service/common/ExternalToken.java b/src/main/java/us/kbase/auth2/service/common/ExternalToken.java index 363a0c55..f61577b3 100644 --- a/src/main/java/us/kbase/auth2/service/common/ExternalToken.java +++ b/src/main/java/us/kbase/auth2/service/common/ExternalToken.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import java.util.Map; +import java.util.Objects; import us.kbase.auth2.lib.token.StoredToken; @@ -17,6 +18,7 @@ public class ExternalToken { private final String name; private final String user; private final Map custom; + private final String mfa; public ExternalToken(final StoredToken storedToken) { requireNonNull(storedToken, "storedToken"); @@ -28,11 +30,16 @@ public ExternalToken(final StoredToken storedToken) { expires = storedToken.getExpirationDate().toEpochMilli(); created = storedToken.getCreationDate().toEpochMilli(); custom = storedToken.getContext().getCustomContext(); + mfa = storedToken.getMFA().getDescription(); } public String getType() { return type; } + + public String getMfa() { // must be Lowercase or templates don't work + return mfa; + } public String getId() { return id; @@ -60,71 +67,25 @@ public Map getCustom() { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (int) (created ^ (created >>> 32)); - result = prime * result + ((custom == null) ? 0 : custom.hashCode()); - result = prime * result + (int) (expires ^ (expires >>> 32)); - result = prime * result + ((id == null) ? 0 : id.hashCode()); - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((type == null) ? 0 : type.hashCode()); - result = prime * result + ((user == null) ? 0 : user.hashCode()); - return result; + return Objects.hash(created, custom, expires, id, mfa, name, type, user); } @Override public boolean equals(Object obj) { - if (this == obj) { + if (this == obj) return true; - } - if (obj == null) { + if (obj == null) return false; - } - if (getClass() != obj.getClass()) { + if (getClass() != obj.getClass()) return false; - } ExternalToken other = (ExternalToken) obj; - if (created != other.created) { - return false; - } - if (custom == null) { - if (other.custom != null) { - return false; - } - } else if (!custom.equals(other.custom)) { - return false; - } - if (expires != other.expires) { - return false; - } - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - if (name == null) { - if (other.name != null) { - return false; - } - } else if (!name.equals(other.name)) { - return false; - } - if (type == null) { - if (other.type != null) { - return false; - } - } else if (!type.equals(other.type)) { - return false; - } - if (user == null) { - if (other.user != null) { - return false; - } - } else if (!user.equals(other.user)) { - return false; - } - return true; + return created == other.created + && Objects.equals(custom, other.custom) + && expires == other.expires + && Objects.equals(id, other.id) + && Objects.equals(mfa, other.mfa) + && Objects.equals(name, other.name) + && Objects.equals(type, other.type) + && Objects.equals(user, other.user); } } diff --git a/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java b/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java index 3ffc9c4c..a8d713c4 100644 --- a/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java +++ b/src/test/java/us/kbase/test/auth2/service/ServiceTestUtils.java @@ -233,6 +233,7 @@ public static void checkReturnedToken( assertThat("incorrect token context", uitoken.get("custom"), is(customContext)); assertThat("incorrect token type", uitoken.get("type"), is(type.getDescription())); + assertThat("incorrect mfa", uitoken.get("mfa"), is(mfa.getDescription())); final long created = (long) uitoken.get("created"); TestCommon.assertCloseToNow(created); assertThat("incorrect expires", uitoken.get("expires"), diff --git a/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java index f2a61abf..844abfa0 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java @@ -190,7 +190,7 @@ public void createAndGetToken() { ImmutableMap.of("user", "whee", "display", "whoo"))); assertThat("user create failed", ures.getStatus(), is(200)); - final Map response = createToken("whee", "Login", "foo", "Used"); + final Map response = createToken("whee", "Login", "foo", "NotUsed"); final long created = (long) response.get("created"); response.remove("created"); @@ -205,6 +205,7 @@ public void createAndGetToken() { final Map expected = new HashMap<>(); expected.put("type", "Login"); + expected.put("mfa", "NotUsed"); expected.put("name", "foo"); expected.put("user", "whee"); expected.put("custom", Collections.emptyMap()); diff --git a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java index 40947ba3..428d6466 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java @@ -319,7 +319,6 @@ public void createTokenNoName() throws Exception { .thenReturn(new NewToken(StoredToken.getBuilder( TokenType.DEV, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) - .withMFA(MFAStatus.UNKNOWN) .build(), "a token")); @@ -333,7 +332,7 @@ TokenType.DEV, uuid, new UserName("foo")) final NewAPIToken expected = new NewAPIToken(new NewToken(StoredToken.getBuilder( TokenType.DEV, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) - .withMFA(MFAStatus.USED) + .withMFA(MFAStatus.UNKNOWN) .build(), "a token"), 30000L); @@ -367,6 +366,7 @@ TokenType.AGENT, uuid, new UserName("foo")) TokenType.AGENT, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) .withTokenName(new TokenName("whee")) + .withMFA(MFAStatus.USED) .build(), "a token"), 30000L); @@ -438,6 +438,7 @@ public void getToken() throws Exception { when(auth.testModeGetToken(new IncomingToken("a token"))).thenReturn( StoredToken.getBuilder(TokenType.DEV, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(30000)) + .withMFA(MFAStatus.NOT_USED) .build()); when(auth.getSuggestedTokenCacheTime()).thenReturn(40000L); @@ -447,6 +448,7 @@ public void getToken() throws Exception { final APIToken expected = new APIToken(StoredToken.getBuilder( TokenType.DEV, uuid, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(30000)) + .withMFA(MFAStatus.NOT_USED) .build(), 40000); diff --git a/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java b/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java index 0e449f99..1eea37fc 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TokenEndpointTest.java @@ -151,6 +151,7 @@ TokenType.AGENT, id, new UserName("foo")) .withTokenName(new TokenName("bar")) .withContext(TokenCreationContext.getBuilder() .withCustomContext("whee", "whoo").build()) + .withMFA(MFAStatus.USED) .build(), it.getHashedToken().getTokenHash()); final URI target = UriBuilder.fromUri(host).path("/api/V2/token").build(); @@ -168,6 +169,7 @@ TokenType.AGENT, id, new UserName("foo")) final Map expected = MapBuilder.newHashMap() .with("type", "Agent") + .with("mfa", "Used") .with("id", id.toString()) .with("created", 10000) .with("expires", 1000000000000000L) diff --git a/src/test/java/us/kbase/test/auth2/service/common/ExternalTokenTest.java b/src/test/java/us/kbase/test/auth2/service/common/ExternalTokenTest.java index 48b5111a..1b024e32 100644 --- a/src/test/java/us/kbase/test/auth2/service/common/ExternalTokenTest.java +++ b/src/test/java/us/kbase/test/auth2/service/common/ExternalTokenTest.java @@ -14,6 +14,7 @@ import nl.jqno.equalsverifier.EqualsVerifier; import us.kbase.auth2.lib.TokenCreationContext; import us.kbase.auth2.lib.UserName; +import us.kbase.auth2.lib.token.MFAStatus; import us.kbase.auth2.lib.token.StoredToken; import us.kbase.auth2.lib.token.TokenName; import us.kbase.auth2.lib.token.TokenType; @@ -46,6 +47,7 @@ TokenType.AGENT, id, new UserName("foo")) assertThat("incorrect name", et.getName(), is("bar")); assertThat("incorrect custom context", et.getCustom(), is(ImmutableMap.of("whee", "whoo"))); + assertThat("incorrect MFA", et.getMfa(), is("Unknown")); } @Test @@ -56,6 +58,7 @@ TokenType.AGENT, id, new UserName("foo")) .withLifeTime(Instant.ofEpochMilli(10000), 15000) .withContext(TokenCreationContext.getBuilder() .withCustomContext("whee", "whoo").build()) + .withMFA(MFAStatus.USED) .build()); assertThat("incorrect type", et.getType(), is("Agent")); @@ -66,6 +69,7 @@ TokenType.AGENT, id, new UserName("foo")) assertThat("incorrect name", et.getName(), is((String) null)); assertThat("incorrect custom context", et.getCustom(), is(ImmutableMap.of("whee", "whoo"))); + assertThat("incorrect MFA", et.getMfa(), is("Used")); } @Test diff --git a/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java b/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java index 5f64fa01..fe426485 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/TokensTest.java @@ -154,6 +154,7 @@ public void getTokensMinimalInput() throws Exception { .with("service", false) .with("current", MapBuilder.newHashMap() .with("type", "Login") + .with("mfa", "Unknown") .with("id", id) .with("expires", 1000000000010000L) .with("created", 10000) @@ -200,6 +201,7 @@ public void getTokensMaximalInput() throws Exception { .withNullableDevice("dev") .withNullableOS("o", "osv") .build()) + .withMFA(MFAStatus.USED) .build(), token.getHashedToken().getTokenHash()); @@ -211,6 +213,7 @@ public void getTokensMaximalInput() throws Exception { .withNullableAgent("ag2", "agv2") .withNullableDevice("dev2") .build()) + .withMFA(MFAStatus.NOT_USED) // this should never happen for an agent token fwiw .build(), "somehash"); @@ -256,6 +259,7 @@ public void getTokensMaximalInput() throws Exception { .with("service", true) .with("current", MapBuilder.newHashMap() .with("type", "Login") + .with("mfa", "Used") .with("id", id) .with("expires", 1000000000010000L) .with("created", 10000) @@ -272,6 +276,7 @@ public void getTokensMaximalInput() throws Exception { .with("tokens", Arrays.asList( MapBuilder.newHashMap() .with("type", "Developer") + .with("mfa", "Unknown") .with("id", id3) .with("expires", 3000000000030000L) .with("created", 30000) @@ -287,6 +292,7 @@ public void getTokensMaximalInput() throws Exception { .build(), MapBuilder.newHashMap() .with("type", "Agent") + .with("mfa", "NotUsed") .with("id", id2) .with("expires", 2000000000020000L) .with("created", 20000) diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMaximalInput.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMaximalInput.testdata index 193a2fe1..c4189c92 100644 --- a/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMaximalInput.testdata +++ b/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMaximalInput.testdata @@ -19,6 +19,7 @@ Expiration and creation dates are in milliseconds from the epoch. Name: wugga
ID: edc1dcbb-d370-4660-a639-01a72f0d578a
Type: Login
+MFA: Used
Created: 10000
Expires: 1000000000010000
OS: o osv
@@ -31,6 +32,7 @@ Custom: {foo=bar}
Name: whee
ID: 653cc5ce-37e6-4e61-ac25-48831657f257
Type: Developer
+MFA: Unknown
Created: 30000
Expires: 3000000000030000
OS:
@@ -44,6 +46,7 @@ Custom: {}

ID: 8351a73a-d4c7-4c00-9a7d-012ace5d9519
Type: Agent
+MFA: NotUsed
Created: 20000
Expires: 2000000000020000
OS:
diff --git a/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMinimalInput.testdata b/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMinimalInput.testdata index 728b30d3..4dac8b67 100644 --- a/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMinimalInput.testdata +++ b/src/test/resources/us/kbase/test/auth2/service/ui/TokensTest_getTokensMinimalInput.testdata @@ -11,6 +11,7 @@ Expiration and creation dates are in milliseconds from the epoch.

Current token:

ID: edc1dcbb-d370-4660-a639-01a72f0d578a
Type: Login
+MFA: Unknown
Created: 10000
Expires: 1000000000010000
OS:
diff --git a/templates/admintoken.mustache b/templates/admintoken.mustache index de34b1d0..1190d552 100644 --- a/templates/admintoken.mustache +++ b/templates/admintoken.mustache @@ -9,6 +9,7 @@ Name: {{name}}
{{/name}} ID: {{id}}
Type: {{type}}
+MFA: {{mfa}}
Created: {{created}}
Expires: {{expires}}
OS: {{os}} {{osver}}
diff --git a/templates/adminusertokens.mustache b/templates/adminusertokens.mustache index dd41c2bd..a2b76ab0 100644 --- a/templates/adminusertokens.mustache +++ b/templates/adminusertokens.mustache @@ -15,6 +15,7 @@ Name: {{name}}
{{/name}} ID: {{id}}
Type: {{type}}
+MFA: {{mfa}}
Created: {{created}}
Expires: {{expires}}
OS: {{os}} {{osver}}
diff --git a/templates/tokens.mustache b/templates/tokens.mustache index ffe0a907..f573e45b 100644 --- a/templates/tokens.mustache +++ b/templates/tokens.mustache @@ -26,6 +26,7 @@ Name: {{name}}
{{/name}} ID: {{id}}
Type: {{type}}
+MFA: {{mfa}}
Created: {{created}}
Expires: {{expires}}
OS: {{os}} {{osver}}
@@ -42,6 +43,7 @@ Name: {{name}}
{{/name}} ID: {{id}}
Type: {{type}}
+MFA: {{mfa}}
Created: {{created}}
Expires: {{expires}}
OS: {{os}} {{osver}}
From 9c678b83339f5695583a6e07c1f0418dc201b969 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 17:54:25 -0800 Subject: [PATCH 22/24] Clarify comment --- src/main/java/us/kbase/auth2/service/common/ExternalToken.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/us/kbase/auth2/service/common/ExternalToken.java b/src/main/java/us/kbase/auth2/service/common/ExternalToken.java index f61577b3..0a5a703a 100644 --- a/src/main/java/us/kbase/auth2/service/common/ExternalToken.java +++ b/src/main/java/us/kbase/auth2/service/common/ExternalToken.java @@ -37,7 +37,7 @@ public String getType() { return type; } - public String getMfa() { // must be Lowercase or templates don't work + public String getMfa() { // method name must be Lowercase or templates don't work return mfa; } From e3e0b4b11cb00cbe44f63db89eac1135c622dd8b Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Tue, 18 Nov 2025 17:50:18 -0800 Subject: [PATCH 23/24] Bump version + release notes --- RELEASE_NOTES.md | 10 +++++++++- src/main/java/us/kbase/auth2/Version.java | 2 +- .../test/auth2/service/common/ServiceCommonTest.java | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1ecd3193..4b3b2513 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,14 +1,22 @@ # Authentication Service MKII release notes -## 0.7.2 +## 0.8.0 +* BACKWARDS INCOMPATIBILITY: In flight login sessions when the server is upgraded will fail. + For a completely safe transition, stop the server, remove any temporary session data, and + bring the new server up. * BACKWARDS INCOMPATIBILITY: Repeated or trailing underscores are no longer allowed in usernames. Existing usernames are unaffected. +* The MultiFactor Authentication status is now available for tokens fetched from the service. + Currently only OrcID supports MFA statuses other than `Unknown`. Other statuses are `Used` and + `Not Used`. * Fixed a bug where usernames with underscores would not be matched in username searches if an underscore was an interior character of a search prefix. * Fixed a bug where a MongoDB error would be thrown if a user search prefix resulted in no search terms if it had no valid characters for the requested search, whether user name or display name. Now a service error is thrown. +* The `/tokens` endpoint can now accept `Service` or `service` to specify that a service token + should be created. ## 0.7.1 diff --git a/src/main/java/us/kbase/auth2/Version.java b/src/main/java/us/kbase/auth2/Version.java index bd75936f..2e472882 100644 --- a/src/main/java/us/kbase/auth2/Version.java +++ b/src/main/java/us/kbase/auth2/Version.java @@ -5,6 +5,6 @@ public class Version { /** The version of the KBase Auth2 service. */ - public static final String VERSION = "0.7.2"; + public static final String VERSION = "0.8.0"; } diff --git a/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java b/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java index 5f4983dc..ad9ccae8 100644 --- a/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java +++ b/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java @@ -47,7 +47,7 @@ public class ServiceCommonTest { public static final String SERVICE_NAME = "Authentication Service"; - public static final String SERVER_VER = "0.7.2"; + public static final String SERVER_VER = "0.8.0"; public static final String GIT_ERR = "Missing git commit file gitcommit, should be in us.kbase.auth2"; From 830793bbd45757f73e609dd735696752ffa9d2fb Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Wed, 19 Nov 2025 14:23:45 -0800 Subject: [PATCH 24/24] Improve test mode create token test --- .../test/auth2/service/api/TestModeTest.java | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java index 428d6466..0e6d8a31 100644 --- a/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java +++ b/src/test/java/us/kbase/test/auth2/service/api/TestModeTest.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.Map; +import java.util.Map.Entry; import java.util.UUID; import javax.ws.rs.core.HttpHeaders; @@ -342,35 +343,43 @@ TokenType.DEV, uuid, new UserName("foo")) @Test public void createTokenWithName() throws Exception { - final Authentication auth = mock(Authentication.class); - final TestMode tm = new TestMode(auth); - - final UUID uuid = UUID.randomUUID(); - - when(auth.testModeCreateToken( - new UserName("foo"), new TokenName("whee"), TokenType.AGENT, MFAStatus.USED)) - .thenReturn(new NewToken(StoredToken.getBuilder( - TokenType.AGENT, uuid, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) - .withTokenName(new TokenName("whee")) - .withMFA(MFAStatus.USED) - .build(), - "a token")); - - when(auth.getSuggestedTokenCacheTime()).thenReturn(30000L); - - final NewAPIToken token = tm.createTestToken( - new CreateTestToken("foo", "whee", "Agent", " Used \t ")); - - final NewAPIToken expected = new NewAPIToken(new NewToken(StoredToken.getBuilder( - TokenType.AGENT, uuid, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) - .withTokenName(new TokenName("whee")) - .withMFA(MFAStatus.USED) - .build(), - "a token"), 30000L); - - assertThat("incorrect token", token, is(expected)); + final Map tests = ImmutableMap.of( + " Used \t ", MFAStatus.USED, + " \n NotUsed ", MFAStatus.NOT_USED, + "Unknown", MFAStatus.UNKNOWN + ); + for (final Entry e: tests.entrySet()) { + + final Authentication auth = mock(Authentication.class); + final TestMode tm = new TestMode(auth); + + final UUID uuid = UUID.randomUUID(); + + when(auth.testModeCreateToken( + new UserName("foo"), new TokenName("whee"), TokenType.AGENT, e.getValue())) + .thenReturn(new NewToken(StoredToken.getBuilder( + TokenType.AGENT, uuid, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) + .withTokenName(new TokenName("whee")) + .withMFA(e.getValue()) + .build(), + "a token")); + + when(auth.getSuggestedTokenCacheTime()).thenReturn(30000L); + + final NewAPIToken token = tm.createTestToken( + new CreateTestToken("foo", "whee", "Agent", e.getKey())); + + final NewAPIToken expected = new NewAPIToken(new NewToken(StoredToken.getBuilder( + TokenType.AGENT, uuid, new UserName("foo")) + .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) + .withTokenName(new TokenName("whee")) + .withMFA(e.getValue()) + .build(), + "a token"), 30000L); + + assertThat("incorrect token", token, is(expected)); + } } @Test