Enable oauth2 login

Replace openid provider with oauth2. Oauth2 provides more details about
user profile and contains profile picture url. This patch contains
the groups_oauth2 module, an oauth2 authorization implementation for Drupal
written for openstackid.org provider. See module's readme for required
variable settings.

Change-Id: I30fc363d60a5f679194dfd0f9d6c6453f783f9aa
This commit is contained in:
Marton Kiss 2014-11-21 11:38:44 +01:00
parent d86a9875ab
commit b9db93a633
12 changed files with 514 additions and 5 deletions

View File

@ -152,6 +152,8 @@ dependencies[] = groups_footer
dependencies[] = groups_homepage
dependencies[] = groups_common
dependencies[] = groups_auth
dependencies[] = groups_oauth2
dependencies[] = groups_oauth2_picture
dependencies[] = groups_feeds
dependencies[] = groups_events
dependencies[] = groups_events_pages

View File

@ -181,6 +181,16 @@ function groups_update_7107() {
}
}
/**
* Enable groups_oauth2 modules.
*/
function groups_update_7108() {
if ((!module_exists('groups_oauth2')) && (!module_exists('groups_oauth2_picture'))) {
module_enable(array('groups_oauth2', 'groups_oauth2_picture'));
drupal_flush_all_caches();
}
}
/**
* Add markdown filter with permissions.
*/

View File

@ -36,6 +36,10 @@ function groups_auth_menu_alter(&$items) {
$items['user/%user/openid']['page callback'] = 'groups_auth_user_identities';
$items['user/%user/openid']['access callback'] = 'groups_auth_role_access';
$items['user/%user/openid']['access arguments'] = array('administrator');
$items['user/%user/edit-profile']['access arguments'] = array('administrator');
$items['user/%user/edit-profile']['access callback'] = 'groups_auth_role_access';
$items['user/%user/edit']['access arguments'] = array('administrator');
$items['user/%user/edit']['access callback'] = 'groups_auth_role_access';
}
/**
@ -138,10 +142,14 @@ function groups_auth_form_user_register_form_alter(&$form, &$form_state) {
*/
function groups_auth_login() {
$return_to = url('openid/authenticate',
array('absolute' => TRUE, 'query' => array('destination' => '<front>')));
$openid_url = variable_get('groups_openid_provider', 'https://openstackid-dev.openstack.org');
openid_begin($openid_url, $return_to, array());
if (module_exists('groups_oauth2')) {
groups_oauth2_user_login_submit();
} else {
$return_to = url('openid/authenticate',
array('absolute' => TRUE, 'query' => array('destination' => '<front>')));
$openid_url = variable_get('groups_openid_provider', 'https://openstackid-dev.openstack.org');
openid_begin($openid_url, $return_to, array());
}
}
/**
@ -177,6 +185,15 @@ function groups_auth_commons_utility_links_alter(&$links) {
if (isset($links['signup'])) {
$links['signup']['href'] = 'login';
}
if (isset($links['name'])) {
$links['name']['title'] = t('View profile');
}
if (isset($links['logout'])) {
$links['logout']['weight'] = 1000;
}
if (isset($links['settings'])) {
unset($links['settings']);
}
}
/**

View File

@ -0,0 +1,28 @@
Description
-----------
This module provides a basic oauth2 authentication for openstackid.org provider. Supports
fetching of first and family name, and fetch profile picture url. As openstackid provider
requires https communication, and the profile image assets not available through ssl,
the groups_auth2 module supports fetching of profile pictures into a local directory.
Requirements
------------
Drupal 7.x
Properly configured Oauth2 provider.
Variables
---------
oauth2_fetch_profile_picture:boolean
If set to TRUE, downloads profile picture during login into the public://profile-images
directory. Default value is FALSE.
groups_oauth2_provider:string
Contains the url of oauth2 provider. For openstackid, set it to https://openstackid.org
groups_oauth2_client_id:string
The client id assigned for this specific application.
groups_oauth2_client_secret:string
The client secret assigned for the client_id.

View File

@ -0,0 +1,40 @@
<?php
/**
* Menu callback: displays the groups_oauth2 module settings page.
*
* @ingroup forms
*
* @see groups_oauth2_admin_settings_validate()
*/
function groups_oauth2_admin_settings($form) {
$form['groups_oauth2_provider'] = array(
'#type' => 'textfield',
'#title' => t('OAuth2 Provider URL'),
'#default_value' => variable_get('groups_oauth2_provider'),
'#required' => TRUE,
);
$form['groups_oauth2_client_id'] = array(
'#type' => 'textfield',
'#title' => t('OAuth2 Client ID'),
'#default_value' => variable_get('groups_oauth2_client_id'),
'#required' => TRUE,
);
$form['groups_oauth2_client_secret'] = array(
'#type' => 'textfield',
'#title' => t('OAuth2 Client Secret'),
'#default_value' => variable_get('groups_oauth2_client_secret'),
'#required' => TRUE,
);
$form['#validate'][] = 'groups_oauth2_admin_settings_validate';
return system_settings_form($form);
}
/**
* Form validation handler for groups_oauth2_admin_settings().
*/
function groups_oauth2_admin_settings_validate($form, &$form_state) {
if (valid_url($form_state['values']['groups_oauth2_provider'], TRUE) == FALSE) {
form_set_error('allmyles_wsclient', t('The provider url value must be a valid url.'));
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* @file
* This file contains API documentation for the Groups OAuth2 module. Note that
* all of this code is merely for example purposes, it is never executed when
* using the Groups OAuth2 module.
*/
/**
* Hook to map account data to a Drupal user after connecting.
*
* This hook is fired after a user account is created by groups
* oauth2 module.
*
* @param $account
* A user account array.
* @param $userinfo
* A user profile returned by oauth2 provider.
* @return
* None.
*/
function hook_oauth2_user_save(&$account, $userinfo) {
}

View File

@ -0,0 +1,7 @@
name = Groups OAuth2
description = "Groups Authentication module - uses OAuth2. Handle authentication to openstackid.org service"
core = 7.x
package = Groups - Authentication
version = "7.x-1.0"
project = "groups"

View File

@ -0,0 +1,320 @@
<?php
/**
* Implements hook_menu()
*/
function groups_oauth2_menu() {
$items['oauth2/response'] = array(
'page callback' => 'groups_oauth2_response_handler',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['admin/config/services/oauth2'] = array(
'title' => 'Oauth2 Settings',
'description' => 'OpenStackID Oauth2 settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('groups_oauth2_admin_settings'),
'access arguments' => array('administer site configuration'),
'file' => 'groups_oauth2.admin.inc',
);
return $items;
}
/**
* Implements hook_form_alter()
*
* Add Login with OpenStackID button to login form.
*/
function groups_oauth2_form_user_login_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'user_login' || $form_id == 'user_login_block') {
$form['submit_groups'] = array(
'#type' => 'submit',
'#value' => t('Login with OpenStackID'),
'#submit' => array('groups_oauth2_user_login_submit'),
'#limit_validation_errors' => array(),
'#weight' => 1000,
);
}
}
/**
* Handle login form submit.
*
* Redirect to oauth2 site and request an authorization code.
*/
function groups_oauth2_user_login_submit() {
if (!_groups_oauth2_validate_provider_settings()) {
return FALSE;
}
if (isset($_GET['destination'])) {
unset($_GET['destination']);
}
$redirect_uri = url('oauth2/response', array('https' => FALSE, 'absolute' => TRUE));
$url = _groups_oauth2_authorize_url('code', $redirect_uri, 'auto', 'profile email');
drupal_goto($url);
}
/**
* Oauth2 response handler callback.
*
* Process to authorization code, invoke token and login or create
* a new user.
*/
function groups_oauth2_response_handler() {
if (isset($_GET['code'])) {
if (!_groups_oauth2_validate_provider_settings()) {
return FALSE;
}
$redirect_uri = url('oauth2/response', array('https' => $GLOBALS['is_https'], 'absolute' => TRUE));
$response = _groups_oauth2_request_token('authorization_code', $_GET['code'], $redirect_uri);
if ($response->code == 200) {
$data = json_decode($response->data);
$userinfo = groups_oauth2_get_userinfo($data->access_token);
if (!$userinfo) {
drupal_set_message(t("Failed to retrieve user information"), 'error');
drupal_goto();
return;
}
$username = check_plain(strtolower($userinfo->name).'-'.strtolower($userinfo->family_name));
$query = "SELECT uid FROM {users} WHERE name = :name";
$uid = db_query($query, array(':name' => $username))->fetchField();
$i = 0;
while ($uid) {
$i++;
$uid = db_query($query, array(':name' => ($username . $i)))->fetchField();
}
if ($i > 0) {
$username = $username . $i;
}
$account = groups_oauth2_find_existing_user($userinfo);
if (!$account) {
$edit = array(
'name' => $username,
'mail' => $userinfo->email,
'init' => $userinfo->email,
'status' => 1,
'timezone' => variable_get('date_default_timezone'),
'data' => array(
'oauth2_fullname' => check_plain($userinfo->name.' '.$userinfo->family_name),
),
);
$account = user_save(NULL, $edit);
// Allow other modules to manipulate the user information after save.
foreach (module_implements('oauth2_user_save') as $module) {
$function = $module . '_oauth2_user_save';
$function($account, $userinfo);
}
}
$form_state['uid'] = $account->uid;
user_login_submit(array(), $form_state);
drupal_goto('user/' . $account->uid);
} else {
$data = json_decode($response->data);
drupal_set_message(t("The account is not authenticated. Reason: @error",
array('@error' => is_object($data) ? $data->error : 'Not known.')), 'error');
}
}
drupal_goto();
}
/**
* Fetch user info from oauth2 server.
*/
function groups_oauth2_get_userinfo($access_token) {
$query = array('access_token' => $access_token);
$request_url = url(variable_get('groups_oauth2_provider').'/api/v1/users/me', array('query' => $query));
$response = drupal_http_request($request_url);
if ($response->code == 200) {
return json_decode($response->data);
}
return FALSE;
}
/**
* Find a Drupal user by oauth2 user info.
*/
function groups_oauth2_find_existing_user($userinfo) {
if ($user = user_load_by_mail($userinfo->email)) {
return $user;
}
return FALSE;
}
/**
* Ensure groups oauth2 authentication variables.
*
* @return boolean
* Return TRUE if groups_oauth2_provider, groups_oauth2_client_id and
* groups_oauth2_client_secret are set, otherwise throw an error message.
*/
function _groups_oauth2_validate_provider_settings() {
$provider_url = variable_get('groups_oauth2_provider');
$client_id = variable_get('groups_oauth2_client_id');
$client_secret = variable_get('groups_oauth2_client_secret');
if ((!isset($provider_url)) || (!isset($client_id)) || (!isset($client_secret))) {
drupal_set_message(t('Groups oauth2 provider not properly configured.'), 'warning');
return FALSE;
}
return TRUE;
}
/**
* Build oauth2 authorization url.
*
* @param string $response_type
* Oauth2 response type, use code for basic flow
* @param string $redirect_uri
* Determines where the response will be sent
* @param string $approval_prompt
* Approval prompt, set to auto
* @param string $scope
* Aouth2 scope requesting access for
* @return string
* The full url for authorization code request.
*/
function _groups_oauth2_authorize_url($response_type, $redirect_uri, $approval_prompt, $scope) {
$params = array(
'response_type' => $response_type,
'client_id' => variable_get('groups_oauth2_client_id'),
'redirect_uri' => $redirect_uri,
'approval_prompt' => $approval_prompt,
'scope' => $scope,
);
$provider_url = variable_get('groups_oauth2_provider');
return $provider_url.'/oauth2/auth?'.http_build_query($params);
}
/**
* Send a token request based on authorization code to oauth2 provider.
*
* @param string $grant_type
* Set to 'authorization_code'
* @param string $code
* Code received from authorization request.
* @param string $redirect_uri
* The redirect uri set for this application.
* @return [type]
* Http response of token request.
*/
function _groups_oauth2_request_token($grant_type, $code, $redirect_uri) {
$params = array(
'grant_type' => $grant_type,
'code' => $code,
'redirect_uri' => $redirect_uri,
'client_id' => variable_get('groups_oauth2_client_id'),
'client_secret' => variable_get('groups_oauth2_client_secret'),
);
$elements = array();
foreach ($params as $k => $v) {
$elements[] = "$k=$v";
}
$data = implode('&', $elements);
$options = array(
'method' => 'POST',
'data' => $data,
'timeout' => 15,
'headers' => array('Content-Type' => 'application/x-www-form-urlencoded'),
);
$provider_url = variable_get('groups_oauth2_provider');
return drupal_http_request($provider_url.'/oauth2/token', $options);
}
/**
* Return the profile picture filename from url.
*/
function _groups_oauth2_get_profile_picture_filename($url) {
$parts = parse_url($url);
$basename = end(explode('/', $parts['path']));
return 'public://profile-images/'.$basename;
}
/**
* Implements hook_theme().
*/
function groups_oauth2_theme() {
return array(
'groups_oauth2_user_picture_override' => array(
'variables' => array(
'account' => NULL,
'picture_url' => NULL,
),
),
);
}
/**
* Implements hook_theme_registry_alter(), override
* @param [type] $theme_registry [description]
* @return [type] [description]
*/
function groups_oauth2_theme_registry_alter(&$theme_registry) {
// Re-register the original theme function under a new name.
$theme_registry['groups_oauth2_user_picture_orig'] = $theme_registry['user_picture'];
// Override theme username
$theme_registry['user_picture'] = array(
'arguments' => array('account' => NULL),
'function' => 'groups_oauth2_theme_user_picture_override',
'preprocess functions' => array(),
'theme path' => drupal_get_path('module', 'groups_oauth2'),
);
}
/**
* Make rendering of Oauth2 user picture themeable
*/
function theme_groups_oauth2_user_picture_override($variables) {
$picture_url = $variables['picture_url'];
$account = $variables['account'];
$image = theme('image', array('path' => $picture_url, 'alt' => $account->name));
return '<div class="picture">' . $image . '</div>';
}
/**
* Override user picture theme, and render oauth2 picture if available.
*/
function groups_oauth2_theme_user_picture_override($variables) {
$account = $variables['account'];
if (isset($account->data['oauth2_picture'])) {
if (variable_get('oauth2_fetch_profile_picture', FALSE)) {
$image_path = _groups_oauth2_get_profile_picture_filename($account->data['oauth2_picture']);
$picture_url = file_create_url($image_path);
} else {
$picture_url = $account->data['oauth2_picture'];
}
$output = theme('groups_oauth2_user_picture_override', array('account' => $account, 'picture_url' => $picture_url));
} else {
$output = theme('groups_oauth2_user_picture_orig', array('account' => $account));
}
return $output;
}
/**
* Implements hook_username_alter().
*
* @see realname_username_alter()
*/
function groups_oauth2_username_alter(&$name, $account) {
static $in_username_alter = FALSE;
if (empty($account->uid)) {
// Don't alter anonymous users or objects that do not have any user ID.
return;
}
// Name was loaded/generated via hook_user_load(), so re-use it.
if (isset($account->data['oauth2_fullname'])) {
if (drupal_strlen($account->data['oauth2_fullname'])) {
$name = $account->data['oauth2_fullname'];
}
return;
}
// Real name was not yet available for the account so we need to generate it.
// Because tokens may call format_username() we need to prevent recursion.
if (!$in_username_alter) {
$in_username_alter = TRUE;
// If $account->realname was undefined, then the user account object was
// not properly loaded. We must enforce calling user_load().
if ($oauth2_account = user_load($account->uid)) {
groups_oauth2_username_alter($name, $oauth2_account);
}
$in_username_alter = FALSE;
}
}

View File

@ -0,0 +1,7 @@
name = Groups OAuth2 Profile Picture
description = "Profile Picture support for Groups OAuth2 authentication"
core = 7.x
package = Groups - Authentication
version = "7.x-1.0"
project = "groups"

View File

@ -0,0 +1,13 @@
<?php
/**
* Implements hook_install().
*
* Creates profile-images directory under site/default/files.
*/
function groups_oauth2_picture_install() {
$destination = 'public://profile-images';
if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
throw new Exception("Unable to create directory $destination.");
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Implements hook_oauth2_user_save().
*
* Fetch profile picture from oauth2 provider and link it to
* Drupal user account.
*
* @param $account
* A user account array.
* @param $userinfo
* A user profile returned by oauth2 provider.
* @return
* None.
*/
function groups_oauth2_picture_oauth2_user_save(&$account, $userinfo) {
if (variable_get('oauth2_fetch_profile_picture', FALSE)) {
$image_path = _groups_oauth2_get_profile_picture_filename($userinfo->picture);
$picture_result = drupal_http_request($userinfo->picture);
$picture_file = file_save_data($picture_result->data, $image_path, FILE_EXISTS_REPLACE);
$image_info = image_get_info($image_path);
$file = new StdClass();
$validators = array(
'file_validate_is_image' => array(),
);
$errors = file_validate($picture_file, $validators);
if (empty($errors)) {
$picture_file->uid = $account->uid;
$picture_file = file_save($picture_file);
file_usage_add($picture_file, 'user', 'user', $account->uid);
db_update('users')
->fields(array(
'picture' => $picture_file->fid,
))
->condition('uid', $account->uid)
->execute();
}
}
$account->data['oauth2_picture'] = $userinfo->picture;
user_save($account);
}

View File

@ -49,7 +49,7 @@ function openstack_bootstrap_links__commons_utility_links(&$variables) {
$variables['attributes']['class'][] = 'dropdown-menu-right';
$output = '<div class="btn-group">';
$output .= '<button class="btn dropdown-toggle" type="button" data-toggle="dropdown"><span class="glyphicon glyphicon-user"></span>
'.$GLOBALS['user']->name.'<span class="caret"></span>
'.format_username($user).'<span class="caret"></span>
</button>';
$output .= theme_links($variables);
$output .= '</div>';