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.client.mcp.auth;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.net.*;
022import java.time.*;
023import java.util.*;
024
025/**
026 * Immutable result of a Dynamic Client Registration (SEP-837) round-trip — the client credentials and metadata an
027 * authorization server issued, keyed by the AS {@code issuer} for SEP-2352 credential binding.
028 *
029 * <p>
030 * <b>Never log {@code clientSecret} / {@code registrationAccessToken}.</b>  {@link #toString()} redacts both
031 * secret-bearing fields (mirroring the {@code OAuthToken} / {@code KeyedSecret} redaction discipline) so credential
032 * material never reaches logs via this record's auto-generated form.
033 *
034 * @param clientId The registered {@code client_id}.  Never {@code null}.
035 * @param clientSecret The issued {@code client_secret}, if the AS issued one (absent for a public/native client).
036 * @param clientSecretExpiresAt The instant the {@code client_secret} expires, if the AS returned a non-zero
037 *    {@code client_secret_expires_at}.
038 * @param registrationAccessToken The RFC 7592 registration access token, if the AS issued one.
039 * @param registrationClientUri The RFC 7592 registration client URI, if the AS returned one.
040 * @param issuer The authorization server {@code issuer} these credentials were registered with (the SEP-2352 key).
041 *    Never {@code null}.
042 * @param redirectUris The redirect URIs that were registered, in registration order.  Never {@code null}.
043 * @param applicationType The declared {@code application_type}.  Never {@code null}.
044 * @param extras Any non-standard fields the AS returned.  Read-only map.  Never {@code null}.
045 * @since 10.0.0
046 */
047public record McpClientRegistration(
048      String clientId,
049      Optional<String> clientSecret,
050      Optional<Instant> clientSecretExpiresAt,
051      Optional<String> registrationAccessToken,
052      Optional<URI> registrationClientUri,
053      URI issuer,
054      List<URI> redirectUris,
055      McpApplicationType applicationType,
056      Map<String,Object> extras) {
057
058   /** Placeholder shown for an absent secret-bearing / optional field in {@link #toString()}. */
059   private static final String NONE = "<none>";
060
061   /**
062    * Compact constructor enforcing non-null fields and defensively copying the collections.
063    */
064   public McpClientRegistration {
065      Objects.requireNonNull(clientId, "clientId");
066      Objects.requireNonNull(clientSecret, "clientSecret");
067      Objects.requireNonNull(clientSecretExpiresAt, "clientSecretExpiresAt");
068      Objects.requireNonNull(registrationAccessToken, "registrationAccessToken");
069      Objects.requireNonNull(registrationClientUri, "registrationClientUri");
070      Objects.requireNonNull(issuer, "issuer");
071      Objects.requireNonNull(applicationType, "applicationType");
072      redirectUris = redirectUris == null ? List.of() : u(cp(redirectUris));
073      extras = extras == null ? Map.of() : u(cp(extras));
074   }
075
076   /**
077    * Whether these credentials include a client secret (a confidential client) versus a public/native client that
078    * authenticates with PKCE alone.
079    *
080    * @return <jk>true</jk> if a {@code client_secret} was issued.
081    */
082   public boolean isConfidential() {
083      return clientSecret.isPresent();
084   }
085
086   /**
087    * Redacts the secret-bearing fields so credential material never reaches logs via this record's
088    * {@code toString()}.
089    *
090    * @return A redacted string form disclosing only non-secret metadata.
091    */
092   @Override
093   public String toString() {
094      return "McpClientRegistration[clientId=" + clientId
095         + ", issuer=" + issuer
096         + ", applicationType=" + applicationType
097         + ", redirectUris=" + redirectUris
098         + ", clientSecret=" + (clientSecret.isPresent() ? "<redacted>" : NONE)
099         + ", clientSecretExpiresAt=" + clientSecretExpiresAt.map(Object::toString).orElse(NONE)
100         + ", registrationAccessToken=" + (registrationAccessToken.isPresent() ? "<redacted>" : NONE)
101         + ", registrationClientUri=" + registrationClientUri.map(Object::toString).orElse(NONE)
102         + ", extras=" + extras
103         + "]";
104   }
105}