From 0dcc4fb5e6fd375dfa1ca018f09450834d9dd6fd Mon Sep 17 00:00:00 2001 From: Diana Krepinska Vilkolakova Date: Tue, 9 Nov 2021 15:01:02 +0100 Subject: [PATCH] [ELY-2254] Provide a LoginModule compatible security realm. --- .../security/auth/realm/ElytronMessages.java | 15 +- .../auth/realm/JaasSecurityRealm.java | 332 ++++++++++++------ .../server/ServerAuthenticationContext.java | 3 +- .../security/auth/JaasSecurityRealmTest.java | 158 +++++++-- .../security/auth/TestCallbackHandler.java | 3 +- .../security/auth/TestLoginModule.java | 59 +--- .../security/auth/TestLoginModule2.java | 87 +++++ .../wildfly/security/auth/jaas-login.config | 6 + .../wildfly/security/auth/jaas-login2.config | 3 + .../org/wildfly/security/auth/login.config | 3 - 10 files changed, 488 insertions(+), 181 deletions(-) create mode 100644 tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule2.java create mode 100644 tests/base/src/test/resources/org/wildfly/security/auth/jaas-login.config create mode 100644 tests/base/src/test/resources/org/wildfly/security/auth/jaas-login2.config delete mode 100644 tests/base/src/test/resources/org/wildfly/security/auth/login.config diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java index bc6ab0fc5e0..b6a4adc3b20 100644 --- a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java +++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java @@ -29,11 +29,15 @@ import org.jboss.logging.annotations.LogMessage; import org.jboss.logging.annotations.Message; import org.jboss.logging.annotations.MessageLogger; +import org.jboss.logging.annotations.Param; import org.jboss.logging.annotations.ValidIdRange; import org.jboss.logging.annotations.ValidIdRanges; import org.wildfly.security.auth.server.RealmUnavailableException; import org.wildfly.security.auth.server.SecurityRealm; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; + /** * Log messages and exceptions for Elytron. * @@ -55,7 +59,7 @@ interface ElytronMessages extends BasicLogger { @LogMessage(level = Logger.Level.DEBUG) @Message(id = 1007, value = "JAAS authentication failed for principal %s") - void debugJAASAuthenticationFailure(Principal principal, @Cause Throwable cause); + void debugInfoJaasAuthenticationFailure(Principal principal, @Cause Throwable cause); @Message(id = 1008, value = "Failed to create login context") RealmUnavailableException failedToCreateLoginContext(@Cause Throwable cause); @@ -133,4 +137,13 @@ interface ElytronMessages extends BasicLogger { @Message(id = 13001, value = "Realm is failing over.") void realmFailover(@Cause RealmUnavailableException rue); + @Message(id = 13002, value = "%s does not handle a callback of type %s") + UnsupportedCallbackException unableToHandleCallback(@Param Callback callback, String callbackHandler, String callbackType); + + @Message(id = 13003, value = "Failed to load JAAS configuration file.") + RealmUnavailableException failedToLoadJaasConfigFile(); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 13004, value = "JAAS logout failed for principal %s") + void debugInfoJaasLogoutFailure(Principal principal, @Cause Throwable cause); } diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/JaasSecurityRealm.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/JaasSecurityRealm.java index 77b5b84c13b..cdba184c99a 100644 --- a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/JaasSecurityRealm.java +++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/JaasSecurityRealm.java @@ -18,7 +18,6 @@ package org.wildfly.security.auth.realm; -import static java.lang.System.getSecurityManager; import static org.wildfly.security.auth.realm.ElytronMessages.log; import javax.security.auth.Subject; @@ -27,28 +26,31 @@ import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; -import java.io.IOException; +import java.io.File; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.security.AccessController; +import java.net.URI; +import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; import java.security.Principal; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.security.acl.Group; +import java.security.Security; +import java.security.URIParameter; import java.security.spec.AlgorithmParameterSpec; -import java.util.Enumeration; -import java.util.Set; import org.wildfly.common.Assert; -import org.wildfly.security.auth.callback.CallbackUtil; +import org.wildfly.security.auth.callback.CredentialCallback; import org.wildfly.security.auth.principal.NamePrincipal; +import org.wildfly.security.authz.Attributes; import org.wildfly.security.authz.AuthorizationIdentity; import org.wildfly.security.auth.server.RealmIdentity; import org.wildfly.security.auth.server.RealmUnavailableException; import org.wildfly.security.auth.server.SecurityRealm; import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.authz.MapAttributes; import org.wildfly.security.credential.Credential; import org.wildfly.security.evidence.Evidence; import org.wildfly.security.evidence.PasswordGuessEvidence; @@ -60,28 +62,74 @@ */ public class JaasSecurityRealm implements SecurityRealm { - private final String loginConfiguration; - + private static final String DEFAULT_CONFIGURATION_POLICY_TYPE = "JavaLoginConfig"; + private final URI jaasConfigFilePath; + private final String entry; private final CallbackHandler handler; + private final ClassLoader classLoader; + + /** + * Construct a new instance. + * + * @param entry JAAS configuration file entry (must not be {@code null}) + */ + public JaasSecurityRealm(final String entry) { + this(entry, (String) null); + } + + /** + * Construct a new instance. + * + * @param entry JAAS configuration file entry (must not be {@code null}) + * @param classLoader classLoader to use with LoginContext, this class loader must contain LoginModule CallbackHandler classes + */ + public JaasSecurityRealm(final String entry, final ClassLoader classLoader) { + this(entry, null, classLoader); + } + + /** + * Construct a new instance. + * + * @param entry JAAS configuration file entry (must not be {@code null}) + * @param jaasConfigFilePath path to JAAS configuration file + */ + public JaasSecurityRealm(final String entry, final String jaasConfigFilePath) { + this(entry, jaasConfigFilePath, null); + } /** * Construct a new instance. * - * @param loginConfiguration the login configuration name to use + * @param entry JAAS configuration file entry (must not be {@code null}) + * @param jaasConfigFilePath path to JAAS configuration file + * @param classLoader classLoader to use with LoginContext, this class loader must contain LoginModule CallbackHandler classes */ - public JaasSecurityRealm(final String loginConfiguration) { - this(loginConfiguration, null); + public JaasSecurityRealm(final String entry, final String jaasConfigFilePath, final ClassLoader classLoader) { + this(entry, jaasConfigFilePath, classLoader, null); } /** * Construct a new instance. * - * @param loginConfiguration the login configuration name to use - * @param handler the JAAS callback handler to use + * @param entry JAAS configuration file entry (must not be {@code null}) + * @param jaasConfigFilePath path to JAAS configuration file + * @param callbackHandler callbackHandler to pass to LoginContext + * @param classLoader classLoader to use with LoginContext, this class loader must contain LoginModule CallbackHandler classes */ - public JaasSecurityRealm(final String loginConfiguration, final CallbackHandler handler) { - this.loginConfiguration = loginConfiguration; - this.handler = handler; + public JaasSecurityRealm(final String entry, final String jaasConfigFilePath, final ClassLoader classLoader, final CallbackHandler callbackHandler) { + Assert.checkNotNullParam("entry", entry); + if (jaasConfigFilePath != null) { + this.jaasConfigFilePath = Paths.get(jaasConfigFilePath).toUri(); + } else { + this.jaasConfigFilePath = null; + } + this.entry = entry; + this.handler = callbackHandler; + if (classLoader != null) { + this.classLoader = classLoader; + } else { + this.classLoader = Thread.currentThread().getContextClassLoader(); + } } @Override @@ -103,47 +151,58 @@ public SupportLevel getCredentialAcquireSupport(final Class evidenceType, final String algorithmName) throws RealmUnavailableException { Assert.checkNotNullParam("evidenceType", evidenceType); - return PasswordGuessEvidence.class.isAssignableFrom(evidenceType) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED; + return SupportLevel.POSSIBLY_SUPPORTED; } - private LoginContext createLoginContext(final String loginConfig, final Subject subject, final CallbackHandler handler) throws RealmUnavailableException { - if (getSecurityManager() != null) { - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> new LoginContext(loginConfig, subject, handler)); - } catch (PrivilegedActionException pae) { - throw ElytronMessages.log.failedToCreateLoginContext(pae.getCause()); + /** + * @param entry login configuration file entry + * @param subject classLoader to use with LoginContext, this class loader must contain LoginModule CallbackHandler classes + * @param callbackHandler callbackHandler to pass to LoginContext + * @return the instance of LoginContext + * @throws RealmUnavailableException + */ + private LoginContext createLoginContext(final String entry, final Subject subject, final CallbackHandler callbackHandler) throws RealmUnavailableException { + if (jaasConfigFilePath != null) { + File file = new File(this.jaasConfigFilePath); + if (!file.exists() && !file.isDirectory()) { + throw ElytronMessages.log.failedToLoadJaasConfigFile(); } } - else { - try { - return new LoginContext(loginConfig, subject, handler); - } catch (LoginException le) { - throw ElytronMessages.log.failedToCreateLoginContext(le); + try { + if (jaasConfigFilePath == null) { + return new LoginContext(entry, subject, callbackHandler); + } else { + return new LoginContext(entry, subject, callbackHandler, Configuration.getInstance(DEFAULT_CONFIGURATION_POLICY_TYPE, new URIParameter(jaasConfigFilePath))); } + } catch (LoginException | NoSuchAlgorithmException le) { + throw ElytronMessages.log.failedToCreateLoginContext(le); } } - private CallbackHandler createCallbackHandler(final Principal principal, final PasswordGuessEvidence evidence) throws RealmUnavailableException { - if (handler == null) { - return new DefaultCallbackHandler(principal, evidence); - } - else { + private CallbackHandler createCallbackHandler(final Principal principal, final Evidence evidence) { + if (handler != null) { try { - final CallbackHandler callbackHandler = handler.getClass().newInstance(); - // preserve backwards compatibility: custom handlers were allowed in the past as long as they had a public setSecurityInfo method. + final CallbackHandler callbackHandler = handler.getClass().getConstructor().newInstance(); + // custom handlers were allowed in the past as long as they had a public setSecurityInfo method. Use this method if it exists final Method setSecurityInfo = handler.getClass().getMethod("setSecurityInfo", Principal.class, Object.class); setSecurityInfo.invoke(callbackHandler, principal, evidence); return callbackHandler; - } catch (Exception e) { - throw ElytronMessages.log.failedToInstantiateCustomHandler(e); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + // ignore if this method does not exist + return handler; } + } else if (Security.getProperty("auth.login.defaultCallbackHandler") != null) { + // security property "auth.login.defaultCallbackHandler" is not null so LoginContext will initialize it itself + return null; + } else { + return new JaasSecurityRealmDefaultCallbackHandler(principal, evidence); } } private class JaasRealmIdentity implements RealmIdentity { private final Principal principal; - + private LoginContext loginContext; private Subject subject; private JaasRealmIdentity(final Principal principal) { @@ -154,6 +213,10 @@ public Principal getRealmIdentityPrincipal() { return principal; } + public Subject getSubject() { + return subject; + } + @Override public SupportLevel getCredentialAcquireSupport(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException { return JaasSecurityRealm.this.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec); @@ -183,31 +246,32 @@ public SupportLevel getEvidenceVerifySupport(final Class evi @Override public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableException { Assert.checkNotNullParam("evidence", evidence); - if (evidence instanceof PasswordGuessEvidence) { - this.subject = null; - boolean successfulLogin; - final CallbackHandler callbackHandler = createCallbackHandler(principal, (PasswordGuessEvidence) evidence); + this.subject = null; + boolean successfulLogin; + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader(classLoader); + } + final CallbackHandler callbackHandler = createCallbackHandler(principal, evidence); final Subject subject = new Subject(); - final LoginContext context = createLoginContext(loginConfiguration, subject, callbackHandler); - - log.tracef("Trying to authenticate subject %s using LoginContext %s using JaasSecurityRealm", - principal, context); - + loginContext = createLoginContext(entry, subject, callbackHandler); + log.tracef("Trying to authenticate subject %s using LoginContext %s using JaasSecurityRealm", principal, loginContext); try { - context.login(); + loginContext.login(); successfulLogin = true; - this.subject = subject; - } catch (LoginException le) { - ElytronMessages.log.debugJAASAuthenticationFailure(principal, le); + this.subject = loginContext.getSubject(); + } catch (LoginException loginException) { successfulLogin = false; + ElytronMessages.log.debugInfoJaasAuthenticationFailure(principal, loginException); } - return successfulLogin; - } else { - return false; + } finally { + Thread.currentThread().setContextClassLoader(oldClassLoader); } + return successfulLogin; } - public boolean exists() throws RealmUnavailableException { + public boolean exists() { /* we don't really know that the identity exists, but we know that there is always * an authorization identity so that's as good as {@code true} */ @@ -216,80 +280,138 @@ public boolean exists() throws RealmUnavailableException { @Override public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { - return new JaasAuthorizationIdentity(this.principal, this.subject); + return JaasAuthorizationIdentity.fromSubject(subject); + } + + @Override + public void dispose() { + // call logout in order to empty the subject + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); + try { + try { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader(classLoader); + } + if (loginContext != null) { + loginContext.logout(); + } + } catch (LoginException e) { + ElytronMessages.log.debugInfoJaasLogoutFailure(this.principal, e); + } + } finally { + Thread.currentThread().setContextClassLoader(oldClassLoader); + } } } - private static class DefaultCallbackHandler implements CallbackHandler { + /** + * Default CallbackHandler passed to the LoginContext when none is provided to JAAS security realm and none is configured in the "auth.login.defaultCallbackHandler" security property. + */ + private static class JaasSecurityRealmDefaultCallbackHandler implements CallbackHandler { private final Principal principal; - private final PasswordGuessEvidence evidence; + private final Object evidence; - private DefaultCallbackHandler(final Principal principal, final PasswordGuessEvidence evidence) { + private JaasSecurityRealmDefaultCallbackHandler(final Principal principal, final Evidence evidence) { this.principal = principal; this.evidence = evidence; } @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { Assert.checkNotNullParam("callbacks", callbacks); - for (Callback callback : callbacks) { if (callback instanceof NameCallback) { - NameCallback nameCallback = (NameCallback) callback; + NameCallback nc = (NameCallback) callback; if (principal != null) - nameCallback.setName(this.principal.getName()); - } - else if (callback instanceof PasswordCallback) { - ((PasswordCallback) callback).setPassword(evidence.getGuess()); + nc.setName(principal.getName()); + } else if (callback instanceof PasswordCallback) { + if (evidence instanceof PasswordGuessEvidence) { + ((PasswordCallback) callback).setPassword(((PasswordGuessEvidence) evidence).getGuess()); + } else { + PasswordCallback pc = (PasswordCallback) callback; + char[] password = getPassword(); + if (password != null) + pc.setPassword(password); + } + } else if (callback instanceof CredentialCallback && evidence instanceof Credential) { + final CredentialCallback credentialCallback = (CredentialCallback) callback; + Credential credential = (Credential) evidence; + if (credentialCallback.isCredentialSupported(credential)) { + credentialCallback.setCredential(credential); + } + } else { + throw ElytronMessages.log.unableToHandleCallback(callback, this.getClass().getName(), callback.getClass().getCanonicalName()); } - else { - CallbackUtil.unsupported(callback); + } + } + + /** + * Source: A utility method for obtaining of password taken from + * https://github.com/picketbox/picketbox/blob/master/security-jboss-sx/jbosssx/src/main/java/org/jboss/security/auth/callback/JBossCallbackHandler.java + * on November 2021 + *

+ * Try to convert the credential value into a char[] using the + * first of the following attempts which succeeds: + *

+ * 1. Check for instanceof char[] + * 2. Check for instanceof String and then use toCharArray() + * 3. See if credential has a toCharArray() method and use it + * 4. Use toString() followed by toCharArray(). + * + * @return a char[] representation of the credential. + */ + private char[] getPassword() { + char[] password = null; + if (evidence instanceof char[]) { + password = (char[]) evidence; + } else if (evidence instanceof String) { + String s = (String) evidence; + password = s.toCharArray(); + } else { + try { + Class[] types = {}; + Method m = evidence.getClass().getMethod("toCharArray", types); + Object[] args = {}; + password = (char[]) m.invoke(evidence, args); + } catch (Exception e) { + if (evidence != null) { + String s = evidence.toString(); + password = s.toCharArray(); + } } } + return password; } } + /** + * A JAAS realm's authorization identity. Roles are mapped from all Subject's principals with the following rule: + * key of the attribute is principal's simple classname and the value is principal's name + */ private static class JaasAuthorizationIdentity implements AuthorizationIdentity { - private static final String CALLER_PRINCIPAL_GROUP = "CallerPrincipal"; - - private final Principal principal; - private Principal callerPrincipal; - private final Subject subject; - - private JaasAuthorizationIdentity(final Principal principal, final Subject subject) { - this.principal = principal; - this.subject = subject; - // check if the subject has a caller principal group - if it has then we should use that principal. - this.callerPrincipal = getCallerPrincipal(subject); - } + private MapAttributes attributes; - /** - * Obtains the caller principal from the specified {@link Subject}. This method looks for a group called {@code - * CallerPrincipal} and if it finds one it returns the first {@link java.security.Principal} in the group. - * - * @param subject the {@link javax.security.auth.Subject} to be inspected. - * @return the first {@link java.security.Principal} found in the {@code CallerPrincipal} group or {@code null} if - * a caller principal couldn't be found. - */ - private Principal getCallerPrincipal(Subject subject) { - Principal callerPrincipal = null; + private static JaasAuthorizationIdentity fromSubject(final Subject subject) { + MapAttributes attributes = new MapAttributes(); + // map all subject's principals to attributes with the following rule: + // key of the attribute is principal's simple classname and the value is principal's name if (subject != null) { - Set principals = subject.getPrincipals(); - if (principals != null && !principals.isEmpty()) { - for (Principal principal : principals) { - if (principal instanceof Group && principal.getName().equals(CALLER_PRINCIPAL_GROUP)) { - Enumeration enumeration = ((Group) principal).members(); - if (enumeration.hasMoreElements()) { - callerPrincipal = enumeration.nextElement(); - break; - } - } - } + for (Principal principal : subject.getPrincipals()) { + attributes.addLast(principal.getClass().getSimpleName(), principal.getName()); } } - return callerPrincipal; + return new JaasAuthorizationIdentity(attributes); + } + + private JaasAuthorizationIdentity(MapAttributes attributes) { + this.attributes = attributes; + } + + @Override + public Attributes getAttributes() { + return attributes; } } } diff --git a/auth/server/base/src/main/java/org/wildfly/security/auth/server/ServerAuthenticationContext.java b/auth/server/base/src/main/java/org/wildfly/security/auth/server/ServerAuthenticationContext.java index 21196ad1140..2798d35242f 100644 --- a/auth/server/base/src/main/java/org/wildfly/security/auth/server/ServerAuthenticationContext.java +++ b/auth/server/base/src/main/java/org/wildfly/security/auth/server/ServerAuthenticationContext.java @@ -2039,6 +2039,7 @@ AuthorizedAuthenticationState doAuthorization(final boolean requireLoginPermissi SecurityIdentity authorizedIdentity = Assert.assertNotNull(domain.transform(new SecurityIdentity(domain, authenticationPrincipal, realmInfo, authorizationIdentity, domain.getCategoryRoleMappers(), IdentityCredentials.NONE, IdentityCredentials.NONE))); authorizedIdentity = authorizedIdentity.withPublicCredentials(publicCredentials).withPrivateCredentials(privateCredentials); + if (log.isTraceEnabled()) { log.tracef("Authorizing principal %s.", authenticationPrincipal.getName()); if (authorizationIdentity != null) { @@ -2062,8 +2063,8 @@ AuthorizedAuthenticationState doAuthorization(final boolean requireLoginPermissi ElytronMessages.log.trace("Authorization succeed"); return new AuthorizedAuthenticationState(authorizedIdentity, authenticationPrincipal, realmInfo, realmIdentity, mechanismRealmConfiguration, mechanismConfiguration); } - @Override + @Override boolean authorize(final Principal authorizationId, final boolean authorizeRunAs) throws RealmUnavailableException { final AuthorizedAuthenticationState authzState = doAuthorization(true); if (authzState == null) { diff --git a/tests/base/src/test/java/org/wildfly/security/auth/JaasSecurityRealmTest.java b/tests/base/src/test/java/org/wildfly/security/auth/JaasSecurityRealmTest.java index 82e5bb9eaee..603f7aa7ee3 100644 --- a/tests/base/src/test/java/org/wildfly/security/auth/JaasSecurityRealmTest.java +++ b/tests/base/src/test/java/org/wildfly/security/auth/JaasSecurityRealmTest.java @@ -20,21 +20,25 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import org.junit.AfterClass; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.wildfly.security.auth.permission.LoginPermission; import org.wildfly.security.auth.principal.NamePrincipal; -import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.auth.server.RealmUnavailableException; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.auth.server.ServerAuthenticationContext; import org.wildfly.security.auth.realm.JaasSecurityRealm; import org.wildfly.security.auth.server.RealmIdentity; import org.wildfly.security.auth.server.SecurityRealm; import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.credential.PublicKeyCredential; import org.wildfly.security.evidence.PasswordGuessEvidence; -import org.wildfly.security.evidence.X509PeerCertificateChainEvidence; /** * Testsuite for the {@link org.wildfly.security.auth.realm.JaasSecurityRealm}. @@ -45,65 +49,165 @@ public class JaasSecurityRealmTest { @BeforeClass - public static void init() { - System.setProperty("java.security.auth.login.config", JaasSecurityRealmTest.class.getResource("login.config").toString()); + public static void beforeClass() { + System.setProperty("java.security.auth.login.config", JaasSecurityRealmTest.class.getResource("jaas-login.config").toString()); + } + + @AfterClass + public static void afterClass() { + System.clearProperty("java.security.auth.login.config"); } @Test - public void testJaasSecurityRealm() throws Exception { + public void testSmokeJaasSecurityRealm() throws Exception { - // create a JAAS security realm with the default callback handler. - SecurityRealm realm = new JaasSecurityRealm("test"); + // create a JAAS security realm with the file from system property and default callback handler. + SecurityRealm realm = new JaasSecurityRealm("Entry1"); // test the creation of a realm identity. RealmIdentity realmIdentity = realm.getRealmIdentity(new NamePrincipal("elytron")); assertNotNull("Unexpected null realm identity", realmIdentity); - // check the supported credential types (the default handler can only handle char[], String and ClearPassword credentials).. + // we do not allow to obtain the credentials from the JAAS realm assertEquals("Invalid credential support", SupportLevel.UNSUPPORTED, realmIdentity.getCredentialAcquireSupport(PasswordCredential.class, "blah", null)); assertEquals("Invalid credential support", SupportLevel.UNSUPPORTED, realmIdentity.getCredentialAcquireSupport(PublicKeyCredential.class, null, null)); + // we do not know what type of evidence the custom realms support so the result should be possibly supported + assertEquals("Invalid credential support", SupportLevel.POSSIBLY_SUPPORTED, realmIdentity.getEvidenceVerifySupport(PasswordGuessEvidence.class, "blah")); + // the JAAS realm identity cannot be used to obtain credentials, so getCredential should always return null. assertNull("Invalid non null credential", realmIdentity.getCredential(PasswordCredential.class, null)); - // use the realm identity to verify all supported credentials - this will trigger a JAAS login that will use the test module. + // use the realm identity to verify provided credentials - this will trigger a JAAS login that will use the test module. assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence("passwd12#$".toCharArray()))); assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("wrongpass".toCharArray()))); // get the authenticated realm identity after successfully verifying the credential. assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence("passwd12#$".toCharArray()))); - AuthorizationIdentity authRealmIdentity = realmIdentity.getAuthorizationIdentity(); - assertNotNull("Unexpected null authenticated realm identity", authRealmIdentity); - // check if the authenticated identity returns the caller principal as set by the test login module. -// Principal authPrincipal = authRealmIdentity.getPrincipal(); -// assertNotNull("Unexpected null principal", authPrincipal); -// assertEquals("Invalid principal name", new NamePrincipal("auth-caller"), authPrincipal); - - // dispose the auth realm identity - should trigger a JAAS logout that clears the subject. - // TODO - some other solution is needed here! We can no longer force JAAS logout in an authorization scenario. - //authPrincipal = authRealmIdentity.getPrincipal(); - // after the logout, the subject no longer contains a caller principal so the identity should return the same principal as the realm identity. - //assertNotNull("Unexpected null principal", authPrincipal); - //assertEquals("Invalid principal name", new NamePrincipal("elytron"), authPrincipal); + assertNotNull("Unexpected null authenticated realm identity", realmIdentity.getAuthorizationIdentity()); + } + + @Test(expected = IllegalArgumentException.class) + public void testEntryCannotBeNull() { + new JaasSecurityRealm(null); + } + @Test(expected = RealmUnavailableException.class) + public void testPathMustExist() throws RealmUnavailableException { + JaasSecurityRealm realm = new JaasSecurityRealm("entry", "this/path/does/not/exist"); + RealmIdentity realmIdentity = realm.getRealmIdentity(new NamePrincipal("javajoe")); + realmIdentity.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray())); } @Test public void testJaasSecurityRealmWithCustomCallbackHandler() throws Exception { // create a JAAS realm that takes a custom callback handler. - SecurityRealm realm = new JaasSecurityRealm("test", new TestCallbackHandler()); + SecurityRealm realm = new JaasSecurityRealm("Entry1", null, null, new TestCallbackHandler()); + // create a new realm identity using the realm. + RealmIdentity realmIdentity = realm.getRealmIdentity(new NamePrincipal("javajoe")); + // verify the credentials using the custom callback handler. + assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("wrongpass".toCharArray()))); + } + + @Test + public void testJaasSecurityRealmWithEntry2() throws Exception { + // create a JAAS realm that takes a custom callback handler. + SecurityRealm realm = new JaasSecurityRealm("Entry2", null, null, new TestCallbackHandler()); // create a new realm identity using the realm. RealmIdentity realmIdentity = realm.getRealmIdentity(new NamePrincipal("javajoe")); + // verify the credentials using the custom callback handler. + assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("wrongpass".toCharArray()))); - assertEquals("Invalid credential support", SupportLevel.SUPPORTED, realmIdentity.getEvidenceVerifySupport(PasswordGuessEvidence.class, null)); - assertEquals("Invalid credential support", SupportLevel.UNSUPPORTED, realmIdentity.getEvidenceVerifySupport(X509PeerCertificateChainEvidence.class, null)); + realmIdentity = realm.getRealmIdentity(new NamePrincipal("userFromTestModule2")); // verify the credentials using the custom callback handler. - assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence("userPassword".toCharArray()))); + } + + @Test + public void testJaasSecurityRealmWithConfiguredPathToJAASConfigFile() throws Exception { + + SecurityRealm realm = new JaasSecurityRealm("Entry1", "./src/test/resources/org/wildfly/security/auth/jaas-login2.config", null); + RealmIdentity realmIdentity = realm.getRealmIdentity(new NamePrincipal("javajoe")); + assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + + realmIdentity = realm.getRealmIdentity(new NamePrincipal("userFromTestModule2")); assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("wrongpass".toCharArray()))); + assertTrue(realmIdentity.verifyEvidence(new PasswordGuessEvidence("userPassword".toCharArray()))); + + SecurityDomain securityDomain = SecurityDomain.builder().setDefaultRealmName("default").addRealm("default", realm).build() + .setPermissionMapper(((permissionMappable, roles) -> LoginPermission.getInstance())) + .build(); + ServerAuthenticationContext sac1 = securityDomain.createNewAuthenticationContext(); + sac1.setAuthenticationName("userFromTestModule2"); + assertFalse(sac1.verifyEvidence(new PasswordGuessEvidence("incorrectPassword".toCharArray()))); + assertTrue(sac1.verifyEvidence(new PasswordGuessEvidence("userPassword".toCharArray()))); + Assert.assertTrue(sac1.authorize()); + Assert.assertTrue(sac1.exists()); + } + + @Test + public void testJaasAuthorizationIdentityRoles() throws Exception { // is in role + SecurityRealm realm = new JaasSecurityRealm("Entry1", null, null, new TestCallbackHandler()); + SecurityDomain securityDomain = SecurityDomain.builder().setDefaultRealmName("default").addRealm("default", realm).build() + .setPermissionMapper(((permissionMappable, roles) -> LoginPermission.getInstance())) + .build(); + ServerAuthenticationContext sac1 = securityDomain.createNewAuthenticationContext(); + sac1.setAuthenticationPrincipal(new NamePrincipal("javajoe")); + assertTrue(sac1.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + Assert.assertTrue(sac1.authorize()); + Assert.assertTrue(sac1.exists()); + Assert.assertTrue(sac1.getAuthorizedIdentity().getRoles().contains("Admin")); + Assert.assertTrue(sac1.getAuthorizedIdentity().getRoles().contains("User")); + Assert.assertTrue(sac1.getAuthorizedIdentity().getRoles().contains("Guest")); + Assert.assertFalse(sac1.getAuthorizedIdentity().getRoles().contains("Non_existent_role")); + } + @Test + public void testJaasRealmAttributes() throws Exception { + SecurityRealm realm = new JaasSecurityRealm("Entry1", null, null, new TestCallbackHandler()); + SecurityDomain securityDomain = SecurityDomain.builder().setDefaultRealmName("default").addRealm("default", realm).build() + .setPermissionMapper(((permissionMappable, roles) -> LoginPermission.getInstance())) + .build(); + ServerAuthenticationContext sac1 = securityDomain.createNewAuthenticationContext(); + sac1.setAuthenticationPrincipal(new NamePrincipal("javajoe")); + assertTrue(sac1.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + Assert.assertTrue(sac1.authorize()); + Assert.assertTrue(sac1.exists()); + Assert.assertTrue(sac1.getAuthorizedIdentity().getAttributes().containsKey("NamePrincipal")); + Assert.assertEquals("whoami", sac1.getAuthorizedIdentity().getAttributes().get("NamePrincipal").get(0)); + Assert.assertEquals("anonymous", sac1.getAuthorizedIdentity().getAttributes().get("AnonymousPrincipal").get(0)); + Assert.assertNotEquals("non_existent_attribute", sac1.getAuthorizedIdentity().getAttributes().get("NamePrincipal").get(0)); + Assert.assertNotEquals("whoami", sac1.getAuthorizedIdentity().getAttributes().get("NonExistentAttributeKey").get(0)); + Assert.assertEquals("Admin", sac1.getAuthorizedIdentity().getAttributes().get("Roles").get(0)); + Assert.assertEquals("User", sac1.getAuthorizedIdentity().getAttributes().get("Roles").get(1)); + Assert.assertEquals("Guest", sac1.getAuthorizedIdentity().getAttributes().get("Roles").get(2)); + } + + @Test + public void testJaasRealmWithProvidedClassLoader() throws Exception { + SecurityRealm realm = new JaasSecurityRealm("Entry1", null, TestLoginModule2.class.getClassLoader()); + SecurityDomain domainWithRewriter = SecurityDomain.builder().setDefaultRealmName("default").addRealm("default", realm).build() + .setPermissionMapper(((permissionMappable, roles) -> LoginPermission.getInstance())) + .build(); + ServerAuthenticationContext sac1 = domainWithRewriter.createNewAuthenticationContext(); + sac1.setAuthenticationPrincipal(new NamePrincipal("javajoe")); + assertTrue(sac1.verifyEvidence(new PasswordGuessEvidence("$#21pass".toCharArray()))); + Assert.assertTrue(sac1.authorize()); + Assert.assertTrue(sac1.exists()); + Assert.assertTrue(sac1.getAuthorizedIdentity().getAttributes().containsKey("NamePrincipal")); + Assert.assertEquals("whoami", sac1.getAuthorizedIdentity().getAttributes().get("NamePrincipal").get(0)); + Assert.assertEquals("anonymous", sac1.getAuthorizedIdentity().getAttributes().get("AnonymousPrincipal").get(0)); + Assert.assertNotEquals("non_existent_attribute", sac1.getAuthorizedIdentity().getAttributes().get("NamePrincipal").get(0)); + Assert.assertNotEquals("whoami", sac1.getAuthorizedIdentity().getAttributes().get("NonExistentAttributeKey").get(0)); + Assert.assertEquals("Admin", sac1.getAuthorizedIdentity().getAttributes().get("Roles").get(0)); + Assert.assertEquals("User", sac1.getAuthorizedIdentity().getAttributes().get("Roles").get(1)); + Assert.assertEquals("Guest", sac1.getAuthorizedIdentity().getAttributes().get("Roles").get(2)); } } diff --git a/tests/base/src/test/java/org/wildfly/security/auth/TestCallbackHandler.java b/tests/base/src/test/java/org/wildfly/security/auth/TestCallbackHandler.java index aecbddc69d1..2dcbe084b12 100644 --- a/tests/base/src/test/java/org/wildfly/security/auth/TestCallbackHandler.java +++ b/tests/base/src/test/java/org/wildfly/security/auth/TestCallbackHandler.java @@ -23,7 +23,6 @@ import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; -import java.io.IOException; import java.security.Principal; import org.wildfly.security.evidence.Evidence; @@ -57,7 +56,7 @@ public void setSecurityInfo(final Principal principal, final Object evidence) { } @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { if (callbacks == null) throw new IllegalArgumentException("The callbacks argument cannot be null"); diff --git a/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule.java b/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule.java index 9110e05aa98..d405162f4db 100644 --- a/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule.java +++ b/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule.java @@ -28,15 +28,15 @@ import javax.security.auth.spi.LoginModule; import java.io.IOException; import java.security.Principal; -import java.security.acl.Group; import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; +import org.wildfly.security.auth.principal.AnonymousPrincipal; import org.wildfly.security.auth.principal.NamePrincipal; +import org.wildfly.security.credential.BearerTokenCredential; +import org.wildfly.security.credential.PasswordCredential; +import org.wildfly.security.password.interfaces.ClearPassword; /** * A {@link javax.security.auth.spi.LoginModule} implementation used in the JAAS security realm tests. It uses a static @@ -46,8 +46,7 @@ */ public class TestLoginModule implements LoginModule { - private final Map usersMap = new HashMap(); - private Principal principal; + private final Map usersMap = new HashMap<>(); private Subject subject; private CallbackHandler handler; @@ -73,20 +72,22 @@ public boolean login() throws LoginException { } final String username = nameCallback.getName(); - this.principal = new NamePrincipal(username); final char[] password = passwordCallback.getPassword(); char[] storedPassword = this.usersMap.get(username); - return Arrays.equals(storedPassword, password); + return password != null && username != null && Arrays.equals(storedPassword, password); } @Override public boolean commit() throws LoginException { - this.subject.getPrincipals().add(this.principal); - // add a caller principal group for testing purposes. - final Group group = new TestGroup("CallerPrincipal"); - group.addMember(new NamePrincipal("auth-caller")); - this.subject.getPrincipals().add(group); + this.subject.getPrincipals().add(new NamePrincipal("whoami")); + this.subject.getPrincipals().add(new AnonymousPrincipal()); + this.subject.getPrincipals().add(new Roles("Admin")); + this.subject.getPrincipals().add( new Roles("User")); + this.subject.getPrincipals().add(new Roles("Guest")); + subject.getPrivateCredentials().add(new PasswordCredential(ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, "myPrivatePassword".toCharArray()))); + subject.getPrivateCredentials().add(new BearerTokenCredential("myPrivateToken")); + subject.getPublicCredentials().add(new BearerTokenCredential("myPublicToken")); return true; } @@ -101,42 +102,16 @@ public boolean logout() throws LoginException { return true; } - /** - * A {@code Group} implementation used in the tests to store the caller principal. - */ - private class TestGroup implements Group { + private static class Roles implements Principal { - private String name; - private HashSet principals; + private final String name; - public TestGroup(final String name) { + Roles(final String name) { this.name = name; - this.principals = new HashSet(); } - @Override public String getName() { return this.name; } - - @Override - public boolean addMember(Principal user) { - return this.principals.add(user); - } - - @Override - public boolean removeMember(Principal user) { - return this.principals.remove(user); - } - - @Override - public boolean isMember(Principal member) { - return this.principals.contains(member); - } - - @Override - public Enumeration members() { - return Collections.enumeration(this.principals); - } } } diff --git a/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule2.java b/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule2.java new file mode 100644 index 00000000000..4aeae5ae34a --- /dev/null +++ b/tests/base/src/test/java/org/wildfly/security/auth/TestLoginModule2.java @@ -0,0 +1,87 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.security.auth; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link LoginModule} implementation used in the JAAS security realm tests. It uses a static + * map of username -> password to determine if a login is successful or not. + * + * @author Stefan Guilhen + */ +public class TestLoginModule2 implements LoginModule { + + private final Map usersMap = new HashMap<>(); + private Subject subject; + private CallbackHandler handler; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.handler = callbackHandler; + this.usersMap.put("userFromTestModule2", "userPassword".toCharArray()); + } + + @Override + public boolean login() throws LoginException { + // obtain the incoming username and password from the callback handler + NameCallback nameCallback = new NameCallback("Username"); + PasswordCallback passwordCallback = new PasswordCallback("Password", false); + Callback[] callbacks = new Callback[]{nameCallback, passwordCallback}; + try { + this.handler.handle(callbacks); + } catch(UnsupportedCallbackException | IOException e) { + throw new LoginException("Error handling callback: " + e.getMessage()); + } + + final String username = nameCallback.getName(); + final char[] password = passwordCallback.getPassword(); + + char[] storedPassword = this.usersMap.get(username); + return Arrays.equals(storedPassword, password); + } + + @Override + public boolean commit() throws LoginException { + return true; + } + + @Override + public boolean abort() throws LoginException { + return true; + } + + @Override + public boolean logout() throws LoginException { + this.subject.getPrincipals().clear(); + return true; + } +} diff --git a/tests/base/src/test/resources/org/wildfly/security/auth/jaas-login.config b/tests/base/src/test/resources/org/wildfly/security/auth/jaas-login.config new file mode 100644 index 00000000000..06d3c52da6b --- /dev/null +++ b/tests/base/src/test/resources/org/wildfly/security/auth/jaas-login.config @@ -0,0 +1,6 @@ +Entry1 { + org.wildfly.security.auth.TestLoginModule required; +}; +Entry2 { + org.wildfly.security.auth.TestLoginModule2 required; +}; diff --git a/tests/base/src/test/resources/org/wildfly/security/auth/jaas-login2.config b/tests/base/src/test/resources/org/wildfly/security/auth/jaas-login2.config new file mode 100644 index 00000000000..721f6bafce9 --- /dev/null +++ b/tests/base/src/test/resources/org/wildfly/security/auth/jaas-login2.config @@ -0,0 +1,3 @@ +Entry1 { + org.wildfly.security.auth.TestLoginModule2 required; +}; diff --git a/tests/base/src/test/resources/org/wildfly/security/auth/login.config b/tests/base/src/test/resources/org/wildfly/security/auth/login.config deleted file mode 100644 index c1da640ecb1..00000000000 --- a/tests/base/src/test/resources/org/wildfly/security/auth/login.config +++ /dev/null @@ -1,3 +0,0 @@ -test { - org.wildfly.security.auth.TestLoginModule required; -}; \ No newline at end of file