OidcClaimsValidatorTest.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.oidc.rp;

import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
import org.apache.cxf.rs.security.jose.jwk.JwkUtils;
import org.apache.cxf.rs.security.jose.jws.JwsHeaders;
import org.apache.cxf.rs.security.jose.jws.JwsSignatureVerifier;
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;

import org.junit.Test;

import static org.junit.Assert.assertNotNull;

public class OidcClaimsValidatorTest {

    private static final String SELF_ISSUED_ISSUER = "https://self-issued.me";
    private static final String CLIENT_ID = "client-id";

    // EC P-256 public key for self-issued tests
    private static final String EC_256_KEY = "{"
        + "\"kty\": \"EC\","
        + "\"x\": \"CEuRLUISufhcjrj-32N0Bvl3KPMiHH9iSw4ohN9jxrA\","
        + "\"y\": \"EldWz_iXSK3l_S7n4w_t3baxos7o9yqX0IjzG959vHc\","
        + "\"crv\": \"P-256\""
        + "}";

    // --- validateJwtClaims: self-issued path ---

    @Test
    public void testSelfIssuedValidTokenAccepted() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        validator.validateJwtClaims(buildValidSelfIssuedClaims(), CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedMissingSubjectRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        // sub intentionally absent
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedMissingAudienceRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("key-thumbprint");
        // aud intentionally absent
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedWrongAudienceRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("key-thumbprint");
        claims.setAudience("wrong-client-id");
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedExpiredTokenRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("key-thumbprint");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now - 600); // expired 10 minutes ago
        claims.setIssuedAt(now - 700);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedMissingExpiryRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("key-thumbprint");
        claims.setAudience(CLIENT_ID);
        // exp intentionally absent
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedMissingIssuedAtRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("key-thumbprint");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        // iat intentionally absent

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedMissingSubJwkRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("key-thumbprint");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);
        // sub_jwk intentionally absent

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testSelfIssuedSubJwkThumbprintMismatchRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("wrong-thumbprint");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);
        claims.setClaim("sub_jwk", EC_256_KEY);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    // --- getInitializedSignatureVerifier: self-issued branch ---

    @Test
    public void testSignatureVerifierUsesSubJwkWhenIssIsSelfIssued() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        String thumbprint = JwkUtils.getThumbprint(EC_256_KEY);

        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject(thumbprint);
        claims.setClaim("sub_jwk", EC_256_KEY);

        JwsHeaders headers = new JwsHeaders();
        headers.setSignatureAlgorithm(SignatureAlgorithm.ES256);

        JwsSignatureVerifier verifier = validator.getInitializedSignatureVerifier(new JwtToken(headers, claims));
        assertNotNull(verifier);
    }

    @Test(expected = SecurityException.class)
    public void testSignatureVerifierRejectsSubJwkWithWrongThumbprint() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setSupportSelfIssuedProvider(true);

        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject("wrong-thumbprint");
        claims.setClaim("sub_jwk", EC_256_KEY);

        JwsHeaders headers = new JwsHeaders();
        headers.setSignatureAlgorithm(SignatureAlgorithm.ES256);

        validator.getInitializedSignatureVerifier(new JwtToken(headers, claims));
    }

    // --- validateJwtClaims: normal issuer path ---

    @Test
    public void testNormalIssuerValidTokenAccepted() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        validator.validateJwtClaims(buildValidNormalIssuerClaims(), CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerMissingIssuerRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        // issuer intentionally absent
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerWrongIssuerRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://wrong-issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerMissingSubjectRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        // sub intentionally absent
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerMissingAudienceRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        // aud intentionally absent
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerWrongAudienceRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience("wrong-client-id");
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerExpiredTokenRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now - 600); // expired 10 minutes ago
        claims.setIssuedAt(now - 700);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerMissingExpiryRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        // exp intentionally absent
        claims.setIssuedAt(now - 10);

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerMissingIssuedAtRejectedWhenRequired() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        // iat intentionally absent

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test(expected = OAuthServiceException.class)
    public void testNormalIssuerWrongAzpRejected() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);
        claims.setClaim("azp", "wrong-client-id"); // azp doesn't match clientId

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    @Test
    public void testNormalIssuerValidTokenWithAzpAccepted() {
        OidcClaimsValidator validator = new OidcClaimsValidator();
        validator.setIssuerId("https://issuer.example.com");

        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);
        claims.setClaim("azp", CLIENT_ID); // azp matches clientId

        validator.validateJwtClaims(claims, CLIENT_ID, true);
    }

    // --- helper ---

    private static JwtClaims buildValidNormalIssuerClaims() {
        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer("https://issuer.example.com");
        claims.setSubject("subject-value");
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);
        return claims;
    }

    private static JwtClaims buildValidSelfIssuedClaims() {
        long now = System.currentTimeMillis() / 1000;
        String thumbprint = JwkUtils.getThumbprint(EC_256_KEY);
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(SELF_ISSUED_ISSUER);
        claims.setSubject(thumbprint);
        claims.setAudience(CLIENT_ID);
        claims.setExpiryTime(now + 300);
        claims.setIssuedAt(now - 10);
        claims.setClaim("sub_jwk", EC_256_KEY);
        return claims;
    }
}