*/
private $data;
/**
* Discovery_Document constructor.
*
* Validates that all OIDC-spec-required fields are present and that the
* server advertises support for all features this client depends on.
*
* @param array $data The full discovery document data.
*
* @throws Discovery_Failed_Exception If spec-required fields are missing or empty.
* @throws Server_Capability_Exception If the server lacks required capabilities.
*
* phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- Server_Capability_Exception is thrown by validate_server_capabilities().
*/
public function __construct( array $data ) {
$spec_invalid = [];
foreach ( self::SPEC_REQUIRED_STRING_FIELDS as $key ) {
if ( ! isset( $data[ $key ] ) || ! \is_string( $data[ $key ] ) || $data[ $key ] === '' ) {
$spec_invalid[] = $key;
}
}
foreach ( self::SPEC_REQUIRED_ARRAY_FIELDS as $key ) {
if ( ! isset( $data[ $key ] ) || ! self::is_non_empty_string_array( $data[ $key ] ) ) {
$spec_invalid[] = $key;
}
}
if ( ! empty( $spec_invalid ) ) {
throw new Discovery_Failed_Exception(
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
\sprintf( 'OIDC discovery document has missing or invalid spec-required fields: %s', \implode( ', ', $spec_invalid ) ),
);
}
$client_invalid = [];
foreach ( self::CLIENT_REQUIRED_STRING_FIELDS as $key ) {
if ( ! isset( $data[ $key ] ) || ! \is_string( $data[ $key ] ) || $data[ $key ] === '' ) {
$client_invalid[] = $key;
}
}
if ( ! empty( $client_invalid ) ) {
throw new Server_Capability_Exception(
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
\sprintf( 'Server does not provide endpoints required by this client: %s', \implode( ', ', $client_invalid ) ),
);
}
self::validate_server_capabilities( $data );
$this->data = $data;
}
/**
* Returns the issuer identifier.
*
* @return string
*/
public function get_issuer(): string {
return $this->data['issuer'];
}
/**
* Returns the authorization endpoint URL.
*
* @return string
*/
public function get_authorization_endpoint(): string {
return $this->data['authorization_endpoint'];
}
/**
* Returns the token endpoint URL.
*
* @return string
*/
public function get_token_endpoint(): string {
return $this->data['token_endpoint'];
}
/**
* Returns the dynamic client registration endpoint URL.
*
* @return string
*/
public function get_registration_endpoint(): string {
return $this->data['registration_endpoint'];
}
/**
* Returns the token revocation endpoint URL.
*
* @return string
*/
public function get_revocation_endpoint(): string {
return $this->data['revocation_endpoint'];
}
/**
* Returns the JSON Web Key Set URI.
*
* @return string
*/
public function get_jwks_uri(): string {
return $this->data['jwks_uri'];
}
/**
* Returns the full discovery document data for cache storage.
*
* Stores the complete server response so that newly required fields
* are available from cache without requiring a fresh fetch.
*
* @return array The full discovery document.
*/
public function to_array(): array {
return $this->data;
}
/**
* Validates that the server advertises support for all required features.
*
* @param array $config The parsed discovery document.
*
* @return void
*
* @throws Server_Capability_Exception If the server lacks required capabilities.
*/
private static function validate_server_capabilities( array $config ): void {
$checks = [
[
'field' => 'code_challenge_methods_supported',
'required' => 'S256',
'message' => 'Server does not support S256 PKCE code challenge method.',
],
[
'field' => 'grant_types_supported',
'required' => 'authorization_code',
'message' => 'Server does not support authorization_code grant type.',
],
[
'field' => 'grant_types_supported',
'required' => 'refresh_token',
'message' => 'Server does not support refresh_token grant type.',
],
[
'field' => 'grant_types_supported',
'required' => 'client_credentials',
'message' => 'Server does not support client_credentials grant type.',
],
[
'field' => 'token_endpoint_auth_methods_supported',
'required' => 'private_key_jwt',
'message' => 'Server does not support private_key_jwt authentication.',
],
[
'field' => 'token_endpoint_auth_signing_alg_values_supported',
'required' => 'EdDSA',
'message' => 'Server does not support EdDSA for token endpoint auth signing.',
],
[
'field' => 'dpop_signing_alg_values_supported',
'required' => 'EdDSA',
'message' => 'Server does not support EdDSA for DPoP signing.',
],
];
foreach ( $checks as $check ) {
$supported = ( $config[ $check['field'] ] ?? [] );
if ( ! \is_array( $supported ) || ! \in_array( $check['required'], $supported, true ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
throw new Server_Capability_Exception( $check['message'] );
}
}
}
/**
* Checks whether a value is a non-empty array containing only strings.
*
* phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- Validation method, accepts any type for checking.
*
* @param mixed $value The value to check.
*
* phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint
*
* @return bool True if the value is a non-empty array of strings.
*/
private static function is_non_empty_string_array( $value ): bool {
if ( ! \is_array( $value ) || $value === [] ) {
return false;
}
foreach ( $value as $item ) {
if ( ! \is_string( $item ) ) {
return false;
}
}
return true;
}
}