Зачем нужен 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 тоже…