Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5527329
Fix user search bugs re underscores + mongo errors
MrCreosote Jun 3, 2024
f4f368c
Merge pull request #439 from kbase/dev-user_search
MrCreosote Jun 4, 2024
d0ef8c9
add gradle to dependabot file
Xiangs18 Jun 14, 2025
f49190b
change schedule.interval to monthly
Xiangs18 Jun 16, 2025
47f6a01
Merge pull request #444 from kbase/dev-update_dependenbot
Xiangs18 Jun 16, 2025
cbbb3b5
Move LoginTest -> LoginIntegrationTest
MrCreosote Oct 2, 2025
11012d2
Move login integration test files to correct locations
MrCreosote Oct 2, 2025
9cc6caa
Merge pull request #490 from kbase/dev-no_unserscores
MrCreosote Oct 2, 2025
457ebb4
Disallow repeating and trailing underscores in new user names
MrCreosote Oct 2, 2025
e0ac681
Merge pull request #491 from kbase/dev-no_unserscores
MrCreosote Oct 3, 2025
d655e8b
Update username suggestion endpoint for underscore changes
MrCreosote Oct 7, 2025
8ee390b
santize -> sanitize
MrCreosote Oct 7, 2025
6ededd3
Merge pull request #492 from kbase/dev-underscore
MrCreosote Oct 7, 2025
b077abd
Fix flaky test by ensuring deterministic identity ordering
dauglyon Nov 4, 2025
27bd1c1
Merge pull request #493 from kbase/fix-flaky-identity-ordering-test
dauglyon Nov 4, 2025
5760026
Add MFAStatus class
MrCreosote Nov 16, 2025
37f5bf4
Merge pull request #494 from kbase/dev-mfa
MrCreosote Nov 17, 2025
e1c53a9
Add identity provider response class
MrCreosote Nov 16, 2025
509b8cd
Use IdentityProviderResponse class in auth
MrCreosote Nov 17, 2025
00e7c2a
Merge pull request #495 from kbase/dev-mfa
MrCreosote Nov 17, 2025
d48dd96
Fix immutability bug in identity provider config
MrCreosote Nov 17, 2025
fa6eaae
Merge pull request #496 from kbase/dev-mfa
MrCreosote Nov 17, 2025
d474311
Merge pull request #497 from kbase/dev-mfa
MrCreosote Nov 17, 2025
aa9b52a
Add MFA state determination code to OrcID provider
MrCreosote Nov 17, 2025
366044e
Merge pull request #498 from kbase/dev-mfa
MrCreosote Nov 17, 2025
487555b
Add MFAStatus to Stored Token
MrCreosote Nov 17, 2025
11a03bd
Merge pull request #499 from kbase/dev-mfa
MrCreosote Nov 17, 2025
2b7fc4b
Store token MFA status in Mongo
MrCreosote Nov 17, 2025
77e5463
Merge pull request #500 from kbase/dev-mfa
MrCreosote Nov 18, 2025
3000b4a
Make token type case insenstive when creating serv / dev tokens
MrCreosote Nov 18, 2025
7328bb1
Merge pull request #501 from kbase/dev-mfa
MrCreosote Nov 18, 2025
4b0e4d2
Update MFA descriptions
MrCreosote Nov 18, 2025
49407f2
Merge pull request #502 from kbase/dev-mfa
MrCreosote Nov 18, 2025
d2a68a6
Add MFA to TemporarySessionData LOGINIDENTS operation
MrCreosote Nov 18, 2025
801888b
Allow specifying the MFA status for test mode tokens
MrCreosote Nov 18, 2025
e28ad11
Add mfa to external endpoints
MrCreosote Nov 19, 2025
3d6f50a
Merge pull request #503 from kbase/dev-mfa
MrCreosote Nov 19, 2025
ed38c26
Merge pull request #504 from kbase/dev-mfa
MrCreosote Nov 19, 2025
9c678b8
Clarify comment
MrCreosote Nov 19, 2025
0487515
Merge pull request #505 from kbase/dev-mfa
MrCreosote Nov 19, 2025
e3e0b4b
Bump version + release notes
MrCreosote Nov 19, 2025
99516be
Merge pull request #506 from kbase/dev-mfa
MrCreosote Nov 19, 2025
830793b
Improve test mode create token test
MrCreosote Nov 19, 2025
77a783a
Merge pull request #507 from kbase/dev-mfa
MrCreosote Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 2 additions & 0 deletions deploy.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/us/kbase/auth2/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
95 changes: 58 additions & 37 deletions src/main/java/us/kbase/auth2/lib/Authentication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1156,7 +1162,7 @@ public Map<UserName, DisplayName> getUserDisplayNames(
public Optional<UserName> getAvailableUserName(final String suggestedUserName)
throws AuthStorageException {
requireNonNull(suggestedUserName, "suggestedUserName");
final Optional<UserName> target = UserName.sanitizeName(suggestedUserName);
final Optional<UserName> target = NewUserName.sanitizeName(suggestedUserName);
Optional<UserName> availableUserName = Optional.empty();
if (target.isPresent()) {
availableUserName = getAvailableUserName(target.get(), false, true);
Expand All @@ -1181,10 +1187,17 @@ private Optional<UserName> 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<UserName, DisplayName> users = storage.getUserDisplayNames(spec, -1);
final boolean match = users.containsKey(suggestedUserName);
final boolean hasNumSuffix = sugStrip.length() != sugName.length();
Expand Down Expand Up @@ -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<RemoteIdentity> 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 -
Expand All @@ -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;
}
Expand All @@ -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<RemoteIdentity> 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);
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<PolicyID> policyIDs,
Expand All @@ -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<RemoteIdentity> 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<RemoteIdentity> ids = new HashSet<>(tsd.getIdentities().get());
final Optional<RemoteIdentity> match = getIdentity(identityID, ids);
if (!match.isPresent()) {
throw new UnauthorizedException(String.format(
Expand Down Expand Up @@ -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
Expand All @@ -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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<RemoteIdentity> 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<RemoteIdentity> ids = new HashSet<>(tsd.getIdentities().get());
final Optional<RemoteIdentity> ri = getIdentity(identityID, ids);
if (!ri.isPresent()) {
throw new UnauthorizedException(String.format(
Expand Down Expand Up @@ -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<RemoteIdentity> getIdentity(
Expand Down Expand Up @@ -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<RemoteIdentity> ids = idp.getIdentities(
authcode, tids.getPKCECodeVerifier().get(), true, environment);
final Set<RemoteIdentity> ids = idp.getIdentities( // don't care about MFA here (yet)
authcode, tids.getPKCECodeVerifier().get(), true, environment).getIdentities();
final Set<RemoteIdentity> 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
Expand Down Expand Up @@ -3165,7 +3186,7 @@ public <T extends ExternalConfig> 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");
Expand Down
Loading
Loading