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 §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 * — 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 >= 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 * — 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}