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}