Web API GitHub OAuth2 Code Flow

This article demonstrates how to setup an OAuth2 code flow example using GitHub as an authorization server and a Web API service as a resource server. Thanks to Jerrie Pelser and Thinktecture for providing code and blogs which made it easy to research this and setup a working example.

Code: https://github.com/damienbod/WebAppGitHubCodeFlow

Setting up GitHub OAuth2

Login to GitHub and click the account settings button:

oauth2Github01

Clieck the Applications menu:
oauth2Github02

Now you should be at https://github.com/settings/applications

Click the Register new application button. Fill in the textboxes as required. The URL in the Authorization callback URL will be used in the Code Flow client application as well as the Client ID and the Client Secret.

oauth2Github03

The Client ID and Client Secret will be used to authorize your application. The Client secret should remain a secret… These will be used in the Code Flow client application.
oauth2Github04

The OAuth2 Code Flow Client

The Code Flow OAuth2 client project is a simple MVC application. The application uses the Client Secret and Client ID to login to the GitHub OAuth2 server. The GitHub returns an OAuth2 access code. The client application uses this code the get the access token and the access token is then used to get the secure data. NOTE: The access token never leaves the server. Only the access code is returned to the client. This is more secure than the OAuth2 Implicit Flow.

When the client is started, the OAuth2 login URL is created using the Thinktecture.IdentityModel.Client. This is not required, but saves you doing it all yourself. The ClientId, ClientSecret, and the RedirectUrl must match what you configured on the GitHub server. If not, your application will not work. Also, all URLs MUST use HTTPS, otherwise, this authorization, authentication is worthless.

  • ClientId = Client ID
  • ClientSecret = Client Secret
  • RedirectUrl = Authorization callback URL
private const string OAuth2AuthorizeEndpoint = "https://github.com/login/oauth/authorize";
private const string OAuth2TokenEndpoint = "https://github.com/login/oauth/access_token";
private const string ClientId = "192b91f57fde31898794";
private const string ClientSecret = "03359f5248df067a6dbc504a3f33c7f8bab10df2";
private const string RedirectUrl = "http://localhost:60703/DemoOath2CodeFlowMvc/callback";
private const string Scopes = "user";
// should be a new random string every time the method is called.
private string State = "29385860478549569433784333"; 

public string CreateCodeFlowUrl()
{
  var client = new OAuth2Client(new Uri(OAuth2AuthorizeEndpoint));
  return client.CreateCodeFlowUrl(ClientId, Scopes, RedirectUrl, State);           
}

When you start the application, click the OAuth2 Code Flow button. This starts the authorization process.
oauth2Github05

You are redirected to GitHub. This shows you what permissions the application wants to use from your profile. Sometimes applications require to many rights, so you should be careful when you login using external providers. Do you really want to application to have full access to your github user…
oauth2Github06

After a successful login, you end up back at the callback page in your application.
oauth2Github07

The client can now use this access code to get the secure data:

 @using (Html.BeginForm("SecureData", "DemoOath2CodeFlowMvc"))
{
 <input type="hidden" id="code" name="code" value="@ViewBag.Code" />
 <input type="submit" value="Get secure data now" />
}

The MVC controller uses the code to get an access token. Then it gets the payload using the access token. The access token never leaves the server layer. It is sent between the client server, the resource server and the GitHub provider.

public async Task<ActionResult> SecureData(string code)
{
 TokenData content = await _gitHubCodeFlowSecurity.GetToken(code);
 string data = await GetResourceSecureData(content.Access_Token, 4);
 ViewBag.SecureData = data;
 return View();
}

This is the method used to get the token from the GitHub server using the access code.

 private const string OAuth2AuthorizeEndpoint = "https://github.com/login/oauth/authorize";
 private const string OAuth2TokenEndpoint = "https://github.com/login/oauth/access_token";
 private const string ClientId = "192b91f57fde31898794";
 private const string ClientSecret = "d1cae5b35559501b391341e111d1cfefec9089d2";
 private const string RedirectUrl = "http://localhost:60703/DemoOath2CodeFlowMvc/callback";
 private const string Scopes = "user";
 // should be a new random string every time the method is called.
 private string State = "29385860478549569433784333";

public async Task<TokenData> GetToken(string code)
{
  var client = new HttpClient();
  string parameters = "client_id=" + HttpUtility.UrlEncode(ClientId)
    + "&redirect_uri=" + HttpUtility.UrlEncode(RedirectUrl) + "&client_secret=" + HttpUtility.UrlEncode(ClientSecret)
    + "&code=" + HttpUtility.UrlEncode(code) + "&state=" + State;

  var uri = new Uri(OAuth2TokenEndpoint + "?" + parameters);
  client.DefaultRequestHeaders.Accept.Clear();
  client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

  HttpResponseMessage response = await client.PostAsync(uri, new StringContent(parameters));
  var tokenData = await response.Content.ReadAsAsync<TokenData>();
  return tokenData;
}

Once the server has a valid access token, it can now be used to get the secure data. This method calls the http://localhost:50182/api/ResourceServerWebApi/{id} resource. This must also be HTTPS in a real application. When the resource server is setup correctly, it will return a payload, when the correct token is supplied.

private async Task<string> GetResourceSecureData(string token, int id)
{
  string secureData = "";
  var client = new HttpClient();
  client.SetBearerToken(token);

  client.BaseAddress = new Uri("http://localhost:50182/api/ResourceServerWebApi/" + id);
  client.DefaultRequestHeaders.Accept.Clear();
  client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

  HttpResponseMessage response = await client.GetAsync("");
  if (response.IsSuccessStatusCode)
  {
   secureData = await response.Content.ReadAsAsync<string>();
  }
  else
  {
    return response.ReasonPhrase;
  }
    return secureData;
}

The Web API Resource Server

The resource server is a Web API service which returns the payload. The server trusts the OAuth2 server and the client server. When the server receives a HTTP request, the access token is validated and authorization/authentication is or can be checked. An implementation of an OWIN middleware is used to validate the token. The OWIN middleware requires only the AuthenticateCoreAsync method. See this link from Brockallen for more information on OWIN Authentication Middleware.

This Resource Server creates its own owin middleware called AuthenticationMiddlewareForGitHubOAuth2. This will be used to validate github access tokens.

using System.Net.Http;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security.Infrastructure;
using Owin;

namespace WebAppResourceServer.OwinMiddleware
{
    public class AuthenticationMiddlewareForGitHubOAuth2 : AuthenticationMiddleware<GitHubAuthenticationOptions>
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger _logger;

        public AuthenticationMiddlewareForGitHubOAuth2(Microsoft.Owin.OwinMiddleware next, IAppBuilder app, GitHubAuthenticationOptions options) : base(next, options)
        {
            if (Options.Provider == null)
            {
                Options.Provider = new AuthenticationProviderForGitHubOAuth2();
            }

            _logger = app.CreateLogger<AuthenticationMiddlewareForGitHubOAuth2>();

            _httpClient = new HttpClient(new WebRequestHandler());
            _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Owin GitHub middleware to validate token");
            _httpClient.DefaultRequestHeaders.ExpectContinue = false;
        }

        protected override AuthenticationHandler<GitHubAuthenticationOptions> CreateHandler()
        {
            return  new AuthenticationHandlerForGitHubTokenValidation(_httpClient, _logger);
        }
    }
}

To validate the token an AuthenticationHandler is required which implements the AuthenticateCoreAsync method. This class was built using this example. Thanks to Jerrie Pelser for providing it.

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using Newtonsoft.Json.Linq;

namespace WebAppResourceServer.OwinMiddleware
{
    public class AuthenticationHandlerForGitHubTokenValidation : AuthenticationHandler<GitHubAuthenticationOptions>
    {
        private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";

        private readonly ILogger _logger;
        private readonly HttpClient _httpClient;

        public AuthenticationHandlerForGitHubTokenValidation(HttpClient httpClient, ILogger logger)
        {
            _httpClient = httpClient;
            _logger = logger;
        }

        protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
        {
            try
            {
                // Get the token from the header
                var tokenHeader = Request.Headers.Get("Authorization");
                var token = tokenHeader.Replace("Bearer ", "");
                if (string.IsNullOrEmpty(token))
                {
                    return null;
                }

                // Get the GitHub user
                var userDataRequestUsingAccessToken = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user?access_token=" + Uri.EscapeDataString(token));
                userDataRequestUsingAccessToken.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                HttpResponseMessage userResponse = await _httpClient.SendAsync(userDataRequestUsingAccessToken, Request.CallCancelled);
                userResponse.EnsureSuccessStatusCode();
                var text = await userResponse.Content.ReadAsStringAsync();
                JObject user = JObject.Parse(text);

                var context = new GitHubAuthenticatedContext(Context, user, token)
                {
                    Identity = new ClaimsIdentity(
                        Options.AuthenticationType,
                        ClaimsIdentity.DefaultNameClaimType,
                        ClaimsIdentity.DefaultRoleClaimType)
                };

                if (!string.IsNullOrEmpty(context.Id))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType));
                }
                if (!string.IsNullOrEmpty(context.UserName))
                {
                    context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, XmlSchemaString, Options.AuthenticationType));
                }
                if (!string.IsNullOrEmpty(context.Name))
                {
                    context.Identity.AddClaim(new Claim("urn:github:name", context.Name, XmlSchemaString, Options.AuthenticationType));
                }
                if (!string.IsNullOrEmpty(context.Link))
                {
                    context.Identity.AddClaim(new Claim("urn:github:url", context.Link, XmlSchemaString, Options.AuthenticationType));
                }

                await Options.Provider.Authenticated(context);

                return new AuthenticationTicket(context.Identity, context.Properties);
            }
            catch (Exception ex)
            {
                _logger.WriteError(ex.Message);
            }
            return new AuthenticationTicket(null, null);
        }

    }
}

This method calls the GitHub api user service to get the user details from the access token. If valid, an AuthenticationTicket is created. The user/roles of the claims could also be checked if required. See this link for for other examples of OWIN Middleware. These work well, if your MVC application is the client and the resource server. Thinktecture also provide good OAuth2 OWIN Middleware components.

The OWIN Middleware is added to the OWIN Startup class.

using Microsoft.Owin;
using Owin;
using WebAppResourceServer;
using WebAppResourceServer.OwinMiddleware;

[assembly: OwinStartup(typeof(Startup))]

namespace WebAppResourceServer
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(typeof(AuthenticationMiddlewareForGitHubOAuth2), app, new GitHubAuthenticationOptions());
            app.UseWebApi(WebApiConfig.Register());
        }
    }
}

Conclusions

It is not so complicated to create an OAuth2 workflow for a Web API resource. The biggest problem is that each global provider does it different and has to be implemented differently. The OWIN middleware is flexible but requires a few classes to setup anything basic. HTTPS is not forced anywhere and as the tokens and codes are usually sent without any encryption, it is not very safe, if not done correctly… To be more secure, the OAuth2 guidelines require more help, for example OpenId. Thinktecture provide a good solution for OAuth2. Looking forward to using IdentityServer V3 when its completed.

Links:

http://www.beabigrockstar.com/guides/aspnet-mvc-github-login/

http://www.beabigrockstar.com/blog/owin-oauth-provider-github/

https://developer.github.com/v3/oauth/

http://brockallen.com/2013/08/07/owin-authentication-middleware-architecture/

http://lbadri.wordpress.com/2013/08/03/owin-authentication-middleware-for-hawk-in-thinktecture-identitymodel-45/

http://tools.ietf.org/id/draft-ietf-oauth-v2-31.html

One comment

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 )

Facebook photo

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

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: