Pages

Monday, May 8, 2017

IdentityServer3 with PKCE Part 2 - Protected Resource Server

This series simulates a native application accessing a protected Web API resource, using OAuth2 via IdentityServer3. It demonstrates using Proof Key for Code Exchange (PKCE), and is in four parts:

  1. Build a simple authorization server, consumed by native application.
  2. Build a protected resource.
  3. Persist server configuration to database.
  4. Persist user data to database using Microsoft.Identity and SQL Server.

Important
This series does not create an OpenID Connect (OIDC) server. It is OAuth-only, since the PKCE specification doesn't require OIDC.

Where We're At

We can get a token, but there's nothing to use it on. We've configured IdentityServer with information about our client application (PretendMobile), and we used that information (Client ID, etc) to get a token.

Now we'll add a Web API with a protected method that just returns some claim data. It will be configured to:

  1. Accept the access token our pretend mobile app gets from the authorization server.
  2. Validate that token against the authorization server.

We'll use the same project as the authorization server, but it's important to know that the Web API could be in a completely separate project.

Web API

References
Simplest OAuth Server
Simplest OAuth2 Walkthrough Code

Install the packages for Web API, and being able to accept the IdentityServer tokens. Note IdentityModel version, which is latest for .Net 4.6 (2.x is for .Net Core).

Install-Package Microsoft.AspNet.WebApi.Client -Version 5.2.3 -Project AuthAndApi
Install-Package Microsoft.AspNet.WebApi.Owin -Version 5.2.3 -Project AuthAndApi
Install-Package IdentityModel -Version 1.9.2 -Project AuthAndApi
Install-Package IdentityServer3.AccessTokenValidation -Version 2.15.0 -Project AuthAndApi

Add two things to the OWIN pipeline: accepting access tokens, and using WebApi.

Updated Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
//Added
using IdentityServer3.Core.Configuration;
using Owin;
using AuthAndApi.IdOptions;
using AuthAndApi.IdUsers;
using System.Web.Http;
using IdentityServer3.AccessTokenValidation;

namespace AuthAndApi
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            //Configure IdentityServer for issuing authentication tokens
            var options = new IdentityServerOptions
            {
                Factory = new IdentityServerServiceFactory()
                .UseInMemoryClients(Clients.Get())
                .UseInMemoryScopes(Scopes.Get())
                .UseInMemoryUsers(Users.Get()),
                //SSL MUST be used in production
#if DEBUG
                RequireSsl = false
#endif
            };
            app.UseIdentityServer(options);

            // Configure Web API to accept access tokens from IdentityServer, and require a scope of 'email'
            app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
            {
                //In this case, the authorization server is also the Web API server
                Authority = "http://localhost:27230",
                ValidationMode = ValidationMode.ValidationEndpoint,
                RequiredScopes = new[] { "email" }
            });

            //Configure Web API
            var config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();
            app.UseWebApi(config);
        }
    }
}

Add a Controllers folder, and a controller named Test.

2017-05-06_1140242017-05-06_1141442017-05-06_114210

Create an API method that requires authorization. The method checks for evidence a user authorized (and not just the application).

Note
Code shamelessly stolen from the IdentityServer samples

TestController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
//Added
using System.Security.Claims;

namespace AuthAndApi.Controllers
{
    [Route("test")]
    [Authorize]
    public class TestController : ApiController
    {
        public IHttpActionResult Get()
        {
            var user = User as ClaimsPrincipal;

            var subjectClaim = user.FindFirst("sub");
            if (subjectClaim != null)
            {
                return Json(new
                {
                    message = "OK user",
                    client = user.FindFirst("client_id").Value,
                    subject = subjectClaim.Value,
                    email = user.FindFirst("email").Value
                });
            }
            else
            {
                return Json(new
                {
                    message = "User claim not found for this client_id",
                    client = user.FindFirst("client_id").Value
                });
            }
        }
    }
}

The API server now has a protected resource. Let's prove it requires authorization. Run the application. The console will still get an access token, but we're not doing anything with it, yet. Leave the project running and manually enter this URL into a browser to try to access the protected resource.

http://localhost:27230/test

The server will return something like this:

<Error>
  <Message>Authorization has been denied for this request.</Message>
</Error>

Pretend Mobile App - Access Protected Resource

In the PretendMobile project, add and call a method that access the protected Web API resource. The token is added to the request in the Authorization header.

        static void Main(string[] args)
        {
            //get token
            var response = GetAccessToken();
            Console.WriteLine("Access Token: " + response.AccessToken);
            //use token
            CallProtectedApiResource(response.AccessToken);
            Console.ReadLine();
        }

        static void CallProtectedApiResource(string access_token)
        {
            var client = new HttpClient();
            client.SetBearerToken(access_token);
            Console.WriteLine(client.GetStringAsync("http://localhost:27230/test").Result);
        }

Run the solution. After authorizing, the application successfully gets the protected resource's data.

2017-05-06_121035

Wrap Up

We now have a fully functioning mobile application! Next up, persisting the IdentityServer configuration.

No comments:

Post a Comment