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.auth; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.security.*; 022import java.util.*; 023 024/** 025 * A {@link Principal} that exposes typed read-only access to a map of claims. 026 * 027 * <p> 028 * Used by token validators that produce structured assertions about the authenticated subject — 029 * e.g. JWTs (the {@code juneau-rest-server-auth-jwt} {@code JwtTokenValidator} returns a 030 * {@code ClaimsPrincipal} with the verified token's claim set) or OAuth 2.0 introspection responses. 031 * 032 * <p> 033 * The "principal name" returned by {@link #getName()} is supplied at construction time; callers can 034 * pick whichever claim is canonical for their identity model (typically {@code sub}). The full claim 035 * set is reachable via {@link #getClaims()} and per-claim typed lookup via 036 * {@link #getClaim(String, Class)}. 037 * 038 * <p> 039 * Instances are <b>immutable</b> — the claims map supplied to the constructor is defensively 040 * copied and exposed as an unmodifiable view. {@code ClaimsPrincipal} can be safely cached and 041 * shared across requests. 042 * 043 * <h5 class='section'>Type coercion contract for {@link #getClaim(String, Class)}:</h5> 044 * 045 * <ul> 046 * <li>If the underlying value is already an instance of the requested type, it is returned directly. 047 * <li>If the requested type is {@link String}, the value's {@link Object#toString()} is returned. 048 * <li>{@link Number} sub-types ({@link Integer}, {@link Long}, {@link Double}) coerce via the 049 * matching {@code Number.xxxValue()} accessor; non-numeric values yield {@link Optional#empty()}. 050 * <li>{@link Boolean} coerces from {@link Boolean} only. 051 * <li>Any other mismatch yields {@link Optional#empty()} rather than a cast exception. 052 * </ul> 053 * 054 * <p> 055 * The coercion table is intentionally conservative — the validator that built the claims map is 056 * the authoritative source of richer type-mapping. If a JWT validator decodes a claim as a 057 * {@code List<String>}, callers can read it back as {@code List.class} or pull individual 058 * elements via {@link #getClaims()} and operate on the {@link List} directly. 059 * 060 * <h5 class='section'>See Also:</h5><ul> 061 * <li class='jc'>{@link Auth} 062 * <li class='jc'>{@link TokenValidator} 063 * <li class='link'><a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc7519#section-4">RFC 7519 §4 — JWT Claims</a> 064 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerAuthGuards">AuthN Guards — Bearer / API-Key / JWT</a> 065 * </ul> 066 * 067 * @since 10.0.0 068 */ 069public class ClaimsPrincipal implements Principal { 070 071 private final String name; 072 private final Map<String,Object> claims; 073 074 /** 075 * Constructor. 076 * 077 * @param name The principal name. Must not be <jk>null</jk> or blank. 078 * @param claims The claim map. May be <jk>null</jk> (treated as an empty map). Defensively copied. 079 */ 080 public ClaimsPrincipal(String name, Map<String,Object> claims) { 081 reqnb("name", name); 082 this.name = name; 083 this.claims = claims == null 084 ? Collections.emptyMap() 085 : u(cp(claims)); 086 } 087 088 @Override /* Overridden from Principal */ 089 public String getName() { 090 return name; 091 } 092 093 /** 094 * Returns an unmodifiable view of the full claim set. 095 * 096 * @return The full claim map. Never <jk>null</jk>. 097 */ 098 public Map<String,Object> getClaims() { 099 return claims; 100 } 101 102 /** 103 * Returns the value of a claim by name with the supplied target type. 104 * 105 * <p> 106 * See the class-level Javadoc for the coercion contract. 107 * 108 * @param <T> The target type. 109 * @param claimName The claim name. Must not be <jk>null</jk>. 110 * @param type The target type. Must not be <jk>null</jk>. 111 * @return The coerced value, or {@link Optional#empty()} if the claim is absent or cannot be coerced. 112 */ 113 public <T> Optional<T> getClaim(String claimName, Class<T> type) { 114 reqnn("claimName", claimName); 115 reqnn("type", type); 116 var v = claims.get(claimName); 117 if (v == null) 118 return oe(); 119 return o(coerce(v, type)); 120 } 121 122 /** 123 * Returns <jk>true</jk> if a claim with the supplied name exists in the claim set. 124 * 125 * @param claimName The claim name. Must not be <jk>null</jk>. 126 * @return <jk>true</jk> if the claim is present, even if its value is the JSON {@code null} literal. 127 */ 128 public boolean hasClaim(String claimName) { 129 reqnn("claimName", claimName); 130 return claims.containsKey(claimName); 131 } 132 133 @SuppressWarnings({ 134 "unchecked" // Cast is safe: type parameter is verified by isInstance check before casting. 135 }) 136 private static <T> T coerce(Object v, Class<T> type) { 137 if (type.isInstance(v)) 138 return (T) v; 139 if (type == String.class) 140 return (T) v.toString(); 141 if (v instanceof Number v2) { 142 if (type == Integer.class || type == int.class) 143 return (T) Integer.valueOf(v2.intValue()); 144 if (type == Long.class || type == long.class) 145 return (T) Long.valueOf(v2.longValue()); 146 if (type == Double.class || type == double.class) 147 return (T) Double.valueOf(v2.doubleValue()); 148 if (type == Float.class || type == float.class) 149 return (T) Float.valueOf(v2.floatValue()); 150 } 151 return null; 152 } 153 154 @Override /* Overridden from Object */ 155 public String toString() { 156 return "ClaimsPrincipal[name=" + name + ", claims=" + claims.keySet() + "]"; 157 } 158}