OAuth2.0, MVC4 и Yandex

Если вы разрабатываете проект на MVC 4 с использованием авторизации по протоколу OAuth 2,0 используя DotNetOpenAuth и хотите прикрутить авторизацию через аккаунты Yandex, то у вас определенно возникнут с этим трудности.

Ну во-первых, нужно будет написать OAuth2,0 клиент для работы с Яндексом. В этом нет ни чего сложного. Нужно только взять уже написанный клиент, скажем, для Facebook (из репов DotNetOpenAuth на GitHub) и подправить некоторый код.


Я пошел немного другим путем и написал по своему, но суть от этого не меняется:

//-----------------------------------------------------------------------
// <copyright file="YandexOAuthClient.cs" company="R@Me0">
// Copyright (c) R@Me0. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace DotNetOpenAuth.AspNet.Clients
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Web;
using DotNetOpenAuth.Messaging;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Runtime.Serialization;

/// <summary>
/// The yandex client.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Yandex", Justification = "Brand name")]
public sealed class YandexClient : OAuth2Client
{
#region Constants and Fields

/// <summary>
/// The authorization endpoint.
/// </summary>
private const string AuthorizationEndpoint = "https://oauth.yandex.ru/authorize?response_type=code&amp;amp;amp;client_id={0}&amp;amp;amp;state={1}";

/// <summary>
/// The token endpoint.
/// </summary>
private const string TokenEndpoint = "https://oauth.yandex.ru/token";

private const string TokenPostFormat = "grant_type=authorization_code&amp;amp;amp;code={0}&amp;amp;amp;client_id={1}&amp;amp;amp;client_secret={2}&amp;amp;amp;state={3}";

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

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

#endregion

#region Constructors and Destructors

/// <summary>
/// Initializes a new instance of the <see cref="YandexClient"/> class.
/// </summary>
/// <param name="appId">
/// The app id.
/// </param>
/// <param name="appSecret">
/// The app secret.
/// </param>
public YandexClient(string appId, string appSecret)
: base("yandex")
{
if (string.IsNullOrEmpty(appId))
throw new ArgumentException("appId");

if (string.IsNullOrEmpty(appSecret))
throw new ArgumentException("appSecret");

this.appId = appId;
this.appSecret = appSecret;
}

#endregion

#region Methods

/// <summary>
/// The get service login url.
/// </summary>
/// <param name="returnUrl">
/// The return url.
/// </param>
/// <returns>An absolute URI.</returns>
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
return new Uri(
string.Format(AuthorizationEndpoint, appId, Uri.EscapeDataString(returnUrl.AbsoluteUri))
);
}

public override void RequestAuthentication(HttpContextBase context, Uri returnUrl){
HttpContextBase c = context;
Uri r = returnUrl;

string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri;
context.Response.Redirect(redirectUrl, endResponse: true);

}

/// <summary>
/// Obtains an access token given an authorization code and callback URL.
/// </summary>
/// <param name="returnUrl">
/// The return url.
/// </param>
/// <param name="authorizationCode">
/// The authorization code.
/// </param>
/// <returns>
/// The access token.
/// </returns>
protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
{

var message = string.Format(TokenPostFormat, authorizationCode, appId, appSecret, Uri.EscapeDataString(returnUrl.AbsoluteUri));
var tokenRequest = WebRequest.Create(TokenEndpoint);
tokenRequest.ContentType = "application/x-www-form-urlencoded";
tokenRequest.ContentLength = message.Length;
tokenRequest.Method = "POST";

using (var requestStream = tokenRequest.GetRequestStream())
{
var writer = new StreamWriter(requestStream);
writer.Write(message);
writer.Flush();
}

var tokenResponse = (HttpWebResponse)tokenRequest.GetResponse();
if (tokenResponse.StatusCode == HttpStatusCode.OK)
{
using (var responseStream = tokenResponse.GetResponseStream())
{
var reader = new StreamReader(responseStream);
var responseText = reader.ReadToEnd();
try
{
JObject j = JObject.Parse(responseText);

//JArray j = JArray.ReadFrom(reader);

return j["access_token"].ToString();
}
catch (Exception)
{
return null;
}
}
}

return null;
}

/// <summary>
/// The get user data.
/// </summary>
/// <param name="accessToken">
/// The access token.
/// </param>
/// <returns>A dictionary of profile data.</returns>
protected override IDictionary<string, string> GetUserData(string accessToken)
{
var request = WebRequest.Create("https://login.yandex.ru/info?format=json&amp;amp;amp;oauth_token=" + accessToken);
JObject json = null;

using (var response = request.GetResponse())
{
using (var responseStream = response.GetResponseStream())
{
var reader = new StreamReader(responseStream);
json = JObject.Parse(reader.ReadToEnd());
}
}

var userData = new Dictionary<string, string>();

userData.Add("id", (string)json["id"]);
userData.Add("username", (string)json["default_email"]);
userData.Add("name", (string)json["display_name"]);
userData.Add("gender", (string)json["sex"]);
userData.Add("birthday", (string)json["birthday"]);

return userData;

}

#endregion
}

}

Дальше, в AppStart/AuthConfig.cs подключаем новый драйвер авторизации:

OAuthWebSecurity.RegisterClient(
new DotNetOpenAuth.AspNet.Clients.YandexClient(
"c9-----8bbfd4----7a54d----13851d",
"d5-------9ea---090045------cd602"
), "Яндекс", null
);

Казалось бы все, кнопки появляются, перенаправление на Яндекс идет, НО авторизация, даже после подтверждения пользователем не проходит.

Почему? Все просто: потому, что Yandex в своей реализации OAuth аутентификации немного отступил от стандарта, а именно, он не принимает redirect_uri, без которого разработанный наш клиент не получает определенные параметры необходимые для подтверждения регистрации на нашем сайте.

Лирическое отступление: решение я искал очень долго (часа 4) перекопал весь код OAuth2Client перепробовал кучу вариантов, пока не нашел разумное решение.

Итак, если вы внимательно смотрели код, то возможно обратили внимание на шаблоны строк «AuthorizationEndpoint» и «TokenEndpoint». В них появился добавился параметр «state», в котором можно передать все что душе угодно. Он без изменений подсоединится к адресу, на который будет стучаться Яндекс, подтверждая (или не подтверждая) авторизацию.

Так вот, мое решение базируется именно на использовании этого параметра. Как вы можете увидеть, в функциях «GetServiceLoginUrl» и «QueryAccessToken», мы передаем в него нашу redirect_uri.

Далее, в нашем коде, в контроллере обрабатывающем ответы от OAuth сервисов (AccountController) нужно немного изменить код. Так, чтобы, если ответ приходит от Яндекса, мы брали параметр state и делали редирект на него. Я сделал это вот таким образом:

public ActionResult ExternalLoginCallback(string returnUrl)
{
string state = this.HttpContext.Request.QueryString.Get("state");
string redirected = this.HttpContext.Request.QueryString.Get("redirected");
if ((redirected== null || redirected == string.Empty) &amp;amp;&amp;amp; (returnUrl == string.Empty || returnUrl == null) &amp;amp;&amp;amp; state != string.Empty)
{
//returnUrl = Uri.UnescapeDataString(state);
return Redirect(state + "&amp;amp;" + this.HttpContext.Request.QueryString.ToString() + "&amp;amp;redirected=redirected");
}
AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
if (!result.IsSuccessful)
{
return RedirectToAction("ExternalLoginFailure");
}

if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false))
{
return RedirectToLocal(returnUrl);
}

if (User.Identity.IsAuthenticated)
{
// If the current user is logged in add the new account
OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, User.Identity.Name);
return RedirectToLocal(returnUrl);
}
else
{
// User is new, ask for their desired membership name
string loginData = OAuthWebSecurity.SerializeProviderUserId(result.Provider, result.ProviderUserId);
ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(result.Provider).DisplayName;
ViewBag.ReturnUrl = returnUrl;
return View("ExternalLoginConfirmation", new RegisterExternalLoginModel { UserName = result.UserName, ExternalLoginData = loginData });
}
}

Изменения коснулись только первых 7 строчек кода, точнее эти строчки были добавлены в стандартный код.
Да, кстати, здесь нужно отметить, что я ввел еще один параметр, добавляемый к в QueryString при работе с яндексом — это параметр «redirected», он нужен для того, чтобы наш алгоритм не ушел в бесконечный редирект.

PS. Этот метод реализации OAuth авторизации можно использовать не только для Яндекса, но и для всех сервисов, которые не принимают redirect_uri.

Комментирование приветствуется. Так же, было бы неплохо услышать критику и варианты более красивой реализации, если они у вас появятся

Теги: .NET, ASP MVC4, ASP.NET, DotNetOpenAuth, MVC 4, OAuth 2.0, Web-programing, Yandex

Комментарии ()