OpenID Connect Native Apps

In this article

    Example code on Github

    This tutorial describes how to use IdentityModel’s OpenID Connect (OIDC) client library to authenticate towards SuperOffice SuperID using the native app workflow. It demonstrates how to set the required OpenId Connect options using settings you receive after registering a SuperOffice Online custom or standard app.

    Getting Started

    The only prerequisite is that you must have an application registered as a Desktop or Mobile application in SuperOffice Online, and a tenant with a user for testing login. You must have the ApplicationId and ApplicationToken ready to use as detailed in the settings below.

    In Visual Studio, create either a .NET Core or .NET Framework console application.

    In the Package Manager Console, install the IdentityModel OpenID Connect client using the following command:

    Install-Package IdentityModel.OidcClient
    

    The IdentityModel.OidcClient package has a fairly long list of dependencies that will also be installed and listed in the package.config file.

    With the .NET Framework sample, it’s important to note the possibility that some of the packages do not update assembly references in the app.config file. In my case, at the time of this writing, several of the assemblies were listed in the assemblyBinding element and needed to be updated from version 4.1.2.0 to 4.3.0 (installed with the packages).

    If you make any changes, i.e. install a new package, etc, and run the application only to observe the following exception, you may have to examine the app.config file and update the assemblyBindings accordingly.

    Exception.PNG

    As of this writing, I had to update the dependent versions from 4.1.2.0 to 4.0.3.

    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
          <dependentAssembly>
            <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
          </dependentAssembly>
          <dependentAssembly>
            <assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.3.0" />
          </dependentAssembly>
          <dependentAssembly>
            <assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.2.0.0" newVersion="4.3.0" />
          </dependentAssembly>
          <dependentAssembly>
            <assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.3.0" />
          </dependentAssembly>
          <dependentAssembly>
            <assemblyIdentity name="System.Linq.Expressions" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.3.0" />
          </dependentAssembly>
          <dependentAssembly>
            <assemblyIdentity name="System.Linq" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.3.0" />
          </dependentAssembly>
          <dependentAssembly>
            <assemblyIdentity name="System.Runtime.Extensions" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
            <bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.3.0" />
          </dependentAssembly>
        </assemblyBinding>
      </runtime>
    

    Program Code

    This example uses demonstrates the concepts in a .NET framework console application, and is almost identical to the IdentityModel github project page. There are a couple extra lines of code to be aware of for SuperOffice SuperID, after setting the client options.

    With regards to the OAuth2 native app flow, the application must establish a listener, preferrably on the loopback IP address, and waits for the response in the default browser. While localhost can be used, it is NOT recommended. Here is a quote from the specification.

    ...the use of localhost is NOT RECOMMENDED.
    Specifying a redirect URI with the loopback IP literal
    rather than localhost avoids inadvertently listening on
    network interfaces other than the loopback interface.
    It is also less susceptible to client-side firewalls
    and misconfigured host name resolution on the user's device.
    

    Here I take the advice from the specification and use the loopback address followed by a randomly chosen port number for the callback. The redirect url used here does include an additional route, and is defined in SuperOffice Operations Center (OC) as well.

    While on the subject, understand that all Windows and mobile apps defined in OC must adhere to the specification. When registered, your application can define any redirect URL, but it must start with "http://127.0.0.1:{port}/{path}" for IPv4, and "http://[::1]:{port}/{path}" for IPv6.

    Internally, OC uses regular expressions to support any port address, so the application redirect URL for this example application is IPv4 and defined as “^http://127.0.0.1\:\d{4,10}/desktop-callback$”. You are free to use any path text instead of desktop-callback.

    // create a redirect URI using an available port on the loopback address.
    string redirectUri = string.Format("http://127.0.0.1:7890/desktop-callback/");
    
    // create an HttpListener to listen for requests on that redirect URI.
    var http = new HttpListener();
    http.Prefixes.Add(redirectUri);
    http.Start();
    

    Prior to invoking the default browser to begin the login procedure, set the OIDC client options. The Authority property needs to be set to the SuperOffice SuperID login URL.

    While some OIDC implementations implement the optional UserInfo endpoint that clients call to obtain and populate a users claims, SuperOffice populates all user profile data in the id_token claims, and therefore we set to LoadProfile property to false.

    After registering a SuperOffice Online app, you receive an ApplicationID and an Application Token. These values are the OAuth2 ClientID and ClientSecret, respectively.

    While SuperOffice only requires the openid scope, as required by the OIDC specification, more scopes can be included but will be ignored as of this writing.

    The Redirect option property must match the Redirect URL where the listener is listening for the authentication response.

    var options = new OidcClientOptions
    {
        Authority = "https://sod.superoffice.com/login",
        LoadProfile = false,
        ClientId = "YOUR_APPLICATION_ID",
        ClientSecret = "YOUR_APPLICATION_TOKEN",
        Scope = "openid profile api",
        RedirectUri = "http://127.0.0.1:7890/desktop-callback",
        ResponseMode = OidcClientOptions.AuthorizeResponseMode.FormPost,
        Flow = OidcClientOptions.AuthenticationFlow.Hybrid,
    };
    

    To ensure a smooth experience, set the following Policy properties that:

    1. Validates the issuer name.
    2. Requires the response includes a token hash.

    This must be done after the options are instantiated because the Policy property is only created in the construction of the options class itself, and therefore can not be included in the initializer along with the other properties.

    options.Policy.Discovery.ValidateIssuerName = false;
    options.Policy.RequireAccessTokenHash = false;
    

    Instantiate the OIDC client, passing in the client options, and then call PrepareLoginAsync method to validate the configuration options and set the nonce and state.

    var client = new OidcClient(options);
    var state = await client.PrepareLoginAsync();
    

    Nearing the end, it’s time to start the default browser process and initiate the authentication process.

    The application waits until a response is received, and then ensures there is something returned before reading the response body. The body should be the JWT token.

     // open system browser to start authentication
    Process.Start(state.StartUrl);
    
    // wait for the authorization response.
    var context = await http.GetContextAsync();
    
    // get the request body
    var formData = string.Empty;
    if (context.Request.HasEntityBody)
    {
        using (var body = context.Request.InputStream)
        {
            using (var reader = new System.IO.StreamReader(
                body,
                context.Request.ContentEncoding))
            {
                formData = reader.ReadToEnd();
            }
        }
    }
    

    Instead of just leaving the browser hanging, go ahead and write something useful to it prior to processing the JWT. In this example, I give a warning message that the browser is about to re-direct to the community site, and set the refresh properties to do so after 5 seconds.

    // sends an HTTP response to the browser.
    var response = context.Response;
    
    // create HTML to send to the browser
    string responseString =
    @"<html>
        <head>
            <meta http-equiv='refresh'
                  content='5;url=https://community.superoffice.com'>
        </head>
        <body>
            <h1>Redirecting you to the SuperOffice Community...</h1>
        </body>
    </html>";
    
    // convert the markup to byte[] format
    var buffer = Encoding.UTF8.GetByte(responseString);
    response.ContentLength64 = buffer.Length;
    
    // get the response output stream to write to
    var responseOutput = response.OutputStream;
    
    // write the HTML to the output stream
    // and then close the stream.
    await responseOutput.WriteAsync(buffer, 0,buffer.Length);
    responseOutput.Close();
    

    Finally we can process the result but passing the formData result into the client’s ProcessResponseAsync method with the state.

    var result = await client.ProcessResponseAsync(formData, state);
    

    The result is this method is a LoginResult, which contains all of the details you might expect, i.e. the AccessToken and RefreshToken. Other libraries may call their login result container something differently, but they should all contain the key elements.

    namespace IdentityModel.OidcClient
    {
        public class LoginResult : Result
        {
            public virtual ClaimsPrincipal User { get; internal set; }
    
            public virtual string AccessToken { get; internal set; }
    
            public virtual string IdentityToken { get; internal set; }
    
            public virtual string RefreshToken { get; internal set; }
    
            public virtual DateTime AccessTokenExpiration { get; internal set; }
    
            public virtual DateTime AuthenticationTime { get; internal set; }
    
            public virtual HttpMessageHandler RefreshTokenHandler { get; internal set; }
        }
    }
    

    From this point on it’s completely up to the application to decide what to do with the information provided, but it’s only logical to assume the application will want to access the users claims to obtain the tenants SOAP or REST web service endpoints.

    if (!result.IsError)
    {
       // iterate over the list of claims
        foreach (var claim in result.User.Claims)
        {
            Console.WriteLine("{0}: {1}", claim.Type, claim.Value);
        }
    
        // write out the access token
        Console.WriteLine("Access token:\n{0}", result.AccessToken);
    
        // if present, write out the refresh token
        if (!string.IsNullOrWhiteSpace(result.RefreshToken))
        {
            Console.WriteLine("Refresh token:\n{0}", result.RefreshToken);
        }
    
        // get the base NetServer SOAP Endpoint
        string soapUrl = result.User.Claims.Where(c => c.Type.Contains("netserver_url")).Select(n => n.Value).FirstOrDefault();
    
        // get the base NetServer REST Endpoint
        string restUrl = result.User.Claims.Where(c => c.Type.Contains("webapi_url")).Select(n => n.Value).FirstOrDefault();
    }
    else // write out the authentication error
    {
        Console.WriteLine("\n\nError:\n{0}", result.Error);
    }
    

    Conclusion

    The main benefit of using OAuth2 Native App flow on Windows and mobile devices is having that single sign-on experience. When the browser is opened navigates to SuperOffice, SuperID can access the clients SuperOffice cookie. When SuperID verifies the user is already authenticated, the user will bybass re-entering credentials and will simply be redirected back to the client and have a great single sign-on experience.