Friday, January 18, 2013

Customer Portal – switching authentication from LiveId to Forms

I have got a requirement from one of customers create customer portal. It was my first experience with portals so I decided not to build custom website but to install and configure Customer Portal. I would not write how to install and configure this solution – it is well described. Once it was configured I had a phone conference with customer and first requirement and main challenge for me was to remove LiveId authentication and replace it with other authentication.
I googled and found following article that described how to solve the same issue for CRM 4.0 that was written by MVP fellow Dylan Haskins (thanks a lot, I owe you a beer). Here are steps to replace LiveId authentication with Forms authentication (I assume that you’ve already configured Customer Portal):

1. Open web.config file and find following text:
<authentication mode="None"/>

Replace it with:

<authentication mode="Forms">
  <forms name=".ASPXAUTH" loginUrl="login"
   defaultUrl="default.aspx" protection="All" timeout="30" path="/"
   requireSSL="false" slidingExpiration="true"
   cookieless="UseDeviceProfile" domain=""
   enableCrossAppRedirects="false">
  </forms>
</authentication>

2. Open Pages/login.aspx file and replace content of page with following markup:

<%@ Page Language="C#" MasterPageFile="~/MasterPages/Default.master" AutoEventWireup="True" CodeBehind="Login.aspx.cs" Inherits="Site.Pages.Login" %>

<asp:Content ID="Content1" ContentPlaceHolderID="ContentBottom" runat="server">
        <asp:Login
            ID="Login1" runat="server" onauthenticate="Login1_Authenticate">
        </asp:Login>
</asp:Content>

3. Open Pages/login.aspx.cs file and replace whole code with following:

using System;
using System.Web.Security;
using Xrm;
using System.Linq;
using Microsoft.Xrm.Portal;
using Microsoft.Xrm.Portal.Configuration;
using Microsoft.Xrm.Portal.Access;
using Microsoft.Xrm.Portal.Cms;
using Microsoft.Xrm.Portal.Core;

namespace Site.Pages
{
    public partial class Login : PortalPage
    {

        private Contact _loginContact;
        protected Contact LoginContact
        {
            get
            {
                if (_loginContact != null)
                {
                    return _loginContact;
                }

                _loginContact = XrmContext.ContactSet
                        .FirstOrDefault(c => c.Adx_username == Login1.UserName
                            && (c.Adx_password == Login1.Password));

                XrmContext.Detach(_loginContact);
                return _loginContact;
            }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if ((User != null && User.Identity != null) && User.Identity.IsAuthenticated)
            {
                var redirectUrl = !string.IsNullOrEmpty(Request.QueryString["ReturnUrl"])
                    ? Request["ReturnUrl"]
                    : !string.IsNullOrEmpty(Request.QueryString["URL"])
                        ? Request["URL"]
                        : "/";

                Response.Redirect(redirectUrl);
            }
        }

        protected void Login1_Authenticate(object sender, System.Web.UI.WebControls.AuthenticateEventArgs e)
        {
            if (LoginContact == null)
            {
                e.Authenticated = false;
            }
            else
            {
                if (LoginContact.Adx_username == Login1.UserName)
                {
                    if (LoginContact.Adx_changepasswordatnextlogon.Value)
                    {
                        var portal = PortalCrmConfigurationManager.CreatePortalContext();
                        var website = (Adx_website)portal.Website;
                        var page = (Adx_webpage)portal.ServiceContext.GetPageBySiteMarkerName(portal.Website, "ChangePassword");

                        string redirectURL = page.Adx_PartialUrl + "?UserName=" + Server.UrlEncode(Login1.UserName) + 
                            "&Password=" + Server.UrlEncode(Login1.Password);
                        Response.Redirect(redirectURL);
                    }
                    else
                    {
                        LoginContact.Adx_LastSuccessfulLogon = DateTime.Now;

                        XrmContext.Attach(LoginContact);
                        XrmContext.UpdateObject(LoginContact);
                        XrmContext.SaveChanges();
                        XrmContext.Detach(LoginContact);

                        e.Authenticated = true;
                        FormsAuthentication.RedirectFromLoginPage(Login1.UserName, true);
                    }
                }
                else
                {
                    e.Authenticated = false;
                }
            }

        }
    }
}

4. In Page folder create Logout.aspx page. Open codebehind – Logout.aspx.cs and put following code inside:

using System;
using System.Linq;
using Microsoft.Xrm.Portal.Access;
using Microsoft.Xrm.Portal.Cms;
using Microsoft.Xrm.Portal.Core;
using Microsoft.Xrm.Portal.Web;
using Xrm;
using System.Web.UI.WebControls;
using Site.Library;
using System.Web.Security;
using Microsoft.Xrm.Portal.Configuration;

namespace Site.Pages
{
    public partial class Logout : PortalPage
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            FormsAuthentication.SignOut();

            var portal = PortalCrmConfigurationManager.CreatePortalContext();
            var website = (Adx_website)portal.Website;
            var page = (Adx_webpage)portal.ServiceContext.GetPageBySiteMarkerName(portal.Website, "Home");
            Response.Redirect(page.Adx_PartialUrl);
        }
    }
}

5. Open MasterPages/Default.master and find following markup:

<asp:LinkButton ID="LogoutLink" runat="server" Text='<%$ Snippet: links/logout, Logout %>' OnClick="LogoutLink_Click"/>

Replace it with following markup:

<asp:HyperLink ID="LogoutLink" runat="server" Text='<%$ Snippet: links/logout, Logout %>' NavigateUrl="/Pages/logout.aspx"/>

6. Clean and build your website.

7. For test purposes create a contact in CRM:


8. Open Customer Portal and try to log in:





That’s it and in case you are too lazy to do provided steps you can find source code of a project here:

23 comments:

  1. Честно говоря, недолюбливаю Customer Portal с момента его появления, возможно, что сейчас его сильно переписали.

    Хочу несколько дополнить приведённое решение.

    Возможно, что вместо

    var redirectUrl = !string.IsNullOrEmpty(Request.QueryString["ReturnUrl"])
    ? Request["ReturnUrl"]
    : !string.IsNullOrEmpty(Request.QueryString["URL"])
    ? Request["URL"]
    : "/";

    можно использовать:

    string targetPath = FormsAuthentication.GetRedirectUrl(String.Empty, false);

    можно также использовать такой метод для редиректа на ReturnUrl (не понадобиться даже отдельно извлекать ReturnUrl):

    FormsAuthentication.RedirectFromLoginPage(UserName, false);


    Не очень хорошо передавать логин и пароль в открытом виде в URL'е. Это делается тут:

    string redirectURL = page.Adx_PartialUrl + "?UserName=" + Server.UrlEncode(Login1.UserName) +
    "&Password=" + Server.UrlEncode(Login1.Password);
    Response.Redirect(redirectURL);

    Тогда уж надо всё взаимодействие с сервером пускать по TLS-каналу.

    Также является плохим тоном хранение пароля в открытом виде. Как минимум его надо хэшировать (ещё лучше вместе с каким-то другим секретным значением, например, так: http://crrm.ru/articles/2010/03/secure-pass-storing-in-mscrm).

    В идеале, я бы порекомендовал написать собственный Membership-провайдер (а если требуется, то и Role-провайдер). Это не очень сложно, но позволяет использовать всю мощь провайдеров в ASP.NET (быстрое встраивание, встроенный механизм восстановления паролей).

    ReplyDelete
    Replies
    1. Переписать можно всё ;) Вопрос в необходимости и в том заплатят за это или нет. Насчёт хранения пароля в открытом виде не спорю, безспорно лучше использовать хеш вместо самого пароля, но на это опять таки необходимо время и переработка кода и не мне вам объяснять то, что донести новые сроки и стоимость до бизнеса зачастую является тяжёлой, кропотливой, а зачастую и напрасной задачей.

      Delete
  2. HI,
    I have followed your blog and deployed the customer portal. While giving user name and password in the login page it is again redirecting to the same page and unable to access other pages. But it successfully authenticated and I can see last logon time in the CRM contact page.

    But it is not redirecting to other pages. Please help us to get the solution in this issue.

    ReplyDelete
    Replies
    1. Hello,

      Not sure what's wrong. Portal works fine for my customer.

      Delete
  3. Thank you very much for sharing this post

    ReplyDelete
    Replies
    1. Hi ,

      As per blog i'm followed all the points , now the portal is working fine . i have some doubts

      1.I need to replace contoso logo .
      2.when i'm clicking on some links it shows the messages "like You do not have sufficient permissions to view this page"
      3.i can't able to create a invition code in crm

      cheers
      Dhamodharan

      Delete
    2. Hi ,

      i got the solution for logo replacement .

      Cheers
      Dhamodharan

      Delete
    3. Hello,

      Glad that you've solved your issue!

      Kind regards,
      Andrii.

      Delete
  4. Hi,I have followed your instructions and successfully deployed the customer portal.
    But I've the problem, that there is no password field in my contact entity (http://4.bp.blogspot.com/-5zZo7gBHWT4/UPmZ5_NuriI/AAAAAAAABp0/xdy7KdUWRXw/s1600/1.png).
    Have you any suggestion for me, to solve this problem?

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Hi, i managed now to add the missing field, but I still not able to Login.
      The webpage gives out this message:
      [NullReferenceException: Object reference not set to an instance of an object.]
      Site.Pages.Login.Login1_Authenticate(Object sender, AuthenticateEventArgs e) in D:\Alle\CustomerPortal\Web\Pages\Login.aspx.cs:65
      System.Web.UI.WebControls.Login.AttemptLogin() +159
      System.Web.UI.WebControls.Login.OnBubbleEvent(Object source, EventArgs e) +90
      System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +51
      System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3553

      Can you help me? :(

      Delete
    3. Hello Hendrik,

      The best way as for me is to attach with VS to your customer portal put breakpoint and get an answer why solution doesn't work.

      Kind regards,
      Andrii.

      Delete
    4. Hello Andrii,

      unfortunately VS displays no error or something else.
      I used the portal files from your ZIP-archive, but when trying to log in, I get the error.
      I hope you can help me - I'm pretty frustrated because I did not find the error :(

      Kind regards,
      Hendrik

      Delete
  5. Hello Hendrik,

    VS can detect compile-time errors but not run-time. To solve your issue you will have to using VS attach to IIS process (w3wp.exe) and put breakpoint to method Login1_Authenticate. Then you should try to authenticate and breakpoint should be reached. Then you will have to go line by line with debugger to find the reason of error you are getting.

    Kind regards,
    Andrii.

    ReplyDelete
  6. Hi !

    I'm trying to install my first Dynamics install but i've got a ridiculous problem : How to build the custom package to impot it into Dynamics ?

    Thanks for your help

    ReplyDelete
    Replies
    1. Hello,

      Not sure what do you want to do... Can you please explain what do you want to achieve?

      Kind regards,
      Andrii.

      Delete
    2. Hi !
      Thanks for your help.

      I'm a rookie in Dynamics CRM 2011 and just use the system as simple as possible, on-premise version (we use Action Pack licences).
      I try to install the Customer Portal but i need that the authentication to use the "contacts" instead of on Windows LiveID or AD.
      i import the Customer Portal solution in my Dynamics Organisation and modifiy the web files with your instructions.
      Now i try to activate this web site by use the "websitecopy" application, the copy is ok but i don't know how to access the website...

      I think i've got this problems:
      - i think the installation of the Customer Portal solution in Dynamis must be made AFTER rebuild the package with your modifications, but i don't know how
      - i don't now what i must do to deploy correctly the customer portal website
      - i really need your help :)

      I hope that my problems are clearly explain...

      Thanks in advance.

      Delete
    3. Hello, I got your issue.

      You should activate your portal using websitecopy - this will create required records in CRM.
      After you will have to deploy to your IIS website files. Check following thread - http://social.microsoft.com/Forums/en-US/crmlabs/thread/43836cf2-8ab1-459c-a2f9-e753a674d0eb

      Kind regards,
      Andrii.

      Delete
  7. Thanks for your help ! You've got it !

    The wesite correctly appear in Internet Explorer but now; i got a login problem :


    Server Error in '/' Application.
    --------------------------------------------------------------------------------


    Value cannot be null.
    Parameter name: entity
    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.ArgumentNullException: Value cannot be null.
    Parameter name: entity

    Source Error:


    An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

    Stack Trace:



    [ArgumentNullException: Value cannot be null.
    Parameter name: entity]
    Microsoft.Xrm.Sdk.Client.OrganizationServiceContext.Detach(Entity entity) +91
    Site.Pages.Login.get_LoginContact() in C:\Projects\Poltev Sergey\CustomerPortal\Web\Pages\Login.aspx.cs:30
    Site.Pages.Login.Login1_Authenticate(Object sender, AuthenticateEventArgs e) in C:\Projects\Poltev Sergey\CustomerPortal\Web\Pages\Login.aspx.cs:51
    System.Web.UI.WebControls.Login.AttemptLogin() +166
    System.Web.UI.WebControls.Login.OnBubbleEvent(Object source, EventArgs e) +93
    System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +52
    System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3707


    A last idea for this last problem ? :)

    Thank you very much
    Regards

    ReplyDelete
  8. Sorry, don't take care about my last post, i use the wrong sources for the website.
    i'm trying with the good ones...

    Best Regards

    ReplyDelete
  9. It seems that you've found error in the code ;) I'm not ideal and I already fixed that issue on the customer's portal. I will update portal and provide updated version.

    Kind regards,
    Andrii.

    ReplyDelete
  10. OK... you know what ?... i've got a problem yet...hum... sorry.

    When i try to access the login page, i've got this message :


    Server Error in '/' Application.
    --------------------------------------------------------------------------------


    Compilation Error
    Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.

    Compiler Error Message: CS1061: 'ASP.pages_login_aspx' does not contain a definition for 'Login1_Authenticate' and no extension method 'Login1_Authenticate' accepting a first argument of type 'ASP.pages_login_aspx' could be found (are you missing a using directive or an assembly reference?)

    Source Error:




    Line 2:
    Line 3:
    Line 4:
    Line 6:


    Source File: c:\Web\Pages\Login.aspx Line: 4

    Any Idea ?

    Thanks a lot
    Best Regards

    ReplyDelete