tag route page net for data asp all asp.net-mvc asp.net-mvc-4 openid dotnetopenauth asp.net-4.5

route - Cómo utilizar proveedores de OpenID con URL de identificador único en ASP.NET MVC4



tag helpers asp net core (1)

Desarrollé la siguiente solución que me funciona, y la estoy compartiendo en caso de que ayude a otros, pero realmente me gustaría ver si me falta un método más directo o una "mejor práctica".

Básicamente, debe implementar un OpenIdClient que se inicialice con un ProviderIdentifier que tenga una URL que contenga la palabra clave __username__ .

En tiempo de ejecución, el nombre del proveedor y el nombre del usuario pasan al controlador de la Account , donde el cliente proveedor se selecciona por nombre y el nombre de usuario se sustituye por la palabra clave __username__ antes de que se envíe la solicitud de autenticación al proveedor.

El cliente OpenID

Las clases de proveedor OpenID de DotNetOpenAuth contribuidas por Microsoft heredan la clase base DotNetOpenAuth.AspNet.Clients.OpenIdClient , que implementa la interfaz IAuthenticationClient requerida para las clases de proveedores de OpenID. Comenzando con la fuente del proveedor de Google porque tiene una implementación directa, personalícela para crear una clase GenericOpenIdClient que funcione con proveedores que usen URL personalizadas.

Para crear la URL personalizada en tiempo de ejecución, aceptaremos el nombre de usuario de OpenID como un fragmento de URI y reemplazaremos todas las instancias de __username__ en la URL con el nombre de usuario enviado por el usuario. Los proveedores deben estar registrados con las URL durante el inicio de la aplicación, por lo que no podemos simplemente registrar una URL de proveedor en tiempo de ejecución cuando se conoce el nombre de usuario.

Utilizaremos OpenID Selector para enviar un formulario a la acción ExternalLogin nuestro controlador de Account con el valor del formulario del provider establecido en el nombre del proveedor y nombre de usuario en el provider;{username} formato provider;{username} . OpenId Selector tiene lógica incorporada para sustituir todas las instancias de {username} con la entrada de un cuadro de texto presentado al usuario. En el lado del servidor, dividiremos el nombre del proveedor del nombre de usuario, buscaremos el proveedor por nombre de aquellos registrados al inicio de la aplicación y estableceremos la propiedad GenericOpenIdClient.UserName en el nombre de usuario enviado por el usuario.

Cuando se crea la solicitud de autenticación para enviarla al proveedor de OpenID, comprobaremos la propiedad GenericOpenIdClient.UserName , y si se establece, GenericOpenIdClient.UserName la URL del proveedor con el nombre de usuario antes de enviar la solicitud. Para hacerlo, debemos anular el método RequestAuthentication() para crear la solicitud de autenticación con nuestra URL personalizada. __username__ se usa en lugar de {username} aquí porque { y } no son caracteres válidos para un nombre de host, por lo que la creación de URL que los incluya se vuelve problemática cuando necesitamos registrarlos como identificadores de proveedores genéricos.

/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; } } } }

Para registrar los proveedores incorporados en las nuevas clases DotNetOpenAuth contribuidas por Microsoft, quite el comentario de los proveedores existentes de Microsoft, Facebook, Twitter y Google, y agregue una llamada para registrar el proveedor Yahoo integrado. Los proveedores de OpenID que estamos a punto de implementar no necesitan claves, pero deberá obtener claves de los proveedores de OAuth (Microsoft, Facebook y Twitter) si desea usarlas. El resto de los proveedores disponibles en el paquete OpenID Selector se pueden agregar a su gusto.

/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()); } } }

Por último, necesitamos analizar el valor del formulario del proveedor enviado a la acción ExternalLogin del controlador de la Account por OpenID Selector para verificar el delimitador ";" que indica que hay un nombre de usuario presente. Si es así, analizamos el nombre del proveedor y el nombre de usuario.

/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 })); }

La interfaz de usuario

La implementación de UI se hace mucho más fácil con OpenID Selector de fuente abierta. Descargue OpenID Selector y personalícelo para utilizarlo con las clases OAuthWebSecurity .

  1. Cree una nueva carpeta openid en su aplicación web en: /Content/openid
  2. Copie las carpetas css , images , images.large e openid-selector descarga del openid-selector /Content/openid carpeta /Content/openid , luego incluya los archivos en su proyecto.
  3. Desde la carpeta js la descarga openid-jquery.js , copie openid-jquery.js y openid-en.js en la carpeta /Scripts su aplicación web, luego incluya los archivos de su proyecto.
  4. Abra el archivo openid-en.js y personalícelo para que las URL del proveedor sean los nombres de proveedores que agregará en su archivo AuthConfig.cs . Para proveedores con URL personalizadas, use el formato 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 no incluye imágenes para Microsoft o Twitter, así que descargue sus logotipos favoritos de Microsoft y Twitter (azul sobre blanco), /Content/openid/images.large a GIF a 100x60 píxeles y luego colóquelos en la carpeta /Content/openid/images.large . Lea las instrucciones en el archivo OpenID Selector README.txt si desea usar una sola imagen de sprite en lugar de imágenes separadas. Establecer openid.no_sprite = false; en openid-en.js si usa el sprite.

Registre los archivos JS y CSS como un nuevo paquete. Abra /App_Start/BundleConfig.cs y agregue los siguientes conjuntos de scripts y estilos en el método 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"));

Prefiero el estilo de "sombra" del Selector de OpenID, así que openid-shadow.css usar solo el archivo CSS openid-shadow.css y personalicé las siguientes clases para trabajar en la plantilla de inicio de sesión de 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; }

Para crear un lugar genérico para agregar scripts CSS a la etiqueta <head> la página, agregue una sección de head en la parte inferior de la etiqueta <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>

Luego, en el archivo /Views/Account/Login.cshtml , personalice la vista de Login agregando los paquetes de OpenID que registramos previamente a las secciones correspondientes en la parte inferior de la página.

/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> }

El último elemento de la interfaz de usuario implica reemplazar el formulario predeterminado de ExternalLogin con el formulario de selección de OpenID.

/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> }

El nuevo SimpleMembershipProvider implementado en ASP.NET MVC4 permite un soporte fácil e integrado para dos populares proveedores de OpenID (Google y Yahoo) y tres proveedores de OAuth (Microsoft, Facebook, Twitter).

Los proveedores implementados en DotNetOpenAuth.AspNet.Clients para utilizar con SimpleMembershipProvider usan URL estáticas para sus servicios de identidad, es decir, todos los usuarios usan la misma URL conocida para acceder al proveedor. Los identificadores OpenID de los usuarios son independientes de la URL utilizada para acceder al servicio de identidad.

Por ejemplo, la URL del servicio OpenID de Google es https://www.google.com/accounts/o8/id para todos los usuarios.

Esto funciona con SimpleMembershipProvider en MVC4, donde la URL del proveedor de identidad debe ser conocida, constante y registrada en el momento en que se inicia su aplicación MVC.

El problema es que otros proveedores de OpenID comúnmente usan el identificador único de OpenID del usuario como URL para acceder al servicio de identidad.

Por ejemplo, AOL y WordPress usan https://openid.aol.com/{username} y https://{username}.wordpress.com , respectivamente.

Si reemplaza SimpleMembershipProvider con su propia implementación de un ExtendedMembershipProvider , puede implementar sus propias implementaciones de proveedor, pero luego no funciona con el controlador de Account MVC4 listo para usar.

¿Cómo se implementa una nueva Parte que SimpleMembershipProvider OpenID utilizando SimpleMembershipProvider , cuando el proveedor usa identificadores únicos con el nombre de usuario en la URL?