diff --git a/auth/server/base/src/main/java/org/wildfly/security/auth/server/RequestInformationCallback.java b/auth/server/base/src/main/java/org/wildfly/security/auth/server/RequestInformationCallback.java new file mode 100644 index 00000000000..ca8a4eb43eb --- /dev/null +++ b/auth/server/base/src/main/java/org/wildfly/security/auth/server/RequestInformationCallback.java @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 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.server; + +import org.wildfly.security.auth.callback.ExtendedCallback; + +import javax.security.auth.callback.Callback; +import java.util.HashMap; + +import static org.wildfly.common.Assert.checkNotNullParam; + +/** + * A {@link javax.security.auth.callback.Callback} to inform a server authentication context about current authentication request. + * + */ +public class RequestInformationCallback implements ExtendedCallback { + + /** + * request URI of the current authentication request + */ + public static final String REQUEST_URI = "Request-URI"; + + /** + * Properties of the current authentication request + */ + private final HashMap props; + + /** + * Construct a new instance of this {@link Callback}. + * + * @param props Properties of the current authentication request + */ + public RequestInformationCallback(HashMap props) { + checkNotNullParam("props", props); + this.props = props; + } + + /** + * Get the properties of this request. + * + * @return properties of the current authentication request + */ + public HashMap getProperties() { + return this.props; + } +} 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..e20efff14f5 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 @@ -24,12 +24,14 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.net.URI; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -1162,6 +1164,14 @@ private void handleOne(final Callback[] callbacks, final int idx) throws IOExcep AuthenticationConfigurationCallback authenticationConfigurationCallback = (AuthenticationConfigurationCallback) callback; saslSkipCertificateVerification = authenticationConfigurationCallback.getSaslSkipCertificateVerification(); handleOne(callbacks, idx + 1); + } else if (callback instanceof RequestInformationCallback) { + HashMap props = ((RequestInformationCallback) callback).getProperties(); + Object requestUriObject = props.get(RequestInformationCallback.REQUEST_URI); + URI requestUri = requestUriObject instanceof URI ? (URI) requestUriObject : null; + Attributes runtimeAttributes = new MapAttributes(); + runtimeAttributes.addFirst(RequestInformationCallback.REQUEST_URI, requestUri != null ? requestUri.toString() : null); + addRuntimeAttributes(runtimeAttributes); + handleOne(callbacks, idx + 1); } else { CallbackUtil.unsupported(callback); @@ -2107,15 +2117,58 @@ void succeed() { void fail(final boolean requireInProgress) { final SecurityIdentity capturedIdentity = getSourceIdentity(); final AtomicReference stateRef = getStateRef(); - if (! stateRef.compareAndSet(this, FAILED)) { + if (!stateRef.compareAndSet(this, FAILED)) { stateRef.get().fail(requireInProgress); return; } - SecurityRealm.safeHandleRealmEvent(getRealmInfo().getSecurityRealm(), new RealmFailedAuthenticationEvent(realmIdentity, null, null)); + SecurityRealm.safeHandleRealmEvent(getRealmInfo().getSecurityRealm(), new RealmFailedAuthenticationEvent(getRealmIdentityWithRuntimeAttributes(), null, null)); SecurityDomain.safeHandleSecurityEvent(capturedIdentity.getSecurityDomain(), new SecurityAuthenticationFailedEvent(capturedIdentity, realmIdentity.getRealmIdentityPrincipal())); realmIdentity.dispose(); } + private RealmIdentity getRealmIdentityWithRuntimeAttributes() { + return new RealmIdentity() { + @Override + public Principal getRealmIdentityPrincipal() { + return realmIdentity.getRealmIdentityPrincipal(); + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException { + return realmIdentity.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec); + } + + @Override + public C getCredential(Class credentialType) throws RealmUnavailableException { + return realmIdentity.getCredential(credentialType); + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { + return realmIdentity.getEvidenceVerifySupport(evidenceType, algorithmName); + } + + @Override + public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException { + return realmIdentity.verifyEvidence(evidence); + } + + @Override + public boolean exists() throws RealmUnavailableException { + return realmIdentity.exists(); + } + + @Override + public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { + if (realmIdentity.exists()) { + return AuthorizationIdentity.basicIdentity(realmIdentity.getAuthorizationIdentity(), runtimeAttributes); + } else { + return AuthorizationIdentity.basicIdentity(AuthorizationIdentity.EMPTY, runtimeAttributes); + } + } + }; + } + @Override void setPrincipal(final Principal principal, final boolean exclusive) { if (isSamePrincipal(principal)) { diff --git a/http/util/src/main/java/org/wildfly/security/http/util/SetRequestInformationCallbackMechanismFactory.java b/http/util/src/main/java/org/wildfly/security/http/util/SetRequestInformationCallbackMechanismFactory.java new file mode 100644 index 00000000000..bdca06b2405 --- /dev/null +++ b/http/util/src/main/java/org/wildfly/security/http/util/SetRequestInformationCallbackMechanismFactory.java @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 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.http.util; + +import org.wildfly.security.auth.server.RequestInformationCallback; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; +import org.wildfly.security.http.HttpServerRequest; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.wildfly.common.Assert.checkNotNullParam; + +/** + * A wrapper {@link HttpServerAuthenticationMechanismFactory} that sets the request information using the current authentication request. + * + * @author Diana Krepinska + */ +public class SetRequestInformationCallbackMechanismFactory implements HttpServerAuthenticationMechanismFactory { + + private final HttpServerAuthenticationMechanismFactory delegate; + + /** + * Construct a wrapping mechanism factory instance. + * + * @param delegate the wrapped mechanism factory + */ + public SetRequestInformationCallbackMechanismFactory(final HttpServerAuthenticationMechanismFactory delegate) { + this.delegate = checkNotNullParam("delegate", delegate); + } + + @Override + public String[] getMechanismNames(Map properties) { + return delegate.getMechanismNames(properties); + } + + @Override + public HttpServerAuthenticationMechanism createAuthenticationMechanism(final String mechanismName, Map properties, + final CallbackHandler callbackHandler) throws HttpAuthenticationException { + final HttpServerAuthenticationMechanism mechanism = delegate.createAuthenticationMechanism(mechanismName, properties, callbackHandler); + return mechanism != null ? new HttpServerAuthenticationMechanism() { + + @Override + public String getMechanismName() { + return mechanism.getMechanismName(); + } + + @Override + public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { + try { + HashMap props = new HashMap<>(); + props.put(RequestInformationCallback.REQUEST_URI, request.getRequestURI()); + callbackHandler.handle(new Callback[]{new RequestInformationCallback(props)}); + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + + mechanism.evaluateRequest(request); + } + + @Override + public void dispose() { + mechanism.dispose(); + } + + } : null; + } +} diff --git a/tests/base/src/test/java/org/wildfly/security/auth/server/CustomRealm.java b/tests/base/src/test/java/org/wildfly/security/auth/server/CustomRealm.java new file mode 100644 index 00000000000..733b6cfc182 --- /dev/null +++ b/tests/base/src/test/java/org/wildfly/security/auth/server/CustomRealm.java @@ -0,0 +1,116 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 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.server; + +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.server.event.RealmEvent; +import org.wildfly.security.auth.server.event.RealmFailedAuthenticationEvent; +import org.wildfly.security.auth.server.event.RealmSuccessfulAuthenticationEvent; +import org.wildfly.security.credential.Credential; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.evidence.PasswordGuessEvidence; + +import java.security.Principal; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class CustomRealm implements SecurityRealm { + public boolean wasAssertionError = false; + + // this realm does not allow acquiring credentials + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, + AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException { + return SupportLevel.UNSUPPORTED; + } + + // this realm will be able to verify password evidences only + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) + throws RealmUnavailableException { + return PasswordGuessEvidence.class.isAssignableFrom(evidenceType) ? SupportLevel.POSSIBLY_SUPPORTED : SupportLevel.UNSUPPORTED; + } + + public RealmIdentity getRealmIdentity(final Principal principal) throws RealmUnavailableException { + + if ("myadmin".equals(principal.getName())) { // identity "myadmin" will have password "mypassword" + return new RealmIdentity() { + public Principal getRealmIdentityPrincipal() { + return principal; + } + + public SupportLevel getCredentialAcquireSupport(Class credentialType, + String algorithmName, AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException { + return SupportLevel.UNSUPPORTED; + } + + public C getCredential(Class credentialType) throws RealmUnavailableException { + return null; + } + + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) { + return PasswordGuessEvidence.class.isAssignableFrom(evidenceType) ? SupportLevel.SUPPORTED : SupportLevel.UNSUPPORTED; + } + + // evidence will be accepted if it is password "mypassword" + public boolean verifyEvidence(Evidence evidence) { + if (evidence instanceof PasswordGuessEvidence) { + PasswordGuessEvidence guess = (PasswordGuessEvidence) evidence; + try { + return Arrays.equals("mypassword".toCharArray(), guess.getGuess()); + + } finally { + guess.destroy(); + } + } + return false; + } + + public boolean exists() { + return true; + } + }; + } + return RealmIdentity.NON_EXISTENT; + } + + @Override + public void handleRealmEvent(RealmEvent event) { + try { + + if (event instanceof RealmSuccessfulAuthenticationEvent) { + assertEquals("10.12.14.16", ((RealmSuccessfulAuthenticationEvent) event).getAuthorizationIdentity().getRuntimeAttributes().get("Source-Address").get(0)); + assertEquals("www.test-request-uri.org", ((RealmSuccessfulAuthenticationEvent) event).getAuthorizationIdentity().getRuntimeAttributes().get("Request-URI").get(0)); + assertEquals("myadmin", ((RealmSuccessfulAuthenticationEvent) event).getRealmIdentity().getRealmIdentityPrincipal().getName()); + } + if (event instanceof RealmFailedAuthenticationEvent) { + try { + assertEquals("10.12.14.16", ((RealmFailedAuthenticationEvent) event).getRealmIdentity().getAuthorizationIdentity().getRuntimeAttributes().get("Source-Address").get(0)); + assertEquals("www.test-request-uri.org", ((RealmFailedAuthenticationEvent) event).getRealmIdentity().getAuthorizationIdentity().getRuntimeAttributes().get("Request-URI").get(0)); + assertEquals("myadmin", ((RealmFailedAuthenticationEvent) event).getRealmIdentity().getRealmIdentityPrincipal().getName()); + } catch (RealmUnavailableException e) { + fail("RealmFailedAuthenticationEvent should have runtime attributes associated"); + } + } + } catch (AssertionError e) { + wasAssertionError = true; + } + } + } + diff --git a/tests/base/src/test/java/org/wildfly/security/auth/server/RealmEventTest.java b/tests/base/src/test/java/org/wildfly/security/auth/server/RealmEventTest.java new file mode 100644 index 00000000000..8db7633742d --- /dev/null +++ b/tests/base/src/test/java/org/wildfly/security/auth/server/RealmEventTest.java @@ -0,0 +1,110 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 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.server; + +import org.junit.Assert; +import org.junit.Test; +import org.wildfly.security.auth.permission.LoginPermission; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.MapAttributes; +import org.wildfly.security.credential.PasswordCredential; +import org.wildfly.security.password.PasswordFactory; +import org.wildfly.security.password.WildFlyElytronPasswordProvider; +import org.wildfly.security.password.interfaces.ClearPassword; +import org.wildfly.security.password.spec.ClearPasswordSpec; +import org.wildfly.security.permission.PermissionVerifier; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.InvalidKeySpecException; +import java.util.HashMap; + +import static org.junit.Assert.fail; +import static org.wildfly.security.authz.RoleDecoder.KEY_SOURCE_ADDRESS; + +public class RealmEventTest { + + private static SecurityDomain usersDomain; + private static CustomRealm usersRealm; + private static final Provider provider = WildFlyElytronPasswordProvider.getInstance(); + + private ServerAuthenticationContext setupAndGetServerAuthenticationContext() throws IOException, UnsupportedCallbackException, URISyntaxException { + Security.addProvider(provider); + + usersRealm = new CustomRealm(); + SecurityDomain.Builder builder = SecurityDomain.builder(); + builder.addRealm("users", usersRealm).build(); + builder.setDefaultRealmName("users"); + builder.setPermissionMapper((permissionMappable, roles) -> PermissionVerifier.from(new LoginPermission())); + usersDomain = builder.build(); + + ServerAuthenticationContext serverAuthenticationContext = usersDomain.createNewAuthenticationContext(); + serverAuthenticationContext.addRuntimeAttributes(createRuntimeAttributesWithSourceAddress()); + + HashMap props = new HashMap<>(); + props.put(RequestInformationCallback.REQUEST_URI, new URI("www.test-request-uri.org")); + CallbackHandler callbackHandler = serverAuthenticationContext.createCallbackHandler(); + callbackHandler.handle(new Callback[]{new RequestInformationCallback(props)}); + return serverAuthenticationContext; + } + + @Test + public void testRealmSuccessfulAuthenticationEvent() throws IOException, UnsupportedCallbackException, URISyntaxException { + ServerAuthenticationContext serverAuthenticationContext = setupAndGetServerAuthenticationContext(); + try { + serverAuthenticationContext.setAuthenticationName("myadmin"); + serverAuthenticationContext.addPublicCredential(new PasswordCredential( + PasswordFactory.getInstance(ClearPassword.ALGORITHM_CLEAR).generatePassword( + new ClearPasswordSpec("mypassword".toCharArray())))); + + serverAuthenticationContext.authorize(); + } catch (RealmUnavailableException | InvalidKeySpecException | NoSuchAlgorithmException e) { + fail(); + } + serverAuthenticationContext.succeed(); + if (usersRealm.wasAssertionError) { + Assert.fail("Realm successful authentication event did not contain expected information"); + } + } + + @Test + public void testRealmFailedAuthenticationEvent() throws NoSuchAlgorithmException, IOException, UnsupportedCallbackException, InvalidKeySpecException, URISyntaxException { + ServerAuthenticationContext serverAuthenticationContext = setupAndGetServerAuthenticationContext(); + serverAuthenticationContext.setAuthenticationName("myadmin"); + serverAuthenticationContext.addPublicCredential(new PasswordCredential( + PasswordFactory.getInstance(ClearPassword.ALGORITHM_CLEAR).generatePassword( + new ClearPasswordSpec("wrongPassword".toCharArray())))); + serverAuthenticationContext.fail(); + if (usersRealm.wasAssertionError) { + Assert.fail("Realm failed authentication event did not contain expected information"); + } + } + + private Attributes createRuntimeAttributesWithSourceAddress() { + MapAttributes runtimeAttributes = new MapAttributes(); + runtimeAttributes.addFirst(KEY_SOURCE_ADDRESS, "10.12.14.16"); + return runtimeAttributes; + } +}