Securing your Web API

Every SharePoint developer has probably done it: create a web service to access SharePoint data from a provider-hosted add-in and allow the web service to make app-only calls to SharePoint using an AppOnly access token. You forget that the web service doesn’t require any authorization, and doing so, people from outside your organisation – who know the existence of your web service – will  be able to access your SharePoint data as well, because by default there isn’t any authorization required to use the web service. You should never forget to only allow authenticated users by enabling Azure Active Directory Authentication for your web service!

In this blogpost I will show you how you can access your SharePoint data in a secure way, without using the AppOnly token. I will not even create a SharePoint add-in.

The three main components that will be used in this demo are:
– Azure AD application
– Azure web app to host your web service
– SharePoint online

Basicaly this means you will need to be global administrator on Azure to complete all of the steps.

Build your Web API in Visual Studio

Create a Visual Studio project

Open Visual Studio and create a new ASP.NET Web Application

securewebapi_vs_1

Create an empty project. I opted to add folders and references for Web API and to host the web app in the cloud.

securewebapi_vs_2

Because I selected to host the app in the cloud in the previous step, I can create the app service that will host the API from the same wizard.securewebapi_vs_3

Click Create.

Add NuGet packages

We will start to add some NuGet packages to the backend web project. Click Tools > NuGet Package Manager > Package Manager Console. Install the following packages:

Install-Package Microsoft.AspNet.WebApi
Install-Package Microsoft.AspNet.WebApi.Owin
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.Owin.Security.ActiveDirectory
Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory -Version 2.15.204151539
Install-Package SharePointPnPCoreOnline

Add Owin Startup Class

We will start by telling the Owin Framework what class is responsible for the Owin initialization.
Add a new item of type OWIN Startup class to the backend web project.

securewebapi_vs_4_owin

Add the following code snippet to the Startup class

[assembly: OwinStartup(typeof(Spikes.SecureWebAPIWeb.Startup))]
namespace Spikes.OAuthWebAPI
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);

            HttpConfiguration config = new HttpConfiguration();

            WebApiConfig.Register(config);
            app.UseWebApi(config);

            var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
            jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }

        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
                },
            });
        }
    }
}

Update WebApiConfig Class

Open the class WebApiConfig located in the App_Start folder and update with the folowing code.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        config.EnableCors();

        config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
        );

        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
    }
}

Add a Controller to Handle Requests

Create a new Folder named Controllers. Add a new Web API 2 Controller.

securewebapi_vs_5_controller_1

Give it a name.

securewebapi_vs_5_controller_2

Add the following code to it. The Authorize attribute will make sure that only authorized calls are allowed to access the controller. The GetSharePointAccessToken will get a new token to access SharePoint Online and get the sites’s web title.

[Authorize]
[RoutePrefix("api/v1/oauth")]
public class OAuthController : ApiController
{
    [HttpGet]
    [Route("getwebtitle")]
    public string GetWebTitle(string sharePointUrl)
    {
        string accessToken = GetSharePointAccessToken(sharePointUrl, this.Request.Headers.Authorization.Parameter);

        using (var context = TokenHelper.GetClientContextWithAccessToken(sharePointUrl, accessToken))
        {
            Web web = context.Web;
            context.Load(web);
            context.ExecuteQuery();
            return web.Title;
        }
    }

    internal static string GetSharePointAccessToken(string url, string accessToken)
    {
        string clientID = ConfigurationManager.AppSettings["ida:ClientID"];
        string clientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];

        var appCred = new ClientCredential(clientID, clientSecret);
        var authContext = new AuthenticationContext("https://login.windows.net/common");

        AuthenticationResult authResult = authContext.AcquireToken(new Uri(url).GetLeftPart(UriPartial.Authority), appCred, new UserAssertion(accessToken));
        return authResult.AccessToken;
    }
}

Configure your Azure web app

Log on to your Azure portal and go to the web application you have created earlier. First of all, we will make sure that this web app can only be accessed by athenticated users.

Authentication / Authorization

Open  your web app and go to Authentication / Authorizatio settings. Turn on App Service Authentication and select Azure AD as authentication provider.

securewebapi_ap_auth_1

Choose Express settings > Create New AD App. securewebapi_ap_auth_2

After you have clicked Ok, you should save your settings! An Azure AD Application will be created in the background.

securewebapi_ap_auth_3

CORS settings

To make sure we can access our web API, we also have to update the CORS settings. More information about CORS can be found here. I have allowed all origins here, but of course in real life, you also want to restrict that.

securewebapi_ap_cors

Configure your Azure AD Application

The Azure AD application has been created in the background for us, but still we need to update some settings and grab some information from it to make our solution work.

OAuth 2.0 Authorization Code Grant Flow

Your first step is to generate a client secret (key) in the configuration page of your Azure AD application. Be aware that once your client secret is generated, you will not be able to see it again – so store it safely once you get it. We will need this later.

securewebapi_ap_app_keys

OAuth 2.0 Implicit Grant Flow

If you’re building a SPA web app you may be in need of the implicit grant flow, as you’ll perform most of your computing on the client-side. Before you can use the implicit grant flow with your Azure AD application – you will need to enable it in your Azure AD application. Go to the manifest of your application and update the setting oauth2AllowImplicitFlow from false to true.

securewebapi_ap_app_manifest

Reply Urls

We also need to update the Reply URLs. Add the url(s) from where you will call the web API. If the location is not listed here, you will get an error when trying to access it. You can also see this as some kind of security.

securewebapi_ap_app_urls

Permissions

Here we will set the permissions our application will need. If you want to act on behalf of a user, you should pick delegated permissions. In this example we will as the Office 365 SharePoint Online API.

securewebapi_ap_app_permissions_1

securewebapi_ap_app_permissions_2

Properties

Now everything is set up, we will need to grab some parameters. We will need those later add them to the web.config of our web application and to our javascript that will call the web API.

  • TenantId: you can find in the endpoints section. Take any of the endpoints urls. The guid in there corresponds to your tenant ID
  • ClientId: the Application ID, found in the properties page of your application
  • Audience: the App ID URI, found in the properties page of your application

securewebapi_ap_app_properties_1

securewebapi_ap_app_properties_2

Consent

One last step to start using our API, is provide and admin consent to our application. Update the following url with the parameters of your own application and paste it in your browser.

https://login.microsoftonline.com//oauth2/authorize?response_type=code&client_id=&redirect_uri=&prompt=admin_consent

Log in with your admin account. You will be redirected to a content page which provides you an overview of permissions you have granted to your application. Click Accept.

Note that every time, you make changes to the permissions, you need to regrant the application consent.

securewebapi_ap_app_consent

Configure & publish the web app

Now that everything is set up, we Open the web.config of the web app and add the following parameters:

securewebapi_vs_6_appsettings

We are ready to publish our web app to Azure!

Let’s test!

To test the web API, I have created a simple HTML page with a button that will call the web API. The result is printed to the console.

I have added a content editor web part to a SharePoint site in my tenant and referenced this html file.

<!DOCTYPE html>
<html>
<body>
<h1>OAuth Web API test page</h1>
<input type="button" id="button" name="button" value="Click me" />

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js"></script>
<script type="text/javascript">

  $("#button").click(function(){
    var webTitle = GetTitle();
  });

  "use strict";

  window.config = {
	instance: 'https://login.microsoftonline.com/',
	tenant: '' // Update with your tenant ID,
    clientId: '' // Update with your application ID,
    postLogoutRedirectUri: window.location.origin,
    endpoints: {
       endPointApiUri: 'https://spikesoauthwebapi.azurewebsites.net' // Update with your azure web url
    },
    cacheLocation: 'localStorage' // enable this for IE, as sessionStorage does not work for localhost.
  };

  function GetTitle() {
	var authContext = new AuthenticationContext(config);
    // Check For & Handle Redirect From AAD After Login
    var isCallback = authContext.isCallback(window.location.hash);
    authContext.handleWindowCallback();
    if (isCallback && !authContext.getLoginError()) {
      window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
    }
    // If not logged in force login
    var user = authContext.getCachedUser();
    if (user) {
      // Logged in already
    }
    else {
      // NOTE: you may want to render the page for anonymous users and render
      // a login button which runs the login function upon click.
      authContext.login();
    }

    authContext.acquireToken(authContext.config.clientId, function (error, token) {
  	if (error || !token) {
  		console.log("ADAL error occurred: " + error);
  		return;
  	}
  	console.log("ADAL token: " + token);
  	var uri = config.endpoints.endPointApiUri +  "/api/v1/oauth/getwebtitle";	

  	$.ajax({
  		type: "GET",
  		url: uri,
  		data: {
  			"sharePointUrl": "" // Update with a SharePoint Url in your tenant
  		},
  		headers: {
  			'Accept': 'application/json',
  			"Authorization": "Bearer " + token
  		}
  	}).done(function (response) {
  	  console.log("Success.");
	  console.log(response);
	  return response;
  	}).fail(function () {
  		console.log("Failed.");
  	});
    });
  }

</script>

</body>
</html>

Check the browser console – you will find the token and the name of the site!

securewebapi_test_1

Now what will happen if we open the same html file from a location that is not defined in our Azure AD application? Outside the SharePoint context?

You will be redirected to the Microsoft online sign in page where you are asked for your credentials. So far, so good.

securewebapi_test_2a

But you will get an error message because the reply address is not a valid URL..

securewebapi_test_2

Sources:

The following source code on PnP was very big help while testing Azure Active Directory Authentication and writing this blog: https://github.com/SharePoint/PnP/tree/master/Samples/AzureAD.WebAPI.SPOnline

Koen Koninckx

Koen Koninckx

O365 Developer @ Spikes

Advertisements

3 responses to “Securing your Web API

  1. Pingback: Walkthrough: Building a Custom Web API for use with SharePoint Online – Rob Windsor's Weblog·

  2. Really trttwsoruhy blog. Please keep updating with great posts like this one. I have booked marked your site and am about to email it to a few friends of mine that I know would enjoy reading..

    Like

  3. Pingback: Securing an Angular application with Azure AD | Spikes Apps·

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s