diff --git a/plugins/kolab_sso/README b/plugins/kolab_sso/README index bad65b82..0e7d8309 100644 --- a/plugins/kolab_sso/README +++ b/plugins/kolab_sso/README @@ -1,79 +1,80 @@ Single Sign On Authentication for Kolab --------------------------------------- This plugin adds possibility to authenticate users via external authentication -services. Currently the only supported method of authentication is OpenID Connect. +services. Currently it supports various authentication methods based on +OAuth2 and OpenID Connect (and requires JWT token use). Because Kolab backends do not support token authentication it is required to use master user (sasl proxy) authentication where possible and service user credentials in other places. See IMPLEMENTATION NOTES below for details. Plugin requires libkolab plugin and HTTP/Request2 library. Plugin contains BSD-licensed https://github.com/firebase/php-jwt (v5.0.0) library. Supported algorithms: - HS256, HS385, HS512 (PHP hash extension required) - RS256, RS384, RS512 (PHP openssl extension required). INSTALLATION ------------ Enable plugin in Roundcube's main configuration file. Make sure it is first on the list of plugins, especially before any authentication plugin, e.g. kolab_auth. Default return-URL for Auth Provider is https://host.roundcube?_task=login&_action=sso, but not all providers support query params. To workaround this limitation you have to define an alias URI or redirect. For example: RewriteEngine On RewriteCond %{REQUEST_URI} ^/roundcubemail RewriteRule "^sso" "/roundcubemail/?_task=login&_action=sso" [L,QSA] For the above "alias" plugin configuration should include 'response_uri' = '/sso' and on the provider side configured URI will be https://host/roundcubemail/sso. IMPLEMENTATION NOTES -------------------- Specification: https://openid.net/specs/openid-connect-core-1_0.html Limitations: - OpenIDC Discovery and Dynamic Client Registration is not supported, - Authentication with Authorization Code Flow is the only one supported, - required grant types: authorization_code, refresh_token (for token refresh), - required claims: id_token, - required scopes: email, openid, offline_access (for token refresh), - endpoints used: token, authorization, - supported signature algorithms: HS256, HS385, HS512, RS256, RS384, RS512. - supported token endpoint auth methods: client_secret_basic, client_secret_post. [IMAP] Because Kolab backend do not support token authentication it is required to use master user (sasl proxy) authentication, i.e. you have to put master user credentials in plugin's config. [SMTP] For the same reason and also because the same master user does not work in Postfix, you have to specify SMTP connection parameters/user+password. [LDAP] Global addressbook (read-only) requires LDAP user/password. 'user_specific' option does not work as well as proxy authentication. [Freebusy] Authentication into kolab-freebusy service is not yet implemented. A solution for now is to add Roundcube host(s) into trustednetworks.allow option in kolab-freebusy service config. [Chwala] Authentication to Chwala will work if it uses the same (session) database as Roundcube. Additionally set $config['fileapi_plugins'] = array('kolab_sso', 'kolab_auth', 'kolab_folders'); Authenticating to Seafile/WebDAV storage is not supported (until it's custom user-defined storage). TODO ---- - kolab_delegation (LDAP auth for write operation) - Chwala+Seafile - Chwala+WebDAV - Freebusy auth - Mattermost auth - Extended token validation diff --git a/plugins/kolab_sso/drivers/openidc.php b/plugins/kolab_sso/drivers/oauth2.php similarity index 81% copy from plugins/kolab_sso/drivers/openidc.php copy to plugins/kolab_sso/drivers/oauth2.php index c12d27da..6792d35f 100644 --- a/plugins/kolab_sso/drivers/openidc.php +++ b/plugins/kolab_sso/drivers/oauth2.php @@ -1,413 +1,436 @@ * - * Copyright (C) 2018, Kolab Systems AG + * Copyright (C) 2018-2019, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -class kolab_sso_openidc +class kolab_sso_oauth2 { - protected $id = 'openidc'; - protected $config = array(); protected $plugin; + protected $id = 'oauth2'; + protected $config = array(); + protected $params = array(); + protected $defaults = array( + 'scope' => 'email', + 'token_type' => 'access_token', + 'user_field' => 'email', + 'validate_items' => array('aud'), + ); + /** + * Object constructor + * + * @param rcube_plugin $plugin kolab_sso plugin object + * @param array $config Driver configuration + */ public function __construct($plugin, $config) { $this->plugin = $plugin; $this->config = $config; $this->plugin->require_plugin('libkolab'); } /** * Authentication request (redirect to SSO service) */ public function authorize() { $params = array( 'response_type' => 'code', - 'scope' => 'openid email offline_access', - 'client_id' => $this->config['client_id'], + 'scope' => $this->get_param('scope'), + 'client_id' => $this->get_param('client_id'), 'state' => $this->plugin->rc->get_request_token(), 'redirect_uri' => $this->redirect_uri(), ); - // TODO: Other params by config: display, prompt, max_age, + // Add extra request parameters (don't overwrite params set above) + if (!empty($this->config['extra_params'])) { + $params = array_merge((array) $this->config['extra_params'], $params); + } $url = $this->config['auth_uri'] ?: (unslashify($this->config['uri']) . '/authorize'); $url .= '?' . http_build_query($params); $this->plugin->debug("[{$this->id}][authorize] Redirecting to $url"); header("Location: $url"); die; } /** * Authorization response validation */ public function response() { $this->plugin->debug("[{$this->id}][authorize] Response: " . $_SERVER['REQUEST_URI']); $this->error = $this->error_message( rcube_utils::get_input_value('error', rcube_utils::INPUT_GET), rcube_utils::get_input_value('error_description', rcube_utils::INPUT_GET), rcube_utils::get_input_value('error_uri', rcube_utils::INPUT_GET) ); if ($this->error) { return; } $state = rcube_utils::get_input_value('state', rcube_utils::INPUT_GET); $code = rcube_utils::get_input_value('code', rcube_utils::INPUT_GET); if (!$state) { $this->plugin->debug("[{$this->id}][response] State missing"); $this->error = $this->plugin->gettext('errorinvalidresponse'); return; } if ($state != $this->plugin->rc->get_request_token()) { $this->plugin->debug("[{$this->id}][response] Invalid response state"); $this->error = $this->plugin->gettext('errorinvalidresponse'); return; } if (!$code) { $this->plugin->debug("[{$this->id}][response] Code missing"); $this->error = $this->plugin->gettext('errorinvalidresponse'); return; } return $this->request_token($code); } /** * Error message for the response handler */ public function response_error() { if ($this->error) { return $this->plugin->rc->gettext('loginfailed') . ' ' . $this->error; } } /** * Existing session validation */ public function validate_session($session) { $this->plugin->debug("[{$this->id}][validate] Session: " . json_encode($session)); // Sanity checks if (empty($session) || empty($session['code']) || empty($session['validto']) || empty($session['email'])) { $this->plugin->debug("[{$this->id}][validate] Session invalid"); return; } // Check expiration time $now = new DateTime('now', new DateTimezone('UTC')); $validto = new DateTime($session['validto'], new DateTimezone('UTC')); // Don't refresh often than TTL/2 $validto->sub(new DateInterval(sprintf('PT%dS', $session['ttl']/2))); if ($now < $validto) { $this->plugin->debug("[{$this->id}][validate] Token valid, skipping refresh"); return $session; } // No refresh_token, not possible to refresh if (empty($session['refresh_token'])) { $this->plugin->debug("[{$this->id}][validate] Session cannot be refreshed"); return; } // Renew tokens $info = $this->request_token($session['code'], $session['refresh_token']); if (!empty($info)) { // Make sure the email didn't change if (!empty($info['email']) && $info['email'] != $session['email']) { $this->plugin->debug("[{$this->id}][validate] Email address change"); return; } $session = array_merge($session, $info); $this->plugin->debug("[{$this->id}][validate] Session valid: " . json_encode($session)); return $session; } } /** * Authentication Token request (or token refresh) */ protected function request_token($code, $refresh_token = null) { $mode = $refresh_token ? 'token-refresh' : 'token'; $url = $this->config['token_uri'] ?: ($this->config['uri'] . '/token'); $params = array( - 'client_id' => $this->config['client_id'], - 'client_secret' => $this->config['client_secret'], + 'client_id' => $this->get_param('client_id'), + 'client_secret' => $this->get_param('client_secret'), 'grant_type' => $refresh_token ? 'refresh_token' : 'authorization_code', ); if ($refresh_token) { $params['refresh_token'] = $refresh_token; - $params['scope'] = 'openid email offline_access'; + $params['scope'] = $this->get_param('scope'); } else { $params['code'] = $code; $params['redirect_uri'] = $this->redirect_uri(); } + // Add extra request parameters (don't overwrite params set above) + if (!empty($this->config['extra_params'])) { + $params = array_merge((array) $this->config['extra_params'], $params); + } + $post = http_build_query($params); $this->plugin->debug("[{$this->id}][$mode] Requesting POST $url?$post"); try { // TODO: JWT-based methods of client authentication // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.9 $request = $this->get_request($url, 'POST'); - $request->setAuth($this->config['client_id'], $this->config['client_secret']); + $request->setAuth($params['client_id'], $params['client_secret']); $request->setBody($post); $response = $request->send(); $status = $response->getStatus(); $response = $response->getBody(); $this->plugin->debug("[{$this->id}][$mode] Response: $response"); $response = @json_decode($response, true); if ($status != 200 || !is_array($response) || !empty($response['error'])) { $err = $this->error_text(is_array($response) ? $response['error'] : null); throw new Exception("OpenIDC request failed with error: $err"); } } catch (Exception $e) { $this->error = $this->plugin->gettext('errorunknown'); rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, false); return; } // Example response: { // "access_token":"ACCESS_TOKEN", // "token_type":"bearer", // "expires_in":2592000, // "refresh_token":"REFRESH_TOKEN", // "scope":"read", // "uid":100101, // "info":{"name":"Mark E. Mark","email":"mark@example.com"} // } if (empty($response['access_token']) || empty($response['token_type']) || strtolower($response['token_type']) != 'bearer' ) { $this->error = $this->plugin->gettext('errorinvalidresponse'); $this->plugin->debug("[{$this->id}][$mode] Error: Invalid or unsupported response"); return; } $ttl = $response['expires_in'] ?: 600; $validto = new DateTime(sprintf('+%d seconds', $ttl), new DateTimezone('UTC')); + $token = $response[$this->get_param('token_type')]; $result = array( 'code' => $code, 'access_token' => $response['access_token'], // 'token_type' => $response['token_type'], 'validto' => $validto->format(DateTime::ISO8601), 'ttl' => $ttl, ); if (!empty($response['refresh_token'])) { $result['refresh_token'] = $response['refresh_token']; } - if (!empty($response['id_token'])) { + if (!empty($token)) { try { - $key = $this->config['client_secret']; + $key = $params['client_secret']; if (!empty($this->config['pubkey'])) { $pubkey = trim(preg_replace('/\r?\n[\s\t]+/', "\n", $this->config['pubkey'])); if (strpos($pubkey, '-----') !== 0) { $pubkey = "-----BEGIN PUBLIC KEY-----\n" . trim(chunk_split($pubkey, 64, "\n")) . "\n-----END PUBLIC KEY-----"; } if ($keyid = openssl_pkey_get_public($pubkey)) { $key = $keyid; } else { throw new Exception("Failed to extract public key"); } } $jwt = new Firebase\JWT\JWT; $jwt::$leeway = 60; - $payload = $jwt->decode($response['id_token'], $key, array_keys(Firebase\JWT\JWT::$supported_algs)); - $email = $this->config['debug_email'] ?: $payload->email; - - if (empty($email)) { - throw new Exception("No email address in JWT token"); - } + $payload = $jwt->decode($token, $key, array_keys(Firebase\JWT\JWT::$supported_algs)); - if (!in_array($this->config['client_id'], (array) $payload->aud)) { - throw new Exception("Token audience does not match"); - } - - // More extended token validation - // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - $result['email'] = $email; + $result['email'] = $this->validate_token_payload($payload); } catch (Exception $e) { $this->error = $this->plugin->gettext('errorinvalidtoken'); rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), true, false); return; } } return $result; } + /** + * Validates JWT token payload and returns user/email + */ + protected function validate_token_payload($payload) + { + $items = $this->get_maram('validate_items'); + $email = $this->config['debug_email'] ?: $payload->{$this->get_param('user_field')}; + + if (empty($email)) { + throw new Exception("No email address in JWT token"); + } + + foreach ((array) $items as $item_name) { + // More extended token validation + // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + switch (strtolower($item_name)) { + case 'aud': + if (!in_array($this->get_param('client_id'), (array) $payload->aud)) { + throw new Exception("Token audience does not match"); + } + break; + } + } + + return $email; + } + /** * The URL to use when redirecting the user from SSO back to Roundcube */ protected function redirect_uri() { // response_uri is useful when the Provider does not allow // URIs with parameters. In such case set response_uri = '/sso' // and define a redirect in http server, example for Apache: // RewriteRule "^sso" "/roundcubemail/?_task=login&_action=sso" [L,QSA] $redirect_params = empty($this->config['response_uri']) ? array('_action' => 'sso') : array(); $url = $this->plugin->rc->url($redirect_params, false, true); if (!empty($this->config['response_uri'])) { $url = unslashify(preg_replace('/\?.*$/', '', $url)) . '/' . ltrim($this->config['response_uri'], '/'); } return $url; } /** * Get HTTP/Request2 object */ protected function get_request($url, $type) { $config = array_intersect_key($this->config, array_flip(array( 'ssl_verify_peer', 'ssl_verify_host', 'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase', 'follow_redirects', ))); return libkolab::http_request($url, $type, $config); } /** * Returns (localized) user-friendly error message */ protected function error_message($error, $description, $uri) { if (empty($error)) { return; } $msg = $this->error_text($error); rcube::raise_error(array( 'message' => "[SSO] $msg." . ($description ? " $description" : '') . ($uri ? " ($uri)" : '') ), true, false); $label = 'error' . str_replace('_', '', $error); if (!$this->plugin->rc->text_exists($label, 'kolab_sso')) { $label = 'errorunknown'; } return $this->plugin->gettext($label); } /** * Returns error text for specified OpenIDC error code */ protected function error_text($error) { switch ($error) { - // OAuth2 codes case 'invalid_request': return "Request malformed"; case 'unauthorized_client': return "The client is not authorized"; case 'invalid_client': return "Client authentication failed"; case 'access_denied': return "Request denied"; case 'unsupported_response_type': return "Unsupported response type"; case 'invalid_grant': return "Invalid authorization grant"; case 'unsupported_grant_type': return "Unsupported authorization grant"; case 'invalid_scope': return "Invalid scope"; case 'server_error': return "Server error"; case 'temporarily_unavailable': return "Service temporarily unavailable"; - // OpenIDC codes - case 'interaction_required': - return "End-User interaction required"; - case 'login_required': - return "End-User authentication required"; - case 'account_selection_required': - return "End-User account selection required"; - case 'consent_required': - return "End-User consent required"; - case 'invalid_request_uri': - return "Invalid request_uri"; - case 'invalid_request_object': - return "Invalid Request Object"; - case 'request_not_supported': - return "Request not supported"; - case 'request_uri_not_supported': - return "request_uri param not supported"; - case 'registration_not_supported': - return "Registration not supported"; } return "Unknown error"; } + + /** + * Returns (hardcoded/configured/default) value of a configuration param + */ + protected function get_param($name) + { + return $this->params[$name] ?: ($this->config[$name] ?: $this->defaults[$name]); + } } diff --git a/plugins/kolab_sso/drivers/openidc.php b/plugins/kolab_sso/drivers/openidc.php index c12d27da..bb5e9bbc 100644 --- a/plugins/kolab_sso/drivers/openidc.php +++ b/plugins/kolab_sso/drivers/openidc.php @@ -1,413 +1,67 @@ * * Copyright (C) 2018, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -class kolab_sso_openidc -{ - protected $id = 'openidc'; - protected $config = array(); - protected $plugin; - - - public function __construct($plugin, $config) - { - $this->plugin = $plugin; - $this->config = $config; - - $this->plugin->require_plugin('libkolab'); - } - - /** - * Authentication request (redirect to SSO service) - */ - public function authorize() - { - $params = array( - 'response_type' => 'code', - 'scope' => 'openid email offline_access', - 'client_id' => $this->config['client_id'], - 'state' => $this->plugin->rc->get_request_token(), - 'redirect_uri' => $this->redirect_uri(), - ); - - // TODO: Other params by config: display, prompt, max_age, - - $url = $this->config['auth_uri'] ?: (unslashify($this->config['uri']) . '/authorize'); - $url .= '?' . http_build_query($params); - - $this->plugin->debug("[{$this->id}][authorize] Redirecting to $url"); - - header("Location: $url"); - die; - } - - /** - * Authorization response validation - */ - public function response() - { - $this->plugin->debug("[{$this->id}][authorize] Response: " . $_SERVER['REQUEST_URI']); - - $this->error = $this->error_message( - rcube_utils::get_input_value('error', rcube_utils::INPUT_GET), - rcube_utils::get_input_value('error_description', rcube_utils::INPUT_GET), - rcube_utils::get_input_value('error_uri', rcube_utils::INPUT_GET) - ); - - if ($this->error) { - return; - } - - $state = rcube_utils::get_input_value('state', rcube_utils::INPUT_GET); - $code = rcube_utils::get_input_value('code', rcube_utils::INPUT_GET); - - if (!$state) { - $this->plugin->debug("[{$this->id}][response] State missing"); - $this->error = $this->plugin->gettext('errorinvalidresponse'); - return; - } - - if ($state != $this->plugin->rc->get_request_token()) { - $this->plugin->debug("[{$this->id}][response] Invalid response state"); - $this->error = $this->plugin->gettext('errorinvalidresponse'); - return; - } - - if (!$code) { - $this->plugin->debug("[{$this->id}][response] Code missing"); - $this->error = $this->plugin->gettext('errorinvalidresponse'); - return; - } - return $this->request_token($code); - } - - /** - * Error message for the response handler - */ - public function response_error() - { - if ($this->error) { - return $this->plugin->rc->gettext('loginfailed') . ' ' . $this->error; - } - } - - /** - * Existing session validation - */ - public function validate_session($session) - { - $this->plugin->debug("[{$this->id}][validate] Session: " . json_encode($session)); - - // Sanity checks - if (empty($session) || empty($session['code']) || empty($session['validto']) || empty($session['email'])) { - $this->plugin->debug("[{$this->id}][validate] Session invalid"); - return; - } - - // Check expiration time - $now = new DateTime('now', new DateTimezone('UTC')); - $validto = new DateTime($session['validto'], new DateTimezone('UTC')); - - // Don't refresh often than TTL/2 - $validto->sub(new DateInterval(sprintf('PT%dS', $session['ttl']/2))); - if ($now < $validto) { - $this->plugin->debug("[{$this->id}][validate] Token valid, skipping refresh"); - return $session; - } - - // No refresh_token, not possible to refresh - if (empty($session['refresh_token'])) { - $this->plugin->debug("[{$this->id}][validate] Session cannot be refreshed"); - return; - } - - // Renew tokens - $info = $this->request_token($session['code'], $session['refresh_token']); +require_once __DIR__ . '/oauth2.php'; - if (!empty($info)) { - // Make sure the email didn't change - if (!empty($info['email']) && $info['email'] != $session['email']) { - $this->plugin->debug("[{$this->id}][validate] Email address change"); - return; - } - - $session = array_merge($session, $info); - - $this->plugin->debug("[{$this->id}][validate] Session valid: " . json_encode($session)); - return $session; - } - } - - /** - * Authentication Token request (or token refresh) - */ - protected function request_token($code, $refresh_token = null) - { - $mode = $refresh_token ? 'token-refresh' : 'token'; - $url = $this->config['token_uri'] ?: ($this->config['uri'] . '/token'); - $params = array( - 'client_id' => $this->config['client_id'], - 'client_secret' => $this->config['client_secret'], - 'grant_type' => $refresh_token ? 'refresh_token' : 'authorization_code', - ); - - if ($refresh_token) { - $params['refresh_token'] = $refresh_token; - $params['scope'] = 'openid email offline_access'; - } - else { - $params['code'] = $code; - $params['redirect_uri'] = $this->redirect_uri(); - } - - $post = http_build_query($params); - - $this->plugin->debug("[{$this->id}][$mode] Requesting POST $url?$post"); - - try { - // TODO: JWT-based methods of client authentication - // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.9 - - $request = $this->get_request($url, 'POST'); - $request->setAuth($this->config['client_id'], $this->config['client_secret']); - $request->setBody($post); - - $response = $request->send(); - $status = $response->getStatus(); - $response = $response->getBody(); - - $this->plugin->debug("[{$this->id}][$mode] Response: $response"); - - $response = @json_decode($response, true); - - if ($status != 200 || !is_array($response) || !empty($response['error'])) { - $err = $this->error_text(is_array($response) ? $response['error'] : null); - throw new Exception("OpenIDC request failed with error: $err"); - } - } - catch (Exception $e) { - $this->error = $this->plugin->gettext('errorunknown'); - rcube::raise_error(array( - 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), - true, false); - return; - } - - // Example response: { - // "access_token":"ACCESS_TOKEN", - // "token_type":"bearer", - // "expires_in":2592000, - // "refresh_token":"REFRESH_TOKEN", - // "scope":"read", - // "uid":100101, - // "info":{"name":"Mark E. Mark","email":"mark@example.com"} - // } - - if (empty($response['access_token']) || empty($response['token_type']) - || strtolower($response['token_type']) != 'bearer' - ) { - $this->error = $this->plugin->gettext('errorinvalidresponse'); - $this->plugin->debug("[{$this->id}][$mode] Error: Invalid or unsupported response"); - return; - } - - $ttl = $response['expires_in'] ?: 600; - $validto = new DateTime(sprintf('+%d seconds', $ttl), new DateTimezone('UTC')); - - $result = array( - 'code' => $code, - 'access_token' => $response['access_token'], - // 'token_type' => $response['token_type'], - 'validto' => $validto->format(DateTime::ISO8601), - 'ttl' => $ttl, - ); - - if (!empty($response['refresh_token'])) { - $result['refresh_token'] = $response['refresh_token']; - } - - if (!empty($response['id_token'])) { - try { - $key = $this->config['client_secret']; - - if (!empty($this->config['pubkey'])) { - $pubkey = trim(preg_replace('/\r?\n[\s\t]+/', "\n", $this->config['pubkey'])); - - if (strpos($pubkey, '-----') !== 0) { - $pubkey = "-----BEGIN PUBLIC KEY-----\n" . trim(chunk_split($pubkey, 64, "\n")) . "\n-----END PUBLIC KEY-----"; - } - - if ($keyid = openssl_pkey_get_public($pubkey)) { - $key = $keyid; - } - else { - throw new Exception("Failed to extract public key"); - } - } - - $jwt = new Firebase\JWT\JWT; - $jwt::$leeway = 60; - - $payload = $jwt->decode($response['id_token'], $key, array_keys(Firebase\JWT\JWT::$supported_algs)); - $email = $this->config['debug_email'] ?: $payload->email; - - if (empty($email)) { - throw new Exception("No email address in JWT token"); - } - - if (!in_array($this->config['client_id'], (array) $payload->aud)) { - throw new Exception("Token audience does not match"); - } - - // More extended token validation - // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - $result['email'] = $email; - } - catch (Exception $e) { - $this->error = $this->plugin->gettext('errorinvalidtoken'); - rcube::raise_error(array( - 'line' => __LINE__, 'file' => __FILE__, 'message' => $e->getMessage()), - true, false); - return; - } - } - - return $result; - } - - /** - * The URL to use when redirecting the user from SSO back to Roundcube - */ - protected function redirect_uri() - { - // response_uri is useful when the Provider does not allow - // URIs with parameters. In such case set response_uri = '/sso' - // and define a redirect in http server, example for Apache: - // RewriteRule "^sso" "/roundcubemail/?_task=login&_action=sso" [L,QSA] - - $redirect_params = empty($this->config['response_uri']) ? array('_action' => 'sso') : array(); - - $url = $this->plugin->rc->url($redirect_params, false, true); - - if (!empty($this->config['response_uri'])) { - $url = unslashify(preg_replace('/\?.*$/', '', $url)) . '/' . ltrim($this->config['response_uri'], '/'); - } - - return $url; - } - - /** - * Get HTTP/Request2 object - */ - protected function get_request($url, $type) - { - $config = array_intersect_key($this->config, array_flip(array( - 'ssl_verify_peer', - 'ssl_verify_host', - 'ssl_cafile', - 'ssl_capath', - 'ssl_local_cert', - 'ssl_passphrase', - 'follow_redirects', - ))); - - return libkolab::http_request($url, $type, $config); - } - - /** - * Returns (localized) user-friendly error message - */ - protected function error_message($error, $description, $uri) - { - if (empty($error)) { - return; - } - - $msg = $this->error_text($error); - - rcube::raise_error(array( - 'message' => "[SSO] $msg." . ($description ? " $description" : '') . ($uri ? " ($uri)" : '') - ), true, false); - - $label = 'error' . str_replace('_', '', $error); - if (!$this->plugin->rc->text_exists($label, 'kolab_sso')) { - $label = 'errorunknown'; - } - - return $this->plugin->gettext($label); - } +class kolab_sso_openidc extends kolab_sso_oauth2 +{ + protected $id = 'openidc'; + protected $params = array( + 'scope' => 'openid email offline_access', + 'token_type' => 'id_token', + ); /** * Returns error text for specified OpenIDC error code */ protected function error_text($error) { + // OpenIDC-specific codes switch ($error) { - // OAuth2 codes - case 'invalid_request': - return "Request malformed"; - case 'unauthorized_client': - return "The client is not authorized"; - case 'invalid_client': - return "Client authentication failed"; - case 'access_denied': - return "Request denied"; - case 'unsupported_response_type': - return "Unsupported response type"; - case 'invalid_grant': - return "Invalid authorization grant"; - case 'unsupported_grant_type': - return "Unsupported authorization grant"; - case 'invalid_scope': - return "Invalid scope"; - case 'server_error': - return "Server error"; - case 'temporarily_unavailable': - return "Service temporarily unavailable"; - // OpenIDC codes case 'interaction_required': return "End-User interaction required"; case 'login_required': return "End-User authentication required"; case 'account_selection_required': return "End-User account selection required"; case 'consent_required': return "End-User consent required"; case 'invalid_request_uri': return "Invalid request_uri"; case 'invalid_request_object': return "Invalid Request Object"; case 'request_not_supported': return "Request not supported"; case 'request_uri_not_supported': return "request_uri param not supported"; case 'registration_not_supported': return "Registration not supported"; } - return "Unknown error"; + // Fallback to OAuth2-specific codes + return parent::error_text($error); } } diff --git a/plugins/kolab_sso/drivers/seas.php b/plugins/kolab_sso/drivers/seas.php new file mode 100644 index 00000000..1dc8a45a --- /dev/null +++ b/plugins/kolab_sso/drivers/seas.php @@ -0,0 +1,38 @@ + + * Beat Rubischon + * + * Copyright (C) 2018, Kolab Systems AG + * Copyright (C) 2019, Adfinis SyGroup AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +require_once __DIR__ . '/oauth2.php'; + + +class kolab_sso_seas extends kolab_sso_oauth2 +{ + protected $id = 'seas'; + protected $defaults = array( + 'scope' => 'USER', + 'token_type' => 'access_token', + 'user_field' => 'user_name', + 'validate_items' => array(), + ); +}