updated plugin OpenID Connect Generic version 3.11.3

This commit is contained in:
2026-06-03 21:29:12 +00:00
committed by Gitium
parent 44cba94bcb
commit 766faf9ed9
151 changed files with 8558 additions and 1213 deletions

View File

@ -91,6 +91,33 @@ class OpenID_Connect_Generic_Client {
*/
private $acr_values;
/**
* The JWKS endpoint URL for JWT signature verification.
*
* @see OpenID_Connect_Generic_Option_Settings::endpoint_jwks
*
* @var string
*/
private $endpoint_jwks;
/**
* The issuer URL for JWT validation.
*
* @see OpenID_Connect_Generic_Option_Settings::issuer
*
* @var string
*/
private $issuer;
/**
* The JWKS cache TTL in seconds.
*
* @see OpenID_Connect_Generic_Option_Settings::jwks_cache_ttl
*
* @var int
*/
private $jwks_cache_ttl;
/**
* The state time limit. States are only valid for 3 minutes.
*
@ -100,6 +127,15 @@ class OpenID_Connect_Generic_Client {
*/
private $state_time_limit = 180;
/**
* Allow HTTP requests to internal/private network endpoints.
*
* @see OpenID_Connect_Generic_Option_Settings::allow_internal_idp
*
* @var bool
*/
private $allow_internal_idp;
/**
* The logger object instance.
*
@ -110,18 +146,22 @@ class OpenID_Connect_Generic_Client {
/**
* Client constructor.
*
* @param string $client_id @see OpenID_Connect_Generic_Option_Settings::client_id for description.
* @param string $client_secret @see OpenID_Connect_Generic_Option_Settings::client_secret for description.
* @param string $scope @see OpenID_Connect_Generic_Option_Settings::scope for description.
* @param string $endpoint_login @see OpenID_Connect_Generic_Option_Settings::endpoint_login for description.
* @param string $endpoint_userinfo @see OpenID_Connect_Generic_Option_Settings::endpoint_userinfo for description.
* @param string $endpoint_token @see OpenID_Connect_Generic_Option_Settings::endpoint_token for description.
* @param string $redirect_uri @see OpenID_Connect_Generic_Option_Settings::redirect_uri for description.
* @param string $acr_values @see OpenID_Connect_Generic_Option_Settings::acr_values for description.
* @param int $state_time_limit @see OpenID_Connect_Generic_Option_Settings::state_time_limit for description.
* @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging object instance.
* @param string $client_id @see OpenID_Connect_Generic_Option_Settings::client_id for description.
* @param string $client_secret @see OpenID_Connect_Generic_Option_Settings::client_secret for description.
* @param string $scope @see OpenID_Connect_Generic_Option_Settings::scope for description.
* @param string $endpoint_login @see OpenID_Connect_Generic_Option_Settings::endpoint_login for description.
* @param string $endpoint_userinfo @see OpenID_Connect_Generic_Option_Settings::endpoint_userinfo for description.
* @param string $endpoint_token @see OpenID_Connect_Generic_Option_Settings::endpoint_token for description.
* @param string $redirect_uri @see OpenID_Connect_Generic_Option_Settings::redirect_uri for description.
* @param string $acr_values @see OpenID_Connect_Generic_Option_Settings::acr_values for description.
* @param string $endpoint_jwks @see OpenID_Connect_Generic_Option_Settings::endpoint_jwks for description.
* @param string $issuer @see OpenID_Connect_Generic_Option_Settings::issuer for description.
* @param int $jwks_cache_ttl @see OpenID_Connect_Generic_Option_Settings::jwks_cache_ttl for description.
* @param int $state_time_limit @see OpenID_Connect_Generic_Option_Settings::state_time_limit for description.
* @param bool $allow_internal_idp @see OpenID_Connect_Generic_Option_Settings::allow_internal_idp for description.
* @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging object instance.
*/
public function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri, $acr_values, $state_time_limit, $logger ) {
public function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri, $acr_values, $endpoint_jwks, $issuer, $jwks_cache_ttl, $state_time_limit, $allow_internal_idp, $logger ) {
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->scope = $scope;
@ -130,10 +170,52 @@ class OpenID_Connect_Generic_Client {
$this->endpoint_token = $endpoint_token;
$this->redirect_uri = $redirect_uri;
$this->acr_values = $acr_values;
$this->endpoint_jwks = $endpoint_jwks;
$this->issuer = $issuer;
$this->jwks_cache_ttl = $jwks_cache_ttl;
$this->state_time_limit = $state_time_limit;
$this->allow_internal_idp = $allow_internal_idp;
$this->logger = $logger;
}
/**
* Make a safe HTTP GET request with optional internal endpoint support.
*
* By default, uses wp_safe_remote_get() which blocks requests to internal/private
* networks (SSRF protection). If allow_internal_idp is enabled, uses wp_remote_get()
* to allow connections to localhost and private network identity providers.
*
* @param string $url The URL to request.
* @param array $args Optional. Request arguments.
*
* @return array|WP_Error Response array or WP_Error on failure.
*/
private function http_get( $url, $args = array() ) {
if ( $this->allow_internal_idp ) {
return wp_remote_get( $url, $args );
}
return wp_safe_remote_get( $url, $args );
}
/**
* Make a safe HTTP POST request with optional internal endpoint support.
*
* By default, uses wp_safe_remote_post() which blocks requests to internal/private
* networks (SSRF protection). If allow_internal_idp is enabled, uses wp_remote_post()
* to allow connections to localhost and private network identity providers.
*
* @param string $url The URL to request.
* @param array $args Optional. Request arguments.
*
* @return array|WP_Error Response array or WP_Error on failure.
*/
private function http_post( $url, $args = array() ) {
if ( $this->allow_internal_idp ) {
return wp_remote_post( $url, $args );
}
return wp_safe_remote_post( $url, $args );
}
/**
* Provides the configured Redirect URI supplied to the IDP.
*
@ -162,7 +244,19 @@ class OpenID_Connect_Generic_Client {
public function validate_authentication_request( $request ) {
// Look for an existing error of some kind.
if ( isset( $request['error'] ) ) {
return new WP_Error( 'unknown-error', 'An unknown error occurred.', $request );
$error_code = sanitize_text_field( $request['error'] );
$error_message = 'An unknown error occurred.';
// Use the IDP's error description if available for better diagnostics.
if ( ! empty( $request['error_description'] ) ) {
$error_message = sprintf(
'IDP error %s: %s',
$error_code,
sanitize_text_field( $request['error_description'] )
);
}
return new WP_Error( $error_code, $error_message, $request );
}
// Make sure we have a legitimate authentication code and valid state.
@ -232,7 +326,7 @@ class OpenID_Connect_Generic_Client {
// Call the server and ask for a token.
$start_time = microtime( true );
$response = wp_remote_post( $this->endpoint_token, $request );
$response = $this->http_post( $this->endpoint_token, $request );
$end_time = microtime( true );
$this->logger->log( $this->endpoint_token, 'request_authentication_token', $end_time - $start_time );
@ -265,7 +359,7 @@ class OpenID_Connect_Generic_Client {
// Call the server and ask for new tokens.
$start_time = microtime( true );
$response = wp_remote_post( $this->endpoint_token, $request );
$response = $this->http_post( $this->endpoint_token, $request );
$end_time = microtime( true );
$this->logger->log( $this->endpoint_token, 'request_new_tokens', $end_time - $start_time );
@ -341,8 +435,16 @@ class OpenID_Connect_Generic_Client {
// Attempt the request including the access token in the query string for backwards compatibility.
$start_time = microtime( true );
$response = wp_remote_get( $this->endpoint_userinfo, $request );
$end_time = microtime( true );
$response = $this->http_get( $this->endpoint_userinfo, $request );
// This endpoint can support GET or POST requests according to spec, but some IDPs only allow one.
// If the GET request failed to produce valid json, attempt a POST request.
// Spec: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest.
if ( ! is_wp_error( $response ) && json_decode( $response['body'] ) === null ) {
$response = $this->http_post( $this->endpoint_userinfo, $request );
}
$end_time = microtime( true );
$this->logger->log( $this->endpoint_userinfo, 'request_userinfo', $end_time - $start_time );
if ( is_wp_error( $response ) ) {
@ -360,8 +462,8 @@ class OpenID_Connect_Generic_Client {
* @return string
*/
public function new_state( $redirect_to ) {
// New state w/ timestamp.
$state = md5( mt_rand() . microtime( true ) );
// New state with cryptographically secure random bytes.
$state = bin2hex( random_bytes( 16 ) );
$state_value = array(
$state => array(
'redirect_to' => $redirect_to,
@ -438,7 +540,7 @@ class OpenID_Connect_Generic_Client {
}
/**
* Extract the id_token_claim from the token_response.
* Extract and validate the id_token_claim from the token_response.
*
* @param array $token_response The token response.
*
@ -450,14 +552,47 @@ class OpenID_Connect_Generic_Client {
return new WP_Error( 'no-identity-token', __( 'No identity token.', 'daggerhart-openid-connect-generic' ), $token_response );
}
// Break apart the id_token in the response for decoding.
// Check if JWKS endpoint is configured for JWT signature verification.
if ( ! empty( $this->endpoint_jwks ) ) {
// Use configured issuer if provided, otherwise derive from endpoint_login.
$issuer = ! empty( $this->issuer )
? $this->issuer
: $this->get_issuer_from_endpoint( $this->endpoint_login );
// Use JWT validator for secure signature verification.
$jwt_validator = new OpenID_Connect_Generic_JWT_Validator(
$this->endpoint_jwks,
$this->client_id,
$issuer,
$this->jwks_cache_ttl,
$this->allow_internal_idp,
$this->logger
);
// Validate JWT signature and claims.
$id_token_claim = $jwt_validator->validate_id_token( $token_response['id_token'] );
if ( is_wp_error( $id_token_claim ) ) {
$this->logger->log( $id_token_claim, 'jwt-validation-failed' );
return $id_token_claim;
}
return $id_token_claim;
}
$this->logger->log(
'SECURITY WARNING: JWKS endpoint not configured. JWT signatures are NOT being verified. This is a critical security vulnerability. Configure the JWKS endpoint immediately in Settings > OpenID Connect Client to secure authentication.',
'jwks-not-configured-insecure'
);
// Legacy JWT decoding without signature verification (INSECURE).
$tmp = explode( '.', $token_response['id_token'] );
if ( ! isset( $tmp[1] ) ) {
return new WP_Error( 'missing-identity-token', __( 'Missing identity token.', 'daggerhart-openid-connect-generic' ), $token_response );
}
// Extract the id_token's claims from the token.
// Extract the id_token's claims from the token (no signature verification).
$id_token_claim = json_decode(
base64_decode(
str_replace( // Because token is encoded in base64 URL (and not just base64).
@ -472,6 +607,38 @@ class OpenID_Connect_Generic_Client {
return $id_token_claim;
}
/**
* Extract issuer URL from endpoint URL.
*
* The issuer is typically the base URL (scheme + host + trailing slash).
*
* @param string $endpoint_url The full endpoint URL.
*
* @return string The issuer URL.
*/
public function get_issuer_from_endpoint( $endpoint_url ) {
$parsed = wp_parse_url( $endpoint_url );
if ( ! $parsed || ! isset( $parsed['scheme'] ) || ! isset( $parsed['host'] ) ) {
return $endpoint_url;
}
$issuer = $parsed['scheme'] . '://' . $parsed['host'];
// Add port if non-standard.
if ( isset( $parsed['port'] ) ) {
$default_ports = array(
'http' => 80,
'https' => 443,
);
if ( ! isset( $default_ports[ $parsed['scheme'] ] ) || $parsed['port'] != $default_ports[ $parsed['scheme'] ] ) {
$issuer .= ':' . $parsed['port'];
}
}
return $issuer;
}
/**
* Ensure the id_token_claim contains the required values.
*
@ -484,11 +651,71 @@ class OpenID_Connect_Generic_Client {
return new WP_Error( 'bad-id-token-claim', __( 'Bad ID token claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate the identification data and it's value.
// Validate the identification data and its value.
if ( ! isset( $id_token_claim['sub'] ) || empty( $id_token_claim['sub'] ) ) {
return new WP_Error( 'no-subject-identity', __( 'No subject identity.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate expiration claim.
if ( ! isset( $id_token_claim['exp'] ) ) {
return new WP_Error( 'missing-exp', __( 'Token missing expiration claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
if ( time() >= $id_token_claim['exp'] ) {
return new WP_Error( 'token-expired', __( 'Token has expired.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate issued at claim.
if ( ! isset( $id_token_claim['iat'] ) ) {
return new WP_Error( 'missing-iat', __( 'Token missing issued at claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate audience claim matches client_id (can be string or array).
if ( ! isset( $id_token_claim['aud'] ) ) {
return new WP_Error( 'missing-aud', __( 'Token missing audience claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
$aud = $id_token_claim['aud'];
$audience_valid = false;
if ( is_array( $aud ) ) {
$audience_valid = in_array( $this->client_id, $aud, true );
} elseif ( is_string( $aud ) ) {
$audience_valid = ( $aud === $this->client_id );
}
if ( ! $audience_valid ) {
return new WP_Error( 'invalid-aud', __( 'Token audience does not match client.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate issuer claim if configured or endpoint_login is available.
$expected_issuer = ! empty( $this->issuer ) ?
$this->issuer :
( ! empty( $this->endpoint_login ) ? $this->get_issuer_from_endpoint( $this->endpoint_login ) : '' );
if ( ! empty( $expected_issuer ) ) {
if ( ! isset( $id_token_claim['iss'] ) ) {
return new WP_Error( 'missing-iss', __( 'Token missing issuer claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
if ( rtrim( $id_token_claim['iss'], '/' ) !== rtrim( $expected_issuer, '/' ) ) {
$this->logger->log(
sprintf(
'Issuer mismatch - Expected: "%s", Received: "%s". Configure the correct issuer in Settings > OpenID Connect Client > Issuer field, or via the OIDC_ISSUER constant.',
$expected_issuer,
$id_token_claim['iss']
),
'issuer-mismatch'
);
return new WP_Error(
'invalid-iss',
sprintf(
__( 'Token issuer does not match expected issuer.', 'daggerhart-openid-connect-generic' ),
),
$id_token_claim
);
}
}
// Validate acr values when the option is set in the configuration.
if ( ! empty( $this->acr_values ) && isset( $id_token_claim['acr'] ) ) {
if ( $this->acr_values != $id_token_claim['acr'] ) {