diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f506fa4b..d2a5af66 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' + interval: monthly + 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 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 4dfc4809..4b3b2513 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,23 @@ # Authentication Service MKII release notes +## 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 * Publishes a shadow jar on jitpack.io for supporting tests in other repos. 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/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/Version.java b/src/main/java/us/kbase/auth2/Version.java index 30993a69..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.1"; + public static final String VERSION = "0.8.0"; } diff --git a/src/main/java/us/kbase/auth2/lib/Authentication.java b/src/main/java/us/kbase/auth2/lib/Authentication.java index 93ba621d..0c1662da 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; @@ -91,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. * @@ -436,7 +438,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, @@ -511,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) @@ -743,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()); @@ -905,8 +909,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; @@ -1156,7 +1162,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); @@ -1181,10 +1187,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(); @@ -1780,14 +1793,16 @@ 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 && - 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 - @@ -1801,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; } @@ -1824,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); @@ -1861,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(), @@ -1953,7 +1969,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, @@ -1974,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( @@ -2010,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 @@ -2026,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()); @@ -2075,7 +2095,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(); @@ -2298,11 +2318,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( @@ -2332,7 +2353,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( @@ -2520,8 +2541,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 @@ -3165,7 +3186,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..8c220923 --- /dev/null +++ b/src/main/java/us/kbase/auth2/lib/NewUserName.java @@ -0,0 +1,83 @@ +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; + +/** 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); + } + } + + 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. + * @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" + ); + } + } + + /** 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(); + builder.append("NewUserName [getName()="); + builder.append(getName()); + builder.append("]"); + return builder.toString(); + } +} 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/UserName.java b/src/main/java/us/kbase/auth2/lib/UserName.java index 97af8559..64422918 100644 --- a/src/main/java/us/kbase/auth2/lib/UserName.java +++ b/src/main/java/us/kbase/auth2/lib/UserName.java @@ -1,10 +1,12 @@ package us.kbase.auth2.lib; -import static java.util.Objects.requireNonNull; +import static us.kbase.auth2.lib.Utils.checkStringNoCheckedException; -import java.util.Optional; +import java.util.Arrays; +import java.util.List; 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; @@ -16,13 +18,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***"; + protected final static String ROOT_NAME = "***ROOT***"; /** The username for the root user. */ public final static UserName ROOT; @@ -35,8 +36,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); + 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. @@ -68,20 +69,26 @@ 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. + private static String cleanUserName(final String putativeName) { + 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 + * 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 Optional sanitizeName(final String suggestedUserName) { - requireNonNull(suggestedUserName, "suggestedUserName"); - final String s = suggestedUserName.toLowerCase().replaceAll(INVALID_CHARS_REGEX, "") - .replaceAll("^[^a-z]+", ""); - try { - return s.isEmpty() ? Optional.empty() : Optional.of(new UserName(s)); - } catch (IllegalParameterException | MissingParameterException e) { - throw new RuntimeException("This should be impossible", e); - } + 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 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/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/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/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/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/lib/storage/mongo/Fields.java b/src/main/java/us/kbase/auth2/lib/storage/mongo/Fields.java index b05aba6d..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 @@ -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. */ @@ -159,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 e1c2a287..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 @@ -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(); } @@ -1131,6 +1140,11 @@ public Map testModeGetUserDisplayNames(final Set getDisplayNames( final String collection, final Document query, @@ -1156,6 +1170,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 +1181,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 +1195,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); } @@ -1685,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); } @@ -1759,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/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..d7487ec5 --- /dev/null +++ b/src/main/java/us/kbase/auth2/lib/token/MFAStatus.java @@ -0,0 +1,68 @@ +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. 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", "Used"), + + /** User explicitly chose not to use MFA when available. */ + 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", "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/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/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..391051f8 100644 --- a/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java +++ b/src/main/java/us/kbase/auth2/providers/OrcIDIdentityProviderFactory.java @@ -6,8 +6,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.Arrays; -import java.util.HashSet; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Set; @@ -31,9 +30,11 @@ 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; +import us.kbase.auth2.lib.token.MFAStatus; /** A factory for a OrcID identity provider. * @author gaprice@lbl.gov @@ -47,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 { @@ -58,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"; @@ -70,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. @@ -83,6 +94,7 @@ public OrcIDIdentityProvider(final IdentityProviderConfig idc) { idc.getIdentityProviderFactoryClassName()); } this.cfg = idc; + skipMFA = "true".equals(idc.getCustomConfiguation().get(DISABLE_MFA)); } @Override @@ -103,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") @@ -134,7 +147,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 +161,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, accessToken.mfa); } private RemoteIdentity getIdentity(final OrcIDAccessTokenResponse accessToken) @@ -211,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( @@ -228,6 +244,7 @@ private OrcIDAccessTokenResponse( this.accessToken = accessToken.trim(); this.fullName = fullName == null ? null : fullName.trim(); this.orcID = orcID.trim(); + this.mfa = mfaStatus; } } @@ -256,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/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/api/TestMode.java b/src/main/java/us/kbase/auth2/service/api/TestMode.java index 1e054d6f..35589b61 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; @@ -49,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; @@ -99,7 +101,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)); @@ -118,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) @@ -149,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) @@ -160,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/ExternalToken.java b/src/main/java/us/kbase/auth2/service/common/ExternalToken.java index 363a0c55..0a5a703a 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() { // method name 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/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/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/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/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/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/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/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/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/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/AuthenticationLinkTest.java b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java index 9802ff3a..9817d098 100644 --- a/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/AuthenticationLinkTest.java @@ -65,12 +65,14 @@ 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; 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; @@ -323,7 +325,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 +385,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 +443,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 +451,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 +526,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 +593,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 +665,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( @@ -883,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( @@ -1157,7 +1160,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 +1210,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); @@ -1571,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( @@ -1865,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( @@ -2382,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 11f4bbc4..eb3f4bc1 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; @@ -73,12 +74,14 @@ 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; 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; @@ -88,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); @@ -240,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(set(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); @@ -342,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(set(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 @@ -446,7 +471,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); @@ -469,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( @@ -512,13 +538,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( @@ -551,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( @@ -592,13 +618,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( @@ -645,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( @@ -686,13 +717,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( @@ -728,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( @@ -823,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", @@ -1029,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))) @@ -1067,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( @@ -1118,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))) @@ -1164,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( @@ -1234,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( @@ -1378,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 UserName("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 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 UserName("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).deleteTemporarySessionData(token.getHashedToken()); - - assertThat("incorrect new token", nt, is(new NewToken(StoredToken.getBuilder( - TokenType.LOGIN, tokenID, new UserName("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 @@ -1467,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); @@ -1478,12 +1573,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 +1588,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(), @@ -1540,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")))) @@ -1575,16 +1685,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 +1705,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(), @@ -1656,10 +1766,10 @@ 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 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 +1797,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 +1822,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 +1851,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 +1882,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 +1913,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 +1945,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(); @@ -1867,12 +1977,18 @@ 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 - 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(); @@ -1900,23 +2016,29 @@ 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); 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(); @@ -1942,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); @@ -1952,14 +2080,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(); @@ -1986,23 +2114,29 @@ 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); 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(); @@ -2029,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); @@ -2048,11 +2190,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(); @@ -2079,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); @@ -2099,11 +2249,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(); @@ -2132,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), @@ -2145,10 +2303,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 +2316,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, @@ -2175,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 @@ -2267,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"), @@ -2340,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( @@ -2460,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")); @@ -2568,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, @@ -2590,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"), @@ -2616,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"), @@ -2652,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"), @@ -2690,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"), @@ -2730,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"), @@ -2776,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"), @@ -2824,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/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/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/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/UserNameTest.java b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java index 6a99f5e0..93875268 100644 --- a/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java +++ b/src/test/java/us/kbase/test/auth2/lib/UserNameTest.java @@ -3,12 +3,14 @@ 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; 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; @@ -23,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 @@ -57,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, @@ -69,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", @@ -100,24 +135,59 @@ public void compareFail() throws Exception { @Test public void equals() throws Exception { EqualsVerifier.forClass(UserName.class).usingGetClass().verify(); + EqualsVerifier.forClass(NewUserName.class).usingGetClass().verify(); } @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("999 8 6 \t ѱ ** (())"), - is(Optional.empty())); + assertThat( + "incorrect sanitize", + NewUserName.sanitizeName(" 999aF____A8 ea6t \t ѱ ** J___(())___"), + is(Optional.of(new UserName("af_a8ea6tj"))) + ); + assertThat( + "incorrect sanitize", + 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")); } } + @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/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"))); } } 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); + } + } + +} 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/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/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/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(); 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..a3cba428 --- /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("Used")); + assertThat("incorrect NotUsed description", MFAStatus.NOT_USED.getDescription(), + is("NotUsed")); + assertThat("incorrect Unknown description", MFAStatus.UNKNOWN.getDescription(), + is("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)); + } + } +} 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); 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..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,18 +3,16 @@ 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; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import org.junit.After; import org.junit.BeforeClass; @@ -36,7 +34,10 @@ 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; import us.kbase.auth2.providers.OrcIDIdentityProviderFactory.OrcIDIdentityProvider; import us.kbase.test.auth2.TestCommon; @@ -91,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"), @@ -103,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), @@ -175,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")); @@ -197,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( @@ -206,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")); @@ -239,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()), @@ -250,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 @@ -323,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(); @@ -380,31 +451,81 @@ 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)); - final Set 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)); + 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) + ), + mfa + ))); } @Test @@ -418,12 +539,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 @@ -448,6 +568,7 @@ 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 @@ -477,6 +597,7 @@ public void getIdentityWithLinkURLAndEnvironment() throws Exception { new URL("https://ologinredir2.com"), new URL("https://olinkredir2.com")) .withEnvironment("e3", new URL("https://lo.com"), new URL("https://li.com")) + .withCustomConfiguration("disable-mfa", "true") .build(); final IdentityProvider idp = new OrcIDIdentityProvider(idconfig); final String orcID = "0000-0001-1234-5678"; @@ -486,12 +607,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( @@ -503,6 +623,30 @@ private void setUpCallAuthToken( final String name, final String orcID) throws Exception { + setUpCallAuthToken( + authCode, authtoken, redirect, clientID, clientSecret, name, orcID, null + ); + } + + private void setUpCallAuthToken( + final String authCode, + final String authtoken, + final String redirect, + final String clientID, + final String clientSecret, + final String name, + final String orcID, + final String idToken + ) + throws Exception { + Map resp = map( + "access_token", authtoken, + "name", name, + "orcid", orcID + ); + if (idToken != null) { + resp.put("id_token", idToken); + } mockClientAndServer.when( new HttpRequest() .withMethod("POST") @@ -520,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)) ); } @@ -581,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(); 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..a8d713c4 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; @@ -53,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; @@ -83,7 +85,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()); @@ -223,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) @@ -230,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"), @@ -247,7 +251,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( @@ -258,9 +262,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}"))); @@ -290,6 +295,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? @@ -299,9 +305,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}"))); @@ -331,6 +338,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/TestModeIntegrationTest.java b/src/test/java/us/kbase/test/auth2/service/api/TestModeIntegrationTest.java index 748a9566..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"); + 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()); @@ -240,19 +241,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 1657dd2a..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; @@ -32,6 +33,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; @@ -45,6 +47,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; @@ -85,7 +88,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 +110,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 +135,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 +149,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 +157,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, @@ -299,7 +316,7 @@ 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)) @@ -308,44 +325,61 @@ TokenType.DEV, uuid, new UserName("foo")) 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.UNKNOWN) + .build(), + "a token"), 30000L); + + assertThat("incorrect token", token, is(expected)); + } } @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)) - .thenReturn(new NewToken(StoredToken.getBuilder( - TokenType.AGENT, uuid, new UserName("foo")) - .withLifeTime(Instant.ofEpochMilli(10000), Instant.ofEpochMilli(20000)) - .withTokenName(new TokenName("whee")) - .build(), - "a token")); - - when(auth.getSuggestedTokenCacheTime()).thenReturn(30000L); - - final NewAPIToken token = tm.createTestToken(new CreateTestToken("foo", "whee", "Agent")); - - 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")) - .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 @@ -358,11 +392,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")); } @@ -370,14 +404,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")); @@ -405,6 +447,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); @@ -414,6 +457,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 4bdb2a34..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 @@ -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; @@ -150,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(); @@ -167,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) @@ -226,7 +229,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 +250,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/api/UserEndpointTest.java b/src/test/java/us/kbase/test/auth2/service/api/UserEndpointTest.java index 200c866c..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 @@ -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(); @@ -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))) @@ -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/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/common/ServiceCommonTest.java b/src/test/java/us/kbase/test/auth2/service/common/ServiceCommonTest.java index bf662eb9..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.1"; + 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"; 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/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 new file mode 100644 index 00000000..0b5c6d86 --- /dev/null +++ b/src/test/java/us/kbase/test/auth2/service/ui/LoginIntegrationTest.java @@ -0,0 +1,2932 @@ +package us.kbase.test.auth2.service.ui; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.when; +import static us.kbase.test.auth2.TestCommon.calculatePKCEChallenge; +import static us.kbase.test.auth2.TestCommon.inst; +import static us.kbase.test.auth2.TestCommon.set; +import static us.kbase.test.auth2.service.ServiceTestUtils.enableLogin; +import static us.kbase.test.auth2.service.ServiceTestUtils.enableProvider; +import static us.kbase.test.auth2.service.ServiceTestUtils.enableRedirect; +import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestHTML; +import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestJSON; +import static us.kbase.test.auth2.service.ServiceTestUtils.setLoginCompleteRedirect; +import static us.kbase.test.auth2.service.ServiceTestUtils.setEnvironment; + +import java.net.URI; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientProperties; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; + +import us.kbase.auth2.kbase.KBaseAuthConfig; +import us.kbase.auth2.lib.DisplayName; +import us.kbase.auth2.lib.EmailAddress; +import us.kbase.auth2.lib.PasswordHashAndSalt; +import us.kbase.auth2.lib.PolicyID; +import us.kbase.auth2.lib.Role; +import us.kbase.auth2.lib.TemporarySessionData; +import us.kbase.auth2.lib.TemporarySessionData.Operation; +import us.kbase.auth2.lib.UserName; +import us.kbase.auth2.lib.exceptions.AuthException; +import us.kbase.auth2.lib.exceptions.AuthenticationException; +import us.kbase.auth2.lib.exceptions.ErrorType; +import us.kbase.auth2.lib.exceptions.IllegalParameterException; +import us.kbase.auth2.lib.exceptions.InvalidTokenException; +import us.kbase.auth2.lib.exceptions.MissingParameterException; +import us.kbase.auth2.lib.exceptions.NoSuchEnvironmentException; +import us.kbase.auth2.lib.exceptions.NoSuchIdentityProviderException; +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; +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; +import us.kbase.auth2.lib.user.LocalUser; +import us.kbase.auth2.lib.user.NewUser; +import us.kbase.test.auth2.MapBuilder; +import us.kbase.test.auth2.MockIdentityProviderFactory; +import us.kbase.test.auth2.MongoStorageTestManager; +import us.kbase.test.auth2.StandaloneAuthServer; +import us.kbase.test.auth2.TestCommon; +import us.kbase.test.auth2.StandaloneAuthServer.ServerThread; +import us.kbase.test.auth2.service.ServiceTestUtils; +import us.kbase.testutils.RegexMatcher; + +public class LoginIntegrationTest { + + //TODO TEST convert most of these to unit tests, but keep enough for integration tests + + private static final UUID UID = UUID.randomUUID(); + private static final UUID UID2 = UUID.randomUUID(); + private static final UUID UID3 = UUID.randomUUID(); + + private static final String DB_NAME = "test_login_ui"; + private static final String COOKIE_NAME = "login-cookie"; + + private static final RemoteIdentity REMOTE1 = new RemoteIdentity( + new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1", "full1", "e1@g.com")); + + private static final RemoteIdentity REMOTE2 = new RemoteIdentity( + new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2", "full2", "e2@g.com")); + + private static final RemoteIdentity REMOTE3 = new RemoteIdentity( + new RemoteIdentityID("prov", "id3"), + new RemoteIdentityDetails("user3", "full3", "e3@g.com")); + + private static final Client CLI = ClientBuilder.newClient(); + + private static MongoStorageTestManager manager = null; + private static StandaloneAuthServer server = null; + private static int port = -1; + private static String host = null; + + @BeforeClass + public static void beforeClass() throws Exception { + manager = new MongoStorageTestManager(DB_NAME); + final Path cfgfile = ServiceTestUtils.generateTempConfigFile(manager, DB_NAME, COOKIE_NAME); + TestCommon.getenv().put("KB_DEPLOYMENT_CONFIG", cfgfile.toString()); + server = new StandaloneAuthServer(KBaseAuthConfig.class.getName()); + new ServerThread(server).start(); + System.out.println("Main thread waiting for server to start up"); + while (server.getPort() == null) { + Thread.sleep(1000); + } + port = server.getPort(); + host = "http://localhost:" + port; + } + + @AfterClass + public static void afterClass() throws Exception { + if (server != null) { + server.stop(); + } + if (manager != null) { + manager.destroy(); + } + } + + @Before + public void beforeTest() throws Exception { + ServiceTestUtils.resetServer(manager, host, COOKIE_NAME); + } + + @Test + public void startDisplayLoginDisabled() throws Exception { + // returns crappy html only + final WebTarget wt = CLI.target(host + "/login/"); + final String res = wt.request().get().readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + } + + @Test + public void startDisplayWithOneProvider() throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + + final WebTarget wt = CLI.target(host + "/login/"); + final String res = wt.request().get().readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + } + + @Test + public void startDisplayWithTwoProviders() throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableProvider(host, COOKIE_NAME, admintoken, "prov2"); + + final WebTarget wt = CLI.target(host + "/login/"); + final String res = wt.request().get().readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + } + + @Test + public void suggestName() throws Exception { + final WebTarget wt = CLI.target(host + "/login/suggestname/***FOOTYPANTS***"); + @SuppressWarnings("unchecked") + final Map res = wt.request().get().readEntity(Map.class); + assertThat("incorrect expected name", res, + is(ImmutableMap.of("availablename", "footypants"))); + } + + @Test + public void loginStartMinimalInput() throws Exception { + final Form form = new Form(); + form.param("provider", "prov1"); + final NewCookie expectedsession = new NewCookie("issessiontoken", "true", + "/login", null, "session choice", 30 * 60, false); + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + + loginStart(form, null, expectedsession, expectedredirect, null); + } + + @Test + public void loginStartHeaderEnvironment() throws Exception { + final Form form = new Form(); + form.param("provider", "prov1"); + form.param("environment", "env2"); + final NewCookie expectedsession = new NewCookie("issessiontoken", "true", + "/login", null, "session choice", 30 * 60, false); + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + + loginStart(form, "env1", expectedsession, expectedredirect, "env1"); + } + + @Test + public void loginStartEmptyStringsWithFormEnvironmentWhitespaceHeader() throws Exception { + final Form form = new Form(); + form.param("provider", "prov1"); + form.param("redirecturl", " \t \n "); + form.param("stayloggedin", " \t \n "); + form.param("environment", "myenv"); + final NewCookie expectedsession = new NewCookie("issessiontoken", "true", + "/login", null, "session choice", 30 * 60, false); + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + + loginStart(form, " \t ", expectedsession, expectedredirect, "myenv"); + } + + @Test + public void loginStartWithRedirectAndNonSessionCookieWithWhitespaceEnvironment() + throws Exception { + final String redirect = "https://foobar.com/thingy/stuff"; + final Form form = new Form(); + form.param("provider", "prov1"); + form.param("redirecturl", redirect); + form.param("stayloggedin", "f"); + form.param("environment", " \t "); + final NewCookie expectedsession = new NewCookie("issessiontoken", "false", + "/login", null, "session choice", 30 * 60, false); + final NewCookie expectedredirect = new NewCookie("loginredirect", redirect, + "/login", null, "redirect url", 30 * 60, false); + + loginStart(form, null, expectedsession, expectedredirect, null); + } + + @Test + public void loginStartWithRedirectAndNonSessionCookieWithEnvironment() throws Exception { + final String redirect = "https://foobaz.com/thingy/stuff"; + final Form form = new Form(); + form.param("provider", "prov1"); + form.param("redirecturl", redirect); + form.param("stayloggedin", "f"); + form.param("environment", "env1"); + final NewCookie expectedsession = new NewCookie("issessiontoken", "false", + "/login", null, "session choice", 30 * 60, false); + final NewCookie expectedredirect = new NewCookie("loginredirect", redirect, + "/login", null, "redirect url", 30 * 60, false); + + loginStart(form, null, expectedsession, expectedredirect, "env1"); + } + + private void loginStart( + final Form form, + final String headerEnv, + final NewCookie expectedsession, + final NewCookie expectedredirect, + final String expectedEnv) + throws Exception { + final IdentityProvider provmock = MockIdentityProviderFactory + .MOCKS.get("prov1"); + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + enableRedirect(host, COOKIE_NAME, admintoken, "https://foobaz.com/thingy", "env1"); + + final String url = "https://foo.com/someurlorother"; + + final StateMatcher stateMatcher = new StateMatcher(); + final PKCEChallengeMatcher pkceMatcher = new PKCEChallengeMatcher(); + when(provmock.getLoginURI( + argThat(stateMatcher), argThat(pkceMatcher), eq(false), eq(expectedEnv))) + .thenReturn(new URI(url)); + + final WebTarget wt = CLI.target(host + "/login/start"); + final Builder b = wt.request(); + if (headerEnv != null) { + b.header("X-DOEKBASE-ENVIRONMENT", headerEnv); + } + final Response res = b.post( + Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); + + assertEnvironmentCookieCorrect(res, expectedEnv, 30 * 60); + + final NewCookie process = res.getCookies().get("in-process-login-token"); + final NewCookie expectedprocess = new NewCookie("in-process-login-token", + process.getValue(), + "/login", null, "logintoken", -1, false); + assertThat("incorrect login process cookie", process, is(expectedprocess)); + + final TemporarySessionData ti = manager.storage.getTemporarySessionData( + new IncomingToken(process.getValue()).getHashedToken()); + assertThat("incorrect temp op", ti.getOperation(), is(Operation.LOGINSTART)); + assertThat("incorrect state", + ti.getOAuth2State(), is(Optional.of(stateMatcher.capturedState))); + assertThat("incorrect pkce challenge", + calculatePKCEChallenge(ti.getPKCECodeVerifier().get()), + is(pkceMatcher.capturedChallenge) + ); + + final NewCookie session = res.getCookies().get("issessiontoken"); + assertThat("incorrect session cookie", session, is(expectedsession)); + + final NewCookie redirect = res.getCookies().get("loginredirect"); + assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); + } + + @Test + public void loginStartFailNoProvider() throws Exception { + failLoginStart(new Form(), 400, "Bad Request", new MissingParameterException("provider")); + + final Form form = new Form(); + form.param("provider", null); + failLoginStart(form, 400, "Bad Request", new MissingParameterException("provider")); + + final Form form2 = new Form(); + form2.param("provider", " \t \n "); + failLoginStart(form2, 400, "Bad Request", new MissingParameterException("provider")); + } + + @Test + public void loginStartFailNoSuchProvider() throws Exception { + final Form form = new Form(); + form.param("provider", "prov3"); + failLoginStart(form, 401, "Unauthorized", new NoSuchIdentityProviderException("prov3")); + } + + @Test + public void loginStartFailNoSuchEnvironment() throws Exception { + final Form form = new Form(); + form.param("provider", "fake"); + form.param("environment", "env3"); + form.param("redirecturl", "https://foo.com"); + failLoginStart(form, 400, "Bad Request", new NoSuchEnvironmentException("env3")); + } + + @Test + public void loginStartFailBadRedirect() throws Exception { + final Form form = new Form(); + form.param("provider", "fake"); + form.param("redirecturl", "this ain't no gotdamned url"); + failLoginStart(form, 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: this ain't no gotdamned url")); + + // toURI chokes on ^s + final Form form2 = new Form(); + form2.param("provider", "fake"); + form2.param("redirecturl", "https://foobar.com/stuff/thingy?a=^h"); + failLoginStart(form2, 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); + + final Form form3 = new Form(); + form3.param("provider", "fake"); + form3.param("redirecturl", "https://foobar.com/stuff/thingy"); + failLoginStart(form3, 400, "Bad Request", new IllegalParameterException( + "Post-login redirects are not enabled")); + + // with environment without redirect url prefix configured + final Form form4 = new Form(); + form4.param("provider", "fake"); + form4.param("redirecturl", "https://foobar.com/stuff/thingy"); + form4.param("environment", "env1"); + failLoginStart(form4, 400, "Bad Request", new IllegalParameterException( + "Post-login redirects are not enabled for environment env1")); + + final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); + enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); + failLoginStart(form3, 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + + // with environment with url prefix configured + enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2/", "env1"); + failLoginStart(form4, 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + } + + private void failLoginStart( + final Form form, + final int expectedHTTPCode, + final String expectedHTTPError, + final AuthException e) + throws Exception { + final WebTarget wt = CLI.target(host + "/login/start"); + final Response res = wt.request().header("Accept", MediaType.APPLICATION_JSON).post( + Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + failRequestJSON(res, expectedHTTPCode, expectedHTTPError, e); + } + + @Test + public void loginCompleteImmediateLoginMinimalInput() throws Exception { + loginCompleteImmediateLoginMinimalInput(null); + } + + @Test + public void loginCompleteImmediateLoginMinimalInputWithEnvironment() throws Exception { + loginCompleteImmediateLoginMinimalInput("env1"); + } + + private void loginCompleteImmediateLoginMinimalInput(final String env) throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkceohgodohgod", "foobartoken"); + + loginCompleteImmediateLoginStoreUser(authcode, "pkceohgodohgod", env, MFAStatus.UNKNOWN); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Builder b = wt.request() + .cookie("in-process-login-token", "foobartoken"); + if (env != null) { + b.cookie("environment", env); + } + final Response res = b.get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); + + assertLoginProcessTokensRemoved(res); + + final NewCookie token = res.getCookies().get(COOKIE_NAME); + final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), + "/", null, "authtoken", -1, false); + assertThat("incorrect auth cookie less token", token, is(expectedtoken)); + assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); + + loginCompleteImmediateLoginCheckToken(token, MFAStatus.UNKNOWN); + } + + @Test + public void loginCompleteImmediateLoginEmptyStringInput() throws Exception { + // also tests that the empty error string is ignored. + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkcethisisinhumane", "foobartoken"); + + loginCompleteImmediateLoginStoreUser( + authcode, "pkcethisisinhumane", null, MFAStatus.USED + ); + + final WebTarget wt = loginCompleteSetUpWebTargetEmptyError(authcode, state); + final Response res = wt.request() + .cookie("in-process-login-token", "foobartoken") + .cookie("loginredirect", " \t ") + .cookie("issessiontoken", " \t ") + .get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); + + assertLoginProcessTokensRemoved(res); + + final NewCookie token = res.getCookies().get(COOKIE_NAME); + final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), + "/", null, "authtoken", -1, false); + assertThat("incorrect auth cookie less token", token, is(expectedtoken)); + assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); + + loginCompleteImmediateLoginCheckToken(token, MFAStatus.USED); + } + + @Test + public void loginCompleteImmediateLoginRedirectAndTrueSession() throws Exception { + loginCompleteImmediateLoginRedirectAndTrueSession(null, "https://foobar.com/thingy/stuff"); + } + + @Test + public void loginCompleteImmediateLoginRedirectAndTrueSessionWithEnvironment() + throws Exception { + loginCompleteImmediateLoginRedirectAndTrueSession( + "env2", "https://foobar.com/t2hingy/stuff"); + } + + private void loginCompleteImmediateLoginRedirectAndTrueSession( + final String env, + final String url) + throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + enableRedirect(host, COOKIE_NAME, admintoken, "https://foobar.com/t2hingy", "env2"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkcepkcepkcepkce", "foobartoken"); + + loginCompleteImmediateLoginStoreUser(authcode, "pkcepkcepkcepkce", env, MFAStatus.NOT_USED); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Builder b = wt.request() + .cookie("in-process-login-token", "foobartoken") + .cookie("loginredirect", url) + .cookie("issessiontoken", "true"); + if (env != null) { + b.cookie("environment", env); + } + final Response res = b.get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); + + assertLoginProcessTokensRemoved(res); + + final NewCookie token = res.getCookies().get(COOKIE_NAME); + final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), + "/", null, "authtoken", -1, false); + assertThat("incorrect auth cookie less token", token, is(expectedtoken)); + assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); + + loginCompleteImmediateLoginCheckToken(token, MFAStatus.NOT_USED); + } + + @Test + public void loginCompleteImmediateLoginRedirectAndFalseSession() throws Exception { + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkceoohhooohhhmm", "foobartoken"); + + loginCompleteImmediateLoginStoreUser(authcode, "pkceoohhooohhhmm", null, MFAStatus.USED); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Response res = wt.request() + .cookie("in-process-login-token", "foobartoken") + .cookie("loginredirect", "https://foobar.com/thingy/stuff") + .cookie("issessiontoken", "false") + .get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), + is(new URI("https://foobar.com/thingy/stuff"))); + + assertLoginProcessTokensRemoved(res); + + final NewCookie token = res.getCookies().get(COOKIE_NAME); + final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), + "/", null, "authtoken", token.getMaxAge(), false); + assertThat("incorrect auth cookie less token and max age", token, is(expectedtoken)); + assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); + TestCommon.assertCloseTo(token.getMaxAge(), 14 * 24 * 3600, 10); + + loginCompleteImmediateLoginCheckToken(token, MFAStatus.USED); + } + + private void loginCompleteImmediateLoginStoreUser( + final String authcode, + final String pkce, + final String environment, + final MFAStatus mfa) + throws Exception { + final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( + authcode, pkce, environment, mfa); + + manager.storage.createUser(NewUser.getBuilder( + new UserName("whee"), UID, new DisplayName("dn"), Instant.ofEpochMilli(20000), + remoteIdentity) + .build()); + } + + 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 MFAStatus mfa) + throws Exception { + + ServiceTestUtils.checkReturnedToken(manager, uitoken, customContext, userName, + TokenType.LOGIN, mfa, null, 14 * 24 * 3600 * 1000, true); + } + + private void checkLoginToken( + final String token, + final Map customContext, + final UserName userName, + final MFAStatus mfa) + throws Exception { + ServiceTestUtils.checkStoredToken(manager, token, customContext, userName, TokenType.LOGIN, + mfa, null, 14 * 24 * 3600 * 1000); + } + + private void assertLoginProcessTokensRemoved(final Response res) { + final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", + "/login", null, "session choice", 0, false); + final NewCookie session = res.getCookies().get("issessiontoken"); + assertThat("incorrect session cookie", session, is(expectedsession)); + + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + final NewCookie redirect = res.getCookies().get("loginredirect"); + assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); + + final NewCookie expectedinprocess = new NewCookie("in-process-login-token", "no token", + "/login", null, "logintoken", 0, false); + final NewCookie inprocess = res.getCookies().get("in-process-login-token"); + assertThat("incorrect process cookie", inprocess, is(expectedinprocess)); + + assertEnvironmentCookieRemoved(res); + } + + private void assertEnvironmentCookieRemoved(final Response res) { + final NewCookie expectedenv = new NewCookie("environment", "no env", + "/login", null, "environment", 0, false); + final NewCookie envcookie = res.getCookies().get("environment"); + assertThat("incorrect state cookie", envcookie, is(expectedenv)); + } + + private void assertEnvironmentCookieCorrect( + final Response res, + final String env, + final int lifetime) { + if (env != null) { + final NewCookie envcookie = res.getCookies().get("environment"); + final NewCookie expectedenv = new NewCookie("environment", env, + "/login", null, "environment", envcookie.getMaxAge(), false); + assertThat("incorrect env cookie", envcookie, is(expectedenv)); + TestCommon.assertCloseTo(envcookie.getMaxAge(), lifetime, 10); + } else { + assertEnvironmentCookieRemoved(res); + } + } + + @Test + public void loginCompleteDelayedMinimalInput() throws Exception { + loginCompleteDelayedMinimalInput(null); + } + + @Test + public void loginCompleteDelayedMinimalInputWithEnvironment() throws Exception { + loginCompleteDelayedMinimalInput("env1"); + } + + private void loginCompleteDelayedMinimalInput(final String env) throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkceopraisethedarkgodsbelow", "foobartoken"); + + final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( + authcode, "pkceopraisethedarkgodsbelow", env, MFAStatus.USED); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Builder b = wt.request() + .cookie("in-process-login-token", "foobartoken"); + if (env != null) { + b.cookie("environment", env); + } + final Response res = b.get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/login/choice"))); + + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + final NewCookie redirect = res.getCookies().get("loginredirect"); + assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); + + final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", + "/login", null, "session choice", 0, false); + final NewCookie session = res.getCookies().get("issessiontoken"); + assertThat("incorrect session cookie", session, is(expectedsession)); + + assertEnvironmentCookieCorrect(res, env, 30 * 60); + + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.USED, res); + } + + @Test + public void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect() throws Exception { + loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( + null, "https://whee.com/bleah"); + } + + @Test + public void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirectAndAlsoEnvironment() + throws Exception { + loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( + "env2", "https://whoo.com/bleah"); + } + + private void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( + final String env, + final String url) + throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + setLoginCompleteRedirect(host, admintoken, "https://whee.com/bleah"); + setLoginCompleteRedirect(host, COOKIE_NAME, admintoken, "https://whoo.com/bleah", "env2"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkceisinmybrainicanseeall", "foobartoken"); + + final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( + authcode, "pkceisinmybrainicanseeall", env, MFAStatus.NOT_USED); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Builder b = wt.request() + .cookie("in-process-login-token", "foobartoken") + .cookie("loginredirect", " \t ") + .cookie("issessiontoken", " \t "); + if (env != null) { + b.cookie("environment", env); + } + final Response res = b.get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); + + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + final NewCookie redirect = res.getCookies().get("loginredirect"); + assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); + + final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", + "/login", null, "session choice", 0, false); + final NewCookie session = res.getCookies().get("issessiontoken"); + assertThat("incorrect session cookie", session, is(expectedsession)); + + assertEnvironmentCookieCorrect(res, env, 30 * 60); + + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.NOT_USED, res); + } + + @Test + public void loginCompleteDelayedLoginRedirectAndTrueSession() throws Exception { + loginCompleteDelayedLoginRedirectAndTrueSession( + null, "https://whee.com/bleah", "https://foobar.com/thingy/stuff"); + } + + + @Test + public void loginCompleteDelayedLoginRedirectAndTrueSessionAndEnvironment() throws Exception { + loginCompleteDelayedLoginRedirectAndTrueSession( + "env1", "https://whoo.com/bleah", "https://foobar2.com/thingy/stuff"); + } + + private void loginCompleteDelayedLoginRedirectAndTrueSession( + final String env, + final String completeURL, + final String redirectURL) + throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + setLoginCompleteRedirect(host, admintoken, "https://whee.com/bleah"); + final Form form = new Form(); + form.param("environment", "env1"); + form.param("completeloginredirect", "https://whoo.com/bleah"); + form.param("allowedloginredirect", "https://foobar2.com/thingy"); + setEnvironment(host, COOKIE_NAME, admintoken, form); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkcuwgahngalftaghn", "foobartoken"); + + final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( + authcode, "pkcuwgahngalftaghn", env, MFAStatus.UNKNOWN); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Builder b = wt.request() + .cookie("in-process-login-token", "foobartoken") + .cookie("loginredirect", redirectURL) + .cookie("issessiontoken", "true"); + if (env != null) { + b.cookie("environment", env); + } + final Response res = b.get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(completeURL))); + + final NewCookie redirect = res.getCookies().get("loginredirect"); + final NewCookie expectedredirect = new NewCookie( + "loginredirect", redirectURL, + "/login", null, "redirect url", redirect.getMaxAge(), false); + assertThat("incorrect redirect cookie less max age", redirect, is(expectedredirect)); + TestCommon.assertCloseTo(redirect.getMaxAge(), 30 * 60, 10); + + final NewCookie session = res.getCookies().get("issessiontoken"); + final NewCookie expectedsession = new NewCookie("issessiontoken", "true", + "/login", null, "session choice", session.getMaxAge(), false); + assertThat("incorrect session cookie less max age", session, is(expectedsession)); + TestCommon.assertCloseTo(session.getMaxAge(), 30 * 60, 10); + + assertEnvironmentCookieCorrect(res, env, 30 * 60); + + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.UNKNOWN, res); + } + + @Test + public void loginCompleteDelayedLoginRedirectAndFalseSession() throws Exception { + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + enableRedirect(host, admintoken, "https://foobar.com/thingy"); + setLoginCompleteRedirect(host, admintoken, "https://whee.com/bleah"); + + final String authcode = "foobarcode"; + final String state = "foobarstate"; + + saveTemporarySessionData(state, "pkceifeelmoistandsprightly", "foobartoken"); + + final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( + authcode, "pkceifeelmoistandsprightly", null, MFAStatus.NOT_USED); + + final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); + final Response res = wt.request() + .cookie("in-process-login-token", "foobartoken") + .cookie("loginredirect", "https://foobar.com/thingy/stuff") + .cookie("issessiontoken", "false") + .get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), + is(new URI("https://whee.com/bleah"))); + + final NewCookie redirect = res.getCookies().get("loginredirect"); + final NewCookie expectedredirect = new NewCookie( + "loginredirect", "https://foobar.com/thingy/stuff", + "/login", null, "redirect url", redirect.getMaxAge(), false); + assertThat("incorrect redirect cookie less max age", redirect, is(expectedredirect)); + TestCommon.assertCloseTo(redirect.getMaxAge(), 30 * 60, 10); + + final NewCookie session = res.getCookies().get("issessiontoken"); + final NewCookie expectedsession = new NewCookie("issessiontoken", "false", + "/login", null, "session choice", session.getMaxAge(), false); + assertThat("incorrect session cookie less max age", session, is(expectedsession)); + TestCommon.assertCloseTo(session.getMaxAge(), 30 * 60, 10); + + loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, MFAStatus.NOT_USED, res); + } + + private void loginCompleteDelayedCheckTempAndStateCookies( + final RemoteIdentity remoteIdentity, + final MFAStatus mfa, + final Response res) + throws Exception { + + final NewCookie tempCookie = res.getCookies().get("in-process-login-token"); + final NewCookie expectedtemp = new NewCookie("in-process-login-token", + tempCookie.getValue(), + "/login", null, "logintoken", -1, false); + assertThat("incorrect temp cookie less value and max age", tempCookie, is(expectedtemp)); + + final TemporarySessionData tis = manager.storage.getTemporarySessionData( + 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) { + final URI target = UriBuilder.fromUri(host) + .path("/login/complete/prov1") + .queryParam("code", authcode) + .queryParam("state", state) + .build(); + + return CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + } + + private WebTarget loginCompleteSetUpWebTargetEmptyError( + final String authcode, final String state) { + final URI target = UriBuilder.fromUri(host) + .path("/login/complete/prov1") + .queryParam("code", authcode) + .queryParam("state", state) + .queryParam("error", " \t ") + .build(); + + return CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + } + + private RemoteIdentity loginCompleteSetUpProviderMock( + final String authcode, + final String pkce, + final String environment, + final MFAStatus mfa) + throws Exception { + + final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); + final RemoteIdentity remoteIdentity = new RemoteIdentity( + new RemoteIdentityID("prov1", "prov1id"), + new RemoteIdentityDetails("user", "full", "email@email.com")); + when(provmock.getIdentities(authcode, pkce, false, environment)) + .thenReturn(IdentityProviderResponse.from(remoteIdentity, mfa)); + return remoteIdentity; + } + + public void saveTemporarySessionData(final String state, final String pkce, final String token) + throws AuthStorageException { + manager.storage.storeTemporarySessionData(TemporarySessionData.create( + UUID.randomUUID(), Instant.now(), Instant.now().plusSeconds(10)) + .login(state, pkce), + IncomingToken.hash(token)); + } + + @Test + public void loginCompleteProviderError() throws Exception { + // the various input paths for the redirect cookie and the session cookie are exactly + // the same as for the delayed login so not testing them again here + + final URI target = UriBuilder.fromUri(host) + .path("/login/complete/prov1") + .queryParam("error", "errorwhee") + .build(); + + final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + final Response res = wt.request().get(); + + assertThat("incorrect status code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/login/choice"))); + + final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", + "/login", null, "redirect url", 0, false); + final NewCookie redirect = res.getCookies().get("loginredirect"); + assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); + + final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", + "/login", null, "session choice", 0, false); + final NewCookie session = res.getCookies().get("issessiontoken"); + assertThat("incorrect session cookie", session, is(expectedsession)); + + final NewCookie tempCookie = res.getCookies().get("in-process-login-token"); + final NewCookie expectedtemp = new NewCookie("in-process-login-token", + tempCookie.getValue(), + "/login", null, "logintoken", -1, false); + assertThat("incorrect temp cookie less value", tempCookie, is(expectedtemp)); + + final TemporarySessionData tis = manager.storage.getTemporarySessionData( + new IncomingToken(tempCookie.getValue()).getHashedToken()); + + assertThat("incorrect op", tis.getOperation(), is(Operation.ERROR)); + assertThat("incorrect error", tis.getError(), is(Optional.of("errorwhee"))); + } + + @Test + public void loginCompleteFailStateMismatch() throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + saveTemporarySessionData("important state", "pkce", "foobartoken"); + + final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobartoken") + .cookie("issessiontoken", "false"); + + failRequestJSON(request.get(), 401, "Unauthorized", + new AuthenticationException(ErrorType.AUTHENTICATION_FAILED, + "State values do not match, this may be a CSRF attack")); + } + + @Test + public void loginCompleteFailNoProviderState() throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + saveTemporarySessionData("important state", "pkce", "foobartoken"); + + final URI target = UriBuilder.fromUri(host) + .path("/login/complete/prov1") + .queryParam("code", "foocode") + .build(); + + final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("issessiontoken", "false") + .cookie("in-process-login-token", "foobartoken"); + + failRequestJSON(request.get(), 401, "Unauthorized", + new AuthenticationException(ErrorType.AUTHENTICATION_FAILED, + "State values do not match, this may be a CSRF attack")); + } + + @Test + public void loginCompleteFailNoToken() throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + enableProvider(host, COOKIE_NAME, admintoken, "prov1"); + + final URI target = UriBuilder.fromUri(host) + .path("/login/complete/prov1") + .queryParam("state", "somestate") + .build(); + + final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.get(), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + } + + @Test + public void loginCompleteFailNoAuthcode() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/complete/prov1") + .queryParam("state", "somestate") + .build(); + + final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("issessiontoken", "false") + .cookie("in-process-login-token", "foobartoken"); + + failRequestJSON(request.get(), 400, "Bad Request", + new MissingParameterException("authorization code")); + } + + @Test + public void loginCompleteFailNoSuchProvider() throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableProvider(host, COOKIE_NAME, admintoken, "prov2"); + + final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobartoken"); + + failRequestJSON(request.get(), 401, "Unauthorized", + new NoSuchIdentityProviderException("prov1")); + } + + @Test + public void loginCompleteFailNoSuchEnvironment() throws Exception { + final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("environment", "env3") + .cookie("loginredirect", "http://foo.com"); + + failRequestJSON(request.get(), 400, "Bad Request", new NoSuchEnvironmentException("env3")); + } + + @Test + public void loginCompleteFailBadRedirect() throws Exception { + final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("loginredirect", "not a url no sir"); + + failRequestJSON(request.get(), 400, "Bad Request", + new IllegalParameterException("Illegal redirect URL: not a url no sir")); + + // toURI chokes on ^s + request.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); + + failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); + + request.cookie("loginredirect", "https://foobar.com/stuff/thingy"); + + failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( + "Post-login redirects are not enabled")); + + final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); + enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); + failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + + // test with environment + request.cookie("environment", "env1"); + failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( + "Post-login redirects are not enabled for environment env1")); + + enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2", "env1"); + failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + } + + @Test + public void loginChoice3Create2Login() throws Exception { + // this tests a bunch of orthogonal test cases. Doesn't make much sense to split it up + // since there has to be *some* output for the test, might as well include independent + // cases. + // tests a choice with 3 options to create an account, 2 options to login with an account, + // one of which has two linked IDs. + // tests create accounts having missing email and fullnames and illegal + // email and fullnames. + // tests one of the suggested usernames containing a @ and existing in the system. + // tests one of the users being disabled. + // tests policy ids. + // tests with no redirect cookie. + final Set idents = new HashSet<>(); + for (int i = 1; i < 5; i++) { + idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), + new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); + } + idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id5"), + new RemoteIdentityDetails("user&at@bleah.com", null, null))); + idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id6"), + new RemoteIdentityDetails("whee", "foo\nbar", "not an email"))); + + final TemporarySessionData data = TemporarySessionData.create( + UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) + .login(idents, MFAStatus.USED); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("userat"), + UID, new DisplayName("f"), Instant.ofEpochMilli(30000)).build(), + new PasswordHashAndSalt("foobarbazbat".getBytes(), "aaa".getBytes())); + + manager.storage.createUser(NewUser.getBuilder( + new UserName("ruser1"), UID2, new DisplayName("disp1"), inst(10000), + new RemoteIdentity(new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1a", "full1a", "e1a@g.com"))) + .withPolicyID(new PolicyID("foo"), Instant.ofEpochMilli(60000)) + .withPolicyID(new PolicyID("bar"), Instant.ofEpochMilli(70000)) + .build()); + manager.storage.link(new UserName("ruser1"), + new RemoteIdentity(new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2a", "full2a", "e2a@g.com"))); + manager.storage.createUser(NewUser.getBuilder( + new UserName("ruser2"), UID3, new DisplayName("disp2"), inst(10000), + new RemoteIdentity(new RemoteIdentityID("prov", "id3"), + new RemoteIdentityDetails("user3a", "full3a", "e3a@g.com"))).build()); + when(manager.mockClock.instant()).thenReturn(Instant.ofEpochMilli(40000)); + manager.storage.disableAccount(new UserName("ruser2"), new UserName("adminwhee"), + "Said nasty, but true, things about Steve"); + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + enableLogin(host, admintoken); + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final WebTarget wt = CLI.target(target); + + final String res = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .get() + .readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + + @SuppressWarnings("unchecked") + final Map json = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .header("accept", MediaType.APPLICATION_JSON) + .get() + .readEntity(Map.class); + + final Map expectedJson = new HashMap<>(); + expectedJson.put("pickurl", "pick"); + expectedJson.put("createurl", "create"); + expectedJson.put("cancelurl", "cancel"); + expectedJson.put("suggestnameurl", "suggestname"); + expectedJson.put("redirecturl", null); + expectedJson.put("expires", 11493000000000L); + expectedJson.put("creationallowed", true); + expectedJson.put("provider", "prov"); + expectedJson.put("create", Arrays.asList( + ImmutableMap.of("provusername", "user4", + "availablename", "user4", + "provfullname", "full4", + "id", "4a3cd1ac3f1ffd5d2fecabcfc1856485", + "provemail", "e4@g.com"), + MapBuilder.newHashMap() + .with("provusername", "user&at@bleah.com") + .with("availablename", "userat2") + .with("provfullname", null) + .with("id", "78f2c2dbc07bfc9838c45f601a92762d") + .with("provemail", null) + .build(), + MapBuilder.newHashMap() + .with("provusername", "whee") + .with("availablename", "whee") + .with("provfullname", null) + .with("id", "ccf1ab20b4b412c515182c16f6176b3f") + .with("provemail", null) + .build() + )); + expectedJson.put("login", Arrays.asList( + ImmutableMap.builder() + .put("adminonly", false) + .put("loginallowed", true) + .put("disabled", false) + .put("policyids", Arrays.asList( + ImmutableMap.of("id", "bar", "agreedon", 70000), + ImmutableMap.of("id", "foo", "agreedon", 60000) + )) + .put("id", "5fbea2e6ce3d02f7cdbde0bc31be8059") + .put("user", "ruser1") + .put("provusernames", Arrays.asList("user2", "user1")) + .build(), + ImmutableMap.builder() + .put("adminonly", false) + .put("loginallowed", false) + .put("disabled", true) + .put("policyids", Collections.emptyList()) + .put("id", "de0702aa7927b562e0d6be5b6527cfb2") + .put("user", "ruser2") + .put("provusernames", Arrays.asList("user3")) + .build() + )); + + ServiceTestUtils.assertObjectsEqual(json, expectedJson); + } + + @Test + public void loginChoice2LoginWithRedirectAndLoginDisabled() throws Exception { + // tests with redirect cookie + // tests with login disabled and admin user + // tests with trailing slash on target + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + enableRedirect(host, admintoken, "https://foo.com/whee"); + + final Set idents = new HashSet<>(); + for (int i = 1; i < 3; i++) { + idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), + new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); + } + + final TemporarySessionData data = TemporarySessionData.create( + UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) + .login(idents, MFAStatus.NOT_USED); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + manager.storage.createUser(NewUser.getBuilder( + new UserName("ruser1"), UID, new DisplayName("disp1"), inst(10000), + new RemoteIdentity(new RemoteIdentityID("prov", "id1"), + new RemoteIdentityDetails("user1a", "full1a", "e1a@g.com"))) + .build()); + manager.storage.createUser(NewUser.getBuilder( + new UserName("ruser2"), UID2, new DisplayName("disp2"), inst(10000), + new RemoteIdentity(new RemoteIdentityID("prov", "id2"), + new RemoteIdentityDetails("user2a", "full2a", "e2a@g.com"))) + .build()); + manager.storage.updateRoles(new UserName("ruser2"), set(Role.ADMIN), set()); + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice/") + .build(); + + final WebTarget wt = CLI.target(target); + + final String res = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .cookie("loginredirect", "https://foo.com/whee/bleah") + .get() + .readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + + @SuppressWarnings("unchecked") + final Map json = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .cookie("loginredirect", "https://foo.com/whee/bleah") + .header("accept", MediaType.APPLICATION_JSON) + .get() + .readEntity(Map.class); + + final Map expectedJson = new HashMap<>(); + expectedJson.put("pickurl", "../pick"); + expectedJson.put("createurl", "../create"); + expectedJson.put("cancelurl", "../cancel"); + expectedJson.put("suggestnameurl", "../suggestname"); + expectedJson.put("redirecturl", "https://foo.com/whee/bleah"); + expectedJson.put("expires", 11493000000000L); + expectedJson.put("creationallowed", false); + expectedJson.put("provider", "prov"); + expectedJson.put("create", Collections.emptyList()); + expectedJson.put("login", Arrays.asList( + ImmutableMap.builder() + .put("adminonly", true) + .put("loginallowed", false) + .put("disabled", false) + .put("policyids", Collections.emptyList()) + .put("id", "ef0518c79af70ed979907969c6d0a0f7") + .put("user", "ruser1") + .put("provusernames", Arrays.asList("user1")) + .build(), + ImmutableMap.builder() + .put("adminonly", false) + .put("loginallowed", true) + .put("disabled", false) + .put("policyids", Collections.emptyList()) + .put("id", "5fbea2e6ce3d02f7cdbde0bc31be8059") + .put("user", "ruser2") + .put("provusernames", Arrays.asList("user2")) + .build() + )); + + ServiceTestUtils.assertObjectsEqual(json, expectedJson); + } + + @Test + public void loginChoice2CreateAndLoginDisabled() throws Exception { + loginChoice2CreateAndLoginDisabled(null); + } + + @Test + public void loginChoice2CreateAndLoginDisabledAndEnvironment() throws Exception { + // without a redirect cookie, env should do nothing + loginChoice2CreateAndLoginDisabled("env1"); + } + + private void loginChoice2CreateAndLoginDisabled(final String env) throws Exception { + // tests with login disabled + // tests with trailing slash on target + // tests empty string for redirect + + final Set idents = new HashSet<>(); + for (int i = 1; i < 3; i++) { + idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), + new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); + } + final TemporarySessionData data = TemporarySessionData.create( + UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) + .login(idents, MFAStatus.UNKNOWN); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice/") + .build(); + + final WebTarget wt = CLI.target(target); + + final Builder b = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .cookie("loginredirect", " \t "); + if (env != null) { + b.cookie("environment", env); + } + final String res = b.get().readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + + final Builder bjson = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .cookie("loginredirect", " \t ") + .header("accept", MediaType.APPLICATION_JSON); + if (env != null) { + bjson.cookie("environment", env); + } + @SuppressWarnings("unchecked") + final Map json = bjson.get().readEntity(Map.class); + + final Map expectedJson = new HashMap<>(); + expectedJson.put("pickurl", "../pick"); + expectedJson.put("createurl", "../create"); + expectedJson.put("cancelurl", "../cancel"); + expectedJson.put("suggestnameurl", "../suggestname"); + expectedJson.put("redirecturl", null); + expectedJson.put("expires", 11493000000000L); + expectedJson.put("creationallowed", false); + expectedJson.put("provider", "prov"); + expectedJson.put("create", Arrays.asList( + ImmutableMap.of("provusername", "user1", + "availablename", "user1", + "provfullname", "full1", + "id", "ef0518c79af70ed979907969c6d0a0f7", + "provemail", "e1@g.com"), + ImmutableMap.of("provusername", "user2", + "availablename", "user2", + "provfullname", "full2", + "id", "5fbea2e6ce3d02f7cdbde0bc31be8059", + "provemail", "e2@g.com") + )); + expectedJson.put("login", Collections.emptyList()); + + ServiceTestUtils.assertObjectsEqual(json, expectedJson); + } + + @Test + public void loginChoice2CreateWithRedirectURL() throws Exception { + loginChoice2CreateWithRedirectURL(null, "https://foo.com/whee/stuff"); + } + + @Test + public void loginChoice2CreateWithRedirectURLAndEnvironment() throws Exception { + loginChoice2CreateWithRedirectURL("env2", "https://bar.com/whee/stuff"); + } + + private void loginChoice2CreateWithRedirectURL(final String env, final String url) + throws Exception { + // tests with redirect cookie + // note that the html form response does not include the redirect url + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + enableRedirect(host, admintoken, "https://foo.com/whee"); + enableRedirect(host, COOKIE_NAME, admintoken, "https://bar.com/whee", "env2"); + enableLogin(host, admintoken); + + final Set idents = new HashSet<>(); + for (int i = 1; i < 3; i++) { + idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), + new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); + } + final TemporarySessionData data = TemporarySessionData.create( + UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) + .login(idents, MFAStatus.USED); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final WebTarget wt = CLI.target(target); + + final Builder b = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .cookie("loginredirect", url); + if (env != null) { + b.cookie("environment", env); + } + final String res = b.get().readEntity(String.class); + + TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), + TestCommon.getCurrentMethodName())); + + final Builder bjson = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .cookie("loginredirect", url) + .header("accept", MediaType.APPLICATION_JSON); + if (env != null) { + bjson.cookie("environment", env); + } + @SuppressWarnings("unchecked") + final Map json = bjson.get().readEntity(Map.class); + // since we don't care about the order of the providers + @SuppressWarnings("unchecked") + final List> l = (List>) json.get("create"); + json.put("create", new HashSet<>(l)); + + final Map expectedJson = new HashMap<>(); + expectedJson.put("pickurl", "pick"); + expectedJson.put("createurl", "create"); + expectedJson.put("cancelurl", "cancel"); + expectedJson.put("suggestnameurl", "suggestname"); + expectedJson.put("redirecturl", url); + expectedJson.put("expires", 11493000000000L); + expectedJson.put("creationallowed", true); + expectedJson.put("provider", "prov"); + expectedJson.put("create", set( + ImmutableMap.of("provusername", "user1", + "availablename", "user1", + "provfullname", "full1", + "id", "ef0518c79af70ed979907969c6d0a0f7", + "provemail", "e1@g.com"), + ImmutableMap.of("provusername", "user2", + "availablename", "user2", + "provfullname", "full2", + "id", "5fbea2e6ce3d02f7cdbde0bc31be8059", + "provemail", "e2@g.com") + )); + expectedJson.put("login", Collections.emptyList()); + + ServiceTestUtils.assertObjectsEqual(json, expectedJson); + } + + @Test + public void loginChoiceFailNoToken() throws Exception { + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final WebTarget wt = CLI.target(target); + + failRequestHTML(wt.request().get(), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + + final Builder res = wt.request() + .header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(res.get(), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + } + + @Test + public void loginChoiceFailEmptyToken() throws Exception { + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final Builder res = CLI.target(target).request() + .cookie("in-process-login-token", " \t "); + + failRequestHTML(res.get(), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + + res.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(res.get(), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + } + + @Test + public void loginChoiceFailBadToken() throws Exception { + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final WebTarget wt = CLI.target(target); + + final Builder res = wt.request() + .cookie("in-process-login-token", "foobarbaz"); + + final Builder jsonrequest = wt.request() + .cookie("in-process-login-token", "foobarbaz") + .header("accept", MediaType.APPLICATION_JSON); + + failRequestHTML(res.get(), 401, "Unauthorized", + new InvalidTokenException("Temporary token")); + + failRequestJSON(jsonrequest.get(), 401, "Unauthorized", + new InvalidTokenException("Temporary token")); + } + + @Test + public void loginChoiceFailNoSuchEnvironment() throws Exception { + + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final WebTarget wt = CLI.target(target); + + final Builder res = wt.request() + .cookie("environment", "env3") + .cookie("loginredirect", "https://foo.com") + .cookie("in-process-login-token", "foobarbaz"); + + final Builder jsonrequest = wt.request() + .cookie("environment", "env3") + .cookie("loginredirect", "https://foo.com") + .cookie("in-process-login-token", "foobarbaz") + .header("accept", MediaType.APPLICATION_JSON); + + failRequestHTML(res.get(), 400, "Bad Request", new NoSuchEnvironmentException("env3")); + + failRequestJSON(jsonrequest.get(), 400, "Bad Request", + new NoSuchEnvironmentException("env3")); + } + + @Test + public void loginChoiceFailBadRedirect() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/choice") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobarbaz") + .cookie("loginredirect", "not a url no sir"); + + final Builder jsonrequest = wt.request() + .cookie("in-process-login-token", "foobarbaz") + .header("accept", MediaType.APPLICATION_JSON) + .cookie("loginredirect", "not a url no sir"); + + failRequestHTML(request.get(), 400, "Bad Request", + new IllegalParameterException("Illegal redirect URL: not a url no sir")); + failRequestJSON(jsonrequest.get(), 400, "Bad Request", + new IllegalParameterException("Illegal redirect URL: not a url no sir")); + + // toURI chokes on ^s + request.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); + jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); + + failRequestHTML(request.get(), 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); + failRequestJSON(jsonrequest.get(), 400, "Bad Request", new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); + + request.cookie("loginredirect", "https://foobar.com/stuff/thingy"); + jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy"); + + failRequestHTML(request.get(), 400, "Bad Request", + new IllegalParameterException("Post-login redirects are not enabled")); + failRequestJSON(jsonrequest.get(), 400, "Bad Request", + new IllegalParameterException("Post-login redirects are not enabled")); + + final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); + enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); + + failRequestHTML(request.get(), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + failRequestJSON(jsonrequest.get(), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + + // with envs + request.cookie("environment", "env1"); + jsonrequest.cookie("environment", "env1"); + + failRequestHTML(request.get(), 400, "Bad Request", new IllegalParameterException( + "Post-login redirects are not enabled for environment env1")); + failRequestJSON(jsonrequest.get(), 400, "Bad Request", new IllegalParameterException( + "Post-login redirects are not enabled for environment env1")); + + enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2/", "env1"); + + failRequestHTML(request.get(), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + failRequestJSON(jsonrequest.get(), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + } + + @Test + 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") + )), + MFAStatus.UNKNOWN + ); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + final URI target = UriBuilder.fromUri(host) + .path("/login/cancel") + .build(); + + final WebTarget wt = CLI.target(target); + final Response res = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .post(null); + + assertThat("incorrect response code", res.getStatus(), is(204)); + assertLoginProcessTokensRemoved(res); + assertNoTempToken(tt); + } + + @Test + 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") + )), + MFAStatus.UNKNOWN + ); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + final URI target = UriBuilder.fromUri(host) + .path("/login/cancel") + .build(); + + final WebTarget wt = CLI.target(target); + final Response res = wt.request() + .cookie("in-process-login-token", tt.getToken()) + .delete(); + + assertThat("incorrect response code", res.getStatus(), is(204)); + assertLoginProcessTokensRemoved(res); + assertNoTempToken(tt); + } + + private void assertNoTempToken(final TemporaryToken tt) throws Exception { + try { + manager.storage.getTemporarySessionData( + new IncomingToken(tt.getToken()).getHashedToken()); + fail("expected exception getting temp token"); + } catch (NoSuchTokenException e) { + // pass + } + } + + @Test + public void loginCancelFailNoToken() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/cancel") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder res = wt.request() + .header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(res.post(null), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + failRequestJSON(res.delete(), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + } + + @Test + public void loginPickFormMinimalInput() throws Exception { + loginPickFormMinimalInput(null); + } + + @Test + public void loginPickFormMinimalInputWithEnvironment() throws Exception { + // without a redirect url env makes no difference + loginPickFormMinimalInput("env1"); + } + + private void loginPickFormMinimalInput(final String env) throws Exception { + final TemporaryToken tt = loginPickSetup(MFAStatus.UNKNOWN); + + final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + + final Response res = req.post(Entity.form(form)); + + assertLoginProcessTokensRemoved(res); + + assertThat("incorrect response code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); + + loginPickOrCreateCheckSessionToken(res, MFAStatus.UNKNOWN); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + + assertNoTempToken(tt); + } + + @Test + public void loginPickJSONMinimalInput() throws Exception { + loginPickJSONMinimalInput(null); + } + + @Test + public void loginPickJSONMinimalInputWithException() throws Exception { + loginPickJSONMinimalInput("env1"); + } + + private void loginPickJSONMinimalInput(final String env) throws Exception { + final TemporaryToken tt = loginPickSetup(MFAStatus.USED); + + final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); + + final Response res = req.post(Entity.json( + ImmutableMap.of("id", "ef0518c79af70ed979907969c6d0a0f7"))); + + assertThat("incorrect response code", res.getStatus(), is(200)); + + assertLoginProcessTokensRemoved(res); + + @SuppressWarnings("unchecked") + final Map response = res.readEntity(Map.class); + + assertThat("incorrect redirect url", response.get("redirecturl"), is((String) null)); + + @SuppressWarnings("unchecked") + final Map token = (Map) response.get("token"); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.USED); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + + assertNoTempToken(tt); + } + + @Test + public void loginPickFormMaximalInput() throws Exception { + loginPickFormMaximalInput(null, "https://foo.com/baz/bat"); + } + + @Test + public void loginPickFormMaximalInputWithEnvironment() throws Exception { + loginPickFormMaximalInput("env1", "https://bar.com/baz/bat"); + } + + private void loginPickFormMaximalInput(final String env, final String url) throws Exception { + final TemporaryToken tt = loginPickSetup(MFAStatus.NOT_USED); + + final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) + .cookie("loginredirect", url) + .cookie("issessiontoken", "false"); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("linkall", "true"); + form.param("policyids", "foo, bar, "); + // tests empty item is ignored + form.param("customcontext", " a , 1 ; b \t , 2 ; ;"); + + final Response res = req.post(Entity.form(form)); + + assertLoginProcessTokensRemoved(res); + + 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"), MFAStatus.NOT_USED + ); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE3))); + assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), + is(set(new PolicyID("foo"), new PolicyID("bar")))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); + + assertNoTempToken(tt); + } + + @Test + public void loginPickJsonMaximalInput() throws Exception { + loginPickJsonMaximalInput(null, "https://foo.com/baz/bat"); + } + + @Test + public void loginPickJsonMaximalInputWithEnvironment() throws Exception { + loginPickJsonMaximalInput("env1", "https://bar.com/baz/bat"); + } + + private void loginPickJsonMaximalInput(final String env, final String url) throws Exception { + final TemporaryToken tt = loginPickSetup(MFAStatus.UNKNOWN); + + final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) + .cookie("loginredirect", url) + .cookie("issessiontoken", "false"); + + final Response res = req.post(Entity.json( + ImmutableMap.of("id", "ef0518c79af70ed979907969c6d0a0f7", + "linkall", true, + "policyids", Arrays.asList("foo", "bar"), + "customcontext", ImmutableMap.of("a", 1, "b", 2)))); + + assertThat("incorrect response code", res.getStatus(), is(200)); + + assertLoginProcessTokensRemoved(res); + + @SuppressWarnings("unchecked") + final Map response = res.readEntity(Map.class); + + assertThat("incorrect redirect url", response.get("redirecturl"), is(url)); + + @SuppressWarnings("unchecked") + final Map token = (Map) response.get("token"); + 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()); + assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE3))); + assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), + is(set(new PolicyID("foo"), new PolicyID("bar")))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); + + assertNoTempToken(tt); + } + + @Test + public void loginPickFormEmptyStrings() throws Exception { + + final TemporaryToken tt = loginPickSetup(MFAStatus.USED); + + final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) + .cookie("loginredirect", " \t ") + .cookie("issessiontoken", "true"); + + final Form form = new Form(); + form.param("id", " ef0518c79af70ed979907969c6d0a0f7 "); + form.param("policyids", " \t \n "); + form.param("linkall", null); + form.param("customcontext", " \t \n "); + + final Response res = req.post(Entity.form(form)); + + assertThat("incorrect response code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); + + loginPickOrCreateCheckSessionToken(res, MFAStatus.USED); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + + assertNoTempToken(tt); + } + + @Test + public void loginPickJsonEmptyData() throws Exception { + + final TemporaryToken tt = loginPickSetup(MFAStatus.NOT_USED); + + final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) + .cookie("loginredirect", " \t ") + .cookie("issessiontoken", "false"); + + final Response res = req.post(Entity.json( + ImmutableMap.of("id", " ef0518c79af70ed979907969c6d0a0f7 ", + "linkall", false, + "policyids", Collections.emptyList(), + "customcontext", Collections.emptyMap()))); + + assertThat("incorrect response code", res.getStatus(), is(200)); + + assertLoginProcessTokensRemoved(res); + + @SuppressWarnings("unchecked") + final Map response = res.readEntity(Map.class); + + assertThat("incorrect redirect url", response.get("redirecturl"), is((String) null)); + + @SuppressWarnings("unchecked") + final Map token = (Map) response.get("token"); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.NOT_USED); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + + assertNoTempToken(tt); + } + + @Test + public void loginPickFailNoSuchEnvironment() throws Exception { + loginPickOrCreateFailNoSuchEnvironment("/login/pick"); + } + + private void loginPickOrCreateFailNoSuchEnvironment(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("environment", "env3") + .cookie("loginredirect", "https://foo.com"); + final Builder jsonrequest = wt.request() + .cookie("environment", "env3") + .header("accept", MediaType.APPLICATION_JSON) + .cookie("loginredirect", "https://foo.com"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new NoSuchEnvironmentException("env3")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new NoSuchEnvironmentException("env3")); + } + + @Test + public void loginPickFailBadRedirect() throws Exception { + loginPickOrCreateFailBadRedirect("/login/pick"); + } + + private void loginPickOrCreateFailBadRedirect(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("loginredirect", "not a url no sir"); + final Builder jsonrequest = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("loginredirect", "not a url no sir"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new IllegalParameterException("Illegal redirect URL: not a url no sir")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new IllegalParameterException("Illegal redirect URL: not a url no sir")); + + // toURI chokes on ^s + request.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); + jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); + + request.cookie("loginredirect", "https://foobar.com/stuff/thingy"); + jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new IllegalParameterException("Post-login redirects are not enabled")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new IllegalParameterException("Post-login redirects are not enabled")); + + final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); + enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + + // with envs + request.cookie("environment", "env1"); + jsonrequest.cookie("environment", "env1"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new IllegalParameterException( + "Post-login redirects are not enabled for environment env1")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new IllegalParameterException( + "Post-login redirects are not enabled for environment env1")); + + enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2/", "env1"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new IllegalParameterException( + "Illegal redirect URL: https://foobar.com/stuff/thingy")); + } + + @Test + public void loginPickFailBadCustomContextString() throws Exception { + loginPickOrCreateFailBadCustomContextString("/login/pick"); + } + + private void loginPickOrCreateFailBadCustomContextString(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request(); + + final Form form = new Form(); + form.param("customcontext", " foo, bar, baz ; a, b"); + + failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", + new IllegalParameterException( + "Bad key/value pair in custom context: foo, bar, baz")); + } + + @Test + public void loginPickFailNoToken() throws Exception { + loginPickOrCreateFailNoToken("/login/pick"); + } + + private void loginPickOrCreateFailNoToken(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request(); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new NoTokenProvidedException("Missing in-process-login-token")); + } + + @Test + public void loginPickFailNoID() throws Exception { + loginPickOrCreateFailNoID("/login/pick"); + } + + private void loginPickOrCreateFailNoID(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", + new MissingParameterException("id")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", + new MissingParameterException("id")); + } + + @Test + public void loginPickFailEmptyID() throws Exception { + loginPickOrCreateFailEmptyID("/login/pick"); + } + + private void loginPickOrCreateFailEmptyID(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + final Form form = new Form(); + form.param("id", " \t "); + + failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", + new MissingParameterException("id")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of("id", " \t "))), + 400, "Bad Request", new MissingParameterException("id")); + } + + @Test + public void loginPickFailBadToken() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/pick") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + final Form form = new Form(); + form.param("id", "an id"); + + failRequestHTML(request.post(Entity.form(form)), 401, "Unauthorized", + new InvalidTokenException("Temporary token")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of("id", "an id"))), + 401, "Unauthorized", new InvalidTokenException("Temporary token")); + } + + @Test + public void loginPickFailNoJSON() throws Exception { + loginPickOrCreateFailNoJSON("/login/pick"); + } + + private void loginPickOrCreateFailNoJSON(final String path) throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(null)), + 400, "Bad Request", new MissingParameterException("JSON body missing")); + } + + @Test + public void loginPickFailJSONWithAdditionalProperties() throws Exception { + loginPickOrCreateFailJSONWithAdditionalProperties("/login/pick"); + } + + private void loginPickOrCreateFailJSONWithAdditionalProperties(final String path) + throws Exception { + final URI target = UriBuilder.fromUri(host) + .path(path) + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of("foo", "bar"))), + 400, "Bad Request", new IllegalParameterException( + "Unexpected parameters in request: foo")); + } + + @Test + public void loginPickFailBadBoolean() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/pick") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "linkall", Collections.emptyList()))), + 400, "Bad Request", new IllegalParameterException( + "linkall must be a boolean")); + } + + @Test + public void loginPickFailNullPolicyID() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/pick") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "policyids", Arrays.asList("foo", null)))), + 400, "Bad Request", new MissingParameterException("policy id")); + } + + @Test + public void loginPickFailEmptyPolicyID() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/pick") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "policyids", Arrays.asList("foo", " \t ")))), + 400, "Bad Request", new MissingParameterException("policy id")); + } + + private void loginPickOrCreateCheckExtendedToken( + final Response res, + final Map customContext, + final MFAStatus mfa) + throws Exception { + assertLoginProcessTokensRemoved(res); + + final NewCookie token = res.getCookies().get(COOKIE_NAME); + final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), + "/", null, "authtoken", token.getMaxAge(), false); + assertThat("incorrect auth cookie less token", token, is(expectedtoken)); + TestCommon.assertCloseTo(token.getMaxAge(), 14 * 24 * 3600, 10); + + checkLoginToken(token.getValue(), customContext, new UserName("u1"), mfa); + } + + private void loginPickOrCreateCheckSessionToken(final Response res, final MFAStatus mfa) + throws Exception, MissingParameterException, IllegalParameterException { + assertLoginProcessTokensRemoved(res); + + final NewCookie token = res.getCookies().get(COOKIE_NAME); + final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), + "/", null, "authtoken", -1, false); + assertThat("incorrect auth cookie less token", token, is(expectedtoken)); + + checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("u1"), mfa); + } + + private Builder loginPickOrCreateRequestBuilder( + final TemporaryToken tt, + final URI target, + final String environment) { + final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); + final Builder req = wt.request() + .cookie("in-process-login-token", tt.getToken()); + if (environment != null) { + req.cookie("environment", environment); + } + return req; + } + + private TemporaryToken loginPickSetup(final MFAStatus mfa) throws Exception { + + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableRedirect(host, admintoken, "https://foo.com/baz"); + enableRedirect(host, COOKIE_NAME, admintoken, "https://bar.com/baz", "env1"); + + final TemporarySessionData data = TemporarySessionData.create( + UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) + .login(set(REMOTE1, REMOTE2, REMOTE3), mfa); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + manager.storage.createUser(NewUser.getBuilder( + new UserName("u1"), UID, new DisplayName("d"), Instant.now(), REMOTE1).build()); + manager.storage.createUser(NewUser.getBuilder( + new UserName("u2"), UID2, new DisplayName("d"), Instant.now(), REMOTE2).build()); + + return tt; + } + + @Test + public void loginCreateFormMinimalInput() throws Exception { + loginCreateFormMinimalInput(null); + } + + @Test + public void loginCreateFormMinimalInputWithEnvironment() throws Exception { + // without a redirect url specified env makes no difference + loginCreateFormMinimalInput("env2"); + } + + private void loginCreateFormMinimalInput(final String env) throws Exception { + final TemporaryToken tt = loginCreateSetup(MFAStatus.UNKNOWN); + + final URI target = UriBuilder.fromUri(host).path("/login/create").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("user", "u1"); + form.param("display", "disp1"); + form.param("email", "e1@g.com"); + + final Response res = req.post(Entity.form(form)); + + assertLoginProcessTokensRemoved(res); + + assertThat("incorrect response code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); + + loginPickOrCreateCheckSessionToken(res, MFAStatus.UNKNOWN); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); + assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); + + assertNoTempToken(tt); + } + + @Test + public void loginCreateJSONMinimalInput() throws Exception { + loginCreateJSONMinimalInput(null); + } + + @Test + public void loginCreateJSONMinimalInputWithEnvironment() throws Exception { + loginCreateJSONMinimalInput("env2"); + } + + + private void loginCreateJSONMinimalInput(final String env) throws Exception { + final TemporaryToken tt = loginCreateSetup(MFAStatus.USED); + + final URI target = UriBuilder.fromUri(host).path("/login/create").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); + + final Map json = ImmutableMap.of( + "id", "ef0518c79af70ed979907969c6d0a0f7", + "user", "u1", + "display", "disp1", + "email", "e1@g.com"); + + final Response res = req.post(Entity.json(json)); + + assertThat("incorrect response code", res.getStatus(), is(201)); + + assertLoginProcessTokensRemoved(res); + + @SuppressWarnings("unchecked") + final Map response = res.readEntity(Map.class); + + assertThat("incorrect redirect url", response.get("redirecturl"), is((String) null)); + + @SuppressWarnings("unchecked") + final Map token = (Map) response.get("token"); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.USED); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); + assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); + + assertNoTempToken(tt); + } + + @Test + public void loginCreateFormMaximalInput() throws Exception { + loginCreateFormMaximalInput(null, "https://foo.com/baz/bat"); + } + + @Test + public void loginCreateFormMaximalInputFromEnvironment() throws Exception { + loginCreateFormMaximalInput("env2", "https://bar.com/baz/bat"); + } + + private void loginCreateFormMaximalInput(final String env, final String url) throws Exception { + final TemporaryToken tt = loginCreateSetup(MFAStatus.NOT_USED); + + final URI target = UriBuilder.fromUri(host).path("/login/create").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) + .cookie("loginredirect", url) + .cookie("issessiontoken", "false"); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("user", "u1"); + form.param("display", "disp1"); + form.param("email", "e1@g.com"); + form.param("linkall", "true"); + form.param("policyids", "foo, bar, "); + // tests empty item is ignored + form.param("customcontext", " a , 1 ; b \t , 2 ; ;"); + + final Response res = req.post(Entity.form(form)); + + assertLoginProcessTokensRemoved(res); + + 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"), MFAStatus.NOT_USED + ); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE2))); + assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), + is(set(new PolicyID("foo"), new PolicyID("bar")))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); + assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); + assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); + + assertNoTempToken(tt); + } + + @Test + public void loginCreateJSONMaximalInput() throws Exception { + loginCreateJSONMaximalInput(null, "https://foo.com/baz/bat"); + } + + @Test + public void loginCreateJSONMaximalInputWithEnvironment() throws Exception { + loginCreateJSONMaximalInput("env2", "https://bar.com/baz/bat"); + } + + private void loginCreateJSONMaximalInput(final String env, final String url) throws Exception { + final TemporaryToken tt = loginCreateSetup(MFAStatus.UNKNOWN); + + final URI target = UriBuilder.fromUri(host).path("/login/create").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) + .cookie("loginredirect", url) + .cookie("issessiontoken", "false"); + + final Map json = MapBuilder.newHashMap() + .with("id", "ef0518c79af70ed979907969c6d0a0f7") + .with("user", "u1") + .with("display", "disp1") + .with("email", "e1@g.com") + .with("linkall", true) + .with("policyids", Arrays.asList("foo", "bar")) + //tests empty item is ignored + .with("customcontext", ImmutableMap.of("a", 1, "b", 2)) + .build(); + + final Response res = req.post(Entity.json(json)); + + assertThat("incorrect response code", res.getStatus(), is(201)); + + assertLoginProcessTokensRemoved(res); + + @SuppressWarnings("unchecked") + final Map response = res.readEntity(Map.class); + + assertThat("incorrect redirect url", response.get("redirecturl"), is(url)); + + @SuppressWarnings("unchecked") + final Map token = (Map) response.get("token"); + 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()); + assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE2))); + assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), + is(set(new PolicyID("foo"), new PolicyID("bar")))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); + TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); + assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); + assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); + + assertNoTempToken(tt); + } + + @Test + public void loginCreateFormEmptyStrings() throws Exception { + + final TemporaryToken tt = loginCreateSetup(MFAStatus.USED); + + final URI target = UriBuilder.fromUri(host).path("/login/create").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) + .cookie("loginredirect", " \t ") + .cookie("issessiontoken", "true"); + + final Form form = new Form(); + form.param("id", " ef0518c79af70ed979907969c6d0a0f7 "); + form.param("user", "u1"); + form.param("display", " disp1 "); + form.param("email", " e1@g.com "); + form.param("linkall", null); + form.param("policyids", " \t \n "); + form.param("customcontext", " \t \n "); + + final Response res = req.post(Entity.form(form)); + + assertLoginProcessTokensRemoved(res); + + assertThat("incorrect response code", res.getStatus(), is(303)); + assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); + + loginPickOrCreateCheckSessionToken(res, MFAStatus.USED); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); + assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); + + assertNoTempToken(tt); + } + + @Test + public void loginCreateJSONEmptyInput() throws Exception { + + final TemporaryToken tt = loginCreateSetup(MFAStatus.NOT_USED); + + final URI target = UriBuilder.fromUri(host).path("/login/create").build(); + + final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) + .cookie("loginredirect", " \t ") + .cookie("issessiontoken", "true"); + + final Map json = MapBuilder.newHashMap() + .with("id", " ef0518c79af70ed979907969c6d0a0f7 ") + .with("user", "u1") + .with("display", " disp1 ") + .with("email", " e1@g.com ") + .with("linkall", false) + .with("policyids", Collections.emptyList()) + //tests empty item is ignored + .with("customcontext", Collections.emptyMap()) + .build(); + + final Response res = req.post(Entity.json(json)); + + assertThat("incorrect response code", res.getStatus(), is(201)); + + assertLoginProcessTokensRemoved(res); + + @SuppressWarnings("unchecked") + final Map response = res.readEntity(Map.class); + + assertThat("incorrect redirect url", response.get("redirecturl"), + is((String)null)); + + @SuppressWarnings("unchecked") + final Map token = (Map) response.get("token"); + checkLoginToken(token, Collections.emptyMap(), new UserName("u1"), MFAStatus.NOT_USED); + + final AuthUser u = manager.storage.getUser(new UserName("u1")); + TestCommon.assertCloseToNow(u.getLastLogin().get()); + assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); + assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); + assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); + assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); + + assertNoTempToken(tt); + } + + public void loginCreateFailNoSuchEnvironment() throws Exception { + loginPickOrCreateFailNoSuchEnvironment("/login/create"); + } + + @Test + public void loginCreateFailBadRedirect() throws Exception { + loginPickOrCreateFailBadRedirect("/login/create"); + } + + @Test + public void loginCreateFailBadCustomContextString() throws Exception { + loginPickOrCreateFailBadCustomContextString("/login/create"); + } + + @Test + public void loginCreateFailNoToken() throws Exception { + loginPickOrCreateFailNoToken("/login/create"); + } + + @Test + public void loginCreateFailNoID() throws Exception { + loginPickOrCreateFailNoID("/login/create"); + } + + @Test + public void loginCreateFailEmptyID() throws Exception { + loginPickOrCreateFailEmptyID("/login/create"); + } + + @Test + public void loginCreateFailBadToken() throws Exception { + + loginCreateSetup(MFAStatus.UNKNOWN); + + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("user", "u1"); + form.param("display", "disp1"); + form.param("email", "e1@g.com"); + + failRequestHTML(request.post(Entity.form(form)), 401, "Unauthorized", + new InvalidTokenException("Temporary token")); + + request.header("accept", MediaType.APPLICATION_JSON); + + final Map json = ImmutableMap.of( + "id", "ef0518c79af70ed979907969c6d0a0f7", + "user", "u1", + "display", "disp1", + "email", "e1@g.com"); + + failRequestJSON(request.post(Entity.json(json)), + 401, "Unauthorized", new InvalidTokenException("Temporary token")); + } + + @Test + public void loginCreateFailNoJSON() throws Exception { + loginPickOrCreateFailNoJSON("/login/create"); + } + + @Test + public void loginCreateFailJSONWithAdditionalProperties() throws Exception { + loginPickOrCreateFailJSONWithAdditionalProperties("/login/create"); + } + + @Test + public void loginCreateFailBadBoolean() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "user", "u1", + "display", "disp1", + "email", "e1@g.com", + "linkall", Collections.emptyList()))), + 400, "Bad Request", new IllegalParameterException( + "linkall must be a boolean")); + } + + @Test + public void loginCreateFailNullPolicyID() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "user", "u1", + "display", "disp1", + "email", "e1@g.com", + "policyids", Arrays.asList("foo", null)))), + 400, "Bad Request", new MissingParameterException("policy id")); + } + + @Test + public void loginCreateFailEmptyPolicyID() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .header("accept", MediaType.APPLICATION_JSON) + .cookie("in-process-login-token", "foobar"); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "user", "u1", + "display", "disp1", + "email", "e1@g.com", + "policyids", Arrays.asList("foo", " \t ")))), + 400, "Bad Request", new MissingParameterException("policy id")); + } + + @Test + public void loginCreateFailBadUserID() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("user", "Au1"); + form.param("display", "disp1"); + form.param("email", "e1@g.com"); + + failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", + new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, + "Illegal character in user name Au1: A")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "user", "Au1", + "display", "disp1", + "email", "e1@g.com"))), + 400, "Bad Request", new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, + "Illegal character in user name Au1: A")); + } + + @Test + public void loginCreateFailBadDisplayName() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("user", "u1"); + form.param("display", "di\tsp1"); + form.param("email", "e1@g.com"); + + failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", + new IllegalParameterException( + "display name contains control characters")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "user", "u1", + "display", "dis\tp1", + "email", "e1@g.com"))), + 400, "Bad Request", new IllegalParameterException( + "display name contains control characters")); + } + + @Test + public void loginCreateFailBadEmail() throws Exception { + final URI target = UriBuilder.fromUri(host) + .path("/login/create") + .build(); + + final WebTarget wt = CLI.target(target); + final Builder request = wt.request() + .cookie("in-process-login-token", "foobar"); + + final Form form = new Form(); + form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); + form.param("user", "u1"); + form.param("display", "disp1"); + form.param("email", "e1@g.@com"); + + failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", + new IllegalParameterException(ErrorType.ILLEGAL_EMAIL_ADDRESS, + "e1@g.@com")); + + request.header("accept", MediaType.APPLICATION_JSON); + + failRequestJSON(request.post(Entity.json(ImmutableMap.of( + "id", "whee", + "user", "u1", + "display", "disp1", + "email", "e1@g.@com"))), + 400, "Bad Request", new IllegalParameterException(ErrorType.ILLEGAL_EMAIL_ADDRESS, + "e1@g.@com")); + } + + private TemporaryToken loginCreateSetup(final MFAStatus mfa) throws Exception { + final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); + + enableLogin(host, admintoken); + enableRedirect(host, admintoken, "https://foo.com/baz"); + enableRedirect(host, COOKIE_NAME, admintoken, "https://bar.com/baz", "env2"); + + final TemporarySessionData data = TemporarySessionData.create( + UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) + .login(set(REMOTE1, REMOTE2), mfa); + + final TemporaryToken tt = new TemporaryToken(data, "this is a token"); + + manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); + + return tt; + } +} 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 index 97c18ae9..73c7446b 100644 --- a/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java +++ b/src/test/java/us/kbase/test/auth2/service/ui/LoginTest.java @@ -1,2903 +1,140 @@ package us.kbase.test.auth2.service.ui; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.argThat; -import static org.mockito.Mockito.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static us.kbase.test.auth2.TestCommon.calculatePKCEChallenge; -import static us.kbase.test.auth2.TestCommon.inst; -import static us.kbase.test.auth2.TestCommon.set; -import static us.kbase.test.auth2.service.ServiceTestUtils.enableLogin; -import static us.kbase.test.auth2.service.ServiceTestUtils.enableProvider; -import static us.kbase.test.auth2.service.ServiceTestUtils.enableRedirect; -import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestHTML; -import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestJSON; -import static us.kbase.test.auth2.service.ServiceTestUtils.setLoginCompleteRedirect; -import static us.kbase.test.auth2.service.ServiceTestUtils.setEnvironment; - -import java.net.URI; -import java.nio.file.Path; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import static org.junit.Assert.fail; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.Invocation.Builder; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; +import javax.servlet.http.HttpServletRequest; -import org.glassfish.jersey.client.ClientProperties; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; -import com.google.common.collect.ImmutableMap; - -import us.kbase.auth2.kbase.KBaseAuthConfig; -import us.kbase.auth2.lib.DisplayName; -import us.kbase.auth2.lib.EmailAddress; -import us.kbase.auth2.lib.PasswordHashAndSalt; -import us.kbase.auth2.lib.PolicyID; -import us.kbase.auth2.lib.Role; -import us.kbase.auth2.lib.TemporarySessionData; -import us.kbase.auth2.lib.TemporarySessionData.Operation; -import us.kbase.auth2.lib.UserName; -import us.kbase.auth2.lib.exceptions.AuthException; -import us.kbase.auth2.lib.exceptions.AuthenticationException; +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.lib.exceptions.InvalidTokenException; -import us.kbase.auth2.lib.exceptions.MissingParameterException; -import us.kbase.auth2.lib.exceptions.NoSuchEnvironmentException; -import us.kbase.auth2.lib.exceptions.NoSuchIdentityProviderException; -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.RemoteIdentity; -import us.kbase.auth2.lib.identity.RemoteIdentityDetails; -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.TemporaryToken; -import us.kbase.auth2.lib.token.TokenType; -import us.kbase.auth2.lib.user.AuthUser; -import us.kbase.auth2.lib.user.LocalUser; -import us.kbase.auth2.lib.user.NewUser; -import us.kbase.test.auth2.MapBuilder; -import us.kbase.test.auth2.MockIdentityProviderFactory; -import us.kbase.test.auth2.MongoStorageTestManager; -import us.kbase.test.auth2.StandaloneAuthServer; -import us.kbase.test.auth2.TestCommon; -import us.kbase.test.auth2.StandaloneAuthServer.ServerThread; -import us.kbase.test.auth2.service.ServiceTestUtils; -import us.kbase.testutils.RegexMatcher; +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 { - - //TODO TEST convert most of these to unit tests, but keep enough for integration tests - - private static final UUID UID = UUID.randomUUID(); - private static final UUID UID2 = UUID.randomUUID(); - private static final UUID UID3 = UUID.randomUUID(); - - private static final String DB_NAME = "test_login_ui"; - private static final String COOKIE_NAME = "login-cookie"; - - private static final RemoteIdentity REMOTE1 = new RemoteIdentity( - new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1", "full1", "e1@g.com")); - - private static final RemoteIdentity REMOTE2 = new RemoteIdentity( - new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2", "full2", "e2@g.com")); - - private static final RemoteIdentity REMOTE3 = new RemoteIdentity( - new RemoteIdentityID("prov", "id3"), - new RemoteIdentityDetails("user3", "full3", "e3@g.com")); - - private static final Client CLI = ClientBuilder.newClient(); - - private static MongoStorageTestManager manager = null; - private static StandaloneAuthServer server = null; - private static int port = -1; - private static String host = null; - - @BeforeClass - public static void beforeClass() throws Exception { - manager = new MongoStorageTestManager(DB_NAME); - final Path cfgfile = ServiceTestUtils.generateTempConfigFile(manager, DB_NAME, COOKIE_NAME); - TestCommon.getenv().put("KB_DEPLOYMENT_CONFIG", cfgfile.toString()); - server = new StandaloneAuthServer(KBaseAuthConfig.class.getName()); - new ServerThread(server).start(); - System.out.println("Main thread waiting for server to start up"); - while (server.getPort() == null) { - Thread.sleep(1000); - } - port = server.getPort(); - host = "http://localhost:" + port; - } - - @AfterClass - public static void afterClass() throws Exception { - if (server != null) { - server.stop(); - } - if (manager != null) { - manager.destroy(); - } - } - - @Before - public void beforeTest() throws Exception { - ServiceTestUtils.resetServer(manager, host, COOKIE_NAME); - } - - @Test - public void startDisplayLoginDisabled() throws Exception { - // returns crappy html only - final WebTarget wt = CLI.target(host + "/login/"); - final String res = wt.request().get().readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - } - - @Test - public void startDisplayWithOneProvider() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - - final WebTarget wt = CLI.target(host + "/login/"); - final String res = wt.request().get().readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - } - - @Test - public void startDisplayWithTwoProviders() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableProvider(host, COOKIE_NAME, admintoken, "prov2"); - - final WebTarget wt = CLI.target(host + "/login/"); - final String res = wt.request().get().readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - } - - @Test - public void suggestName() throws Exception { - final WebTarget wt = CLI.target(host + "/login/suggestname/***FOOTYPANTS***"); - @SuppressWarnings("unchecked") - final Map res = wt.request().get().readEntity(Map.class); - assertThat("incorrect expected name", res, - is(ImmutableMap.of("availablename", "footypants"))); - } - - @Test - public void loginStartMinimalInput() throws Exception { - final Form form = new Form(); - form.param("provider", "prov1"); - final NewCookie expectedsession = new NewCookie("issessiontoken", "true", - "/login", null, "session choice", 30 * 60, false); - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - - loginStart(form, null, expectedsession, expectedredirect, null); - } - - @Test - public void loginStartHeaderEnvironment() throws Exception { - final Form form = new Form(); - form.param("provider", "prov1"); - form.param("environment", "env2"); - final NewCookie expectedsession = new NewCookie("issessiontoken", "true", - "/login", null, "session choice", 30 * 60, false); - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - - loginStart(form, "env1", expectedsession, expectedredirect, "env1"); - } - - @Test - public void loginStartEmptyStringsWithFormEnvironmentWhitespaceHeader() throws Exception { - final Form form = new Form(); - form.param("provider", "prov1"); - form.param("redirecturl", " \t \n "); - form.param("stayloggedin", " \t \n "); - form.param("environment", "myenv"); - final NewCookie expectedsession = new NewCookie("issessiontoken", "true", - "/login", null, "session choice", 30 * 60, false); - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - loginStart(form, " \t ", expectedsession, expectedredirect, "myenv"); - } + // these are unit tests, not integration tests. - @Test - public void loginStartWithRedirectAndNonSessionCookieWithWhitespaceEnvironment() - throws Exception { - final String redirect = "https://foobar.com/thingy/stuff"; - final Form form = new Form(); - form.param("provider", "prov1"); - form.param("redirecturl", redirect); - form.param("stayloggedin", "f"); - form.param("environment", " \t "); - final NewCookie expectedsession = new NewCookie("issessiontoken", "false", - "/login", null, "session choice", 30 * 60, false); - final NewCookie expectedredirect = new NewCookie("loginredirect", redirect, - "/login", null, "redirect url", 30 * 60, false); - - loginStart(form, null, expectedsession, expectedredirect, null); - } + // 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 loginStartWithRedirectAndNonSessionCookieWithEnvironment() throws Exception { - final String redirect = "https://foobaz.com/thingy/stuff"; - final Form form = new Form(); - form.param("provider", "prov1"); - form.param("redirecturl", redirect); - form.param("stayloggedin", "f"); - form.param("environment", "env1"); - final NewCookie expectedsession = new NewCookie("issessiontoken", "false", - "/login", null, "session choice", 30 * 60, false); - final NewCookie expectedredirect = new NewCookie("loginredirect", redirect, - "/login", null, "redirect url", 30 * 60, false); - - loginStart(form, null, expectedsession, expectedredirect, "env1"); - } - - private void loginStart( - final Form form, - final String headerEnv, - final NewCookie expectedsession, - final NewCookie expectedredirect, - final String expectedEnv) - throws Exception { - final IdentityProvider provmock = MockIdentityProviderFactory - .MOCKS.get("prov1"); - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - enableRedirect(host, COOKIE_NAME, admintoken, "https://foobaz.com/thingy", "env1"); - - final String url = "https://foo.com/someurlorother"; - - final StateMatcher stateMatcher = new StateMatcher(); - final PKCEChallengeMatcher pkceMatcher = new PKCEChallengeMatcher(); - when(provmock.getLoginURI( - argThat(stateMatcher), argThat(pkceMatcher), eq(false), eq(expectedEnv))) - .thenReturn(new URI(url)); - - final WebTarget wt = CLI.target(host + "/login/start"); - final Builder b = wt.request(); - if (headerEnv != null) { - b.header("X-DOEKBASE-ENVIRONMENT", headerEnv); - } - final Response res = b.post( - Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); - - assertEnvironmentCookieCorrect(res, expectedEnv, 30 * 60); - - final NewCookie process = res.getCookies().get("in-process-login-token"); - final NewCookie expectedprocess = new NewCookie("in-process-login-token", - process.getValue(), - "/login", null, "logintoken", -1, false); - assertThat("incorrect login process cookie", process, is(expectedprocess)); + 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 - final TemporarySessionData ti = manager.storage.getTemporarySessionData( - new IncomingToken(process.getValue()).getHashedToken()); - assertThat("incorrect temp op", ti.getOperation(), is(Operation.LOGINSTART)); - assertThat("incorrect state", - ti.getOAuth2State(), is(Optional.of(stateMatcher.capturedState))); - assertThat("incorrect pkce challenge", - calculatePKCEChallenge(ti.getPKCECodeVerifier().get()), - is(pkceMatcher.capturedChallenge) + 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) ); - - final NewCookie session = res.getCookies().get("issessiontoken"); - assertThat("incorrect session cookie", session, is(expectedsession)); - - final NewCookie redirect = res.getCookies().get("loginredirect"); - assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); - } - - @Test - public void loginStartFailNoProvider() throws Exception { - failLoginStart(new Form(), 400, "Bad Request", new MissingParameterException("provider")); - - final Form form = new Form(); - form.param("provider", null); - failLoginStart(form, 400, "Bad Request", new MissingParameterException("provider")); - - final Form form2 = new Form(); - form2.param("provider", " \t \n "); - failLoginStart(form2, 400, "Bad Request", new MissingParameterException("provider")); - } - - @Test - public void loginStartFailNoSuchProvider() throws Exception { - final Form form = new Form(); - form.param("provider", "prov3"); - failLoginStart(form, 401, "Unauthorized", new NoSuchIdentityProviderException("prov3")); - } - - @Test - public void loginStartFailNoSuchEnvironment() throws Exception { - final Form form = new Form(); - form.param("provider", "fake"); - form.param("environment", "env3"); - form.param("redirecturl", "https://foo.com"); - failLoginStart(form, 400, "Bad Request", new NoSuchEnvironmentException("env3")); - } - - @Test - public void loginStartFailBadRedirect() throws Exception { - final Form form = new Form(); - form.param("provider", "fake"); - form.param("redirecturl", "this ain't no gotdamned url"); - failLoginStart(form, 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: this ain't no gotdamned url")); - - // toURI chokes on ^s - final Form form2 = new Form(); - form2.param("provider", "fake"); - form2.param("redirecturl", "https://foobar.com/stuff/thingy?a=^h"); - failLoginStart(form2, 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); - - final Form form3 = new Form(); - form3.param("provider", "fake"); - form3.param("redirecturl", "https://foobar.com/stuff/thingy"); - failLoginStart(form3, 400, "Bad Request", new IllegalParameterException( - "Post-login redirects are not enabled")); - - // with environment without redirect url prefix configured - final Form form4 = new Form(); - form4.param("provider", "fake"); - form4.param("redirecturl", "https://foobar.com/stuff/thingy"); - form4.param("environment", "env1"); - failLoginStart(form4, 400, "Bad Request", new IllegalParameterException( - "Post-login redirects are not enabled for environment env1")); - - final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); - enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); - failLoginStart(form3, 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - - // with environment with url prefix configured - enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2/", "env1"); - failLoginStart(form4, 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - } - - private void failLoginStart( - final Form form, - final int expectedHTTPCode, - final String expectedHTTPError, - final AuthException e) - throws Exception { - final WebTarget wt = CLI.target(host + "/login/start"); - final Response res = wt.request().header("Accept", MediaType.APPLICATION_JSON).post( - Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); - - failRequestJSON(res, expectedHTTPCode, expectedHTTPError, e); - } - - @Test - public void loginCompleteImmediateLoginMinimalInput() throws Exception { - loginCompleteImmediateLoginMinimalInput(null); - } - - @Test - public void loginCompleteImmediateLoginMinimalInputWithEnvironment() throws Exception { - loginCompleteImmediateLoginMinimalInput("env1"); - } - - private void loginCompleteImmediateLoginMinimalInput(final String env) throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkceohgodohgod", "foobartoken"); - - loginCompleteImmediateLoginStoreUser(authcode, "pkceohgodohgod", env); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Builder b = wt.request() - .cookie("in-process-login-token", "foobartoken"); - if (env != null) { - b.cookie("environment", env); - } - final Response res = b.get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - - assertLoginProcessTokensRemoved(res); - - final NewCookie token = res.getCookies().get(COOKIE_NAME); - final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), - "/", null, "authtoken", -1, false); - assertThat("incorrect auth cookie less token", token, is(expectedtoken)); - assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - - loginCompleteImmediateLoginCheckToken(token); - } - - @Test - public void loginCompleteImmediateLoginEmptyStringInput() throws Exception { - // also tests that the empty error string is ignored. - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkcethisisinhumane", "foobartoken"); - - loginCompleteImmediateLoginStoreUser(authcode, "pkcethisisinhumane", null); - - final WebTarget wt = loginCompleteSetUpWebTargetEmptyError(authcode, state); - final Response res = wt.request() - .cookie("in-process-login-token", "foobartoken") - .cookie("loginredirect", " \t ") - .cookie("issessiontoken", " \t ") - .get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - - assertLoginProcessTokensRemoved(res); - - final NewCookie token = res.getCookies().get(COOKIE_NAME); - final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), - "/", null, "authtoken", -1, false); - assertThat("incorrect auth cookie less token", token, is(expectedtoken)); - assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - - loginCompleteImmediateLoginCheckToken(token); - } - - @Test - public void loginCompleteImmediateLoginRedirectAndTrueSession() throws Exception { - loginCompleteImmediateLoginRedirectAndTrueSession(null, "https://foobar.com/thingy/stuff"); - } - - @Test - public void loginCompleteImmediateLoginRedirectAndTrueSessionWithEnvironment() - throws Exception { - loginCompleteImmediateLoginRedirectAndTrueSession( - "env2", "https://foobar.com/t2hingy/stuff"); - } - - private void loginCompleteImmediateLoginRedirectAndTrueSession( - final String env, - final String url) - throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - enableRedirect(host, COOKIE_NAME, admintoken, "https://foobar.com/t2hingy", "env2"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkcepkcepkcepkce", "foobartoken"); - - loginCompleteImmediateLoginStoreUser(authcode, "pkcepkcepkcepkce", env); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Builder b = wt.request() - .cookie("in-process-login-token", "foobartoken") - .cookie("loginredirect", url) - .cookie("issessiontoken", "true"); - if (env != null) { - b.cookie("environment", env); - } - final Response res = b.get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); - - assertLoginProcessTokensRemoved(res); - - final NewCookie token = res.getCookies().get(COOKIE_NAME); - final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), - "/", null, "authtoken", -1, false); - assertThat("incorrect auth cookie less token", token, is(expectedtoken)); - assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - - loginCompleteImmediateLoginCheckToken(token); - } - - @Test - public void loginCompleteImmediateLoginRedirectAndFalseSession() throws Exception { - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkceoohhooohhhmm", "foobartoken"); - - loginCompleteImmediateLoginStoreUser(authcode, "pkceoohhooohhhmm", null); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Response res = wt.request() - .cookie("in-process-login-token", "foobartoken") - .cookie("loginredirect", "https://foobar.com/thingy/stuff") - .cookie("issessiontoken", "false") - .get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), - is(new URI("https://foobar.com/thingy/stuff"))); - - assertLoginProcessTokensRemoved(res); - - final NewCookie token = res.getCookies().get(COOKIE_NAME); - final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), - "/", null, "authtoken", token.getMaxAge(), false); - assertThat("incorrect auth cookie less token and max age", token, is(expectedtoken)); - assertThat("incorrect token", token.getValue(), is(RegexMatcher.matches("[A-Z2-7]{32}"))); - TestCommon.assertCloseTo(token.getMaxAge(), 14 * 24 * 3600, 10); - - loginCompleteImmediateLoginCheckToken(token); - } - - private void loginCompleteImmediateLoginStoreUser( - final String authcode, - final String pkce, - final String environment) - throws Exception { - final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, pkce, environment); - - manager.storage.createUser(NewUser.getBuilder( - new UserName("whee"), UID, new DisplayName("dn"), Instant.ofEpochMilli(20000), - remoteIdentity) - .build()); - } - - private void loginCompleteImmediateLoginCheckToken(final NewCookie token) throws Exception { - checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("whee")); - } - - private void checkLoginToken( - final Map uitoken, - final Map customContext, - final UserName userName) - throws Exception { - - ServiceTestUtils.checkReturnedToken(manager, uitoken, customContext, userName, - TokenType.LOGIN, null, 14 * 24 * 3600 * 1000, true); } - private void checkLoginToken( + private void createLocalUserFail( + final Login login, + final HttpServletRequest req, final String token, - final Map customContext, - final UserName userName) - throws Exception { - ServiceTestUtils.checkStoredToken(manager, token, customContext, userName, TokenType.LOGIN, - null, 14 * 24 * 3600 * 1000); - } - - private void assertLoginProcessTokensRemoved(final Response res) { - final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", - "/login", null, "session choice", 0, false); - final NewCookie session = res.getCookies().get("issessiontoken"); - assertThat("incorrect session cookie", session, is(expectedsession)); - - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - final NewCookie redirect = res.getCookies().get("loginredirect"); - assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); - - final NewCookie expectedinprocess = new NewCookie("in-process-login-token", "no token", - "/login", null, "logintoken", 0, false); - final NewCookie inprocess = res.getCookies().get("in-process-login-token"); - assertThat("incorrect process cookie", inprocess, is(expectedinprocess)); - - assertEnvironmentCookieRemoved(res); - } - - private void assertEnvironmentCookieRemoved(final Response res) { - final NewCookie expectedenv = new NewCookie("environment", "no env", - "/login", null, "environment", 0, false); - final NewCookie envcookie = res.getCookies().get("environment"); - assertThat("incorrect state cookie", envcookie, is(expectedenv)); - } - - private void assertEnvironmentCookieCorrect( - final Response res, - final String env, - final int lifetime) { - if (env != null) { - final NewCookie envcookie = res.getCookies().get("environment"); - final NewCookie expectedenv = new NewCookie("environment", env, - "/login", null, "environment", envcookie.getMaxAge(), false); - assertThat("incorrect env cookie", envcookie, is(expectedenv)); - TestCommon.assertCloseTo(envcookie.getMaxAge(), lifetime, 10); - } else { - assertEnvironmentCookieRemoved(res); - } - } - - @Test - public void loginCompleteDelayedMinimalInput() throws Exception { - loginCompleteDelayedMinimalInput(null); - } - - @Test - public void loginCompleteDelayedMinimalInputWithEnvironment() throws Exception { - loginCompleteDelayedMinimalInput("env1"); - } - - private void loginCompleteDelayedMinimalInput(final String env) throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkceopraisethedarkgodsbelow", "foobartoken"); - - final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkceopraisethedarkgodsbelow", env); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Builder b = wt.request() - .cookie("in-process-login-token", "foobartoken"); - if (env != null) { - b.cookie("environment", env); - } - final Response res = b.get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/login/choice"))); - - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - final NewCookie redirect = res.getCookies().get("loginredirect"); - assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); - - final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", - "/login", null, "session choice", 0, false); - final NewCookie session = res.getCookies().get("issessiontoken"); - assertThat("incorrect session cookie", session, is(expectedsession)); - - assertEnvironmentCookieCorrect(res, env, 30 * 60); - - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); - } - - @Test - public void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect() throws Exception { - loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( - null, "https://whee.com/bleah"); - } - - @Test - public void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirectAndAlsoEnvironment() - throws Exception { - loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( - "env2", "https://whoo.com/bleah"); - } - - private void loginCompleteDelayedEmptyStringInputAndAlternateChoiceRedirect( - final String env, - final String url) - throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - setLoginCompleteRedirect(host, admintoken, "https://whee.com/bleah"); - setLoginCompleteRedirect(host, COOKIE_NAME, admintoken, "https://whoo.com/bleah", "env2"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkceisinmybrainicanseeall", "foobartoken"); - - final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkceisinmybrainicanseeall", env); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Builder b = wt.request() - .cookie("in-process-login-token", "foobartoken") - .cookie("loginredirect", " \t ") - .cookie("issessiontoken", " \t "); - if (env != null) { - b.cookie("environment", env); + 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); } - final Response res = b.get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(url))); - - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - final NewCookie redirect = res.getCookies().get("loginredirect"); - assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); - - final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", - "/login", null, "session choice", 0, false); - final NewCookie session = res.getCookies().get("issessiontoken"); - assertThat("incorrect session cookie", session, is(expectedsession)); - - assertEnvironmentCookieCorrect(res, env, 30 * 60); - - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); - } - - @Test - public void loginCompleteDelayedLoginRedirectAndTrueSession() throws Exception { - loginCompleteDelayedLoginRedirectAndTrueSession( - null, "https://whee.com/bleah", "https://foobar.com/thingy/stuff"); - } - - - @Test - public void loginCompleteDelayedLoginRedirectAndTrueSessionAndEnvironment() throws Exception { - loginCompleteDelayedLoginRedirectAndTrueSession( - "env1", "https://whoo.com/bleah", "https://foobar2.com/thingy/stuff"); - } - - private void loginCompleteDelayedLoginRedirectAndTrueSession( - final String env, - final String completeURL, - final String redirectURL) - throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - setLoginCompleteRedirect(host, admintoken, "https://whee.com/bleah"); - final Form form = new Form(); - form.param("environment", "env1"); - form.param("completeloginredirect", "https://whoo.com/bleah"); - form.param("allowedloginredirect", "https://foobar2.com/thingy"); - setEnvironment(host, COOKIE_NAME, admintoken, form); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkcuwgahngalftaghn", "foobartoken"); - - final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkcuwgahngalftaghn", env); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Builder b = wt.request() - .cookie("in-process-login-token", "foobartoken") - .cookie("loginredirect", redirectURL) - .cookie("issessiontoken", "true"); - if (env != null) { - b.cookie("environment", env); + 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); } - final Response res = b.get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(completeURL))); - - final NewCookie redirect = res.getCookies().get("loginredirect"); - final NewCookie expectedredirect = new NewCookie( - "loginredirect", redirectURL, - "/login", null, "redirect url", redirect.getMaxAge(), false); - assertThat("incorrect redirect cookie less max age", redirect, is(expectedredirect)); - TestCommon.assertCloseTo(redirect.getMaxAge(), 30 * 60, 10); - - final NewCookie session = res.getCookies().get("issessiontoken"); - final NewCookie expectedsession = new NewCookie("issessiontoken", "true", - "/login", null, "session choice", session.getMaxAge(), false); - assertThat("incorrect session cookie less max age", session, is(expectedsession)); - TestCommon.assertCloseTo(session.getMaxAge(), 30 * 60, 10); - - assertEnvironmentCookieCorrect(res, env, 30 * 60); - - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); - } - - @Test - public void loginCompleteDelayedLoginRedirectAndFalseSession() throws Exception { - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - enableRedirect(host, admintoken, "https://foobar.com/thingy"); - setLoginCompleteRedirect(host, admintoken, "https://whee.com/bleah"); - - final String authcode = "foobarcode"; - final String state = "foobarstate"; - - saveTemporarySessionData(state, "pkceifeelmoistandsprightly", "foobartoken"); - - final RemoteIdentity remoteIdentity = loginCompleteSetUpProviderMock( - authcode, "pkceifeelmoistandsprightly", null); - - final WebTarget wt = loginCompleteSetUpWebTarget(authcode, state); - final Response res = wt.request() - .cookie("in-process-login-token", "foobartoken") - .cookie("loginredirect", "https://foobar.com/thingy/stuff") - .cookie("issessiontoken", "false") - .get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), - is(new URI("https://whee.com/bleah"))); - - final NewCookie redirect = res.getCookies().get("loginredirect"); - final NewCookie expectedredirect = new NewCookie( - "loginredirect", "https://foobar.com/thingy/stuff", - "/login", null, "redirect url", redirect.getMaxAge(), false); - assertThat("incorrect redirect cookie less max age", redirect, is(expectedredirect)); - TestCommon.assertCloseTo(redirect.getMaxAge(), 30 * 60, 10); - - final NewCookie session = res.getCookies().get("issessiontoken"); - final NewCookie expectedsession = new NewCookie("issessiontoken", "false", - "/login", null, "session choice", session.getMaxAge(), false); - assertThat("incorrect session cookie less max age", session, is(expectedsession)); - TestCommon.assertCloseTo(session.getMaxAge(), 30 * 60, 10); - - loginCompleteDelayedCheckTempAndStateCookies(remoteIdentity, res); - } - - private void loginCompleteDelayedCheckTempAndStateCookies( - final RemoteIdentity remoteIdentity, - final Response res) - throws Exception { - - final NewCookie tempCookie = res.getCookies().get("in-process-login-token"); - final NewCookie expectedtemp = new NewCookie("in-process-login-token", - tempCookie.getValue(), - "/login", null, "logintoken", -1, false); - assertThat("incorrect temp cookie less value and max age", tempCookie, is(expectedtemp)); - - final TemporarySessionData tis = manager.storage.getTemporarySessionData( - new IncomingToken(tempCookie.getValue()).getHashedToken()); - - assertThat("incorrect stored ids", tis.getIdentities().get(), is(set(remoteIdentity))); - } - - private WebTarget loginCompleteSetUpWebTarget(final String authcode, final String state) { - final URI target = UriBuilder.fromUri(host) - .path("/login/complete/prov1") - .queryParam("code", authcode) - .queryParam("state", state) - .build(); - - return CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); - } - - private WebTarget loginCompleteSetUpWebTargetEmptyError( - final String authcode, final String state) { - final URI target = UriBuilder.fromUri(host) - .path("/login/complete/prov1") - .queryParam("code", authcode) - .queryParam("state", state) - .queryParam("error", " \t ") - .build(); - - return CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); } - private RemoteIdentity loginCompleteSetUpProviderMock( - final String authcode, - final String pkce, - final String environment) - throws Exception { - - final IdentityProvider provmock = MockIdentityProviderFactory.MOCKS.get("prov1"); - final RemoteIdentity remoteIdentity = new RemoteIdentity( - new RemoteIdentityID("prov1", "prov1id"), - new RemoteIdentityDetails("user", "full", "email@email.com")); - when(provmock.getIdentities(authcode, pkce, false, environment)) - .thenReturn(set(remoteIdentity)); - return remoteIdentity; - } - - public void saveTemporarySessionData(final String state, final String pkce, final String token) - throws AuthStorageException { - manager.storage.storeTemporarySessionData(TemporarySessionData.create( - UUID.randomUUID(), Instant.now(), Instant.now().plusSeconds(10)) - .login(state, pkce), - IncomingToken.hash(token)); - } - - @Test - public void loginCompleteProviderError() throws Exception { - // the various input paths for the redirect cookie and the session cookie are exactly - // the same as for the delayed login so not testing them again here - - final URI target = UriBuilder.fromUri(host) - .path("/login/complete/prov1") - .queryParam("error", "errorwhee") - .build(); - - final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); - final Response res = wt.request().get(); - - assertThat("incorrect status code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/login/choice"))); - - final NewCookie expectedredirect = new NewCookie("loginredirect", "no redirect", - "/login", null, "redirect url", 0, false); - final NewCookie redirect = res.getCookies().get("loginredirect"); - assertThat("incorrect redirect cookie", redirect, is(expectedredirect)); - - final NewCookie expectedsession = new NewCookie("issessiontoken", "no session", - "/login", null, "session choice", 0, false); - final NewCookie session = res.getCookies().get("issessiontoken"); - assertThat("incorrect session cookie", session, is(expectedsession)); - - final NewCookie tempCookie = res.getCookies().get("in-process-login-token"); - final NewCookie expectedtemp = new NewCookie("in-process-login-token", - tempCookie.getValue(), - "/login", null, "logintoken", -1, false); - assertThat("incorrect temp cookie less value", tempCookie, is(expectedtemp)); - - final TemporarySessionData tis = manager.storage.getTemporarySessionData( - new IncomingToken(tempCookie.getValue()).getHashedToken()); - - assertThat("incorrect op", tis.getOperation(), is(Operation.ERROR)); - assertThat("incorrect error", tis.getError(), is(Optional.of("errorwhee"))); - } - - @Test - public void loginCompleteFailStateMismatch() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - saveTemporarySessionData("important state", "pkce", "foobartoken"); - - final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobartoken") - .cookie("issessiontoken", "false"); - - failRequestJSON(request.get(), 401, "Unauthorized", - new AuthenticationException(ErrorType.AUTHENTICATION_FAILED, - "State values do not match, this may be a CSRF attack")); - } - - @Test - public void loginCompleteFailNoProviderState() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - saveTemporarySessionData("important state", "pkce", "foobartoken"); - - final URI target = UriBuilder.fromUri(host) - .path("/login/complete/prov1") - .queryParam("code", "foocode") - .build(); - - final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); - - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("issessiontoken", "false") - .cookie("in-process-login-token", "foobartoken"); - - failRequestJSON(request.get(), 401, "Unauthorized", - new AuthenticationException(ErrorType.AUTHENTICATION_FAILED, - "State values do not match, this may be a CSRF attack")); - } - - @Test - public void loginCompleteFailNoToken() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - enableProvider(host, COOKIE_NAME, admintoken, "prov1"); - - final URI target = UriBuilder.fromUri(host) - .path("/login/complete/prov1") - .queryParam("state", "somestate") - .build(); - - final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); - - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.get(), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - } - - @Test - public void loginCompleteFailNoAuthcode() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/complete/prov1") - .queryParam("state", "somestate") - .build(); - - final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); - - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("issessiontoken", "false") - .cookie("in-process-login-token", "foobartoken"); - - failRequestJSON(request.get(), 400, "Bad Request", - new MissingParameterException("authorization code")); - } - - @Test - public void loginCompleteFailNoSuchProvider() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableProvider(host, COOKIE_NAME, admintoken, "prov2"); - - final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobartoken"); - - failRequestJSON(request.get(), 401, "Unauthorized", - new NoSuchIdentityProviderException("prov1")); - } - - @Test - public void loginCompleteFailNoSuchEnvironment() throws Exception { - final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("environment", "env3") - .cookie("loginredirect", "http://foo.com"); - - failRequestJSON(request.get(), 400, "Bad Request", new NoSuchEnvironmentException("env3")); - } - - @Test - public void loginCompleteFailBadRedirect() throws Exception { - final WebTarget wt = loginCompleteSetUpWebTarget("foobarcode", "foobarstate"); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("loginredirect", "not a url no sir"); - - failRequestJSON(request.get(), 400, "Bad Request", - new IllegalParameterException("Illegal redirect URL: not a url no sir")); - - // toURI chokes on ^s - request.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); - - failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); - - request.cookie("loginredirect", "https://foobar.com/stuff/thingy"); - - failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( - "Post-login redirects are not enabled")); - - final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); - enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); - failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - - // test with environment - request.cookie("environment", "env1"); - failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( - "Post-login redirects are not enabled for environment env1")); - - enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2", "env1"); - failRequestJSON(request.get(), 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - } - - @Test - public void loginChoice3Create2Login() throws Exception { - // this tests a bunch of orthogonal test cases. Doesn't make much sense to split it up - // since there has to be *some* output for the test, might as well include independent - // cases. - // tests a choice with 3 options to create an account, 2 options to login with an account, - // one of which has two linked IDs. - // tests create accounts having missing email and fullnames and illegal - // email and fullnames. - // tests one of the suggested usernames containing a @ and existing in the system. - // tests one of the users being disabled. - // tests policy ids. - // tests with no redirect cookie. - final Set idents = new HashSet<>(); - for (int i = 1; i < 5; i++) { - idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), - new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); - } - idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id5"), - new RemoteIdentityDetails("user&at@bleah.com", null, null))); - idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id6"), - new RemoteIdentityDetails("whee", "foo\nbar", "not an email"))); - - final TemporarySessionData data = TemporarySessionData.create( - UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(new UserName("userat"), - UID, new DisplayName("f"), Instant.ofEpochMilli(30000)).build(), - new PasswordHashAndSalt("foobarbazbat".getBytes(), "aaa".getBytes())); - - manager.storage.createUser(NewUser.getBuilder( - new UserName("ruser1"), UID2, new DisplayName("disp1"), inst(10000), - new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1a", "full1a", "e1a@g.com"))) - .withPolicyID(new PolicyID("foo"), Instant.ofEpochMilli(60000)) - .withPolicyID(new PolicyID("bar"), Instant.ofEpochMilli(70000)) - .build()); - manager.storage.link(new UserName("ruser1"), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2a", "full2a", "e2a@g.com"))); - manager.storage.createUser(NewUser.getBuilder( - new UserName("ruser2"), UID3, new DisplayName("disp2"), inst(10000), - new RemoteIdentity(new RemoteIdentityID("prov", "id3"), - new RemoteIdentityDetails("user3a", "full3a", "e3a@g.com"))).build()); - when(manager.mockClock.instant()).thenReturn(Instant.ofEpochMilli(40000)); - manager.storage.disableAccount(new UserName("ruser2"), new UserName("adminwhee"), - "Said nasty, but true, things about Steve"); - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - enableLogin(host, admintoken); - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final WebTarget wt = CLI.target(target); - - final String res = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .get() - .readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - - @SuppressWarnings("unchecked") - final Map json = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .header("accept", MediaType.APPLICATION_JSON) - .get() - .readEntity(Map.class); - - final Map expectedJson = new HashMap<>(); - expectedJson.put("pickurl", "pick"); - expectedJson.put("createurl", "create"); - expectedJson.put("cancelurl", "cancel"); - expectedJson.put("suggestnameurl", "suggestname"); - expectedJson.put("redirecturl", null); - expectedJson.put("expires", 11493000000000L); - expectedJson.put("creationallowed", true); - expectedJson.put("provider", "prov"); - expectedJson.put("create", Arrays.asList( - ImmutableMap.of("provusername", "user4", - "availablename", "user4", - "provfullname", "full4", - "id", "4a3cd1ac3f1ffd5d2fecabcfc1856485", - "provemail", "e4@g.com"), - MapBuilder.newHashMap() - .with("provusername", "user&at@bleah.com") - .with("availablename", "userat2") - .with("provfullname", null) - .with("id", "78f2c2dbc07bfc9838c45f601a92762d") - .with("provemail", null) - .build(), - MapBuilder.newHashMap() - .with("provusername", "whee") - .with("availablename", "whee") - .with("provfullname", null) - .with("id", "ccf1ab20b4b412c515182c16f6176b3f") - .with("provemail", null) - .build() - )); - expectedJson.put("login", Arrays.asList( - ImmutableMap.builder() - .put("adminonly", false) - .put("loginallowed", true) - .put("disabled", false) - .put("policyids", Arrays.asList( - ImmutableMap.of("id", "bar", "agreedon", 70000), - ImmutableMap.of("id", "foo", "agreedon", 60000) - )) - .put("id", "5fbea2e6ce3d02f7cdbde0bc31be8059") - .put("user", "ruser1") - .put("provusernames", Arrays.asList("user2", "user1")) - .build(), - ImmutableMap.builder() - .put("adminonly", false) - .put("loginallowed", false) - .put("disabled", true) - .put("policyids", Collections.emptyList()) - .put("id", "de0702aa7927b562e0d6be5b6527cfb2") - .put("user", "ruser2") - .put("provusernames", Arrays.asList("user3")) - .build() - )); - - ServiceTestUtils.assertObjectsEqual(json, expectedJson); - } - - @Test - public void loginChoice2LoginWithRedirectAndLoginDisabled() throws Exception { - // tests with redirect cookie - // tests with login disabled and admin user - // tests with trailing slash on target - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - enableRedirect(host, admintoken, "https://foo.com/whee"); - - final Set idents = new HashSet<>(); - for (int i = 1; i < 3; i++) { - idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), - new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); - } - - final TemporarySessionData data = TemporarySessionData.create( - UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - manager.storage.createUser(NewUser.getBuilder( - new UserName("ruser1"), UID, new DisplayName("disp1"), inst(10000), - new RemoteIdentity(new RemoteIdentityID("prov", "id1"), - new RemoteIdentityDetails("user1a", "full1a", "e1a@g.com"))) - .build()); - manager.storage.createUser(NewUser.getBuilder( - new UserName("ruser2"), UID2, new DisplayName("disp2"), inst(10000), - new RemoteIdentity(new RemoteIdentityID("prov", "id2"), - new RemoteIdentityDetails("user2a", "full2a", "e2a@g.com"))) - .build()); - manager.storage.updateRoles(new UserName("ruser2"), set(Role.ADMIN), set()); - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice/") - .build(); - - final WebTarget wt = CLI.target(target); - - final String res = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .cookie("loginredirect", "https://foo.com/whee/bleah") - .get() - .readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - - @SuppressWarnings("unchecked") - final Map json = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .cookie("loginredirect", "https://foo.com/whee/bleah") - .header("accept", MediaType.APPLICATION_JSON) - .get() - .readEntity(Map.class); - - final Map expectedJson = new HashMap<>(); - expectedJson.put("pickurl", "../pick"); - expectedJson.put("createurl", "../create"); - expectedJson.put("cancelurl", "../cancel"); - expectedJson.put("suggestnameurl", "../suggestname"); - expectedJson.put("redirecturl", "https://foo.com/whee/bleah"); - expectedJson.put("expires", 11493000000000L); - expectedJson.put("creationallowed", false); - expectedJson.put("provider", "prov"); - expectedJson.put("create", Collections.emptyList()); - expectedJson.put("login", Arrays.asList( - ImmutableMap.builder() - .put("adminonly", true) - .put("loginallowed", false) - .put("disabled", false) - .put("policyids", Collections.emptyList()) - .put("id", "ef0518c79af70ed979907969c6d0a0f7") - .put("user", "ruser1") - .put("provusernames", Arrays.asList("user1")) - .build(), - ImmutableMap.builder() - .put("adminonly", false) - .put("loginallowed", true) - .put("disabled", false) - .put("policyids", Collections.emptyList()) - .put("id", "5fbea2e6ce3d02f7cdbde0bc31be8059") - .put("user", "ruser2") - .put("provusernames", Arrays.asList("user2")) - .build() - )); - - ServiceTestUtils.assertObjectsEqual(json, expectedJson); - } - - @Test - public void loginChoice2CreateAndLoginDisabled() throws Exception { - loginChoice2CreateAndLoginDisabled(null); - } - - @Test - public void loginChoice2CreateAndLoginDisabledAndEnvironment() throws Exception { - // without a redirect cookie, env should do nothing - loginChoice2CreateAndLoginDisabled("env1"); - } - - private void loginChoice2CreateAndLoginDisabled(final String env) throws Exception { - // tests with login disabled - // tests with trailing slash on target - // tests empty string for redirect - - final Set idents = new HashSet<>(); - for (int i = 1; i < 3; i++) { - idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), - new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); - } - final TemporarySessionData data = TemporarySessionData.create( - UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice/") - .build(); - - final WebTarget wt = CLI.target(target); - - final Builder b = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .cookie("loginredirect", " \t "); - if (env != null) { - b.cookie("environment", env); - } - final String res = b.get().readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - - final Builder bjson = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .cookie("loginredirect", " \t ") - .header("accept", MediaType.APPLICATION_JSON); - if (env != null) { - bjson.cookie("environment", env); - } - @SuppressWarnings("unchecked") - final Map json = bjson.get().readEntity(Map.class); - - final Map expectedJson = new HashMap<>(); - expectedJson.put("pickurl", "../pick"); - expectedJson.put("createurl", "../create"); - expectedJson.put("cancelurl", "../cancel"); - expectedJson.put("suggestnameurl", "../suggestname"); - expectedJson.put("redirecturl", null); - expectedJson.put("expires", 11493000000000L); - expectedJson.put("creationallowed", false); - expectedJson.put("provider", "prov"); - expectedJson.put("create", Arrays.asList( - ImmutableMap.of("provusername", "user1", - "availablename", "user1", - "provfullname", "full1", - "id", "ef0518c79af70ed979907969c6d0a0f7", - "provemail", "e1@g.com"), - ImmutableMap.of("provusername", "user2", - "availablename", "user2", - "provfullname", "full2", - "id", "5fbea2e6ce3d02f7cdbde0bc31be8059", - "provemail", "e2@g.com") - )); - expectedJson.put("login", Collections.emptyList()); - - ServiceTestUtils.assertObjectsEqual(json, expectedJson); - } - - @Test - public void loginChoice2CreateWithRedirectURL() throws Exception { - loginChoice2CreateWithRedirectURL(null, "https://foo.com/whee/stuff"); - } - - @Test - public void loginChoice2CreateWithRedirectURLAndEnvironment() throws Exception { - loginChoice2CreateWithRedirectURL("env2", "https://bar.com/whee/stuff"); - } - - private void loginChoice2CreateWithRedirectURL(final String env, final String url) - throws Exception { - // tests with redirect cookie - // note that the html form response does not include the redirect url - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - enableRedirect(host, admintoken, "https://foo.com/whee"); - enableRedirect(host, COOKIE_NAME, admintoken, "https://bar.com/whee", "env2"); - enableLogin(host, admintoken); - - final Set idents = new HashSet<>(); - for (int i = 1; i < 3; i++) { - idents.add(new RemoteIdentity(new RemoteIdentityID("prov", "id" + i), - new RemoteIdentityDetails("user" + i, "full" + i, "e" + i + "@g.com"))); - } - final TemporarySessionData data = TemporarySessionData.create( - UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(idents); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final WebTarget wt = CLI.target(target); - - final Builder b = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .cookie("loginredirect", url); - if (env != null) { - b.cookie("environment", env); - } - final String res = b.get().readEntity(String.class); - - TestCommon.assertNoDiffs(res, TestCommon.getTestExpectedData(getClass(), - TestCommon.getCurrentMethodName())); - - final Builder bjson = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .cookie("loginredirect", url) - .header("accept", MediaType.APPLICATION_JSON); - if (env != null) { - bjson.cookie("environment", env); - } - @SuppressWarnings("unchecked") - final Map json = bjson.get().readEntity(Map.class); - // since we don't care about the order of the providers - @SuppressWarnings("unchecked") - final List> l = (List>) json.get("create"); - json.put("create", new HashSet<>(l)); - - final Map expectedJson = new HashMap<>(); - expectedJson.put("pickurl", "pick"); - expectedJson.put("createurl", "create"); - expectedJson.put("cancelurl", "cancel"); - expectedJson.put("suggestnameurl", "suggestname"); - expectedJson.put("redirecturl", url); - expectedJson.put("expires", 11493000000000L); - expectedJson.put("creationallowed", true); - expectedJson.put("provider", "prov"); - expectedJson.put("create", set( - ImmutableMap.of("provusername", "user1", - "availablename", "user1", - "provfullname", "full1", - "id", "ef0518c79af70ed979907969c6d0a0f7", - "provemail", "e1@g.com"), - ImmutableMap.of("provusername", "user2", - "availablename", "user2", - "provfullname", "full2", - "id", "5fbea2e6ce3d02f7cdbde0bc31be8059", - "provemail", "e2@g.com") - )); - expectedJson.put("login", Collections.emptyList()); - - ServiceTestUtils.assertObjectsEqual(json, expectedJson); - } - - @Test - public void loginChoiceFailNoToken() throws Exception { - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final WebTarget wt = CLI.target(target); - - failRequestHTML(wt.request().get(), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - - final Builder res = wt.request() - .header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(res.get(), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - } - - @Test - public void loginChoiceFailEmptyToken() throws Exception { - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final Builder res = CLI.target(target).request() - .cookie("in-process-login-token", " \t "); - - failRequestHTML(res.get(), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - - res.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(res.get(), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - } - - @Test - public void loginChoiceFailBadToken() throws Exception { - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final WebTarget wt = CLI.target(target); - - final Builder res = wt.request() - .cookie("in-process-login-token", "foobarbaz"); - - final Builder jsonrequest = wt.request() - .cookie("in-process-login-token", "foobarbaz") - .header("accept", MediaType.APPLICATION_JSON); - - failRequestHTML(res.get(), 401, "Unauthorized", - new InvalidTokenException("Temporary token")); - - failRequestJSON(jsonrequest.get(), 401, "Unauthorized", - new InvalidTokenException("Temporary token")); - } - - @Test - public void loginChoiceFailNoSuchEnvironment() throws Exception { - - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final WebTarget wt = CLI.target(target); - - final Builder res = wt.request() - .cookie("environment", "env3") - .cookie("loginredirect", "https://foo.com") - .cookie("in-process-login-token", "foobarbaz"); - - final Builder jsonrequest = wt.request() - .cookie("environment", "env3") - .cookie("loginredirect", "https://foo.com") - .cookie("in-process-login-token", "foobarbaz") - .header("accept", MediaType.APPLICATION_JSON); - - failRequestHTML(res.get(), 400, "Bad Request", new NoSuchEnvironmentException("env3")); - - failRequestJSON(jsonrequest.get(), 400, "Bad Request", - new NoSuchEnvironmentException("env3")); - } - - @Test - public void loginChoiceFailBadRedirect() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/choice") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobarbaz") - .cookie("loginredirect", "not a url no sir"); - - final Builder jsonrequest = wt.request() - .cookie("in-process-login-token", "foobarbaz") - .header("accept", MediaType.APPLICATION_JSON) - .cookie("loginredirect", "not a url no sir"); - - failRequestHTML(request.get(), 400, "Bad Request", - new IllegalParameterException("Illegal redirect URL: not a url no sir")); - failRequestJSON(jsonrequest.get(), 400, "Bad Request", - new IllegalParameterException("Illegal redirect URL: not a url no sir")); - - // toURI chokes on ^s - request.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); - jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); - - failRequestHTML(request.get(), 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); - failRequestJSON(jsonrequest.get(), 400, "Bad Request", new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); - - request.cookie("loginredirect", "https://foobar.com/stuff/thingy"); - jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy"); - - failRequestHTML(request.get(), 400, "Bad Request", - new IllegalParameterException("Post-login redirects are not enabled")); - failRequestJSON(jsonrequest.get(), 400, "Bad Request", - new IllegalParameterException("Post-login redirects are not enabled")); - - final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); - enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); - - failRequestHTML(request.get(), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - failRequestJSON(jsonrequest.get(), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - - // with envs - request.cookie("environment", "env1"); - jsonrequest.cookie("environment", "env1"); - - failRequestHTML(request.get(), 400, "Bad Request", new IllegalParameterException( - "Post-login redirects are not enabled for environment env1")); - failRequestJSON(jsonrequest.get(), 400, "Bad Request", new IllegalParameterException( - "Post-login redirects are not enabled for environment env1")); - - enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2/", "env1"); - - failRequestHTML(request.get(), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - failRequestJSON(jsonrequest.get(), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - } - - @Test - 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")))); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - final URI target = UriBuilder.fromUri(host) - .path("/login/cancel") - .build(); - - final WebTarget wt = CLI.target(target); - final Response res = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .post(null); - - assertThat("incorrect response code", res.getStatus(), is(204)); - assertLoginProcessTokensRemoved(res); - assertNoTempToken(tt); - } - - @Test - 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")))); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - final URI target = UriBuilder.fromUri(host) - .path("/login/cancel") - .build(); - - final WebTarget wt = CLI.target(target); - final Response res = wt.request() - .cookie("in-process-login-token", tt.getToken()) - .delete(); - - assertThat("incorrect response code", res.getStatus(), is(204)); - assertLoginProcessTokensRemoved(res); - assertNoTempToken(tt); - } - - private void assertNoTempToken(final TemporaryToken tt) throws Exception { - try { - manager.storage.getTemporarySessionData( - new IncomingToken(tt.getToken()).getHashedToken()); - fail("expected exception getting temp token"); - } catch (NoSuchTokenException e) { - // pass - } - } - - @Test - public void loginCancelFailNoToken() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/cancel") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder res = wt.request() - .header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(res.post(null), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - failRequestJSON(res.delete(), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - } - - @Test - public void loginPickFormMinimalInput() throws Exception { - loginPickFormMinimalInput(null); - } - - @Test - public void loginPickFormMinimalInputWithEnvironment() throws Exception { - // without a redirect url env makes no difference - loginPickFormMinimalInput("env1"); - } - - private void loginPickFormMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginPickSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - - final Response res = req.post(Entity.form(form)); - - assertLoginProcessTokensRemoved(res); - - assertThat("incorrect response code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - - loginPickOrCreateCheckSessionToken(res); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - - assertNoTempToken(tt); - } - - @Test - public void loginPickJSONMinimalInput() throws Exception { - loginPickJSONMinimalInput(null); - } - - @Test - public void loginPickJSONMinimalInputWithException() throws Exception { - loginPickJSONMinimalInput("env1"); - } - - private void loginPickJSONMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginPickSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); - - final Response res = req.post(Entity.json( - ImmutableMap.of("id", "ef0518c79af70ed979907969c6d0a0f7"))); - - assertThat("incorrect response code", res.getStatus(), is(200)); - - assertLoginProcessTokensRemoved(res); - - @SuppressWarnings("unchecked") - final Map response = res.readEntity(Map.class); - - assertThat("incorrect redirect url", response.get("redirecturl"), is((String) null)); - - @SuppressWarnings("unchecked") - final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - - assertNoTempToken(tt); - } - - @Test - public void loginPickFormMaximalInput() throws Exception { - loginPickFormMaximalInput(null, "https://foo.com/baz/bat"); - } - - @Test - public void loginPickFormMaximalInputWithEnvironment() throws Exception { - loginPickFormMaximalInput("env1", "https://bar.com/baz/bat"); - } - - private void loginPickFormMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginPickSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) - .cookie("loginredirect", url) - .cookie("issessiontoken", "false"); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("linkall", "true"); - form.param("policyids", "foo, bar, "); - // tests empty item is ignored - form.param("customcontext", " a , 1 ; b \t , 2 ; ;"); - - final Response res = req.post(Entity.form(form)); - - assertLoginProcessTokensRemoved(res); - - 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")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE3))); - assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), - is(set(new PolicyID("foo"), new PolicyID("bar")))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); - - assertNoTempToken(tt); - } - - @Test - public void loginPickJsonMaximalInput() throws Exception { - loginPickJsonMaximalInput(null, "https://foo.com/baz/bat"); - } - - @Test - public void loginPickJsonMaximalInputWithEnvironment() throws Exception { - loginPickJsonMaximalInput("env1", "https://bar.com/baz/bat"); - } - - private void loginPickJsonMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginPickSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) - .cookie("loginredirect", url) - .cookie("issessiontoken", "false"); - - final Response res = req.post(Entity.json( - ImmutableMap.of("id", "ef0518c79af70ed979907969c6d0a0f7", - "linkall", true, - "policyids", Arrays.asList("foo", "bar"), - "customcontext", ImmutableMap.of("a", 1, "b", 2)))); - - assertThat("incorrect response code", res.getStatus(), is(200)); - - assertLoginProcessTokensRemoved(res); - - @SuppressWarnings("unchecked") - final Map response = res.readEntity(Map.class); - - assertThat("incorrect redirect url", response.get("redirecturl"), is(url)); - - @SuppressWarnings("unchecked") - final Map token = (Map) response.get("token"); - checkLoginToken(token, ImmutableMap.of("a", "1", "b", "2"), new UserName("u1")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE3))); - assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), - is(set(new PolicyID("foo"), new PolicyID("bar")))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); - - assertNoTempToken(tt); - } - - @Test - public void loginPickFormEmptyStrings() throws Exception { - - final TemporaryToken tt = loginPickSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) - .cookie("loginredirect", " \t ") - .cookie("issessiontoken", "true"); - - final Form form = new Form(); - form.param("id", " ef0518c79af70ed979907969c6d0a0f7 "); - form.param("policyids", " \t \n "); - form.param("linkall", null); - form.param("customcontext", " \t \n "); - - final Response res = req.post(Entity.form(form)); - - assertThat("incorrect response code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - - loginPickOrCreateCheckSessionToken(res); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - - assertNoTempToken(tt); - } - - @Test - public void loginPickJsonEmptyData() throws Exception { - - final TemporaryToken tt = loginPickSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/pick").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) - .cookie("loginredirect", " \t ") - .cookie("issessiontoken", "false"); - - final Response res = req.post(Entity.json( - ImmutableMap.of("id", " ef0518c79af70ed979907969c6d0a0f7 ", - "linkall", false, - "policyids", Collections.emptyList(), - "customcontext", Collections.emptyMap()))); - - assertThat("incorrect response code", res.getStatus(), is(200)); - - assertLoginProcessTokensRemoved(res); - - @SuppressWarnings("unchecked") - final Map response = res.readEntity(Map.class); - - assertThat("incorrect redirect url", response.get("redirecturl"), is((String) null)); - - @SuppressWarnings("unchecked") - final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - - assertNoTempToken(tt); - } - - @Test - public void loginPickFailNoSuchEnvironment() throws Exception { - loginPickOrCreateFailNoSuchEnvironment("/login/pick"); - } - - private void loginPickOrCreateFailNoSuchEnvironment(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("environment", "env3") - .cookie("loginredirect", "https://foo.com"); - final Builder jsonrequest = wt.request() - .cookie("environment", "env3") - .header("accept", MediaType.APPLICATION_JSON) - .cookie("loginredirect", "https://foo.com"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new NoSuchEnvironmentException("env3")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new NoSuchEnvironmentException("env3")); - } - - @Test - public void loginPickFailBadRedirect() throws Exception { - loginPickOrCreateFailBadRedirect("/login/pick"); - } - - private void loginPickOrCreateFailBadRedirect(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("loginredirect", "not a url no sir"); - final Builder jsonrequest = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("loginredirect", "not a url no sir"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new IllegalParameterException("Illegal redirect URL: not a url no sir")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new IllegalParameterException("Illegal redirect URL: not a url no sir")); - - // toURI chokes on ^s - request.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); - jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy?a=^h"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy?a=^h")); - - request.cookie("loginredirect", "https://foobar.com/stuff/thingy"); - jsonrequest.cookie("loginredirect", "https://foobar.com/stuff/thingy"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new IllegalParameterException("Post-login redirects are not enabled")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new IllegalParameterException("Post-login redirects are not enabled")); - - final IncomingToken adminToken = ServiceTestUtils.getAdminToken(manager); - enableRedirect(host, adminToken, "https://foobar.com/stuff2/"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - - // with envs - request.cookie("environment", "env1"); - jsonrequest.cookie("environment", "env1"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new IllegalParameterException( - "Post-login redirects are not enabled for environment env1")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new IllegalParameterException( - "Post-login redirects are not enabled for environment env1")); - - enableRedirect(host, COOKIE_NAME, adminToken, "https://foobar.com/stuff2/", "env1"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - failRequestJSON(jsonrequest.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new IllegalParameterException( - "Illegal redirect URL: https://foobar.com/stuff/thingy")); - } - - @Test - public void loginPickFailBadCustomContextString() throws Exception { - loginPickOrCreateFailBadCustomContextString("/login/pick"); - } - - private void loginPickOrCreateFailBadCustomContextString(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request(); - - final Form form = new Form(); - form.param("customcontext", " foo, bar, baz ; a, b"); - - failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", - new IllegalParameterException( - "Bad key/value pair in custom context: foo, bar, baz")); - } - - @Test - public void loginPickFailNoToken() throws Exception { - loginPickOrCreateFailNoToken("/login/pick"); - } - - private void loginPickOrCreateFailNoToken(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request(); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new NoTokenProvidedException("Missing in-process-login-token")); - } - - @Test - public void loginPickFailNoID() throws Exception { - loginPickOrCreateFailNoID("/login/pick"); - } - - private void loginPickOrCreateFailNoID(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - failRequestHTML(request.post(Entity.form(new Form())), 400, "Bad Request", - new MissingParameterException("id")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(Collections.emptyMap())), 400, "Bad Request", - new MissingParameterException("id")); - } - - @Test - public void loginPickFailEmptyID() throws Exception { - loginPickOrCreateFailEmptyID("/login/pick"); - } - - private void loginPickOrCreateFailEmptyID(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - final Form form = new Form(); - form.param("id", " \t "); - - failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", - new MissingParameterException("id")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of("id", " \t "))), - 400, "Bad Request", new MissingParameterException("id")); - } - - @Test - public void loginPickFailBadToken() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/pick") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - final Form form = new Form(); - form.param("id", "an id"); - - failRequestHTML(request.post(Entity.form(form)), 401, "Unauthorized", - new InvalidTokenException("Temporary token")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of("id", "an id"))), - 401, "Unauthorized", new InvalidTokenException("Temporary token")); - } - - @Test - public void loginPickFailNoJSON() throws Exception { - loginPickOrCreateFailNoJSON("/login/pick"); - } - - private void loginPickOrCreateFailNoJSON(final String path) throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(null)), - 400, "Bad Request", new MissingParameterException("JSON body missing")); - } - - @Test - public void loginPickFailJSONWithAdditionalProperties() throws Exception { - loginPickOrCreateFailJSONWithAdditionalProperties("/login/pick"); - } - - private void loginPickOrCreateFailJSONWithAdditionalProperties(final String path) - throws Exception { - final URI target = UriBuilder.fromUri(host) - .path(path) - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of("foo", "bar"))), - 400, "Bad Request", new IllegalParameterException( - "Unexpected parameters in request: foo")); - } - - @Test - public void loginPickFailBadBoolean() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/pick") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "linkall", Collections.emptyList()))), - 400, "Bad Request", new IllegalParameterException( - "linkall must be a boolean")); - } - - @Test - public void loginPickFailNullPolicyID() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/pick") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "policyids", Arrays.asList("foo", null)))), - 400, "Bad Request", new MissingParameterException("policy id")); - } - - @Test - public void loginPickFailEmptyPolicyID() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/pick") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "policyids", Arrays.asList("foo", " \t ")))), - 400, "Bad Request", new MissingParameterException("policy id")); - } - - private void loginPickOrCreateCheckExtendedToken( - final Response res, - final Map customContext) - throws Exception { - assertLoginProcessTokensRemoved(res); - - final NewCookie token = res.getCookies().get(COOKIE_NAME); - final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), - "/", null, "authtoken", token.getMaxAge(), false); - assertThat("incorrect auth cookie less token", token, is(expectedtoken)); - TestCommon.assertCloseTo(token.getMaxAge(), 14 * 24 * 3600, 10); - - checkLoginToken(token.getValue(), customContext, new UserName("u1")); - } - - private void loginPickOrCreateCheckSessionToken(final Response res) - throws Exception, MissingParameterException, IllegalParameterException { - assertLoginProcessTokensRemoved(res); - - final NewCookie token = res.getCookies().get(COOKIE_NAME); - final NewCookie expectedtoken = new NewCookie(COOKIE_NAME, token.getValue(), - "/", null, "authtoken", -1, false); - assertThat("incorrect auth cookie less token", token, is(expectedtoken)); - - checkLoginToken(token.getValue(), Collections.emptyMap(), new UserName("u1")); - } - - private Builder loginPickOrCreateRequestBuilder( - final TemporaryToken tt, - final URI target, - final String environment) { - final WebTarget wt = CLI.target(target).property(ClientProperties.FOLLOW_REDIRECTS, false); - final Builder req = wt.request() - .cookie("in-process-login-token", tt.getToken()); - if (environment != null) { - req.cookie("environment", environment); - } - return req; - } - - private TemporaryToken loginPickSetup() throws Exception { - - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableRedirect(host, admintoken, "https://foo.com/baz"); - enableRedirect(host, COOKIE_NAME, admintoken, "https://bar.com/baz", "env1"); - - final TemporarySessionData data = TemporarySessionData.create( - UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(set(REMOTE1, REMOTE2, REMOTE3)); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - manager.storage.createUser(NewUser.getBuilder( - new UserName("u1"), UID, new DisplayName("d"), Instant.now(), REMOTE1).build()); - manager.storage.createUser(NewUser.getBuilder( - new UserName("u2"), UID2, new DisplayName("d"), Instant.now(), REMOTE2).build()); - - return tt; - } - - @Test - public void loginCreateFormMinimalInput() throws Exception { - loginCreateFormMinimalInput(null); - } - - @Test - public void loginCreateFormMinimalInputWithEnvironment() throws Exception { - // without a redirect url specified env makes no difference - loginCreateFormMinimalInput("env2"); - } - - private void loginCreateFormMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/create").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("user", "u1"); - form.param("display", "disp1"); - form.param("email", "e1@g.com"); - - final Response res = req.post(Entity.form(form)); - - assertLoginProcessTokensRemoved(res); - - assertThat("incorrect response code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - - loginPickOrCreateCheckSessionToken(res); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); - assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); - - assertNoTempToken(tt); - } - - @Test - public void loginCreateJSONMinimalInput() throws Exception { - loginCreateJSONMinimalInput(null); - } - - @Test - public void loginCreateJSONMinimalInputWithEnvironment() throws Exception { - loginCreateJSONMinimalInput("env2"); - } - - - private void loginCreateJSONMinimalInput(final String env) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/create").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env); - - final Map json = ImmutableMap.of( - "id", "ef0518c79af70ed979907969c6d0a0f7", - "user", "u1", - "display", "disp1", - "email", "e1@g.com"); - - final Response res = req.post(Entity.json(json)); - - assertThat("incorrect response code", res.getStatus(), is(201)); - - assertLoginProcessTokensRemoved(res); - - @SuppressWarnings("unchecked") - final Map response = res.readEntity(Map.class); - - assertThat("incorrect redirect url", response.get("redirecturl"), is((String) null)); - - @SuppressWarnings("unchecked") - final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); - assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); - - assertNoTempToken(tt); - } - - @Test - public void loginCreateFormMaximalInput() throws Exception { - loginCreateFormMaximalInput(null, "https://foo.com/baz/bat"); - } - - @Test - public void loginCreateFormMaximalInputFromEnvironment() throws Exception { - loginCreateFormMaximalInput("env2", "https://bar.com/baz/bat"); - } - - private void loginCreateFormMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/create").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) - .cookie("loginredirect", url) - .cookie("issessiontoken", "false"); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("user", "u1"); - form.param("display", "disp1"); - form.param("email", "e1@g.com"); - form.param("linkall", "true"); - form.param("policyids", "foo, bar, "); - // tests empty item is ignored - form.param("customcontext", " a , 1 ; b \t , 2 ; ;"); - - final Response res = req.post(Entity.form(form)); - - assertLoginProcessTokensRemoved(res); - - 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")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE2))); - assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), - is(set(new PolicyID("foo"), new PolicyID("bar")))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); - assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); - assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); - - assertNoTempToken(tt); - } - - - @Test - public void loginCreateJSONMaximalInput() throws Exception { - loginCreateJSONMaximalInput(null, "https://foo.com/baz/bat"); - } - - @Test - public void loginCreateJSONMaximalInputWithEnvironment() throws Exception { - loginCreateJSONMaximalInput("env2", "https://bar.com/baz/bat"); - } - - private void loginCreateJSONMaximalInput(final String env, final String url) throws Exception { - final TemporaryToken tt = loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/create").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, env) - .cookie("loginredirect", url) - .cookie("issessiontoken", "false"); - - final Map json = MapBuilder.newHashMap() - .with("id", "ef0518c79af70ed979907969c6d0a0f7") - .with("user", "u1") - .with("display", "disp1") - .with("email", "e1@g.com") - .with("linkall", true) - .with("policyids", Arrays.asList("foo", "bar")) - //tests empty item is ignored - .with("customcontext", ImmutableMap.of("a", 1, "b", 2)) - .build(); - - final Response res = req.post(Entity.json(json)); - - assertThat("incorrect response code", res.getStatus(), is(201)); - - assertLoginProcessTokensRemoved(res); - - @SuppressWarnings("unchecked") - final Map response = res.readEntity(Map.class); - - assertThat("incorrect redirect url", response.get("redirecturl"), is(url)); - - @SuppressWarnings("unchecked") - final Map token = (Map) response.get("token"); - checkLoginToken(token, ImmutableMap.of("a", "1", "b", "2"), new UserName("u1")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("expected two identities", u.getIdentities(), is(set(REMOTE1, REMOTE2))); - assertThat("incorrect policy ids", u.getPolicyIDs().keySet(), - is(set(new PolicyID("foo"), new PolicyID("bar")))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("foo"))); - TestCommon.assertCloseToNow(u.getPolicyIDs().get(new PolicyID("bar"))); - assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); - assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); - - assertNoTempToken(tt); - } - - @Test - public void loginCreateFormEmptyStrings() throws Exception { - - final TemporaryToken tt = loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/create").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) - .cookie("loginredirect", " \t ") - .cookie("issessiontoken", "true"); - - final Form form = new Form(); - form.param("id", " ef0518c79af70ed979907969c6d0a0f7 "); - form.param("user", "u1"); - form.param("display", " disp1 "); - form.param("email", " e1@g.com "); - form.param("linkall", null); - form.param("policyids", " \t \n "); - form.param("customcontext", " \t \n "); - - final Response res = req.post(Entity.form(form)); - - assertLoginProcessTokensRemoved(res); - - assertThat("incorrect response code", res.getStatus(), is(303)); - assertThat("incorrect target uri", res.getLocation(), is(new URI(host + "/me"))); - - loginPickOrCreateCheckSessionToken(res); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); - assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); - - assertNoTempToken(tt); - } - - @Test - public void loginCreateJSONEmptyInput() throws Exception { - - final TemporaryToken tt = loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host).path("/login/create").build(); - - final Builder req = loginPickOrCreateRequestBuilder(tt, target, null) - .cookie("loginredirect", " \t ") - .cookie("issessiontoken", "true"); - - final Map json = MapBuilder.newHashMap() - .with("id", " ef0518c79af70ed979907969c6d0a0f7 ") - .with("user", "u1") - .with("display", " disp1 ") - .with("email", " e1@g.com ") - .with("linkall", false) - .with("policyids", Collections.emptyList()) - //tests empty item is ignored - .with("customcontext", Collections.emptyMap()) - .build(); - - final Response res = req.post(Entity.json(json)); - - assertThat("incorrect response code", res.getStatus(), is(201)); - - assertLoginProcessTokensRemoved(res); - - @SuppressWarnings("unchecked") - final Map response = res.readEntity(Map.class); - - assertThat("incorrect redirect url", response.get("redirecturl"), - is((String)null)); - - @SuppressWarnings("unchecked") - final Map token = (Map) response.get("token"); - checkLoginToken(token, Collections.emptyMap(), new UserName("u1")); - - final AuthUser u = manager.storage.getUser(new UserName("u1")); - TestCommon.assertCloseToNow(u.getLastLogin().get()); - assertThat("only one identity", u.getIdentities(), is(set(REMOTE1))); - assertThat("incorrect policy ids", u.getPolicyIDs(), is(Collections.emptyMap())); - assertThat("incorrect display name", u.getDisplayName(), is(new DisplayName("disp1"))); - assertThat("incorrect email", u.getEmail(), is(new EmailAddress("e1@g.com"))); - - assertNoTempToken(tt); - } - - public void loginCreateFailNoSuchEnvironment() throws Exception { - loginPickOrCreateFailNoSuchEnvironment("/login/create"); - } - - @Test - public void loginCreateFailBadRedirect() throws Exception { - loginPickOrCreateFailBadRedirect("/login/create"); - } - - @Test - public void loginCreateFailBadCustomContextString() throws Exception { - loginPickOrCreateFailBadCustomContextString("/login/create"); - } - - @Test - public void loginCreateFailNoToken() throws Exception { - loginPickOrCreateFailNoToken("/login/create"); - } - - @Test - public void loginCreateFailNoID() throws Exception { - loginPickOrCreateFailNoID("/login/create"); - } - - @Test - public void loginCreateFailEmptyID() throws Exception { - loginPickOrCreateFailEmptyID("/login/create"); - } - - @Test - public void loginCreateFailBadToken() throws Exception { - - loginChoiceSetup(); - - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("user", "u1"); - form.param("display", "disp1"); - form.param("email", "e1@g.com"); - - failRequestHTML(request.post(Entity.form(form)), 401, "Unauthorized", - new InvalidTokenException("Temporary token")); - - request.header("accept", MediaType.APPLICATION_JSON); - - final Map json = ImmutableMap.of( - "id", "ef0518c79af70ed979907969c6d0a0f7", - "user", "u1", - "display", "disp1", - "email", "e1@g.com"); - - failRequestJSON(request.post(Entity.json(json)), - 401, "Unauthorized", new InvalidTokenException("Temporary token")); - } - - @Test - public void loginCreateFailNoJSON() throws Exception { - loginPickOrCreateFailNoJSON("/login/create"); - } - - @Test - public void loginCreateFailJSONWithAdditionalProperties() throws Exception { - loginPickOrCreateFailJSONWithAdditionalProperties("/login/create"); - } - - @Test - public void loginCreateFailBadBoolean() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "user", "u1", - "display", "disp1", - "email", "e1@g.com", - "linkall", Collections.emptyList()))), - 400, "Bad Request", new IllegalParameterException( - "linkall must be a boolean")); - } - - @Test - public void loginCreateFailNullPolicyID() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "user", "u1", - "display", "disp1", - "email", "e1@g.com", - "policyids", Arrays.asList("foo", null)))), - 400, "Bad Request", new MissingParameterException("policy id")); - } - - @Test - public void loginCreateFailEmptyPolicyID() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .header("accept", MediaType.APPLICATION_JSON) - .cookie("in-process-login-token", "foobar"); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "user", "u1", - "display", "disp1", - "email", "e1@g.com", - "policyids", Arrays.asList("foo", " \t ")))), - 400, "Bad Request", new MissingParameterException("policy id")); - } - - @Test - public void loginCreateFailBadUserID() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("user", "Au1"); - form.param("display", "disp1"); - form.param("email", "e1@g.com"); - - failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", - new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, - "Illegal character in user name Au1: A")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "user", "Au1", - "display", "disp1", - "email", "e1@g.com"))), - 400, "Bad Request", new IllegalParameterException(ErrorType.ILLEGAL_USER_NAME, - "Illegal character in user name Au1: A")); - } - - @Test - public void loginCreateFailBadDisplayName() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("user", "u1"); - form.param("display", "di\tsp1"); - form.param("email", "e1@g.com"); - - failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", - new IllegalParameterException( - "display name contains control characters")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "user", "u1", - "display", "dis\tp1", - "email", "e1@g.com"))), - 400, "Bad Request", new IllegalParameterException( - "display name contains control characters")); - } - - @Test - public void loginCreateFailBadEmail() throws Exception { - final URI target = UriBuilder.fromUri(host) - .path("/login/create") - .build(); - - final WebTarget wt = CLI.target(target); - final Builder request = wt.request() - .cookie("in-process-login-token", "foobar"); - - final Form form = new Form(); - form.param("id", "ef0518c79af70ed979907969c6d0a0f7"); - form.param("user", "u1"); - form.param("display", "disp1"); - form.param("email", "e1@g.@com"); - - failRequestHTML(request.post(Entity.form(form)), 400, "Bad Request", - new IllegalParameterException(ErrorType.ILLEGAL_EMAIL_ADDRESS, - "e1@g.@com")); - - request.header("accept", MediaType.APPLICATION_JSON); - - failRequestJSON(request.post(Entity.json(ImmutableMap.of( - "id", "whee", - "user", "u1", - "display", "disp1", - "email", "e1@g.@com"))), - 400, "Bad Request", new IllegalParameterException(ErrorType.ILLEGAL_EMAIL_ADDRESS, - "e1@g.@com")); - } - - private TemporaryToken loginChoiceSetup() throws Exception { - final IncomingToken admintoken = ServiceTestUtils.getAdminToken(manager); - - enableLogin(host, admintoken); - enableRedirect(host, admintoken, "https://foo.com/baz"); - enableRedirect(host, COOKIE_NAME, admintoken, "https://bar.com/baz", "env2"); - - final TemporarySessionData data = TemporarySessionData.create( - UUID.randomUUID(), Instant.ofEpochMilli(1493000000000L), 10000000000000L) - .login(set(REMOTE1, REMOTE2)); - - final TemporaryToken tt = new TemporaryToken(data, "this is a token"); - - manager.storage.storeTemporarySessionData(data, IncomingToken.hash("this is a token")); - - return tt; - } } 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/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 dbcb507a..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 @@ -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; @@ -50,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; @@ -152,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) @@ -198,6 +201,7 @@ public void getTokensMaximalInput() throws Exception { .withNullableDevice("dev") .withNullableOS("o", "osv") .build()) + .withMFA(MFAStatus.USED) .build(), token.getHashedToken().getTokenHash()); @@ -209,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"); @@ -254,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) @@ -270,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) @@ -285,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) @@ -388,7 +396,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() @@ -402,7 +411,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 @@ -421,62 +431,65 @@ 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, MFAStatus.UNKNOWN, "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, MFAStatus.UNKNOWN, "foo", + 100_000_000L * 24 * 3600 * 1000L, true); + } } @Test 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 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 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}}