Saturday, May 14, 2011

Tutorial–ASP.Net MVC 3, claim-based architecture and Windows Azure ACS (5)

Finally we are ready to implement our final scenario. Again, the goal is to allow users to logon to our ASP.Net MVC 3 service site using either a local id, or any supported ids (Google, Windows Live, and Yahoo! to start with). And our service site should be shield from most of the complexity of federating with multiple identity providers.imageThis part involves quite some changes in the STS code, as we are extending the STS project to serve as both a FP and a STS. However, the service project remains intact. There’s even no configuration updates. As far as the service project concerns, it’s talking to a trusted FP, and it doesn’t need to worry about anything else.

Part I: Relocate local STS.

In this part we’ll move the Default.aspx page to a sub-folder, and set up the sub-foder to use form authentication to preserve local STS functionality.

  1. In STS project, create a new “STS” folder.
  2. Move Default.aspx page into this folder and remove the file to TokenPoster.aspx. Renaming an ASP.Net page is a little tricky, you need to manually verify if all class names in both code-behind and aspx page are updated correctly. And you may have to reload the project to eliminate some confusions of Visual Studio.
  3. Add a new Default.aspx page to the root folder of STS project. This page displays two options for user to pick from a local STS or ACS.
  4. Add two buttons to the page:
    <asp:LinkButton Text="Use Local STS" runat="server" ID="btnLocalSTS" 
                onclick="btnLocalSTS_Click" /><br />
    <asp:LinkButton Text="Use Azure ACS" runat="server" ID="btnACS" 
                onclick="btnACS_Click" />
  5. In code-behind, put in the following code:
    protected void Page_Load(object sender, EventArgs e)
    {
         if (!string.IsNullOrEmpty(Request.QueryString[WSFederationConstants.Parameters.Action]))
            ViewState["F-Action"] = Request.QueryString[WSFederationConstants.Parameters.Action];
         if (!string.IsNullOrEmpty(Request.QueryString["wtrealm"]))
            ViewState["F-wtrealm"] = Request.QueryString["wtrealm"];
    }
    protected void btnLocalSTS_Click(object sender, EventArgs e)
    {
        try
        {
            Response.Redirect("STS/TokenPoster.aspx?wa=" + ViewState["F-Action"].ToString() + "&wtrealm=" + ViewState["F-wtrealm"].ToString().Replace(":", "%3A").Replace("/", "%2F"));
        }
        catch (Exception exception)
        {
            throw new Exception("An unexpected error occurred when processing the request. See inner exception for details.", exception);
        }
    }
  6. Modify web.config to put STS folder under form authentication protection:
    <configuration>
      ......
      <location path="STS">
        <system.web>
          <authorization>
            <deny users="?"/>
          </authorization>
        </system.web>
      </location>
      <system.web>
        ......
        <authentication mode="Forms">
          <forms loginUrl="Login.aspx"/>
        </authentication>
        ......
  7. Also, we need to disable default FAM redirecting by setting passiveRedirectEnabled="false"

After these modifications you should be able to use the local STS to logon. The code in step 6 is straightforward – it saves logon request when page is loaded, and it forwards the request to TokenPoster.aspx (former Default.aspx) when user clicks on the local STS link in the new Default.aspx page.

Part II: Re-enabling ACS.

In step 7 above we disabled FAM default redirecting. To put FAM back to work again, we’ll need to trigger the authentication process in the code. Modify code-behind of Default.aspx page:

protected void Page_Load(object sender, EventArgs e)
{
   if (!string.IsNullOrEmpty(Request.QueryString[WSFederationConstants.Parameters.Action]))
        ViewState["F-Action"] = Request.QueryString[WSFederationConstants.Parameters.Action];
    if (!string.IsNullOrEmpty(Request.QueryString["wtrealm"]))
        ViewState["F-wtrealm"] = Request.QueryString["wtrealm"];
        
    var fam = FederatedAuthentication.WSFederationAuthenticationModule;
    if (fam.CanReadSignInResponse(Request, true))
    {
        string url = fam.GetSignInResponseMessage(Request).Context;
        SignInRequestMessage requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri(new Uri(url + "?wa=wsignin1.0&wtrealm=" + url.Replace(":", "%3A").Replace("/", "%2F");));
        if (User != null && User.Identity != null && User.Identity.IsAuthenticated)
        {
            SecurityTokenService sts = new CustomSecurityTokenService(CustomSecurityTokenServiceConfiguration.Current);
            SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest(requestMessage, User, sts);
            FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse(responseMessage, Response);
        }
    }
}

protected void btnACS_Click(object sender, EventArgs e)
{
    var fam = FederatedAuthentication.WSFederationAuthenticationModule;
    var signIn = new SignInRequestMessage(new Uri(fam.Issuer), "http://localhost:4011/ZerodistWeb_STS/");
    signIn.Context = ViewState["F-wtrealm"].ToString();
    Response.Redirect(signIn.WriteQueryString());
}

When ACS button is clicked, a sign in request is generated and forwarded to ACS. After authentication, ACS redirects back to the Default.aspx page (as configured in ACS). Then, in the Page_Load event, we examine if we can read a response message. If so, we’ll use our STS to issue a security token back to the MVC application.

Part III: Test run.

  1. Compile and launch.image
  2. Type in http://127.0.0.1:81/Portal in address bar.
  3. You’ll see the new selection page:image
  4. Click on “Use Azure ACS” link. You’ll be redirected to another IDP selection page:image
  5. Pick an IDP and log in. You’ll be redirected back to Portal page with granted access:image
  6. You can repeat the steps to try out Admin page with different IDPs. It should all work fine. Optionally you can also change the STS to grant Admin role to more user names (currently we only grant Admin role to user with name “Admin”).

What’s next?

Congratulations! You’ve made this far. That’s something, isn’t?

However, our quest is far from over. We still need to enable HTTPS, to enable token encryption, to extend STS to do actual authentication/authorization, to deploy everything on to Azure platform … so, keep tuned!

7 comments:

  1. Hey wanted to ask a question ,Which one you think is better in respect of the implementation PHP or asp.net. ?
    thanks in advance.

    .net obfuscator

    ReplyDelete
  2. First of all, very often a technology is picked over another based on things other than technical merits such as company policy/culture, team expertise, or merely personal preference. So I can’t say whether PHP or ASP.Net is always preferable than the other. However, I’d like to use ASP.Net, especially ASP.Net MVC for 1) Better IDE support. Visual Studio is very comprehensive yet simple to use and it provides you tons of productivity tools; 2) A consistent stack. A website doesn’t stop at web pages, it needs backend as well. With ASP.Net (and C#) you get a consistent coding style across the layers of your application; 3) I prefer ASP.Net MVC, as simple as that :).

    ReplyDelete
  3. Haishi, this is an excellent series, thanks for taking the time! Just one question though: wouldn't it be simpler to simply register a custom STS with ACS, so that logging in w/ existing credentials is just another option alongside the existing providers?

    Something along the lines of this:
    http://blogs.blackmarble.co.uk/blogs/sspencer/archive/2011/01/06/creating-your-own-identity-provider-for-windows-azure-appfabric-access-control.aspx

    ReplyDelete
  4. Hi Nariman:

    Indeed. And the article you provided is nice and simple. Thank you. However the scenario here is a little different. What I want to achieve is to have a "shared" user profile across multiple user ids. For example I'd like my users to be able to log in to my website using any of her ids while still being mapped to same set of profile settings. Although it's not shown in this series, FP in the middle gives me this opportunity to do something like this regardless of limitations of specific claims provided by different IPs.

    ReplyDelete
  5. Interesting. If all you have is different emails for the user, how are you able to map those to a single profile?

    For our scenario, we just need to continue to support ASP.NET membership model alongside Yahoo, Facebook and Google. I think linking the STS with ACS would serve just fine, even if the user were to login with a Google account that was cross-registered w/ the ASP.NET membership provider.

    ReplyDelete
  6. Mapping can be implied (such as mapping by certain claims such as e-Mail), or via claim transformation, or, in more complex cases, by custom mapping logic. Regardless how the mapping is done, the SP should be protected from such handling, as suggested by claim-based architecture principles. In my case I need quite complex mappings & tracking, which is not provided by ACS (just yet). Depends on your scenario, implied mapping may work out just fine for you, I agree.

    ReplyDelete
  7. Just an FYI, here's another way to have a shared profile across a number of logins: http://stackoverflow.com/questions/3218225/using-openid-via-dotnetopenauth-along-with-user-roles-and-other-membership-prov

    ReplyDelete