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 &mdash;
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> &mdash; 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 &mdash; 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 &sect;4 &mdash; 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}