JwtBearerGrantHandlerTest.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.grants.jwt;

import java.lang.reflect.Field;

import jakarta.ws.rs.core.MultivaluedMap;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageImpl;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureProvider;
import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureVerifier;
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer;
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration;
import org.apache.cxf.rs.security.oauth2.common.Client;
import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
import org.apache.cxf.rs.security.oauth2.grants.OAuthDataProviderImpl;
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
import org.apache.cxf.rs.security.oauth2.tokens.bearer.BearerAccessToken;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

public class JwtBearerGrantHandlerTest {

    private static final String SIGNING_KEY = "jwt-grant-test-signing-key";

    @Before
    public void setUp() throws Exception {
        setThreadLocalMessage(new MessageImpl());
    }

    @After
    public void tearDown() throws Exception {
        setThreadLocalMessage(null);
    }

    @Test
    public void testMismatchedClientAndSubjectRejected() {
        JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
        handler.setDataProvider(new SubjectAwareDataProvider());
        handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));

        Client client = new Client("fuzz-client", "secret", true);
        String assertion = createSignedAssertion("trusted-issuer", "victim-user");

        MultivaluedMap<String, String> params = new MetadataMap<>();
        params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);

        try {
            handler.createAccessToken(client, params);
            fail("OAuthServiceException expected");
        } catch (OAuthServiceException expected) {
            assertEquals("invalid_grant", expected.getMessage());
        }
    }

    @Test
    public void testMatchingClientAndSubjectAccepted() {
        JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
        handler.setDataProvider(new SubjectAwareDataProvider());
        handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));

        Client client = new Client("fuzz-client", "secret", true);
        String assertion = createSignedAssertion("trusted-issuer", client.getClientId());

        MultivaluedMap<String, String> params = new MetadataMap<>();
        params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);

        ServerAccessToken token = handler.createAccessToken(client, params);

        assertNotNull(token);
        assertNotNull(token.getSubject());
        assertEquals(client.getClientId(), token.getSubject().getLogin());
    }

    private static String createSignedAssertion(String issuer, String subject) {
        long now = System.currentTimeMillis() / 1000;
        JwtClaims claims = new JwtClaims();
        claims.setIssuer(issuer);
        claims.setSubject(subject);
        claims.setIssuedAt(now);
        claims.setExpiryTime(now + 300);

        JwsJwtCompactProducer producer = new JwsJwtCompactProducer(claims);
        return producer.signWith(new HmacJwsSignatureProvider(SIGNING_KEY, SignatureAlgorithm.HS256));
    }

    private static final class SubjectAwareDataProvider extends OAuthDataProviderImpl {
        @Override
        public ServerAccessToken createAccessToken(AccessTokenRegistration accessToken) {
            BearerAccessToken token = new BearerAccessToken(accessToken.getClient(), 3600);
            token.setSubject(accessToken.getSubject());
            token.setGrantType(accessToken.getGrantType());
            return token;
        }
    }

    private static void setThreadLocalMessage(Message message) throws Exception {
        Field f = PhaseInterceptorChain.class.getDeclaredField("CURRENT_MESSAGE");
        f.setAccessible(true);
        @SuppressWarnings("unchecked")
        ThreadLocal<Message> tl = (ThreadLocal<Message>) f.get(null);
        if (message == null) {
            tl.remove();
        } else {
            tl.set(message);
        }
    }
}