Monday, March 25, 2013

Using DotNetOpenAuth with Constant Contact

I was writing the OAuth portion of my client’s cloud application which will communicate with Constant Contact, allowing them to manage contacts and send trigger emails. Having used DotNetOpenAuth before, it figured to be a great fit here. Now that it is all working, it is a great fit, but I do want to describe some issues I had to overcome.

Before we start, let me just say that the Constant Contact documentation is really well done. The other nice thing is we do not (currently) have to worry about refresh token management, as Constant Contact does not expire their issued access tokens.

Implementation Details

The implementation is fairly straight-forward. I have a static consumer class that handles the access token retrieval, and then in my ASP.NET MVC4 website, a controller which calls the consumer.

Consumer

public static class ConstantContactConsumer
{
    /// <summary>
    /// Constant Contact service description
    /// </summary>
    public static readonly AuthorizationServerDescription ServiceDescription = new AuthorizationServerDescription
    {
        TokenEndpoint = new Uri("
https://oauth2.constantcontact.com/oauth2/oauth/token"),
        AuthorizationEndpoint = new Uri("
https://oauth2.constantcontact.com/oauth2/oauth/siteowner/authorize"),
    };

    /// <summary>
    /// Gets the client using the static service description, client id, and client secret
    /// </summary>
    /// <returns></returns>
    private static WebServerClient GetClient()
    {
        WebServerClient client = new WebServerClient(
            ServiceDescription,
            ConfigurationManager.AppSettings["ConstantContactConsumerKey"],
            ConfigurationManager.AppSettings["ConstantContactConsumerSecret"]
            );
        client.AuthorizationTracker = new TokenManager();
        return client;
    }

    /// <summary>
    /// Begins the authorization process by calling the Authorization endpoint
    /// </summary>
    /// <param name="CallbackUrl"></param>
    public static void BeginAuthorization(string CallbackUrl)
    {
        var client = GetClient();
        client.RequestUserAuthorization(null, new Uri(CallbackUrl));
    }

    /// <summary>
    /// Finishes authorization by passing the authorization code to the Token endpoint
    /// </summary>
    /// <returns></returns>
    public static string FinishAuthorization()
    {
        string accessToken = null;
        var client = GetClient();
        client.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings[CLIENT_SECRET]);

        IAuthorizationState authorization = client.ProcessUserAuthorization();

        if (authorization != null)
            accessToken = authorization.AccessToken;

        return accessToken;
    }

}

MVC Controller

public ActionResult GenerateToken()
{
    string callback = Url.Action("ConstantContact", "OAuth", null, Request.Url.Scheme);
    ConstantContactConsumer.BeginAuthorization(callback);

    // Should not get here
    return RedirectToAction("Index");
}

public ActionResult ConstantContact()
{
    string accessToken = ConstantContactConsumer.FinishAuthorization();

    //
    // Store token
    //

    return RedirectToAction("Index");
}

When you browse to http://localhost:12345/OAuth/GenerateToken, the OAuthController will call the BeginAuthorization() method of the consumer. The user is redirected to the Constant Contact website to log in and grant access, then is redirected to http://localhost:12345/OAuth/ConstantContact. Once there, the FinishAuthorization() method of the consumer is called, and the access token retrieved.

HTTP or HTTPS?

According to Constant Contact’s documentation, the redirect URI should be HTTPS. However, the URI needs to be valid, and the HTTPS requirement is not enforced.

You can also use a localhost URI, which is awesome for testing! In this example, I use http://localhost:12345/OAuth/ConstantContact as the Redirect URI.

The “username” parameter

You can see in Constant Contact’s OAuth2 documentation that when you call the authorize URL, you will be redirected to the callback URL with 2 query string parameters: “code”, which is exchanged for an access token when calling the token URL, and “username”, which is used when calling the Constant Contact API. The resulting URL is http://localhost:12345/OAuth/ConstantContact?code=abc123abc123&username=joesflowers.

What is not obvious or intuitive is how DotNetOpenAuth exchanges the code for an access token. When the FinishAuthorization() method calls the client.ProcessUserAuthorization() method, DNOA pulls the authorization code from the URL, and treats the remaining URL (http://localhost:12345/OAuth/ConstantContact?username=joesflowers) as the Redirect URI. Here’s the problem: Your redirect URI is (or should be) http://localhost:12345/OAuth/ConstantContact.

This is sent by DNOA to Constant Contact:

https://oauth2.constantcontact.com/oauth2/oauth/token?client_id=CLIENTID&client_secret=SECRET:
    code: abc123abc123
    redirect_uri: http://localhost:12345/OAuth/ConstantContact?username=joesflowers
    grant_type: authorization_code
    client_id: CLIENTID
    client_secret: ********

The response from Constant Contact is:

{
  "error": "redirect_uri_mismatch",
  "error_description": "Redirect URI mismatch."
}

Solution

The bottom line here is, while we need to know the “username” when we call the API later, we need to remove that parameter from the Redirect URI DNOA provides to Constant Contact. This is done in the consumer, by modifying the FinishAuthorization() method. All we do is create a modified HTTP Request, which will be passed to the DNOA client’s ProcessUserAuthorization() method:

/// <summary>
/// Finishes authorization by passing the authorization code to the Token endpoint
/// </summary>
/// <returns></returns>
public static string FinishAuthorization()
{
    string accessToken = null;
    var client = GetClient();
    client.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings[CLIENT_SECRET]);

    HttpRequest request = HttpContext.Current.Request;

    // Reproduce request URI
    UriBuilder absoluteUri = new UriBuilder(request.Url.GetLeftPart(UriPartial.Path));
    foreach (string key in request.QueryString.Keys)
    {
        if (key != "username")
            absoluteUri.AppendQueryArgument(key, request.QueryString[key]);
    }

    // Reproduce request headers
    WebHeaderCollection headers = new WebHeaderCollection();
    foreach (string header in request.Headers)
    {
        headers.Add(header, request.Headers[header]);
    }

    // Create request info
    var requestInfo = HttpRequestInfo.Create(request.HttpMethod, absoluteUri.Uri, headers, request.InputStream);

    IAuthorizationState authorization = client.ProcessUserAuthorization(requestInfo);

    if (authorization != null)
        accessToken = authorization.AccessToken;

    return accessToken;
}

I’ll look for a better solution within DNOA, but this does the trick for now.

No comments:

Post a Comment