Pull to refresh

OpenID, OAuth и другие плюшки

ASP
Tutorial

Зачем нужен OpenID



Вот бывает так, заходишь на сайт любимый, а там ссылка на другой сайт, а там статья ну очень интересная и главное – полезная – и хочется добавить комментарий, типа «Молодцы!» и чтобы добавить комментарий, нужно зарегистрироваться, а чтобы зарегистрироваться нужно ввести «Имя», «Фамилия», «Логин», «Email», «Email еще раз», «Пароль», «Снова Пароль», «Прочитал правила и согласен со всем что тут будет происходить» и «Капчу». И жмакаешь «Зарегистрироваться», а тут бац – «Логин» — занят, и поля «Пароль», «Снова Пароль», «Капча» — стерты. Ну вот так. Вводишь другой свой логин (который второй, и не главный и не любимый) и снова пароль, снова снова пароль (постите) и капчу, и бац – всё ок, только забыл снова поставить галку «Прочитал правила...». Ну ладно, прошел еще раз круги ада, на мыло вышло письмо, активировал аккаунт, так, а где там была статья, да и ну их, не молодцы они, ну т.е. молодцы ну и хрен с этим, знают и так.
Проведите эксперимент, в вашей любимой почте сделайте поиск по слову «activate» — вот примерно столько вы регистрировались на сайтах.
А с другой стороны думаешь, а давайте упростим, и делаешь простое добавление комментария: «Имя», «Email», «Сообщение» — причем «Email» можно не вводить. Через 3 месяца заходишь, а там – СПАМ! Ладно, почистил – и ноль эффекта, спам продолжает, добавил капчу – ну вроде ок, но потом снова как-то они ее обходят. И внимание(!) – вводим регистрацию… Ой!
Но есть (УРА!) – OpenID.

Начал разбираться. Вот статья про то как OpenID работает. Тут был облом, в том что OpenID провайдер передает на наш сайт только ClaimedIdentifier, который в данном случае выглядит у меня andriy-chernikov.myopenid.com, ну ладно, можно отрезать часть и выделить andriy-chernikov – вполне себе читабельно. Продолжаем, рассуждаем – нормальные пацаны пользуются gmailом, а у google тоже есть OpenID, только мой ClaimedIdentifier (FriendlyIdentifierForDisplay вообще не катит, он там имени не содержит) выглядит там так: www.google.com/accounts/o8/id?id=AItOawl7JUIQLXJf1Z_x1MoYu21XbfBuzvoriso.
«Привет, AItOawl7JUIQLXJf1Z_x1MoYu21XbfBuzvoriso, рады снова видеть вас на нашем сайте»
— что-то как-то не звучит. Выясняем что помимо OpenId идентификатора можно выпросить и другие данные, даже Email! Ну вы понимаете, т.е. если потенциально пользователь оставит сообщение «Молодец! Только тут у тебя не работает вот это, это и это под IE, а тут ты изобрел велосипед, надо было просто такую библиотечку посмотреть, а про вот то расписано очень хорошо вот по такому-то линку» я же могу ему написать «Чувак, премного благодарен». Сайт (зная его email) ему вышлет уведомление, и этот Человек поймет, что комментарий оставленный 3 месяца назад был полезен, ему же будет приятно. Кроме того, как вы понимаете этот email не надо проверять, так как он уже проверен.
В общем, задача:
Сделать так, чтобы пользователь не регистрируясь, а только нажав кнопочку выдал нам данные о себе, и был зарегистрирован, и смог оставить комментарий, ну или поставить рейтинг (за ЗОЙЧ проголосовать, например), а спамеры – идут боком.

OpenID



Первое – выбираем поставщиков (provider). Это будут: Google, Yandex, MailRu, Rambler, Livejournal, MyOpenID. На мой взгляд – это самые популярные сервисы у нас. (про vkontakte, twitter и facebook — позже).
Линки для OpenID авторизации:



Списки авторизованных сайтов (когда будете тестировать – понадобится):


Создаем новый проект ASP.NET MVC3. Скачиваем и добавляем в него DotNetOpenAuth. OpenID у вас скорее всего есть.
Создаем контроллер для авторизации по OpenID (как у handcode [спасибо]):
public class OpenIdController : Controller
{
    private static OpenIdRelyingParty openIdProvider = new OpenIdRelyingParty();
    public ActionResult Index(string userOpenId)
    {
        // Ответ с сайта провайдера.
        IAuthenticationResponse response = openIdProvider.GetResponse();
        // response равен null, если запроса на OpenID провайдер мы не делали.
        if (response == null)
        {
            Identifier id;
            // Пытаемся распарсить OpenID клиента.
            if (Identifier.TryParse(userOpenId, out id))
            {
                try
                {
                    // Делаем редирект на сайт провайдера OpenID
                    IAuthenticationRequest request = openIdProvider.CreateRequest(userOpenId);
                    return request.RedirectingResponse.AsActionResult();
                }
                catch (ProtocolException ex)
                {
                    TempData["error"] = ex.Message;
                }
            }
            return RedirectToAction("Index", "Login");
        }
        else
        {
            // Ответ с сайта провайдера OpenID
            switch (response.Status)
            {
                // Успешная аутентификация
                case AuthenticationStatus.Authenticated:
                    {
                        TempData["id"] = response.ClaimedIdentifier;
                        return RedirectToAction("Index", "Main");
                    }
                case AuthenticationStatus.Canceled:
                    {
                        TempData["message"] = "Аутентификация была отменена пользователем";
                        return RedirectToAction("Index", "Main");
                    }
                case AuthenticationStatus.Failed:
                    {
                        TempData["message"] = "Аутентификация не удалась из за ошибки.";
                        TempData["error"] = response.Exception.Message;
                        return RedirectToAction("Index", "Main");
                    }
                default:
                    {
                        return RedirectToAction("Index", "Main");
                    }            }        }    }}


Теперь самое главное – как запросить дополнительные данные? Есть 2 способа: FetchRequest и ClaimRequest. Опытным путем выясняем что:
  1. Для google – нужно использовать FetchRequest
  2. Для yandex, mailru, rambler, myopenId – нужно использовать ClaimRequest
  3. В ЖЖ живут жлобы и не дают вообще никаких подробностей

Для FetchRequest делаем так. После создания запроса к провайдеру указываем, что нам еще интересно было бы узнать:
try
{
    // Делаем редирект на сайт провайдера OpenID
    IAuthenticationRequest request = openIdProvider.CreateRequest(userOpenId);
    FetchRequest fetch = new FetchRequest();
    fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email, true));
    fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.First, true));
    fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.Last, true));
    fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Preferences.Language, true));
    request.AddExtension(fetch);
    return request.RedirectingResponse.AsActionResult();
 }


А после успешной аутентификации забираем эти данные:
case AuthenticationStatus.Authenticated:
{
	var fetches = response.GetExtension<FetchResponse>();
	if (fetches != null)
	{
		string str = string.Empty;

		str += string.Format("Email : {0} <br/>", fetches.Attributes[WellKnownAttributes.Contact.Email].Values[0]);
		str += string.Format("Имя : {0} <br/>", fetches.Attributes[WellKnownAttributes.Name.First].Values[0]);
		str += string.Format("Фамилия : {0} <br/>", fetches.Attributes[WellKnownAttributes.Name.Last].Values[0]);
		str += string.Format("Язык : {0} <br/>", fetches.Attributes[WellKnownAttributes.Preferences.Language].Values[0]);

		TempData["info"] = str;
	}
	TempData["id"] = response.ClaimedIdentifier;
	return RedirectToAction("Index", "Main");
}


Для ClaimsRequest аналогично:<br/>
try
{
	// Делаем редирект на сайт провайдера OpenID
	IAuthenticationRequest request = openIdProvider.CreateRequest(userOpenId);
	ClaimsRequest claim = new ClaimsRequest();

	claim.BirthDate = DemandLevel.Require;
	claim.Country = DemandLevel.Require;
	claim.Email = DemandLevel.Require;
	claim.FullName = DemandLevel.Require;
	claim.Gender = DemandLevel.Require;
	claim.Language = DemandLevel.Require;
	claim.Nickname = DemandLevel.Require;
	claim.PostalCode = DemandLevel.Require;
	claim.TimeZone = DemandLevel.Require;

	request.AddExtension(claim);
	return request.RedirectingResponse.AsActionResult();
}


И
// Успешная аутентификация
case AuthenticationStatus.Authenticated:
	{
		var claims = response.GetExtension<ClaimsResponse>();
		if (claims != null)
		{
			string str = string.Empty;

			str += string.Format("Дата рождения: {0} <br/>", claims.BirthDate);
			str += string.Format("Страна: {0}<br/>", claims.Country);
			str += string.Format("Email: {0}<br/>", claims.Email);
			str += string.Format("Полное имя: {0}<br/>", claims.FullName);
			str += string.Format("Пол: {0}<br/>", claims.Gender);
			str += string.Format("Язык: {0}<br/>", claims.Language);
			str += string.Format("Ник: {0}<br/>", claims.Nickname);
			str += string.Format("Индекс: {0}<br/>", claims.PostalCode);
			str += string.Format("Временная зона: {0}<br/>", claims.TimeZone);
			TempData["info"] = str;
		}
		TempData["id"] = response.ClaimedIdentifier;
		return RedirectToAction("Index", "Main");
	}


Ну всё кажется что супер, но(!) есть проблемы, первая проблема – это ответ от Рамблера, dotNetOpenAuth обрабатывает его ответ как ошибочный. И вообще Рамблер в openid.claimed_id и openid.identity – возвращает линк на спецификацию, ну в общем, делаем обработку в секции когда вроде бы ошибка:
case AuthenticationStatus.Failed:
{
	//Тут хитро не парсит сам DoTNetOpenAuth
	var email = Request.Params["openid.sreg.email"];
	var fullname = Request.Params["openid.sreg.fullname"];
	var nickname = Request.Params["openid.sreg.nickname"];

	if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(fullname) || string.IsNullOrEmpty(nickname))
	{
		TempData["message"] = "Аутентификация не удалась из за ошибки.";
		TempData["error"] = response.Exception.Message;
	}
	else
	{
		AuthOpenID("http://id.rambler.ru/users/" + nickname, fullname);
	}
	return RedirectToAction("Index", "Main");
}


Проблемы с MailRu. Кстати, он не работает с сайтом, который находится локально. Т.е. localhost – это не сайт и выдается ошибка «Bad Realm» — что решается собственно загрузив сайт куда-то.
Я с Украины, у меня время на час раньше по сравнению с московским, поэтому в записи ответа приходит на час больше, т.е. как бы получается что ответ получен не через пару секунд, а через 1 час и пару секунд. И обрабатывается как ошибка. Добавляем немного настроек в Web.Config:

<configSections> 
	<section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/> 
</configSections> 
... 
<dotNetOpenAuth> 
	<messaging clockSkew="60:00:00" lifetime="10:00:00" strict="true"> 
		<untrustedWebRequest timeout="00:10:00" readWriteTimeout="00:01:00" maximumBytesToRead="1048576" maximumRedirections="10"> 
			... 
		</untrustedWebRequest> 
	</messaging> 
</dotNetOpenAuth>


Вторая ошибка, что ClaimResponse не разбирает параметры. Просто забираем из Request.Params:
// Успешная аутентификация
case AuthenticationStatus.Authenticated:
	{
		string str = string.Empty;
		
		str += string.Format("Email: {0}<br/>", Request.Params["openid.sreg.email"]);
		str += string.Format("Полное имя: {0}<br/>", Request.Params["openid.sreg.fullname"]);
		str += string.Format("Пол: {0}<br/>", Request.Params["openid.sreg.gender"]);
		str += string.Format("Ник: {0}<br/>", Request.Params["openid.sreg.nickname"]);

		TempData["info"] = str;
		TempData["id"] = response.ClaimedIdentifier;
		return RedirectToAction("Index", "Main");
	}

</code>


Итог по OpenID

Имеем быструю аутентификацию по OpenID, из которых только livejournal не дает там email.

OAuth


Вообще OAuth – не для того преназначена, чтобы моментально проходить аутентификацию, очень хорошо про OAuth расписано тут. Но(!) аутентификация там есть, мы этим и воспользуемся. Для начала выберем героев:
Facebook, Twitter, ВКонтакте.
В каждом из них нужно создать приложение, это просто, я просто укажу где это делается:


Потом нам еще понадобится убирать разрешения для приложений, так что вот ссылки:



Начнем с Twitterа


У твиттера есть Consumer key и Consumer secret – важные величины, которые надо куда-то спрятать и никому не показывать.
Так же в настройках надо бы установить Callback URL, Website и Application Website – но работает и без этого.
Далее всё просто, мы просто смотрим что в примерах dotNetOpenAuth есть пример аутентификации через twitter – поэтому мы просто копируем себе и всё.
Facebook


О, тут я убил 4 часа на то, чтобы прикрутить OAuth который в dotNetOpenAuth для авторизации в фейсбуке. Но не вышло. Потом я нашел отличную библиотеку: http://facebooknet.codeplex.com/ — там есть рабочий пример (на локалхосте у меня не заработало, а вот на сайте – всё ок). Кстати очень удобная для направленной интеграции с фейсбуком, куча всего. Есть в NuGet.
Так вот, создав Facebook-приложение имеем 3 ключа:
  • ApplicationID
  • ApplicationKey
  • ApplicationSecret


Они нам все нужны будут. Также настраиваем во вкладке Web Site Site URL и Site Domain.
Переходим на страницу http://developers.facebook.com/docs/authentication и изучаем как там это происходит:
Шаг 1. Я даю свой ApplicationID и прошу выдать мне информацию о себе, кстати в запрос добавляем scope=email чтобы нам дали еще email:
graph.facebook.com/oauth/authorize?client_id={0}&redirect_uri={1}&scope=email

Пользователь видит:


Если пользователь разрешает – мы получаем Код (code). Если нет – то получаем error_description, что пользователь не хочет вам ничего давать.
Шаг 2. Далее мы с этим code идем и запрашиваем access_token:
graph.facebook.com/oauth/access_token?client_id={0}&redirect_uri={1}&client_secret={2}&code={3}


Тут нам возвращают строку типа:
access_token=114782445239544|2.izExIa_6jpjjhUy3s7EDZw__.3600.1291809600-708770020|VbEE8KVlLTMygGmYwm-V08aVKgY&expired=12010


Или ошибку в JSON, что как бы нелогично так с форматами играться, ну да ладно.
Шаг 3. Получив access_token мы запрашиваем пользовательские данные:
graph.facebook.com/me?access_token={0}

На что в JSON формате получаем много информации об пользователе. Ура!
ВКонтакте


Вконтакте как всегда выделился, чтобы написать пошагово как что сделать, чтобы было как в фейсбуке – они просто дали свой виджет и написали «скажи куда послать данные». Виджету надо передать ApplicationID и всё. Вконтакте не передает вообще никакой email – зато скидывает ссылку на аватарку и фоточку пользователя. Javacript:
VK.init({ apiId: vkontakteAppId });
VK.Widgets.Auth("vk_auth", { width: "210px", authUrl: '/vkontakte' });


А в нужном контроллере получаем:
public class VkontakteController : Controller
{
	public ActionResult Index(string first_name, string last_name, string uid)
	{
		var str = string.Empty;
		str += string.Format("Имя : {0}<br/>", first_name);
		str += string.Format("Фамилия: {0}<br/>", last_name);
		TempData["info"] = str;
		TempData["id"] = "http://vkontakte.ru/id" + uid;
		return RedirectToAction("Index", "Main");
	}
}


Итоги


Мы частично решили поставленную задачу. MyOpenID, Yandex, Google, Facebook, MailRu – вообще молодцы и клевые. Twitter, Livejournal, ВКонтакте – зажали email (про твиттер особо не ручусь). Rambler – вообще молодцы были бы если бы не накосячили с реализацией протокола.
Пример можно глянуть тут: http://cocosanka.ru
Исходники скачать тут: http://bitbucket.org/chernikov/smartauth
P.S.: Я знаю про Loginza. При попытке аутентификации на google он запрашивает еще и контакты с гуглопочты. Я конечно не параноик, но зачем им это? А?
P.P.S: Я тут узнал, что Яндекс сделал OAuth тоже…
Tags:openIdOAuthgoogleyandextwitterfacebookvkontaktemail.ruramblerlivejournaldotNetOpenAuthFacebook.net
Hubs: ASP
Total votes 79: ↑67 and ↓12+55
Views22K

Popular right now