Azure AD Integration

This year I’ve been working a lot more with Azure. On of my tasks has been to integrate other application to each other using Azure AD. Here are some of my finding and good to know things in case someone else runs into them:

  • Create a new MVC Application in Visual Studio
  • When this is done, press the second mouse button on the project and go to “Add” > “Connected Services”. This will allow you to create an O365 connection.
  • Select Office 365 API Services.
  • In the new Window select or type in your domain.
  • Next, create new Azure AD application configuration or use an existing one by providing the GUID for the application.
    • Make sure you select the “Configure Single Sign-On using Azure AD” option
    • Make sure that your application is multi-tenant:
      • This works so that you register your app with you own Azure AD domain, then after that external Azure AD tenants and their users are registered through an “onboarding” process. The process will the user or admin user for privileges to use certain information from the AD or other resources. These are defined in the Azure AD application settings.
      • Notice you are using an application ID and key to connect to your own organization Azure AD then the users are only onboarding using the multi-tenant option in the Azure AD application configuration.
    • Next select what kind of privileges your application needs from the Azure AD and O365.
    • You need onboarding functionality from here: https://azure.microsoft.com/en-us/documentation/samples/active-directory-dotnet-webapp-multitenant-openidconnect/
    • In Global.asax.cs application_start function add the following: AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
      • If this is missing, then you claim will not work properly.
    • If you are using a SQL Server database and Entity Framework remember to update you model from the database and remember primary key connections. If the Entity Framework update does not work then removing and adding the database tables should force an update. Also remember to clean and build your project if nothing else helps.
    • If you get this error: Error: {“error”:”invalid_grant”,”error_description”:”AADSTS70002: Error validating credentials. AADSTS70000: The provided access grant is invalid or malformed…..
    • Doing a redirect the proper way in an MVC apllication using the following piece of code in your contoller: return Redirect(returnUrl);
      • If you use the normal way in ASP .NET: Response.Redirect(returnUrl); you will run into trouble. The error message might look something like this:
        • Server Cannot Append Header After HTTP headers have been sent Exception at @Html.AntiForgery
        • You could set the AntiForgeryConfig.SuppressXFrameOptionsHeader = true; in the Application_start, but this will lower your security and not advisable.

 

Advertisements

SharePoint change document set and items content type to a new content type

I’ll put it simply:

This is a PowerShell script that you can use to change a content type of a library or list to another one. This script can identify between Document Sets and library or list items.


param (
[string]$WebsiteUrl = "http://portal.spdev.com/",
[string]$OldCTName = "DSTestCT",
[string]$NewCTName = "DSTestCT"
)

if ( (Get-PSSnapin -Name MicroSoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null )
{
Add-PsSnapin MicroSoft.SharePoint.PowerShell
}

function Reset-ListContentType ($WebUrl, $ListName, $OldCTName, $NewCTName)
{
$web = $null
try
{
$web = Get-SPWeb $WebUrl

$list = $web.Lists.TryGetList($ListName)
$oldCT = $list.ContentTypes[$OldCTName]

$isChildOfCT = $list.ContentTypes.BestMatch($rootNewCT.ID).IsChildOf($rootNewCT.ID);
if($oldCT -ne $null -and $isChildOfCT -eq $false)
{
$hasOldCT = $true
$isFoldersCTReseted = Reset-SPFolderContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
Reset-SPFileContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
Remove-ListContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
if($hasOldCT -eq $true)
{
Add-ListContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
if($isFoldersCTReseted -eq $true)
{
Set-SPFolderContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
}
}
}


}catch
{

}
finally
{
if($web)
{
$web.Dispose()
}
}

}

function Remove-ListContentType ($web, $list, $OldCTName, $NewCTName)
{


$oldCT = $list.ContentTypes[$OldCTName]

$isChildOfCT = $list.ContentTypes.BestMatch($oldCT.ID).IsChildOf($oldCT.ID);

if($isChildOfCT -eq $true)
{
$list.ContentTypes.Delete($oldCT.ID)
}
$web.Dispose()

return $isChildOfCT
}

function Add-ListContentType ($web, $list, $OldCTName, $NewCTName)
{



$list.ContentTypes.Add($rootNewCT)

$web.Dispose()
}

function Reset-SPFolderContentType ($web, $list, $OldCTName, $NewCTName)
{
#Get web, list and content type objects

$isFoldersCTReseted = $false


$isChildOfCT = $list.ContentTypes.BestMatch($rootNewCT.ID).IsChildOf($rootNewCT.ID);

$oldCT = $list.ContentTypes[$OldCTName]
$folderCT = $list.ContentTypes["Folder"]
$newCT = $rootNewCT

$newCTID = $newCT.ID

#Check if the values specified for the content types actually exist on the list
if (($oldCT -ne $null) -and ($newCT -ne $null))
{
$list.Folders | ForEach-Object {

if ($_.ContentType.ID.IsChildOf($rootNewCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($oldCT.ID) -eq $true -and $_.Folder.ProgID -eq "Sharepoint.DocumentSet")
{
Write-Host "Found a document set: " $_.Name "Processing document set"
$item = $list.GetItemById($_.ID);
$item["ContentTypeId"] = $folderCT.Id
$item.Update()
$isFoldersCTReseted = $true
}
}
}

$web.Dispose()

return $isFoldersCTReseted
}

function Set-SPFolderContentType ($web, $list, $OldCTName, $NewCTName)
{
#Get web, list and content type objects



$folderCT = $list.ContentTypes["Folder"]
$newCT = $list.ContentTypes[$NewCTName]

#Check if the values specified for the content types actually exist on the list
if (($newCT -ne $null))
{
$list.Folders | ForEach-Object {
if ($_.ContentType.ID.IsChildOf($newCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($folderCT.ID) -eq $true -and $_.Folder.ProgID -eq "Sharepoint.DocumentSet")
{
$item = $list.GetItemById($_.ID);
$item["ContentTypeId"] = $newCT.Id
$item.Update()
}
}
}

$web.Dispose()
}


function Reset-SPFileContentType ($web, $list, $OldCTName, $NewCTName)
{
#Get web, list and content type objects



$isChildOfCT = $list.ContentTypes.BestMatch($rootNewCT.ID).IsChildOf($rootNewCT.ID);

$oldCT = $list.ContentTypes[$OldCTName]
$folderCT = $list.ContentTypes["Folder"]
$newCT = $rootNewCT

$newCTID = $newCT.ID

#Check if the values specified for the content types actually exist on the list
if (($oldCT -ne $null) -and ($newCT -ne $null))
{
$list.Folders | ForEach-Object {
if ($_.ContentType.ID.IsChildOf($rootNewCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($oldCT.ID) -eq $true)
{
$_["ContentTypeId"] = $folderCT.Id
$_.Update()
}
}
#Go through each item in the list
$list.Items | ForEach-Object {
Write-Host "Item present CT ID :" $_.ContentType.ID
Write-Host "CT ID To change from :" $oldCT.ID
Write-Host "NEW CT ID to change to:" $rootNewCT.ID

#Check if the item content type currently equals the old content type specified
if ($_.ContentType.ID.IsChildOf($rootNewCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($oldCT.ID) -eq $true)
{
#Check the check out status of the file
if ($_.File.CheckOutType -eq "None")
{
Change the content type association for the item
$item = $list.GetItemById($_.ID);
$item.File.CheckOut()
write-host "Resetting content type for file: " $_.Name "from: " $oldCT.Name "to: " $newCT.Name

$item["ContentTypeId"] = $newCTID
$item.UpdateOverwriteVersion()
Write-Host "Item changed CT ID :" $item.ContentType.ID
$item.File.CheckIn("Content type changed to " + $newCT.Name, 1)
}
else
{
write-host "File" $_.Name "is checked out to" $_.File.CheckedOutByUser.ToString() "and cannot be modified"
}
}
else
{
write-host "File" $_.Name "is associated with the content type" $_.ContentType.Name "and shall not be modified"
}
}
}
else
{
write-host "One of the content types specified has not been attached to the list"$list.Title
return
}

$web.Dispose()
}

$web = Get-SPWeb $WebsiteUrl
$rootWeb = $web.Site.RootWeb;
$rootNewCT = $rootWeb.AvailableContentTypes[$NewCTName]

Foreach ($list in $web.Lists) {
Write-Host $list.BaseType
if($list.Hidden -eq $false -and $list.BaseType -eq "DocumentLibrary")
{
Write-Host "Processing list: " $list.Title
Reset-ListContentType –WebUrl $WebsiteUrl –ListName $list.Title –OldCTName $OldCTName –NewCTName $NewCTName
}
}

$web.Dispose()

 

How to display all of the refinement options in SharePoint 2013 Search results

By default for some reason, I could not show all of the items, I could define how many items are shown and but not get rid of the “show more” links under the refinements.

This JavaScript snippet can help you. It will hide the “short list” containing a minimum of items to be displayed, then it will show the long list and finally it will hide the “show more” link.

</pre>
<script>
 $( document ).ready(function() {
$('#unselLongList').show();
$('#unselShortList').hide();
$('#unselToggle').hide();

});
</script> 

SharePoint Search and changing the Created and LastModified fields

If you want to change the LastModified and Created fields of a list item or a document you can use this code to do that:


SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite site = new SPSite("site + web url"))
{
using (SPWeb web = site.OpenWeb())
{
web.AllowUnsafeUpdates = true;
SPListItem listItem = web.GetListItem(this.tbItemURL.Text);

if (this.cCreatedDate.SelectedDate != new DateTime())
listItem[SPBuiltInFieldId.Created] = this.cCreatedDate.SelectedDate;
else
listItem[SPBuiltInFieldId.Created] = listItem[SPBuiltInFieldId.Created];

if (this.cModifiedDate.SelectedDate != new DateTime())
listItem[SPBuiltInFieldId.Modified] = this.cModifiedDate.SelectedDate;
else
listItem[SPBuiltInFieldId.Modified] = listItem[SPBuiltInFieldId.Modified];

listItem.UpdateOverwriteVersion();
if (listItem.ParentList.EnableMinorVersions)
{
listItem.File.Publish("SPFileUpload");
}
if (listItem.ModerationInformation != null)
{
listItem.File.Approve("SPFileUpload");
}




web.AllowUnsafeUpdates = false;
}
}
});

The reason why the above is done is because if you simply change the date values and use the UpdateOverwireVersion() it will not be enough. The search will not be able to index right values EVEN if you check the UI or use PowerShell or SharePoint Manager etc. All of those show that the value is OK but actually is not. The manually inserted dates will not show up in search unless the item or document is published and approved. So no matter that SP says remember this :).

another thing is that this code above does not allow to set a LastModified IF you have publishing/versioning enabled. After calling the UpdateOverwriteVersion function the item or document will be checked out and remains in that state until something is done. Once you do that you are back to where you started the modified date is changed automatically by SP.

Do not have a solution yet. Maybe you have?

PowerShell script how to backup lists and restore them in another location through list templates

Ok, this is one way of doing this. Perhaps this is useful in a situation where you need to backup lists with their data from an older SharePoint version and restore it in another SharePoint version. Preferably doing this quickly without worrying about site templates or something more than the list and its data.

The script below gets lists in a target web location, saves them as list templates and download them in the hard drive.

Approach 1: Export the list through Export-SPWeb and restore with Import-SPWeb

param (
 [string]$SPSiteFilter = "http://portal.spdev.com/",
 [string]$SPWebFilter = "http://portal.spdev.com/",
 [string]$SPWebFilterRelative = "",
 [string]$destination = "C:\\temp"
 )
 
 $site = Get-SPWeb $SPSiteFilter
 
 
 # Get the webs lists and save them as list templates
 $lists = $site.lists
Foreach ($list in $lists) {
Write-Host $list.BaseType
 if($list.Hidden -eq $false -and $list.BaseType -eq "GenericList")
 {
 Write-Host $list.Title + " " + $list.RootFolder.ServerRelativeUrl
 
 $filePath = $destination + "\" + $list.Title + ".cmp"
 $itemURL = $SPWebFilterRelative + $list.RootFolder.ServerRelativeUrl
 Write-Host $filePath
 Write-Host $itemURL
 Write-Host $SPWebFilter
 export-spweb -identity $SPWebFilter -path $filePath -itemurl $itemURL –includeusersecurity
 #$list.SaveAsTemplate($list.Title+ " " + $ListTempalteIDPostFix,$list.Title,"Site list Backup",1)
 }
 }

 $site.Dispose()
param (
 [string]$SPSiteFilter = "http://portal.spdev.com/subsite/",
 [string]$source = "C:\\temp"
 )
 
 $site = Get-SPWeb $SPSiteFilter
 
 Get-ChildItem $source -Filter *.cmp | 
Foreach-Object {
 Write-Host $_.FullName
 #Upload the list temaplte to the list templates gallery
 Import-SPWeb -identity $SPSiteFilter -path $_.FullName –force
 
}

$site.Dispose()

Approach 2: As list templates


param (
[string]$SPSiteFilter = &quot;http://site/&quot;,
[string]$SPWebFilter = &quot;http://site/myweb/&quot;,
[string]$ListTempalteIDPostFix = &quot;SU&quot;,
[string]$destination = &quot;C:\\temp&quot;
)

$site = Get-SPSite $SPSiteFilter
$web = Get-SPWeb $SPWebFilter
$templatesLibrary = $site.RootWeb.GetListFromUrl(&quot;/_catalogs/lt/Forms/AllItems.aspx&quot;)

if(!$templatesLibrary)
{
Write-Host &quot;List template gallery not found. Program execution halted.&quot;
return
}

# Get the webs lists and save them as list templates
$lists = $web.lists
Foreach ($list in $lists) {
Write-Host $list.BaseType
if($list.Hidden -eq $false -and $list.BaseType -eq &quot;GenericList&quot;)
{
Write-Host $list.Title
$list.SaveAsTemplate($list.Title+ &quot; &quot; + $ListTempalteIDPostFix,$list.Title,&quot;Site list Backup&quot;,1)
}
}

# Read the list templates from the gallery and save them to the hard drive
$folder = $templatesLibrary.RootFolder
foreach ($file in $folder.Files) {
#Ensure destination directory
$destinationfolder = $destination

#Download file
$binary = $file.OpenBinary()
$stream = New-Object System.IO.FileStream($destinationfolder + &quot;/&quot; + $file.Name), Create
$writer = New-Object System.IO.BinaryWriter($stream)
$writer.write($binary)
$writer.Close()

}
For ($i=$folder.Files.Count; $i -gt 0 -or $i -eq 0; $i--)
{
$folder.Files[$i].Delete()
}

$web.Dispose()
$site.Dispose()

This script uploads the list templates into the targeted SharePoint site.

param (
[string]$SPSite = &quot;http://portal3.dev.com/sites/sc&quot;,
[string]$SiteTemplatesLocation = &quot;C:\\temp&quot;,
[string]$SiteTemplatesGalleryName = &quot;List Template Gallery&quot;
)
# $site = Get-SPSite $SPSiteFilter
# $web = Get-SPWeb $SPSiteFilter
# $templatesLibrary = $site.RootWeb.GetListFromUrl(&quot;/_catalogs/lt/Forms/AllItems.aspx&quot;)
# $Files = $templatesLibrary.RootFolder.Files

Function UploadListTemplate($WebURL, $TemplateFilePath)
{
#Get the Web
$web = Get-SPWeb $WebURL
#Get the List template Gallery Folder
$TemplateFolder = $web.GetFolder($SiteTemplatesGalleryName)
#Get the Files collection
$TemplateFileCollection = $TemplateFolder.Files
#Get the Template file from Local File system
$TemplateFile = Get-ChildItem $TemplateFilePath

#Open the File in Read mode and Add to Templates collection
$TemplateFileCollection.Add(&quot;_catalogs/lt/$($TemplateFile.Name)&quot;, $TemplateFile.OpenRead(), $true)
Write-Host &quot;Done!Template has been uploaded!!&quot;
$web.Dispose()

}

Get-ChildItem $SiteTemplatesLocation -Filter *.stp |
Foreach-Object {
Write-Host $_.FullName
#Upload the list temaplte to the list templates gallery
UploadListTemplate $SPSite $_.FullName

}

This script will create the lists based on the list templates NOTICE that this does not differentiate between existing list templates not uploaded with the script above.

 param (
 [string]$SPSiteFilter = &quot;http://portal3.dev.com/sites/sc&quot;,
 [string]$ListTempalteIDPostFix = &quot;SU&quot;,
 [string]$destination = &quot;C:\\temp&quot;
 )

 $site = Get-SPSite $SPSiteFilter
 $web = Get-SPWeb $SPSiteFilter

 # Create lists based on the list templates
$site.GetCustomListTemplates($web) |
Foreach-Object {
if($web.Lists.TryGetList($_.Name.Replace($ListTempalteIDPostFix,&quot;&quot;)) -eq $null)
{
 $web.Lists.Add($_.Name.Replace($ListTempalteIDPostFix,&quot;&quot;), $_.Description, $_)
 }
}

 $web.Dispose()
 $site.Dispose()
 

If your list has content that is too large to save as a template try to run this script and remember to change to the default value which I think should be: 52428800


$docSize = 500000000
$webservice = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
$webservice.MaxTemplateDocumentSize = $docSize
$webservice.Update()

Prerequisites for custom list templates

SharePoint List must meet the following criteria to move a list by using a list template (source):

  • The list must contain less than 10 megabytes (MB) of list data.
  • You must be a member of a site group with the Manage Lists right on the source site and on the destination site. By default, the Manage Lists right is included in the Web Designer and Administrator site groups.
  • The source site and the destination site must be based on the same site template.

How to use Microsoft Graph to get office groups listed in a native (Console) application

To get groups from using Microsoft Graph you have to do the following things:

  • Register an application using Azure AD and give required privileges to the application
  • Request a token for your application using the client ID and client secret key
  • Make the API call to using REST by specifying that your realm where to get the groups from

Register your application through Azure AD

Go to https://portal.azure.com/ or https://manage.windowsazure.com

Next from the Azure navigation go to your active directory. Next you should see different options in the top area of the your AAD, such as:

Users, Groups, Applications, Domain and so on.

Select the Applications section.

aad1

Next you should get a listing of applications configured in your AAD.

Next from the AAD UI select ADD to add a new application.

aad2

Select what type of an application it is. You can use both option. I used the configurations of an MVC application which allowed me to work just fine. What matters is the client ID, client secret key and the privileges. The redirect URL is not necessary for a Native Application, you can type in anything as long as it is a URL.

aad4aad5

 

After your application is created go to the configure section.

aad6

Here find the client ID and copy store it somewhere to be used later.

Then go to the keys section and add a new key(client secret)

aad7

Then scroll down and go the applications section and add the Microsoft Graph application and select the needed privileges. In this case, you would like at least to be able to read groups.

aad8aad9

That is it, next is some code.

Request a token for your application

This is the piece of code that will get the token from your application. The constants and parameters will be explained soon.

public static string GetTokenForApplication(String realm, String clientId, String clientSecret)
 {
 AuthenticationContext authenticationContext = new AuthenticationContext(Constants.UnifiedApiConnect.AzureADAuthority + realm, false);
 // Config for OAuth client credentials 
 ClientCredential clientCred = new ClientCredential(clientId, clientSecret);

 AuthenticationResult authenticationResult = authenticationContext.AcquireToken(Constants.UnifiedApiConnect.O365UnifiedAPIResource,
 clientCred);
 string token = authenticationResult.AccessToken;
 return token;
 }

Make the API Call to get the groups

This is the code that will get the token and request data from the Graph API


TokenHelper.Token = Program.GetTokenForApplication(realm, clientId, clientSecret);
 List&amp;lt;Group&amp;gt; groups = new List&amp;lt;Group&amp;gt;();
 string APIURL = Constants.UnifiedApiConnect.O365UnifiedAPIResource + "v1.0/" + realm + "/groups?$filter=groupTypes/any(c:c+eq+'Unified')";
 try
 {
 groups = GroupsHttpHelper.GetGroups(APIURL);
 foreach (var group in groups)
 {
 groupsSite.Add(new SiteDirectoryEntity() { Title = group.displayName, URL = String.Format(Program.O365OutlookMailGroupURL, realm, realmLCID, group.mail) });
 }
 }
 catch (Exception ex)
 {
 Logger.Error("Error in processing O365 groups through MS Graph: " + ex.Message + "\n" + ex.StackTrace);
 }

This is the code that will do the request to the Graph API, notice that the data is deserialized into objects.

 


public class GroupsHttpHelper
 {
 public static List&amp;lt;Group&amp;gt; GetGroups(string apiUrl)
 {
 if (String.IsNullOrEmpty(apiUrl)) { throw new ArgumentNullException("apiUrl"); }

 List&amp;lt;Group&amp;gt; groups = new List&amp;lt;Group&amp;gt;();

 string responseContent = GroupsHttpHelper.GetHttpResource(apiUrl);
 var responseObject = JsonConvert.DeserializeObject&amp;lt;GraphResponse&amp;lt;Group&amp;gt;&amp;gt;(responseContent);
 foreach (var item in responseObject.value)
 {
 groups.Add(item);
 }

 return groups;

 }

 public static string GetHttpResource(string url)
 {
 string responseContent = String.Empty;

 string token = TokenHelper.Token;

 var request = (HttpWebRequest)HttpWebRequest.Create(url);

 request.Method = "GET";
 request.Accept = "application/json";
 request.Headers.Add("Authorization", "Bearer " + token);

 var response = request.GetResponse();
 using (var reader = new StreamReader(response.GetResponseStream()))
 {
 responseContent = reader.ReadToEnd();
 }

 return responseContent;
 }
 }

public class Group
 {
 public string accessType { get; set; }

 public bool? allowExternalSenders { get; set; }

 public bool? autoSubscribeNewMembers { get; set; }
 public string description { get; set; }
 public string displayName { get; set; }
 public string[] groupTypes { get; set; }
 public string id { get; set; } // identifier

 public bool isSubscribedByMail { get; set; }
 public string mail { get; set; }

 public bool? mailEnabled { get; set; }
 public string mailNickname { get; set; }
 public string onPremisesLastSyncDateTime { get; set; } //timestamp
 public string onPremisesSecurityIdentifier { get; set; }

 public bool? onPremisesSyncEnabled { get; set; }
 public string[] proxyAddresses { get; set; }

 public bool? securityEnabled { get; set; }
 public int unseenCount { get; set; }
 public string visibility { get; set; }
 }

Namespaces needed in the code:

using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;

NuGet Packages needed:

Active Directory Authentication Library – Id: Microsoft.IdentityModel.Clients.ActiveDirectory
Json.NET – Id: Newtonsoft.Json

WebConfig Values and parameters in the code

 

<add key=”MSGraphGourps_Realm” value=”yourrealm.com” />

<add key=”ClientId” value=”your client ID in the Azure AD application” />
<add key=”ClientSecret” value=”your client secret key in the Azure AD application” />

The URL format for the authentication context request is something like this: https://login.microsoftonline.com/yourrealm.com

To acquire the token: https://graph.microsoft.com/

To get the groups the URL is: https://graph.microsoft.com/1.0/yourrealm.com/groups

If you are not sure where to get your realm from there are two ways:

  1. Get it from the URL in your browser bar when you are viewing an O365 outlook mail box or group: https://outlook.office.com/owa/?realm=yourrealm.com
  2. The other option is to use the Azure Management UI and again in the url your should find it: https://manage.windowsazure.com/yourrealm.com

 

Getting other information from Groups

One Drive:

https://graph.microsoft.com/v1.0/your realm name/groups/{your group guid}/drive/root

Owners:

https://graph.microsoft.com/v1.0/your realm name/groups/{your group guid}/owners

Group Logo: