Зачем нужен 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 авторизации:
- Google — www.google.com/accounts/o8/id
- Yandex — openid.yandex.ru
- MailRu — [имя пользователя].id.mail.ru
- Livejournal — [имя пользователя].livejournal.com
- Рамблер — rambler.ru
- MyOpenID — myopenid.com
Списки авторизованных сайтов (когда будете тестировать – понадобится):
- Google — https://www.google.com/accounts/IssuedAuthSubTokens?hl=ru
- Yandex — http://openid.yandex.ru/settings/trust_roots/
- MailRu — http://openid.mail.ru/sitelist
- Livejournal — не могу найти
- Рамблер — нету
- MyOpenID — https://www.myopenid.com/sites
Создаем новый проект 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. Опытным путем выясняем что:
- Для google – нужно использовать FetchRequest
- Для yandex, mailru, rambler, myopenId – нужно использовать ClaimRequest
- В ЖЖ живут жлобы и не дают вообще никаких подробностей
Для 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, ВКонтакте.
В каждом из них нужно создать приложение, это просто, я просто укажу где это делается:
- Facebook — http://www.facebook.com/developers/apps.php
- Twitter — http://twitter.com/apps
- ВКонтакте — http://vkontakte.ru/apps.php#act=admin
Потом нам еще понадобится убирать разрешения для приложений, так что вот ссылки:
- Facebook — http://www.facebook.com/settings/?tab=applications
- Twitter — http://twitter.com/settings/connections
- ВКонтакте — не надо
Начнем с Twitterа

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

О, тут я убил 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 тоже…
