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.auth.oauth.flow;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.time.*;
022import java.util.*;
023
024/**
025 * Immutable record returned by every OAuth 2.0 flow helper on a successful token acquisition.
026 *
027 * <p>
028 * <b>Never log {@code accessToken} / {@code refreshToken} / {@code idToken}.</b>  {@link #toString()} redacts the
029 * secret-bearing fields so token material never reaches logs via this record's auto-generated form.
030 *
031 * @param accessToken The opaque or JWT access-token string.  Never {@code null}.
032 * @param tokenType The token type per RFC 6749 &sect;5.1 (typically {@code "Bearer"}).  Never {@code null}.
033 * @param expiresAt The instant at which the access token expires.  May be {@link Instant#MAX} when the
034 *    IdP returned no {@code expires_in}.
035 * @param refreshToken The refresh token, if the IdP issued one.
036 * @param scope The granted scopes, if the IdP returned a {@code scope} field.
037 * @param idToken The OIDC ID token (a JWT), if the flow scope included {@code openid}.  <b>Not validated</b>
038 *    &mdash; see {@link #idToken()}.
039 * @since 10.0.0
040 */
041public record OAuthToken(
042      String accessToken,
043      String tokenType,
044      Instant expiresAt,
045      Optional<String> refreshToken,
046      Optional<Set<String>> scope,
047      Optional<String> idToken) {
048
049   /**
050    * Compact constructor enforcing non-null fields.
051    */
052   public OAuthToken {
053      Objects.requireNonNull(accessToken, "accessToken");
054      Objects.requireNonNull(tokenType, "tokenType");
055      Objects.requireNonNull(expiresAt, "expiresAt");
056      Objects.requireNonNull(refreshToken, "refreshToken");
057      Objects.requireNonNull(scope, "scope");
058      Objects.requireNonNull(idToken, "idToken");
059      scope = scope.map(s -> u(cp(s)));
060   }
061
062   /**
063    * Returns whether the token is past its expiration instant relative to the supplied clock.
064    *
065    * @param now The reference instant (typically {@code Clock.instant()}).
066    * @param skew Skew tolerance (subtracted from {@link #expiresAt}).  Must be non-negative.
067    * @return <jk>true</jk> if {@code now &gt;= expiresAt - skew}.
068    */
069   public boolean isExpired(Instant now, Duration skew) {
070      Objects.requireNonNull(now, "now");
071      Objects.requireNonNull(skew, "skew");
072      if (skew.isNegative())
073         throw iaex("skew must be non-negative (was %s)", skew);
074      return !now.isBefore(expiresAt.minus(skew));
075   }
076
077   /**
078    * The OIDC ID token (a JWT), if the flow scope included {@code openid}.
079    *
080    * <p>
081    * <b>SECURITY CAVEAT:</b> this ID token is returned verbatim and is <b>NOT validated</b> by the flow helpers
082    * &mdash; its signature and its {@code iss} / {@code aud} / {@code exp} / {@code nonce} claims are not checked.
083    * Callers MUST NOT trust its claims for authentication or authorization decisions without performing full
084    * {@code IDTokenValidator}-based verification first.
085    *
086    * @return The raw ID token, or an empty {@link Optional} if the IdP issued none.
087    */
088   @Override
089   public Optional<String> idToken() {
090      return idToken;
091   }
092
093   /**
094    * Redacts the secret-bearing fields so token material never reaches logs via this record's {@code toString()}.
095    *
096    * @return A redacted string form disclosing only non-secret metadata.
097    */
098   @Override
099   public String toString() {
100      return "OAuthToken[tokenType=" + tokenType
101         + ", expiresAt=" + expiresAt
102         + ", accessToken=<redacted>"
103         + ", refreshToken=" + (refreshToken.isPresent() ? "<redacted>" : "<none>")
104         + ", idToken=" + (idToken.isPresent() ? "<redacted>" : "<none>")
105         + ", scope=" + scope.orElse(Set.of())
106         + "]";
107   }
108}