Category Archives: Office 365

Beyond the supported – Cross forest style Exchange Online Migration

Migration and consolidation projects always put our skills to the test and in some cases forces us to do things that are not fully supported. But sometimes, the end justifies the means.

Some time ago, I did an Exchange Online onboarding project in a quite complex scenario, at least in terms of the identities. 10+ Active Directories and almost as many Exchange environments. On top of that, due to several reasons, it was not possible to create trusts between the different directories. Not the easiest starting point…
Looking at all different options we agreed upon to synchronize one of the directories to Azure AD. This meant that all users not in that directory were to get new accounts, effectively taking the first step towards a common environment.

– Identites – check!

The next step were to determine the migration strategy for the Exchange environments that was ranging from Exchange 2007 -> 2013. Given the quite complex scenario without trusts between the environments, our minds were set to use a third-party tool to do all the migrations. I always like challenging the obvious path, so I wanted to see if it was possible to do native mailbox moves even though there was no possibility to use the regular Hybrid Configuration Wizard.

Guess what – following cross-forest migration principals and copying the relevant attributes between the environments worked great! Cutting $100k in third-party software licensing costs from the project budget is never a bad thing either 🙂

A little simplified, we ended up with a scenario / environment looking like below:

fakehybrid

Remember that this is a quite complex scenario where you have to do most preparations “manually”, so don’t try this unless you really need this kind of scenario. Also note that the same approach works in a staged migration scenario from Exchange 2003/2007.

Assumptions:

  • You have a way to match users between the different environments when moving the attributes.
  • The target directory/forest has been prepared with the Exchange server schema.
  • SMTP/Port 25 is open from the source Exchange environments for outgoing coexistence mailflow.
  • You are in control of your incoming mailflow as well as AutoDiscover, depending on how your environment/domains look like. 🙂
  • This method will be used to migrate mailboxes, not for long-term coexistence. If you have separate SMTP domains per environment, free/busy sharing can be configured if needed.

Preparation steps: 
Source Exchange Environment:

1. Add the tenant routing domain (tenant.mail.onmicrosoft.com) as an accepted domain. Also create a send connector to solve outgoing email in the transition
2. Add the routing email address (smtp:samaccountname@tenant.mail.onmicrosoft.com) to all users in scope for migration.
3. Export the attributes needed to perform the migration from the source Exchange environment. The following attributes are needed for a successful migration. I have used Export-Clixml instead of a CSV-file for simpler handling importing back the Guid Attributes.

  • mail
  • mailNickname
  • proxyAddresses
  • msExchMailboxGuid       
  • msExchArchiveGuid  
  • legacyExchangeDN          
  • msExchRecipientDisplayType
  • msExchRecipientTypeDetails

4. Export mailbox permissions/calendar permissions for users/shared mailboxes if needed.
5. Enable MRSProxy in the Exchange Environment. Also create a service account with at least Recipient Management permissions to use setting up the migration endpoint in Exchange Online.

Target Active Directory / Exchange Online environment:
1. Set up an “Exchange Remote” migration endpoint towards the MRSProxy earlier created.
2. Import the attributes earlier exported in the user directory.
3. After a successful directory synchronization, verify that the users in scope shows up as Mail Users in Exchange Online. If you did the Exchange schema update after you installed AADConnect, don’t forget to refresh the schema there as well, otherwise you won’t synchronize the imported attributes.
4. Perform a test move and verify that everything works as expected. Just as in the case of a “normal” move, the mailbox will be converted to a remote mailbox after the move has been completed.

Migration/post migration steps 
1. Plan and execute your migration batches. Remember that this is not a regular hybrid environment with free/busy etc., so plan your batches accordingly.
2. Import eventual mailbox/calendar permissions after each batch.
3. Switch MX/Autodiscover to Office 365
4. Decommission the source Exchange environment.

Summary
One approach does not fit all and sometimes you have to think a little bit out of the box. I hope this post was interesting and that it might give you some creative ideas running in to similar scenarios. For any questions or feedback, feel free to comment here or ping me on Twitter, @daltondhcp.

/Johan

ADFS Customization – add custom help form to the login page

In the spirit of sharing more “not-so-evergreen” ADFS customizations, I wanted to share another customization request I got a while ago.
The case was very simple, they customer wanted to fit more end-user information in the login flow than the ADFS login page could fit without sending the user to another page.
Onload.js to the rescue again! This time we are using it to simply create an additional form where we present additional information when clicking on the help button.

My example looks like this:
2017-01-01_21-06-44
2017-01-01_21-36-25
2017-01-01_21-36-43

Note that everything is in the JavaScript code, including the help page text. If preferred, that part could be loaded from another location.

Customize ADFS with help page (yes, I should move my content to GitHub)

// Author: Johan Dahlbom
// Blog: 365lab.net
// Twitter: @daltondhcp
// Get DOM elements and save as objects
var loginMessage = document.getElementById('loginMessage'),
    loginArea = document.getElementById('loginArea'),
    loginForm = document.getElementById('loginForm'),
	userNameInput = document.getElementById('userNameInput'),
    helpContent,
    usernameLink,
    passwordResetLink,
	errorText = document.getElementById("errorText"),
	introArea = document.getElementById("introduction"),
	authArea = document.getElementById("authArea");

var showingHelper = false,
    showingLoginform = false;

// CREATE CONTENT FUNCTIONS

function createHelpersForLoginForm() {
  //Create the hyperlink to the help form
  passwordResetLink = document.createElement('a');
  var linkText = "Need help?";
  passwordResetLink.appendChild(document.createTextNode(linkText));
  passwordResetLink.title = linkText;
  passwordResetLink.href = "#";
  passwordResetLink.onclick = toggleHelpContent;

  loginArea.appendChild(passwordResetLink);
}

function createHelpContent() {
  if (!authArea) {
    return;
  }
  helpContent = document.createElement("div");
  helpContent.style.display = 'none';

  helpContent.innerHTML = '\
    <br><br>\
    <h2><strong>What is my username?</strong></h2>\
    <p>Your username is the same as your email address. Example: ann.andersson@365lab.net</p><br>\
    <h2><strong>What is my password?</strong></h2>\
    <p>This is a secret chosen by you. It would not be a secret if we told you. If you forgot your password, you can reset it <a href="https://passwordreset.microsoftonline.com/?whr=365lab.net" target="_blank">here</a><br><br>\
    </p>\
    <h2><strong>Support</strong></h2>\
    <p>If you have any issues or questions, please contact our helpdesk at 555-GET-HELP or <a href="mailto:support@365lab.net">support@365lab.net</a><br><br><br></p>\
    ';

  // Link for close help
  var closeHelpContentLink = document.createElement('span');
  closeHelpContentLink.innerHTML = "Back to the login form";
  closeHelpContentLink.className = "submit";
  closeHelpContentLink.onclick = toggleHelpContent;

  // Duplicate it to have one before the content as well.
  // Uncomment these lines if the help  content grows.
  // var closeHelpContentLinkUpper = closeHelpContentLink.cloneNode(true);
  // closeHelpContentLinkUpper.onclick = toggleHelpContent;
  // helpContent.insertBefore(closeHelpContentLinkUpper, helpContent.firstChild);

  helpContent.appendChild(closeHelpContentLink);

  authArea.appendChild(helpContent);
}

function updateUI() {
  // Check for DOM errors
  if (!loginForm || !helpContent) {
    return;
  }

  if (showingHelper) {
    openHelpContent();
  } else {
    closeHelpContent();
  }
}

function toggleHelpContent() {
  showingHelper = !showingHelper;

  updateUI();
}

function openHelpContent() {
	helpContent.style.display="block";
	loginArea.style.display="none"
}

function closeHelpContent() {
	helpContent.style.display="none";
	loginArea.style.display="block"
}

// Create DOM elements 
createHelpersForLoginForm();
createHelpContent();
updateUI();

As usual, you update onload.js with the Set-AdfsWebTheme cmdlet.
Hope this will be useful for some of you guys! If you have questions, ping me o twitter or email!

Thanks,
/Johan

ADFS Customization – Branding per domain for Azure AD/Office 365

Branding your services can be very important for many reasons where recognizability and company profile are the most common ones. Making the marketing department happy is not a bad thing either.
With ADFS in Server 2016, the capability to do branding on a relying party basis was added. This was something that you in 2012 R2 needed to use JavaScript to achieve. Please note that the customizations in this post works both in 2012R2 and 2016.

While the default branding options above fit most customer needs, I recently had a customer case where two municipalities were onboarding to Office 365. Since they were sharing the same AD domain, they also shared their ADFS environment. You can probably guess where we are going now… They want continue using the same ADFS environment but have different branding depending on the login domain in Office 365 / Azure AD. I have got this request before, but have usually talked out the customers of going that path and instead agree upon common branding, but not this time. Not the most evergreen solution, but all for the customers, right? 🙂

There is a uservoice for the same request using managed domains directly in Azure AD – so clearly this is not the first time this has come up.

OVERVIEW
We will customize onload.js to apply different branding depending on the Office 365 / Azure AD domain used to login.
If browsing /idpinitiatedsignon directly or using another RP than Azure AD/Office 365, the default branding should apply. In my example, I will use the domains dom1.365lab.net and dom2.365lab.net. I assume that basic branding/webtheme already is in place.

The branding will apply in the following scenarios:

UPLOAD LOGO AND ILLUSTRATION
Upload the logos and illustrations as in my example script below. For performance and looks, follow the recommendation on sizes etc. on this TechNet page. If you have many domains, using some kind of naming convention might also be a good idea ;-).

$WebThemeName = "365lab"
#The pictures to upload
$UploadData = @{
  logo = @('C:\temp\Branding\dom1.365lab.net_logo.png',
           'C:\temp\Branding\dom2.365lab.net_logo.png')
  illustration = @('C:\temp\Branding\dom1.365lab.net_illustration.jpg',
                  'C:\temp\Branding\dom2.365lab.net_illustration.jpg')
}                                                                                                              
#Loop through the image HT and upload the files accordingly
foreach ($ImageType in $UploadData.Keys) {
  $UploadData[$ImageType] | ForEach-Object -Process {
    $FileName = $_.Split('\')[-1]
    Set-AdfsWebTheme -TargetName $WebThemeName `
                      -AdditionalFileResource @{
                        Uri = '/adfs/portal/{0}/{1}' -f $ImageType,$FileName
                        Path = $_.ToString() 
                      } 
  }                  
}                                                                                                                                                                                                                               

CUSTOMIZATION OF ONLOAD.JS
The JavaScript basically loops through an array and checks if the request has been referred from any of the domains in scope for customizations. In the example, the username placeholder and the login message are customized based on the “domainconfig” data as well. If you don’t know how to export/import onload.js to ADFS, looking in to this article prior doing any changes might be a good idea.

//Variables
var locationUrl = window.location.href.toLowerCase(),
	referrerUrl = document.referrer.toLowerCase(),
    logoDomain = document.getElementById('header'),
    loginMessage = document.getElementById('loginMessage'),
    userNameInput = document.getElementById('userNameInput'),
    domainconfig = [
        {domain:"dom1.365lab.net", companyName: "365lab Domain 1", logo:"dom1.365lab.net_logo.png", illustration:"dom1.365lab.net_illustration.jpg"},
        {domain:"dom2.365lab.net", companyName: "365lab Domain 2", logo:"dom2.365lab.net_logo.png", illustration:"dom2.365lab.net_illustration.jpg"}
    ];

function checkUrlForDomain(domainName) {
  return locationUrl.indexOf(domainName) > -1 || referrerUrl.indexOf(domainName) > -1;
}

for (var j = 0; j < domainconfig.length; j++){
  var domainName = domainconfig[j].domain;
  if (checkUrlForDomain(domainName)) {
     var logo = domainconfig[j].logo;
     var illustration = domainconfig[j].illustration;
     var companyName = domainconfig[j].companyName;

     //for troubleshooting purposes
     //console.log(domainName); 
     //console.log(logo);
     //console.log(illustration);

     //Change Logo
     logoDomain.innerHTML = "<img class='logoImage' src='/adfs/portal/logo/" + logo +"' alt='" + domainName + "'" +">"
     //Change illustration
     document.getElementsByTagName('style')[0].innerHTML = ".illustrationClass {background-image:url(/adfs/portal/illustration/" + illustration + ");}"; 
     //Change login message
     loginMessage.innerHTML = "<h2>Sign in with your " + companyName + " account </h2>" ;
     //Change username placeholder
     userNameInput.placeholder = "firstname.lastname@" + domainName ;
  }
}

After updated onload.js with your code, upload the changes to your webtheme with the following PowerShell cmdlet.

$WebThemeName = "365lab"
Set-AdfsWebTheme -TargetName $WebThemeName `
                 -AdditionalFileResource @{
                    Uri="/adfs/portal/script/onload.js"
                    path="C:\temp\script\onload.js"
                  }

RESULTS
Voila! We now have different branding in our ADFS depending on the domain suffix entered in Office 365 / Azure AD. Note that the code won’t change branding if you change the domain suffix in the username field after hitting the ADFS farm.

idpInitiatedSignOn
2016-12-30_11-00-39
Browsing outlook.com/dom1.365lab.net
2016-12-30_10-59-33
Browsing outlook.com/dom2.365lab.net
2016-12-30_11-00-16

Good luck with your branding and as always, let me know if you have feedback!

Happy new year!

/Johan

Assign individual parts of licenses with Azure AD PowerShell V2.0

Azure AD PowerShell V2 has been in GA for almost a month now. Even though some features (like converting a domain to federated) are missing as of now, it is really time to start to rewrite all those old MSOnline module scripts as AAD PS PM Rob de Jong reminded me of this thursday.

Since licensing still is the most important task to automate in customer environments I thought it was a good idea to share some of the basics on how to licensing works in V2.0. Some day, when ‘Azure AD Group Based licensing’ is in place, we can get rid of most of these these licensing scripts.

Since most of my customers rarely roll out all services at once, I thought it was a good idea to show you how to roll out a few serviceplans at a time with Azure AD PowerShell V2.0. If you want to look in to the basics, fellow MVP Simon Wåhlin wrote a great post about this a month ago.

Find your LicenseSku’s and ServicePlans:
Luckily enough, the equivalent to Get-MsolAccountSku, Get-AzureADSubscribedSku gives us pretty much the same information as its predecessor.
aadsku
As seen in the screenshot, we can still identify the licenses with their partnumber even though we need the SkuId’s when assigning the licenses later which is really helpful converting your old scripts.

Finding the individual services is as easy, you only need to look in to the ServicePlans property instead of ServiceStatus. As with the base license, you’ll need to use the ServicePlanId of the specific plan if you want to disable it.
serviceplans

Putting it together and assign individual ServicePlans of a license:
In my example, I will enable a non-licensed user an E3 that has Exchange Online, Skype for Business and Office 365 ProPlus enabled. In staged onboarding cases usually choose the ones to enable in my scripts instead of hard coding the disabled ones, since Microsoft tend to add new serviceplans from time to time (Teams, PowerApps, Flow etc.). You can of course do it the other way around if that fits your organization better.
Note that it is still a requirement to assign the UsageLocation on the user before assigning the license.

#The user that will get a license
$UserToLicense = Get-AzureADUser -ObjectId "johan@365lab.net"

#Define the plans that will be enabled (Exchange Online, Skype for Business and Office 365 ProPlus )
$EnabledPlans = 'EXCHANGE_S_ENTERPRISE','MCOSTANDARD','OFFICESUBSCRIPTION'
#Get the LicenseSKU and create the Disabled ServicePlans object
$LicenseSku = Get-AzureADSubscribedSku | Where-Object {$_.SkuPartNumber -eq 'ENTERPRISEPACK'} 
#Loop through all the individual plans and disable all plans except the one in $EnabledPlans
$DisabledPlans = $LicenseSku.ServicePlans | ForEach-Object -Process { 
  $_ | Where-Object -FilterScript {$_.ServicePlanName -notin $EnabledPlans }
}

#Create the AssignedLicense object with the License and DisabledPlans earlier created
$License = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicense
$License.SkuId = $LicenseSku.SkuId
$License.DisabledPlans = $DisabledPlans.ServicePlanId

#Create the AssignedLicenses Object 
$AssignedLicenses = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicenses
$AssignedLicenses.AddLicenses = $License
$AssignedLicenses.RemoveLicenses = @()

#Assign the license to the user
Set-AzureADUserLicense -ObjectId $UserToLicense.ObjectId -AssignedLicenses $AssignedLicenses

In the latest preview version of the module (2.0.0.44) that I used when writing the post, I didn’t manage to assign a license to a new user without defining the RemoveLicenses property of the AssignedLicenses object.
2016-12-25_13-21-58

Verifying the licenses in the portal (yeah, could of course have done that with PowerShell as well.), it looks just as expected. If I wanted to enable more plans for the user I could just change the EnabledPlans array and run the script again.
2016-12-25_13-30-16

Summary
Azure AD PowerShell V2.0 gives us all needed functionality to keep automating our license assignment in Azure AD. It might take you a bit longer to learn it since it is somewhat more “PowerShelly” with the different objects used to assign the licenses but apart from that, I really like it. I have not done any scientific tests (might do), but it seems like it is also much faster using the Graph API endpoints than the old endpoints doing different actions against Azure AD.
I will follow this post up with hopefully new and updated versions of my larger and more advanced scripts built on the old module.

Let me know if you have questions!

/Johan

Common questions using Office 365 with ADFS and Azure MFA

Azure Multi Factor Authentication (MFA) is a great service that has been included in Office 365 for almost 2,5 years. The adoption has really been great – at least from an admin user perspective where 99% of my customers admins have it enabled (I usually force them).
From an end user perspective we have more technical and informational challenges, which means that the adoption has not been as great as on the admin side. Hopefully the new shiny Conditional access policies for specific workloads will boost the adoption a bit.

The purpose of this post is to share the most common questions I get from customers about using Azure MFA included in Office 365 (in most cases in combination with ADFS).

Q: Can we pre-stage the MFA authentication methods so the end user doesn’t have to enroll after being enabled for MFA?
2016-07-15_12-56-53
A: As of now, unfortunately no – I tried to build a PowerShell function to pre-populate the authentication methods if the user already had a mobile phone number. I was fooled and thought it worked, but when I tried on a user that never had enrolled MFA before, it failed. Troubleshooting further, the required MFA property “StrongAuthenticationUserDetails” is not possible to pre-populate programmaticly, yet. Maybe the new AzureAD module can help here in the future.
2016-07-15_13-04-332016-07-15_13-10-20
Conclusion – we have to instruct our users to enroll for MFA.

Q: Can we prevent MFA from kicking in when authenticating from our internal network?
A: Absolutely – There are some options depending on if you have Azure AD Premium or not.

With Azure AD Premium
Here you can choose to “white list” your external IP addresses (which of course works with or without ADFS), or check the “Skip multi-factor authentication for requests from federated users on my intranet” checkbox. This will make Azure AD decide about MFA based on the insidecorporatenetwork claims issued by your own ADFS.
2016-07-15_13-32-05
The following PowerShell rows will add the required Issuance Transform Rules to your Azure AD RP.

$ByPassMFAClaims = @"
@RuleName = "Passtrough - InsideCorporateNetwork"
c:[Type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork"]
 => issue(claim = c);

@RuleName = "Passtrough - PSSO"
c:[Type == "http://schemas.microsoft.com/2014/03/psso"]
 => issue(claim = c);
"@
$AzureADRP = Get-AdfsRelyingPartyTrust -Name "Microsoft Office 365 Identity Platform" 
Set-AdfsRelyingPartyTrust -TargetName $AzureADRP.Name `
                          -IssuanceTransformRules ($AzureADRP.IssuanceTransformRules + $ByPassMFAClaims)

Without Azure AD Premium
Without Azure AD Premium we don’t have the same choices in service settings.
2016-07-15_13-30-57
We can however achieve the same result, but instead of passing through the insidecorporatenetwork claims, we use it in ADFS and “tell” Azure AD that MFA is already taken care of. This can be done with the claim rules as below.

$ByPassMFAClaims = @"
@RuleName = "Bypass MFA from inside - Without Azure AD Premium"
EXISTS([Type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork", Value == "true"])
 => issue(Type = "http://schemas.microsoft.com/claims/authnmethodsreferences", Value = "http://schemas.microsoft.com/claims/multipleauthn");

@RuleName = "Passtrough - PSSO"
c:[Type == "http://schemas.microsoft.com/2014/03/psso"]
 => issue(claim = c);
"@
$AzureADRP = Get-AdfsRelyingPartyTrust -Name "Microsoft Office 365 Identity Platform" 
Set-AdfsRelyingPartyTrust -TargetName $AzureADRP.Name `
                          -IssuanceTransformRules ($AzureADRP.IssuanceTransformRules + $ByPassMFAClaims)

Q: Do we really need to use the f****** app passwords?
A: Not really anymore, at least not if you are using ADFS. I usually turn it off.
First of all most rich clients (Including Outlook/SfB on mobile devices) do now support Modern Authentication (ADAL), which means they can handle MFA out of the box. So as long as you have updated clients, you most often only need to handle ActiveSync (native mail clients in all kinds of devices). My approach here is usually to exclude them from MFA to get rid of the app password need, but enable conditional access in order to control the devices.

Below you find a claims rule for the ActiveSync protocol that issues the multipleauthn claim which Azure AD will honor by skipping MFA for the request.

$Claims = @"
@Claims= "Bypass MFA for ActiveSync"
EXISTS([Type == "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-endpoint-absolute-path", Value=="/adfs/services/trust/2005/usernamemixed"]) && 
EXISTS([Type == "http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-application", Value =~"^(Microsoft.Exchange.(Autodiscover|ActiveSync))$"]) 
=> Issue(Type = "http://schemas.microsoft.com/claims/authnmethodsreferences", Value = "http://schemas.microsoft.com/claims/multipleauthn"); 
"@
$AzureADRP = Get-AdfsRelyingPartyTrust -Name "Microsoft Office 365 Identity Platform" 
Set-AdfsRelyingPartyTrust -TargetName $AzureADRP.Name `
                          -IssuanceTransformRules ($AzureADRP.IssuanceTransformRules + $Claims)

Please note that additional rules may be needed depending on if you have an Exchange or Skype for Business Hybrid environment.

Q: Can we combine Azure MFA with our already implemented on-premises MFA solution?
A: It works fine to combine Azure MFA with any MFA solution that integrates with ADFS. The only thing you need to do is issue the authnmethodsreferences on the Azure AD RP to prevent users from getting “Double MFA” like SmartCard + Azure MFA. 🙂 For example, if I have Cert Auth as an enabled MFA provider as below, I only have to authenticate with my VSM/SmartCard even if MFA is enabled on the user.
2016-07-15_14-27-41
The needed claims rule looks like this:

$Claims= @"
@RuleName = "Passthrough - MFA"
c:[Type == "http://schemas.microsoft.com/claims/authnmethodsreferences"]
 => issue(claim = c);
"@
$AzureADRP = Get-AdfsRelyingPartyTrust -Name "Microsoft Office 365 Identity Platform" 
Set-AdfsRelyingPartyTrust -TargetName $AzureADRP.Name `
                          -IssuanceTransformRules ($AzureADRP.IssuanceTransformRules + $Claims)

The above also works with the new Conditional Access policies for Exchange and SharePoint online.

Wrap up
Hopefully this post has given you some good insights what to think about implementing Azure MFA for Office 365. It is not a complete walk in the park, but it’s definately doable for most organizations.
As always, there are lots of if’s and but’s in all different environments. If I have missed something or if you have other, more specific questions, let me know!

/Johan

How to resolve “We’ve run into a problem with your Office 365 subscription” with PowerShell

A colleague of mine performed a tenant to tenant migration a while ago. After the switchover to the new tenant, almost all users Office 365 ProPlus installations started to complain with the error message “We’ve run into a problem with your Office 365 subscription”. 2016-07-13_21-29-33
Signing out/in trying to activate the installation again didn’t help and after a while he found a solution at Jaap Wesselius blog.

The solution was to run the OSPP.vbs script located in the Office installation folder as in the screenshot below. Running the script with the /unpkey parameter removes the existing license and forces the user/client to re-register.
2016-07-13_21-41-40

So – all good? The only challenge now was that he had over 200 clients to run the script on.

To help him automate this, I wrote a simple PowerShell script as a wrapper around OSPP.vbs to automate the key deactivation. It simply locates OSPP.vbs from the installation folder(s), fetches the key(s) and last removes all existing activations. Please note that it needs to run elevated and that it removes ALL activations on the machine.
2016-07-13_21-54-06

<#
    .SYNOPSIS
    This script locates OSPP.vbs and removes all product keys to trigger O365 reactivation. It will remove ALL product keys.
    .NOTES
    File Name: 
    Author   : Johan Dahlbom, johan[at]dahlbom.eu
    Blog     : 365lab.net
    The script is provided “AS IS” with no guarantees, no warranties, and they confer no rights.
#>
#Check that the script runs with privileged rights
if (-not([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    Write-Warning "You need to have Administrator rights to run this script!`nPlease re-run this script as an Administrator in an elevated powershell prompt!"
    break
}
#Find OSPP.vbs path and run the command with the dstatus option (Last 1...)
$OSPP = Resolve-Path -Path "C:\Program Files*\Microsoft Offic*\Office*\ospp.vbs" | Select-Object -ExpandProperty Path -Last 1
Write-Output -InputObject "OSPP Location is: $OSPP"
$Command = "cscript.exe '$OSPP' /dstatus"
$DStatus = Invoke-Expression -Command $Command

#Get product keys from OSPP.vbs output.
$ProductKeys = $DStatus | Select-String -SimpleMatch "Last 5" | ForEach-Object -Process { $_.tostring().split(" ")[-1]}

if ($ProductKeys) {
    Write-Output -InputObject "Found $(($ProductKeys | Measure-Object).Count) productkeys, proceeding with deactivation..."
    #Run OSPP.vbs per key with /unpkey option.
    foreach ($ProductKey in $ProductKeys) {
        Write-Output -InputObject "Processing productkey $ProductKey"
        $Command = "cscript.exe '$OSPP' /unpkey:$ProductKey"
        Invoke-Expression -Command $Command
    }
} else {
    Write-Output -InputObject "Found no keys to remove... "
}

Hope this helps you if running in to this issue and as always – let me know if you have any questions!

/Johan

Recursively enumerate Azure AD Group members with PowerShell

Many of the scripts used to assign licenses for Azure AD / Office 365 users are utilizing groups to assign the licenses. On the topic I have received quite a lot requests on nested group support, which is not possible with Get-MsolGroupMember as of now.

To solve this problem, I have built a simple function canned Get-JDMSOLGroupMember. It checks if a group consists of other groups, and loops through them until all group members of all groups have been enumerated. In the end it will sort the members so that no duplicate member records are returned. It will only return users, not groups.

See the screenshot below on how it works:
2016-01-07_23-46-04

Get-JDMsolGroupMember

function Get-JDMsolGroupMember { 
<#
.SYNOPSIS
    The function enumerates Azure AD Group members with the support for nested groups.
.EXAMPLE
    Get-JDMsolGroupMember 6d34ab03-301c-4f3a-8436-98f873ec121a
.EXAMPLE
    Get-JDMsolGroupMember -ObjectId  6d34ab03-301c-4f3a-8436-98f873ec121a -Recursive
.EXAMPLE
    Get-MsolGroup -SearchString "Office 365 E5" | Get-JDMsolGroupMember -Recursive
.NOTES
    Author   : Johan Dahlbom, johan[at]dahlbom.eu
    Blog     : 365lab.net 
    The script are provided “AS IS” with no guarantees, no warranties, and it confer no rights.
#>

    param(
        [CmdletBinding(SupportsShouldProcess=$true)]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0)]
        [ValidateScript({Get-MsolGroup -ObjectId $_})]
        $ObjectId,
        [switch]$Recursive
    )
    begin {
        $MSOLAccountSku = Get-MsolAccountSku -ErrorAction Ignore -WarningAction Ignore
        if (-not($MSOLAccountSku)) {
            throw "Not connected to Azure AD, run Connect-MsolService"
        }
    } 
    process {
        Write-Verbose -Message "Enumerating group members in group $ObjectId"
        $UserMembers = Get-MsolGroupMember -GroupObjectId $ObjectId -MemberObjectTypes User -All
        if ($PSBoundParameters['Recursive']) {
            $GroupsMembers = Get-MsolGroupMember -GroupObjectId $ObjectId -MemberObjectTypes Group -All
            if ($GroupsMembers) {
                Write-Verbose -Message "$ObjectId have $($GroupsMembers.count) group(s) as members, enumerating..."
                $GroupsMembers | ForEach-Object -Process {
                    Write-Verbose "Enumerating nested group $($_.Displayname) ($($_.ObjectId))"
                    $UserMembers += Get-JDMsolGroupMember -Recursive -ObjectId $_.ObjectId 
                }
            }
        }
        Write-Output ($UserMembers | Sort-Object -Property EmailAddress -Unique) 
        
    }
    end {
    
    }
}

As usual, let me know if you find issues or have any other suggestions.

Enjoy!

/Johan

License your Office 365/Azure AD users with Azure Automation

A while ago I wrote a post regarding schedule your Office 365 automation jobs in Azure Automation. At that time, the Azure AD PowerShell module did not support Azure Automation due to the Online Services Sign in Assistant dependency.

The preview Azure AD PowerShell module released in september do however use ADAL for authentication which solves this problem.
This is great news for all currently running licensing scripts in regular servers as scheduled tasks!

Since many probably will move from their scheduled tasks to Automation for their licensing scripts, I thought it was a good idea sharing on how to make the switch. I will use the “new” Azure portal and assume that you already have an Automation account.

1. Since the MSOnline module is not included by default in Azure Automation you need to download and install the module on your computer. When that is done you need to add the module folders (MSOnline and MSOnlineExtended) to a zip file each. The easiest way to do that is of course with PowerShell 🙂
The following code assumes that you have installed the modules in the default location and the zip files will be put on your desktop.

Add-Type -AssemblyName 'system.io.compression.filesystem'

Get-ChildItem -Path $PSHOME\Modules\MSonline* | ForEach-Object -Process {
    $Source = $_.FullName
    $Destination = '{0}\Desktop\{1}.zip' -f $env:USERPROFILE,$_.Name  
    [Io.compression.zipfile]::CreateFromDirectory($Source, $Destination) 
}

2. Upload the zipped modules files as assets to your automation account.
2015-11-28_17-38-18
2015-11-28_17-43-302015-11-28_17-42-44
3. Now add your license service account as an asset to your automation account.
If you haven’t created one you can use the below PowerShell lines to do it. Note that the account only needs to have the “User management administrator” role.

#Create the service account
New-MsolUser -UserPrincipalName licenserobot@acme333.onmicrosoft.com `
             -DisplayName "LicenseRobot" `
             -FirstName "License" `
             -LastName "Robot" `
             -Password 'password' `
             -ForceChangePassword $false `
             -PasswordNeverExpires $true 
#Add the user as an "User Account Administrator"
Add-MsolRoleMember -RoleName "User Account Administrator" `
                   -RoleMemberEmailAddress "licenserobot@acme333.onmicrosoft.com"

2015-11-28_18-06-16
4. Create your runbook and import the script. In this case I am using a slight modified version of this script, which you will find if you expand the script below below. This version are using the GroupObjectGuid to identify the groups instead of the DisplayName. Since the script is using Write-Output and Write-Warning for logging, Azure Automation will automatically handle basic logging.

LicenseAzureADUsers

#Connect to Azure AD
$Credentials = Get-AutomationPSCredential -Name 'LicenseRobot'
	
Connect-MsolService -Credential $Credentials

$Licenses = @{
                 'E1' = @{ 
                          LicenseSKU = 'tenant:STANDARDPACK'
                          Group = 'cb41a390-4312-4ecf-896e-086f64652690'
                          DisabledPlans = "SHAREPOINTSTANDARD"
                        }                        
                
                 'E3' = @{ 
                          LicenseSKU = 'tenant:ENTERPRISEPACK'
                          Group = '4549bd70-232a-4b71-8afe-4d431e5464ae'
                          DisabledPlans = "SHAREPOINTENTERPRISE","SHAREPOINTWAC"
                        }
                 'EMS' = @{
                         LicenseSKU = 'tenant:EMS'
                         Group = '9abc1736-049b-4c97-9fb6-8b6b02f9e7d5'
                 }
            
            }
    
$UsageLocation = 'SE'
    
#Get all currently licensed users and put them in a custom object
$LicensedUserDetails = Get-MsolUser -All -Synchronized | Where-Object {$_.IsLicensed -eq 'True'} | ForEach-Object {
    [pscustomobject]@{
                UserPrincipalName = $_.UserPrincipalName
                License = $_.Licenses.AccountSkuId
                DisabledPlans = $_.Licenses.Servicestatus | Where-Object {$_.Provisioningstatus -contains "Disabled"}
    }
}
   
#Create array for users to change or delete
$UsersToChangeOrDelete = @()
    
foreach ($license in $Licenses.Keys) {
      
    #Get current group name and ObjectID from Hashtable
    $GroupID = $Licenses[$license].Group
    $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU}
    $DisabledPlans =  $licenses[$license].DisabledPlans
    Write-Output "Checking for unlicensed $license users in group $GroupID..."
    #Get all members of the group in current scope
    $GroupMembers = ''
    $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All).EmailAddress
    #Get all already licensed users in current scope
    $ActiveUsers = ($LicensedUserDetails | Where-Object {$_.License -eq $licenses[$license].LicenseSKU}).UserPrincipalName 
    $UsersToHandle = ''
    $UsersToAdd = ''

    if ($GroupMembers) {  
        
        if ($ActiveUsers) {  
            #Compare $Groupmembers and $Activeusers
            #Users which are in the group but not licensed, will be added
            #Users licensed, but not, will be evaluated for deletion or change of license 
            $UsersToHandle = Compare-Object -ReferenceObject $GroupMembers -DifferenceObject $ActiveUsers -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            $UsersToAdd = ($UsersToHandle | Where-Object {$_.SideIndicator -eq '<='}).InputObject
            $UsersToChangeOrDelete += ($UsersToHandle | Where-Object {$_.SideIndicator -eq '=>'}).InputObject  
        } else {  
            #No licenses currently assigned for the license in scope, assign licenses to all group members.
            $UsersToAdd = $GroupMembers
        }
    
    } else {  
        Write-Warning  "Group $GroupID is empty - will process removal or move of all users with license $($AccountSKU.AccountSkuId)"
        #If no users are a member in the group, add them for deletion or change of license.
        $UsersToChangeOrDelete += $ActiveUsers
    }
            
    if ($UsersToAdd -match "[.]") {
  
        foreach ($User in $UsersToAdd){
    
            #Process all users for license assignment, if not already licensed with the SKU in order.  
            $MsolUser = Get-MsolUser -UserPrincipalName $User
           
            #Assign licenses for users
            if ($MsolUser.Licenses.AccountSkuId -notcontains $AccountSku.AccountSkuId) {
                try {  
                    #Assign UsageLocation and License.
                    $LicenseAssignmentHt = @{
                        UserPrincipalname = $User
                        AddLicenses = $AccountSKU.AccountSkuId
                    }
                    #Set custom license options to not enable SharePoint / OneDrive by Default
                    
                    if ($DisabledPlans) {
                        $LicenseOptions = New-MsolLicenseOptions -AccountSkuId $AccountSKU.AccountSkuId -DisabledPlans $DisabledPlans
                        $LicenseAssignmentHt["LicenseOptions"] = $LicenseOptions
                    }
                    Set-MsolUser -UserPrincipalName $user -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
                    Set-MsolUserLicense @LicenseAssignmentHt -ErrorAction Stop -WarningAction Stop
                    Write-Output "SUCCESS: Licensed $User with $license"
                } catch {  
                    Write-Warning "Error when licensing $User$_"
    
                }
            
            }  
        }
    }
}
    
#Process users for change or deletion
if ($UsersToChangeOrDelete -ne $null) {
        foreach ($User in $UsersToChangeOrDelete) {
          if ($user -ne $null) {
    
            #Fetch users old license for later usage
            $OldLicense = ($LicensedUserDetails | Where-Object {$_.UserPrincipalName -eq $User}).License
                 
             #Loop through to check if the user group assignment has been changed, and put the old and the new license in a custom object.
             #Only one license group per user is currently supported.
             $ChangeLicense = $Licenses.Keys | ForEach-Object {
                  $GroupID = $Licenses[$_].Group
                  if (Get-MsolGroupMember -All -GroupObjectId $GroupID | Where-Object {$_.EmailAddress -eq $User}) {
                     [pscustomobject]@{
                        OldLicense = $OldLicense
                        NewLicense = $Licenses[$_].LicenseSKU
                     }
                  } 
    
              }
    
              if ($ChangeLicense) {
                    #The user were assigned to another group, switch license to the new one.
                    try {  
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $ChangeLicense.OldLicense -AddLicenses $ChangeLicense.NewLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Changed license for user $User from $($ChangeLicense.OldLicense) to $($ChangeLicense.NewLicense)"
                    } catch {  
                          Write-Warning "Error when changing license on $User`r`n$_"
                    }
                      
              } else {  
                               
                    #The user is no longer a member of any license group, remove license
                    Write-Warning "$User is not a member of any group, license will be removed... "
                    try {  
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $OldLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Removed $OldLicense for $User"
                    } catch {  
                          Write-Warning "Error when removing license on user`r`n$_"
                    }
              }
         }
    }
}

2016-01-03_14-33-11
2016-01-03_15-29-22
5. Verify that the runbook is working as intended by starting it in the test pane. 2016-01-03_14-54-40
Make sure to solve eventual errors before you publish and schedule the runbook. The most common error is login errors for the automation service account.
6. Publish and schedule the runbook. In my case I will run it one time every hour.
2016-01-03_15-21-34
2016-01-03_15-19-28

You are now all set and can hopefully disable a scheduled task in your on premises environment. If thinking about running this in production, keep in mind that the PowerShell module is still in preview (I haven’t experienced any problems, but…).

Let me know if you have any questions or suggestions, and happy automation!

/Johan

From Password Sync to ADFS – Reset the state of Azure AD Connect

Setting up your identity bridge using Azure AD Connect requires you to choose authentication/user sign in methods for your users as in the screenshot below.
(A detailed guide on the entire installation can be found here.)
2015-09-27_15-20-37

If you for example have chosen to use Password Synchronization and later want to reconfigure your solution to get SSO with ADFS, you don’t have the possibility to change authentication mode in the AAD Connect wizard after having it configured once.
2015-09-27_15-09-59
2015-09-27_18-33-33

In this case, we could of course just configure our ADFS servers from outside AAD Connect to achieve our goal with SSO.

If we really want to use AAD Connect, we have to “reset the state” of AAD Connect. How this is done is described in KB3008643. The KB do however refer to an xml file which should be present in %LocalAppData%. Trying to do a “state reset” a while ago, I didn’t manage to find the file there.
I did however find a file named PersistedState.xml in C:\ProgramData\AADConnect. 2015-09-27_18-51-51
Hopefully the KB article will be updated to reflect the reality soon.

Renaming the PersistedState.xml file will make the wizard run as it did in the initial state, where you have the possibility to change the authentication settings. Keep in mind that you will need to reconfigure all other settings again. If you are unsure about the settings, make sure to make a proper backup of your synchronization settings prior to doing any changes.
2015-09-27_18-58-52

Hope this helps if running in to this issue and let me know if you have any questions!

/Johan

Exchange Online (Archive) offboarding – “Multiple objects with Guid were found”

Quite a few customers that are using Exchange Online Archiving in an Exchange Hybrid environment, where they have their primary mailboxes on-premises and the archive mailboxes in Exchange Online. This makes a very flexible solution and a perfect start to utilize but not go all in to the cloud.

Just as in a “regular” hybrid environment, you have the possibility to on and offboard the archive mailboxes from and to the on-premises Exchange server. While testing offboarding of an archive mailbox a while ago, I ran in to an interesting error related to the ExchangeGuid attribute.

The exact error message trying to start the moverequest was as follows:
2015-08-19_07-54-19
“Multiple objects with Guid 1b2eaa95-0d64-4469-9fb2-d8f9be3e28ce were found”

Multiple objects with the same Guid, could that really be true? Investigating further confirmed the error message. All users in Exchange Online/Office365 had the same value in the ExchangeGuid attribute, even if they didn’t have an archive mailbox.
2015-08-18_12-44-21

SOLUTION
The following kb describes a similiar problem in a hybrid environment, but the exact solution could not be applied in my scenario since the RemoteMailbox-cmdlets isn’t working in EOA scenarios. It do however state that in order for a successful move, the ExchangeGuid should be the same on-premises as in the cloud.

After doing the change on one user as below the move started and completed successfully.

#Connect to Exchange Online and Exchange On Premises with prefixes
$cred = Get-Credential
$OnPremSess = New-PSSession -ConfigurationName Microsoft.Exchange `
                            -Authentication Kerberos `
                            -ConnectionUri http://exchangeserver/powershell
$O365Sess = New-PSSession -ConfigurationName Microsoft.Exchange `
                          -Authentication Basic `
                          -ConnectionUri https://outlook.office365.com/powershell-liveid/ `
                          -AllowRedirection:$true `
                          -Credential $cred
Import-PSSession $O365Sess -Prefix Cloud -AllowClobber
Import-PSSession $OnPremSess -Prefix OnPrem -AllowClobber

#Get the On-Premises mailbox
$Mailbox = Get-OnPremMailbox -Identity johan@365lab.net
#Get the Exchange Online MailUser
$MailUser = Get-CloudMailUser -Identity johan@365lab.net
#Change the ExchangeGuid to match the on-premises Guid
$MailUser | Set-CloudMailUser -ExchangeGuid $Mailbox.ExchangeGuid

Since it worked on one user, I then decided to to the change on all users that had EOA activated, as below:

#Get all Mailboxes with EOA Activated
$Mailboxes = Get-OnPremMailbox -Filter {(Archivestate -eq "HostedProvisioned")}
foreach ($Mailbox in $Mailboxes) {
    $CloudUser = Get-CloudMailUser -Identity $mailbox.userprincipalname
    if ($CloudUser.ExchangeGuid -ne $mailbox.ExchangeGuid) {
        $clouduser | Set-CloudMailUser -ExchangeGuid $mailbox.ExchangeGuid
    }
}

I still have a MS support case open regarding how the attribute ended up the same on all users and will update the post when we have identified the root cause.

Let me know if you have questions!

/Johan