diff --git a/auth-oauth2/config.php b/auth-oauth2/config.php index 1194792..a349923 100644 --- a/auth-oauth2/config.php +++ b/auth-oauth2/config.php @@ -54,6 +54,14 @@ public function getResourceOwnerDetailstUrl() { return $this->get('urlResourceOwnerDetails'); } + public function getLogoutUrl() { + return $this->get('urlLogout'); + } + + public function getPostLogoutRedirectUri() { + return $this->get('urlPostLogout') ?: osTicket::get_base_url(); + } + public function getAttributeFor($name, $default=null) { return $this->get("attr_$name", $default); } @@ -229,6 +237,46 @@ function getAllOptions() { 'default' => '', ) ), + 'logout_settings' => new SectionBreakField(array( + 'label' => $__('Logout Settings'), + 'hint' => $__('Optional OIDC RP-Initiated Logout configuration'), + 'visibility' => new VisibilityConstraint( + new Q(array('auth_type__eq' => 'auth')), + VisibilityConstraint::HIDDEN + ), + )), + 'urlLogout' => new TextboxField( + array( + 'label' => $__('Logout Endpoint'), + 'hint' => $__('OIDC end_session_endpoint. When set, users will be redirected here on logout to also end their IdP session (e.g. Keycloak: https://host/realms/{realm}/protocol/openid-connect/logout). Leave blank to disable IdP logout.'), + 'required' => false, + 'configuration' => array( + 'size' => 64, + 'length' => 0 + ), + 'default' => '', + 'visibility' => new VisibilityConstraint( + new Q(array('auth_type__eq' => 'auth')), + VisibilityConstraint::HIDDEN + ), + ) + ), + 'urlPostLogout' => new TextboxField( + array( + 'label' => $__('Post-Logout Redirect URI'), + 'hint' => $__('Where to redirect after the IdP logout completes. Defaults to osTicket base URL if blank. Must be registered as an allowed post-logout redirect URI in your IdP client configuration.'), + 'required' => false, + 'configuration' => array( + 'size' => 64, + 'length' => 0 + ), + 'default' => '', + 'visibility' => new VisibilityConstraint( + new Q(array('auth_type__eq' => 'auth')), + VisibilityConstraint::HIDDEN + ), + ) + ), 'scopes' => new TextboxField( array( 'label' => $__('Scopes'), diff --git a/auth-oauth2/oauth2.php b/auth-oauth2/oauth2.php index 441ab6b..f8a4538 100644 --- a/auth-oauth2/oauth2.php +++ b/auth-oauth2/oauth2.php @@ -90,6 +90,12 @@ function callback($resp, $ref=null) { && ($token=$this->provider->getToken($resp['code'])) && ($attrs=$token->getOwnerAttributes())) { $this->resetState(); + // Store OIDC logout data for use by signOut + if (($idToken = $token->getIdToken())) + $this->session['id_token'] = $idToken; + $this->session['logout_url'] = $this->config->getLogoutUrl(); + $this->session['post_logout_uri'] = $this->config->getPostLogoutRedirectUri(); + $this->session['client_id'] = $this->config->getClientId(); // Attempt to signIn the user based on returned attributes $result = $this->signIn($attrs); if ($result instanceof AuthenticatedUser) { @@ -146,7 +152,28 @@ function redirectTo($url) { } static function signOut($user) { + // Capture OIDC logout data before parent clears the session + $sess = $_SESSION[':oauth'][static::$id] ?? []; + $idToken = $sess['id_token'] ?? null; + $logoutUrl = $sess['logout_url'] ?? null; + $postLogoutUri = $sess['post_logout_uri'] ?? null; + $clientId = $sess['client_id'] ?? null; + + // Clear local osTicket session (existing behaviour) parent::signOut($user); + + // Clear OAuth session data + unset($_SESSION[':oauth'][static::$id]); + + // Perform RP-Initiated Logout if an end_session_endpoint is configured + if ($logoutUrl) { + $params = array_filter([ + 'id_token_hint' => $idToken, + 'client_id' => $clientId, + 'post_logout_redirect_uri' => $postLogoutUri, + ]); + Http::redirect($logoutUrl . '?' . http_build_query($params)); + } } abstract function signIn($attrs); @@ -652,6 +679,11 @@ protected function getUniqueName() { return $this->getJwt()->unique_name; } + public function getIdToken() { + $values = $this->getValues(); + return $values['id_token'] ?? null; + } + public function setOwner(Object $owner) { $this->owner = $owner; }