Tag Archives: Authentication

Using the OAuth 2.0 device flow to authenticate users in desktop apps

Over the last few years, OpenID Connect has become one of the most common ways to authenticate users in a web application. But if you want to use it in a desktop application, it can be a little awkward…

Authorization code flow

OpenID Connect is an authentication layer built on top of OAuth 2.0, which means that you have to use one of the OAuth 2.0 authorization flows. A few years ago, there were basically two possible flows that you could use in a desktop client application to authenticate a user:

The password flow is pretty easy to use (basically, just exchange the user’s login and password for a token), but it requires that the client app is highly trusted, since it gets to manipulate the user’s credentials directly. This flow is now disallowed by OAuth 2.0 Security Best Current Practice.

The authorization code flow is a bit more complex, but has the advantage that the client application never sees the user’s password. The problem is that it requires web navigation with a redirection to the client application, which isn’t very practical in a desktop app. There are ways to achieve this, but none of them is perfect. Here are two common ones:

  • Open the authorization page in a WebView, and intercept the navigation to the redirect URI to get the authorization code. Not great, because the app could get the credentials from the WebView (at least on some platforms), and it requires that the WebView supports intercepting the navigation (probably not possible on some platforms).
  • Open the authorization page in the default web browser, and use an application protocol (e.g. myapp://auth) associated with the client application for the redirect URI. Unfortunately, a recent Chrome update made this approach impractical, because it always prompts the user to open the URL in the client application.

In addition, in order to protect against certain attack vectors, it’s recommended to use the PKCE extension when using the authorization code grant, which contributes to make the implementation more complex.

Finally, many identity providers require that the client authenticates with its client secret when calling the token endpoint, even though it’s not required by the spec (it’s only required for confidential clients). This is problematic, since the client app will probably be installed on many machines, and is definitely not a confidential client. The user could easily extract the client secret, which is therefore no longer secret.

An easier way : device flow

Enter device flow (or, more formally, device authorization grant). Device flow is a relatively recent addition to OAuth 2.0 (the first draft was published in 2016), and was designed for connected devices that don’t have a browser or have limited user input capabilities. How would you authenticate on such a device if you don’t have a keyboard? Well, it’s easy: do it on another device! Basically, when you need to authenticate, the device will display a URL and a code (it could also display a QR code to avoid having to copy the URL), and start polling the identity provider to ask if authentication is complete. You navigate to the URL in the browser on your phone or computer, log in when prompted to, and enter the code. When you’re done, the next time the device polls the IdP, it will receive a token: the flow is complete. The Azure AD documentation has a nice sequence diagram that helps understand the flow.

When you think of it, this approach is quite simple, and more straightforward than the more widely used redirection-based flows (authorization code and implicit flow). But what does it have to do with desktop apps, you ask? Well, just because it was designed for input constrained devices doesn’t mean you can’t use it on a full-fledged computer. As discussed earlier, the redirection-based flows are impractical to use in non-web applications; the device flow doesn’t have this problem.

In practice, the client application can directly open the authentication page in the browser, with the code as a query parameter, so the user doesn’t need to copy them. The user just needs to sign in with the IdP, give their consent for the application, and it’s done. Of course, if the user is already already signed in with the IdP and has already given their consent, the flow completes immediately.

The device flow is not very commonly used in desktop apps yet, but you can see it in action in the Azure CLI, when you do az login.

A simple implementation

OK, this post has been a little abstract so far, so let’s build something! We’re going to create a simple console app that authenticates a user using the device flow.

In this example, I use Azure AD as the identity provider, because it’s easy and doesn’t require any setup (of course, you could also do this with your IdP of choice, like Auth0, Okta, a custom IdP based on IdentityServer, etc.). Head to the Azure Portal, in the Azure Active Directory blade, App registrations tab. Create a new registration, give it any name you like, and select "Accounts in this organizational directory only (Default directory only – Single tenant)" for the Supported Account Types (it would also work in multi-tenant mode, of course, but let’s keep things simple for now). Also enter a redirect URI for a public client. It shouldn’t be necessary for the device flow, and it won’t actually be used, but for some reason, authentication will fail if it’s not defined… one of Azure AD’s quirks, I guess.

App registration

Now, go to the Authentication tab of the app, in the Advanced settings section, and set Treat application as a public client to Yes.

Public client

And that’s all for the app registration part. Just take note of these values in the app’s Overview tab:

  • Application ID (client ID in OAuth terminology)
  • Directory ID (a.k.a. tenant ID; this is your Azure AD tenant)

Now, in our program, the first step is to issue a request to the device code endpoint to start the authorization flow. The OpenID Connect discovery document on Azure AD is incomplete and doesn’t mention the device code endpoint, but it can be found in the documentation. We need to send the client id of our application and the requested scopes. In this case, we use openid, profile and offline_access (to get a refresh token), but in real-world scenario you’ll probably need an API scope as well.

private const string TenantId = "<your tenant id>";
private const string ClientId = "<your client id>";

private static async Task<DeviceAuthorizationResponse> StartDeviceFlowAsync(HttpClient client)
{
    string deviceEndpoint = $"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/devicecode";
    var request = new HttpRequestMessage(HttpMethod.Post, deviceEndpoint)
    {
        Content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["client_id"] = ClientId,
            ["scope"] = "openid profile offline_access"
        })
    };
    var response = await client.SendAsync(request);
    response.EnsureSuccessStatusCode();
    var json = await response.Content.ReadAsStringAsync();
    return JsonSerializer.Deserialize<DeviceAuthorizationResponse>(json);
}

private class DeviceAuthorizationResponse
{
    [JsonPropertyName("device_code")]
    public string DeviceCode { get; set; }

    [JsonPropertyName("user_code")]
    public string UserCode { get; set; }

    [JsonPropertyName("verification_uri")]
    public string VerificationUri { get; set; }

    [JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }

    [JsonPropertyName("interval")]
    public int Interval { get; set; }
}

Let’s call this method and open the verification_uri from the response in the browser. The user will need to enter the user_code in the authorization page.

using var client = new HttpClient();
var authorizationResponse = await StartDeviceFlowAsync(client);
Console.WriteLine("Please visit this URL: " + authorizationResponse.VerificationUri);
Console.WriteLine("And enter the following code: " + authorizationResponse.UserCode);
OpenWebPage(authorizationResponse.VerificationUri);

This opens the following page:

Enter user code

Note: the specs for the device flow mention an optional verification_uri_complete property in the authorization response, which includes the user_code. Unfortunately, this is not supported by Azure AD, so the user has to enter the code manually.

Now, while the user is entering the code and logging in, we start polling the IdP to get a token. We need to specify urn:ietf:params:oauth:grant-type:device_code as the grant_type, and provide the device_code from the authorization response.

var tokenResponse = await GetTokenAsync(client, authorizationResponse);
Console.WriteLine("Access token: ");
Console.WriteLine(tokenResponse.AccessToken);
Console.WriteLine("ID token: ");
Console.WriteLine(tokenResponse.IdToken);
Console.WriteLine("refresh token: ");
Console.WriteLine(tokenResponse.IdToken);

...

private static async Task<TokenResponse> GetTokenAsync(HttpClient client, DeviceAuthorizationResponse authResponse)
{
    string tokenEndpoint = $"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token";

    // Poll until we get a valid token response or a fatal error
    int pollingDelay = authResponse.Interval;
    while (true)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
        {
            Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code",
                ["device_code"] = authResponse.DeviceCode,
                ["client_id"] = ClientId
            })
        };
        var response = await client.SendAsync(request);
        var json = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            return JsonSerializer.Deserialize<TokenResponse>(json);
        }
        else
        {
            var errorResponse = JsonSerializer.Deserialize<TokenErrorResponse>(json);
            switch(errorResponse.Error)
            {
                case "authorization_pending":
                    // Not complete yet, wait and try again later
                    break;
                case "slow_down":
                    // Not complete yet, and we should slow down the polling
                    pollingDelay += 5;                            
                    break;
                default:
                    // Some other error, nothing we can do but throw
                    throw new Exception(
                        $"Authorization failed: {errorResponse.Error} - {errorResponse.ErrorDescription}");
            }

            await Task.Delay(TimeSpan.FromSeconds(pollingDelay));
        }
    }
}

private class TokenErrorResponse
{
    [JsonPropertyName("error")]
    public string Error { get; set; }

    [JsonPropertyName("error_description")]
    public string ErrorDescription { get; set; }
}

private class TokenResponse
{
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; }

    [JsonPropertyName("id_token")]
    public string IdToken { get; set; }

    [JsonPropertyName("refresh_token")]
    public string RefreshToken { get; set; }

    [JsonPropertyName("token_type")]
    public string TokenType { get; set; }

    [JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }

    [JsonPropertyName("scope")]
    public string Scope { get; set; }
}

When the user completes the login process in the browser, the next call to the token endpoint returns an access_token, id_token and refresh_token (if you requested the offline_access scope).

When the access token expires, you can use the refresh token to get a new one, as described in the specs.

Conclusion

As you can see, the device flow is pretty easy to implement; it’s quite straightforward, with no redirection mechanism. Its simplicity also makes it quite secure, with very few angles of attack. In my opinion, it’s the ideal flow for desktop or console applications.

You can find the full code for this article in this repository.

Google+ shutdown: fixing Google authentication in ASP.NET Core

A few months ago, Google decided to shutdown Google+, due to multiple data leaks. More recently, they announced that the Google+ APIs will be shutdown on March 7, 2019, which is pretty soon! In fact, calls to these APIs might start to fail as soon as January 28, which is less than 3 weeks from now. You might think that it doesn’t affect you as a developer; but if you’re using Google authentication in an ASP.NET Core app, think again! The built-in Google authentication provider (services.AddAuthentication().AddGoogle(...)) uses a Google+ API to retrieve information about the signed-in user, which will soon stop working. You can read the details in this Github thread. Note that it also affects classic ASP.NET MVC.

OK, now I’m listening. How do I fix it?

Fortunately, it’s not too difficult to fix. There’s already a pull request to fix it in ASP.NET Core, and hopefully an update will be released soon. In the meantime, you can either:

  • use the workaround described here, which basically specifies a different user information endpoint and adjusts the JSON mappings.
  • or use the generic OpenID Connect authentication provider instead, which I think is better than the built-in provider anyway, because you can get all the necessary information directly from the ID token, without making an extra API call.

Using OpenID Connect to authenticate with Google

So, let’s see how to change our app to use the OpenID Connect provider instead of the built-in Google provider, and configure it to get the same results as before.

First, let’s install the Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet package to the project, if it’s not already there.

Then, we go to the place where we add the built-in Google provider (the call to AddGoogle, usually in the Startup class), and remove that call.

Instead, we add the OpenID Connect provider, point it to the Google OpenID Connect authority URL, and set the client id (the same that we were using for the built-in Google provider):

services
    .AddAuthentication()
    .AddOpenIdConnect(
        authenticationScheme: "Google",
        displayName: "Google",
        options =>
        {
            options.Authority = "https://accounts.google.com/";
            options.ClientId = configuration["Authentication:Google:ClientId"];
        });

We also need to adjust the callback path to be the same as before, so that the redirect URI configured for the Google app still works; and while we’re at it, let’s also configure the signout paths.

options.CallbackPath = "/signin-google";
options.SignedOutCallbackPath = "/signout-callback-google";
options.RemoteSignOutPath = "/signout-google";

The default configuration already includes the openid and profile scopes, but if we want to have access to the user’s email address as we did before, we also need to add the email scope:

options.Scope.Add("email");

And that’s it! Everything should work as it did before. Here’s a Gist that shows the code before and after the change.

Hey, where’s the client secret?

You might have noticed that we didn’t specify the client secret. Why is this?

The built-in Google provider is actually just a generic OAuth provider with Google-specific configuration. It uses the authorization code flow, which requires the client secret to exchange the authorization code for an access token, which in turn is used to call the user information endpoint.

But by default the OpenId Connect provider uses the implicit flow. There isn’t an authorization code: an id_token is provided directly to the redirect_uri, and there’s no need to call any API, so no secret is needed. If, for some reason, you don’t want to use the implicit flow, just change options.ResponseType to code (the default is id_token), and set options.ClientSecret as appropriate. You should also set options.GetClaimsFromUserInfoEndpoint to true to get the user details (name, email…), since you won’t have an id_token to get them from.

Multitenant Azure AD issuer validation in ASP.NET Core

If you use Azure AD authentication and want to allow users from any tenant to connect to your ASP.NET Core application, you need to configure the Azure AD app as multi-tenant, and use a "wildcard" tenant id such as organizations or common in the authority URL:

openIdConnectOptions.Authority = "https://login.microsoftonline.com/organizations/v2.0";

The problem when you do that is that with the default configuration, the token validation will fail because the issuer in the token won’t match the issuer specified in the OpenID metadata. This is because the issuer from the metadata includes a placeholder for the tenant id:

https://login.microsoftonline.com/{tenantid}/v2.0

But the iss claim in the token contains the URL for the actual tenant, e.g.:

https://login.microsoftonline.com/64c5f641-7e94-4d21-ae5c-9747994e4211/v2.0

A workaround that is often suggested is to disable issuer validation in the token validation parameters:

openIdConnectOptions.TokenValidationParameters.ValidateIssuer = false;

However, if you do that the issuer won’t be validated at all. Admittedly, it’s not much of a problem, since the token signature will prove the issuer identity anyway, but it still bothers me…

Fortunately, you can control how the issuer is validated, by specifying the TokenValidator property:

options.TokenValidationParameters.IssuerValidator = ValidateIssuerWithPlaceholder;

Where ValidateIssuerWithPlaceholder is the method that validates the issuer. In that method, we need to check if the issuer from the token matches the issuer with a placeholder from the metadata. To do this, we just replace the {tenantid} placeholder with the value of the token’s tid claim (which contains the tenant id), and check that the result matches the token’s issuer:

private static string ValidateIssuerWithPlaceholder(string issuer, SecurityToken token, TokenValidationParameters parameters)
{
    // Accepts any issuer of the form "https://login.microsoftonline.com/{tenantid}/v2.0",
    // where tenantid is the tid from the token.

    if (token is JwtSecurityToken jwt)
    {
        if (jwt.Payload.TryGetValue("tid", out var value) &&
            value is string tokenTenantId)
        {
            var validIssuers = (parameters.ValidIssuers ?? Enumerable.Empty<string>())
                .Append(parameters.ValidIssuer)
                .Where(i => !string.IsNullOrEmpty(i));

            if (validIssuers.Any(i => i.Replace("{tenantid}", tokenTenantId) == issuer))
                return issuer;
        }
    }

    // Recreate the exception that is thrown by default
    // when issuer validation fails
    var validIssuer = parameters.ValidIssuer ?? "null";
    var validIssuers = parameters.ValidIssuers == null
        ? "null"
        : !parameters.ValidIssuers.Any()
            ? "empty"
            : string.Join(", ", parameters.ValidIssuers);
    string errorMessage = FormattableString.Invariant(
        $"IDX10205: Issuer validation failed. Issuer: '{issuer}'. Did not match: validationParameters.ValidIssuer: '{validIssuer}' or validationParameters.ValidIssuers: '{validIssuers}'.");

    throw new SecurityTokenInvalidIssuerException(errorMessage)
    {
        InvalidIssuer = issuer
    };
}

With this in place, you’re now able to fully validate tokens from any Azure AD tenant without skipping issuer validation.

Happy coding, and merry Christmas!