Skip to content

Commit 3b09bf8

Browse files
authored
Add configurable issuer setting for JWT validation (#639)
Fixes issuer mismatch errors when the IDP's issuer differs from the login endpoint base URL. The issuer can be configured via the Issuer setting, OIDC_ISSUER constant, or auto-populated from discovery documents. Defaults to deriving from endpoint_login for backward compatibility.
1 parent 1810fff commit 3b09bf8

6 files changed

Lines changed: 71 additions & 8 deletions

includes/openid-connect-generic-client-wrapper.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -914,11 +914,16 @@ private function get_claim( $claimname, $userinfo, &$claimvalue ) {
914914

915915
// Check if JWKS endpoint is configured for JWT signature verification.
916916
if ( ! empty( $this->settings->endpoint_jwks ) ) {
917+
// Use configured issuer if provided, otherwise derive from endpoint_login.
918+
$issuer = ! empty( $this->settings->issuer ) ?
919+
$this->settings->issuer :
920+
( ! empty( $this->settings->endpoint_login ) ? $this->client->get_issuer_from_endpoint( $this->settings->endpoint_login ) : '' );
921+
917922
// Use JWT validator for secure signature verification.
918923
$jwt_validator = new OpenID_Connect_Generic_JWT_Validator(
919924
$this->settings->endpoint_jwks,
920925
$this->settings->client_id,
921-
$this->client->get_issuer_from_endpoint( $this->settings->endpoint_login ),
926+
$issuer,
922927
$this->settings->jwks_cache_ttl,
923928
$this->settings->allow_internal_idp,
924929
$this->logger

includes/openid-connect-generic-client.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ class OpenID_Connect_Generic_Client {
100100
*/
101101
private $endpoint_jwks;
102102

103+
/**
104+
* The issuer URL for JWT validation.
105+
*
106+
* @see OpenID_Connect_Generic_Option_Settings::issuer
107+
*
108+
* @var string
109+
*/
110+
private $issuer;
111+
103112
/**
104113
* The JWKS cache TTL in seconds.
105114
*
@@ -146,12 +155,13 @@ class OpenID_Connect_Generic_Client {
146155
* @param string $redirect_uri @see OpenID_Connect_Generic_Option_Settings::redirect_uri for description.
147156
* @param string $acr_values @see OpenID_Connect_Generic_Option_Settings::acr_values for description.
148157
* @param string $endpoint_jwks @see OpenID_Connect_Generic_Option_Settings::endpoint_jwks for description.
158+
* @param string $issuer @see OpenID_Connect_Generic_Option_Settings::issuer for description.
149159
* @param int $jwks_cache_ttl @see OpenID_Connect_Generic_Option_Settings::jwks_cache_ttl for description.
150160
* @param int $state_time_limit @see OpenID_Connect_Generic_Option_Settings::state_time_limit for description.
151161
* @param bool $allow_internal_idp @see OpenID_Connect_Generic_Option_Settings::allow_internal_idp for description.
152162
* @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging object instance.
153163
*/
154-
public function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri, $acr_values, $endpoint_jwks, $jwks_cache_ttl, $state_time_limit, $allow_internal_idp, $logger ) {
164+
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 ) {
155165
$this->client_id = $client_id;
156166
$this->client_secret = $client_secret;
157167
$this->scope = $scope;
@@ -161,6 +171,7 @@ public function __construct( $client_id, $client_secret, $scope, $endpoint_login
161171
$this->redirect_uri = $redirect_uri;
162172
$this->acr_values = $acr_values;
163173
$this->endpoint_jwks = $endpoint_jwks;
174+
$this->issuer = $issuer;
164175
$this->jwks_cache_ttl = $jwks_cache_ttl;
165176
$this->state_time_limit = $state_time_limit;
166177
$this->allow_internal_idp = $allow_internal_idp;
@@ -543,11 +554,16 @@ public function get_id_token_claim( $token_response ) {
543554

544555
// Check if JWKS endpoint is configured for JWT signature verification.
545556
if ( ! empty( $this->endpoint_jwks ) ) {
557+
// Use configured issuer if provided, otherwise derive from endpoint_login.
558+
$issuer = ! empty( $this->issuer )
559+
? $this->issuer
560+
: $this->get_issuer_from_endpoint( $this->endpoint_login );
561+
546562
// Use JWT validator for secure signature verification.
547563
$jwt_validator = new OpenID_Connect_Generic_JWT_Validator(
548564
$this->endpoint_jwks,
549565
$this->client_id,
550-
$this->get_issuer_from_endpoint( $this->endpoint_login ),
566+
$issuer,
551567
$this->jwks_cache_ttl,
552568
$this->allow_internal_idp,
553569
$this->logger
@@ -671,15 +687,16 @@ public function validate_id_token_claim( $id_token_claim ) {
671687
return new WP_Error( 'invalid-aud', __( 'Token audience does not match client.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
672688
}
673689

674-
// Validate issuer claim if endpoint_login is configured.
675-
if ( ! empty( $this->endpoint_login ) ) {
690+
// Validate issuer claim if configured or endpoint_login is available.
691+
$expected_issuer = ! empty( $this->issuer ) ?
692+
$this->issuer :
693+
( ! empty( $this->endpoint_login ) ? $this->get_issuer_from_endpoint( $this->endpoint_login ) : '' );
694+
695+
if ( ! empty( $expected_issuer ) ) {
676696
if ( ! isset( $id_token_claim['iss'] ) ) {
677697
return new WP_Error( 'missing-iss', __( 'Token missing issuer claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
678698
}
679699

680-
// Extract expected issuer from endpoint_login (base URL).
681-
$expected_issuer = $this->get_issuer_from_endpoint( $this->endpoint_login );
682-
683700
if ( rtrim( $id_token_claim['iss'], '/' ) !== rtrim( $expected_issuer, '/' ) ) {
684701
return new WP_Error(
685702
'invalid-iss',

includes/openid-connect-generic-option-settings.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* @property string $endpoint_token The IDP token validation endpoint URL.
3636
* @property string $endpoint_end_session The IDP logout endpoint URL.
3737
* @property string $endpoint_jwks The IDP JWKS endpoint URL for JWT signature verification.
38+
* @property string $issuer The IDP issuer URL for JWT validation (optional - derived from endpoint_login if not set).
3839
* @property int $jwks_cache_ttl The JWKS cache TTL in seconds.
3940
* @property string $acr_values The Authentication contract as defined on the IDP.
4041
*
@@ -98,6 +99,7 @@ class OpenID_Connect_Generic_Option_Settings {
9899
'endpoint_token' => 'OIDC_ENDPOINT_TOKEN_URL',
99100
'endpoint_userinfo' => 'OIDC_ENDPOINT_USERINFO_URL',
100101
'endpoint_jwks' => 'OIDC_ENDPOINT_JWKS_URL',
102+
'issuer' => 'OIDC_ISSUER',
101103
'login_type' => 'OIDC_LOGIN_TYPE',
102104
'scope' => 'OIDC_CLIENT_SCOPE',
103105
'create_if_does_not_exist' => 'OIDC_CREATE_IF_DOES_NOT_EXIST',

includes/openid-connect-generic-settings-page.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ private function get_settings_fields() {
308308
'disabled' => defined( 'OIDC_ENDPOINT_JWKS_URL' ),
309309
'section' => 'client_settings',
310310
),
311+
'issuer' => array(
312+
'title' => __( 'Issuer', 'daggerhart-openid-connect-generic' ),
313+
'description' => __( 'Identity provider issuer URL for JWT validation. If not set, the issuer will be automatically derived from the Login Endpoint URL. Only configure this if your IDP uses a different issuer than the base URL of the login endpoint.', 'daggerhart-openid-connect-generic' ),
314+
'example' => 'https://example.com',
315+
'type' => 'text',
316+
'disabled' => defined( 'OIDC_ISSUER' ),
317+
'section' => 'client_settings',
318+
),
311319
'jwks_cache_ttl' => array(
312320
'title' => __( 'JWKS Cache TTL (seconds)', 'daggerhart-openid-connect-generic' ),
313321
'description' => __( 'Time in seconds to cache JWKS keys. Default: 3600 (1 hour)', 'daggerhart-openid-connect-generic' ),
@@ -776,6 +784,7 @@ private function populate_settings_from_discovery( $discovery ) {
776784
'token_endpoint' => 'endpoint_token',
777785
'userinfo_endpoint' => 'endpoint_userinfo',
778786
'jwks_uri' => 'endpoint_jwks',
787+
'issuer' => 'issuer',
779788
'end_session_endpoint' => 'endpoint_end_session',
780789
);
781790

openid-connect-generic.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public function init() {
159159
$this->get_redirect_uri( $this->settings ),
160160
$this->settings->acr_values,
161161
$this->settings->endpoint_jwks,
162+
$this->settings->issuer ?? '',
162163
$this->settings->jwks_cache_ttl,
163164
$this->get_state_time_limit( $this->settings ),
164165
$this->settings->allow_internal_idp,

tests/phpunit/includes/openid-connect-generic-client_test.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,33 @@ public function test_validate_id_token_claim_with_audience_array() {
265265
$this->assertTrue( $result );
266266
}
267267

268+
/**
269+
* Test validate_id_token_claim uses configured issuer over derived issuer.
270+
*
271+
* @group ClientTests
272+
* @group IssuerValidation
273+
*/
274+
public function test_validate_id_token_claim_with_explicit_issuer() {
275+
$client = $this->create_client(
276+
array(
277+
'endpoint_login' => 'https://login.example.com/authorize',
278+
'issuer' => 'https://issuer.example.com', // Explicit issuer differs from login endpoint.
279+
'client_id' => 'test_client',
280+
)
281+
);
282+
283+
$id_token_claim = array(
284+
'sub' => 'user123',
285+
'iss' => 'https://issuer.example.com', // Matches configured issuer, not derived from endpoint_login.
286+
'aud' => 'test_client',
287+
'exp' => time() + 3600,
288+
'iat' => time(),
289+
);
290+
291+
$result = $client->validate_id_token_claim( $id_token_claim );
292+
$this->assertTrue( $result );
293+
}
294+
268295
/**
269296
* Helper to create client instance for testing.
270297
*
@@ -283,6 +310,7 @@ private function create_client( $settings = array() ) {
283310
'redirect_uri' => 'https://example.com/callback',
284311
'acr_values' => '',
285312
'endpoint_jwks' => '',
313+
'issuer' => '',
286314
'jwks_cache_ttl' => 3600,
287315
'state_time_limit' => 180,
288316
'allow_internal_idp' => false,
@@ -302,6 +330,7 @@ private function create_client( $settings = array() ) {
302330
$merged['redirect_uri'],
303331
$merged['acr_values'],
304332
$merged['endpoint_jwks'],
333+
$merged['issuer'],
305334
$merged['jwks_cache_ttl'],
306335
$merged['state_time_limit'],
307336
$merged['allow_internal_idp'],

0 commit comments

Comments
 (0)