11

ASP.NET MVC4に新しくSimpleMembershipProvider実装されたものにより、2つの人気のあるOpenIDプロバイダー(GoogleとYahoo)と3つのOAuthプロバイダー(Microsoft、Facebook、Twitter)の簡単な組み込みサポートが可能になります。

DotNetOpenAuth.AspNet.Clientsで使用するために実装されたプロバイダーSimpleMembershipProviderは、IDサービスに静的URLを使用します。つまり、すべてのユーザーが同じ既知のURLを使用してプロバイダーにアクセスします。ユーザーのOpenID識別子は、IDサービスへのアクセスに使用されるURLとは別のものです。

たとえば、GoogleのOpenIDサービスのURLはhttps://www.google.com/accounts/o8/idすべてのユーザーを対象としています。

これSimpleMembershipProviderはMVC4で機能します。この場合、MVCアプリの起動時に、IDプロバイダーのURLが既知であり、一定であり、登録されている必要があります。

問題は、他のOpenIDプロバイダーは通常、ユーザーの一意のOpenID識別子をIDサービスにアクセスするためのURLとして使用することです。

たとえば、AOLとWordPressはそれぞれhttps://openid.aol.com/{username}https://{username}.wordpress.comを使用します。

SimpleMembershipProviderを独自の実装に置き換えると、ExtendedMembershipProvider独自のプロバイダー実装をロールできますが、そのままではMVC4コントローラーでは機能しませんAccount

SimpleMembershipProviderプロバイダーがURLのユーザー名で一意の識別子を使用する場合、を使用して新しいOpenID証明書利用者をどのように実装しますか?

4

1 に答える 1

18

私は自分に合った次のソリューションを開発しました。他の人に役立つ場合に備えて共有していますが、私が見逃しているより直接的な方法または「ベストプラクティス」があるかどうかを確認したいと思います。

基本的に、キーワードを含むURLを持つOpenIdClientで初期化されるを実装する必要があります。ProviderIdentifier__username__

実行時に、プロバイダー名とユーザー名がコントローラーに渡されAccount、プロバイダークライアントが名前で選択され、認証要求がプロバイダーに送信される前に、ユーザー名が__username__キーワードに置き換えられます。


OpenIDクライアント

Microsoftが提供するDotNetOpenAuthOpenIDプロバイダークラスは、OpenIDプロバイダークラスに必要なインターフェイスDotNetOpenAuth.AspNet.Clients.OpenIdClientを実装する基本クラスを継承します。実装が簡単なGoogleプロバイダーIAuthenticationClientのソースから始めて、カスタムURLを使用してプロバイダーと連携するクラスを作成するようにカスタマイズします。GenericOpenIdClient

実行時にカスタムURLを作成するには、OpenIDユーザー名をURIフラグメントとして受け入れ、__username__URL内のすべてのインスタンスをユーザーが送信したユーザー名に置き換えます。アプリケーションの起動時にプロバイダーをURLに登録する必要があるため、ユーザー名がわかっている実行時にプロバイダーのURLを登録するだけでは不十分です。

OpenID Selectorを使用して、フォームの値をプロバイダー名とユーザー名に設定された形式で、AccountコントローラーのExternalLoginアクションにフォームを送信します。OpenId Selectorには、のすべてのインスタンスをユーザーに表示されるテキストボックスからの入力に置き換えるロジックが組み込まれています。サーバー側では、プロバイダー名をユーザー名から分割し、アプリケーションの起動時に登録されたプロバイダーから名前でプロバイダーを検索し、ユーザーが送信したユーザー名にプロパティを設定します。providerprovider;{username}{username}GenericOpenIdClient.UserName

OpenIDプロバイダーに送信する認証リクエストが作成されると、GenericOpenIdClient.UserNameプロパティがチェックされ、設定されている場合は、リクエストを送信する前にユーザー名を使用してプロバイダーのURLが再作成されます。そのためにはRequestAuthentication()、カスタムURLを使用して認証リクエストを作成するメソッドをオーバーライドする必要があります。はホスト名の有効な文字ではないため、ここ__username__の代わりにが使用されます。したがって、それらを含むURLを作成することは、それらを汎用プロバイダー識別子として登録する必要がある場合に問題になります。{username}{}

/GenericOpenIdClient.cs

namespace DotNetOpenAuth.AspNet.Clients
{
    using System;
    using System.Collections.Generic;
    using System.Web;
    using System.Xml.Linq;
    using DotNetOpenAuth.OpenId;
    using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
    using DotNetOpenAuth.OpenId.RelyingParty;

    public class GenericOpenIdClient : OpenIdClient
    {
        #region Constants and Fields

        /// <summary>
        /// The openid relying party.
        /// </summary>
        /// <remarks>
        /// Pass null as applicationStore to specify dumb mode. Create a protected field to use internally; we can't access the private base class field.
        /// </remarks>
        protected static readonly OpenIdRelyingParty RelyingParty = new OpenIdRelyingParty(applicationStore: null);

        /// <summary>
        /// The provider identifier.
        /// </summary>
        /// <remarks>
        /// Create a protected field to use internally; we can't access the private base class field.
        /// </remarks>
        protected readonly Identifier providerIdentifier;

        #endregion

        #region Constructors and Destructors

        public GenericOpenIdClient(string providerName, Identifier providerIdentifier)
            : base(providerName, providerIdentifier) 
        {
            this.providerIdentifier = providerIdentifier; // initialize our internal field as well
        }

        #endregion

        #region Public Properties

        public String UserName { get; set; }

        #endregion

        #region Protected Properties

        /// <summary>
        /// The provider Identifier with the "__username__" keyword replaced with the value of the UserName property.
        /// </summary>
        protected Identifier ProviderIdentifier
        {
            get
            {
                var customIdentifier = String.IsNullOrWhiteSpace(this.UserName) ?
                    this.providerIdentifier :
                    Identifier.Parse(HttpUtility.UrlDecode(this.providerIdentifier).Replace("__username__", this.UserName));
                return customIdentifier;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets the extra data obtained from the response message when authentication is successful.
        /// </summary>
        /// <param name="response">
        /// The response message. 
        /// </param>
        /// <returns>A dictionary of profile data; or null if no data is available.</returns>
        protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response)
        {
            FetchResponse fetchResponse = response.GetExtension<FetchResponse>();
            if (fetchResponse != null)
            {
                var extraData = new Dictionary<string, string>();
                extraData.AddItemIfNotEmpty("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email));
                extraData.AddItemIfNotEmpty("country", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country));
                extraData.AddItemIfNotEmpty("firstName", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.First));
                extraData.AddItemIfNotEmpty("lastName", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Last));

                return extraData;
            }

            return null;
        }

        public override void RequestAuthentication(HttpContextBase context, Uri returnUrl)
        {
            var realm = new Realm(returnUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped));
            IAuthenticationRequest request = RelyingParty.CreateRequest(ProviderIdentifier, realm, returnUrl);

            // give subclasses a chance to modify request message, e.g. add extension attributes, etc.
            this.OnBeforeSendingAuthenticationRequest(request);

            request.RedirectToProvider();
        }

        /// <summary>
        /// Called just before the authentication request is sent to service provider.
        /// </summary>
        /// <param name="request">
        /// The request. 
        /// </param>
        protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request)
        {
            // Attribute Exchange extensions
            var fetchRequest = new FetchRequest();
            fetchRequest.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
            fetchRequest.Attributes.AddOptional(WellKnownAttributes.Contact.HomeAddress.Country);
            fetchRequest.Attributes.AddRequired(WellKnownAttributes.Name.First);
            fetchRequest.Attributes.AddRequired(WellKnownAttributes.Name.Last);

            request.AddExtension(fetchRequest);
        }

        #endregion
    }

    /// <summary>
    /// The dictionary extensions.
    /// </summary>
    internal static class DictionaryExtensions
    {
        /// <summary>
        /// Adds the value from an XDocument with the specified element name if it's not empty.
        /// </summary>
        /// <param name="dictionary">
        /// The dictionary. 
        /// </param>
        /// <param name="document">
        /// The document. 
        /// </param>
        /// <param name="elementName">
        /// Name of the element. 
        /// </param>
        public static void AddDataIfNotEmpty(
            this Dictionary<string, string> dictionary, XDocument document, string elementName)
        {
            var element = document.Root.Element(elementName);
            if (element != null)
            {
                dictionary.AddItemIfNotEmpty(elementName, element.Value);
            }
        }

        /// <summary>
        /// Adds a key/value pair to the specified dictionary if the value is not null or empty.
        /// </summary>
        /// <param name="dictionary">
        /// The dictionary. 
        /// </param>
        /// <param name="key">
        /// The key. 
        /// </param>
        /// <param name="value">
        /// The value. 
        /// </param>
        public static void AddItemIfNotEmpty(this IDictionary<string, string> dictionary, string key, string value)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            if (!string.IsNullOrEmpty(value))
            {
                dictionary[key] = value;
            }
        }
    }
}

Microsoftが提供する新しいDotNetOpenAuthクラスに組み込まれているプロバイダーを登録するには、既存のMicrosoft、Facebook、Twitter、およびGoogleプロバイダーのコメントを解除し、組み込みのYahooプロバイダーを登録するための呼び出しを追加します。実装しようとしているOpenIDプロバイダーにはキーは必要ありませんが、使用する場合はOAuthプロバイダー(Microsoft、Facebook、Twitter)からキーを取得する必要があります。OpenID Selectorパッケージで利用可能な残りのプロバイダーは、お好みに合わせて追加できます。

/App_Start/AuthConfig.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DotNetOpenAuth.AspNet.Clients;
using DotNetOpenAuth.OpenId.RelyingParty;
using Microsoft.Web.WebPages.OAuth;
using Mvc4ApplicationOpenAuth.Models;

namespace Mvc4ApplicationOpenAuth
{
    public static class AuthConfig
    {
        public static void RegisterAuth()
        {
            // To let users of this site log in using their accounts from other sites such as Microsoft, Facebook, and Twitter,
            // you must update this site. For more information visit http://go.microsoft.com/fwlink/?LinkID=252166

            //OAuthWebSecurity.RegisterMicrosoftClient(
            //    clientId: "",
            //    clientSecret: "");

            //OAuthWebSecurity.RegisterTwitterClient(
            //    consumerKey: "",
            //    consumerSecret: "");

            //OAuthWebSecurity.RegisterFacebookClient(
            //    appId: "",
            //    appSecret: "");

            OAuthWebSecurity.RegisterGoogleClient();
            OAuthWebSecurity.RegisterYahooClient();
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("Aol", "https://openid.aol.com/__username__"), "Aol", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("LiveJournal", "https://__username__.livejournal.com/"), "LiveJournal", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("WordPress", "https://__username__.wordpress.com/"), "WordPress", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("Blogger", "https://__username__.blogspot.com/"), "Blogger", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("VeriSign", "https://__username__.pip.verisignlabs.com/"), "VeriSign", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("ClaimID", "https://claimid.com/__username__"), "ClaimID", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("ClickPass", "https://clickpass.com/public/__username__"), "ClickPass", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("Google Profile", "https://www.google.com/profiles/__username__"), "Google Profile", new Dictionary());
            OAuthWebSecurity.RegisterClient(new GenericOpenIdClient("MyOpenID", "https://__username__.myopenid.com/"), "MyOpenID", new Dictionary());
        }
    }
}

Account最後に、OpenID SelectorによってコントローラーのExternalLoginアクションに送信されたプロバイダーフォームの値を解析して、「;」を確認する必要があります。ユーザー名が存在することを示す区切り文字。その場合、プロバイダー名とユーザー名を解析します。

/Controllers/AccountController.cs

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
    if (provider.Contains(';'))
    {
        string[] providerParts = provider.Split(';');
        if (providerParts.Length == 2)
        {
            AuthenticationClientData clientData;
            if (OAuthWebSecurity.TryGetOAuthClientData(providerParts[0], out clientData))
            {
                var genericClient = clientData.AuthenticationClient as GenericOpenIdClient;
                if (genericClient != null)
                {
                    provider = providerParts[0];
                    genericClient.UserName = providerParts[1];
                }
            }
        }
    }

    return new ExternalLoginResult(provider, Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
}

UI

UIの実装は、オープンソースのOpenIDSelectorを使用するとはるかに簡単になります。OpenID SelectorをダウンロードOAuthWebSecurityし、クラスで使用できるようにカスタマイズします。

  1. 次の場所でWebアプリに新しいopenidフォルダーを作成します。/Content/openid
  2. 、、、、およびフォルダーを ダウンロードからフォルダーcssにコピーしてからimages、プロジェクトにファイルを含めます。 images.largeimages.smallopenid-selector/Content/openid
  3. openid-selectorダウンロードのjsフォルダーから、コピーopenid-jquery.jsopenid-en.jsてWebアプリの/Scriptsフォルダーに移動し、プロジェクトのファイルを含めます。
  4. ファイルを開いてopenid-en.jsカスタマイズし、プロバイダーのURLがファイルに追加するプロバイダー名になるようにしAuthConfig.csます。カスタムURLを持つプロバイダーの場合は、次の形式を使用しますProvider;{username}

/Scripts/openid-en.js

var providers_large = {
    google : {
        name : 'Google',
        url : 'Google'
    },
    facebook : {
        name : 'Facebook',
        url : 'Facebook',
    },
    twitter: {
        name: 'Twitter',
        url: 'Twitter'
    },
    microsoft : {
        name : 'Microsoft',
        url : 'Microsoft'
    },
    yahoo : {
        name : 'Yahoo',
        url : 'Yahoo'
    },
    aol : {
        name : 'Aol',
        label : 'Enter your Aol screenname.',
        url : 'Aol;{username}'
    }
};

var providers_small = {
    livejournal: {
        name : 'LiveJournal',
        label : 'Enter your Livejournal username.',
        url: 'LiveJournal;{username}'
    },
    wordpress : {
        name : 'WordPress',
        label : 'Enter your WordPress.com username.',
        url: 'WordPress;{username}'
    },
    blogger : {
        name : 'Blogger',
        label : 'Your Blogger account',
        url: 'Blogger;{username}'
    },
    verisign : {
        name : 'VeriSign',
        label : 'Your VeriSign username',
        url: 'VeriSign;{username}'
    },
    claimid : {
        name : 'ClaimID',
        label : 'Your ClaimID username',
        url: 'ClaimID;{username}'
    },
    clickpass : {
        name : 'ClickPass',
        label : 'Enter your ClickPass username',
        url: 'ClickPass;{username}'
    },
    google_profile : {
        name : 'Google Profile',
        label : 'Enter your Google Profile username',
        url: 'Google Profile;{username}'
    },
    myopenid: {
        name: 'MyOpenID',
        label: 'Enter your MyOpenID username.',
        url: 'MyOpenID;{username}'
    }
};

openid.locale = 'en';
openid.sprite = 'en'; // reused in german& japan localization
openid.demo_text = 'In client demo mode. Normally would have submitted OpenID:';
openid.signin_text = 'Log in';
openid.image_title = 'Log in with {provider}';
openid.no_sprite = true;
openid.img_path = '/Content/openid/images/';

OpenID SelectorにはMicrosoftまたはTwitterの画像が付属していないため、お気に入りのMicrosoftおよびTwitter(白地に青)のロゴをダウンロードし、100x60ピクセルのGIFに変換して、/Content/openid/images.largeフォルダーにドロップします。README.txt個別の画像の代わりに単一のスプライト画像を使用する場合は、OpenIDSelectorファイルの手順をお読みください。スプライトを使用する場合は設定openid.no_sprite = false;します。openid-en.js

JSファイルとCSSファイルを新しいバンドルとして登録します。/App_Start/BundleConfig.cs次のスクリプトとスタイルのバンドルを開いて、RegisterBundles()メソッドに追加します。

/App_Start/BundleConfig.cs

bundles.Add(new ScriptBundle("~/bundles/openid").Include(
    "~/Scripts/openid-jquery.js",
    "~/Scripts/openid-en.js"));

bundles.Add(new StyleBundle("~/Content/css/openid").Include("~/Content/openid/css/openid-shadow.css"));

私はOpenIDSelectorの「シャドウ」スタイルを好むので、openid-shadow.cssCSSファイルのみを使用することを選択し、MVC4ログインテンプレートで機能するように次のクラスをカスタマイズしました。

/Content/css/openid/openid-shadow.css

/*#openid_form {
    width: 590px;
}*/

#openid_highlight {
    padding: 0px;
    background-color: #FFFCC9;
    float: left;
    border-radius: 5px; 
    -moz-border-radius: 5px;
    -webkit-border-radius: 5px;
}

.openid_large_btn {
    width: 100px;
    height: 60px;
/* fix for IE 6 only: http://en.wikipedia.org/wiki/CSS_filter#Underscore_hack */
    _width: 104px;
    _height: 64px;

    border: 2px solid #DDD;
    border-right: 2px solid #ccc;
    border-bottom: 2px solid #ccc;
    margin: 3px;
    padding: 3px;
    float: left;
    border-radius: 5px; 
    -moz-border-radius: 5px;
    -webkit-border-radius: 5px;
    box-shadow: 2px 2px 4px #ddd;
    -moz-box-shadow: 2px 2px 4px #ddd;
    -webkit-box-shadow: 2px 2px 4px #ddd;
}

.openid_large_btn:hover {
    margin: 4px 3px 3px 6px;
    padding: 2px 3px 3px 0px;
    border: 2px solid #999;
    box-shadow: none;
    -moz-box-shadow: none;
    -webkit-box-shadow: none;
}

ページの<head>タグにCSSスクリプトを追加する一般的な場所を作成するheadには、タグの下部にセクションを追加し<head>ます。

/Views/Shared/_Layout.cshtml

<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title - My ASP.NET MVC Application</title>
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    @RenderSection("head", false)
</head>

次に、ファイルで、以前に登録したOpenIDバンドルをページ下部の適切なセクションに追加して/Views/Account/Login.cshtml、ビューをカスタマイズします。Login

/Views/Account/Login.cshtml

<section class="social" id="socialLoginForm">
    @Html.Action("ExternalLoginsList", new { ReturnUrl = ViewBag.ReturnUrl })
</section>

@section Head {        
    @Styles.Render("~/Content/css/openid")
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/bundles/openid")
    <script type="text/javascript">
        $(function () {
            openid.init('provider');
        });
    </script>
}

UIの最後の要素には、デフォルトのExternalLoginフォームをOpenIDSelectorフォームに置き換えることが含まれます。

/Views/Account/_ExternalLoginsListPartial.cshtml

using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { id = "openid_form" }))
{
    @Html.AntiForgeryToken()
    <input type="hidden" name="action" value="verify" />

    <h2>Use another service to log in.</h2>
    <br />
    <fieldset id="socialLoginList">
        <legend></legend>

        <div id="openid_choice">
            <div id="openid_btns"></div>
        </div>
        <div id="openid_input_area">
            <input id="provider" name="provider" type="text" value="" />
            <input id="openid_submit" type="submit" value="Log in"/>
        </div>
        <noscript>
            <p>OpenID is service that allows you to log-on to many different websites using a single indentity. Find out <a href="http://openid.net/what/">more about OpenID</a> and <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p>
        </noscript>
    </fieldset>
}
于 2012-10-14T01:04:55.423 に答える