Feel Good.

13 апреля 2010

Аутентификация по OpenID в ASP.NET MVC

В этой статье я хочу поделиться с Вами опытом разворачивания инфраструктуры OpenID в ASP.NET MVC приложении. По тексту я создам простое ASP.NET MVC приложение, поддерживающее аутентификацию клиента по OpenID. Точнее сама аутентификация будет проходить на стороне OpenID провайдера, который, относительно нашего приложения, выступает в роли подтверждающей стороны владения данным OpenID. Что такое OpenID и зачем он нужен можно найти здесь. Существует несколько видов аутентификации, но мы рассмотрим наиболее распространенный - checkid_setup. На пути достижения нашей цели выделим несколько этапов:
  1. Получение OpenID аккаунта.
  2. Установка API DotNetOpenAuth.
  3. Создание тестового проекта на ASP.NET MVC.
  4. Запуск и тестирование.

Шаг первый, подготовительный. Для работы Вам будет необходим OpenID-аккаунт, поэтому идем на сайт любого провайдера OpenID (например www.myopenid.com) и заводим там аккаунт, например handcode. Если такое все прошло удачно, то например, Ваш OpenID будет иметь следующий вид http://handcode.myopenid.com.

Шаг второй, завязка. Чтобы не писать свою библиотеку, реализующую протокол OpenID заходим на страницу для разработчиков http://openid.net/developers/libraries на которой перечислен список доступных библиотек. Выбираем, интересующую нас, библиотеку под .NET: DotNetOpenAuth. Скачиваем и устанавливаем последнюю версию себе на диск. Кстати, с данным API идет несколько интересных примеров. Именно с примера OpenIdRelyingPartyMvc я и начинал свое изучение.

Шаг третий, аппогей. Открываем Visual Studio, создаем новый ASP.NET MVC проект, назовем его MainSite. Удаляем "мусорный" AccountController и все что с ним связано, оставив только HomeController. Добавим reference на OpenID библиотеку DotNetOpenAuth.dll. Приступаем к программированию. Добавим в проект новый контроллер UserController:

using System;

using System.Web;

using System.Web.Security;

using System.Web.Mvc;

 

using DotNetOpenAuth.Messaging;

using DotNetOpenAuth.OpenId;

using DotNetOpenAuth.OpenId.RelyingParty;

 

namespace MainSite.Controllers

{

    public class UserController : Controller

    {

        /// <summary>

        /// DotNetOpenAuth

        /// </summary>

        private static OpenIdRelyingParty openIdProvider = new OpenIdRelyingParty();

 

        public ActionResult Index()

        {

            // Если пользователь аутентифицирован, то

            // покажем ему "закрытую" страницу.

            if (User.Identity.IsAuthenticated)

            {

                return View("Index");

            }

            return View("Login");

        }

 

        /// <summary>

        /// Завершение сеанса.

        /// </summary>

        /// <returns></returns>

        public ActionResult Logout()

        {

            // Обнулим cookies, и выйдем на главную.

            FormsAuthentication.SignOut();

            return RedirectToAction("Index", "Home");

        }

 

        /// <summary>

        /// Отобразим форму логина

        /// </summary>

        /// <returns></returns>

        public ActionResult Login()

        {

            return View("Login");

        }

 

        /// <summary>

        /// Аутентифицирует клиента с userOpenId, делая редирект

        /// на сайт провайдера OpenID.

        /// </summary>

        /// <param name="userOpenId">OpenID клиента</param>

        /// <returns>Результат аутентификации</returns>

        public ActionResult Authenticate(string userOpenId)

        {

            // Ответ с сайта провайдера.

            IAuthenticationResponse response = openIdProvider.GetResponse();

 

            // response равен null, если запроса на OpenID провайдер мы не делали.

            if (response == null)

            {

                Identifier id;

                // Пытаемся распарсить OpenID клиента.

                if (Identifier.TryParse(userOpenId, out id))

                {

                    try

                    {

                        // Делаем редирект на сайт провайдера OpenID

                        return

                            openIdProvider

                            .CreateRequest(userOpenId)

                            .RedirectingResponse

                            .AsActionResult(); // Расширение для MVC

                    }

                    catch (ProtocolException ex)

                    {

                        ViewData["Message"] = ex.Message;

                    }

                }

                else

                {

                    // Не корректный OpenID клиента

                    ViewData["Message"] = "Invalid identifier";

                }

                return View("Login");

            }

            else

            {

                // Ответ с сайта провайдера OpenID

                switch (response.Status)

                {

                    // Успешная аутентификация

                    case AuthenticationStatus.Authenticated:

                    {

                        Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;

                        // Аутентифицированы по cookies.

                        FormsAuthentication.SetAuthCookie(response.ClaimedIdentifier, false);

                        return RedirectToAction("Index", "Home");

                    }

                    // Аутентификация была отменена пользователем

                    case AuthenticationStatus.Canceled:

                    {

                        ViewData["Message"] = "Canceled at provider";

                        return View("Login");

                    }

                    // Аутентификация не удалась из за ошибки.

                    case AuthenticationStatus.Failed:

                    {

                        ViewData["Message"] = response.Exception.Message;

                        return View("Login");

                    }

                    // При прочих, делаем редирект на главную.

                    default:

                    {

                        return RedirectToAction("Index", "Home");

                    }

                }

            }

        }

    }

}


Осталось добавить соответствующие представления для Index и Login, так как обработчики Logout и Authenticate не нуждаются в представлении:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

 

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">

    Index

</asp:Content>

 

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h1>Этот раздел только для аутентифицированных пользователей</h1>

    <p>Ваш OpenID подтвержден: <%=Session["FriendlyIdentifier"] %></p>

    <p>

        <%=Html.ActionLink("Logout", "logout") %>

    </p>

</asp:Content>


и представление Login.aspx:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

 

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">

    Login

</asp:Content>

 

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

 

    <!-- Сообщение об ошибке будем выводить красным -->

    <% if (ViewData["Message"] != null) { %>

    <div style="color: red">

        <%= Html.Encode(ViewData["Message"].ToString())%>

    </div>

    <% } %>

 

    <!-- Форма аутентификации, метод POST -->

    <% using (Html.BeginForm("Authenticate", "User", FormMethod.Post)) { %>   

    <label for="userOpenId">Ваш OpenID:</label><%= Html.TextBox("userOpenId")%>

    <input type="submit" value="Login" />  

    <% } %>

 

</asp:Content>


Теперь добавим XRDS описание нашего приложения, в котором заявим, что поддерживаем OpenID. Для этого изменим Home-контроллер:

using System;

using System.Web;

using System.Web.Mvc;

 

namespace MainSite.Controllers

{

    public class HomeController : Controller

    {

        public ActionResult Index()

        {

            // Добавим в заголовок ответа информацию о

            // расположении XRDS файла

            Response.AppendHeader

            (

                "X-XRDS-Location",

                new Uri(

                            Request.Url,

                            Response.ApplyAppPathModifier("~/Home/xrds")

                        )

                        .AbsoluteUri

            );

 

            // Главная страница.

            return View("Index");

        }

 

        public ActionResult Xrds()

        {

            // Отобразим XRDS описание.

            return View("Xrds");

        }

    }

}


И соответственно добавим само описание XRDS в виде Xrds.aspx (Обратите внимание на ContentType):

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" ContentType="application/xrds+xml" %>

 

<?xml version="1.0" encoding="UTF-8"?>

<xrds:XRDS

    xmlns:xrds="xri://$xrds"

    xmlns:openid="http://openid.net/xmlns/1.0"

    xmlns="xri://$xrd*($v*2.0)">   

    <XRD>

 

        <!-- OpenID 2.0 login service -->

        <Service priority="10">

          <Type>http://specs.openid.net/auth/2.0/signon</Type>

          <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/user/authenticate"))%></URI>

        </Service>

 

        <!-- OpenID 1.0 login service -->

        <Service priority="20">

          <Type>http://openid.net/server/1.0</Type>

          <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/user/authenticate"))%></URI>

        </Service>

 

    </XRD>

</xrds:XRDS>


Не забываем добавить в Web.config строчки:

<authentication mode="Forms">

  <forms defaultUrl="~/Home" loginUrl="~/User/Login" name="OpenIdAuth"/>

</authentication>


Если Вы заранее знаете адрес сервера, где будет располагаться Ваше ASP.NET MVC приложение, то можно указывать абсолютные пути и не вызывать лишний раз ApplyAppPathModifier, это позволит снизить нагрузку на сервер.

Шаг четвертый, заключительный. Запускаем тестовое приложение F5.



Щелкаем "Вход"



Вводим OpenID идентификатор и нажимаем "Login"



Нас направят на сайт провайдера, где нужно ввести пароль. Вводим пароль и нажимаем "Вход"



После успешной аутентификации попадаем на закрытую часть сайта.


Progg it

8 комментариев:

  1. Вы еще не написали, что с помощью OpenId можно извлекать автоматически nickname, email, дату рождения и др. данные с OpenId провайдера.

    ОтветитьУдалить
  2. @Ruslan

    Да, спасибо за замечание. Кстати, имея email пользователя можно воспользоваться сервисом gravatar-ок, для получения аватара для пользователя.

    ОтветитьУдалить
  3. было бы интересно почитать еще про OAuth+MVC

    ОтветитьУдалить
  4. @ankstoo

    Спасибо, как выпадет свободная минутка напишу. Следите за обновлениями)

    ОтветитьУдалить
  5. А за чем в примере код связанный с XRDS? Это вроде нужно только если вы хотите сделать свой сайт OpenID-провайдером. То есть для простой авторизации этого не нужно.

    ОтветитьУдалить
  6. XRDS документ нужен в консъюмере для успешного прохождения Discovery на провайдере. В нем должен храниться return_to параметр, указывающий на адресс по которому расположена Relay Party. Все это по сути нужно, так как в спецификации по OpenID сказано, что провайдер не должен возвращать успешное прохождение аутентефикации, если не удалось провести Discovery. Об этом написано здесь http://blog.nerdbank.net/2008/06/why-yahoo-says-your-openid-site.html
    и здесь
    https://groups.google.com/forum/?fromgroups=#!searchin/dotnetopenid/IsReturnUrlDiscoverable/dotnetopenid/-YCxTS7xaJs/wKUQndt6o44J
    Если это кому-то еще интересно.

    ОтветитьУдалить
  7. @SFH
    Спасибо! Я уверен, что кому-нибудь да и пригодиться.

    ОтветитьУдалить