OAuth2 and DotNetOpenAuth - implementing Google custom client
Asked Answered
A

2

15

I'm having an issue implementing custom OAuth2Client for google using DotNetOpenAuth and MVC4.

I've got to the point where I can successfully make the authorization request to the google endpoint https://accounts.google.com/o/oauth2/auth

and Google asks if the user will allow my application access to their account. All good so far. When the user clicks 'OK', google then calls my callback URL as expected.

The problem is when I call the VerifyAuthentication method on the OAuthWebSecurity class (Microsoft.Web.WebPages.OAuth)

var authenticationResult = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));

It's always returning an AuthenticationResult with IsSuccessful = false and Provider = ""

I've looked into the code for this, and the OAuthWebSecurity class tries to get the Provider name from

Request.QueryString["__provider__"]

but Google is not sending this information back in the querystring. The other provider I have implemented (LinkedIn) is sending the provider name back and it all works just fine.

I'm not sure what I can do from this point, apart from abandon the Microsoft.Web.WebPages.OAuth classes and just use DotNetOpenAuth without them, but I was hoping someone might have another solution I can try...

I've searched extensively, but can't seem to find anything to help ... I've found it really difficult even just to find examples of people doing the same thing, which has really surprised me.

Any help much appreciated!

Allisonallissa answered 5/12, 2012 at 16:9 Comment(5)
I've never used the MS wrappers, just written the DotNetOpenAuth directly and haven't had much problem, its not very complicated to just do that and they have plenty of examples you could just drop in.Mistranslate
Have you considered to file a bug report with google regarding the missing provider string?Sinapism
@PaulTyng - thanks, yes - this is what I've ended up doing in the end.Allisonallissa
@Sinapism good idea, I haven't managed to find where I do that so far though. I'll have a proper look later.Allisonallissa
Cleaned up and tested. Fixed a lot of things, and it works with the new OAuthWebSecurity stuff. Enjoy! github.com/mj1856/DotNetOpenAuth.GoogleOAuth2Silvio
A
12

Update: As Matt Johnson mentions below he has packaged up a solution to this which you can get from GitHub: https://github.com/mj1856/DotNetOpenAuth.GoogleOAuth2

As he notes: DNOA and OAuthWebSecurity for ASP.Net MVC 4 ship with only an OpenId provider for Google. This is an OAuth2 client that you can use instead.

IMPORTANT - If you are using ASP.Net MVC 5, this package is not applicable. You should use Microsoft.Owin.Security.Google instead. (It also ships with the MVC 5 starter templates in VS 2013.)


I got round this in the end by catching the request when it comes in, and doing my own check to see which provider it has come from. Google allow you to send a parameter to the OAuth request called 'state', which they simply pass straight back to you when they make the callback, so I'm using this to pass the provider name for google, and I check for this in the absence of the "__provider__".

something like this:

 public String GetProviderNameFromQueryString(NameValueCollection queryString)
    {
        var result = queryString["__provider__"];

        if (String.IsNullOrWhiteSpace(result))
        {
            result = queryString["state"];
        }

        return result;
    }

I've then implemented a custom OAuth2Client for Google, and I manually call the VerifyAuthentication method on that myself, bypassing the Microsoft wrapper stuff.

 if (provider is GoogleCustomClient)
        {
            authenticationResult = ((GoogleCustomClient)provider).VerifyAuthentication(context, new Uri(String.Format("{0}/oauth/ExternalLoginCallback", context.Request.Url.GetLeftPart(UriPartial.Authority).ToString())));
        }
        else
        {
            authenticationResult = OAuthWebSecurity.VerifyAuthentication(returnUrl);
        } 

This has allowed me to keep the stuff I already had in place for the other providers using the Microsoft wrappers.

As requested by @1010100 1001010, here is my custom OAuth2Client for Google (NOTE: IT NEEDS SOME TIDYING! I HAVEN'T GOT ROUND TO TIDYING THE CODE UP YET. It does work though) :

public class GoogleCustomClient : OAuth2Client
{
    ILogger _logger;

    #region Constants and Fields

    /// <summary>
    /// The authorization endpoint.
    /// </summary>
    private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth";

    /// <summary>
    /// The token endpoint.
    /// </summary>
    private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";

    /// <summary>
    /// The _app id.
    /// </summary>
    private readonly string _clientId;

    /// <summary>
    /// The _app secret.
    /// </summary>
    private readonly string _clientSecret;

    #endregion


    public GoogleCustomClient(string clientId, string clientSecret)
        : base("Google")
    {
        if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException("clientId");
        if (string.IsNullOrWhiteSpace(clientSecret)) throw new ArgumentNullException("clientSecret");

        _logger = ObjectFactory.GetInstance<ILogger>();

        this._clientId = clientId;
        this._clientSecret = clientSecret;
    }

    protected override Uri GetServiceLoginUrl(Uri returnUrl)
    {
        StringBuilder serviceUrl = new StringBuilder();

        serviceUrl.AppendFormat("{0}?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile", AuthorizationEndpoint);
        serviceUrl.Append("&state=google");
        serviceUrl.AppendFormat("&redirect_uri={0}", returnUrl.ToString());
        serviceUrl.Append("&response_type=code");
        serviceUrl.AppendFormat("&client_id={0}", _clientId);

        return new Uri(serviceUrl.ToString());

    }

    protected override IDictionary<string, string> GetUserData(string accessToken)
    {
        RestClient client = new RestClient("https://www.googleapis.com");
        var request = new RestRequest(String.Format("/oauth2/v1/userinfo?access_token={0}", accessToken), Method.GET);
        IDictionary<String, String> extraData = new Dictionary<String, String>();

        var response = client.Execute(request);
        if (null != response.ErrorException)
        {
            return null;
        }
        else
        {
            try
            {
                var json = JObject.Parse(response.Content);

                string firstName = (string)json["given_name"];
                string lastName = (string)json["family_name"];
                string emailAddress = (string)json["email"];
                string id = (string)json["id"];

                extraData = new Dictionary<String, String>
                {
                    {"accesstoken", accessToken}, 
                    {"name", String.Format("{0} {1}", firstName, lastName)},
                    {"firstname", firstName},
                    {"lastname", lastName},
                    {"email", emailAddress},
                    {"id", id}                                           
                };
            }
            catch(Exception ex)
            {
                _logger.Error("Error requesting OAuth user data from Google", ex);
                return null;
            }
            return extraData;
        }

    }

    protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
    {
        StringBuilder postData = new StringBuilder();
        postData.AppendFormat("client_id={0}", this._clientId);
        postData.AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(returnUrl.ToString()));
        postData.AppendFormat("&client_secret={0}", this._clientSecret);
        postData.AppendFormat("&grant_type={0}", "authorization_code");
        postData.AppendFormat("&code={0}", authorizationCode);


        string response = "";
        string accessToken = "";

        var webRequest = (HttpWebRequest)WebRequest.Create(TokenEndpoint);

        webRequest.Method = "POST";
        webRequest.ContentType = "application/x-www-form-urlencoded";

        try
        {

            using (Stream s = webRequest.GetRequestStream())
            {
                using (StreamWriter sw = new StreamWriter(s))
                    sw.Write(postData.ToString());
            }

            using (WebResponse webResponse = webRequest.GetResponse())
            {
                using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
                {
                    response = reader.ReadToEnd();
                }
            }

            var json = JObject.Parse(response);
            accessToken = (string)json["access_token"];
        }
        catch(Exception ex)
        {
            _logger.Error("Error requesting OAuth access token from Google", ex);
            return null;
        }

        return accessToken;

    }

    public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl)
    {

        string code = context.Request.QueryString["code"];
        if (string.IsNullOrEmpty(code))
        {
            return AuthenticationResult.Failed;
        }

        string accessToken = this.QueryAccessToken(returnPageUrl, code);
        if (accessToken == null)
        {
            return AuthenticationResult.Failed;
        }

        IDictionary<string, string> userData = this.GetUserData(accessToken);
        if (userData == null)
        {
            return AuthenticationResult.Failed;
        }

        string id = userData["id"];
        string name;

        // Some oAuth providers do not return value for the 'username' attribute. 
        // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id'
        if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name))
        {
            name = id;
        }

        // add the access token to the user data dictionary just in case page developers want to use it
        userData["accesstoken"] = accessToken;

        return new AuthenticationResult(
            isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData);
    }
Allisonallissa answered 10/12, 2012 at 10:12 Comment(9)
Incidentally - I've found it quite difficult to find an example of a custom OAuth2Client, if anyone would like to see my implementation for Google just give me a shout.Allisonallissa
Marking this as correct in the absence of any other answers, if anything better comes along I will update.Allisonallissa
I've been playing with and trying to get it working nicely. What you've got works fine, but I don't like the extra check of seeing if the callback is for Google then manually checking against the Google OAuth2 client. I've been looking through the DNOA code and can't see why it doesn't call VerifyAuthentication() on the custom Google client when you registered it propery (OAuthWebSecurity.RegisterClient(...)) have you had any luck with that?Monde
@Monde - No ... I also would like to have a cleaner solution to this, but have had to move on and do other stuff. Please let me know if you make any progress with it.Allisonallissa
@Allisonallissa in that 2nd code block, when/where are you setting value of provider? Are you in ExternalLoginCallback when you are doing that?Parament
@Parament - yes, that's right it's in ExternalLoginCallback. At that point 'provider' is the IAuthenticationClient, which I determine from the provider name. I'm getting the provider name using the method in my first code snippet, GetProviderNameFromQueryString(NameValueCollection queryString)Allisonallissa
@Allisonallissa Thanks! I was having issues with using SoundCloud as auth so had to implement something similar.Parament
ExternalLoginCallback is failing because it is also looking for the <code>__sid__</code> in the return QueryString which a new guid on each validation. Google doesn't allow wildcard valid RedirectUrl rules in the OAuth2 API Access client setup, so I strip this off <pre>__sid__</pre> in the returnUrl during the GetServiceLoginUrl and place that into the state. Then setup a specific GoogleCallback which extracts the <pre>__sid__</pre> from the querystring state, and redirects to the ExternalLoginCall back with sid in the querystring. Im still looking into ways of removing extra redirectRomo
@Romo (and all) - I have it working, including the callback rewrite. github.com/mj1856/DotNetOpenAuth.GoogleOAuth2Silvio
N
0

You can add a provider query parameter to the end of your callback url. e.g. https://mywebsite.com/Account/ExternalLoginCallback?provider=google

The you will get it and you don't need the work around.

Nidus answered 16/5, 2014 at 13:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.