*/
class Authentiq_Provider
{
/**
* This cookie is used in the Authentiq OAuth2 handshake for the state parameter.
*/
public static $cookie_name = 'wp_authentiq_state';
protected $options;
private $authentiq_sid = null;
public function __construct($options = null) {
if ($options instanceof Authentiq_Options) {
$this->options = $options;
} else {
$this->options = Authentiq_Options::Instance();
}
}
public function init() {
// Initialize Authentiq state for OAuth2 handshake
add_action('init', array($this, 'initialize_state'));
// Auto-login with Authentiq when set in options
add_action('login_init', array($this, 'auto_login_with_authentiq'));
// Authenticate with Authentiq if there is a valid OAuth2 code
add_action('authenticate', array($this, 'handle_authentiq_redirect'), 10, 3);
// Disable WP password authentication based on Authentiq plugin settings
add_action('wp_authenticate_user', array($this, 'disable_wp_password_login'));
// Disable WP password reset based on Authentiq plugin settings
add_filter('allow_password_reset', array($this, 'allow_password_reset'), 10, 2);
// Custom Authentiq plugin actions for logout
add_action('wp_logout', array($this, 'logout'));
add_action('wp_login', array($this, 'end_wp_session'));
}
public static function get_redirect_url() {
$url = add_query_arg(array(
AUTHENTIQ_OP_REDIRECT_QUERY_PARAM => 1,
), wp_login_url());
return $url;
}
public static function get_authorize_url($extra_scopes = array()) {
$options = Authentiq_Options::Instance();
$client_id = urlencode($options->get('client_id'));
// Always request
// 1. `openid` scope for fetching an id_token
// 2. `aq:push` for push notifications support on subsequent logins
// 3. `email~rs` this is used to create/link a WP user, for this reason email has to be signed
// 4. rest are being used for WP profile
$predefined_scopes = array('openid', 'aq:push', 'email~rs', 'aq:name', 'aq:username');
// WP site requested scopes
$scopes = $options->get('client_scopes');
$scopes = array_merge($scopes, $predefined_scopes);
if (!empty($extra_scopes) && is_array($extra_scopes)) {
$scopes = array_merge($scopes, $extra_scopes);
}
/**
* Filters the requested scopes from Authentiq
*
* @param array $scopes Scopes that will be requested from Authentiq
*/
$scopes = apply_filters('authentiq_requested_scopes', $scopes);
$scope = urlencode(implode(' ', $scopes));
$state_obj = array(
'state' => self::get_state(),
);
// Pass redirect_to with state, in order we can redirect back after sign-in
$redirect_to = get_permalink();
if (!empty($redirect_to)) {
$state_obj['redirect_to'] = $redirect_to;
}
$state = base64_encode(json_encode($state_obj));
$url = add_query_arg(array(
'client_id' => $client_id,
'response_mode' => 'query',
'response_type' => 'code',
'redirect_uri' => urlencode(Authentiq_Provider::get_redirect_url()),
'scope' => $scope,
'prompt' => 'login',
'nonce' => wp_create_nonce(),
'state' => urlencode($state),
), AUTHENTIQ_PROVIDER_AUTHORIZE_URL);
return $url;
}
/**
* It forces user to the Authentiq Provider using a redirect when allowed from plugin options.
*/
function auto_login_with_authentiq() {
if (Authentiq_Helpers::query_vars(AUTHENTIQ_OP_REDIRECT_QUERY_PARAM) !== null) {
return;
}
$auto_login = $this->options->get('auto_login');
if ($auto_login
&& (!isset($_GET['action']) || 'logout' !== $_GET['action'])
&& !isset($_GET[AUTHENTIQ_LOGIN_FORM_QUERY_PARAM])
&& strtolower($_SERVER['REQUEST_METHOD']) === 'get') {
wp_redirect(Authentiq_Provider::get_authorize_url());
die();
}
}
/**
* Handle Authentiq Provider redirects.
*/
function handle_authentiq_redirect() {
if (Authentiq_Helpers::query_vars(AUTHENTIQ_OP_REDIRECT_QUERY_PARAM) === null) {
return;
}
try {
$this->handle_oauth_flow();
} catch (Exception $e) {
return new WP_Error('login_error', $e->getMessage());
}
// } catch (Authentiq_Login_Redirect_Exception $e) {
// return new WP_Error('login_error', $e->getMessage());
//
// } catch (Exception $e) {
//
// $msg = '';
//
// if ($e instanceof Authentiq_Login_Flow_Validation_Exception) {
// $msg .= '
' . __('You failed to log in, because of invalid session.', AUTHENTIQ_LANG) . '
';
//
// } else {
// $msg .= '' . __('Ooops.', AUTHENTIQ_LANG) . '
';
// }
// $msg .= $e->getMessage();
// $msg .= '
';
// $msg .= '' . __('← Back to site', AUTHENTIQ_LANG) . '';
// $msg .= ' | ' . __('Login with Authentiq', AUTHENTIQ_LANG) . '';
//
// wp_die($msg);
// }
}
/**
* Handle OAuth 2.0 errors from Authentiq Provider
*
* @throws Authentiq_Login_Redirect_Exception
*/
static function check_oauth_flow_error() {
if (!empty(Authentiq_Helpers::query_vars('error_description'))) {
throw new Authentiq_Login_Redirect_Exception(Authentiq_Helpers::query_vars('error_description'));
}
if (!empty(Authentiq_Helpers::query_vars('error'))) {
throw new Authentiq_Login_Redirect_Exception(Authentiq_Helpers::query_vars('error'));
}
}
/**
* Handle OAuth 2.0 flow after redirect back from Authentiq Provider
*
* 1. Check if OP returned an error
* 2. Verify `state` returned from Authentiq Provider
* 3. Exchange code for tokens with Authentiq Provider
* 4. If no id_token, fetch userinfo from Authentiq Provider
* 5. If id_token exists, use id_token to get userinfo
* 6. Handle errors
*
* @throws Authentiq_Login_Flow_Validation_Exception
* @throws Authentiq_Login_Redirect_Exception
* @throws Exception
*/
function handle_oauth_flow() {
// 1. Check if OP returned an error
self::check_oauth_flow_error();
// 2. Verify `state` returned from Authentiq Provider
$request_state = Authentiq_Helpers::query_vars('state');
$state_obj = (array)json_decode(base64_decode($request_state));
$state = isset($state_obj['state']) ? $state_obj['state'] : '';
self::verify_state($state);
// Get client info from plugin settings
$client_id = $this->options->get('client_id');
$client_secret = $this->options->get('client_secret');
if (empty($client_id)) {
throw new Authentiq_Login_Redirect_Exception(__('Error: Your Authentiq Client ID has not set in the Authentiq plugin settings.', AUTHENTIQ_LANG));
}
if (empty($client_secret)) {
throw new Authentiq_Login_Redirect_Exception(__('Error: Your Authentiq Client Secret has not set in the Authentiq plugin settings.', AUTHENTIQ_LANG));
}
// 3. Exchange code for tokens with Authentiq Provider
$code = Authentiq_Helpers::query_vars('code');
$token_response = $this->get_token($client_id, $client_secret, 'authorization_code', array(
'redirect_uri' => Authentiq_Provider::get_redirect_url(),
'code' => $code,
));
$data = json_decode($token_response['body']);
// if tokens found
if (isset($data->access_token) || isset($data->id_token)) {
// 4. If no id_token, fetch userinfo from Authentiq Provider
if (!isset($data->id_token)) {
$data->id_token = null;
$userinfo_response = $this->get_user_info($data->access_token);
$userinfo = json_decode($userinfo_response['body']);
// 5. If id_token exists, use id_token to get userinfo
} else {
try {
$decoded_token = JWT::decode($data->id_token, $client_secret, array('HS256'));
} catch (Exception $e) {
$msg = __('Error: There was an issue decoding the id_token.', AUTHENTIQ_LANG);
$msg .= '
' . $e->getMessage();
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
// Validate that this JWT was made for us
if ($client_id !== $decoded_token->aud) {
throw new Exception('This token is not intended for us.');
}
// Validate that nonce is correct, when set
// TODO: nonce has to be set
if (!empty($decoded_token->nonce) && !wp_verify_nonce($decoded_token->nonce)) {
throw new Exception('This token is not correct.');
}
$userinfo = $decoded_token;
}
// Perform user login to WP
if ($this->handle_wp_user($userinfo, $data->id_token, $data->access_token)) {
// Default redirect URL for Authentiq
$redirect_to = $this->options->get('default_login_redirection', home_url());
if (isset($_REQUEST['redirect_to']) && !empty($_REQUEST['redirect_to'])) {
$redirect_to = $_REQUEST['redirect_to'];
} else if (isset($state_obj['redirect_to']) && !empty($state_obj['redirect_to'])) {
$redirect_to = $state_obj['redirect_to'];
}
wp_safe_redirect($redirect_to);
die();
} else {
throw new Exception(__('Failed to login using Authentiq.', AUTHENTIQ_LANG));
}
// 6. Handle errors
} elseif (is_array($token_response['response']) && 401 === (int)$token_response['response']['code']) {
$msg = __('Error: the Client Secret configured on the Authentiq plugin is wrong. Make sure to copy the right one from the Authentiq dashboard.', AUTHENTIQ_LANG);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
} else {
if (!empty($data->error_description)) {
throw new Exception($data->error_description);
}
if (!empty($data->error)) {
throw new Exception($data->error);
}
throw new Exception(__('Failed to login using Authentiq.', AUTHENTIQ_LANG));
}
die();
}
/**
* Exchange code for tokens with Authentiq Provider
*
* @param $client_id
* @param $client_secret
* @param string $grantType
* @param array $request_params
*
* @return array|bool|WP_Error
* @throws Authentiq_Login_Flow_Validation_Exception
*/
static function get_token($client_id, $client_secret = null, $grantType = 'client_credentials', $request_params = array()) {
if (!is_array($request_params)) {
$request_params = array();
}
$endpoint = AUTHENTIQ_PROVIDER_TOKEN_URL;
$request_params['client_id'] = $client_id;
$request_params['client_secret'] = is_null($client_secret) ? '' : $client_secret;
$request_params['grant_type'] = $grantType;
$headers = array(
'content-type' => 'application/x-www-form-urlencoded',
);
$response = wp_remote_post($endpoint, array(
'headers' => $headers,
'body' => $request_params,
));
if ($response instanceof WP_Error) {
$msg = $response->get_error_message();
error_log($msg);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
return $response;
}
/**
* Get userinfo from Authentiq Provider
*
* @param $access_token
*
* @return array|WP_Error
* @throws Authentiq_Login_Flow_Validation_Exception
*/
static function get_user_info($access_token) {
$endpoint = AUTHENTIQ_PROVIDER_USERINFO_URL;
$headers = array(
'Authorization' => 'Bearer ' . $access_token,
);
$response = wp_remote_get($endpoint, array(
'headers' => $headers,
));
if ($response instanceof WP_Error) {
error_log($response->get_error_message());
throw new Authentiq_Login_Flow_Validation_Exception();
}
return $response;
}
/**
* Filters if user is allowed to sign in based on the email domain
*
* @param $email
*
* @throws Authentiq_Login_Flow_Validation_Exception
*/
function filter_user_by_email_domain($email) {
// Get domains from plugin settings
$domains_filter = $this->options->get('filter_user_domains');
// 0: whitelist domains, 1: blacklist domains
$filter_user_domains_condition = $this->options->get('filter_user_domains_condition');
// split domains per new line
$domains_filter = preg_split('/$\R?^/m', $domains_filter);
// remove empty values
$domains_filter = array_filter($domains_filter);
/**
* Filters if user is allowed to sign in based on the email domain
*
* @param array $domains_filter Domains defined in Authentiq plugin settings
* @param int $filter_user_domains_condition -> 0: whitelist domains, 1: blacklist domains
*/
$domains_filter = apply_filters('authentiq_domain_filter', $domains_filter, $filter_user_domains_condition);
// remove empty values
$domains_filter = array_filter($domains_filter);
// no domains to parse
if (empty($domains_filter)) {
return;
}
list($current_email_user, $current_email_domain) = explode('@', $email);
$current_email_domain = trim(strtolower($current_email_domain));
// blacklist domains
if ($filter_user_domains_condition == 1) {
if (in_array($current_email_domain, $domains_filter)) {
$msg = __('This email domain (%s) is not allowed by site administrator.', AUTHENTIQ_LANG);
$msg = sprintf($msg, $current_email_domain);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
// whitelist domains
} else {
if (!in_array($current_email_domain, $domains_filter)) {
$msg = __('This email domain (%s) is not whitelisted by site administrator.', AUTHENTIQ_LANG);
$msg = sprintf($msg, $current_email_domain);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
}
}
/**
* Create new, Update or Link user
* and login this user to WordPress.
*
* returns true if login was successful, false otherwise
*
* @param $userinfo
* @param $id_token
* @param $access_token
*
* @return bool
* @throws Authentiq_Login_Flow_Validation_Exception
*/
function handle_wp_user($userinfo, $id_token, $access_token) {
$email = $userinfo->email;
$email_verified = $userinfo->email_verified;
// If the userinfo has no email or an unverified email,
// and in the options we require a verified email
// notify the user she has to send a verified email from AuthentiqID App
$requires_verified_email = $this->options->get('requires_verified_email');
if ($requires_verified_email) {
if (empty($email)) {
$msg = __('Please set an email at AuthentiqID App.', AUTHENTIQ_LANG);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
if (empty($email_verified)) {
$msg = __('Please verify the email used at AuthentiqID App.', AUTHENTIQ_LANG);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
}
// Filter user based on email domain
if (!empty($email)) {
$this->filter_user_by_email_domain($email);
}
// Get existing user for this Authentiq sub (user_id)
if (isset($userinfo->sub)) {
$user = Authentiq_User::get_user_by_sub($userinfo->sub);
if (!is_null($user) && $user) {
$other_user_using_sub = $user;
}
// TODO: check if another WP exists with same email
// allow user to merge both WP users if email is verified
}
// Check if the user was already signed in, which means that user wanted to link his account
if (is_user_logged_in()) {
if (!empty($other_user_using_sub)) {
$msg = __('This Authentiq ID is already linked to another WordPress user.', AUTHENTIQ_LANG);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
$current_user = wp_get_current_user();
// Check if this user is already linked to another Authentiq ID
if (Authentiq_User::has_authentiq_id($current_user->ID)) {
$msg = __('Current WordPress user is already linked with another Authentiq ID.', AUTHENTIQ_LANG);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
$user = wp_get_current_user();
}
// Link existing WP user using email if is verified
if ((is_null($user) || !$user) && !empty($email) && $email_verified) {
$user = Authentiq_User::get_user_by_email($email);
}
$user = apply_filters('authentiq_get_wp_user', $user, $userinfo);
// If a WP user found
if (!is_null($user) && $user) {
// Update WP user with userinfo
$user_id = Authentiq_User::update_user($user, $userinfo);
// Check if user was created
if (is_wp_error($user_id)) {
throw new Authentiq_User_Exception($user_id->get_error_message());
}
// Store Authentiq session id sent from Provider,
// in order we can logout this single session from the App
if (!empty($userinfo->sid)) {
$this->authentiq_sid = $userinfo->sid;
add_filter('attach_session_information', array($this, 'authentiq_attach_session_information'));
}
$this->login_user_to_wp($user, $userinfo, false, $id_token, $access_token);
return true;
} else {
try {
$allow_signup = $this->options->is_wp_registration_enabled();
if ($allow_signup) {
// Create a new WP user
$user_id = Authentiq_User::create_user($userinfo);
// Check if user was created
if (is_wp_error($user_id)) {
$msg = __('Failed to create WordPress user', AUTHENTIQ_LANG);
$msg .= ': ' . $user_id->get_error_message();
throw new Authentiq_User_Exception($msg);
}
} elseif (!$allow_signup) {
throw new Authentiq_User_Exception(__('User registration is not allowed by site adminitrator.', AUTHENTIQ_LANG));
}
// Store Authentiq session id sent from Provider,
// in order we can logout this single session from the App
if (!empty($userinfo->sid)) {
$this->authentiq_sid = $userinfo->sid;
add_filter('attach_session_information', array($this, 'authentiq_attach_session_information'));
}
$user = get_user_by('id', $user_id);
$this->login_user_to_wp($user, $userinfo, true, $id_token, $access_token);
return true;
} catch (Authentiq_User_Exception $e) {
throw new Authentiq_Login_Flow_Validation_Exception($e->getMessage());
}
}
return false;
}
/**
* Add Authentiq session_id sent from Provider in WP session,
* in order we can logout this single session from the App.
*
* @param $session
*
* @return session object
*/
function authentiq_attach_session_information($session) {
$session['authentiq_sid'] = $this->authentiq_sid;
return $session;
}
/**
* Login the user to WordPress, using WP hooks and actions as appropriate
*
* @param $user
* @param $userinfo
* @param $is_new : `true` if the user was created on WordPress, `false` if not.
* @param $id_token
* @param $access_token
*
* @throws Authentiq_Before_Login_Exception
*/
private function login_user_to_wp($user, $userinfo, $is_new, $id_token, $access_token) {
// Allow other hooks to run prior to login, and handle exception
try {
do_action('authentiq_before_login', $user);
} catch (Exception $e) {
throw new Authentiq_Before_Login_Exception($e->getMessage());
}
wp_set_auth_cookie($user->ID, true);
/**
* Fires after the user has successfully logged in.
*
* @param string $user_login Username.
* @param WP_User $user WP_User object of the logged-in user.
*/
do_action('wp_login', $user->user_login, $user);
do_action('authentiq_user_login', $user->ID, $userinfo, $is_new, $id_token, $access_token);
}
public function end_wp_session() {
if (session_id()) {
session_destroy();
}
}
public function logout() {
$this->end_wp_session();
// TODO: handle logout at Authentiq Provider
}
function disable_wp_password_login($user) {
global $wpdb;
$allow_classic_wp_login = $this->options->allow_classic_wp_login();
if (!$allow_classic_wp_login) {
$msg = __('Passwords have been disabled. Try to login with Authentiq.', AUTHENTIQ_LANG);
return new WP_Error('wp_passwords_disabled', $msg);
} else {
// accepts certain users authenticating based on this rule
// 0: all users, 1: users without Authentiq ID, 2: no users
$allow_classic_wp_login_for = $this->options->get('classic_wp_login_for');
if ($allow_classic_wp_login_for == 1) {
// check if user has already linked with Authentiq ID
$authentiq_id = get_user_meta($user->ID, $wpdb->prefix . 'authentiq_id', true);
if ($authentiq_id) {
$msg = __('You have already linked your account with Authentiq ID. Please use Authentiq ID to sign in.', AUTHENTIQ_LANG);
return new WP_Error('wp_login_failed_authentiq_linked', $msg);
}
} else if ($allow_classic_wp_login_for == 2) {
$msg = __('Passwords have been disabled. Try to login with Authentiq.', AUTHENTIQ_LANG);
return new WP_Error('wp_passwords_disabled', $msg);
}
return $user;
}
}
function allow_password_reset($allow, $user_id) {
global $wpdb;
// if it's not allowed already by another plugin, then follow the rules set
if (!allow) {
return false;
}
$allow_wp_password_login = $this->options->allow_classic_wp_login();
if ($allow_wp_password_login) {
// accepts certain users authenticating based on this rule
// 0: all users, 1: users without Authentiq ID, 2: no users
$allow_classic_wp_login_for = $this->options->get('classic_wp_login_for');
if ($allow_classic_wp_login_for == 1) {
// check if user has already linked with Authentiq ID
$authentiq_id = get_user_meta($user_id, $wpdb->prefix . 'authentiq_id', true);
// allow reset password only when user is not linked with Authentiq
return !$authentiq_id;
}
return $allow_classic_wp_login_for < 2;
}
return false;
}
static function initialize_state($override = false) {
if (!$override && isset($_COOKIE[self::$cookie_name]) && $_COOKIE[self::$cookie_name]) return;
$state = wp_generate_password(24, false);
@setcookie(self::$cookie_name, $state, (time() + (60 * 30)), '/', '', is_ssl(), true);
$_COOKIE[self::$cookie_name] = $state;
return $state;
}
static function get_state() {
if (!isset($_COOKIE[self::$cookie_name]) || !$_COOKIE[self::$cookie_name]) self::initialize_state(true);
return $_COOKIE[self::$cookie_name];
}
static function verify_state($request_state = '') {
$current_state = self::get_state();
if ($request_state && $current_state && $current_state == $request_state) {
self::initialize_state(true);
return true;
} else {
$msg = __('The Authentiq state parameter can not be verified. ' .
'This may be due to this page being cached by another WordPress plugin. Please refresh your page and try again.', AUTHENTIQ_LANG);
throw new Authentiq_Login_Flow_Validation_Exception($msg);
}
}
}