OAuth2 и DotNetOpenAuth - реализация пользовательского клиента Google
У меня проблема с реализацией пользовательского OAuth2Client для Google с использованием DotNetOpenAuth и MVC4.
Я дошел до того, что смог успешно сделать запрос на авторизацию к конечной точке Google https://accounts.google.com/o/oauth2/auth
и Google спрашивает, разрешит ли пользователь моему приложению доступ к своей учетной записи. Пока все хорошо. Когда пользователь нажимает "ОК", Google затем вызывает URL моего обратного вызова, как и ожидалось.
Проблема заключается в том, что я вызываю метод VerifyAuthentication для класса OAuthWebSecurity (Microsoft.Web.WebPages.OAuth).
var authenticationResult = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
Он всегда возвращает AuthenticationResult с IsSuccessful = false
а также Provider = ""
Я рассмотрел код для этого, и класс OAuthWebSecurity пытается получить имя поставщика из
Request.QueryString["__provider__"]
но Google не отправляет эту информацию обратно в строку запроса. Другой провайдер, которого я реализовал (LinkedIn), отправляет имя провайдера обратно, и все работает просто отлично.
Я не уверен, что я могу сделать с этого момента, кроме как отказаться от классов Microsoft.Web.WebPages.OAuth и просто использовать DotNetOpenAuth без них, но я надеялся, что у кого-то может быть другое решение, которое я могу попробовать...
Я много искал, но, похоже, не могу найти ничего, что могло бы помочь... Мне было очень трудно даже найти примеры людей, делающих то же самое, что меня очень удивило.
Любая помощь высоко ценится!
2 ответа
Обновление: как Мэтт Джонсон упоминает ниже, он подготовил решение для этого, которое вы можете получить от GitHub: https://github.com/mj1856/DotNetOpenAuth.GoogleOAuth2
Как он отмечает: DNOA и OAuthWebSecurity для ASP.Net MVC 4 поставляются только с поставщиком OpenId для Google. Это клиент OAuth2, который вы можете использовать вместо этого.
ВАЖНО. Если вы используете ASP.Net MVC 5, этот пакет не применяется. Вместо этого вы должны использовать Microsoft.Owin.Security.Google. (Он также поставляется с начальными шаблонами MVC 5 в VS 2013.)
В конце концов я обошел это, поймав запрос, когда он поступил, и выполнив собственную проверку на предмет того, от какого поставщика он пришел. Google позволяет вам отправлять параметр в запрос OAuth с именем "state", который они просто передают прямо вам, когда выполняют обратный вызов, поэтому я использую это для передачи имени поставщика для Google, и я проверяю это в отсутствие "__provider__"
,
что-то вроде этого:
public String GetProviderNameFromQueryString(NameValueCollection queryString)
{
var result = queryString["__provider__"];
if (String.IsNullOrWhiteSpace(result))
{
result = queryString["state"];
}
return result;
}
Затем я реализовал пользовательский OAuth2Client для Google, и я сам вручную вызвал метод VerifyAuthentication, обойдя оболочку Microsoft.
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);
}
Это позволило мне сохранить то, что у меня уже было, для других провайдеров, использующих оболочки Microsoft.
В соответствии с запросом @1010100 1001010, вот мой пользовательский OAuth2Client для Google (ПРИМЕЧАНИЕ: ЭТО НЕОБХОДИМО НЕКОТОРОЕ ОЧИСТИТЬ! Я ПОЛУЧИЛ КРУГЛЫЙ ДИСПЛЕЙ К КОДУ. Однако это работает):
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);
}
Вы можете добавить параметр запроса провайдера в конец вашего URL обратного вызова. например, https://mywebsite.com/Account/ExternalLoginCallback?__provider__=google поставщик = Google
Вы получите это, и вам не нужно работать вокруг.