SamlOAuthValidatorTest.java

/**
 * 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);
            }
        }
    }
}