Time Cockpit OpenID Connect Hybrid Flow

Monday, November 28, 2016 by Rainer Stropek

Time cockpit has been using OpenID Connect based on IdentityServer for more than two years (see also original announcement). Until recently, all our own client applications and client applications that our customers wrote (example) used the implicit flow. A few weeks ago, a customer approached us who wanted to use hybrid flow. This month, we made the necessary changes to time cockpit to support this authentication scenario.

In this blog post we describe how you can use OpenID Connect hybrid flow for time cockpit in your own applications (e.g. custom mobile app, custom website with ASP.NET MVC, interface programming, etc.). Additionally, we include a .NET Core sample.

Registering Your Application

At the time of writing, time cockpit does not include a self-service portal to create client applications. If you want to write your own application using time cockpit for authentication, please contact us. Please provide the following information:

  1. Email address of your time cockpit account (note that you have to be time cockpit administrator to request a client application).
  2. Name of your client application
  3. Redirect URIs
  4. Requested flow (e.g. implicit, hyrid, etc.)
  5. Optional information:
    1. URL of your web application
    2. Logo URL

Hybrid Flow Step-by-Step

Get URLs From Provider Configuration

You can get time cockpit's OpenID Connect Provider Configuration at https://auth.timecockpit.com/core/.well-known/openid-configuration. From the provider configuration you will get the URLs for the authorization and the token endpoints.

Authentication Request

First, you have to execute an authentication request at the authorization endpoint. You can find detailed information about this request in the OpenID Connect specification. The authentication request will trigger a series of HTTP requests during which the user can login.

GET https://auth.timecockpit.com/core/connect/authorize?
  client_id=86c34ba3-0000-0000-0000-000000000000&
  redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fsignin-oidc&
  response_type=code%20id_token&
  scope=openid%20profile%20email%20offline_access&
  response_mode=form_post&
  nonce=636...&
  state=CfDJ8DDHy... 

Authentication Response

After authentication was successful, your application will receive the authentication response. You can find detailed information about this response in the OpenID Connect specification.

POST http://localhost:5000/signin-oidc 
  Content-Type: application/x-www-form-urlencoded
  code=b1595...&
  id_token=eyJ0eXAiO...&
  scope=openid+profile+email+offline_access&
  state=CfDJ8DDH...&
  session_state=1sKYWTRZ...

Tip: The JWT debugger at https://jwt.io/ is very useful when developing with OpenID Connect.

Get Tokens

Once you got the authorization code as shown in the previous chapter, you can use it to get access and refresh tokens from the token endpoint. You can find detailed information about token requests in the OpenID Connect specification.

POST https://auth.timecockpit.com/core/connect/token
Content-Type: application/x-www-form-urlencoded
  client_id=86c34ba3-0000-0000-0000-000000000000&
  client_secret=...&
  code=b15952e3...&
  grant_type=authorization_code&
  redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fsignin-oidc

In case of success, the result will look something like this:

HTTP/1.1 200 OK
Content-Type: application/json; {
  "id_token":"eyJ0e...",
  "access_token":"eyJ0eXAiOi...",
  "expires_in":3600,
  "token_type":"Bearer",
  "refresh_token":"9e624..."
}

Now you can use the access token for web API requests and the refresh token to refresh your tokens.

.NET Core Example

Many of our customers nowadays use ASP.NET Core to develop web applications. Therefore, I created a minimalist ASP.NET Core example that uses the hybrid flow with time cockpits OpenID Connect implementation.

Note that this sample does not include production-ready code. The code has been reduced as much as possible so that it focuses only on the core aspects relevant for this blog post.

Project Configuration

This is how your project.json file has to look like:

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.1",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0",
    "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Newtonsoft.Json": "9.0.1"
  },
  "frameworks": {
    "netcoreapp1.0": {}
  },
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  }
}

Main Program

The Program.cs file is pretty basic:

using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseUrls("http://*:5000")
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}

Sample Code

The interesting code can be found in Startup.cs:

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;

namespace WebApplication
{
    public class Startup
    {
        // Get client id and secret from time cockpit by requesting it in a
        // support ticket. You can contact us through https://www.timecockpit.com/help-support/contact-us.
        // Note that you should NEVER put that data in source code in real life. This is only 
        // allowed in samples like this, never in productive code!
        private const string CLIENT_ID = "86c34ba3-0000-0000-0000-000000000000";
        private const string CLIENT_SECRET = "....";

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            });
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            // Obviously it would NOT be a good idea to save tokens like this in memory.
            // In practice, you have to store them in a secure location. However, the focus
            // of this sample is not token handling. So we keep it simple. 
            string accessToken = null, refreshToken = null, idToken = null;

            var options = new OpenIdConnectOptions
            {
                ClientId = CLIENT_ID,
                ClientSecret = CLIENT_SECRET,
                Authority = "https://auth.timecockpit.com/core/",
                ResponseType = "code id_token",
                Events = new OpenIdConnectEvents
                {
                    OnTokenResponseReceived = (context) =>
                    {
                        // Get tokens so that we can use them later for accessing time cockpit's
                        // RESTful web API (OData).
                        accessToken = context.TokenEndpointResponse?.AccessToken;
                        refreshToken = context.TokenEndpointResponse?.RefreshToken;
                        idToken = context.TokenEndpointResponse?.IdToken;
                        return Task.CompletedTask;
                    }
                }
            };
            options.Scope.Add("openid");
            options.Scope.Add("email");
            options.Scope.Add("offline_access");
            app.UseOpenIdConnectAuthentication(options);

            app.Run(async context =>
            {
                var user = context.User;
                if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
                {
                    // User isn't authenticated -> trigger login (similar to [Authorize] in ASP.NET MVC)
                    await context.Authentication.ChallengeAsync();
                }
                else
                {
                    // User is authenticated

                    await context.Response.WriteAsync("<html><body><h1>OpenID Connect Demo</h1>");

                    #region Display tokens
                    await context.Response.WriteAsync("<h2>Tokens</h2><ul>");
                    await context.Response.WriteAsync(${body}quot;<li>Access Token: {accessToken ?? "MISSING"}</li>");
                    await context.Response.WriteAsync(${body}quot;<li>Refresh Token: {refreshToken ?? "MISSING"}</li>");
                    await context.Response.WriteAsync(${body}quot;<li>ID Token: {idToken ?? "MISSING"}</li>");
                    await context.Response.WriteAsync("</ul>");
                    #endregion

                    #region Display claims
                    await context.Response.WriteAsync("<h2>Claims</h2><ul>");
                    foreach (var claim in user.Claims)
                    {
                        await context.Response.WriteAsync(${body}quot;<li>{claim.Type}: {claim.Value}</li>");
                    }

                    await context.Response.WriteAsync("</ul>");
                    #endregion

                    #region Demonstrate a web api request
                    await context.Response.WriteAsync("<h2>Countries</h2><ul>");
                    using (var client = new HttpClient())
                    {
                        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                        var response = await client.GetAsync("https://web.timecockpit.com/odata/APP_Country");
                        dynamic result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
                        foreach (dynamic country in result.value)
                        {
                            await context.Response.WriteAsync(${body}quot;<li>{country.APP_IsoCode} {country.APP_CountryName}</li>");
                        }
                    }
                    await context.Response.WriteAsync("</ul>");
                    #endregion

                    #region Demonstrate token refresh
                    // For details about refreshing tokens with OpenID Connect 
                    // see http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
                    await context.Response.WriteAsync("<h2>Refreshing Tokens</h2><ul>");
                    using (var client = new HttpClient())
                    {
                        var response = await client.PostAsync(
                            "https://auth.timecockpit.com/core/connect/token",
                            new FormUrlEncodedContent(new Dictionary<string, string>
                            {
                                ["client_id"] = CLIENT_ID,
                                ["client_secret"] = CLIENT_SECRET,
                                ["grant_type"] = "refresh_token",
                                ["refresh_token"] = refreshToken,
                                ["scope"] = "openid"
                            }));
                        dynamic result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
                        accessToken = result.access_token;
                        refreshToken = result.refresh_token;
                        await context.Response.WriteAsync(${body}quot;<li>New Access Token: {accessToken ?? "MISSING"}</li>");
                        await context.Response.WriteAsync(${body}quot;<li>New Refresh Token: {refreshToken ?? "MISSING"}</li>");
                    }

                    await context.Response.WriteAsync("</ul>");
                    #endregion

                    await context.Response.WriteAsync("</html>");
                }
            });
        }
    }
}

Feedback

Have fun programming with time cockpit. If you have feedback or questions, don't hesitate to contact us.

comments powered by Disqus

Rainer Stropek

Rainer Stropek

Co-founder, architect, developer

Bio

I am co-founder and CEO of the company software architects and have been serving this role since 2008. At software architects my team and I are developing the award-winning SaaS solution time cockpit. Previously, I founded and led IT consulting firms that worked in the area of developing software solutions based on the Microsoft technology stack.

In my work I focus on .NET development and software architecture. I have written some books and articles on C#, database development, Windows Azure, Windows 8 development, WPF, and Silverlight. Regularly I speak at conferences, do workshops and conduct trainings in Europe and the US. Since 2010 I have been MVP for Windows Azure.

I graduated the Higher Technical School Leonding (AT) for MIS with honors and hold a BSc (Hons) Computer Studies of the University of Derby (UK).

Contact

Twitter: @rstropek
Facebook
Google+
Xing
LinkedIn

Authors