Showing posts with label SPA. Show all posts
Showing posts with label SPA. Show all posts

Saturday, September 16, 2023

Call an Azure AD protected API from a SPA using MSAL JS

Overview

This is an upgrade of this great post from Janne Hansen. Thanks to him. He helped me alot!
The goal is to autenticate a user with Azure AD on a Single Page Application made with JavaScript, and, from this page, let the user call an Azure API protected with Azure AD while the user has no need to sign in for the API. There is also a notion of Single Sign On (SSO) in there. At the first sign-in user authenticates with Azure AD, then, is also authenticated for the API
The code of Jane Hansen is available at GitHub:jannehansen/SinglePageAndAAD
It uses MSAL browser V2, and stil works perferctly well.
I had to update Janne Hansen's post rather for the steps of Azure cofiguration.

Authentication Flow - First step

First, to well configure everything for the solution in Azure Portal and understand the code used for MSAL (either JavaScript or Angular) you need to figure out how the double authentication flow is working.
For authenticating a user to Azure AD, you have to use Azure AD Applications. Azure AD applications are like super heroes that receives from a user a demand to be authenticated and let the user pass through Azure AD. But as every super heroes their powers are limited. They can act only for a certain scope: after being authenticated, you can be redirected only for the demanded scope, SPA, API, MS online product, SharePoint online, Outlook online etc...
So, for our solution, first we need two super heroes:
  • one application for the scope of our Single Page Application
  • another application for the scope of our API

  • Testing this first step

    Clone with git or dwonload the code GitHub:jannehansen/SinglePageAndAAD
    Install NPM http-server
    Run a command, navigate to the code repository to the folder functioncall and serve the SPA locally by:
    • running http-server
    • calling http://localhost:8080 in a browser

    Configuring the portal for the SPA

    We are now going to create our first superhero for the SPA authentication.
    In Azure Portal, go to Azure Directory.
    Then, in the left menu, click on "App Registrations", then, click to New Registration:
    Then fill the required fields:
    • The App name
    • Choose SPA as a platform
    • and http://localhost:8080 as the redirect url
    Once App is created you can see the App overview. Note cerefully the Client Id and the tenant Id.
    Then, go back to VS Code and update the client Id and the tenant id in your code:
    then, click the link to sign in. You will have a pop-up to fill your Azure login and password, and will receive the app demand for permission consent.
    By the way, you will see the App name and you will be sure you were calling the good App.
    Finally you get signed in and the SPA displays your AAD account information.
    Important: It is super difficult to get rid of cache with this solution using http-server, thus, use systmatically private/incognito windows while doing your tests.
    And yes, I know, you will have to sign in for every test, and you may get nervous.

    Configuring the portal for the API

    Now, let's do the part dedicated to the API.
    This time, we are not going to use the Azure Active Directory. We are going to use the configurtaion panes of an existing API within Azure portal.
    If you have an API available in your Azure tenant, go to it.
    If you haven't, you can use my previous post to quickly build an API based on PowerShell Azure function, then come back to this post.

    Go to your API configuration within Azure Portal and click on authentication in the left menu
    then click on "add identity provider" button
    then choose Microsof as prodider, and let create new app registration checked.
    Type your AAD app name.
    Important:It is better to create the API and create the Azure Active Directory App from the API Authentication pane, than create the AAD app first then use it for the API authentification because Azure will configure everything for your API.
    Let the other choices by default, even redirect is said not be recommended for API, because we are going to use the redirect later.
    Then your authentication is set API side in the Azure portal.
    Now click the name of your AAD app newly created, and you will be redirected to this new app in Azure AD
    and if you click authentication in the left menu you will see that the redirect link for sign in has been created automatically!
    Now's the time to test that, so paste your API url in a browser and you will be asked to sign in.
    then, once signed, you will see the app permission consent pop-up and will be able to check that you called the good AAD app to authentify:
    and finally, after having consent, you'll see your API display its content in the browser.

    Single Sign On cofiguration


    You might say, well, that's fine all that, but if, as you say, the superheroes are stuck in their own perimeters, how can I log in with the SPA superhero and then, be authenticated for the API with the other superhero without signing in a second time?
    That's because Microsoft Azure Active Directory allows superheroes to delegate permissions!
    Ok, that's funny, but let's be serious, how are you setting that, seriously ?
    It's elementary, my dear Watson
    Go back to the API superhero in AAD and go to Expose API pane. Microsoft has planned this, as you can tell an api what is its client application:
    Click on the "+"
    check the proposed scope and paste the client id of the SPA AAD app
    Here is the final screenshot before saving

    CORS

    To access an API from a SPA, this not the only configurations you have to do. You also have to allow the url of the SPA to call the API. This is CORS (Cross-Origin Resource Sharing). Normally an API can only be accessed by an url with the same domain. Here, were are wrong. SPA is http://localhost and API is https://helloworldfunction1123.azurewebsites.net. So we have to authorize http://localhost to call https://helloworldfunction1123.azurewebsites.net.
    So, go back to Azure Portal in the API configurtaion, locate CORS in the left menu and enter http://localhost:8080 as an authorized domain for CORS.

    JavaScript MSAL code side and testing

    Now, everything in Azure is configured properly, we have to replicate this configuration in our MSAL code.
    You have only 2 params to chang in authRedirect.js The value of the only entry for the scopes table and the url of the API you want to call.
    Get the code
    So now, after the code modification, stop the http-server with ctrl C. Restart it. Open a new inPrivate/Incognito browser window.
    Sign in again to the SPA in if you click the link to the API while signed in:
    you will have the response of your API displayed:

    To get further

    You noticed I have displayed the second authorization token in the browser console. You can decrypt it using JWT.io.
    You will notice we retrieve the authentication origin with the spa client id and the authorization scope for the API.
    The token is saying: "I come from the spa and request authentication for the API"

    Saturday, November 7, 2015

    Embed an Office Group Conversation into SharePoint Online using a JavaScript App, CORS, and the Office 365 Unified API

    I am very happy to publish a post where I can use the brand new Office 365 unified API to embed a conversation of the brand new Office Group into a SharePoint Online page!


    1 - Architecture
    A SharePoint Online html page sends an Ajax request to the Office 365 Unified API and get the data of the Office Group.(The SharePoint Online site and the Office Group belong of course to the same Office 365 tenant).
    It seems simple at first but it wasn't possible since recently. Why?
    Because of the different hostheaders of the different part of Office 365, You can check while navigating through Office 365. For example, recently I created an Office 365 environment and first registered as contososoftware. My domain was contososoftware.onmicrosoft.com. Then I had to buy a domain to activate Yammer (You should have a verified domain in Office 365 to have Yammer activated to Enterprise. The "onmicrosoft.com" is not a domain but an Office 365 tenant name or a default domain and cannot be used to activate Yammer Enterprise. So I bought marccharmois.com.
    (roughly 14 US$ a year, not a big deal). After that, when I go to
    • My mail, My calendar, My contacts (people), My Tasks, the Url starts with https://outlook.office.com/owa/?realm=marccharmois.com#path=
    • NewsFeed, OneDrive, Sites, Delve, Video, the Url starts with https://contososoftware-my.sharepoint.com/
    • Yammer, the Url starts with https://www.yammer.com/marccharmois.com/
    • a SharePoint Online site, the Url starts with https://contososoftware.sharepoint.com/
    So if I want to deploy an html page somewhere Online in Office 365 and make an Ajax request to a part that has a different hostheader (for example displaying some of my mails into a SharePoint Online page), it normally doesn't work because crosss domain Ajax requests are forbidden by the browsers. It is worse if my HTML page is deployed on a server.
    Cross-origin resource sharing (CORS) is a mechanism that allows that. CORS defines a way in which a browser and server can interact to safely determine whether or not to allow the cross-origin request. It allows for more freedom and functionality than purely same-origin requests, but is more secure than simply allowing all cross-origin requests. It is a recommended standard of the W3C. (Wikipedia)
    Of course Microsoft sets up Cors within Office 365 in order developers to be allowed to performed Ajax requests without being bothered by the different hostheaders of Office 365.
    The CORS of Office 365 is based on Microsoft Azure:
    • You register an App in Azure and you are provided with ID's that you place in your code.
    • In the Azure App, you also reference the Urls of the pages that are planned to call the Office 365 API with cross domain requests
    This double registration allows the CORS of Office 365 to properly work and also manage the oauth authentication.

    That leads to the fact that the term "Application" or App has a double meaning. It could be:
    • The pages that you have deployed somewhere on a server or within the Office 365 cloud (for example SharePoint Online) and that performs the Ajax requests and displays the data. Let's call this the physical part of the App, or the physical App
    • The Application that you had to add to Azure for getting the Office 365 CORS and the oauth authentication to work. Let's call that the virtual part of the App, or the virtual App
    In the case of this tutorial, my physical App is just an html page added to a SharePoint Online team site using explorer mode (WebDAV protocol)
    It is very similar of what I did when I embedded a Yammer conversation within SharePoint Online. This is super convenient because I can program and deploy quickly a physical App in Office 365 based on an html page that uses only Javascript!
    It could seem a little bit difficult to understand at this point of the post, but I will explain all the operations to do it step by step with all the detailed screenshots as usual, so don't worry. I also did a diagram that can summarize all this in a clearer way:



    2 - What you need to do this tutorial
    You need:
    Once you can access to an Office 365 environment using a business account with global administrator privileges, the first thing you need to do is register your application with Azure AD.

    3 - Creating the (virtual) App within Microsoft Azure
    3.1 Create a new subscription to Microsoft Azure
    To Navigate to the Azure Portal you can use this link: https://manage.windowsazure.com/
    you can also access to it within 0ffice 365:
    Log on to Office 365. From the Home page, select the Admin icon to open the Office 365 admin center.



    In the menu page along the left side of the page, scroll down to Admin and select Azure AD.



    If prompted, log in using the credentials you created for your O365 subscription.



    After logging in, you should see a screen notifying you that you do not have a subscription



    Create a new subscription.
    If you're using a trial version of Office 365, you'll see a message telling you that Azure AD is limited to customers with paid services. You can still create a trial 30-day Azure subscription at no charge, but you'll need to perform a few extra steps:
    Select your country or region, and then choose Azure subscription.
    Enter your personal information. For verification purposes, enter a telephone number at which you can be reached, and specify whether you want to be sent a text message or called.
    Once you've received your verification code, enter it and choose Verify code.
    Enter payment information, check the agreement, and select Sign up.
    Your credit card will not be charged.
    Do not close or refresh your browser while your Azure subscription is being created.
    Once your Azure subscription is created, choose Portal.
    The Azure Tour appears. You can view it, or choose X to close it.
    You should now see all items in your Azure subscription. It lists a directory with the name of your Office 365 tenant.

    3.2 Register your App in Azure Management Portal

    Once signed in, follow these instructions:
    Click the Active Directory node in the left column and select the directory linked to your Office 365 subscription.



    Select the Applications tab and then Add at the bottom of the screen.





    On the pop-up, select Add an application my organization is developing.



    Choose an explicit name for your app,(I took Embed-OfficeGroup), and select Web application and/or web API as its Type. Then click the arrow to continue.



    The value of Sign-on URL is the URL where your application will be hosted. As I will call the App using JavaScript within a SharePoint page, I put the Url of my SharePoint site.
    The value of App ID URI is a unique identifier for Azure AD to identify your app. You can use http://{your_subdomain}/{YourAppName}, where {your_subdomain} is the subdomain of .onmicrosoft you specified while signing up for your Office 365 Developer Site. Then click the check mark to provision your application. For example, my Office 365 global admin account is, in this tutorial case:

    marc.charmois@marccharmois.onmicrosoft.com





    Now that your app has been provisioned, select the Configure tab.



    Scroll down to the permissions to other applications section and click the Add application button.



    In this tutorial, we want our App to read the conversation of an Office group. To access to the Office Group we have to use the new Office 365 Unified API.
    Click the plus sign in the application's row and then click the check mark at the top right to add it. Then click the check mark at the bottom right to continue.



    In the Office 365 Unified API row, select Delegated Permissions, and in the selection list, choose Read All Groups.



    Click Save to save the app's configuration.







    3.3 Configure your app to allow the OAuth 2.0 implicit grant flow


    In order to get an access token for Office 365 API requests, your application will use the OAuth implicit grant flow. You need to update the application's manifest to allow the OAuth implicit grant flow because it is not allowed by default.
    Select the Configure tab of your application's entry in the Azure Management Portal.


    Using the Manage Manifest button in the drawer, download the manifest file for the application and save it to your computer.









    Open the manifest file with a text editor. Search for the oauth2AllowImplicitFlow property. By default it is set to false; change it to true and save the file.



    Using the Manage Manifest button, upload the updated manifest file.







    You've now successfully registered your application with Azure AD.

    4 - Coding the SharePoint page (physical App) that's using the Azure App (Virtual App)

    4.1 Creating the app.html page

    In your 0ffice 365 environment, create a new site collection. Once it's done, navigate to the "Site Assets" library. Open the library in Explorer mode




    and using the window navigate to the root folder of your site. Create an html page within the folder and name it app.html



    You can now open this page with Visual Studio or Notepad ++ and start to programm the displaying of the Office Group conversation



    You can find the complete app.html page in the dedicated Github repository

    4.2 Determine the resource endpoint

    In order to make any API requests, you'll need to determine the correct endpoint of the resource you want to use. The endpoint you'll use is determined by what information you want from Office 365. Refer to the API reference documentation to get the endpoint you want.
    Mail API reference
    Contacts API reference
    Calendar API reference
    Files API reference
    Alternatively, you can take advantage of the Office 365 unified API (preview) to access all of the APIs from a single endpoint, https://graph.microsoft.com. Refer to the Office 365 unified API reference to browse all of the supported endpoints.
    For this sample, we will use the Office 365 unified API to get a conversation of an Office Group. The endpoint for this operation is
    https://graph.microsoft.com/beta/{your_domain}/groups, where {your_domain} is the domain you specified while signing up for your Office 365 Developer Site. In my case marccharmois.onmicrosoft.com.

    My endpoint is: https://graph.microsoft.com/beta/marccharmois.onmicrosoft.com/groups

    4.3 Get an access token from Azure

    Office 365 uses OAuth 2.0 tokens issued by Azure AD to authenticate JavaScript clients. Tokens are obtained using the OAuth 2.0 implicit grant flow. Using implicit grant, your application requests an access token from Azure AD for the currently signed-in user by sending the user to an authorization URL where the user signs in with their Office 365 credentials and then is redirected back to the app with the access token in the URL.
    The following function builds the authorization URL and navigates to it to begin the authentication process.

    function requestToken() { 
      // Change clientId and replyUrl to reflect your app's values 
      // found on the Configure tab in the Azure Management Portal. 
      // Also change {your_subdomain} to your subdomain for both endpointUrl and resource. 
      var clientId    = 'e77659cc-bf72-4276-bd33-bdd876660a74';//ID of your App in Azure
      var replyUrl    = 'https://marccharmois.sharepoint.com/sites/intranet/app.html'; //my sharepoint page that requests an oauth 2 authentification and data
      //It is also referenced in the REPLY URL field of my App in Azure
      var endpointUrl = 'https://graph.microsoft.com/beta/marccharmois.onmicrosoft.com/groups';
      //var endpointUrl = 'https://marccharmois-my.sharepoint.com/_api/v1.0/me/files';//getting files from SharePoint
      //var endpointUrl = 'https://outlook.office.com/api/V1.0/me/messages';//getting messages  from outlook
      var resource = "https://graph.microsoft.com/";
      //var resource = "https://marccharmois-my.sharepoint.com"; //getting files from SharePoint
      //var resource = "https://outlook.office.com"; //getting messages  from outlook
      
      var authServer  = 'https://login.windows.net/common/oauth2/authorize?';  
      //var authServer  =  'https://login.microsoftonline.com/common/oauth2/authorize?';//this works either
      var responseType = 'token'; 
    
      var url = authServer + 
                "response_type=" + encodeURI(responseType) + "&" + 
                "client_id=" + encodeURI(clientId) + "&" + 
                "resource=" + encodeURI(resource) + "&" + 
                "redirect_uri=" + encodeURI(replyUrl); 
    
      window.location = url; 
    }
    

    At this point what's going on?
    When I use this function, I am redirected to the same page, but with the acces token in the Url as a parameter:





    I can even display the token

    var urlParameterExtraction = new (function () { 
      function splitQueryString(queryStringFormattedString) { 
        var split = queryStringFormattedString.split('&'); 
        // If there are no parameters in URL, do nothing.
        if (split == "") {
          return {};
        } 
        var results = {}; 
        // If there are parameters in URL, extract key/value pairs. 
        for (var i = 0; i < split.length; ++i) { 
          var p = split[i].split('=', 2); 
          if (p.length == 1) 
            results[p[0]] = ""; 
          else 
            results[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); 
        } 
        return results; 
      } 
      // Split the query string (after removing preceding '#'). 
      this.queryStringParameters = splitQueryString(window.location.hash.substr(1)); 
    })(); 
    
    
    
    function displayToken(){
    // Extract token from urlParameterExtraction object.
    var token = urlParameterExtraction.queryStringParameters['access_token'];
    alert('token : \n'+ token);
    }
    
    
    Now that I have the token, here is the code to request the Office 365 Unified API to get all the specific conversation of an Office Group:
    function getToken(){
    
    var token = urlParameterExtraction.queryStringParameters['access_token'];
    return token;
    }
    
    function getGroupsFromO365() { 
      try 
      { 
        //var endpointUrl = 'https://graph.microsoft.com/beta/marccharmois.onmicrosoft.com/groups'; //getting all groups to get the ID of the group you want
     //var endpointUrl = 'https://marccharmois-my.sharepoint.com/_api/v1.0/me/files';//getting files from SharePoint
     //var endpointUrl = 'https://outlook.office.com/api/V1.0/me/messages';//getting messages from SharePoint
     var endpointUrl = "https://graph.microsoft.com/beta/contoso.com/groups('4eba7454-b490-46ae-bb5e-774efaec7c6f')/conversations('AAQkADZjOTdkMTIwLWFjNTItNDUyYy05MTc4LTg1NmJmMDk1MjYxOQAQAD_Ql6WI-GlHs3VmVZsV3QA=')/threads('AAQkADZjOTdkMTIwLWFjNTItNDUyYy05MTc4LTg1NmJmMDk1MjYxOQMkABAAP5CXpYj8aUezdWZVmxXdABAAP5CXpYj8aUezdWZVmxXdAA==')/Posts";
    
        var xhr = new XMLHttpRequest(); 
        xhr.open("GET", endpointUrl); 
        var myToken = getToken();
        // The APIs require an OAuth access token in the Authorization header, formatted like this: 'Authorization: Bearer '. 
        xhr.setRequestHeader("Authorization", "Bearer " + myToken); 
    
        // Process the response from the API.  
        xhr.onload = function () { 
          if (xhr.status == 200) { 
         //alert('data received');
      var message="";  
      var object = JSON.parse(xhr.response); 
      for(i=0;i<object.value.length;i++){
      message+='From: ' + object.value[i].From.EmailAddress.Name + '<BR>';
      message+='At: ' + object.value[i].CreatedDateTime + '<BR>';  
      message+= object.value[i].Body.Content + '<BR>';  
      }
            //var formattedResponse = JSON.stringify(JSON.parse(xhr.response), undefined, 2);
            document.getElementById("results").innerHTML = message;
          } else { 
            document.getElementById("results").textContent = "HTTP " + xhr.status + "<BR>" + xhr.response; 
          } 
        } 
        // Make request.
        xhr.send(); 
      } 
      catch (err) 
      {  
        document.getElementById("results").textContent = "Exception: " + err.message; 
      } 
    }
    
    Now, you can display this app.html into any SharePoint page by calling it within an iframe tag...
    <span style="display:block;margin-bottom:3px;font-size;13px;">Embedded Office Group Conversation:</span> <iframe src="https://marccharmois.sharepoint.com/sites/intranet/app.html" style="height:400px;width=300px;border:solid 1px silver"></iframe>
    
    As I did for this Web Part page;
    5 - Aknowledgements