Integrated ACS with Custom Identity Providers

ACS有個很有趣的機制,那就是ACS可以設定某個群組的Identity Providers為信任同盟,

 

 

/黃忠成

 

 

一個場景

 

   ACS有個很有趣的機制,那就是ACS可以設定某個群組的Identity Providers為信任同盟,

舉例來說,B Mall是一家小型的購物網站,他們擁有自己的使用者資料庫,驗證帳密等動作是透過B Mall自身完成的。

   A Mall則是一家大型的購物網站,與B Mall一樣,擁有自己的使用者資料庫,也自己處理了帳密的驗證動作。

   A Mall與B Mall談了一個合作案,希望B Mall的使用者可以使用同一組帳密就能登入A Mall來購物,但來自B Mall的使用者僅能在A Mall所限定的區域中進行購物。

   這是很常見的情況,很多購物網站都有所謂的某某專區,但問題是,A Mall現在該如何處理這種情況,最直接的答案是把B Mall所有的使用者資料庫匯入A Mall的使用者資料庫中,

但這會有很多問題,例如使用者重複,或是某個使用者被B Mall刪除後,也要通知A Mall做同樣的事,另外,基於保障自己,B Mall自然不希望既有的使用者資料全部被A Mall所掌握,

還有,當合作案中止時,A Mall也要自行刪除這些使用者,所以,匯入B Mall使用者資料至A Mall的做法是短線的手法,會引發很多問題。

   透過ACS的信任同盟機制,可以快速且簡單的解決這個問題,如下圖所示。

圖1

在這種情況下,A Mall與B Mall先使用WIF修改自身的Login頁面,令其相容於ACS的信任同盟模式。

  B Mall使用者可以透過ACS選擇ABMall的Identity Provider來登入,此時ACS會把使用者導向B Mall的Login頁面,待取得Secure Token後,B Mall的使用者就可以在A Mall及B Mall

進行購物動作,只是受限於A Mall所開放的部分專區。

  A Mall的使用者同樣透過ACS登入,此時會轉往A Mall的Login頁面,取得Secure Token後,A Mall的使用者可以自由地在A Mall或是B Mall進行購物。

  簡單的說,在不共享使用者帳密的情況下,A Mall與B Mall透過ACS整合成了SSO(單一簽入)的信任同盟。

 

Creating Custom Identity Providers

 

   要完成上述的任務,A Mall與B Mall的驗證頁面都得進行修改,WIF SDK提供了簡單的Template讓我們完成這個動作。

圖2

先修改Login.aspx.cs,此處只是個範例,所以沒連資料庫來驗證使用者。


//-----------------------------------------------------------------------------
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
//-----------------------------------------------------------------------------

using System;
using System.Web.Security;
using System.Web.UI;

public partial class Login : System.Web.UI.Page
{
    protected void Page_Load( object sender, EventArgs e )
    {
        // Note: Add code to validate user name, password. This code is for illustrative purpose only.
        // Do not use it in production environment.
        if ( !string.IsNullOrEmpty( txtUserName.Text ) )
        {
            if (!CheckUser())
            {
                ClientScript.RegisterStartupScript(typeof(Page), "alert", "alert('wrong user');", true);
                return;
            }
            if ( Request.QueryString["ReturnUrl"] != null )
            {
                FormsAuthentication.RedirectFromLoginPage( txtUserName.Text, false );
            }
            else
            {
                FormsAuthentication.SetAuthCookie( txtUserName.Text, false );
                Response.Redirect( "default.aspx" );
            }
        }
        else if ( !IsPostBack )
        {
            txtUserName.Text = "Adam Carter";
        }
    }

    private bool CheckUser()
    {
        if (txtUserName.Text == "code6421_amall" && txtPassword.Text == "1234")
            return true;
        return false;
    }
}

接著修改App_Code目錄下的CustomSecurityTokenService.cs。


//…………………………..
protected override Scope GetScope( IClaimsPrincipal principal, RequestSecurityToken request )
    {
        ValidateAppliesTo( request.AppliesTo );

        //
        // Note: The signing certificate used by default has a Distinguished name of "CN=STSTestCert",
        // and is located in the Personal certificate store of the Local Computer. Before going into production,
        // ensure that you change this certificate to a valid CA-issued certificate as appropriate.
        //
        Scope scope = new Scope( request.AppliesTo.Uri.OriginalString, SecurityTokenServiceConfiguration.SigningCredentials );

        string encryptingCertificateName = WebConfigurationManager.AppSettings[ "EncryptingCertificateName" ];
        if ( !string.IsNullOrEmpty( encryptingCertificateName ) )
        {
            // Important note on setting the encrypting credentials.
            // In a production deployment, you would need to select a certificate that is specific to the RP that is requesting the token.
            // You can examine the 'request' to obtain information to determine the certificate to use.
            scope.EncryptingCredentials = new X509EncryptingCredentials( CertificateUtil.GetCertificate( StoreName.My, StoreLocation.LocalMachine, encryptingCertificateName ) );
        }
        else
        {
            // If there is no encryption certificate specified, the STS will not perform encryption.
            // This will succeed for tokens that are created without keys (BearerTokens) or asymmetric keys.  
            scope.TokenEncryptionRequired = false;            
        }

        if (!string.IsNullOrEmpty(request.ReplyTo))
        {
            scope.ReplyToAddress = request.ReplyTo;
        }
        else
        {
            scope.ReplyToAddress = scope.AppliesToAddress;
        }
        // Set the ReplyTo address for the WS-Federation passive protocol (wreply). This is the address to which responses will be directed. 
        // In this template, we have chosen to set this to the AppliesToAddress.
        //scope.ReplyToAddress = scope.AppliesToAddress;

        return scope;
    }
//……………………………………..

然後修改web.config中的Issuer Name

 

………

<addkey="IssuerName"value="http://localhost:8028/STSWebSite1/"/>

這個value需對應FederationMetadata目錄中FederationMetadata.xml的entityID。

完成後進入ACS Portal,添加Identity Provider。

圖3

圖4

此處上傳FederationMetadata中的FederationMetadata.xml,接著修改規則群組。

圖5

接著循A Mall模式建立另一個Identity Provider網站,修改其Login.aspx.cs如下。


//-----------------------------------------------------------------------------
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
//-----------------------------------------------------------------------------

using System;
using System.Web.Security;
using System.Web.UI;

public partial class Login : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Note: Add code to validate user name, password. This code is for illustrative purpose only.
        // Do not use it in production environment.
        if (!string.IsNullOrEmpty(txtUserName.Text))
        {
            if (!CheckUser())
            {
                ClientScript.RegisterStartupScript(typeof(Page), "alert", "alert('wrong user');", true);
                return;
            }
            if (Request.QueryString["ReturnUrl"] != null)
            {
                FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, false);
            }
            else
            {
                FormsAuthentication.SetAuthCookie(txtUserName.Text, false);
                Response.Redirect("default.aspx");
            }
        }
        else if (!IsPostBack)
        {
            txtUserName.Text = "Adam Carter";
        }
    }

    private bool CheckUser()
    {
        if (txtUserName.Text == "code6421_bmall" && txtPassword.Text == "1234")
            return true;
        return false;
    }
}

同樣的,需修改App_Code中的CustomSecurityTokenService.cs及web.config中的IssuerName,然後上傳FederationMetadata.xml至ACS。

修改ACS中的信賴憑證者應用程式設定如下。

規則群組改成下面這樣。

圖7

到此,ACS的信任同盟設定就完成了。

 

Creating Secure WCF Service

 

  接下來,我們修改A Mall的WCF Service,讓其對B Mal使用者做出限制。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using Microsoft.IdentityModel.Claims;
using System.Web;

namespace WebApplication15
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "Service1" in code, svc and config file together.
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class Service1 : IService1
    {
        private bool IsAuthenticated()
        {
            return HttpContext.Current.User.Identity.IsAuthenticated;
        }



        public string HelloWorld()
        {
            if (IsAuthenticated())
                return "hello world.";
            else
                throw new Exception("Error.");
        }

        public string HelloWorldAMallOnly()
        {
            if (IsAuthenticated())
            {
                IClaimsPrincipal principal = (IClaimsPrincipal)HttpContext.Current.User;
                string provider = ((IClaimsIdentity)principal.Identity).Claims.FirstOrDefault(
                    c => c.ClaimType == "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider").Value;
                if(provider.Contains("STSWebSite1"))
                    return "hello world.";
                throw new Exception("Error.");
            }
            else
                throw new Exception("Error.");
        }
    }
}

對於B Mall的WCF Service也做同樣的修改,但沒有對A Mall使用者作出使用限制。

OK,我知道,現在可能有點亂,讓我們整理一下。

  1. A Mall與B Mall都擁有自己的應用程式,使用者可以透過這個應用程式來進行購物,其後端為WCF Service。
  2. 現在,A Mall與B Mall形成同盟,所以A Mall的使用者可以透過B Mall的應用程式,此時登入時,ACS會將A Mall使用者導向A Mall的Login頁面。
  3. 當A Mall登入後,可以完全正常的使用B Mall的應用程式。
  4. B Mall使用者除了可用原來的B Mall應用程式進行購物外,也能使用A Mall的應用程式進行購物,此時ACS會把B Mall使用者導向B Mall的登入頁面,登入成功後,

           B Mall的使用者可以正常的使用A Mall的應用程式,但受限於後端的WCF Service,所以只能使用一小部分。

      5. 在正常情況下, 你應該會有兩個WCF Secure Service , 一個是A Mall的,一個是BMall的.

      6. 在正常情況下, 你應該會有兩個Windows Phone應程式,一個是for AMall, 一個是for BMall,分別呼叫不同的WCF Service

我們用先前的DemoACS(Windows Phone)來測試,下面是修改後的程式碼。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
using SL.Phone.Federation.Utilities;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace DemoACS
{
    public partial class MainPage : PhoneApplicationPage
    {
        RequestSecurityTokenResponseStore _rstrStore = null;

        // Constructor
        public MainPage()
        {
            InitializeComponent();
            _rstrStore = (RequestSecurityTokenResponseStore)App.Current.Resources["rstrStore"];
        }

        private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
        {
            if (!_rstrStore.ContainsValidRequestSecurityTokenResponse())
            {
                NavigationService.Navigate(new Uri("/SignInControl.xaml", UriKind.Relative));
            }
            else
            {

                ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
                var store = App.Current.Resources["rstrStore"] as SL.Phone.Federation.Utilities.RequestSecurityTokenResponseStore;
                using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
                {
                    var httpRequestProperty = new HttpRequestMessageProperty();
                    httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + store.SecurityToken;
                    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
                    client.HelloWorldCompleted += (s, args) =>
                    {
                        Dispatcher.BeginInvoke(() =>
                            {
                                MessageBox.Show(args.Result);
                            });
                    };
                    client.HelloWorldAsync();
                }
                textBlock1.Text = "thanks for your login";
            }

        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
            var store = App.Current.Resources["rstrStore"] as SL.Phone.Federation.Utilities.RequestSecurityTokenResponseStore;
            using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
            {
                var httpRequestProperty = new HttpRequestMessageProperty();
                httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + store.SecurityToken;
                OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
                client.HelloWorldAMallOnlyCompleted += (s, args) =>
                {
                    if (args.Error != null)
                    {
                        Dispatcher.BeginInvoke(() =>
                            {
                                MessageBox.Show(args.Error.Message);
                            });
                        return;
                    }
                    Dispatcher.BeginInvoke(() =>
                    {
                        MessageBox.Show(args.Result);
                    });
                };
                client.HelloWorldAMallOnlyAsync();
            }
        }
    }
}

執行程式,會出現如下圖的選擇頁面。

圖8

當使用A Mall登入時,按下按鈕會正常的呼叫HelloWorldAsync。

圖9

當使用B Mall登入時,按下按鈕會出現錯誤,因為B Mall使用者被限制不能呼叫HelloWorldAsync。

圖10