diff --git a/openam-authentication/openam-auth-windowsdesktopsso/src/main/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSO.java b/openam-authentication/openam-auth-windowsdesktopsso/src/main/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSO.java index b24e7d642c..3a4592b20d 100644 --- a/openam-authentication/openam-auth-windowsdesktopsso/src/main/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSO.java +++ b/openam-authentication/openam-auth-windowsdesktopsso/src/main/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSO.java @@ -224,15 +224,17 @@ public int process(Callback[] callbacks, int state) } } catch (Exception ee) { debug.error("Authentication failed with new cred.Stack Trace", ee); - throw new AuthLoginException(amAuthWindowsDesktopSSO, - "auth", null, ee); + throw toAuthLoginException(ee); } } else { debug.error("Authentication failed with PrivilegedActionException wrapped GSSException. Stack Trace", e); throw new AuthLoginException(amAuthWindowsDesktopSSO, "auth", null, e); } - } + } else { + debug.error("Authentication failed with PrivilegedActionException. Stack Trace", e); + throw toAuthLoginException(e); + } } catch (GSSException e1 ){ int major = e1.getMajor(); if (major == GSSException.CREDENTIALS_EXPIRED) { @@ -247,8 +249,7 @@ public int process(Callback[] callbacks, int state) } } catch (Exception ee) { debug.error("Authentication failed with new cred. Stack Trace", ee); - throw new AuthLoginException(amAuthWindowsDesktopSSO, - "auth", null, ee); + throw toAuthLoginException(ee); } } else { debug.error("Authentication failed with GSSException. Stack Trace", e1); @@ -360,6 +361,21 @@ private static Exception extractException(Exception e) { return e; } + /** + * Returns the AuthLoginException that the token check threw inside Subject.doAs, with its error code and + * arguments, or wraps any other failure in an "auth" AuthLoginException. + * + * @param e the exception from the token check, possibly wrapped in PrivilegedActionException(s) + * @return the exception for the module to throw + */ + private static AuthLoginException toAuthLoginException(Exception e) { + Exception cause = extractException(e); + if (cause instanceof AuthLoginException) { + return (AuthLoginException) cause; + } + return new AuthLoginException(amAuthWindowsDesktopSSO, "auth", null, cause); + } + /** * TODO-JAVADOC */ @@ -876,10 +892,12 @@ private void verifyAttributes() throws AuthLoginException { * identity in the organization * @param organization organization or the organization name where the identity will be * looked up - * @return the attribute value for the identity searched. Empty string if not found or - * null if an error occurs + * @return the attribute value for the identity searched, or null if no identity is found + * @throws AuthLoginException if the alias search attribute names cannot be read, or the search fails with an + * identity repository ("idRepoSearch") or SSO ("ssoSearch") error */ - private String searchUserAccount(String attributeValue, String organization) + @VisibleForTesting + String searchUserAccount(String attributeValue, String organization) throws AuthLoginException { String classMethod = "WindowsDesktopSSO.searchUserAccount: "; diff --git a/openam-authentication/openam-auth-windowsdesktopsso/src/main/resources/amAuthWindowsDesktopSSO.properties b/openam-authentication/openam-auth-windowsdesktopsso/src/main/resources/amAuthWindowsDesktopSSO.properties index c01e24189e..7f40b98e1a 100644 --- a/openam-authentication/openam-auth-windowsdesktopsso/src/main/resources/amAuthWindowsDesktopSSO.properties +++ b/openam-authentication/openam-auth-windowsdesktopsso/src/main/resources/amAuthWindowsDesktopSSO.properties @@ -26,6 +26,7 @@ # # Portions Copyrighted 2011-2014 ForgeRock AS +# Portions Copyright 2026 3A Systems, LLC. # @@ -75,5 +76,5 @@ ssoSearch=SSO exception while trying to find the user {0} in the realm {1} notfound=Unable to find the user {0} in the realm {1} i18nTrue=Enabled i18nFalse=Disabled -untrustedToken=Kerberos tokens from realm {0} are not trusted. +untrustedToken=Kerberos token for {0} is not from a trusted realm. diff --git a/openam-authentication/openam-auth-windowsdesktopsso/src/test/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSOKerberosTest.java b/openam-authentication/openam-auth-windowsdesktopsso/src/test/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSOKerberosTest.java index 3628f6aed5..f742142344 100644 --- a/openam-authentication/openam-auth-windowsdesktopsso/src/test/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSOKerberosTest.java +++ b/openam-authentication/openam-auth-windowsdesktopsso/src/test/java/com/sun/identity/authentication/modules/windowsdesktopsso/WindowsDesktopSSOKerberosTest.java @@ -35,6 +35,8 @@ import javax.security.auth.Subject; import java.io.File; +import java.io.IOException; +import java.security.PrivilegedActionException; import java.util.Base64; import java.util.Collections; import java.util.HashMap; @@ -176,7 +178,7 @@ public void renewedServiceLoginIsCachedWhenRetriedTokenFails(String shape, boole TestModule first = newModule("cache-failed-" + shape, negotiate(SERVICE_PRINCIPAL, SPNEGO_MECH), untrusted); first.credentials = credentials; AuthLoginException e = expectThrows(AuthLoginException.class, () -> processAtServerDebugLevel(first)); - assertEquals(e.getErrorCode(), "auth"); + assertEquals(e.getErrorCode(), "untrustedToken"); assertEquals(first.serviceLogins, 2); Subject renewed = credentials.lastUsed(); @@ -209,20 +211,55 @@ public void tokenFromTrustedRealmLogsUserIn() throws Exception { } @Test - public void tokenFromUntrustedRealmDoesNotLogUserIn() throws Exception { + public void tokenFromUntrustedRealmIsRejected() throws Exception { Map options = options(); options.put(ATTR_PREFIX + "kerberos-realms-trusted", Collections.singleton("OTHER.COM")); TestModule module = newModule("untrustedRealm", negotiate(SERVICE_PRINCIPAL, SPNEGO_MECH), options); - // #1174: the "untrustedToken" AuthLoginException is thrown inside Subject.doAs, and process() rethrows only a - // wrapped GSSException, so the module ignores the token instead of failing. Once #1174 is fixed this turns - // red: expect an AuthLoginException with the "untrustedToken" error code instead. - assertEquals(module.process(null, ISAuthConstants.LOGIN_START), ISAuthConstants.LOGIN_IGNORE, - "the module ignores an untrusted token instead of failing: " - + "https://github.com/OpenIdentityPlatform/OpenAM/issues/1174"); + AuthLoginException e = expectThrows(AuthLoginException.class, + () -> module.process(null, ISAuthConstants.LOGIN_START)); + assertEquals(e.getErrorCode(), "untrustedToken"); + // The argument is the user principal from the token, not its realm. + assertEquals(e.getMessage(), "Kerberos token for " + USER_PRINCIPAL + " is not from a trusted realm."); assertNull(module.getPrincipal()); } + @Test + public void tokenForKnownUserLogsUserIn() throws Exception { + Map options = options(); + options.put(ATTR_PREFIX + "lookupUserInRealm", Collections.singleton("true")); + TestModule module = newModule("knownUser", negotiate(SERVICE_PRINCIPAL, SPNEGO_MECH), options); + module.knownUser = USER; + + assertEquals(module.process(null, ISAuthConstants.LOGIN_START), ISAuthConstants.LOGIN_SUCCEED); + assertEquals(module.getPrincipal().getName(), USER); + } + + @Test + public void tokenForUnknownUserIsRejected() throws Exception { + Map options = options(); + options.put(ATTR_PREFIX + "lookupUserInRealm", Collections.singleton("true")); + TestModule module = newModule("unknownUser", negotiate(SERVICE_PRINCIPAL, SPNEGO_MECH), options); + + AuthLoginException e = expectThrows(AuthLoginException.class, + () -> module.process(null, ISAuthConstants.LOGIN_START)); + assertEquals(e.getErrorCode(), "notfound"); + assertEquals(e.getMessageArgs(), new Object[] {USER, "/"}); + assertNull(module.getPrincipal()); + } + + @Test + public void unexpectedFailureInTokenCheckIsRejected() throws Exception { + TestModule module = newModule("unexpectedFailure", negotiate(SERVICE_PRINCIPAL, SPNEGO_MECH), options()); + IOException failure = new IOException("token check failed"); + module.tokenCheckFailure = failure; + + AuthLoginException e = expectThrows(AuthLoginException.class, + () -> module.process(null, ISAuthConstants.LOGIN_START)); + assertEquals(e.getErrorCode(), "auth"); + assertSame(e.getCause(), failure); + } + @Test public void tokenForAnotherServiceIsRejected() throws Exception { TestModule module = newModule("otherService", negotiate(OTHER_SERVICE_PRINCIPAL, SPNEGO_MECH), options()); @@ -294,6 +331,10 @@ private static final class TestModule extends WindowsDesktopSSO { private int serviceLogins; // Null: the service credentials never expire. private ExpiringServiceCredentials credentials; + // Null: the token check runs; otherwise it fails as Subject.doAs reports a checked exception. + private Exception tokenCheckFailure; + // Null: the identity store has no users; otherwise the only user that the search finds. + private String knownUser; TestModule(HttpServletRequest request) { this.request = request; @@ -322,9 +363,18 @@ protected void createUpdateKrb5ConfigFile() { serviceLogins++; } + /** No identity store in this test: finds only the known user and, as the module does, returns the value. */ + @Override + String searchUserAccount(String attributeValue, String organization) { + return attributeValue.equals(knownUser) ? attributeValue : null; + } + @Override void authenticateToken(Subject subject, byte[] kerberosToken, Set trustedRealms) throws Exception { + if (tokenCheckFailure != null) { + throw new PrivilegedActionException(tokenCheckFailure); + } if (credentials != null) { credentials.check(subject); }