Category Archives: The Wonderful World of the Internet

Things related to the Internet one way or another.

What Microsoft Subscription Licenses You Have?

In case you have ever wondered how to see with normal user permissions what licenses that user does have, then wonder no more since this link shoud show it:

https://portal.office.com/account/?ref=Harmony#subscriptions

I tried searching and found this document from Microsoft, but unfortunately it won’t work nowadays as it states:

https://support.microsoft.com/en-us/office/what-microsoft-365-business-product-or-license-do-i-have-f8ab5e25-bf3f-4a47-b264-174b1ee925fd

However the first link I posted does the trick for my needs and hopefully it works for you too.

PHP and Null Coalescing Operator

I’m not an actual programmer so not writing any code as for my daily job, but at past I have written quite many rows of PHP. Lately I have worked with one older website update from PHP7.x to PHP8.x and oh boy how many problems the new more strict NULL value handling logic caused. Or let’s say that it wouldn’t be a problem for PHP8, but seems that when PHP9 comes, it would have been a major issue.

Nevertheless, I decided that I will fix the issues now, little by little so that this problem is not carried any further. After making a huge amount of corrections with isset() function and if/else statements to handle NULL values, I started to think that what if I would write a function ensure_value($_GET['something'], 'default value') that would ensure I always would have a value what PHP8 expects, not NULL.

But then I started to do a bit googling and almost gave up, until I hit to this very-very-good and easy to read article:

Ternary Operator in PHP | How to use the PHP Ternary Operator | Codementor

Like you can see, it has been posted about 5 years ago and I truly hope I would have seen&learnt this back then. 😀 But as I am not a programmer I haven’t kept myself that up to date with PHP and haven’t had this issue with such a magnitude before. So this was a new great learning to have now.

For me the last example of that article is the most valuable one, so in case someone seeks a solution for easy NULL value handling, this is much better than writing your own ensure_value() function:

$something= $_GET[‘something’] ?? ‘default value’;

You can use the same without assigning it into a variable, so as you would expect, it works with functions as well, like:

echo $_GET['something'] ?? 'default value';

Or other functions like implode(), here is an example that prints comma separated list out of an array:

echo implode(', ', $usually_array ?? array()), PHP_EOL;

Above ensures implode() will always receive an array, even an empty one, but it will be happy with that as well. If you wonder what that last “, PHP_EOL” does, it just adds a new line (enter) to the HTML source code, that in some cases makes it easier to read if you need to do some debugging later. I always try to generate as readable HTML as possible, and therefore I try to inject new lines with PHP when it makes sense from HTML point of view, even it is not visible for the end user.

30.7.2024 update: You can even chain multiple Null Coalescing Operators together:

$your_var = $_GET['your_var'] ?? $_POST['your_var'] ?? '';

Above will first try to get your_var from GET inputs, if not succeeding, then trying to get that from POST inputs and very last uses empty string if POST input was not available.

2.10.2024 update: You need to use parenthesis () with Null Coalescing Operator when joining string parts together like first row shows:

$str = 'Hello ' . ($name ?? 'Anonymous') . ' and welcome!';
$str = 'Hello ' . $name ?? 'Anonymous' . ' and welcome!';

If you don’t use parenthesis, then the Null Coalescing Operator will not work as you expect, meaning checking only the variable. Second row would return only “Anonymous and welcome!” if $name is NULL.

Dehydrating OneDrive Files Automatically

Getting OneDrive files dehydrated can be a challenge, especially with multiuser environments like AVD or legacy RDS. For some reason Microsoft haven’t provided proper tooling for this and finding how to do it can also be challenging.

There is for example this tool that can do the cleanups for you:

OneDrive Clean-Up for Azure Virtual Desktop | ITProCloud Blog

But if you are as paranoid as I, you won’t easily trust to binary blobs that someone else has created and sharing at Internet. Note that I’m not in any way saying that above site cannot be trusted, very likely it can be.

I have come up with a PowerShell script, main part of that I got from a collegue (likely he got that somewhere else) and other part that I improved with help of ChatGPT (since I’m a bit lazy when I can utilize tools). The script runs inside the user session and is very suitable to be used for example as logoff script:

$Code = @'

using System;[FlagsAttribute]
public enum FileAttributesEx : uint {
    Readonly = 0x00000001,
    Hidden = 0x00000002,
    System = 0x00000004,
    Directory = 0x00000010,
    Archive = 0x00000020,
    Device = 0x00000040,
    Normal = 0x00000080,
    Temporary = 0x00000100,
    SparseFile = 0x00000200,
    ReparsePoint = 0x00000400,
    Compressed = 0x00000800,
    Offline = 0x00001000,
    NotContentIndexed = 0x00002000,
    Encrypted = 0x00004000,
    IntegrityStream = 0x00008000,
    Virtual = 0x00010000,
    NoScrubData = 0x00020000,
    EA = 0x00040000,
    Pinned = 0x00080000,
    Unpinned = 0x00100000,
    U200000 = 0x00200000,
    RecallOnDataAccess = 0x00400000,
    U800000 = 0x00800000,
    U1000000 = 0x01000000,
    U2000000 = 0x02000000,
    U4000000 = 0x04000000,
    U8000000 = 0x08000000,
    U10000000 = 0x10000000,
    U20000000 = 0x20000000,
    U40000000 = 0x40000000,
    U80000000 = 0x80000000
}
'@

# Check if the type 'FileAttributesEx' already exists before adding it
if (-not ([System.Management.Automation.PSTypeName]'FileAttributesEx').Type) {
    Add-Type $Code
}

# Escape OneDrive path to handle special characters
$escapedOneDrivePath = [System.Management.Automation.WildcardPattern]::Escape($env:OneDrive)

# Create a DirectoryInfo object for the OneDrive path
$oneDriveDirectory = New-Object System.IO.DirectoryInfo $escapedOneDrivePath

# Get list of files
$files = $oneDriveDirectory.GetFiles("*.*", "AllDirectories") | Where-Object {!$_.PSIsContainer}

foreach ($file in $files) {
    # Get file attributes
    $attributes = [FileAttributesEx]$file.Attributes

    # Check file attributes
    if (($attributes -band [FileAttributesEx]::Unpinned) -eq 0 -or
        ($attributes -band [FileAttributesEx]::Offline) -eq 0 -and
        ($attributes -band [FileAttributesEx]::RecallOnDataAccess) -eq 0) {
        # Apply changes to the file
        attrib.exe $file.FullName +U -P /S
    }
}

Nothing prevents you to use Group Policy and schedule it as a task that runs for users, but personally I think it is good enough to run it only during the logoff. This way it will not affect to the user session at all since it happens when user has stopped his/her work.

To set the Group Policy logoff script just open GPMC and select existing or create new GPO. In the User Configuration side go to Policies -> Windows Settings -> Scripts (Logon/Logoff). Open the Logoff settings and add the script to PowerShell tab. If you aren’t using SYSVOL for storage location and using a normal file share instead, remember to give suitable user group or Authenticated Users read+execute permissions to it.

Please note that running PowerShell scripts as logon or logoff scripts can be tricky unless you use code signing or allow PowerShell scripts to run as ExecutionPolicy set to Bypass. However bypassing the execution policy is not advisable… If you cannot use code signing and run it as a trusted script, you can create legacy .cmd file as wrapper for PowerShell which I think is slightly better than just allowing all unsigned scripts to run. Your wrapper.cmd needs to have only one row:

powershell -WindowStyle Hidden -NonInteractive -NoProfile -ExecutionPolicy Bypass -File "OneDrive_Dehydration_Single_User.ps1"

You can include full UNC path of the PS1 file in the script. And if you use wrapper, add it to Scripts tab of Logoff scripts window, not PowerShell Scripts.

Also remember that User Configuration GPOs need to be located to the same OU where user accounts are, unless you are setting GPO Loopback Processing as Merge or Replace mode. But since you likely are a professional admin, you have already done this and likely set this as “replace” to prevent GPOs from user account OU to hassle with your precious AVD/RDS environment, meaning only user policies from the same OU as the servers are processed.

Microsoft’s documentation about Logon/Logoff scripts:

https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn789196(v=ws.11)

Why not just using Storage Sense for this?

Well… The simple answer is that it does not work well with multiuser environments. You of course want to have Storage Sense settings configured so that some temporary file cleanups and similar would happen, but for dehydrating OneDrive files my experiences are not very great with AVD/RDS environments.

I have even tried to run it with very aggressive schedule in forced way by having a scheduled task in computer configuration running as BUILTIN\Users:

cleanmgr.exe /autocleanstoragesense /d %systemdrive%

But even the cleanup starts and looks to be running as expected when followed with Process Explorer, still the OneDrive file dehydration is not working as expected. However Storage Sense should work just fine with normal single user computers without any extra scheduled tasks if your settings are correct.

Cleaning-up Windows explorer.exe Quick Access default entries

I needed to do multiple hours research how to get Windows File Explorer (explorer.exe) Quick Access list cleaned out, since I did not want to remove it completely, but since the server was a Session Host for multiple users, I did not wanted to have libraries like Desktop, Documents, Pictures or Downloads visible.

I already earlier figured out how to remove those libraries under “This PC”, which was easy by just editing the registry with GPO and changing one key value for each entry as “Hide” instead of default “Show” like “Help Desk Survival” blog suggested:

You can hide the libraries folders from file explorer by changing the following registry

Pictures: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ FolderDescriptions\{0ddd015d-b06c-45d5-8c4c-f59713854639}\PropertyBag

Videos: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ FolderDescriptions\{35286a68-3c57-41a1-bbb1-0eae73d76c95}\PropertyBag

Downloads: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ FolderDescriptions\{7d83ee9b-2244-4e70-b1f5-5393042af1e4}\PropertyBag

Music: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ FolderDescriptions\{a0c69a99-21c8-4671-8703-7934162fcf1d}\PropertyBag

Desktop:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ FolderDescriptions\{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}\PropertyBag

Documents: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ FolderDescriptions\{f42ee2d3-909f-4907-8871-4c22fc0bf756}\PropertyBag

For each of these paths you have to change the value of the key ThisPCPolicy to “Hide”

https://helpdesksurvival.wordpress.com/2018/04/19/gpo-hide-libraries-folders-from-file-explorer/

I also had “3D Objects” library under “This PC”, so needed to find out it as well from the registry… It did not had PropertyBag branch by default, but the same “ThisPCPolicy” key like above worked for that as well with “Hide” as its value:

SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions{31C0DD25-9439-4F12-BF41-7FF4EDA38722}\PropertyBag

I truly recommend doing this type of changes as gently as possible, since there can be issues at future Windows versions othervise… Quite many blogs and articles are for example are recommending to delete the registry entries for libraries to get them removed under “This PC”, but based on my experience such things may lead problems at the future. Therefore I opted to use softer methods like properly hiding libraries under “This PC” and cleaning up Quick Access list instead of deleting, which additionally leaves the functionality available for more advanced users to use it if they like.

Cleaning the Quick Access

So how to get the Quick Access cleaned from default entries? The best post I was able find was this from “crypticus” at NTLite’s community, which actually explains how to add and remove entries: https://www.ntlite.com/community/index.php?threads/solved-automaticdestinations-edits-for-quick-access.2691/post-24032

It gave a good idea how to do the clean-up with PowerShell, which is easy to build into login script or to use as login time scheduled task which I prefer much more than login scripts. Here is an example how above post recommended to remove Pictures library:

$p=$env:USERPROFILE + '\Pictures'; $o = New-Object -Com shell.application; ($o.Namespace('shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}').Items() | Where-Object { $_.Path -like $p }).InvokeVerb('unpinfromhome')

If you want to get an idea how this works, I recommend trying these command first, which should give you a list of all the items under the Quick Access:

$o = New-Object -Com shell.application
$o.Namespace('shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}').Items()

But since I had quite specific use case that I wanted to delete all the default entries I started to combine those as “one liner”:

$o = New-Object -Com shell.application; ($o.Namespace("shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}").Items() | Where-Object { ($_.Path -like $env:USERPROFILE + "\Pictures") -or ($_.Path -like $env:USERPROFILE + "\Desktop") -or ($_.Path -like $env:USERPROFILE + "\Downloads") -or ($_.Path -like $env:USERPROFILE + "\Documents") }).InvokeVerb("unpinfromhome")

Above worked just nicely if you ran it only once, or if there were at least one of the specified libraries pinned. Othervise it produced an error about calling a method on null-valued expression… Also the command itself was quite nasty looking, so I needed to improve it. At this point I took the lazy route and gave the above to ChatGPT (3.5) and asked it to mitigate the possible error and when it did that, I continued and asked it to improve the readability of the code. Here is the outcome of that:

$o = New-Object -ComObject shell.application; ($o.Namespace("shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}").Items() | Where-Object { "$env:USERPROFILE\Pictures", "$env:USERPROFILE\Desktop", "$env:USERPROFILE\Downloads", "$env:USERPROFILE\Documents" -contains $_.Path }) | ForEach-Object { $_.InvokeVerb('unpinfromhome') }

This looked really satisfying for my needs, and did not produced any errors, no matter how many times you run it and what entries there were in Quick Access. For some reason my Windows box also had default user desktop pinned, that could be because of my earlier tests or something, but I decided to add C:\Users\Default\Desktop to be cleaned away from Quick Access as well even it should not be there at first place:

$o = New-Object -ComObject shell.application; ($o.Namespace("shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}").Items() | Where-Object { "$env:USERPROFILE\Pictures", "$env:USERPROFILE\Desktop", 'C:\Users\Default\Desktop', "$env:USERPROFILE\Downloads", "$env:USERPROFILE\Documents" -contains $_.Path }) | ForEach-Object { $_.InvokeVerb('unpinfromhome') }

So now I had the clean-up “script” ready and the next thing I needed to do was to automate it for of the all users. Like I wrote earlier, that I don’t like login scripts, I used GPO to create a scheduled task under user context that does have user logon as its trigger:

Since I did not wanted to have any actual script file to be run, I embedded the whole thing inside “-command” parameter of PowerShell. To do that I needed to escape the double quotes, so here is the whole arguments row content:

-noprofile -executionpolicy bypass -command "$o = New-Object -ComObject shell.application; ($o.Namespace(\"shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}\").Items() | Where-Object { \"$env:USERPROFILE\Pictures\", \"$env:USERPROFILE\Desktop\", 'C:\Users\Default\Desktop', \"$env:USERPROFILE\Downloads\", \"$env:USERPROFILE\Documents\" -contains $_.Path }) | ForEach-Object { $_.InvokeVerb('unpinfromhome') }"

Hopefully this eases up your work, since it took too many hours for me to figure everything out. 🙂

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 Entra ID Oauth2 example with PHP

At 2020 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 (after year 2023 known as Entra ID).

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 Entra ID 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";  //Entra ID Tenant ID, with Multitenant apps you can use "common" as Tenant ID, but using specific endpoint is recommended when possible
$client_secret = "entra-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 Entra ID
$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 Entra ID Oauth2 script faced an error!", $output, "X-Priority: 1\nContent-Transfer-Encoding: 8bit\nX-Mailer: PHP/" . phpversion());
  echo "<p>" . $input["Description"] . "</p>" . PHP_EOL;
  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 Entra ID's 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 Entra ID's 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);  //You may want to remove @ to see more details from PHP's log
  if ($json === false) {
    $error = error_get_last();
    if (str_contains(strtolower($error['message']), 'failed to open stream')) {
      errorhandler(array("Description" => "Error received during Bearer token fetch. For some reason this page was not able to connect Entra, usually this happens because of a certificate validation issue that starts to work after you have visited <a href=\"https://portal.office.com\">portal.office.com</a>.", "PHP_Error" => $error, "\$_GET[]" => $_GET, "HTTP_msg" => $options));
    } else {
      errorhandler(array("Description" => "Error received during Bearer token fetch. You probably reloaded the Entra ID login page with parameters in URL and that will not work.", "PHP_Error" => $error, "\$_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), $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);  //You may want to remove @ to see more details from PHP's log
  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 Entra ID 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 “Microsoft Entra ID” 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 Entra ID 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 Entra ID to do so (application admin RBAC role is enough).

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 Entra ID 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 Entra ID 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 need, like on-premises AD username details:

https://graph.microsoft.com/v1.0/me?$select=userPrincipalName,onPremisesSamAccountName,onPremisesDomainName

Note that you might want to check are onPremisesSamAccountName and onPremisesDomainName empty with your PHP code, so don’t trust that all Entra ID users have those attributes available.

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)

01.04.2024 Update:

Some changes made to reflect Azure AD name change as Entra ID + updated above example how to get on-prem AD details. And no, this is not april fool’s post even the day happens to be it. 🙂

02.10.2024 Update:

Slightly improved error reporting for bearer token fetch phase and mitigated PHP warning going into logs. Sometimes there seem to be a problem to validate Microsoft’s Oauth certificate that disappears when user visits some Microsoft page like portal.office.com. Note that the error appearing in the email about failed to open stream is not the only one, and there are more explaining warnings going into PHP log if you remove “@” character from file_get_contents function.

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.

SIPPONEN.COM has moved!

I have physically moved the SIPPONEN.COM to a new service provider. Virtually it is still at the same location as it has been, meaning the address www.sipponen.com.

My earlier service provider Nebula was really good and stable, but after I quickly made a benchmark about the prices it became clear to me that I’m paying too much. With my new service provider Domainmaailma the cost level is one third and as a really nice bonus they use cPanel which means I can configure by myself pretty much what I want to related to my web hotel services. Also adjusting capacities like disk space, network transfer quotas, number of email boxes, databases etc. is really simple and real-time change. This kind of things of course affect to the price of the service, but it is rather “affordable” and scaling is possible up and downwards without any notification periods. This I would call as a cloud service. 🙂

So far it seems that I have found good+cheap+fancy SP for my site. As everybody knows this kind of equation usually “does not compute”, but let’s see. So far so good and since I’m quite DIY type of a person who knows if this really is the perfect service for me. 🙂

At the same time I needed to change my domain registrar from Joker to Enom (to get all the benefits hanging at this SP transition) and seems that this change has now been replicated pretty much all over the globe. Also it seems that there were not any kind of downtime for my site while committing the transition, so this really seems to be too good of being true.

Integrations Ready

Seems that about all the features are working which I wanted to get to SIPPONEN.COM:

  • Blog with many publishing features
  • Photo gallery
  • Temperature measurements more or less nicely incorporated to WordPress page template. A bit quick & dirty solution, but good enough. 🙂
  • Facebook integration – blog posts will be automatically published to my Facebook wall without any extra steps
  • Social media integration for easy sharing and commenting
  • Rather nice Android application to publish new content

There still are some things I will try to improve at the future, but now I declare this as a “production ready” web site. If I have enough time & motivation I will try to re-post some content currently in Facebook photo galleries to my new blog, but that remains to be seen.

Biggest challenge was to find from a zillions of different plugins the ones that fit to my purposes and also are trustworthy. I also needed to familiarize myself to WordPress, but from installing point of view this has been very easy process. Currently I only have 3 plugins: Akismet, Eazyest Gallery and JetPack.

SIPPONEN.COM is under upgrade

I have installed WordPress to power my SIPPONEN.COM web site. This is mainly because my old homemade web publishing engine starts to be so old that it is not able to meet all today’s needs. And since I’m not willing to consume my time to rewrite it I have decided to use some open source tool instead.

This site is still rather incomplete and hopefully I have time & motivation to get it properly running some near future.

Commodore 128 video/scart

I found this at year 2003 when I was searching such solution:

I hooked my C128 to my television set. The same cable can be used
for both 40 and 80 column screen, as I have a switch in it.

This cable is based on an article in C=Lehti 2/89. It had some inaccuracies,
and it is in Finnish. So I'll describe the cable here.

The RGBI connector looks like following, when looking to the machine's rear
side from outside:

	5   4   3   2   1

	  9   8   7   6

(The User's guide and the C=Lehti article used the mirror image of this,
which confused at least me.)

The pins are as follows:

	1 GND	ground
	2 GND	ground
	3 R	red
	4 G	green
	5 B	blue
	6 I	intensity
	7 VIDEO	composite video
	8 HSYNC	horizontal sync
	9 VSYNC	vertical sync

My television has a 21-pin Scart connector, which is used in Europe. It is
a special type of connector that has rectangular plates as pins. The pins
are surrounded by a pentagonal metal frame. It looks like the following:

	_________________________________________
	|                                       |
	| 1   3   5   7   9  11  13  15  17  19 |
	|                                       |
	|                                        
	|   2   4   6   8  10  12  14  16  18  20 
	--------------------------------------------

The needed pins are:

	4, 5, 9, 13, 17	ground
	2		audio, right channel
	6		audio, left channel
	7		blue
	11		green
	15		red
	20		video
	16		fast blank

The Scart connector does not allow use of HSYNC and VSYNC signals, so it
uses a video signal to synchronize the RGB picture. As the video signal
can be used also without RGB, the "fast blank" signal is needed to enable
RGB signal, unless you have a switch in your television to enable RGB.

When the television or monitor gets a positive voltage to the "fast blank" pin,
RGB will be enabled. The C=lehti article instructed to tie VSYNC to this
pin through a 220 ohm resistor to provide the voltage, but it was too weak for
my TV. So I temporarily used a 9 V battery to get some color on the screen.
Finally I hooked that pin to the cassette port's +5V output.

The television expects analog RGB, but the C128 outputs digital RGB. The
signals can be converted to analog using six resistors:

digital              analog
	R ---- R1 ---- R ---- R4 ---+
                                    |
	G ---- R2 ---- G ---- R5 ---+
                                    |
	B ---- R3 ---- B ---- R6 ---+
                                    |
	I --------------------------+

The article suggested R1, R2 and R3 to be 470 ohms and R4, R5 and R6 to be
680 ohms. You can experiment with other values to get good-looking colors
on the screen.

On some C128's, the RGBI connector's VIDEO signal might be actually 40 column
screen's video signal, or the C=Lehti article is simply wrong when claiming
that you can get the 40 column screen via the RGBI connector. In any case,
you get sound and 40 column screen from the 8-pin VIDEO connector. You can
plug a 180 degree 5-pin DIN connector to it. The pin 2 is ground, 4 is video
signal and 3 is audio output.

Finally you have to add a 2*ON-ON switch to switch between 8563 and 8566
screen. Connect it as follows:

                       /
  RGBI Video (7)-----o/  80 column screen
                     /|
Scart Video (20)----/ |
                      |
   VIC Video (4)-----o|  40 column screen
                      |/
  voltage supply-----o/
                     /
 Fast blank (16)-R7-/    R7=220 ohms

                     o

As mentioned above, you might be able to use VSYNC as voltage supply. If
the cassette port's +5V pin is not enough for your TV or monitor, use a
9 V battery or take a +9V or +12V lead from your computer.

Be careful with the 80 column mode. If you reset the computer to 64 mode,
the VDC screen will be out of syncronization, and your monitor may start
to smoke if you leave the cable in 80 column mode for several seconds.

Part list:

	Quantity	Quality
	========	=======
	   3		470 ohm resistors
	   3		680 ohm resistors
	   1		220 ohm resistor
	   1		Scart connector
	   1		D9S connector
	   1		5-pin 180-degree DIN plug
	   1		2*ON-ON switch

To connect your C128's 80 column screen to a CGA monitor, simply connect all
wires.

Have fun connecting!

	Marko Mäkelä