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

  • 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 тоже…
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 21

    +1
    Молодец. Отличная статья. А то я с контактовской авторизацией долго мучался…
      0
      Есть в NuGet.

      Как же это ласкает мое ухо!
        +1
        Хочу такой плагин к вордпрессу! Заверните пожалуйста :-)
          0
          Автор упоминул loginza. Так вот есть их плагин для вордпресса. Недавно как-раз поставил.
          +1
          хочется добавить комментарий, типа «Молодцы!» :)
            +6
            Как-то много танцев с бубнами получается.
            Реально если прочитать спецификацию OpenId 2.0 — все можно сделать очень просто даже своими руками.

            А самое грустное (имхо) — что вы не понимаете как работает вся эта кухня, просто пытаетесь подогнать ее под работу библиотеки dotNetOpenAuth.

            Более того, я уверен что вы оставили пару лазеек для обхода этой аутентификации, т.к. не установили тот же Callback URL. Этот колбэк должен дернуть сервис если пользователь отказался аутентифицироваться. И вы на это дергание должны удалить хранимую запись об аутентификации. Это кстати нужно и чтобы защититься от Replay атаки на OpenID.

            Уверен что частично вас спасает сама библиотека т.к. наверняка делает много проверок по стандарту. Но если вам понадобится схема чуть сложнее, чем «эй либа, получи для юзера какой-нить емейл чтобы я ему поверил», например load-balancing, когда пользователь может прийти от провайдера не на тот сервер, который его отправил, у вас все сломается и вы даже не будете иметь представления — почему сломалось, не говоря уж о том, как это исправить.

            PS: При СlaimedId можно сделать discovery шаги по известному адресу и вы получите данные, с которыми надо работать. Claimed от op-endpoint отличается только в том, что в первом случае юзер говорит — «я владею таким-то Id» вашему сервису, а во втором — он это доказывает на самом сервере провайдера…
              +2
              Недавно занимался прикручиванием OpenID, и по моему это все в полном хаосе. Большинство провайдеров просит ввести полный опенайде, для логина, что пользователю не особо проще. Гугл отдельный кедр, там логин вводить не надо пользователю на стороннем саййте. Вконтакте вообще пошел своим путем. У меня еще к яндексу одна либа не цеплялась.
              И вот не смотря на все эти разночтения, вы еще говорите, что использовать библиотеку это моветон? Это технология же должна была упрощать жизнь, а не усложнять ее. Следовательно и библиотеками для работы с ней должно быть легко пользоваться :(
                +2
                Гугль заявляет что умеет и с id и без. Без пользовательского id — проще схема. С пользовательским id — работает на Stackoverflow.

                Далее — если кто-то из провайдеров неправильно реализует описанный стандарт — этим ОП даже пользоваться не стоит, ИМХО. Другое дело — все ленивые и реализуют только те части стандарта которые им нравятся или которые более приоритетны ( например в части Discovery данных). Все ваши сложности скорей всего были в том, что вы не использовали discovery провайдеров — следовательно вам приходилось угадывать а что провайдер умеет и подбирать сэмплы как это прикрутить. Почитайте спеку ( мне пришлось раза три прочитать) — она не такая уж большая, зато вы поймете как должен вести себя тот или иной провайдер на основании тех данных что он о себе открывает в discovery.

                B еще — я понимаю что для рекламы многие обещали простую жизнь пользователям. Но вот я не слышал чтобы обещали простую жизнь разработчикам.

                Или вы хотели суперсхему для аутентификации чтобы была и простая и доверенная? Такого не бывает — для доверенности схемы необходимы проверки. Дальше будет хуже — например в oauth надо вообще ключиками обмениваться и подписывать запросы, это куда сложнее чем генерить 2-3 get\post запроса в openid. Таковы они, доверенные схемы в недоверенной среде.

                PS: к тому же выбранная библиотека — этакий универсальный комбайн — и чтец и жнец и на дуде игрец — конфигурировать можно и как сервер и как клиент и как оба сразу, а еще можно всякие прослойки прикручивать в каждое отверстие. Поэтому важно понимать что именно, как и на каком этапе происходит ( и соответственно — что именно предоставляет библиотека для каждого этапа обработки).
                  +2
                  Да напишите уже пост на эту тему! Пожалуйста… Всё что знаете, ссылки, примеры, всё сгодится…
                    +2
                    Ну если серьезно интересно — попробую что-нить описать (правда боюсь получится или что-то мегабольшое или по вершкам, последних полно в интернете)

                    И только после выпуска продукта ;)
                      0
                      Когда релиз?
                        0
                        не могу сказать =). скоро.
              0
              Хотел еще про лицензию написать, да оказывается они сменили — теперь BSD
                0
                С подсветкой кода было бы лучше.
                  0
                  Как раз нужна была такая штука. Спасибо вам огромное!
                    0
                    > и хочется добавить комментарий, типа «Молодцы!»
                    Это быстро проходит. Остаются несколько любимых сайтов, а где наследил, где натоптал по одному комменту — уже всех следов-тропинок и не найдёшь и не вспомнишь.
                      +1
                      Twitter — twitter.com/apps
                      ВКонтакте — twitter.com/apps


                      Два раза линк на твитер — поправьте.
                        0
                        Спасибо
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Для wp он врядли сможет упаковать, wp на php написан, а тут про asp.net. Я для php (CI) уже сделал авторизацию через google, facebook, twitter используя Oauth и pecl oauth, правда пока в либу для CI не оформил, просто инклюдами, если что обращайтесь, проконсультирую;)
                          0
                          Пробую в бизнес центре подключиться к wi-fi. Мне предлагают авторизоваться через OAuth (vk, fb, twitter). Какой смысл в авторизации, если они не получают инфорацию о том, кто подключился? Или все-таки получают? Какую еще информацию они смогут выудить из моего аккаунта при авторизации? Почему OAuth, а не OpenID? Там бы я сказал, что я — zelserg без риска передачи пароля или какой другой информации о себе (телефоны, e-mail и пр).

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое