Wednesday, May 27, 2020

Speaking at Valo Online Summit EU 2020

I'm looking forward to speaking at Valo Online Summit EU 2020 on June 3-4, 2020.

The Valo Online Summit EU is an event exclusively dedicated to Valo Partners but this year the keynote ending the actual Summit will be open to everyone. Jeff Teper, the father or SharePoint, Corporate Vice President of Microsoft, will speak in this free keynote about the latest innovations and experiences in the Microsoft 365 world on June 4th at 5 p.m. (CET). This is a big opportunity for Valo partners, their customers and other community members to join this free keynote and learn directly from Jeff! 


I will also contribute to the Valo Online Summit's success with a session about "Extending the Valo Teamwork power using SharePoint Site Designs and Site Scripts". Valo Teamwork helps customers to find and manage Microsoft 365 groups, Microsoft teams and SharePoint sites. My session covers an integration between Valo Teamwork and SharePoint Site Designs. The presentation's history is based on the idea of creating project workspaces with predefined configuration.

I am very happy to be a part of the speakers team! Let's get in touch if you're also going to participate in this event!

Event information:

Thursday, May 7, 2020

Identifying if a Teams personal app runs from Teams Web or Teams Desktop Client

I’ve recently built a Teams personal app that required a different authentication mechanism when running from Teams desktop client. It took me a while to figure out how to distinguish between Teams desktop client and Teams web. As far as I’ve researched, there is no supported way yet to clearly identify where a personal app is running. Hence, I decided to write this blog post which shows how to identify if a Teams personal app runs from Teams web or Teams desktop client.



In my case, I tried to embed a SharePoint page as a Teams personal app. This page had a SharePoint Framework web part which did not run from Teams desktop client without a special authentication algorithm. For different reasons, it was not feasible to utilize this new authentication logic in both scenarios: Teams web and Teams desktop client. Well, it was clear. I had to find a way around this!

After intensive platform comparison, I observed two special characteristics. The document interface (loaded web page representation in the browser) and the navigator interface (identity of the browser):

1. document.referrer

When a Teams personal app runs from Teams web, the referrer can contain a value like: https://teams.microsoft.com/_. When the same personal app runs from Teams desktop client the referrer will then be: https://teams.microsoft.com/iframe-container.html. However, the referrer might change depending on operations executed from the embedded page. Hence, the referrer property is not reliable and didn’t help me in that case. 

2. navigator. userAgent

When a Teams personal app runs from Teams desktop client the userAgent always contain the Teams agent. This is a value which consists of Teams/{version} such as Teams/1.3.00.12058. In case of Teams web, the userAgent property doesn't contain the Teams agent. That made the tweak in my case! 


Summary:

Checking if the value "Teams/" existed in the userAgent property, helped me to identify when my personal app was running from Teams web or Teams desktop client. I am constantly looking for an out of the box solution and hope that Microsoft will provide it soon!

I hope this blog post helps you not spending hours trying to understand the uniqueness of those two platforms.

Wednesday, April 29, 2020

Speaking at Microsoft 365 Saturday Madrid Virtual 2020



I'm looking forward to speaking at Microsoft 365 Saturday Madrid Virtual 2020 on April 30, 2020.

Microsoft 365 Saturday Madrid Virtual (M365MAD) was created by the Microsoft 365 community from Madrid as a digital alternative to the famous SharePoint Saturday Madrid which must be postponed to October 2020 because of the current pandemic we are facing worldwide.

M365MAD is an online and free event which will be broadcasted from YouTube. International and local speakers will share their knowledge and experience about different areas of Microsoft 365. For instance, I will contribute to M365MAD 2020 as a speaker with a session about Site Designs and Site Scripts.

I want to thank the organizers for setting up this event and for having me as a speaker.

Related links:
• Agenda: https://microsoft-365-virtual-summit-spain.sessionize.com/schedule
• YoutTube channel: https://www.youtube.com/channel/UCY1cqnq_HqNlV9SGV5VzDjQ

Monday, April 27, 2020

Understanding the TeamsLogon page

Whenever you create a Teams personal app or a Teams tab, a Teams app package will be required. In case of SharePoint Framework (SPFx), SharePoint generates this app package automatically when the app gets synchronized to Microsoft Teams for instance by using the Sync to Teams ribbon button in the SharePoint app catalog. By default, the Teams app package contains the required images that let your app looks shine in Teams, but also a manifest file which contains important configuration about the app.

The generated Teams app package is convenient, but it does have its limitations - for instance it doesn't include localization or other kind of customization you might want to have. The alternative is to generate the app package manually. Microsoft already provides a very good guidance which you can find here. Therefore, I won't cover that in this blog post. Rather, I want to deep dive on the TeamsLogon page (/_layouts/15/TeamsLogon.aspx) which is part of those app manifests. The findings described here are the output of tests I have done on my test environment and on customers feedback.

TeamsLogon's URL structure:

Along with the request URL
https://{teamSiteDomain}{teamSitePath}/_layouts/15/teamslogon.aspx
two query string parameters are required to request the TeamsLogon page successfully:
- spfx
- dest

TeamsLogon page in action:

Let's forget SPFx for a moment! Maybe you just want to display a SharePoint page as a Teams personal app. The mere fact that SharePoint and Teams run in Microsoft 365 doesn't result in bypassing authentication to access SharePoint resources from a Microsoft Teams app. By default, SharePoint pages won't load inside Microsoft Teams apps. It requires additional configuration! Lucky us, since this is why the TeamsLogon page is therefor.

In simple words, the TeamsLogon page is a bridge between Microsoft Teams and SharePoint that makes the access from Microsoft Teams apps to SharePoint possible. Behind the scenes, this page uses an access token generated by the microsoftTeams SDK to authenticate against SharePoint using the SP.OAuth.NativeClient/Authenticate API. A successful POST call to this API, sets the SPOIDCRL cookie which is used for authentication in SharePoint on behalf of the signed in user. After authentication is completed, TeamsLogon page redirects the user to the page specified in the dest (destination) query string parameter. Moreover, depending if the destination page is a classic or a modern one, the spfx query string parameter will apply some layout modifications to the rendered page.

I've created a simple graphic which describes the TeamsLogon page's process:


dest query string parameter in detail:

The query string parameter dest plays a very important role in this authentication process. It contains the destination URL which will be loaded after authentication is completed. In addition to that, the value provided here will be used to build the base URL, which will be used as part of the SP.OAuth.NativeClient/Authenticate request. This is how the logic to build the base URL based on the dest value looks:

var baseUrl = decodeURIComponent(
window.location.href.substring(window.location.href.indexOf(destQueryParam) + destQueryParam.length));

Important: you can add query string parameters to the destination URL. If you want to have more than one query string parameter in the destination URL, it is important to encode the connecting & character. Otherwise, additional query string parameters will be recognized as being part of the TeamsLogon's URL and not as part of the destination page. This is how the encoded & character looks:

https://{teamSiteDomain}{teamSitePath}/_layouts/15/teamslogon.aspx?spfx=true&dest={teamSitePath}?forceLocale={locale}%26theme={theme}

spfx query string parameter in detail:

This parameter renders SharePoint modern pages inside Teams without navigation, footer and header. Basically, it renders only the page's content. Additionally, spfx requires a boolean value which must be always true. Setting it to false, results in the very famous Sorry, something went wrong error which happens when loading the page:


Also, those changes done by the spfx query string parameter won't apply to classic pages. Basically, navigation, header, footer etc. won't be hidden. If only the page's content should be displayed, additional CSS hack is required. I would personally advise against that and suggest you to use only modern pages inside Teams apps. This is what a classic page looks like when loaded inside Teams:


Final words:

Thank you for your reading this blog post. I hope it clarifies the meaning of the TeamsLogon page!

Thursday, April 16, 2020

List of errors you might face in the SharePoint API Management page

The SharePoint API Management page allows administrators to manage (approve, reject and remove) permissions requested/granted at tenant-level. However, the experience of error in this SharePoint admin area is simply terrible. From my point of view, a good error message should be an educational experience. Unfortunately, this is exactly the opposite in the SharePoint API Management page 😔

This blog post is a continuation of my previous post about a specific error message from the API management page which was causing confusion out there. Since Microsoft hasn't enhanced the error message's content and I have faced other encrypted errors when using the API management page, I decided to list all errors from the API management page in a dedicated blog post which I can maintain over the time.

The list below describes the possible error messages that you might get depending on the operation you want to execute. I also added possible solutions in order to help you getting rid of those inconvenient errors:

Case: 
Approve a requested permission as a SharePoint administrator
Error message:
[HTTP]:500 - [CorrelationId]:53ee2d9f-a0f9-2000-078d-b1ec5183945b [Version]:16.0.0.20001
Solution:
The logged in user must be a global administrator in order to approve requested permissions

Case: 
Reject a requested permission as a SharePoint administrator
Error message:
[HTTP]:403 - [CorrelationId]:5cee2d9f-801d-2000-6cd2-3db457dd64f0 [Version]:16.0.0.20001
Solution:
The logged in user must be a global administrator in order to reject requested permissions

Case: 
Remove a granted permission as a SharePoint administrator
Error message:
Insufficient privileges to complete the operation
Solution:
The logged in user must be a global administrator in order to remove granted permissions

Case: 
Approve a requested permission
Error message:
[HTTP]:400 - [CorrelationId]:a340499f-6054-2000-7120-91cbdfcc05f3 [Version]:16.0.0.20001
Solution:
Ensure the permission/scope wasn't misspelled. E.g.: A permission such as User.ReadWrit.All isn't valid! In this case, the requested permission would be User.ReadWrite.All. Consider correcting the permission and retrying the operation. Also, have a look at the Microsoft Graph permissions reference page for information about supported permissions: https://docs.microsoft.com/en-us/graph/permissions-reference

Case: 
Approve a requested permission
Error message:
[HTTP]:400 - [CorrelationId]:223e499f-00e1-2000-03d0-e19a7de3a6c5 [Version]:16.0.0.20001
Solution:
Ensure the API name/resource wasn't misspelled. E.g.: An API name such as Microsft Graph isn't valid! In this case, the specific resource secured with Azure AD would be Microsoft Graph. However, it is more common to face this error when dealing with  Azure AD enterprise applications. Consider correcting the API name and retrying the operation.

Case: 
Approve a requested permission
Error message:
[HTTP]:400 - [CorrelationId]:a53f499f-3046-2000-6cd2-30c17ac0aab4 [Version]:16.0.0.20001
Solution:
Ensure the API name isn't the application ID or the object ID of the resource secured with Azure AD. Only the display name of the application is supported! Consider correcting the API name and retrying the operation.


Helpful link:

This Microsoft documentation explains very well how to connect to Azure AD-secured APIs. It explains this process based on SharePoint Framework solutions. Check it out for more information!

Wednesday, March 11, 2020

Quick Tip: Understanding the unknown user issue in Microsoft Teams


Yesterday, some Office 365 customers started seen an unwelcome user in their Teams. Along with existing owners, an Unknown User was automatically added to the Team as an owner what was/is causing confusion!

In this case, the Unknown User represents the service principal account that was used to create the Team. So far, it wasn't displayed in Teams as a member or owner, but after a Microsoft update things badly changed 🙁

You can image the side affects of this issue: community reaction, customers opening tickets and so on. Definitely, something we want get fixed as soon as possible. However, Microsoft quickly reacted to this issue. If you have a look at your tenant's Service Health page, you will notice an advisory about the Unknown user in Teams case (TM206109). Consider having a look at it if you are looking for detailed information about it.

Long story short, the problem was caused by some recently update (code change) done by Microsoft. According to the message from the Service Health, they are now working on a hotfix for this issue. Here is how the advisory message looks:


It's comforting to know that Microsoft is already taking care of this glitch. I'm looking forward to getting that service principal account out of my Teams 😊


Edited: 13.03.2020:

According to Microsoft, they've completed reverting the code change and confirmed that impact has been mitigated.

If you are still seen the unknown user in Microsoft Teams, consider contacting the Microsoft support.

Sunday, February 23, 2020

Quick tip: Improving performance when retrieving a user using Microsoft Graph

There are different ways to look for a user using Microsoft Graph. In this blog post, I will cover three different approaches which might impact performance of your application. Especially, if you are dealing with Office 365 tenants which has thousands of users (40k, 500k, more?):

◾ Get a user based on the user principal name or user ID:
E.g.: https://graph.microsoft.com/v1.0/users/adams@m365x214355.onmicrosoft.com
◾ Filter by user principal name or user ID:
E.g.: https://graph.microsoft.com/v1.0/users?$filter=userPrincipalName eq 'adams@m365x214355.onmicrosoft.com'
◾ Get all users using paging then do conditional check
E.g: https://graph.microsoft.com/v1.0/users

Let's take a look at the advantage and disadvantage of the API to retrieve a user based on an identifier. E.g.: https://graph.microsoft.com/v1.0/users/adams@m365x214355.onmicrosoft.com

💚 Fast! I've tested this API in different Office 365 tenants containing 40k to approx. 500k users. There is no significant delay.
💔 This API throws an exception if the specified user doesn't exist in the Office 365 tenant! That can become a big problem if you use this API to search for a user based on user input. If the value to look for was misspelled it throws an exception. That is actually a good way for the API to communicate that the user doesn't exist, but you have to handle this exception. Otherwise, your application will break. BTW, this is how the exception looks:

GET: https://graph.microsoft.com/v1.0/users/adams@m365x214355.onmicrosoft.co

{
    "error": {
        "code": "Request_ResourceNotFound",
        "message": "Resource 'adams@m365x214355.onmicrosoft.co' does not exist or one of its queried reference-property objects are not present.",
        "innerError": {
            "request-id": "e4b98e04-cc46-4ea1-920d-13c61b6ab88f",
            "date": "2020-02-17T12:18:12"
        }
    }
}

This is how error handling could be implemented. In this example, I'm using the Graph SDK and C#:

Additionally, we can look for a user by using the OData query paramenter "filter". E.g.: https://graph.microsoft.com/v1.0/users?$filter=userPrincipalName eq 'adams@m365x214355.onmicrosoft.com' Here the advantage and disadvantage of this API:

💚 Good internal error handling! It won't throw an exception if the user wasn't found. Instead it returns an empty array. The Graph SDK returns null
💚 Fast! I've tested this API in different Office 365 tenants containing 40k to approx. 500k users. There is no significant delay! It was a couple of milliseconds faster than the first approach, but that is definitely not game changing
💔 Nothing! 😀 At least I didn't find anything negative during my tests/work with this approach

Now, lets have a look at the approach "Get all users using paging then do conditional check".

💚 Flexibility! You have full control over the objects. For instance, it is possible to create complex conditional checks
💔 Performance! This high level of flexibility has a very high price. During my tests, it took one minute to go through all users in a tenant with 40k users. In a tenant with 500k users, it took more than 20 minutes to walk through all users. For sure, you could and should just stop the loop once you have found the specified user which will probably reduce the amount of time. Since this API doesn't return all tenant users within the same call, you may have to do several calls to find the user you are looking for which can impact performance negatively


Summary

If you want to search for a user, consider using the first or second approach since they are good about performance. That might be cases where the "Get all users using paging" approach will be useful. But if you don't need it, consider replacing this logic with one of the other approaches. Otherwise, your application will suffer performance problems when installed in tenant with big amount of users.