WP Simple Azure AD Login

I have been searching every now and then a plugin that would easily and freely integrate WordPress to Azure AD (and O365), but the ones I have found from WordPress plugin register have not been suitable for my use for one reason or another: https://wordpress.org/plugins/tags/azure-ad/

There seems to be one that looks promising but it has not been registered to WordPress plugins yet, so it is a manual installation like the one I have created, but it seems to have more features: https://github.com/psignoret/aad-sso-wordpress

However, since I created couple of weeks ago my own example code how to communicate to Azure AD with PHP, I concluded that it might be interesting to see what it takes to create a WordPress plugin out of that. I must warn that the code I’m about to show you is not the nicest one and basically this just enables Azure AD authentication for already registered users if WordPress username and Azure AD userPrincipalName match.

WordPress Login form with added Azure Active Directory login button.

Here is the code that WordPress actually considers as a plugin: (wp-simple-azure-ad-login.php)

<?php
/*
Plugin Name: WP Simple Azure AD Login
Plugin URI: https://www.sipponen.com/archives/4149
Description: Adds Azure AD based authentication to WP via Oauth2
Version: 0.1
License: The MIT License
License URI: https://opensource.org/licenses/MIT
Author: Sami Sipponen
Author URI: https://www.sipponen.com/

Copyright 2020 Sami Sipponen <sami@sipponen.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

function login_page_add_aad_form()
{
    global $redirect_to;
    session_name("simple_azure_ad_autentication");
    session_start();
    $_SESSION = get_option('wp_simple_azure_ad_login_option_name');
    $_SESSION["admin_email"] = get_option('admin_email');
?>
    <script type="text/javascript">
        var loginDiv = document.getElementById('login');
        var aadForm = '</form><form  name="aad-loginform" id="aad-loginform" action="<?php echo esc_url(site_url('wp-content/plugins/wp-simple-azure-ad-login/login.php', 'login_post')); ?>" method="post">\
    <p style="margin-top: 20px; margin-bottom: 20px; font-weight: bold;">Single Sign On with Azure Active Directory:</p>\
    <p class="submit"><input name="aad-submit" id="aad-submit" class="button button-primary button-large" type="submit" value="Login with Azure AD"></p>\
    <input type="hidden" name="redirect_to" value="<?php echo esc_url($redirect_to); ?>">\
    <input type="hidden" name="testcookie" value="1">\
    <input type="hidden" name="wp-login-url" value="<?php echo esc_url(site_url('wp-login.php', 'login_post')); ?>">\
    </form>';
        loginDiv.innerHTML = loginDiv.innerHTML.replace('</form>', aadForm);
    </script>
<?php
}

function simple_azure_ad_autentication($user, $username, $password)
{
    if (isset($_POST["aad-submit"])) {
        session_name("simple_azure_ad_autentication");
        session_start();
        if ($_SESSION["aad-logon-ok"]) {
            $userobj = new WP_User();
            $user = $userobj->get_data_by('login', $_SESSION["userPrincipalName"]);
            $user = new WP_User($user->ID);

            //Cleanups
            $_SESSION = array();
            session_destroy();
            remove_action('authenticate', 'wp_authenticate_username_password', 20);

            if ($user->ID == 0) {
                $user = new WP_Error('denied', __("ERROR: Not a valid user for this system"));
                //Another possibility would be to create the user here
                //We could for example check AAD user group membership and add WP user role according to those
            } else {
                return ($user);
            }
        } else {
            $_SESSION = array();
            session_destroy();
        }
    }
}

add_action('login_footer', 'login_page_add_aad_form');
add_filter('authenticate', 'simple_azure_ad_autentication', 10, 3);

include __DIR__ . '/admin.php';
?>

Here is the login code based on my earlier example: (login.php)

<?php
//This login script is based on Sami Sipponen's Simple Azure Oauth2 Example with PHP:
//https://www.sipponen.com/archives/4024

session_name("simple_azure_ad_autentication");
session_start();

function errorhandler($input)
{
    if (!empty($_SESSION["email_errors"])) {
        $output = "PHP Session ID:    " . session_id() . PHP_EOL;
        $output .= "Client IP Address: " . getenv("REMOTE_ADDR") . PHP_EOL;
        $output .= "Client Browser:    " . $_SERVER["HTTP_USER_AGENT"] . PHP_EOL;
        $output .= PHP_EOL;
        ob_start();  //Start capturing the output buffer
        var_dump($input);  //This is not for debug print, this is to collect the data for the email
        $output .= ob_get_contents();  //Storing the output buffer content to $output
        ob_end_clean();  //While testing, you probably want to comment the next row out
        mb_send_mail($_SESSION["admin_email"], "Your Azure AD Oauth2 script faced an error!", $output, "X-Priority: 1\nContent-Transfer-Encoding: 8bit\nX-Mailer: PHP/" . phpversion());
    }
    exit;
}

if (isset($_POST["wp-login-url"]) and isset($_POST["aad-submit"])) {  //Real authentication part begins
    $_SESSION["post_vars"] = $_POST;
    //First stage of the authentication process; This is just a simple redirect (first load of this page)
    $url = "https://login.microsoftonline.com/" . $_SESSION["tenant_id"] . "/oauth2/v2.0/authorize?";
    $url .= "state=" . session_id();  //This at least semi-random string is likely good enough as state identifier
    $url .= "&scope=User.Read";  //This scope seems to be enough, but you can try "&scope=profile+openid+email+offline_access+User.Read" if you like
    $url .= "&response_type=code";
    $url .= "&approval_prompt=auto";
    $url .= "&client_id=" . $_SESSION["client_id"];
    $url .= "&redirect_uri=" . urlencode($_SESSION["redirect_uri"]);
    header("Location: " . $url);  //So off you go my dear browser and welcome back for round two after some redirects at Azure end

} elseif (isset($_GET["error"])) {  //Second load of this page begins, but hopefully we end up to the next elseif section...
    echo "Some error occurred.";
    $_SESSION["client_secret"] = "<hidden-from-email>";
    errorhandler(array("Description" => "Error received at the beginning of second stage.", "\$_GET[]" => $_GET, "\$_SESSION[]" => $_SESSION));
} elseif (strcmp(session_id(), $_GET["state"]) == 0) {  //Checking that the session_id matches to the state for security reasons
    //And now the browser has returned from its various redirects at Azure side and carrying some gifts inside $_GET

    //Verifying the received tokens with Azure and finalizing the authentication part
    $content = "grant_type=authorization_code";
    $content .= "&client_id=" . $_SESSION["client_id"];
    $content .= "&redirect_uri=" . urlencode($_SESSION["redirect_uri"]);
    $content .= "&code=" . $_GET["code"];
    $content .= "&client_secret=" . urlencode($_SESSION["client_secret"]);
    $options = array(
        "http" => array(  //Use "http" even if you send the request with https
            "method"  => "POST",
            "header"  => "Content-Type: application/x-www-form-urlencoded\r\n" .
                "Content-Length: " . strlen($content) . "\r\n",
            "content" => $content
        )
    );
    $context  = stream_context_create($options);
    $json = file_get_contents("https://login.microsoftonline.com/" . $_SESSION["tenant_id"] . "/oauth2/v2.0/token", false, $context);
    if ($json === false) errorhandler(array("Description" => "Error received during Bearer token fetch.", "PHP_Error" => error_get_last(), "\$_GET[]" => $_GET, "HTTP_msg" => $options));
    $authdata = json_decode($json, true);
    if (isset($authdata["error"])) errorhandler(array("Description" => "Bearer token fetch contained an error.", "\$authdata[]" => $authdata, "\$_GET[]" => $_GET, "HTTP_msg" => $options));

    //Fetching the basic user information that is likely needed by your application
    $options = array(
        "http" => array(  //Use "http" even if you send the request with https
            "method" => "GET",
            "header" => "Accept: application/json\r\n" .
                "Authorization: Bearer " . $authdata["access_token"] . "\r\n"
        )
    );
    $context = stream_context_create($options);
    $json = file_get_contents("https://graph.microsoft.com/v1.0/me", false, $context);
    if ($json === false) errorhandler(array("Description" => "Error received during user data fetch.", "PHP_Error" => error_get_last(), "\$_GET[]" => $_GET, "HTTP_msg" => $options));
    $userdata = json_decode($json, true);  //This should now contain your logged on user information
    if (isset($userdata["error"])) errorhandler(array("Description" => "User data fetch contained an error.", "\$userdata[]" => $userdata, "\$authdata[]" => $authdata, "\$_GET[]" => $_GET, "HTTP_msg" => $options));

    $wp_login = htmlspecialchars($_SESSION["post_vars"]["wp-login-url"]);
    unset($_SESSION["post_vars"]["wp-login-url"]);
?>
    <html>

    <head>
        <title>WP Simple Azure AD Login - Redirecting...</title>
    </head>

    <body onload="document.forms['loginform'].submit();">
        <form method="POST" action="<?php echo $wp_login; ?>" name="loginform" target="_top">
            <?php
            foreach ($_SESSION["post_vars"] as $key => $value) {
                echo "                <input type= \"hidden\" name=\"" . $key . "\" value=\"" . $value . "\">" . PHP_EOL;
            }
            ?>
        </form>
    </body>

    </html>
<?php
    $_SESSION = $userdata;
    $_SESSION["aad-logon-ok"] = true;

} else {
    //If we end up here, something has obviously gone wrong... Likely a hacking attempt since sent and returned state aren't matching and no $_GET["error"] received.
    echo "Hey, please don't try to hack us!";
    $_SESSION["client_secret"] = "<hidden-from-email>";
    errorhandler(array("Description" => "Likely a hacking attempt, due state mismatch or missing POST variable (wp-login-url or aad-submit).", "\$_GET[]" => $_GET, "\$_POST[]" => $_POST, "\$_SESSION[]" => $_SESSION));
}

And here is the admin page code, since basically all WP plugins should have one: (admin.php)

<?php
/*
 Generated by the WordPress Option Page generator
 at http://jeremyhixon.com/wp-tools/option-page/
 And then manually modified...
*/

class WPSimpleAzureADLogin
{
    private $wp_simple_azure_ad_login_options;

    public function __construct()
    {
        add_action('admin_menu', array($this, 'wp_simple_azure_ad_login_add_plugin_page'));
        add_action('admin_init', array($this, 'wp_simple_azure_ad_login_page_init'));
    }

    public function wp_simple_azure_ad_login_add_plugin_page()
    {
        add_options_page(
            'WP Simple Azure AD Login', // page_title
            'WP Simple Azure AD Login', // menu_title
            'manage_options', // capability
            'wp-simple-azure-ad-login', // menu_slug
            array($this, 'wp_simple_azure_ad_login_create_admin_page') // function
        );
    }

    public function wp_simple_azure_ad_login_create_admin_page()
    {
        $this->wp_simple_azure_ad_login_options = get_option('wp_simple_azure_ad_login_option_name');
        if (empty($this->wp_simple_azure_ad_login_options['redirect_uri'])) $this->wp_simple_azure_ad_login_options['redirect_uri'] = plugin_dir_url(__FILE__) . "login.php";
?>

        <div class="wrap">
            <h2>WP Simple Azure AD Login</h2>
            <p>This plugin enables Azure AD Single Sign On functionality for your WordPress. To do that it needs application registration to Azure AD and related configurations need to match at both ends.</p>
            <?php settings_errors(); ?>

            <form method="post" action="options.php">
                <?php
                settings_fields('wp_simple_azure_ad_login_option_group');
                do_settings_sections('wp-simple-azure-ad-login-admin');
                submit_button();
                ?>
            </form>
        </div>
<?php
    }

    public function wp_simple_azure_ad_login_page_init()
    {
        register_setting(
            'wp_simple_azure_ad_login_option_group', // option_group
            'wp_simple_azure_ad_login_option_name', // option_name
            array($this, 'wp_simple_azure_ad_login_sanitize') // sanitize_callback
        );

        add_settings_section(
            'wp_simple_azure_ad_login_setting_section', // id
            'Settings', // title
            array($this, 'wp_simple_azure_ad_login_section_info'), // callback
            'wp-simple-azure-ad-login-admin' // page
        );

        add_settings_field(
            'client_id', // id
            'Azure Application (client) ID', // title
            array($this, 'client_id_callback'), // callback
            'wp-simple-azure-ad-login-admin', // page
            'wp_simple_azure_ad_login_setting_section' // section
        );

        add_settings_field(
            'tenant_id', // id
            'Azure AD Tenant ID', // title
            array($this, 'tenant_id_callback'), // callback
            'wp-simple-azure-ad-login-admin', // page
            'wp_simple_azure_ad_login_setting_section' // section
        );

        add_settings_field(
            'client_secret', // id
            'Client Secret (generated at Azure)', // title
            array($this, 'client_secret_callback'), // callback
            'wp-simple-azure-ad-login-admin', // page
            'wp_simple_azure_ad_login_setting_section' // section
        );

        add_settings_field(
            'redirect_uri', // id
            'Redirect URI (your logon page address)', // title
            array($this, 'redirect_uri_callback'), // callback
            'wp-simple-azure-ad-login-admin', // page
            'wp_simple_azure_ad_login_setting_section' // section
        );

        add_settings_field(
            'email_errors', // id
            'Send errors via email', // title
            array($this, 'email_errors_callback'), // callback
            'wp-simple-azure-ad-login-admin', // page
            'wp_simple_azure_ad_login_setting_section' // section
        );
    }

    public function wp_simple_azure_ad_login_sanitize($input)
    {
        $sanitary_values = array();
        if (isset($input['client_id'])) {
            $sanitary_values['client_id'] = sanitize_text_field($input['client_id']);
        }

        if (isset($input['tenant_id'])) {
            $sanitary_values['tenant_id'] = sanitize_text_field($input['tenant_id']);
        }

        if (isset($input['client_secret'])) {
            $sanitary_values['client_secret'] = sanitize_text_field($input['client_secret']);
        }

        if (isset($input['redirect_uri'])) {
            $sanitary_values['redirect_uri'] = sanitize_text_field($input['redirect_uri']);
        }

        if (isset($input['email_errors'])) {
            $sanitary_values['email_errors'] = $input['email_errors'];
        }

        return $sanitary_values;
    }

    public function wp_simple_azure_ad_login_section_info()
    {
    }

    public function client_id_callback()
    {
        printf(
            '<input class="regular-text" type="text" name="wp_simple_azure_ad_login_option_name[client_id]" id="client_id" value="%s">',
            isset($this->wp_simple_azure_ad_login_options['client_id']) ? esc_attr($this->wp_simple_azure_ad_login_options['client_id']) : ''
        );
    }

    public function tenant_id_callback()
    {
        printf(
            '<input class="regular-text" type="text" name="wp_simple_azure_ad_login_option_name[tenant_id]" id="tenant_id" value="%s">',
            isset($this->wp_simple_azure_ad_login_options['tenant_id']) ? esc_attr($this->wp_simple_azure_ad_login_options['tenant_id']) : ''
        );
    }

    public function client_secret_callback()
    {
        printf(
            '<input class="regular-text" type="text" name="wp_simple_azure_ad_login_option_name[client_secret]" id="client_secret" value="%s">',
            isset($this->wp_simple_azure_ad_login_options['client_secret']) ? esc_attr($this->wp_simple_azure_ad_login_options['client_secret']) : ''
        );
    }

    public function redirect_uri_callback()
    {
        printf(
            '<input class="regular-text" type="text" name="wp_simple_azure_ad_login_option_name[redirect_uri]" id="redirect_uri" value="%s">',
            isset($this->wp_simple_azure_ad_login_options['redirect_uri']) ? esc_attr($this->wp_simple_azure_ad_login_options['redirect_uri']) : ''
        );
    }

    public function email_errors_callback()
    {
        printf(
            '<input type="checkbox" name="wp_simple_azure_ad_login_option_name[email_errors]" id="email_errors" value="email_errors" %s> <label for="email_errors">Send errors to administrator\'s email address.</label>',
            (isset($this->wp_simple_azure_ad_login_options['email_errors']) && $this->wp_simple_azure_ad_login_options['email_errors'] === 'email_errors') ? 'checked' : ''
        );
    }
}
if (is_admin())
    $wp_simple_azure_ad_login = new WPSimpleAzureADLogin();

/* 
 * Retrieve this value with:
 * $wp_simple_azure_ad_login_options = get_option( 'wp_simple_azure_ad_login_option_name' ); // Array of All Options
 * $client_id = $wp_simple_azure_ad_login_options['client_id']; // Azure Application (client) ID
 * $tenant_id = $wp_simple_azure_ad_login_options['tenant_id']; // Azure AD Tenant ID
 * $client_secret = $wp_simple_azure_ad_login_options['client_secret']; // Client Secret (generated at Azure)
 * $redirect_uri = $wp_simple_azure_ad_login_options['redirect_uri']; // Redirect URI (your logon page address)
 * $email_errors = $wp_simple_azure_ad_login_options['email_errors']; // Send errors to administrator\'s email address
 */
Settings page

It is likely that I will not develop this plugin any further, so unless someone else does it, you are likely better off with aad-sso-wordpress plugin I mentioned at the beginning of this post. The little amount what I tested my code, it seems to work and hopefully there are no bigger security issues with it (or compatibility issues). Related to Azure AD configurations needed, please see my earlier example, since the same instructions apply. Please drop me an email if you find some bigger considerations about my quickly handcrafted AAD plugin. And please do remember that you use this software at your own risk, I’m not taking any responsibility of the provided code.

You can also download my plugin as a zip file if you like.

Simple Azure AD Oauth2 example with PHP

I started to study how to do a simple Azure AD Oauth2 authentication without the need to maintain the session and in overall no need for additional bells and whistles. First I of course ended up testing magium/active-directory , but it is a huge 10Mb package of code that is hard to validate and you need Composer to even install it properly that adds even more overhead that I just didn’t want to carry. After doing enough googling and concluding that there is no good example for PHP how to implement a single page authentication with Oauth2 like specified here, I decided to write my own targeted especially for Azure AD integration.

Before going into my example, I need to state that there are nice examples for many programming languages available from Microsoft and other tools and documentation that I find useful:

Here is my heavily commented example code:

<?php
//This login script is based on Sami Sipponen's Simple Azure Oauth2 Example with PHP:
//https://www.sipponen.com/archives/4024

session_start();  //Since you likely need to maintain the user session, let's start it an utilize it's ID later
error_reporting(-1);  //Remove from production version
ini_set("display_errors", "on");  //Remove from production version

//Configuration, needs to match with Azure app registration
$client_id = "00000000-0000-0000-0000-000000000000";  //Application (client) ID
$ad_tenant = "00000000-0000-0000-0000-000000000000";  //Azure Active Directory Tenant ID, with Multitenant apps you can use "common" as Tenant ID, but using specific endpoint is recommended when possible
$client_secret = "azure-app-secret-from-your-app-registration";  //Client Secret, remember that this expires someday unless you haven't set it not to do so
$redirect_uri = "https://your-server.your-domain.com/this-page.php";  //This needs to match 100% what is set in Azure
$error_email = "your.email@your-domain.com";  //If your php.ini doesn't contain sendmail_from, use: ini_set("sendmail_from", "user@example.com");

function errorhandler($input, $email)
{
  $output = "PHP Session ID:    " . session_id() . PHP_EOL;
  $output .= "Client IP Address: " . getenv("REMOTE_ADDR") . PHP_EOL;
  $output .= "Client Browser:    " . $_SERVER["HTTP_USER_AGENT"] . PHP_EOL;
  $output .= PHP_EOL;
  ob_start();  //Start capturing the output buffer
  var_dump($input);  //This is not for debug print, this is to collect the data for the email
  $output .= ob_get_contents();  //Storing the output buffer content to $output
  ob_end_clean();  //While testing, you probably want to comment the next row out
  mb_send_mail($email, "Your Azure AD Oauth2 script faced an error!", $output, "X-Priority: 1\nContent-Transfer-Encoding: 8bit\nX-Mailer: PHP/" . phpversion());
  exit;
}

if (isset($_GET["code"])) echo "<pre>";  //This is just for easier and better looking var_dumps for debug purposes

if (!isset($_GET["code"]) and !isset($_GET["error"])) {  //Real authentication part begins
  //First stage of the authentication process; This is just a simple redirect (first load of this page)
  $url = "https://login.microsoftonline.com/" . $ad_tenant . "/oauth2/v2.0/authorize?";
  $url .= "state=" . session_id();  //This at least semi-random string is likely good enough as state identifier
  $url .= "&scope=User.Read";  //This scope seems to be enough, but you can try "&scope=profile+openid+email+offline_access+User.Read" if you like
  $url .= "&response_type=code";
  $url .= "&approval_prompt=auto";
  $url .= "&client_id=" . $client_id;
  $url .= "&redirect_uri=" . urlencode($redirect_uri);
  header("Location: " . $url);  //So off you go my dear browser and welcome back for round two after some redirects at Azure end

} elseif (isset($_GET["error"])) {  //Second load of this page begins, but hopefully we end up to the next elseif section...
  echo "Error handler activated:\n\n";
  var_dump($_GET);  //Debug print
  errorhandler(array("Description" => "Error received at the beginning of second stage.", "\$_GET[]" => $_GET, "\$_SESSION[]" => $_SESSION), $error_email);
} elseif (strcmp(session_id(), $_GET["state"]) == 0) {  //Checking that the session_id matches to the state for security reasons
  echo "Stage2:\n\n";  //And now the browser has returned from its various redirects at Azure side and carrying some gifts inside $_GET
  var_dump($_GET);  //Debug print

  //Verifying the received tokens with Azure and finalizing the authentication part
  $content = "grant_type=authorization_code";
  $content .= "&client_id=" . $client_id;
  $content .= "&redirect_uri=" . urlencode($redirect_uri);
  $content .= "&code=" . $_GET["code"];
  $content .= "&client_secret=" . urlencode($client_secret);
  $options = array(
    "http" => array(  //Use "http" even if you send the request with https
      "method"  => "POST",
      "header"  => "Content-Type: application/x-www-form-urlencoded\r\n" .
        "Content-Length: " . strlen($content) . "\r\n",
      "content" => $content
    )
  );
  $context  = stream_context_create($options);
  $json = file_get_contents("https://login.microsoftonline.com/" . $ad_tenant . "/oauth2/v2.0/token", false, $context);
  if ($json === false) errorhandler(array("Description" => "Error received during Bearer token fetch.", "PHP_Error" => error_get_last(), "\$_GET[]" => $_GET, "HTTP_msg" => $options), $error_email);
  $authdata = json_decode($json, true);
  if (isset($authdata["error"])) errorhandler(array("Description" => "Bearer token fetch contained an error.", "\$authdata[]" => $authdata, "\$_GET[]" => $_GET, "HTTP_msg" => $options), $error_email);

  var_dump($authdata);  //Debug print

  //Fetching the basic user information that is likely needed by your application
  $options = array(
    "http" => array(  //Use "http" even if you send the request with https
      "method" => "GET",
      "header" => "Accept: application/json\r\n" .
        "Authorization: Bearer " . $authdata["access_token"] . "\r\n"
    )
  );
  $context = stream_context_create($options);
  $json = file_get_contents("https://graph.microsoft.com/v1.0/me", false, $context);
  if ($json === false) errorhandler(array("Description" => "Error received during user data fetch.", "PHP_Error" => error_get_last(), "\$_GET[]" => $_GET, "HTTP_msg" => $options), $error_email);
  $userdata = json_decode($json, true);  //This should now contain your logged on user information
  if (isset($userdata["error"])) errorhandler(array("Description" => "User data fetch contained an error.", "\$userdata[]" => $userdata, "\$authdata[]" => $authdata, "\$_GET[]" => $_GET, "HTTP_msg" => $options), $error_email);

  var_dump($userdata);  //Debug print
} else {
  //If we end up here, something has obviously gone wrong... Likely a hacking attempt since sent and returned state aren't matching and no $_GET["error"] received.
  echo "Hey, please don't try to hack us!\n\n";
  echo "PHP Session ID used as state: " . session_id() . "\n";  //And for production version you likely don't want to show these for the potential hacker
  var_dump($_GET);  //But this being a test script having the var_dumps might be useful
  errorhandler(array("Description" => "Likely a hacking attempt, due state mismatch.", "\$_GET[]" => $_GET, "\$_SESSION[]" => $_SESSION), $error_email);
}
echo "\n<a href=\"" . $redirect_uri . "\">Click here to redo the authentication</a>";  //Only to ease up your tests
?>

The code above for sure is not implementing everything defined by Oauth2 standard, but it seems to do its job. If you plan to use it for something else than just testing, please remove the unnecessary var_dumps and echo “<pre>” from the beginning of the script and of course add the things needed for your application.

The final array $userdata will look like this: (copy & paste from Microsoft’s Graph Explorer test account output)

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
    "businessPhones": [
        "+1 412 555 0109"
    ],
    "displayName": "Megan Bowen",
    "givenName": "Megan",
    "jobTitle": "Auditor",
    "mail": "MeganB@M365x214355.onmicrosoft.com",
    "mobilePhone": null,
    "officeLocation": "12/1110",
    "preferredLanguage": "en-US",
    "surname": "Bowen",
    "userPrincipalName": "MeganB@M365x214355.onmicrosoft.com",
    "id": "48d31887-5fad-4d73-a9f5-3c356e68a038"
}

At Azure AD you need to make an app registration that matches to your application. Here is how to do that:

  1. Just open https://aad.portal.azure.com or https://portal.azure.com and open “Azure Active Directory” there.
  2. From left menu under Manage section open “App registrations”.
  3. Next click “+ New registration” from the top of the view you just opened.
  4. Now you can enter the name of your application and select is your app Single tenant or Multitenant app. And this selection of course depends how publicly you mean to share this application. If you are unsure, select Single tenant to be at the safe side. You can change this later if needed from “Authentication” page. The most important thing in this view is to give the Redirect URI to your authentication page (the page that contains my example code). This needs to be secured with HTTPS, so don’t even bother trying with just http://, since it will not work. However this URL does not need to be publicly available since it is accessed by your browser, not by Azure itself, so even localhost will work as long as you have https:// connection to it.
  5. Since you now have the app registration created and you are in “Overview” page, please copy your Application (client) ID and Directory (tenant) ID, since you will need those with my example code.
  6. And now you are almost ready! There is only the one final thing to do, which is creating the “Client secret” for your registered app. Click “Certificates & secrets” from left menu and from the page that opens click “+New client secret” button. Now you can give some description if you like, but the main thing here is to select how long your secret is valid. After you have selected, just click “Add” button and there you have it. Please note that Azure will not show the secret to you afterwards, so you need to copy it now to a safe place or to create a new one if you lost it.

Since I’m too lazy to take screenshots and blurring out the sensitive content, here is Microsoft’s documentation how to create the App registration, this covers bullets 1-5 from above: https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-ap
The only thing this guide does not show is the Client Secret creation, but that is only couple of clicks and instructed in the last bullet (6).

If you need to get also user groups listed, that is rather easy with:

  $options = array(
    "http" => array( //Use "http" even if you send the request with https
      "method" => "GET",
      "header" => "Accept: application/json\r\n" .
        "Authorization: Bearer " . $authdata["access_token"] . "\r\n"
    )
  );
  $context = stream_context_create($options);
  $json = file_get_contents("https://graph.microsoft.com/v1.0/me/memberOf", false, $context);
  if ($json === false) errorhandler(array("Description" => "Error received during user group data fetch.", "PHP_Error" => error_get_last(), "\$_GET[]" => $_GET, "HTTP_msg" => $options), $error_email);
  $groupdata = json_decode($json, true);  //This should now contain your logged on user memberOf (groups) information
  if (isset($groupdata["error"])) errorhandler(array("Description" => "Group data fetch contained an error.", "\$groupdata[]" => $groupdata, "\$authdata[]" => $authdata, "\$_GET[]" => $_GET, "HTTP_msg" => $options), $error_email);

Locate the above code for example after $userdata section. Please notice that you will need to add slightly more permissions to your app registration or else you will get “empty” array as return:

  1. Open your app registration and API permissions page.
  2. Click “+Add a permission” button.
  3. Select “Microsoft Graph” and then “Delegated permissions”.
  4. Next you need to expand “Group” section and select “Group.Read.All” permission. Click “Add permissions” button from the bottom.
  5. The final thing needed is to grant admin consent to your application, where you obviously need high enough permissions to your Azure Active Directory to do so. Note that the admin consent is only granted for this specific permission you added at step 4.

When mapping the user groups to your application, likely the best attributes for that are onPremisesSecurityIdentifier or securityIdentifier. At least these should not break up easily during the time. If you need to see an example output from https://graph.microsoft.com/v1.0/me/memberOf, just use the Graph Explorer to get it.

Quick way to inject the login data into your $_SESSION variables:

  //This replaces all previous data in your session, but during login process you likely want to do that 
  $_SESSION = $userdata;
  $_SESSION["oauth_bearer"] = $authdata["access_token"];
  $_SESSION["groups"] = $groupdata; 

More information about PHP session variable from:
https://www.php.net/manual/en/reserved.variables.session.php

12.08.2021 Update:

Even there is usually no issues at all to execute the login process just by redirecting the browser to the login page, sometimes the user session with AAD is somehow broken and needs to be refreshed. At such case the login process ends up to bearer token fetch error even everything should be pretty much ok… It starts to work if the user visits some O365 page like portal.office.com or closes the browser and opens it again. I have not figured out why this happens, but I would recommend using AAD to trigger the login process instead of your own login page, which should mitigate the issue. So instead of using the URL of your own login page to trigger the login process, try to use this:

https://account.activedirectory.windowsazure.com/applications/signin/<YOUR_APP_ID_HERE_=_$client_id>?tenantId=<YOUR_TENANT_ID_HERE_=_$ad_tenant>

For above to work you need to specify the full URL of your own login page to Azure AD application registration settings. This can be done from “Branding” blade at portal with “Home page URL” field. Please note that instead of just specifying the homepage you really need to use full URL to your actual login page.

The additional benefit of doing this is that your application will be working from portal.office.com in case someone clicks it from there:

https://www.office.com/apps?auth=2

25.03.2022 Update:

In case you need some other attributes from the user object that are not coming with “/me” by default, you can use $select argument at line 74 to select the properties you really need:

https://graph.microsoft.com/v1.0/me?$select=givenName,surname,userPrincipalName,country

And please remember that the Graph Explorer is your friend to find out what attributes are available, especially if you login to it with your own account so that you can see your own user account data with it. There are quite a lot of properties that you can get: https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties (and your own custom attributes on top of these if you have some)

Disclaimer and Licensing:

There is no license for this code, since it has been meant just as an example. Without modifications and embedding it as part of your software this does not bring much value. Feel free to utilize it as you like, even commercially, but please remember that I’m not taking any responsibility of this code, meaning use at your own risk. If you find a bug or security issue with it, please drop me an email to sami@sipponen.com. In case you like the script, maybe you leave the top comments in place about the origin of the script, but that is not mandatory either.

Long silence…

Has hopefully ended, but we shall see. 😉 I have been posting my projects only to Facebook for some time, so there is some actual proof that I have not just been idling, but unfortunately those are available only to my FB friends.

Reason for this silence is that it was too much work to shrink the images to smaller size and then post those to my blog. And to mitigate this I will try now Instagram where I’m posting my updates. Instagram feed has been integrated to WordPress and you can find this from top navigation bar “Instagram Feed” link. Also I will keep my Instagram feed totally public, so you all can see what I’m posting there.

I have been working with various projects at my garage, but still not much progress with for example my DIY tractor project, which is on hiatus at the moment due other interesting things to be done. Currently I’m taking one Ford Galaxy with DOHC 2.3i 16v engine to pieces with intention to fit Sierra 4×4 oil sump + auxilary systems to it. I will be testing it at DIY test stand with the intention to do emission test before installing the ready package to my Sierra to be sure that it will also be legal. Test stand will not be anythin fancy, just a frame with wheels that can carry engine, gasoline tank, electorinics, etc. needed for engine to run. The actual installation of ready engine to my Sierra will most likely take some time, since it currently has well working 2.0i 8v DOHC engine to which I changed head gasget at the summer. But at least I have the spare engine + gearbox package ready to be bolted on when needed, or when I want to get couple of more horse powers (that might happen sooner than I initally plan if things get tempting 😀 )

But enough for now, let’s see how this Instagram thingy works and will it be easy enough so I will use it. So go and see it! 🙂

BMW tune up 2015 completed

Well, it’s now ready:

IMG_20150716_193445_edit

 

Today’s activities were:

  • Oil change
  • New side indicator lights
  • Gear selector light, red led
  • New rear register plate lights (led)
  • Fixing roof window seal with Sikaflex 521UV
  • Some final paint job
  • Voltage meter back-light changed to red led
  • Trunk  upholstery reassembled
  • Right front fender molding found from local scrapyard and installed (it was missing earlier)
  • Alternator/water-pump belt and power steering belt tightened

What this car really needs are two set of alloys, since those normal rims are really ugly.  But that probably will be taken care of…

Tomorrow I will vacuum clean the interior quickly and then return to work with the camping trailer.

New front brake pads to BWM

Maybe it was already the time to change those?

IMG_20150715_175253

I have never before seen as tight fit for the new brake pads to brake caliber like this BMW had… If the pads would have been 0,5mm thicker they would not be possible to install. Before starting to install the new pads I cleaned all the surfaces from the caliber and the slider where the pads are touching with a file to avoid stiction (+polished and greased sliding tenons). They really needed to squeeze together until the outer pad slipped in to it’s place (almost uncomfortable much). But I guess that’s value for the money since there is now absolutely the maximum amount of wearing surface available.

Tasks for tomorrow:

  • Oil change
  • New side indicator lights
  • Gear selector light (I might also change the voltage meter light to LED)
  • New rear register plate lights
  • Fixing roof window seal with Sikaflex 521UV
  • Some final paint job

Friday I will assemble the trunk upholstery back and that’s it, BWM tune up for this summer is ready.

One thing I forgot to mention is that I installed modern Volvo windshield washing fluid nozzles to replace BMW original ones. I used the original ones adapter to fit the new ones by grinding them flat first and then carving proper hole for Volvo nozzes (which are slightly smaller). Reason for this change was that Volvo nozzles spray the water really nicely (only if you use original Volvo nozzles, the aftermarket ones will not work). The original BWM nozzles just peed the fluid with four pointy spouts and that’s pretty old fashion and lame way which does also cause extensive wear of the glass because the wipers are grinding it partly dry.