From 3ab6e24f0d5dc4055c9e22c5f276bffa1fdd39c1 Mon Sep 17 00:00:00 2001 From: Colm O hEigeartaigh Date: Thu, 9 Jul 2026 16:10:01 +0100 Subject: [PATCH] Fix bug with SAML clientAddress --- .../security/oauth-parent/oauth2-saml/pom.xml | 5 + .../oauth2/saml/SamlOAuthValidator.java | 2 +- .../oauth2/saml/SamlOAuthValidatorTest.java | 127 ++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java diff --git a/rt/rs/security/oauth-parent/oauth2-saml/pom.xml b/rt/rs/security/oauth-parent/oauth2-saml/pom.xml index bb0595011a3..d417395050e 100644 --- a/rt/rs/security/oauth-parent/oauth2-saml/pom.xml +++ b/rt/rs/security/oauth-parent/oauth2-saml/pom.xml @@ -47,5 +47,10 @@ cxf-rt-rs-security-xml ${project.version} + + junit + junit + test + diff --git a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java index 27ccf9ebfb7..94a15e2a564 100644 --- a/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java +++ b/rt/rs/security/oauth-parent/oauth2-saml/src/main/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidator.java @@ -58,7 +58,7 @@ public void setIssuer(String value) { } public void setClientAddress(String value) { - issuer = value; + clientAddress = value; } public void validate(Message message, SamlAssertionWrapper wrapper) { diff --git a/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java b/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java new file mode 100644 index 00000000000..a127ae9bfb2 --- /dev/null +++ b/rt/rs/security/oauth-parent/oauth2-saml/src/test/java/org/apache/cxf/rs/security/oauth2/saml/SamlOAuthValidatorTest.java @@ -0,0 +1,127 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.rs.security.oauth2.saml; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; + +import jakarta.ws.rs.NotAuthorizedException; +import org.apache.wss4j.common.saml.SAMLCallback; +import org.apache.wss4j.common.saml.SAMLUtil; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; +import org.apache.wss4j.common.saml.bean.AudienceRestrictionBean; +import org.apache.wss4j.common.saml.bean.ConditionsBean; +import org.apache.wss4j.common.saml.bean.SubjectBean; +import org.apache.wss4j.common.saml.bean.SubjectConfirmationDataBean; +import org.apache.wss4j.common.saml.bean.Version; +import org.apache.wss4j.common.saml.builder.SAML2Constants; +import org.apache.wss4j.dom.engine.WSSConfig; + +import org.junit.Test; + +public class SamlOAuthValidatorTest { + + static { + WSSConfig.init(); + } + + @Test + public void testValidateWithMatchingClientAddress() throws Exception { + String tokenServiceAddress = "https://service.example.org/token"; + String clientAddress = "192.0.2.10"; + + SamlOAuthValidator validator = new SamlOAuthValidator(); + validator.setAccessTokenServiceAddress(tokenServiceAddress); + validator.setClientAddress(clientAddress); + + SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress, clientAddress); + + // This should pass for a valid assertion but currently fails because + // setClientAddress incorrectly updates issuer instead of clientAddress. + validator.validate(null, wrapper); + } + + @Test(expected = NotAuthorizedException.class) + public void testValidateWithMismatchingClientAddress() throws Exception { + String tokenServiceAddress = "https://service.example.org/token"; + + SamlOAuthValidator validator = new SamlOAuthValidator(); + validator.setAccessTokenServiceAddress(tokenServiceAddress); + validator.setClientAddress("192.0.2.10"); + + SamlAssertionWrapper wrapper = createAssertion(tokenServiceAddress, "192.0.2.20"); + validator.validate(null, wrapper); + } + + private SamlAssertionWrapper createAssertion(String recipient, String clientAddress) throws Exception { + SubjectConfirmationDataBean confirmationData = new SubjectConfirmationDataBean(); + confirmationData.setRecipient(recipient); + confirmationData.setAddress(clientAddress); + confirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5))); + + ConditionsBean conditions = new ConditionsBean(); + conditions.setNotBefore(Instant.now().minusSeconds(30)); + conditions.setNotAfter(Instant.now().plus(Duration.ofMinutes(5))); + AudienceRestrictionBean audienceRestriction = new AudienceRestrictionBean(); + audienceRestriction.setAudienceURIs(Collections.singletonList(recipient)); + conditions.setAudienceRestrictions(Collections.singletonList(audienceRestriction)); + + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(new TestSaml2CallbackHandler(conditions, confirmationData), samlCallback); + return new SamlAssertionWrapper(samlCallback); + } + + private static class TestSaml2CallbackHandler implements CallbackHandler { + private final ConditionsBean conditions; + private final SubjectConfirmationDataBean confirmationData; + + TestSaml2CallbackHandler(ConditionsBean conditions, SubjectConfirmationDataBean confirmationData) { + this.conditions = conditions; + this.confirmationData = confirmationData; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (!(callback instanceof SAMLCallback)) { + throw new UnsupportedCallbackException(callback, "Unrecognized Callback"); + } + + SAMLCallback samlCallback = (SAMLCallback)callback; + samlCallback.setSamlVersion(Version.SAML_20); + samlCallback.setIssuer("https://issuer.example.org"); + samlCallback.setConditions(conditions); + + SubjectBean subject = new SubjectBean( + "alice", + "service.example.org", + SAML2Constants.CONF_BEARER + ); + subject.setSubjectConfirmationData(confirmationData); + samlCallback.setSubject(subject); + } + } + } +}