Quantcast
Channel: SharePoint Developer Support Team Blog
Viewing all 100 articles
Browse latest View live

Setup custom retention policy with custom Expiration Formula for custom Document Content Types in individual webs.

$
0
0

This post is a contribution from Manish Joshi, an engineer with the SharePoint Developer Support team

Custom Formula is implemented with Class CustomFormula.cs, compiled to a custom assembly and consumed through the following method:

Microsoft.Office.RecordsManagement.InformationPolicy.PolicyResourceCollection.Add(xmlExpirationFormula)

 

Following is sample code to install the custom formula

	    string strExpirationFormulaID = "testCustomExpirationFormula";
            string strExpirationFormulaName = "testCustomExpirationFormula";
            string strExpirationFormulaDesc = "testCustomExpirationFormula";

            string xmlExpirationFormula = "<PolicyResource xmlns=\"urn:schemas-microsoft-com:office:server:policy\"" +
                                                         " id = \"" + strExpirationFormulaID + "\"" +
                                                         " featureId=\"Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration\"" +
                                                         " type = \"DateCalculator\"> <Name>" + (strExpirationFormulaName) + "</Name>" +
                                                         "<Description>" + (strExpirationFormulaDesc) + "</Description>" +
                                                         "<AssemblyName>RetentionPolicy, Version=1.0.0.0, Culture=neutral," +
                                                         "PublicKeyToken=f3ef95b35506e557</AssemblyName>" +
                                                         "<ClassName>RetentionPolicy.CustomFormula</ClassName>" +
                                                         "</PolicyResource>";
            try
            {

                PolicyResourceCollection.Delete(strExpirationFormulaID);
            }
            catch (Exception ex)
            {

               // ex.Message.ToString();
            }

            PolicyResource.ValidateManifest(xmlExpirationFormula);
            PolicyResourceCollection.Add(xmlExpirationFormula);

 

To set custom retention policy for a content type with above registered CustomExpirationFormula, the sample code is:

private static string GeneratePolicyCustomData(string _strExpirationFormulaID)
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                sb.AppendLine("<Schedules nextStageId='3'>");
                sb.AppendLine("<Schedule type='Default'>");
                sb.AppendLine("<stages>");

                // Send Expiry Notification when Today = Near Expiry + 0 days
                sb.AppendLine("<data stageId='1'>");
                sb.AppendLine("<formula id='" + _strExpirationFormulaID + "'>");

                sb.AppendLine("</formula>");
                sb.AppendLine("<action type='action' id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin' />");
                sb.AppendLine("</data>");

                sb.AppendLine("</stages>");
                sb.AppendLine("</Schedule>");
                sb.AppendLine("</Schedules>");
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return sb.ToString();
        }

private void SetPolicy(string contentTypeName, string strExpirationFormulaID)
        {
            using (SPSite site = new SPSite(SPContext.Current.Web.Url))
            using (SPWeb web = site.OpenWeb())
            {
                SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.ToLower()).FirstOrDefault();
                if (contentType != null)
                {
                    try
                    {
                        if (Policy.GetPolicy(contentType) != null)
                        {
                            Policy.DeletePolicy(contentType);
                        }

                        Policy.CreatePolicy(contentType, null);
                        Policy policy = Policy.GetPolicy(contentType);

                        string policyCustomData = GeneratePolicyCustomData(strExpirationFormulaID);

                        policy.Items.Add("Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration", policyCustomData);
                        contentType.Update();

                    }
                    catch (Exception ex)
                    {
                        throw ex;
                    }
                }//EndOfIf

            }
        }

  string strExpirationFormulaID = "testCustomExpirationFormula";
  string customCT1 = "CustomDocument1";
  SetPolicy(customCT1, strExpirationFormulaID);

 

To set the custom retention policy at the web’s content type level, use following code to create a child content type for a web.
Be sure to use a different content type name for the root web’s content type though.

For root web,

	    SPContentType sdFileContentType = new SPContentType(contentTypes["SecureDocFile"], contentTypes, "SecureDocFile_rootWeb");
            sdFileContentType.Group = "CentRic Portal Content Types";
            web.ContentTypes.Add(sdFileContentType);

            SPContentType sdMessageContentType = new SPContentType(contentTypes["SecureDocMessage"], contentTypes, "SecureDocMessage_rootWeb");

 

For non-root web:

         SPContentType sdFileContentType = new SPContentType(contentTypes["SecureDocFile"], contentTypes, "SecureDocFile_Web");
            sdFileContentType.Group = "CentRic Portal Content Types";
            web.ContentTypes.Add(sdFileContentType);

            SPContentType sdMessageContentType = new SPContentType(contentTypes["SecureDocMessage"], contentTypes, "SecureDocMessage_Web");

Trigger workflow for an item in the list by using CSOM. (env: SP 2013 on-prem, provider hosted app) and App Only will not work

$
0
0

This post is a contribution from Manish Joshi, an engineer with the SharePoint Developer Support team

There seems to be a check in SharePoint when starting a workflow using CSOM if SharePoint 2013 workflow is started using the App-Only context – if yes, throw Access Denied exception and log this exception.

So there is no other way than to start a SharePoint 2013 workflow in CSOM except using the App+User context

Following code illustrates using an app only access token to create a ClientContext and start the SharePoint 2013 workflow in CSOM.

  1. Get the Client Context using the App only Access Token
    // get app only access token by passing the windows identity as null
    string appOnlyAccessToken = TokenHelper.GetS2SAccessTokenWithWindowsIdentity(hostWeb, null);
    return TokenHelper.GetClientContextWithAccessToken(hostWeb.ToString(), appOnlyAccessToken);
    

     

  2. Invoke the workflow using the Client Context retrieved
    // start the workflow using the CSOM APIs and the client context retrieved previously
    var workflowManager = new WorkflowServicesManager(clientContext, clientContext.Web);
    InteropService workflowInteropService = workflowManager.GetWorkflowInteropService();
    
    // call the StartWorkflow method   - https://msdn.microsoft.com/en-us/library/office/microsoft.sharepoint.client.workflowservices.interopservice.startworkflow.aspx
    workflowInteropService.StartWorkflow(workflowAssociation.Name, corId, list.Id, itemGuid, new Dictionary<string, object>());
    clientContext.ExecuteQuery();
    

     

The above code will throw AccessDenied exception.

 

To get this to work the code needs to be changed as below.

  1. Get the Client Context for App + User context access token
    // pass a valid windows identity to get app + user context
    string appUserAccessToken = TokenHelper.GetS2SAccessTokenWithWindowsIdentity(hostWeb, windowsIdentity);
    return TokenHelper.GetClientContextWithAccessToken(hostWeb.ToString(), appUserAccessToken);
    

     

  2. Invoke the workflow using the Client Context retrieved
    // start the workflow using the CSOM APIs and the client context retrieved previously
    var workflowManager = new WorkflowServicesManager(clientContext, clientContext.Web);
    InteropService workflowInteropService = workflowManager.GetWorkflowInteropService();
    
    // call the StartWorkflow method   - https://msdn.microsoft.com/en-us/library/office/microsoft.sharepoint.client.workflowservices.interopservice.startworkflow.aspx 
    workflowInteropService.StartWorkflow(workflowAssociation.Name, corId, list.Id, itemGuid, new Dictionary<string, object>());
    clientContext.ExecuteQuery();
    

Fixing error “The Request uses too many resources.” in CSOM / REST operation

$
0
0

This post is a contribution from Sohail Sayed, an engineer with the SharePoint Developer Support team

You may occasionally run into the following error when calling a CSOM API in SharePoint

Error : The Request uses too many resources.

If you check the ULS logs you can see the below error

Potentially excessive number of SPRequest objects (16) currently unreleased on thread 54. Ensure that this object or its parent (such as an SPWeb or SPSite) is being properly disposed. This object is holding on to a separate native heap.This object will not be automatically disposed. Allocation Id for this object: {D1CD1BB2-B94B-4129-8791-68DD2ED3C95A} Stack trace of current allocation:  

at Microsoft.SharePoint.SPGlobal.CreateSPRequestAndSetIdentity(SPSite site, String name, Boolean bNotGlobalAdminCode, String strUrl, Boolean bNotAddToContext, Byte[] UserToken, SPAppPrincipalToken appPrincipalToken, String userName, Boolean bIgnoreTokenTimeout, Boolean bAsAnonymous)  

at Microsoft.SharePoint.SPRequestManager.GetContextRequest(SPRequestAuthenticationMode authenticationMode)  

 

There is a check in the SPGlobal.CreateSPRequestAndSetIdentity method whether the number of SPRequest objects exceed a certain limit. If yes you will see the above error in the ULS logs and your CSOM request will fail with the error “The Request uses too many resources.”. The SPRequest objects may get created due to large number of SPSite / SPWeb objects being created by the CSOM request.

The default setting for the maximum number of SPRequest objects that can be created by the CSOM / REST calls is 16. Please note this limit is per CSOM request i.e. per ClientContext.ExecuteQuery call or REST call. This means that you can split your CSOM requests to overcome this error.

However, in some cases a single atomic CSOM operation will throw this error. This may happen in case of activating a feature which creates a lot of SPRequest objects or when provisioning a sub site using CSOM/REST.

The good news is that the default setting for the maximum number of SPRequest objects that can be created by the CSOM / REST calls can be modified using the below powershell command. Run this command from any one of the SharePoint servers.

$webApp = Get-SPWebApplication "<SITEURL>"

$webApp.ClientCallableSettings.MaxResourcesPerRequest = 50

$webApp.Update()

 

Note

This solution applies only for SharePoint On-Premises. This is server side setting and hence you cannot update this for SharePoint Online.

Keep a track on the memory usage on your servers after you update this setting. It is possible there is some custom component on server which is running bad code and may cause high memory usage. In such cases you may be able to fix the issue by optimizing the custom component code without update this setting.

 

Always use File Chunking to Upload Files > 250 MB to SharePoint Online

$
0
0

This post is a contribution from Adam Burns, an engineer with the SharePoint Developer Support team

Some developers may have been confused by some of the information available on blogs (and even on MSDN) about the upper limit of file sizes that can be programmatically uploaded to SharePoint Online using the SharePoint REST API. The maximum file upload size in SharePoint Online follows the default limit that can be seen in Central Admin’s Web Application General Settings for SharePoint On-Premises:

spfilesizesettings

 

Of course, in SharePoint Online you don’t have access to this setting.

Whether you use the /Add method…
https://msdn.microsoft.com/en-us/library/office/dn450841.aspx#bk_FileCollectionAdd

…or the /SaveBinaryStream method…
https://msdn.microsoft.com/en-us/library/office/dn450841.aspx#bk_FileSaveBinaryStream

…in SharePoint Online, you will be limited to the 250 MB file limit. If you attempt to go over this limit and catch the WebException, you will see this:
Response received was -1, Microsoft.SharePoint.Client.InvalidClientQueryExceptionThe request message is too big. The server does not allow messages larger than 262144000 bytes.

In SharePoint On-Premises, you can change the Maximum Upload size (as shown above) to a maximum of 2 GB.

 

So Where’s the Confusion
At this writing, there are some statements in at least one article on MSDN that says things like “The maximum size of a binary file that you can add by using the REST API is 2 GB.
This statement is technically true, but the statement applies to the underlying framework, not the Maximum Upload Size set on the Web Application. This has confused more than a few developers and there may be a generally understanding that you can use the REST endpoints for /Add and /SaveBinaryStream to get around the apparent 250 MB file size limit. This is not true, the 250 MB Maximum Upload Size limit will likely remain in effect and you will need to use the chunked file approach to upload files larger than 250 MB.

 

What About .NET CSOM Code?
It turns out this File Upload limit is not limited to REST endpoints. You will receive the same failure if you use a typical CSOM call with code such as the following:

static void UploadDocumentContent(string baseUrl, string libraryName, string filePath)
{
  try
  {
    ClientContext ctx = new ClientContext(baseUrl);
    ctx.Credentials = GetSharePointOnlineCredentials(baseUrl);
    Web web = ctx.Web;
    List docs = web.Lists.GetByTitle(libraryName);
    byte[] fileData = System.IO.File.ReadAllBytes(filePath);
    using (System.IO.Stream stream = new System.IO.MemoryStream(fileData))
    {
      var fci = new FileCreationInformation
      {
        Url = System.IO.Path.GetFileName(filePath),
        ContentStream = stream,
        Overwrite = true
      };
     Folder folder = docs.RootFolder;
     FileCollection files = folder.Files;
     Microsoft.SharePoint.Client.File file = files.Add(fci);
     ctx.Load(files);
     ctx.Load(file);
     ctx.ExecuteQuery();
   }
 }
 catch (Exception ex)
 {
   Console.Write(ex.ToString());
 }
}

But the exception caught above warns of a timeout exception, which is misleading. If you use Fiddler to see the response to the request to /_vti_bin/client.svc/ProcessQuery, you will see the Microsoft.SharePoint.Client.InvalidClientQueryException noted above.

 

Conclusion
Whether using .NET native application, .NET web applications, or client-side browser-based code, we recommend that you always use the chunked file upload approach in your applications which upload files larger than 250 MB to SharePoint Online. This approach is explained at:
https://github.com/OfficeDev/PnP/tree/dev/Samples/Core.LargeFileUpload#large-file-handling—option-3-startupload-continueupload-and-finishupload
Both .NET-based CSOM and REST enpoints fully support the StartUpload, ContinueUpload and FinishUpload methods, which are the three methods you will need to use to upload file larger than 250 MB to SharePoint Online.

 

Note
The StartUpload , ContinueUpload , CancelUpload and FinishUpload methods of Microsoft.SharePoint.Client.File class which are used for chunk upload are available only in SharePoint Online and SharePoint 2016 OnPremise and are not available in SharePoint 2013 OnPremise.

How to get WebsCount and other details from SharePoint online using Tenant Administration API

$
0
0

This post is a contribution from Mustaq Patel, an engineer with the SharePoint Developer Support team

The following code demonstrates getting the WebsCount and some other properties for site collection using the Tenant Administration API. The Tenant administration API comes with SharePoint Online Client Components SDK https://www.microsoft.com/en-us/download/details.aspx?id=42038

Assemblies to Reference

Microsoft.SharePoint.Client.dll

Microsoft.SharePoint.Client.Runtime.dll

The above two assemblies can be found at C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI

Microsoft.Online.SharePoint.Client.Tenant.dll

This assembly can be found at  C:\Program Files\SharePoint Client Components\16.0\Assemblies)

Below is the code to get the webs count property.

//Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;
using Microsoft.Online.SharePoint.TenantAdministration;
using System.Security;

namespace GetWebCountsSPO
{
    class Program
    {
        static void Main(string[] args)
        {
            GetListOfSiteCollections();
            Console.ReadLine();
        }

        private static void GetListOfSiteCollections()
        {
            string SPOAdminSiteUrl = "https://tenantadmin.sharepoint.com";
            string SPAdminUserName = "tenant-admin@domain.onmicrosoft.com";
            string SPAdminPassword = "password";

            try
            {
                using (ClientContext context = new ClientContext(SPOAdminSiteUrl))
                {
                    context.Credentials = AuthenticateMySPO(SPAdminUserName, SPAdminPassword);
                    Tenant mytenant = new Tenant(context);

                    var allsiteProps = mytenant.GetSiteProperties(0, true);
                    context.Load(allsiteProps, aprop => aprop.Include(a => a.Url));
                    context.ExecuteQuery();

                    foreach (var s in allsiteProps)
                    {
                        //this is needed now, earlier WebsCount can be queried via Tenant.GetSiteProperties
                        var sprops = mytenant.GetSitePropertiesByUrl(s.Url, true);
                        context.Load(sprops);
                        context.ExecuteQuery();

                        Console.WriteLine("SiteCollectionURL:" + sprops.Url);
                        Console.WriteLine("WebsCount:" + sprops.WebsCount);
                        Console.WriteLine("StorageUsage:" + sprops.StorageUsage);
                        Console.WriteLine("TimeZoneId:" + sprops.TimeZoneId);
                        Console.WriteLine("LastContentModifiedDate:" + sprops.LastContentModifiedDate);
                    }
                }
           }
           catch (Exception ex)
           {
                Console.WriteLine("Exception:" +ex.message);
           }

           Console.WriteLine("Done.");

        }

        static SharePointOnlineCredentials AuthenticateMySPO(string uname, string pwd)
        {
            var securePassword = new SecureString();
            foreach (char c in pwd)
            {
                securePassword.AppendChar(c);
            }

            var onlineCredentials = new SharePointOnlineCredentials(uname, securePassword);
            return onlineCredentials;
        }
    }
}

Fixing Client secret expiration errors for Provider hosted add-in on SharePoint Online.

$
0
0

This post is a contribution from Mustaq Patel, an engineer with the SharePoint Developer Support team

You have a  Provider hosted add-in for SharePoint Online hosted in Azure or public facing IIS Server and it fails due to ClientSecret expiry. You will see the below error

Invalid issuer or signature.

at Microsoft.IdentityModel.S2S.Tokens.JsonWebSecurityTokenHandler.VerifySignature(String signingInput, String signature, String algorithm, SecurityToken signingToken)

at Microsoft.IdentityModel.S2S.Tokens.JsonWebSecurityTokenHandler.ReadTokenCore(String token, Boolean isActorToken)

 

You generated new clientSecret and followed article that https://msdn.microsoft.com/en-us/library/office/dn726681(v=office.15).aspx to replace old clientsecret but the app is still failing.

Fix

First to note that https://msdn.microsoft.com/en-us/library/office/dn726681(v=office.15).aspx applies only to the ClientSecret that are not yet expired but about to expire. Since it takes 24 hours to get the ClientSecret effective, having SecondaryClientSecret (which is still unexpired) gives the app a fallback mechanism if the new ClientSecret is not effective yet.

The recommendation is that if the clientsecret is already expired and generating new clientsecret and after waiting for 24 hours still App fails with above error, it is time to clean those lingering secrets for a particular ClientId, generate a new clientSecret. Below steps will walk you through that process.

  1. Identify the ClientId for which ClientSecret is expired. You can find the clientId in Site App Permissions if the app is still installed. If not, you can find it in the web.config of the remote web application. say ClientId is 29b6b386-62a6-45c7-beda-abbaea6eecf2
  2. Connect to MSOnline using tenant admin user with below powershell in SharePoint 2013 powershell
import-module MSOnline

$msolcred = get-credential

connect-msolservice -credential $msolcred

 

  1. Get ServicePrincipals and keys. Printing $keys will give 3 records. While running this script if you get ReturnKeyValue without any output, hit enter on powershell prompt and 3 keys will be printed.
$clientId = "29b6b386-62a6-45c7-beda-abbaea6eecf2"

$keys = Get-MsolServicePrincipalCredential -AppPrincipalId $clientId

$keys

 

  1. Run the below command. Replace KeyId1, KeyId2 and KeyId3 from previous output.
Remove-MsolServicePrincipalCredential -KeyIds @("KeyId1"," KeyId2"," KeyId3") -AppPrincipalId $clientId

 

  1. Confirm all the keys are deleted for the given ClientId by running #3
  1. Generate new ClientSecret for this ClientId. Please note it uses ClientId from #1. The New ClientSecret generated will have a validity for 3 years.
$bytes = New-Object Byte[] 32

$rand = [System.Security.Cryptography.RandomNumberGenerator]::Create()

$rand.GetBytes($bytes)

$rand.Dispose()

$newClientSecret = [System.Convert]::ToBase64String($bytes)

$dtStart = [System.DateTime]::Now

$dtEnd = $dtStart.AddYears(3)

New-MsolServicePrincipalCredential -AppPrincipalId $clientId -Type Symmetric -Usage Sign -Value $newClientSecret -StartDate $dtStart  EndDate $dtEnd

New-MsolServicePrincipalCredential -AppPrincipalId $clientId -Type Symmetric -Usage Verify -Value $newClientSecret   -StartDate $dtStart  EndDate $dtEnd

New-MsolServicePrincipalCredential -AppPrincipalId $clientId -Type Password -Usage Verify -Value $newClientSecret   -StartDate $dtStart  EndDate $dtEnd

$newClientSecret

 

  1. The output is the $newClientSecret. Copy it and replace the old ClientSecret with this one. Make sure ClientId in there is the same we are using throughout. Please note we don’t need SecondaryClientSecret appsettings in here.

Try browsing the app and see if it works, we have seen varied results, most of the time it works, if not wait for 24 hours to propagate ClientSecret to SPO.

TFS Build configuration issue for SharePoint

$
0
0

This post is a contribution from Sohail Sayed, an engineer with the SharePoint Developer Support team

We recently worked on an issue with a customer configuring TFS build for SharePoint 2013

Customer followed the article https://msdn.microsoft.com/en-us/library/ff622991.aspx

There seems to be some changes compared to this article for resolving the SharePoint reference assemblies which I have listed below

 

As per the above msdn link to resolve the references we need to follow the below steps

It is recommended that you copy the SharePoint Server assemblies to the folder:

.. \Program Files\Reference Assemblies\SharePoint\

And then add one of the following registry entries:

For 64-bit systems:

HKEY_LOCAL_SYSTEM\SOFTWARE\Microsoft\Wow6432Node\.NETFramework\v4.0.30319\AssemblyFoldersEx\SharePoint15]@=
“<AssemblyFolderLocation>”.

 

Change

We could not find the HKEY_LOCAL_SYSTEM key in the registry.

Instead we had to update HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wow6432Node\.NETFramework\v4.0.30319\AssemblyFoldersEx\SharePoint]@=
“<AssemblyFolderLocation>”.

 

However this change will not work on its own

We had to add the command line argument “/tv:12.0″ to specify the tool version.

In addition we need to ensure the build is happening as 64 bit and not 32 bit and the build should be able to resolve the SharePoint references successfully.

Fixing issue in making cross domain Ajax call to SharePoint REST service in Chrome

$
0
0

This post is a contribution from Jing Wang, an engineer with the SharePoint Developer Support team
Symptom:
Remote Ajax Application is configured with Windows Authentication. It makes XMLHttpRequest to SharePoint 2013 Web Service, listdata.svc.
Sample code:

<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js" type="text/javascript" ></script>
<script src="http://code.jquery.com/jquery-1.9.1.js" type="text/javascript" ></script>
</head>
<body>
<h1>test page</h1>
                <script type="text/javascript">
                 //Ajax call to use listdata.svc
            var restUrl = "http://SharePointSiteUrl/_vti_bin/listdata.svc/List1";

  $.ajax({
                url: restUrl,
                type: "GET",
                dataType: 'JSON',
         headers: {
                         "Content-Type": "'application/json;odata=verbose'",
                         "Accept": "application/json;odata=verbose",
                         "crossDomain": "true",
                         "credentials":"include"
                         },
                xhrFields: { withCredentials: true        },
                success: function(response) {
                    alert("Success");
                },
                error: function(response){
                    alert("Error" );
                }
            });
                    </script>
</body>
</html>

When use Chrome to browse to the above page, you will see the below error
Failed to load resource: the server responded with a status of 401 dev.contoso.com/_vti_bin/listdata.svc/EMSPropertyLibrary()?$filter......
(Unauthorized)
Below is screen shot of error in Browser Developer tool console window :

Cause:
The XMLHttpRequest was sent with added custom headers, like,
headers.append('Content-Type', 'application/json;odata=verbose');
headers.append('credentials', 'include');
These custom headers made the request NOT a "Simple Request", see reference, https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
Since the request here with header ('Content-Type', 'application/json;odata=verbose'), it is not a Simple Request and the following process will happen.

  1. Browser (Chrome) sent preflight OPTIONS request to SharePoint WFE server, which hosts the listdata.svc, without credential first,
  2. Server returned HTTP/1.1 401 Unauthorized response for the preflight request.
  3. Due to 401 Unauthorized response from server the actual Web Service request will get dropped automatically.

Fiddler trace shows:

Solution:

Step I,

Force SharePoint WFE Server to send Http Status code of 200 for the preflight requests by us IIS’s new URL Rewrite tool:

  1. Install "web platform installer" from https://www.microsoft.com/web/downloads/platform.aspx
  2. Go to "Applications" tab and search for "URL Rewrite" and download it
  3. Open IIS configuration tool (inetmgr) and select the root node having the machine name in the IIS. Double click "URL Rewrite" in the features view on the right hand side.
  4. Add a new blankrule by clicking on Add Rule --> New Blank Rule from the menu on the right
  5. Give it any name
  6. In "Match URL", specify this pattern: .*
  7. In "Conditions" click on Add and  specify this condition entry: {REQUEST_METHOD} and this pattern: ^OPTIONS$
  8. In "Action", specify: action type Personalized response (or Customized reponse), state code 200, reason Preflight, description Preflight
  9. Click on Apply

Now, the server should reply with a 200 status code response to the preflight request, regardless of the authentication.

Step II,
Since this is a CORS request, above change is not enough to make the XMLHttpRequest call go through.
With the changes in Step I, Chrome Browser console shows a different error:
(index):1 XMLHttpRequest cannot load http://***/_vti_bin/listdata.svc...
Request header field crossDomain is not allowed by Access-Control-Allow-Headers in preflight response.

Make the following changes to the web.config for the SharePoint Web Application, to allow more custom headers required to enable CORS:

Sample code block in Web.Config. You will need to update the value of Access-Control-Allow-Origin to point to your remote ajax application.
----

<httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="http://AngularjsSiteUrl" />
        <add name="Access-Control-Allow-Headers" value="Content-Type,Accept,X-FORMS_BASED_AUTH_ACCEPTED,crossDomain,credentials " />
        <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
        <add name="Access-Control-Allow-Credentials" value="true" />
      </customHeaders>
</httpProtocol>

The above changes will help to fix the issue and the Ajax request will now execute successfully.


Creating a Custom Identity Provider and integrating with SharePoint to achieve single sign on with FBA across multiple web applications

$
0
0

This post is a contribution from Sohail Sayed, an engineer with the SharePoint Developer Support team

We had a scenario where we needed to use Forms Based Authentication with SharePoint and at the same time have single sign on across multiple web applications. Configuring FBA with SharePoint can be easily achieved through membership and role provider and the steps are well documented. However, this will not work as a single sign-on scenario across multiple web applications.

You will need to use a Trusted Identity Provider (also referred as Custom Security Token Service) to achieve single sign-on in SharePoint for FBA. It is very easy to create a custom identity provider and configure it with FBA. This blog demonstrates creation of a custom identity provider with FBA. In a subsequent blog we will demonstrate integrating the single sign on with an asp.net site and with Provider Hosted add-ins.

The below steps have been done on Windows Server 2012 R2 Standard and Visual Studio 2013. Same steps will apply for Visual Studio 2015 or later just the location of the folders will change as per the version of Visual Studio. In addition we will use the ASP.NET SqlMembership provider for authentication.

Please refer to the below link for more information on SqlMembership provider
https://msdn.microsoft.com/en-us/library/6e9y4s5t.aspx

 

Enable STS templates
Visual studio has a Project Template that will have most of the functionality for the custom identity provider. Follow the below steps to get the template in your Visual Studio.

  1. Go to Control Panel -> Programs -> Programs and Features -> Turn Windows features on or off
    You need to enable:
    • Windows Identity Foundation 3.5 - to be able to install WIF SDK
    • .NET Framework 3.5 (includes .NET 2.0 and 3.0) - the wif-sdk templates have a 3.5 dependency
  2. Download and install WIF SDK
    http://www.microsoft.com/en-us/download/details.aspx?id=4451
  3. Copy the templates
    Copy the templates from C:\Program Files (x86)\Windows Identity Foundation SDK\v4.0\Visual Studio Extensions\10.0 to C:\Users\<<USERNAME>>\Documents\Visual Studio 2013\Templates\ProjectTemplates\Visual C#.
  4. Create a site from a template
    Now you should be able to use the templates: File -> New -> Web Site -> Installed -> Templates -> Visual C#.

    Use the Asp.net Security Token Service Web Site

Structure of the Project

The Project structure is shown in the below image.

App_Code - This folder contains most of the classes related to Security Token Service functionality. The CustomSecurityTokenService class defines most of the processing logic for our custom STS. This class contains the method GetOutputClaimsIdentity where we define the claims to be passed to the client applications.  The CustomSecurityTokenServiceConfiguration represents the configuration for the custom STS defined in the Web.Config. In addition there are a couple of utility classes.

FederationMetadata - As the name suggests this folder contains the file that defines the Federation Metadata. This will be later consumed in an asp.net application.

Default.aspx is the entry point for client applications requesting authentication to our custom STS. The default.aspx expects wa, wtrealm and wctx parameters which are passed by the client application. Default.aspx will redirect to the Login.aspx if the user is not already logged in else will post the saml token for an authenticated user back to the client application.

The Web.Config file contains the certificate details used by our custom STS. This is specified in the appSetting SigningCertificateName. The value for the SigningCertificateName will be the Subject property of the certificate.

For the above certificate we will have the SigningCertificateName appSetting defined as below

<add key="SigningCertificateName" value="CN=STSNewCert, OU=CSS, O=MS, L=Issqh, S=WA, C=US"/>

 

 

Update Login.aspx page
We need to implement the authentication logic in Login.aspx. Since we are using the SQLMembership provider we can use the asp.net login control. Comment rest of the controls as we only need the login control

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
 <head id="Head1" runat="server">
 <title>Login to the security token service (STS)</title>
 <style type="text/css">
 .style1
 {
 width: 100%;
 }
 .style2
 {
 width: 125px;
 }
 .style3
 {
 font-size: large;
 font-weight: bold;
 }
 </style>
 </head>
 <body>
 <form id="form1" runat="server">
 <asp:Login ID="Login1" runat="server"></asp:Login>
 </form>
 <p>
 Note: You can enter any non-empty user name. The password field
 is optional. To modify claims issued by this STS, make changes to
CustomSecurityTokenService.GetOutputClaimsIdentity.</p>
 </body>
 </html>

Comment all code in Login.aspx.cs as the authentication will be taken care by membership provider configured in next step

 

Web.config Changes in STS

Membership provider
To configure the STS to use custom membership provide add the <membership> element

<membership defaultProvider="aspnetmembership">
 <providers>
 <add name="aspnetmembership" connectionStringName="MyLocalSQLServer" applicationName="MyAppName" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral,            PublicKeyToken=b03f5f7f11d50a3a" />
 </providers>
 </membership>
 Also need to add the connection string for the membership provider in the above case
 <connectionStrings>
 <add name="MyLocalSQLServer" connectionString="Initial Catalog=aspnetdb_claim;data source=SQL;Integrated Security=SSPI;" />
 </connectionStrings>

 

Edit the App_code/CustomSecurityTokenService.cs file
Update the GetOutputClaimsIdentity method to add custom claims.

protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
 {
 if ( null == principal )
 {
 throw new ArgumentNullException( "principal" );
 }

ClaimsIdentity outputIdentity = new ClaimsIdentity();

// Issue custom claims.
 // TODO: Change the claims below to issue custom claims required by your application.
 // Update the application's configuration file too to reflect new claims requirement.

outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Email, principal.Identity.Name ) ); //https://msdn.microsoft.com/en-us/library/system.identitymodel.claims.claimtypes(v=vs.110).aspx
 outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) );

return outputIdentity;
 }

 

Certificate

By default when you create a new ASP.NET Custom STS site, the certificate is specified in the web.config in app settings as below. In the earlier section of this blog I have provided more details on the SigningCertificateName appSetting.
Note - In production scenario we need to use domain signed certificate.

<add key="SigningCertificateName" value="CN=STSTestCert"/>

Publish the STS to a location accessible by the SharePoint application
 

Copy Certificates
Creating a new Asp.net Security token site will add a new certificate to the certificate store. You can find the certificate referenced in the Web.Config of the custom STS site. We need to add certificate to trusted store.

  1. Go to Central Admin --> Security -->Manage Trust.
  2. Add the certificate (.cer) file here

 

Created Trusted Identity Provider
Run the below PowerShell to create the trusted identity provider

$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("c:\cert\STSTestCert.cer")

$map1 = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" -IncomingClaimTypeDisplayName "EmailAddress" SameAsIncoming

$map2=New-SPClaimTypeMapping -IncomingClaimType "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" -IncomingClaimTypeDisplayName "Role" SameAsIncoming

$realm="http://sp/_trust/default.aspx"

$ap=New-SPTrustedIdentityTokenIssuer -Name "SimplePassiveSTS" -Description "A simple Asp.Net STS on DevBox." Realm $realm -ClaimsMappings $map1,$map2 -ImportTrustCertificate $cert -SignInUrl "http://localhost:8013/STSWebSite/default.aspx" -IdentifierClaim $map1.InputClaimType

 

Update "SimplePassiveSTS" in the above command with the desired name of the Trusted Identity Provider

Replace the value of $realm variable in the above command with the url of one of your SharePoint web applications. There will be additional steps for other web applications which is mentioned in the next section. In addition replace the SignInUrl parameter in the New-SPTrustedIdentityTokenIssuer command with the url of the custom STS web site.

After the identity provider has been created go the Central Admin --> Application Management --> Manage web applications. Select the desired Web application and click the Authentication Providers from Ribbon. Edit the desired zone and select the newly created Trusted Identity Provider. Repeat this for the other SharePoint web applications as required.

Configuring the ProviderRealms

In PowerShell code to register the Trusted Identity Provider in the previous section we specified url of one of the web applications in the $realm parameter. This value is passed as wctx and wtrealm parameters to the Custom STS. For those familiar with ADFS this is similar to the Relying Party Identifier in ADFS. ADFS automatically maps this to the Relying Party Endpoint and return back the SAML Token to this endpoint after authentication. In case of the Custom STS the redirection will happen to the value passed in wctx parameter. There is an additional step required for other web applications to work with the custom STS else the url of the web application specified in $realm in the previous section will always be passed in wctx parameter to the custom STS by SharePoint for all the web applications configured to authenticate with the Custom STS.

Run the following script for other web applications that are using the Custom STS for authentication.

$ap = Get-SPTrustedIdentityTokenIssuer -Identity "SimplePassiveSTS"
$ap.ProviderRealms.Add((new-object System.Uri("https://sp:8787")),"https://sp:8787/_trust/default.aspx")
$ap.ProviderRealms.Add((new-object System.Uri("http://sp:8888")),"http://sp:8888/_trust/default.aspx")
$ap.Update()
iisreset

 

Provide appropriate access to the required users and you should be able to login to your SharePoint web application using FBA and experience single sign on across multiple web applications. One thing you will notice is that when adding users, anything you type in the People Picker control will get resolved. To fix this you will need to implement a custom claims provider. I will cover this in a subsequent blog.

 

Update - The second blog which demonstrates integrating the custom identity provider with asp.net web site and provider hosted apps has been published at https://blogs.technet.microsoft.com/sharepointdevelopersupport/2017/07/26/integrating-custom-identity-provider-with-asp-net-and-provider-hosted-apps-to-achieve-single-sign-on-with-forms-based-authentication/

Integrating Custom Identity provider with Asp.net and Provider hosted apps to achieve single sign on with Forms Based Authentication

$
0
0

This post is a contribution from Sohail Sayed, an engineer with the SharePoint Developer Support team

We demonstrated creating a custom identity provider and integrating it with SharePoint to achieve single sign on in the blog post https://blogs.technet.microsoft.com/sharepointdevelopersupport/2017/07/07/creating-a-custom-identity-provider-and-integrating-with-sharepoint-to-achieve-single-sign-on-with-fba-across-multiple-web-applications/. In this blog we will demonstrate integrating the custom identity provider with Asp.Net web sites and Provider hosted apps.

Create a new asp.net web site.
We will start by creating a new asp.net web site. In my case I chose the Empty Web Site Template. This will create an empty web site project that will only contain the web.config file. Add a new web form to the site. If you have an existing asp.net site update the Web.Config with the configuration mentioned in the subsequent sections.

 

Web.Config configuration
The below sections describe the various web.config changes required. Note I am using the configuration for Microsoft.IdentityModel namespace. The same configuration can be done using System.IdentityModel namespace. More information on this can be found at https://docs.microsoft.com/en-us/dotnet/framework/security/namespace-mapping-between-wif-3-5-and-wif-4-5

 

Define the configSections
We need to add the configSections that defines the rest of the configuration. Add the below tags. If your web.config already has a <configSections> element just add the <section> tag.

  <configSections>
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>

 

Modify the system.web section
Modify the <system.web> section as below. Note the requestValidationType defined in the httpRuntimeElement. This defines a class that will perform validation of the sign in request. This class is invoked by the IsSignInResponse method of the Microsoft.IdentityModel.Web.WSFederationAuthenticationModule. We will define this class in a later section in this blog

  <system.web>
    <compilation targetFramework="4.5" debug="true">
      <assemblies>
        <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </assemblies>
    </compilation>
    <authentication mode="None"/>
    <authorization>
      <deny users="?"/>
    </authorization>
    <pages controlRenderingCompatibilityVersion="4.0"/>
    <httpRuntime requestValidationType="SampleRequestValidator"/>
  </system.web>

 

Define system.webServer section
Add the <system.webServer> section which adds the WSFederationAuthenticationModule. This module is responsible for securing the asp.net site and handling the redirection to the custom identity provider

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules>
      <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
      <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
    </modules>
  </system.webServer>

 

Define the microsoft.identityModel section
Finally add the <microsoft.identityModel> section. This section defines the following

    1. Audience Uris indicating the valid audience for the identity provider
    2. Issuer and realm information
    3. Certificate details used for trust and communication
  <microsoft.identityModel>
    <service>
      <audienceUris>
        <add value="https://custom.contoso.com/WebSite1/"/>
      </audienceUris>
      <federatedAuthentication>
        <wsFederation passiveRedirectEnabled="true" issuer="https://custom.contoso.com/customidp/" realm="https://custom.contoso.com/WebSite1/" requireHttps="true"/>
        <cookieHandler requireSsl="false"/>
      </federatedAuthentication>
      <serviceCertificate>
        <certificateReference x509FindType="FindByThumbprint" findValue="88c1d6ea0c3d9ee05459403cc8121e6ace5a7edd" storeLocation="LocalMachine" storeName="My"/>
      </serviceCertificate>
      <applicationService>
        <claimTypeRequired>
          <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" optional="true"/>
          <claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" optional="true"/>
          <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" optional="true"/>
        </claimTypeRequired>
      </applicationService>
      <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedIssuers>
          <add thumbprint="88c1d6ea0c3d9ee05459403cc8121e6ace5a7edd" name="https://custom.contoso.com/customidp/"/>
        </trustedIssuers>
      </issuerNameRegistry>
    </service>
  </microsoft.identityModel>

 

We need to make the following changes to the above section

  • Modify the value in the <audienceUris> and the value of the realm attribute in <wsFederation> tag to the url of the current web site.
  • Change the issuer attribute in the <wsFederation> and the name attribute in the <trustedIssuers>/<add> tag to the url of the trusted identity provider
  • Update the findValue attribute the <certificateReference> tag and the thumbprint attribute <trustedIssuers>/<add> tag with the thumbprint of the certificate used by the trusted identity provider.

Note when copying the thumbprint from the certificate a special character gets added to the start of the thumbprint. This character is not visible in notepad. To remove this copy the thumbprint to the notepad. Press home key on the keyboard so that the cursor is at the beginning of the thumbprint value. Press delete key twice till the first character of the thumbprint is deleted. Manually type back the character.

 

Define the SampleRequestValidator class
In the “Modify system.web section” section we had specified the requestValidationType as SampleRequestValidator. We need to define this class now. Add a new class to the project and name it as SampleRequestValidator. Overwrite the code of this class with the below code

using System;
using System.Web;
using System.Web.Util;

using Microsoft.IdentityModel.Protocols.WSFederation;

/// <summary>
/// This SampleRequestValidator validates the wresult parameter of the
/// WS-Federation passive protocol by checking for a SignInResponse message
/// in the form post. The SignInResponse message contents are verified later by
/// the WSFederationPassiveAuthenticationModule or the WIF signin controls.
/// </summary>

public class SampleRequestValidator : RequestValidator
{
    protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
    {
        validationFailureIndex = 0;

        if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal))
        {
            SignInResponseMessage message = WSFederationMessage.CreateFromFormPost(context.Request) as SignInResponseMessage;

            if (message != null)
            {
                return true;
            }
        }

        return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
    }

}

 

You can find more information on this class at https://social.technet.microsoft.com/wiki/contents/articles/1725.wif-troubleshooting-a-potentially-dangerous-request-form-value-was-detected-from-the-client.aspx

 

Read the user value

Finally we need to be able to read the user value and the claims.

Update the code in the default.aspx Page Load method as below

    protected void Page_Load(object sender, EventArgs e)
    {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            Microsoft.IdentityModel.Claims.ClaimsPrincipal cp = HttpContext.Current.User as Microsoft.IdentityModel.Claims.ClaimsPrincipal;

            if (cp != null)
            {
                foreach (Microsoft.IdentityModel.Claims.Claim claim in cp.Identities[0].Claims)
                {
                    Response.Write(claim.ClaimType + " - " + claim.Value +"<br>");
                }
            }
        }
    }

 

Browse the page. It should redirect you to the custom identity provider login page if you are not already logged in. After successfully login you should be able to see the claims on the page.

Additional Configuration for Provider hosted add-ins.
Provider hosted apps are asp.net web sites and will use the same configuration as defined above. We need to make some additional changes to be able to make calls into SharePoint.

Firstly you need to configure the Provider hosted add-in to use SAML as mentioned in this blog from Steve Peschka

https://samlman.wordpress.com/2015/03/01/using-sharepoint-apps-with-saml-and-fba-sites-in-sharepoint-2013/

The helper classes mentioned in the above blog are available at the below link

https://samlman.wordpress.com/2015/02/28/an-updated-claimstokenhelper-for-sharepoint-2013-high-trust-apps-and-saml/

By default in this code the RetrieveIdentityForSamlClaimsUser method of the TokenHelper class casts the HttpContext.Current.User object as System.IdentityModel.Claims.ClaimsPrincipal. Since in the web.config we are using the configuration for Microsoft.IdentityModel the above operation will return null. To fix this go to the TokenHelper.RetrieveIdentityForSamlClaimsUser.

Find the line System.IdentityModel.Claims.ClaimsPrincipal cp = UserPrincipal as System.IdentityModel.Claims.ClaimsPrincipal;

Change this to

Microsoft.IdentityModel.Claims.ClaimsPrincipal cp = UserPrincipal as Microsoft.IdentityModel.Claims.ClaimsPrincipal;

This should now retrieve a valid claims identity.

In addition in the default.aspx use SMTP to retrieve the ClientContext since we are using email address as the identity claim

            var spContext = SharePointContextProvider.Current.GetSharePointContext(Context);

            // using (var clientContext = spContext.CreateUserClientContextForSPHost())
            using (var clientContext = spContext.CreateUserClientContextForSPHost(TokenHelper.IdentityClaimType.SMTP))
            {
                clientContext.Load(clientContext.Web, web => web.Title);
                clientContext.ExecuteQuery();
                Response.Write(clientContext.Web.Title);
            }

 

If you now deploy the provider hosted add-in you may still get a 401 error. To fix this we need to update the user profile of that user. This is because the user rehydration process in SharePoint will map the identity claim to one of the fields in the user profile. In our case since we are using SMTP in the Provider hosted app when getting the ClientContext we need to ensure the WorkEmail field in the user profile for the current user in SharePoint Central Administration matches the value of the identity claim. The Provider hosted add-in will start working after this change. For more information on user profile configuration with Provider hosted add-ins refer to the below blog

https://samlman.wordpress.com/2015/03/01/oauth-and-the-rehydrated-user-in-sharepoint-2013-howd-they-do-that-and-what-do-i-need-to-know/

Creating SharePoint Calendar All Day or Daily Recurring Event

$
0
0

This post is a contribution from Manish Joshi, an engineer with the SharePoint Developer Support team

Whether you are creating a daily recurring event using SharePoint web service lists.asmx or via REST calls, you will need to make sure that TimeZone property is set to 0 otherwise the SharePoint calendar recurring event will be created but on random instances of the events items you will get following error message:

“Item does not exist. It may have been deleted by another user”

Sample codes below:

Sample code using Lists.asmx web service

Below is the sample XML passed to SharePoint lists.asmx.

var sDoc = "<Batch OnError=\"Continue\">" +
            "               <Method ID=\"1\" Cmd=\"New\">" +
            "                  <Field Name=\"Category\"><![CDATA[Meeting]]></Field>" +
            "                  <Field Name=\"EndDate\"><![CDATA[2017-07-27T239:59:00Z]]></Field>" +
            "                  <Field Name=\"EventDate\"><![CDATA[2017-07-13T09:00:00Z]]></Field>" +
            "                  <Field Name=\"EventType\"><![CDATA[1]]></Field>" +
            "                  <Field Name=\"fAllDayEvent\"><![CDATA[1]]></Field>" +
            "                  <Field Name=\"fRecurrence\"><![CDATA[1]]></Field>" +
            "                  <Field Name=\"Location\"><![CDATA[Redmomd]]></Field>" +
            "                  <Field Name=\"RecurrenceData\"><![CDATA[<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><daily weekday='TRUE' /></repeat><windowEnd>2017-07-27T23:59:00Z</windowEnd></rule></recurrence>]]></Field>" +
            "                  <Field Name=\"Description\"><![CDATA[Daily Recurrence.]]></Field>" +
            "                  <Field Name=\"Title\"><![CDATA[Daily 6]]></Field>" +
            "                  <Field Name=\"UID\"><![CDATA[{" + Guid.NewGuid() + "}]]></Field>" +
            "                  <Field Name=\"TimeZone\"><![CDATA[0]]></Field>" +
            "               </Method>" +
            "            </Batch>";

 

Below is the complete sample code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;

namespace CreateRecurringEvent
{
    class Program
    {
        private static int GetWorkdays(DateTime start, DateTime end)
        {
            int days = 0;

            while (start <= end)
            {
                var day = start.DayOfWeek;
                if (!(day == DayOfWeek.Saturday || day == DayOfWeek.Sunday))
                {
                    days++;
                }

                start = start.AddDays(1);
            }

            return days;
        }

        static void Main(string[] args)
        {

            ListsService.Lists lists = new ListsService.Lists();
            lists.UseDefaultCredentials = true;
            lists.Url = "http://sp/_vti_bin/lists.asmx";


            var start = DateTime.Parse("2017-11-13");
            var end = DateTime.Parse("2017-12-27");
            string title = "Test Recurrence";


            var recurrenceDataXml = new XElement("recurrence",
                                    new XElement("rule",
                                        new XElement("firstDayOfWeek", "su"),
                                        new XElement("repeat",
                                            new XElement("weekly",
                                                new XAttribute("mo", "TRUE"),
                                                new XAttribute("tu", "TRUE"),
                                                new XAttribute("we", "TRUE"),
                                                new XAttribute("th", "TRUE"),
                                                new XAttribute("fr", "TRUE"),
                                                new XAttribute("weekFrequency", "1"))),
                                        new XElement("repeatInstances", GetWorkdays(start, end))));

            var recurrenceDataString = recurrenceDataXml.ToString();

            var newEventXml = new XDocument(
               new XElement("Batch",
                new XAttribute("OnError", "Continue"),
                new XAttribute("ListVersion", "1"),
                new XAttribute("ViewName", ""),
                new XElement("Method",
                  new XAttribute("ID", "1"),
                  new XAttribute("Cmd", "New"),
                  new XElement("Field", new XAttribute("Name", "Title"), title),
                  new XElement("Field", new XAttribute("Name", "EventDate"), String.Format("{0:yyyy'-'MM'-'dd 00':'00':'00}", start)),
                  new XElement("Field", new XAttribute("Name", "EndDate"), String.Format("{0:yyyy'-'MM'-'dd 23':'59':'00}", end)),
                  new XElement("Field", new XAttribute("Name", "fAllDayEvent"), "1"),
                  new XElement("Field", new XAttribute("Name", "fRecurrence"), "1"),
                  new XElement("Field", new XAttribute("Name", "Duration"), 86340),
                  new XElement("Field", new XAttribute("Name", "EventType"), "1"),
                  new XElement("Field", new XAttribute("Name", "UID"), "{" + Guid.NewGuid() + "}"),
                  new XElement("Field", new XAttribute("Name", "TimeZone"), "0"),
                  new XElement("Field", new XAttribute("Name", "RecurrenceData"),
                    new XCData(recurrenceDataString)))));

            var doc = new XmlDocument();
            using (var reader = newEventXml.CreateReader())
                doc.Load(reader);

            System.Xml.XmlNode result = lists.UpdateListItems("Calendar", doc);

            XmlTextReader xr = new XmlTextReader(result.OuterXml, XmlNodeType.Element, null);
            while (xr.Read())
            {
                if (xr.ReadToFollowing("z:row"))
                {
                    if (xr["ows_ID"] != null)
                    {
                        Console.WriteLine(xr["ows_ID"].ToString());
                    }
                }
            }

            Console.WriteLine("Done...!");
            Console.ReadLine();

        }

    }
}

 

Sample REST call in Script Editor Web Part:
Below code demonstrates creation of the recurring event using REST API

<div><button type="button" onclick="CreateDailyRecurringEvent()">Create Daily Recurring Event</button></div>
<br><br>
<div id="Status"></div>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

<script type="text/javascript">
	function createGuid()
	{
	   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
		  var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
		  return v.toString(16);
	   });
	}
	var guid = createGuid();
	function CreateDailyRecurringEvent()
	{
		//create a string that has the events
		var stringStartDate = "2017-07-13T08:00:00Z";
		var stringEndDate = "2017-07-27T23:59:00Z";
		var reccurenceString = "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><daily weekday='TRUE' /></repeat><windowEnd>2017-07-27T23:59:00Z</windowEnd></rule></recurrence>";
		var recReq =
        		{
			url: "http://sp/_api/web/lists/GetByTitle('Events')/items",
			type: "POST",
			data: JSON.stringify({
				'__metadata': {
					'type': 'SP.ListItem'
				},
				'Title': 'Daily 5',
				'EventDate': stringStartDate,
				'EndDate': stringEndDate,
				'Location': 'Seattle',
				'Description': 'Daily 5',
				'Category': 'Meeting',
				'fRecurrence': true,
				'fAllDayEvent': true,
				'RecurrenceData': reccurenceString,
				'TimeZone': 0,
				'UID': guid,
				'EventType': 1
			}),
			headers: {
				"accept": "application/json;odata=verbose",
				"content-type": "application/json;odata=verbose",
				"X-RequestDigest": $("#__REQUESTDIGEST").val()
			},
			success :function () {
				alert("Event data saved.");
			},
			error:function (err) {
				alert("Error occurred while saving question data.");
				console.log("ERROR", err);
			}
		};

		jQuery.ajax(recReq);
	}
</script>

O365 and OneDrive with ADAL, Microsoft Graph API & Office Add-In

$
0
0

This post is a contribution from Manish Joshi, an engineer with the SharePoint Developer Support team

This blog demonstrates creating an Office add-in and retrieving data from SharePoint Online and One Drive within the add-in. The add-in uses ADAL.js for authentication and retrieving access token. In addition it uses Graph API and SharePoint REST API for accessing data from One Drive and SharePoint Online.

Create new Azure Web app

  • Browse to Azure Portal https://portal.azure.com.
  • Click on App Services
  • Click  Add-> Web Apps-> Click Create
  • Set a unique App name. Example : o365officeaddin.
  • Set Resource Group: Either Create New or Select Existing. Click on Create.
  • Browse back to app services to seethe newly created web app.
  • Click on the newly created web app ->  Click Get publish profile from the menu in right section.
  • Click Save File-> The file will be saved in "C:\Users\<<username>>\Downloads" folder with name o365officeaddin.PublishSettings

 

Register new Azure App

  • Click on Azure Active Directory-> Click Switch directory
  • Select your corporate Directory. In my case it’s Contoso.
  • Click App registrations-> Click New application registration
  • Enter Name: o365officeaddin
  • Select Application type: Web App/ API
  • Enter Sign-on URL as the url of the newly created azure web app. Example : https://o365officeaddin.azurewebsites.net/Home.html
  • Click on Azure Active Directory-> Click App registrations-> Click o365officeadin-> Click Properties
  • Set App ID URI: https://yourO365tenant.onmicrosoft.com/o365officeaddin. Click Save.
  • Edit the reply Urls and add the url of your web app with an additional query string “?et=”
    Example -: https://yourO365tenant.onmicrosoft.com/o365officeaddin/Home.html?et=
    Note the reply urls are cases sensitive and you may get an error if there is a case mismatch.
  • Make a note of "Application Id" we will use this later in javascript code for the clientId.
  • Click Manifest-> Update value for “oauth2AllowImplicitFlow” to "true". Click Save

 

Grant Permissions to the new Azure app

  • Click Required permissions-> Click Windows Azure Active Directory
  • Under "Delegated Permissions" check following:
    •  Sign in and read user profile
    •  Read and write directory data
    •  Access the directory as the signed-in user
      Click Save
  • Click Add-> Select an API
  • Click Microsoft Graph-> Click Select
  • Under "Delegated Permissions" check following:
    •  Sign in and read user profile
    •  Read and write directory data
    •  Access the directory as the signed-in user
    •  Have full access to user files
    •  Have full access to all files user can access
      Click Save
  • Click Select-> Click Done
  • Click Add-> Select an API
  • Click Office 365 SharePoint Online-> Click Select
  • Under "Delegated Permissions" check following:
    •  Read and write user files
    •  Run search queries as a user
    •  Read and write managed metadata
  • Click Microsoft Graph->Click Grant Permissions-> Click Yes
  • Click Select-> Click Done

 

Create new Word Add-in Project

  • Launch Visual Studio 2015-> Click New Project
  • Under Office/SharePoint-> Select Web Add-ins-> Select Word Add-in
  • Give Name: o365officeaddin-> Click OK
  • In Home.html Insert following code just after <body>. In the below code update the sharePointTenantName with your tenant name and the clientId with the Application Id we copied in the earlier steps.
    <div id="content-header">
            <strong><span class='app-user navbar-text'></span></strong>
    
            <div class="search-area">
                <input id="tb-search-input" type="text" value="search query" />
                <input id="btn-search" type="button" value="Search" /><input id="btn-onedrive" type="button" value="OneDrive" />
                <div id="search-results">
                </div>
            </div>
        </div>
    
        <script type="text/javascript">
            (function (window, $) {
                // Azure AD App Manifest - Set 'oauth2AllowImplicitFlow' property to 'true' ("oauth2AllowImplicitFlow": true)
                // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-manifest
                var sharePointTenantName = '<<yourO365tenant>>';
                var config = window.config = {
                    tenant: sharePointTenantName + '.onmicrosoft.com',
                    clientId: 'feea2058-6ff6-4847-9dfe-854400becd24',
                    postLogoutRedirectUri: window.location.origin,
                    endpoints: {
                        graphApiUrl: 'https://graph.microsoft.com',
                        sharePointUrl: 'https://' + sharePointTenantName + '.sharepoint.com'
                    },
                    cacheLocation: 'localStorage'
                };
    
                var authContext = new AuthenticationContext(config);
                var $userDisplay = $(".app-user");
                var $searchInput = $('#tb-search-input');
                var $searchButton = $('#btn-search');
                var $searchResultsDiv = $('#search-results');
                var $onedriveButton = $('#btn-onedrive');
    
                var isCallback = authContext.isCallback(window.location.hash);
                authContext.handleWindowCallback();
    
                // Check Login Status, Update UI
                var user = authContext.getCachedUser();
                if (user) {
                    $userDisplay.html(user.userName);
                    $userDisplay.show();
    
                    $searchButton.click(function () {
                        var searchText = $searchInput.val();
                        $searchResultsDiv.empty();
    
                        if (searchText.length > 0) {
                            search(searchText);
                        }
                    });
                    $onedriveButton.click(function () {
                        onedrive();
                    });
                }
                else {
                    authContext.login();
    
                    $userDisplay.empty();
                    $userDisplay.hide();
                    $searchInput.hide();
                    $searchButton.hide();
                }
    
                function search(searchText) {
                    var searchEndpoint = 'https://' + sharePointTenantName + '.sharepoint.com/_api/search/query?querytext=\'' + searchText + '\'';
    
                    authContext.acquireToken(config.endpoints.sharePointUrl, function (error, token) {
                        if (error || !token) {
                            console.log(error);
                        }
                        else {
                            $.ajax({
                                beforeSend: function (request) {
                                    request.setRequestHeader("Accept", "application/json");
                                },
                                type: "GET",
                                url: searchEndpoint,
                                dataType: "json",
                                headers: {
                                    'Authorization': 'Bearer ' + token,
                                }
                            }).done(function (response) {
                                $searchResultsDiv.html(JSON.stringify(response));
    
                            }).fail(function (response) {
                                console.log(response.responseText);
                            });
                        }
                    });
                }
                function onedrive() {
                    var onedriveEndpoint = "https://graph.microsoft.com/v1.0/me";
    
                    authContext.acquireToken(config.endpoints.graphApiUrl, function (error, token) {
                        if (error || !token) {
                            console.log(error);
                        }
                        else {
                            $.ajax({
                                beforeSend: function (request) {
                                    request.setRequestHeader("Accept", "application/json");
                                },
                                type: "GET",
                                url: onedriveEndpoint,
                                dataType: "json",
                                headers: {
                                    'Authorization': 'Bearer ' + token,
                                }
                            }).done(function (response) {
                                $searchResultsDiv.html(JSON.stringify(response));
    
                            }).fail(function (response) {
                                console.log(response.responseText);
                            });
                        }
                    });
                }
            })(window, window.jQuery);
    
        </script>
    

 

  • Add the below script reference in the head section
    <script src="//secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js"></script>
    

 

Publish and install the Add-in

  • Click Build-> Build Solution
  • Right click o365officeaddin project-> Select Publish
  • Current profile: Click <New…>
  • Browse to path where you have saved the o365officeaddin.PublishSettings file. Click Finish
  • Click Deploy your web project-> Click Publish
  • Once publish has succeeded Click Package the add-in
  • Change the URL to the url of the azure web app we created in previous steps. Example https://o365officeaddin.azurewebsites.net. Click Finish
  • Next it will generate the o365officeaddinManifest.xml file. This file will be generated in the folder "bin\Debug\app.publish\OfficeAppManifests".
  • Browse to your Office 365 site
  • Browse to a document library
  • Edit an existing Word Document or create a new one -> Click Open-> Click Word Online
  • Click INSERT-> Click Office Add-ins
  • Click Upload My Add-In
  • Click Browse-> Browse to o365officeaddinManifest.xml location-> Click Upload
  • Click "Show Taskpane" to see your Add-in UI
  • Enter a search text in the search query box-> Click Search-> You will see JSON for the search results returned.

 

  • Click OneDrive button to see it get details of the current user from OneDrive using graph REST endpoint.

 

Installing the Add-in to App Catalog

We can make the add-in available globally instead of having to upload the manifest.xml file everytime. When we packaged the add-in in the earlier step the .app file for the add-in was also generated at the path "bin\Debug\app.publish".  Follow the below steps to make this add-in available globally across your tenant without requiring to upload the manifest.xml file every time.

  • Browse to the app catalog site collection in your tenant
  • Go to Apps for SharePoint
  • Upload the office365addin.app file from the location "bin\Debug\app.publish"
  • Install the add-in in the app catalog site collection by going to Site Actions -> Add an App
  • One the app is installed Browse to document library in any site collection in your tenant and add or edit a word document in browser.
  • Click on "Insert" -> "Office Add-ins" command in the ribbon . You should be able to see your office add-in under My Organization tab

Retrieving thumbnails using Video REST API

$
0
0

This post is a contribution from Vitaly Lyamin, an engineer with the SharePoint Developer Support team

The Video REST API provides an ability to discover and interact with videos. We’ve seen a number of issues retrieving thumbnails where the “ThumbnailURL” endpoint returns an endpoint (e.g. /pVid/video.mp4.PNG?VideoPreview=1) that is not suitable for use with an AAD app access token and is only accessible via cookie-based authentication (passive or active). Fortunately, there are a few solutions for this.

 

Solution 1

The Video REST API “ThumbnailStream” endpoint can be used with and without the width (in pixels) parameter.

Examples

https://tenant.sharepoint.com/portals/hub/_api/VideoService/Channels('ac784366-a780-4e21-8bb6-5851c3288906')/Videos('4cafb0e8-8013-40d9-9d5f-370d90e5edc7')/ThumbnailStream
OR

https://tenant.sharepoint.com/portals/hub/_api/VideoService/Channels('ac784366-a780-4e21-8bb6-5851c3288906')/Videos('4cafb0e8-8013-40d9-9d5f-370d90e5edc7')/ThumbnailStream('100')

Resources

https://msdn.microsoft.com/en-us/office/office365/api/video-rest-operations

https://techcommunity.microsoft.com/t5/Office-365-Video/Video-thumbnail-URL-broken-when-using-OAuth/td-p/7956

 

Solution 2

The “GetPreview.ashx” handler can also be used with several different inputs (simple example below).

Examples

https://tenant.sharepoint.com/portals/hub/_layouts/15/getpreview.ashx?path=https%3A%2F%2Ftenant.sharepoint.com%2Fportals%2FChannel1%2FpVid%2Fmov_bbb.mp4&resolution=1

Resources

https://blogs.msdn.microsoft.com/richard_dizeregas_blog/2014/09/15/developing-apps-against-the-office-graph-part-1/

https://blogs.msdn.microsoft.com/ocarpen/2015/01/09/office-graph-api-and-cortana-chapter-2-how-to-make-queries-by-code-to-the-office-graph/

 

Non-MS Resources:

http://www.n8d.at/blog/image-renditions-available-in-modern-team-sites/

https://blog.beecomedigital.com/2015/07/27/generate-thumbnail-of-a-document-stored-in-sharepoint-from-its-url/

 

Hide App Launcher for all sites in SharePoint 2016 farm

$
0
0

This post is a contribution from Jing Wang, an engineer with the SharePoint Developer Support team

One of our customers had a requirement to hide the App Launcher in On-premise SharePoint 2016 farm. The requirement is to make this happen on all sites in the farm, including existing sites and new sites.
This blog demonstrates how to hide the App Launcher across the farm using a Delegate Control.

 

Finding the control ID to hide

First, let us use IE developer tool to find the id of the html element of the App Launcher.

We can see that the id for the html button for the App Launcher is O365_MainLink_NavMenu. We can use CSS to hide this control.

 

Implementing the delegate control

We need to implement a custom user control and register it for the out of the box delegate control "AdditionalPageHead".

The Delegate Control “AdditionalPageHead” is used in the out of the box master pages, for example, seattle.master and oslo.master.

<asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server" />
<SharePoint:DelegateControl runat="server" ControlId="AdditionalPageHead"
AllowMultipleControls="true" />
<asp:ContentPlaceHolder id="PlaceHolderBodyAreaClass" Visible="true" runat="server" />

Below is the detailed steps of implementation and deployment:

Step 1

  • Create a new SharePoint 2016 - Empty Solution
  • In solution Explorer, select the newly created project , right click on it, Add - New Item - select "User Control", name it as "UserConrol_HideApplauncher".
  • There will be a folder generated under ControlTemplates, folder name took after the project name "hideApplauncher", inside there is the template file "UserConrol_HideApplauncher.ascx:
    The Default content is in the ascx file will be as below
<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="UserControl_HideAppLauncher.ascx.cs" Inherits="hideAppLauncher.ControlTemplates.hideAppLauncher.UserControl_HideAppLauncher" %>
  • Add this style to end of the file and save the file.
<style>
#O365_MainLink_NavMenu{display:none;}
</style>

 

Step 2

  • Add an Empty Element to the project and use it to register above newly created User Control for OOB Delegate Control with Id "AdditionalPageHead". Please note that it is important to add an Empty Element file instead of a Module from the new Project item dialog else you will face issues when changing the scope of the feature to Farm.
  • Right click on the Project in Solution Explorer, Add New Item, choose "Empty Element", name it as "SetDelegateControl".
  • The newly generated element's default Elements.xml has below content.
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
</Elements>
  • Add this inside the Elements tag:
<Control Id="AdditionalPageHead" Sequence="1000" ControlSrc="~/_controltemplates/15/hideApplauncher2/UserControl_hideApplauncher.ascx" />
  • The final xml should look as below
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Control Id="AdditionalPageHead" Sequence="1000" ControlSrc="~/_controltemplates/15/hideApplauncher/UserControl_hideApplauncher.ascx" />
</Elements>

 

Testing the Delegate Control
Adding an empty element will cause a new Feature to be added to the Features folder in the project automatically. This feature will include the above Element "SetDelegateControl".

Do F5 test for this solution now, make sure it does hide the App Launcher on the site where the feature is activated.

 

Deploying the feature as Farm scoped feature

By default the feature for the delegate control is scoped as Web. Hence the App Launcher will be hidden only on the web where the feature has been activated. To hide the App Launcher on all the sites across the SharePoint Farm we can deploy the feature as a Farm scoped feature.

Retract the deployed solution from the SharePoint Farm. 

Go to the Project, select the Feature, change the Feature Scope from default “Web” to “Farm”, and verify the manifest file on second Tab.

Build and publish the solution to hideApplauncher.wsp file.

Then add the solution package to SharePoint farm with SharePoint powershell command -
add-spsolution

Deploy the solution in Central Administration - System Settings - Manage farm solutions,
select the newly added solution and Deploy it.

You will see the Applauncher disappear right away, even for the Central Admin site

With this solution, farm administrator only needs to deploy one farm solution and manage one farm feature.
You can turn the Farm feature off to unhide the App Launcher.

 

Also, you can switch the Feature scope to other levels, like Site or Web Application as your business requirement needs.

Retrieving list item attachment and using Outlook REST endpoint to send email with file attachment

$
0
0

This post is a contribution from Manish Joshi, an engineer with the SharePoint Developer Support team

The purpose of this blog is to walk through the process of calling the Outlook Mail API to send messages in Office 365 and Outlook.com. These guide focuses on the OAuth and REST requests and responses. It will cover the sequence of requests and responses that an app can use to authenticate and send an email messages with file attached from a SharePoint Online list item.

Authentication is covered under:

https://docs.microsoft.com/en-us/outlook/rest/get-started

Note: The outlook endpoint used in this guide https://outlook.office.com/api/beta is beta endpoint and should not be used in Production.

The following sections demonstrate sending an email with attachment from SharePoint Online using the Outlook REST Endpoint. This involves 3 steps which are listed below.

 

POSTMAN calls:

Step 1: Creating an email message

This is a POST request to the endpoint https://outlook.office.com/api/v2.0/me/MailFolders/inbox/messages. The body of the request will contain details like subject, email body and recipient email address. Note the Id returned in the response. This will be used for subsequent requests.

 

POST:

https://outlook.office.com/api/v2.0/me/MailFolders/inbox/messages

Body:

{

"Subject": "Did you see last night's game?",

"Importance": "Low",

"Body": {

"ContentType": "HTML",

"Content": "They were <b>awesome</b>!"

},

"ToRecipients": [

{

"EmailAddress": {

"Address": "garthf@spo.onmicrosoft.com"

}

}

]

}

Status:

201 Created

Response:

{

"@odata.context": "https://outlook.office.com/api/v2.0/$metadata#Me/MailFolders('inbox')/Messages/$entity",

"@odata.id": "https://outlook.office.com/api/v2.0/Users('aaaa2e55-386a-4226-817b-c774a83bbbde@d6f932a7-5f74-41ed-8d92-b27004970770')/Messages('AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA==')",

"@odata.etag": "W/\"CQAAABYAAAD4u6EvSxjoRYdXUVukcUWEAAPywecn\"",

"Id": "AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA==",

"CreatedDateTime": "2017-10-24T23:11:27Z",

"LastModifiedDateTime": "2017-10-24T23:11:27Z",

"ChangeKey": "CQAAABYAAAD4u6EvSxjoRYdXUVukcUWEAAPywecn",

"Categories": [],

"ReceivedDateTime": "2017-10-24T23:11:27Z",

"SentDateTime": "2017-10-24T23:11:27Z",

"HasAttachments": false,

"InternetMessageId": "<CO1PR04MB553B61033EB47364785F9EBF6470@CO1PR04MB553.namprd04.prod.outlook.com>",

"Subject": "Did you see last night's game?",

"BodyPreview": "They were awesome!",

"Importance": "Low",

"ParentFolderId": "AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MAAuAAADKqbIbNdyOUuJz-wQyvCCegEA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA",

"ConversationId": "AAQkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MAAQAEp5UbZ0wvtDnnmReZjdsPc=",

"IsDeliveryReceiptRequested": false,

"IsReadReceiptRequested": false,

"IsRead": true,

"IsDraft": true,

"WebLink": "https://outlook.office365.com/owa/?ItemID=AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz%2FwQyvCCegcA%2BLuhL0sY6EWHV1FbpHFFhAAAAgENAAAA%2BLuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA%3D%3D&exvsurl=1&viewmodel=ReadMessageItem",

"InferenceClassification": "Focused",

"Body": {

"ContentType": "HTML",

"Content": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<meta content=\"text/html; charset=us-ascii\">\r\n</head>\r\n<body>\r\nThey were <b>awesome</b>!\r\n</body>\r\n</html>\r\n"

},

"ToRecipients": [

{

"EmailAddress": {

"Name": "Garth Fort",

"Address": "GarthF@spo.onmicrosoft.com"

}

}

],

"CcRecipients": [],

"BccRecipients": [],

"ReplyTo": []

}

 

Step 2: Adding an Attachment to the previously created email

This is a post request to the endpoint https://outlook.office.com/api/beta/me/messages/<<messageID>>/attachments. <<messageID>> is the ID retrieved from Step 1. The body of the request will contain the path of the file In SPO/ODB to be attached.

POST:

https://outlook.office.com/api/beta/me/messages/AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA==/attachments

Body:

{

"@odata.type": "#Microsoft.OutlookServices.ReferenceAttachment",

"Name": "Test1.txt",

"SourceUrl": "https://spo.sharepoint.com/sites/TestSite11/Lists/MarketPlace/Attachments/1/Test1.txt",

"ProviderType": "other",

"Permission": "Edit",

"IsFolder": "False"

}

Status:

201 Created

Response:

{

"@odata.context": "https://outlook.office.com/api/beta/$metadata#Me/Messages('AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA%3D%3D')/Attachments/$entity",

"@odata.type": "#Microsoft.OutlookServices.ReferenceAttachment",

"@odata.id": "https://outlook.office.com/api/beta/Users('faaa2e55-386a-4226-817b-c774a83bbbde@d6f932a7-5f74-41ed-8d92-b27004970776')/Messages('AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA==')/Attachments('AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAAESABAAJmVbtRdmnUu74pWlPdmpgA==')",

"Id": "AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAAESABAAJmVbtRdmnUu74pWlPdmpgA==",

"LastModifiedDateTime": "2017-10-24T23:12:03Z",

"Name": "Test1.txt",

"ContentType": "text/plain",

"Size": 362,

"IsInline": true,

"SourceUrl": "https://spo.sharepoint.com/sites/TestSite11/Lists/MarketPlace/Attachments/1/Test1.txt",

"ProviderType": "Other",

"ThumbnailUrl": null,

"PreviewUrl": null,

"Permission": "Edit",

"IsFolder": false

}

 

Step 3: Sending the email

The previous two steps involved creating an email message and adding an attachment. The final step is to send the email message. This is a post request to the endpoint https://outlook.office.com/api/beta/me/messages/<<messageID>>/send. The request and response body are empty in this case. The status will be "202 Accepted" if the email message has been successfully sent

POST:

https://outlook.office.com/api/beta/me/messages/AQMkADcwMzljMTUzLTJlMDktNGFiNy1hYzUwLWU5OGYzNzZiMWY1MABGAAADKqbIbNdyOUuJz-wQyvCCegcA_LuhL0sY6EWHV1FbpHFFhAAAAgENAAAA_LuhL0sY6EWHV1FbpHFFhAAD8kJ2LgAAAA==/send

Body:

Status:

202 Accepted

Response:

 


Retrieve granular user actions or usage reports using Search-UnifiedAuditLog cmdlet

$
0
0

This post is a contribution from Manish Joshi, an engineer with the SharePoint Developer Support team

The following blog post demonstrates the steps to retrieve granular user action or usage reports using the Search-UnifiedAuditLog commandlet.

1.       Browse to https://protection.office.com.

           In the left pane, click Search & investigation, and then click Audit log search

Note: You have to first turn on audit logging before you can run an audit log search. If the Start recording user and admin activity link is displayed, click it to turn on auditing. If you don't see this link, auditing has already been turned on for your organization. It will take couple of hours before you are able to see log results in UI or via code.

2.       Browse to https://outlook.office365.com/ecp/

a.       Under permissions – go to admin role

b.       Create a new role, called AuditReportRole

c.       Assign following Roles:

                                 i.            Audit Logs

                               ii.            View-Only Audit Logs

d.       Add Members

Add users (for e.g: garthf@spo.onmicrosoft.com)

e.       Write-Scope --> Default

In the screenshot below. I am creating a new admin role called “AuditReportRole”, assigning minimum required permissions “Audit Logs” and “View-Only Audit Logs” and granting a user “Garth Fort” permission to be able to access the Usage reports.

3.       Use following powershell script, please make changes as per your environment and this will generate .csv file for each user with the actions they have undertaken for last 7 days.

$Username = "garthf@spo.onmicrosoft.com"
$Password = ConvertTo-SecureString 'password' -AsPlainText -Force
$LiveCred = New-Object System.Management.Automation.PSCredential $Username, $Password

$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $LiveCred -Authentication Basic -AllowRedirection

Import-PSSession $session

Connect-MsolService -Credential $LiveCred

$Users = Get-MsolUser | Where-Object {$_.UserPrincipalName -notlike "*#EXT#*" }

$Users | ForEach {

$OutputFile = "C:\SomeFolder\Usage-" + $_.DisplayName + ".csv"

$auditEventsForUser = Search-UnifiedAuditLog -EndDate $((Get-Date)) -StartDate $((Get-Date).AddDays(-7)) -UserIds $_.UserPrincipalName -RecordType SharePoint -Operations FileAccessed,PageViewed,PageViewedExtended


Write-Host "Events for" $_.DisplayName "created at" $_.WhenCreated

$ConvertedOutput = $auditEventsForUser | Select-Object -ExpandProperty AuditData | ConvertFrom-Json

$ConvertedOutput | Select-Object CreationTime,UserId,Operation,Workload,ObjectID,SiteUrl,SourceFileName,ClientIP,UserAgent | Export-Csv $OutputFile -NoTypeInformation -Append
}

Remove-PSSession $session

 

4.   Sample CSV output

5. Please also go thru following articles to better understand the Audit log concept and detailed properties that can be retrieved:

  https://support.office.com/en-us/article/Search-the-audit-log-in-the-Office-365-Security-Compliance-Center-0d4d0f35-390b-4518-800e-0c7ec95e946c?ui=en-US&rs=en-US&ad=US

https://technet.microsoft.com/en-us/library/mt238501(v=exchg.160).aspx

https://support.office.com/en-us/article/Detailed-properties-in-the-Office-365-audit-log-ce004100-9e7f-443e-942b-9b04098fcfc3

Using Office UI Fabric in SharePoint

$
0
0

This post is a contribution from Suresh Meenakshisundaram, an engineer with the SharePoint Developer Support team

Disclaimer:   This blog post is to give more information on the subject and not an official documentation about component or licensing or recommendation from Microsoft.

Office UI fabric is a front-end framework to build UI for application’s extensions, add-ins (MS office applications like word, excel…and O365) based on Office Design Language (ODL). What is ODL? Office Suite products follow set of design standards which is called Office Design Language. Those are nothing but using proprietary

  • Fonts
  • CSS
  • JS
  • Icons
  • Animations

Office UI fabric gives those list of components, so the UI will be more appealing to end user and will not be rendered odd among other office UI screens. In nutshell, Office UI Fabric is a bootstrap for Office & O365.

 

Office UI fabric is split in to couple of projects. Most important one is Fabric Core. As the name implies this is core project of other projects. This project gives the core pieces like CSS, Fonts, Icons, Responsive Grid, Animations.

https://github.com/OfficeDev/office-ui-fabric-core

 

The next fabric project is Office-UI-Fabric-JS. This gives basic functionality components like DatePicker, ColorPicker, PersonaCard..etc. All the codes are written in pure JavaScript.

https://github.com/OfficeDev/office-ui-fabric-js

 

The newest addition to the Office UI Fabric is Office-UI-Fabric-React which is getting lot of attention these days. The components are built using react framework.

https://github.com/OfficeDev/office-ui-fabric-react

 

In addition to above, OfficeDev/Office-UI-fabric-ios is available for iOS components.

The last one is ngOfficeUIFabric/ng-officeuifabric which is Angular version. This is a community project and not driven by Microsoft.

 

There is a chorme extension is available for Fabric which is called “Fabric explorer” used for development purpose. You can search on chrome developer tools extensions  and video is available in https://www.youtube.com/watch?time_continue=17&v=e8v-Zw1iRZs

 

All Fabric assets distributed via GitHub are licensed under the MIT license which means you can use them on any projects provided you follow the MIT license. Specifically, for the fonts, as stated by their license, you can use them only on Office Add-Ins. In this context Office Add-Ins include any web applications that extend the Office User Experience including: Office Add-Ins (previously known as Apps for Office), SharePoint Add-Ins (previously known ad Apps for SharePoint) and Office 365 Apps (those launched from within the Office launcher).

Office UI Fabric for SharePoint 

Fabric is official front-end framework for O365 and SharePoint Online. All the new modern team sites, modern pages and modern lists are built using Office-UI-Fabric.  SPO uses Office-UI-Fabric core & React in SharePoint and pushes update to SharePoint online periodically. Because of this, SPFx generator installs right version Office UI Fabric React as well. So when you build the SPFx components, use the fabric which comes with installation and don’t update it separately. The update will conflict with version in SharePoint and as a result your component may fail.

You can go through examples from https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/use-fabric-react-components

And you can play around with it through https://codepen.io/andrewconnell/pen/NGRvxW

 

Creating custom authentication provider for OneDriveClient and GraphClient

$
0
0

This post is a contribution from Sohail Sayed, an engineer with the SharePoint Developer Support team

The following post demonstrates creating a custom authentication provider which can be used for authentication with the OneDriveClient or GraphClient classes.

 

Why create a custom authentication provider

The One Drive SDK sample at https://github.com/OneDrive/onedrive-sdk-csharp uses the MsaAuthenticationProvider which authenticates with OneDrive Personal only. There is an alternate DelegateAuthenticationProvider which works for OneDrive For Business but only for apps registered in https://apps.dev.microsoft.com and not in https://portal.azure.com .

Also you may scenarios where you may need to customize the authentication process. For example you may want to use a service account credential to authenticate and avoid prompting the user for credentials.

 

Creating the custom authentication provider

Creating a custom authentication provider involves the below steps

1.       Create a class implementing the Microsoft.Graph.IAuthenticationProvider

2.       Implement the method “Task AuthenticateRequestAsync(HttpRequestMessage request)” which will perform the authentication.

The AuthenticateRequestAsync method has a parameter of type HttpRequestMessage which represents the HttpRequest for the OneDriveClient or the GraphClient. The code for the authentication provider needs to add the AccessToken to the Authorization Header for this request.

 

Code Sample

Below implementation will prompt user for the first time to enter the user credentials for OneDrive. You need to reference the following NuGet Packages.

  • Microsoft.Graph.Core
  • Microsoft.IdentityModel.Clients.ActiveDirectory –  ADAL Library
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using System.Security;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace Sample
{
    // simple implementation of ODBAuth
    public class ODBAuthentication : IAuthenticationProvider
    {

        internal const string GraphApiUrl = "https://graph.microsoft.com";
        internal const string Authority = "https://login.microsoftonline.com/";

        public string TenantId { get; private set; }

        public string ClientId { get; private set; }
        public string RedirectUrl { get; set; }
        public string AccessToken { get; private set; }

        private IntPtr handle;

        // this works with a windows application
        // We need to get the current window handle
        // Within a windows form you can get the current windows handle using the property this.Handle
        public ODBAuthentication(string tenantId, string clientId, string redirectUrl, IntPtr handle)
        {
            this.TenantId = tenantId;
            this.ClientId = clientId;
            this.RedirectUrl = redirectUrl;
            this.handle = handle;
        }

        AuthenticationResult authenticationResult = null;

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            AuthenticationContext authenticationContext = new AuthenticationContext(Authority + TenantId, false);

            try
            {
                // tracking if the user has signed atleast once within the app 
                // you can avoid this by using the option PromptBehavior.Auto but this will sign-in with the default user
                // use PromptBehavior.RefreshSession if you want to give user the option to choose the user login first time
                // Alternatively for console application or background applications use the overload of AcquireTokenAsync to signin with a service account using silent sign in
                if (authenticationResult == null)
                {
                    authenticationResult = await authenticationContext
                        .AcquireTokenAsync(GraphApiUrl, ClientId, new Uri(this.RedirectUrl), new PlatformParameters(PromptBehavior.RefreshSession, handle))
                        .ConfigureAwait(false);
                }
                else
                {
                    // Use PromptBehavior.Auto for subsequent requests
                    authenticationResult = await authenticationContext
                      .AcquireTokenAsync(GraphApiUrl, ClientId, new Uri(this.RedirectUrl), new PlatformParameters(PromptBehavior.Auto, handle))
                      .ConfigureAwait(false);
                }


                request.Headers.Add("Authorization", "Bearer " + authenticationResult.AccessToken);

            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

    }
}

Below example uses the custom authentication provider with a OneDriveClient.  You need to create a native app in https://portal.azure.com with appropriate access for OneDrive and pass the TenantName, ClientID and RedirectUrl values from that app.

ODBAuthentication odbAuth = new ODBAuthentication(tenantName, clientId, redirectUrl, this.Handle);
var oneDriveClient = new OneDriveClient("https://graph.microsoft.com/v1.0/me/", odbAuth);

Using the custom authentication provider with GraphClient class is similar.

ODBAuthentication odbAuth = new ODBAuthentication(tenantName, clientId, redirectUrl, this.Handle);
var graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0",odbAuth);

Uploading Large Files using Microsoft Graph API

$
0
0

This post is a contribution from Adam Burns, an engineer with the SharePoint Developer Support team

A while back, I wrote an article explaining that you must use File Chunking if you want to upload files larger than 250 MB to SharePoint or OneDrive for Business, using either the SharePoint REST API or CSOM.  This is because the file upload limit is set in the service and cannot be changed.  Using the Microsoft Graph API is even more restrictive.  The file size limit for uploading files is so small (4 MB) that you really should always use the chunking method, but the implementation is a little different than it is with the SharePoint REST API.  Also, to get an access token for Graph you will always need to use an Azure AD authorization endpoint.

At the bottom of this post you find links to two examples of how to upload larger file using Graph – one that uses plain JavaScript and jQuery and the other using a simple Angular 4 framework.  These examples both use Azure v2.0 authentication protocols documented at: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols.  This means you can use the same code to access resources with either your organizational credentials (such as for SharePoint Online or OneDrive for Business document libraries) or your Microsoft account credentials (such as OneDrive Personal document or contacts).

 

The Basics

The main operation you need is /createUploadSession. The details of the API are clearly documented here: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession#create-an-upload-session

You’ll need to write more code than you may expect, to do this correctly.

Here are the main steps you need to perform to upload a file using the /createUploadSession approach:

  1. Use /createUploadSession to retrieve an upload URL which contains the session identifier. The response will look like this:
    {
      "uploadUrl": "https://sn3302.up.1drv.com/up/fe6987415ace7X4e1eF866337",
      "expirationDateTime": "2015-01-29T09:21:55.523Z",
      "nextExpectedRanges": ["0-"]
    }
    
  2. Use PUT to upload chunks to the uploadUrl. Each chunk should start with a starting range represented by the first number in the “nextExpectedRanges” value in the JSON response from the last PUT. As shown above, the first range will always start with zero. Don’t rely on the end of the range in the “nextExpectedRanges” value. For one thing, this is an array and you may occasionally see multiple ranges. You will usually just pick the starting number of the 0th member of the array. As noted in the documentation, you should use a chunk size which is divisible by 320 KiB (327,680 bytes). So, the headers of your PUT request will look like this:
    PUT https:// {tenant}-my.sharepoint.com/personal/adambu_{tenant}-_onmicrosoft_com/_api/v2.0/drive/items/013JB6FTV6Y2GOVW7725BZO354PWSELRRZ/uploadSession?guid=%277734613e-7e6b-433a-840a-a77a93496928%27&path=%27Code%20Example%20Tagging%20Tool%20Deck.pptx%27&overwrite=True&rename=False&tempauth=eyJ0eXAiOiJKV1Q{....} HTTP/1.1
    Host: {tenant}-my.sharepoint.com
    Connection: keep-alive
    Content-Length: 327680
    Content-Range: bytes 0-327679/1048558
    Accept: */*
    Origin: ------snip------
    

 

Both the linked examples below use 4 main functions to do all this work:

  1. getUploadSession() which makes the initial POST request that retrieves the uploadUrl
  2. uploadChunks() which has the logic to loop based on the response to each PUT call. It in turn calls:
  3. readFragmentAsync() which actually slices up the byte array using FileReader and it’s onloadend event. Here is and example of that method:
    // Reads in the chunk and returns a promise.
        function readFragmentAsync(file, startByte, stopByte) {
            var frag = "";
            const reader = new FileReader();
            console.log("startByte :" + startByte + " stopByte :" + stopByte);
            var blob = file.slice(startByte, stopByte);
            reader.readAsArrayBuffer(blob);
            return new Promise((resolve, reject) =>  {
                reader.onloadend = (event) =>  {
                    console.log("onloadend called  " + reader.result.byteLength);
                    if (reader.readyState === reader.DONE) {
                        frag = reader.result;
                        resolve(frag);
                    }
                };
            });
        }
    
  4. uploadChunk(). This method is also called by uploadChunks to actually execute the ajax PUT call.

All this results in some pretty lengthy code. My examples may be more verbose because I put in a lot of logging to illustrate the process. You may figure out a way to make the code more efficient and terse, but for a production scenario, you will have to add additional code to handle unexpected situations. These scenarios are explained at: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession#create-an-upload-session. My purpose with this blog post was to expose some real-world code examples, because, at the time of this writing, there is almost nothing out there for this API. Once the basic approach is understood, it should not be hard to create tests and guard code to handle unexpected conditions.

 

Special Notes
I want to point out two things which are not mentioned in the above-referenced article.

  1. The articles and examples show the following request body for the initial call to create the upload session:
        const body = {
                "item": {
                    "@microsoft.graph.conflictBehavior":"rename"
                }
            };
    

    As you would expect, conflictBehavior is an enum that would provide different behaviors on the server side when there is a name conflict for the uploaded file. The definition of the enum, describes the following three values: Fail, Replace, Rename.
    When you use “rename,” as the conflict behavior, the file gets an incremented integer as a string appended to the file name, so for “myFile.docx” you would get a new file uploaded called “myFile 1.docx.”
    When you use “replace,” you end up replacing the file and all the metadata is updated. When you use “fail,” as the conflict behavior, the initial POST receives a HTTP 409 response ("The specified item name already exists.") and your code will have to handle the exception.

  2. In most cases, when you upload the final chunk of the file, you will receive a response with the HTTP status code of 201. If you specify a conflict behavior of “replace,” you will receive a HTTP 200 response instead. Therefore, you must handle the response with code with something like:
               if (res.status === 201 || res.status === 200) {
    	  	console.log("Reached last chunk of file.  Status code is: " + res.status);
                     continueRead = false;
               }
    

 

Sample Code
1.       https://github.com/adambu/createuploadsession-graph-jquery

2.      https://github.com/adambu/createuploadsession-graph-angular

Use POSTMan and AAD app to get data from SharePoint Online using Graph API

$
0
0

This post is a contribution from Mustaq Patel, an engineer with the SharePoint Developer Support team

If we want to do a quick check if the AAD app is working against SharePoint Online using Graph API, we can use postman to set this up quickly. This blog post will demonstrate how to use Graph API with grantType = Authorization code. The blog post will also show how to use version 2.0 OAuth2 endpoint URLs.

For using REST API with POSTMan, please follow the below steps. It also shows how to use Client Credentials grant type.

Using OAuth 2.0 AAD App to retrieve data from SPO site using Graph
Step1:
Register your AAD app using apps.dev.microsoft.com or by directly browsing Azure Active Directory that is associated with SharePoint Online Tenant. My registration looks like below using https://apps.dev.microsoft.com

  1. Give app a name. Generate new password and copy it somewhere.
  2. Click Add platform and Select Web. Add Redirect url which needs to be unique. Please note that after .com it should be /oauth2/callback for postman to work. I normally give https://spotenantname.apptitle.com/oauth2/callback
  3. Give permission to the app. Give delegated permission to Microsoft Graph and select Sites.Read.All
  4. Save. The app registration is complete. Keep ApplicationId, password which you copied in #1 and redirecturl for later use in postman.

 

Notes:
The permission you request will differ as per what you are retrieving from SPO. For now I am giving Sites.Read.All which will give read permission to the app on all sharepoint sites.

 

Step 2:

  1. Install latest PostMan (standalone version v5.5.2) from https://www.getpostman.com/apps.
  2. Open postman click Authorization and select OAuth 2.0. Click “Get New Access Token”.
  3. Input values as below.
    Callback Url – this should be the redirect Url we copied from app registration
    Auth Url – should be https://login.windows.net/common/oauth2/authorize?resource=https%3A%2F%2Fgraph.microsoft.com
    Note the resource value is encoded and is https://graph.microsoft.com
    Cliend Id – is the application Id we copied during app registration
    Client Secret – is the password we copied during app registration
  4. Click Request Token. This will ask you to authenticate to your SPO Site and will give you App Consent UI to trust the App. Accept the permissions. Now you can see id_token generated, scroll all the way below and click use token. You may have to cancel the popup 2, 3 times to go to main postman window.
  5. On main postman screen. Provide the Graph Request Url, Select GET operation and click Send. If successful, we should see body showing response from the server like below. For testing we are getting site details by using below Graph request (tenantname will be as per your tenant)https://graph.microsoft.com/v1.0/sites/tenantname.sharepoint.com:/
  6. Below are few graph requests that you can tryGet particular site details (/sites/TeamSite is the path that I am requesting)
    https://graph.microsoft.com/v1.0/sites/tenantname.sharepoint.com:/sites/TeamSiteGet listitems for a list from root site
    https://graph.microsoft.com/v1.0/sites/tenantname.sharepoint.com/lists/DA53F478-07C8-4E10-AD26-76D64347ADDF/itemsGet listitems for another site
    First get that site id by below request
    https://graph.microsoft.com/v1.0/sites/tenantname.sharepoint.com:/sites/TeamSite

    copy the entire id value and replace siteid below
    https://graph.microsoft.com/v1.0/sites/siteid/lists/10FE61F7-EE89-4D86-A8FB-33DA455FAC2D/items

 

Using OAuth 2.0 version 2 endpoints to retrieve data from SPO site using Graph

For using oauth2 version 2.0 endpoint urls and use graph, the steps are exactly same as above, except the step where we get AccessToken, we have to use Scope and AuthUrl, TokenUrl will change.

Also, please note that 2.0 endpoint urls only support graph APIs and that means for SharePoint it is very limited.

Using same above appregistration, here is my request to get AccessToken using oauth2 version 2.0 urls

Callback Url - this should be the redirect Url we copied from app registration
Auth Url – this should be https://login.windows.net/common/oauth2/v2.0/authorize
Access Token Url – this should be https://login.microsoftonline.com/common/oauth2/v2.0/token
Scope – List of scopes / permissions example https://graph.microsoft.com/files.read.all sites.read.all

Main difference between v2.0 urls and older once are below
Auth Url
V1 = https://login.windows.net/common/oauth2/authorize?resource=https%3A%2F%2Fgraph.microsoft.com
V2= https://login.microsoftonline.com/common/oauth2/v2.0/authorize

Token Url
V1 = https://login.microsoftonline.com/common/oauth2/token
V2 = https://login.microsoftonline.com/common/oauth2/v2.0/token

Scope
V1 = not needed
V2 = https://graph.microsoft.com/files.read.all sites.read.all (multiple permissions separated by spaces. We can only have 1 scope which is https://graph.microsoft.com)

 

 

 

Viewing all 100 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>