kolab_sso: Add oauth2 and seas drivers (Bifrost#T194908)

This commit is contained in:
Aleksander Machniak 2019-04-03 12:53:10 +02:00
parent c64a272df3
commit a0454e8080
4 changed files with 487 additions and 358 deletions

View file

@ -2,7 +2,8 @@ Single Sign On Authentication for Kolab
--------------------------------------- ---------------------------------------
This plugin adds possibility to authenticate users via external authentication 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 Because Kolab backends do not support token authentication it is required
to use master user (sasl proxy) authentication where possible and service to use master user (sasl proxy) authentication where possible and service

View file

@ -0,0 +1,436 @@
<?php
/**
* kolab_sso driver implementing OAuth2 Authorization (RFC6749)
* with use of JWT tokens.
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2018-2019, Kolab Systems AG <contact@kolabsys.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class kolab_sso_oauth2
{
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' => $this->get_param('scope'),
'client_id' => $this->get_param('client_id'),
'state' => $this->plugin->rc->get_request_token(),
'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);
}
$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->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'] = $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($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($token)) {
try {
$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($token, $key, array_keys(Firebase\JWT\JWT::$supported_algs));
$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) {
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";
}
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]);
}
}

View file

@ -23,371 +23,24 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
class kolab_sso_openidc
require_once __DIR__ . '/oauth2.php';
class kolab_sso_openidc extends kolab_sso_oauth2
{ {
protected $id = 'openidc'; protected $id = 'openidc';
protected $config = array(); protected $params = array(
protected $plugin; 'scope' => 'openid email offline_access',
'token_type' => 'id_token',
);
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']);
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);
}
/** /**
* Returns error text for specified OpenIDC error code * Returns error text for specified OpenIDC error code
*/ */
protected function error_text($error) protected function error_text($error)
{ {
// OpenIDC-specific codes
switch ($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': case 'interaction_required':
return "End-User interaction required"; return "End-User interaction required";
case 'login_required': case 'login_required':
@ -408,6 +61,7 @@ class kolab_sso_openidc
return "Registration not supported"; return "Registration not supported";
} }
return "Unknown error"; // Fallback to OAuth2-specific codes
return parent::error_text($error);
} }
} }

View file

@ -0,0 +1,38 @@
<?php
/**
* kolab_sso driver implementing Abraxas SEAS Portal OAuth2/JWT flow
*
* @author Aleksander Machniak <machniak@kolabsys.com>
* Beat Rubischon <beat.rubischon@adfinis-sygroup.ch>
*
* Copyright (C) 2018, Kolab Systems AG <contact@kolabsys.com>
* 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 <http://www.gnu.org/licenses/>.
*/
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(),
);
}