001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.mcp.v20260728; 018 019 020import java.security.*; 021import java.util.*; 022 023import org.apache.juneau.rest.server.auth.ClaimsPrincipal; 024import static org.apache.juneau.commons.utils.Shorts.*; 025 026/** 027 * <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc8707">RFC 8707</a> audience enforcement — the 028 * confused-deputy defense pairing with the MCP client's resource indicator. 029 * 030 * <p> 031 * Confirms that a validated access token was actually minted for <i>this</i> resource by checking that the token's 032 * {@code aud} or {@code resource} claim contains this server's resource identifier, rejecting a token whose audience 033 * targets a different resource. 034 * 035 * <h5 class='section'>Claims requirement:</h5> 036 * <p> 037 * Gate-level enforcement applies to a {@link ClaimsPrincipal} (as produced by {@code JwtTokenValidator} and OAuth 038 * introspection). A validator returning a bare {@link Principal} that exposes no claims cannot be checked at the gate; 039 * the {@code requireAudienceClaim} flag decides the disposition: 040 * <ul> 041 * <li><jk>true</jk> (the default) — <b>fail-closed</b>: a claims-less principal is rejected, so the 042 * confused-deputy defense is never silently a no-op. 043 * <li><jk>false</jk> — <b>opt-out</b>: a claims-less principal passes, delegating audience enforcement to the 044 * validator itself (e.g. {@code JwtTokenValidator} enforcing {@code aud} internally, or an introspection AS that 045 * legitimately omits {@code aud}). 046 * </ul> 047 * 048 * @since 10.0.0 049 */ 050public final class McpAudienceValidator { 051 052 private McpAudienceValidator() {} 053 054 /** 055 * Returns whether the principal's token audience matches the expected resource identifier. 056 * 057 * @param principal The authenticated principal. May be <jk>null</jk> (returns <jk>false</jk>). 058 * @param expectedAudience The expected audience (this server's resource identifier). Must not be <jk>null</jk> or 059 * blank. 060 * @param requireAudienceClaim Whether a principal exposing no claims is rejected (fail-closed) rather than passed 061 * (see {@link McpResourceServerConfig#isRequireAudienceClaim()}). 062 * @return <jk>true</jk> if the audience matches, or the principal exposes no claims and {@code requireAudienceClaim} 063 * is <jk>false</jk>; <jk>false</jk> if the principal is <jk>null</jk>, exposes no claims while 064 * {@code requireAudienceClaim} is <jk>true</jk>, or carries claims whose {@code aud}/{@code resource} do not include 065 * the expected audience. 066 */ 067 public static boolean matches(Principal principal, String expectedAudience, boolean requireAudienceClaim) { 068 reqnb("expectedAudience", expectedAudience); 069 if (principal == null) 070 return false; 071 if (!(principal instanceof ClaimsPrincipal cp)) 072 return !requireAudienceClaim; 073 return audienceValues(cp).contains(expectedAudience); 074 } 075 076 private static Set<String> audienceValues(ClaimsPrincipal cp) { 077 var out = new HashSet<String>(); 078 addClaim(out, cp.getClaims().get("aud")); 079 addClaim(out, cp.getClaims().get("resource")); 080 return out; 081 } 082 083 private static void addClaim(Set<String> out, Object v) { 084 if (v == null) 085 return; 086 if (v instanceof Collection<?> c) { 087 for (var o : c) 088 if (o != null) 089 out.add(o.toString()); 090 } else { 091 out.add(v.toString()); 092 } 093 } 094}