Category: Windows

Selenium / New Relic Synthetics login using Microsoft AD + Check for text

After the recent Microsoft Azure AD outage of 15/0/3/2021 I thought how can I actually see whether our page is displaying what should be there and not just a API call error generated because of the Microsoft outage.

To do that I added step 6 ‘find text’ see below.

/**
 * Script Name: {Moodle Login}
 * 
 */

/** CONFIGURATIONS **/

// Theshold for duration of entire script - fails test if script lasts longer than X (in ms)
var ScriptTimeout = 180000;
// Script-wide timeout for all wait and waitAndFind functions (in ms)
var DefaultTimeout = 30000;
// Change to any User Agent you want to use.
// Leave as "default" or empty to use the Synthetics default.
var UserAgent = "default";

/** HELPER VARIABLES AND FUNCTIONS **/

const assert = require('assert'),
	By = $driver.By,
	browser = $browser.manage()
/** BEGINNING OF SCRIPT **/

console.log('Starting synthetics script: {Untitled Test Case}');
console.log('Default timeout is set to ' + (DefaultTimeout/1000) + ' seconds');

// Setting User Agent is not then-able, so we do this first (if defined and not default)
if (UserAgent && (0 !== UserAgent.trim().length) && (UserAgent != 'default')) {
  $browser.addHeader('User-Agent', UserAgent);
  console.log('Setting User-Agent to ' + UserAgent);
}

// Get browser capabilities and do nothing with it, so that we start with a then-able command
$browser.getCapabilities().then(function () { })
	.then(() => {
            logger.log(1, "https://your.site.co.uk/login/index.php");
            return $browser.get("https://your.site.co.uk/login/index.php"), DefaultTimeout;
        })
	.then(() => {
            logger.log(2, "click Sign in on Moodle page");
            return $browser.waitForAndFindElement(By.linkText("Sign in"), DefaultTimeout)
                .then(function (el) {
                    el.click();
                })
        })
	.then(() => {
            logger.log(3, "Pass Test Username");
            return $browser.waitForAndFindElement(By.name("loginfmt"), DefaultTimeout)
                .then(function (el) {
                    el.sendKeys($secure.USER);
                })               
        }).then(function(el){
            //Find and click the login button.
            return $browser.waitForAndFindElement(By.xpath("//input[@value='Next']"), DefaultTimeout)
                .then(function (el) {
                    el.click();
                })
        })            
	.then(() => {
            logger.log(4, "pass the password securely");
            return $browser.waitForElement(By.name("passwd"), DefaultTimeout)
                .then(function (el) {
                    el.sendKeys($secure.PASS_ID);
                })               
        }).then(function(el){
            //Find and click the login button.
            return $browser.waitForAndFindElement(By.xpath("//input[@value='Sign in']"), DefaultTimeout)
                .then(function (el) {
                    el.click();
                })
        })      
    .then(() => {
            logger.log(5, "find YOUR text");
            var textToFind = "YOUR TEXT";
            var pageText = $browser.waitForAndFindElement(By.tagName("html"), DefaultTimeout).getText()
                .then(function(body){
                    assert.ok(body.indexOf(textToFind) != -1,"Text "+ textToFind+ " not found in page");
	        });
        })             
	.then(function() {
		logger.end();
		console.log('Browser script execution SUCCEEDED.');
	}, function(err) {
		logger.end();
		console.log ('Browser script execution FAILED.');
		throw(err);
	});


//** Export Functions
const logger=(function (timeout=3000, mode='production') {

    var startTime = Date.now(),
        stepStartTime = Date.now(),
        prevMsg = '',
        prevStep = 0;


    if (typeof $util == 'undefined'  ){
        $util = {
            insights: {
                set: (msg) => {
                    console.log(`dryRun: sending to Insights using ${msg}`)
                }
            }
        }

    }

    function log(thisStep, thisMsg) {

        if (thisStep > prevStep && prevStep != 0) {
            end()
        }

        stepStartTime = Date.now() - startTime;

        if (mode != "production") {
            stepStartTime = 0

        }

        console.log(`Step ${thisStep}: ${thisMsg} STARTED at ${stepStartTime}ms.`);

        prevMsg = thisMsg;
        prevStep = thisStep;

    }

    function end() {
        var totalTimeElapsed = Date.now() - startTime;
        var prevStepTimeElapsed = totalTimeElapsed - stepStartTime;

        if (mode != 'production') {
            prevStepTimeElapsed = 0
            totalTimeElapsed = 0
        }

        console.log(`Step ${prevStep}: ${prevMsg} FINISHED. It took ${prevStepTimeElapsed}ms to complete.`);

        $util.insights.set(`Step ${prevStep}: ${prevMsg}`, prevStepTimeElapsed);
        if (timeout > 0 && totalTimeElapsed > timeout) {
            throw new Error('Script timed out. ' + totalTimeElapsed + 'ms is longer than script timeout threshold of ' + timeout + 'ms.');
        }
    }

    return {
        log,
        end
    }
})(ScriptTimeout)

Clone a Moodle Theme 3.10 >

Time has come around again for a major upgrade of Moodle 3.8 to 3.10, with this bring the fun of making sure the theme is working okay. Adaptable has just released their latest theme for Moodle 3.10, so I’ve taken this and cloned it. For this makes it easier to know which theme works on which version of Moodle we have installed. In the past we’ve needed to flip the DNS to the new production server.

Hopefully the below will work for you, it took me 10 attempts to crack it, without having random php / file errors showing on the browser or in dev tools.

Step 1

Download the latest version of your theme, we are using the 3.10 version of adaptable here. In Notepad ++ use the ‘Find in Files’ function to locate everything ‘Adaptable‘ which is the current name of the theme, replace that with your new name ‘ManMet-Adaptable‘. This should be two hits in one file, then hit ‘Replace in Files’. This just changes the naming in the language pack

Step 2

Now search for ‘theme/adaptable‘ and replace all with ‘theme/manmet_adaptable‘ (dont use a minus symbol in the theme path). This should find 24 hits in 8 files, now replace all with ‘Replace in Files’.

Step 3

Search for the original name of the theme ‘adaptable‘, change this to ‘manmet_adaptable‘, this is a massive file and replace with 3199 hits in 149 files.

Step 4

Now search the theme code for ‘/adaptable/‘ and replace with ‘/manmet_adaptable/‘. This should replace 33 hits in 9 files.

Step 5

Now find ‘theme_adaptable‘ and change that to ‘theme_yourname‘, in my case this is ‘theme_manmet_adaptable‘. This should be replacing around a large 2201 hits in 148 files.

Step 6

The unknown step.. I think it is required 🙂 I think it’s to do with the tabs in the theme settings. Search for ‘THEME_ADAPTABLE‘ and replace with ‘THEME_MANMET_ADAPTABLE‘, this should return around 18 hits in 7 files.

Step 7

Check places like config.php in the root of your theme and see that the main theme’s .css matches what is in /styles dir, also check that the naming in \jquery\plugins.php matches the name of the .js file in \jquery it should be under

$plugins = array(
    'manmet_adaptable' => array('files' => array('adaptable_v2_1_1_2.js')),

Step 8

Change the name of the theme folder to match, zip it up and deploy for testing – Good luck ! should the above method not work for you, do try the one which was working on < Moodle 3.9 which I wrote in 2018.

How to Change Moodle Azure AD Tenancy

I need to switch a UAT instance of Moodle to an alternative Azure AD Tenancy, this is how I went about it.

See my original post here on how to setup the app registration in Azure here then head to /admin/settings.php?section=local_o365 within the Moodle instance which you want to change over

In the below you need to change over you Application ID & Application Key, you get these from Azure

Lower down on the same page you need to also change these two fields to the new Tenancy and Sharepoint URL

Then Save the page at the very bottom.

Now on your instances Moodle database you need to run something like the following to change all your existing users over to the new Tenancy

#SQL to change all students to new UPN format

update mdl_user
set username =
REPLACE(username, '@.xxx.ac.uk', '@.xxx.ac.uk')
WHERE auth = 'oidc'
AND username regexp '^[0-9]{8}\@.xxx.ac.uk$'
AND email like '%@xxx.xxx.ac.uk';

#SQL to change all staff to new UPN format

update mdl_user
set username =
REPLACE(username, '@.xxx.ac.uk', '@.xxx.ac.uk')
WHERE auth = 'oidc'
AND username regexp '^[0-9]{8}\@.xxx.ac.uk$'
AND email like '%@xxx.ac.uk';

Check that this has had the desired effect in /admin/user.php and try to login to the site with the alternative UPN ID – remember in a incognito tab / private browser window on your browser of choice.

#Lockdown Home projector / Large ‘Disney’ wall art project

How do you go about painting a favourite character on your childs wall?

These are my recommended steps

  • Find suitable wall
  • Clean the wall with sugar soap, this will remove the grime
  • Paint wall if not the correct background colour
  • Purchase some acetate sheets
  • Draw a picture on the sheet with a thin permanent marker
    • If your art skills are not up to that:
    • Trace from another drawing or purchase the more expensive printable acetate sheets and print image on using a laser printer (Do not attempt to put in a non printable acetate sheet in a laser printer – they melt)
  • Find a suitable A4 sized box and tape the acetate sheet to it
  • Cut hole for a light source in back of box (I used a LED Maglite torch, which are super powerful)
  • Place the box and torch in a place where they cannot be moved
  • Darken the room and switch on your home projection unit
  • Use a HB pencil to draw on the wall

Now start painting with Arylic paint, I used these Daler-Rowney, System 3 Introduction Set they include all primary & secondary colours in order to mix up any not included

Fin

Selenium / New Relic Synthetics login using Microsoft AD

We needed a synthetics script which can tell us when our site authentication is broken. The following is running in our New Relic monitoring, but can be ran in numerous other monitoring tools

The following can be adapted for anyone’s site, please note that your authentication process may not hit the following Microsoft screen in log step 4 so might need to be changed or removed completely, in my case it selects the ‘Work or school account’ using a xpath method.

** Additional, I swapped to using a test accounts which doesn’t go to the account selector page shown below, this seems to be working much better across all synthetics servers hosted in different countries – I noticed that French & Italian based ones occasionally threw issues. The account selector page only appears if you have two Microsoft accounts which are using the same email address :

If you received the "Which account do you want to use?" message when you sign in, it means you have two accounts with Microsoft that use the same email address
/**
 * Script Name: {Moodle Login}
 * 
 */

/** CONFIGURATIONS **/

// Theshold for duration of entire script - fails test if script lasts longer than X (in ms)
var ScriptTimeout = 180000;
// Script-wide timeout for all wait and waitAndFind functions (in ms)
var DefaultTimeout = 30000;
// Change to any User Agent you want to use.
// Leave as "default" or empty to use the Synthetics default.
var UserAgent = "default";

/** HELPER VARIABLES AND FUNCTIONS **/

const assert = require('assert'),
	By = $driver.By,
	browser = $browser.manage()
/** BEGINNING OF SCRIPT **/

console.log('Starting synthetics script: {Untitled Test Case}');
console.log('Default timeout is set to ' + (DefaultTimeout/1000) + ' seconds');

// Setting User Agent is not then-able, so we do this first (if defined and not default)
if (UserAgent && (0 !== UserAgent.trim().length) && (UserAgent != 'default')) {
  $browser.addHeader('User-Agent', UserAgent);
  console.log('Setting User-Agent to ' + UserAgent);
}

// Get browser capabilities and do nothing with it, so that we start with a then-able command
$browser.getCapabilities().then(function () { })
	.then(() => {
            logger.log(1, "https://your.site.ac.uk");
            return $browser.get("https://your.site/login/index.php"), DefaultTimeout;
        })
	.then(() => {
            logger.log(2, "click Sign in on Moodle page");
            return $browser.waitForAndFindElement(By.linkText("Sign in"), DefaultTimeout)
                .then(function (el) {
                    el.click();
                })
        })
	.then(() => {
            logger.log(3, "Pass Username");
            return $browser.waitForAndFindElement(By.name("loginfmt"), DefaultTimeout)
                .then(function (el) {
                    el.sendKeys($secure.USERNAME);
                })               
        }).then(function(el){
            //Find and click the login button.
            return $browser.waitForAndFindElement(By.xpath("//input[@value='Next']"), DefaultTimeout)
                .then(function (el) {
                    el.click();
                })
        })          
 	.then(() => {
            logger.log(4, "Select xpath Work or school account");
            return $browser.waitForAndFindElement(By.xpath("(.//*[normalize-space(text()) and normalize-space(.)='Work or school account'])[1]/following::small[1]"), DefaultTimeout) 
                .then(function (el) {
                    el.click();
                })
        })   
	.then(() => {
            logger.log(5, "Pass the password securely");
            return $browser.waitForAndFindElement(By.name("passwd"), DefaultTimeout)
                .then(function (el) {
                    el.sendKeys($secure.PASSWORD);
                })               
        }).then(function(el){
            //Find and click the login button.
            return $browser.waitForAndFindElement(By.xpath("//input[@value='Sign in']"), DefaultTimeout)
                .then(function (el) {
                    el.click();
                })
        })               
	.then(function() {
		logger.end();
		console.log('Browser script execution SUCCEEDED.');
	}, function(err) {
		logger.end();
		console.log ('Browser script execution FAILED.');
		throw(err);
	});


//** Export Functions
const logger=(function (timeout=3000, mode='production') {

    var startTime = Date.now(),
        stepStartTime = Date.now(),
        prevMsg = '',
        prevStep = 0;


    if (typeof $util == 'undefined'  ){
        $util = {
            insights: {
                set: (msg) => {
                    console.log(`dryRun: sending to Insights using ${msg}`)
                }
            }
        }

    }

    function log(thisStep, thisMsg) {

        if (thisStep > prevStep && prevStep != 0) {
            end()
        }

        stepStartTime = Date.now() - startTime;

        if (mode != "production") {
            stepStartTime = 0

        }

        console.log(`Step ${thisStep}: ${thisMsg} STARTED at ${stepStartTime}ms.`);

        prevMsg = thisMsg;
        prevStep = thisStep;

    }

    function end() {
        var totalTimeElapsed = Date.now() - startTime;
        var prevStepTimeElapsed = totalTimeElapsed - stepStartTime;

        if (mode != 'production') {
            prevStepTimeElapsed = 0
            totalTimeElapsed = 0
        }

        console.log(`Step ${prevStep}: ${prevMsg} FINISHED. It took ${prevStepTimeElapsed}ms to complete.`);

        $util.insights.set(`Step ${prevStep}: ${prevMsg}`, prevStepTimeElapsed);
        if (timeout > 0 && totalTimeElapsed > timeout) {
            throw new Error('Script timed out. ' + totalTimeElapsed + 'ms is longer than script timeout threshold of ' + timeout + 'ms.');
        }
    }

    return {
        log,
        end
    }
})(ScriptTimeout)

Lost your MySQL database password?

create a ‘passwordrecovery.py’ Python file with the following contents:

import win32cryptimport osencrypted_data = open("C:\\Users\\{}\\AppData\\Roaming\\MySQL\\Workbench\\workbench_user_data.dat".format(os.getlogin()), "rb").read()clear_data = win32crypt.CryptUnprotectData(encrypted_data, None, None, None, 0)print(clear_data)

Downloaded Python for Windows and install :  Then run the following

pip install pypiwin32

Upgrade it if it suggests to:

C:\Users\<YourUser>\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.8\python.exe -m pip install --upgrade pip

 Add the following to my PATH: 

C:\Users\<YourUser>\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8\LocalCache\local-packages\Python38\Scripts 

Run the following from the command line :
python c:\python\passwordrecovery.py

It then showed me my passwords 🙂

A UK VLE during Pandemic lock-down

It’s been a busy few months for us, so I just thought I’d sum up what we’ve been doing to attempt to protect us during the COVID-19 pandemic:

  • Moved our Moodle instance to Azure
  • Upgraded from Moodle 3.6.3 to Moodle 3.8.2
    • Moved to a Boost based theme
    • Emails passed to on premise Exchange to be sent

The move to Azure was crucial to best serve our students who are now more than ever relying on our VLE. This year all our exams and coursework submissions are taking place within Moodle.

With the above it goes without saying we needed to be on the securest available version available which happens to be the latest release of Moodle, this is in order to minimise any malicious attacks.

If you are planning such moves, I would suggest moving a like for like instance to the cloud as step #1, then perform any required upgrades, don’t be tempted to combine the two stages.

How to Produce Moodle Reports which are like the ones in the Moodle Logs

Ever wondered why you can not get the Moodle database to produce a report just like it appears within Moodle graphical user interface logs?

Well that because rows like ‘Description’ is made from multiple parts.

Its bugged me for a while and googling turned up a blank, so I wrote the following, with a little help from a friend – Ta Jim.

SELECT
    from_unixtime(l.timecreated) Time,
    CONCAT(u.firstname, ' ', u.lastname) AS 'User full name',
    CONCAT(au.firstname, ' ', au.lastname) AS 'Affected user',
    u.username AS 'Username',
    l.contextid AS 'Event context',
    l.component AS 'Component',
    l.Eventname AS 'Event name',
    CONCAT('The user with id ',u.id,' ',l.action,' the ', l.objecttable,' ',
    CASE
        WHEN l.contextlevel = 10 THEN 'system'
        WHEN l.contextlevel = 50 THEN 'course'
        WHEN l.contextlevel = 30 THEN 'user'
        WHEN l.contextlevel = 40 THEN 'category'
        WHEN l.contextlevel = 70 THEN 'activity'
        WHEN l.contextlevel = 80 THEN 'block'
              END,
    ' with module id ', l.courseid) AS 'Description',
    l.origin AS 'Origin',
    l.ip AS 'IP address'
FROM moodle.mdl_logstore_standard_log l
    INNER JOIN moodle.mdl_user u ON u.id = l.userid
    LEFT JOIN moodle.mdl_user AS au ON au.id = l.relateduserid
#Change l.courseid to the internal id of moodle course you want to check out
WHERE l.courseid = '10001'
ORDER BY time DESC;

This will produce results from the database which are like the following from the Moodle Logs in the GUI

Time

User full name

Affected user

Event context

Component

Event name

Description

Origin

IP address

6 February 2020, 9:02 PM Raymond Reid Coursework: Business 101 Coursework Course module viewed The user with id ‘1480’ viewed the ‘course’ activity with course module id ‘2100’. web x.x.x.x

 

Hope it helps..

Moodle Office 365 Azure API permissions

I’ve seen this issue twice now on two different Moodle setups where the required access to read from the Azure Active Directory Graph API (AAD Graph) hasn’t been granted on setting up the app registration through Moodle.

Error received:

AADSTS650056: Misconfigured application. This could be due to one of the following: The client has not listed any permissions for ‘AAD Graph’ in the requested permissions in the client’s application registration.

I think there is possibly a bug in the latest version of the Office 365 and Azure Active Directory plugins for Moodle

I didn’t have this problem when setting our app registration up, but I did in from within Azure not by using the ‘Provide Admin Consent’ button from Moodle’s Microsoft Office 365 Integration setting in local plugins.

The below is how your app registration in Azure should be:

Also check the mdl_auth_oidc_token table and delete any rows which have userid of 0

select * from mdl_auth_oidc_token where userid = '0';

Screenshot_2019-10-15-07-34-07-858_com.android.chrome

Moodle & Azure Active Directory, the trials and tribulations

Recently I’ve undertaken the task of moving our teaching and learning system to authenticate using Azure AD.. There’s some 120,000 accounts on our Virtual Learning Environment, whether these are active / inactive or suspended doesn’t matter they all need to be able to be logged into if required by the user.

So how did I go about it?

By first installing the following plugins from the Office 365 set:

  • Microsoft Office 365 Integration (local_o365)
  • OpenID Connect (auth_oidc)
  • Office 365 Repository (repository_office365)
  • Microsoft Block (block_microsoft)

I used these plugins rather than the core OAuth2 method as they promise much higher level of AD customisation and are developed by Microsoft, as well as the extra levels of integration to Office 365.

The main task I was trying to complete was to attempt to use the account pattern match function, so the existing LDAP accounts could be matched to the ones within our Azure AD and gain a complete single sign on solution for our VLE… All this sounded like a simple task to achieve.

The one thing you must do, is read the instruction manual for this plugin set, this is some 20 pages in length, find a quiet place before attempting this, there’s a lot to take in and a lot of prerequisites that you need to have in place, obviously one of these is a working Azure Active Directory, running in Azure portal.

I won’t go into detail of the installation or setup as the 20 pages does a good job of explaining that. What I do need to explain is the parts of what to do when your trying to achieve something when the manual has stopped.

Such as the one part that lead me down this path, the ability to match any preexisting moodle users with the same named accounts in Azure AD

The manual says “This requires the “Match” setting above to be enabled. When a user is matched, enabling this setting will switch their authentication method to OpenID Connect. They will then log in to Moodle with their Office 365 credentials. Note: Please ensure the OpenID Connect authentication plugin is enabled if you want to use this setting.” and that is it..

Well I can tell you I spent far too much time attempting to get this working to no avail. I’ve since learnt that our hosting partner has also tried it to no avail. So just don’t bother, unless it gets fixed in a later release than 3.5.0.2, see my github issue ticket that I’ve raised to see if it get solved in a future release or an explanation by the developers.

Here is an example of the errors which were being kicked out by the Azure AD sync in scheduled tasks:

......... Syncing user <strong>studentID@tenant.ac.uk</strong>
......... Assigning Moodle user 30 (objectid 3c1a8a67-1234-4ace-bc38-22b0e4aa973e) to application
......... Could not assign user "<strong>studentID@tenant.ac.uk</strong>" Reason: No token available for usersync
......... Found a user in Azure AD that seems to match a user in Moodle
......... moodle username: <strong>studentID</strong>, aad upn:<strong>studentID@tenant.ac.uk</strong>
......... User is already matched.
......... User is now synced.

But nothing gets written back to the database, so whatever ‘No token available for usersync‘ means, is the reason why.

Additional to the original post

Here’s the answer to the above, see my github ticket for full details

Looking at the output you posted, it looks like this user is a “matched” user – i.e. they have not yet logged in to the site, correct? A “matched” user just sets up a user for a future connection, but the user has to actually log in to the Moodle site using their Office 365 credentials to complete the connection. User information syncing will only happen after this connection has been completed.

I’ve not proved this to be the case, as the plugins are only on our Dev server, until the summer upgrade, but I will be trying it again then.

Back to original post

After much soul searching the answer became obvious, just write the required field to the mdl_user table, using SQL on the back-end MySQL database.

Here is the SQL which I used to do that:

The SQL is updating the username and adding our Azure student and staff tenant to that. The regular expression, which you might not require is only checking for a ‘username’ which is 8 digits in length.

#SQL to change all students

UPDATE mdl_user
SET username = concat(ifnull(username,""), '@xxx.xxxx.ac.uk'), auth = 'oidc'
WHERE auth = 'ldap'
AND username regexp '^[0-9]{8}\@xxx\.xxxx\.ac\.uk$'
AND email like '%@xxx.xxxx.ac.uk';

#SQL to change all staff

UPDATE mdl_user
SET username = concat(ifnull(username,""), '@xx.xxxx.ac.uk'), auth = 'oidc'
WHERE auth = 'ldap'
AND username regexp '^[0-9]{8}\@xx\.xxxx\.ac\.uk$'
AND email like '%@xxxx.ac.uk';

I hope the above helps you get to where I am quicker and with less pain.

I’ll finish here for now and go into detail about how to plugins handles new accounts at another time.

Addition to original post : Sync’ing new users

This works like a dream, I increased the cron task ‘Sync users with Azure AD’ to run every 7 mins, so it could chew through our AAD (Azure Active Directory) and it just creates users for you..

User Creation Restriction

Beware when using this, the obvious choice of the UPN / Username is not here, Object ID is not that, I had thought it was and wasted a day of tinkering and wondering why my Reg Ex was not working

(?im)^[0-9]{8}\@staff\.xxx\.ac\.uk$|(?im)^[0-9]{8}\@student\.xxx\.ac\.uk$

It because it wasn’t trying to match to the username, the Object ID looks like this:

06273d5ea-2ef1-1d10-a315-4ac234fe34f2

I’ve asked the developer to add this as a feature enhancement, see here

We’ve now used the option at the bottom, ‘Office 365 Group Membership‘ and created a dynamic security group, this is working as expected, which is fabulous, some people on the GitHub forum are asking for the ability to use more than one group to do this.

We’ve also used RegEx to block logins that are not formatted like our ID 12345678@staff.xxx.ac.uk or 12345678@student.xxx.ac.uk

Using the same regex as above, for these settings see User Restrictions in /admin/settings.php?section=authsettingoidc on your site.

Go Live for Academic period 1920

Lessons learned

September 2019 and all seems to be working well. things I have noted since going live:

  • All accounts need to be set as active, moving from LDAP to OpenID Connect may have left some in a suspended / inactive state, this flag does not get set via what is set on Azure. So making all accounts active is the best thing to do. If a user attempted to authenticate and their Moodle account was inactive then they saw ‘ERROR1’ or equally useless error when using Office365 suite 3.6.0.1
  • Because the ‘Block sign in’ flag from Azure doesn’t pass across there is no way of knowing which accounts can be ‘safely’ deleted. I’m trying to get this added as a feature enhancement.