本記事と別の記事にて Azure ACS を利用してユーザー認証を行い、シングルサインオンを実現する サンプル  Web サイトをSitecore上で構築してみます。今回作成するのはコンテンツデリバリー環境のユーザーを認証するために Azure ACS を使用する Web アプリケーションです。 Sitecoreクライアントインタフェースを使用する編集環境でユーザーをADFSなどのSecurity Token Service を使用して認証を行う場合は、マーケットプレースのADFS Authenticator を使用できるか検討してみてください。

- ADFS Authenticator
https://marketplace.sitecore.net/en/Modules/ADFSAuthenticator.aspx

検証環境は次の通りです

  • Sitecore 7.2 Update 2
  • .NET 4.5
  • Visual Studio 2013 Premium
  • 記事作成時点の Azure ACS

外部公開用のサイトで、 Azure ACS (アクセス制御サービス) を使用したシングルサインオン機能とSitecoreを統合するサンプルを掲載します。といってもプログラム側では WIFの機能 を使用しているだけなので、ほとんど同じ方法でADFSなどのWIFが対応している認証方式((WS-Trust, WS-Federation) をサポートする セキュリティートークンサービスと連携して シングルサインオンを実現できると思います。 

1.Webアプリケーションプロジェクトの作成

Sitecore のソリューションを作成するので、 Sitecore の開発を行うためのプロジェクトを作成します。Visual Studio を起動してからのソリューションを作成します。今回は ScAcs という名前でソリューションを作成しました。

ソリューションを作成したら Web アプリケーションプロジェクトを下図のように作成します。本サンプルでは ScAcs.Web という名前でWebアプリケーションプロジェクトを作成します。

新規 ASP.NET プロジェクトダイアログで Empty のテンプレートを選択します。 Web Forms にチェックしてOK ボタンをクリックします。

プロジェクトファイルが作成されたら Globa.asax, App_Data, Models フォルダーを削除します。Web.config のプロパティーで ビルド アクション を なし にします。Sitecore.Kernel および Sitecore.Analytics.dll の参照を追加して、 開発対象のサイトコアインスタンスへの接続をセットアップしてください。 SitecoreのWebサイトを開発するためのプロジェクトの詳細な設定手順は割愛しますが、詳細は[Sitecore Rocks]ASP.NET Webアプリケーションプロジェクトをセットアップする Sitecore 7.2 版を参考にしてください。

Webアプリケーションプロジェクトの準備が終わったら、シングルサインインで使用する DLL参照を追加します。まず、下図のように System.IdentityModel および System.IdentityModel.Services アセンブリへの参照をプロジェクトに追加します。

続いて、 System.IdentityModel.Tokens.ValidatingIssuerNameRegistry を使用するので本アセンブリを NuGet から取得しておきます。下図のように ValidatingIssuerNameRegistry で検索すると見つかると思います。

Azure ACS を使用してログインするための サブレイアウトを作成します。ソリューションエクスプローラー上で layoutsフォルダーを作成して 右クリック > 追加 > 新しい項目 をクリックします。今回は ExternalLogin という名前でサブレイアウトを作成します。サブレイアウトの定義アイテムは sitecore/layout/Sublayouts 直下に作成しました。

ログイン用の ExternalLogin.ascx は次のように編集しました。デザイン自体はログイン画面にリダイレクトするためのボタンが配置されているだけのマークアップになります。

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ExternalLogin.ascx.cs" Inherits="ScAcs.Web.layouts.ExternalLogin" %>
<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>
<asp:Button runat="server" ID="btnExternalLogin" Text="Azure ACSログイン" OnClick="btnExternalLogin_Click" />

コードビハインドファイル (ExternalLogin.ascx.cs) を次のように編集します。ソースプログラム自体は非常にシンプルで、ボタンがクリックされたら Web.config で設定されたセキュリティートークンサービス(今回は Azure ACS) のログイン画面にリダイレクトするようにしています。RedirectToIdentityProviderの第二引数で認証後戻されるURLは固定で指定していますが、クエリパラメーターのreturnUrlが設定されている場合は、そのURLを指定するなど動的に変更することもできます。

using System;
using System.IdentityModel.Services;

namespace ScAcs.Web.layouts
{

    public partial class ExternalLogin : System.Web.UI.UserControl
    {
        private void Page_Load(object sender, EventArgs e)
        {

        }

        /// <summary>
        /// ボタンがクリックされたら Azure ACS を使用して認証を行います。
        /// 認証後 secure.aspx にリダイレクトします。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void btnExternalLogin_Click(object sender, EventArgs e)
        {
            // Web.config で設定された認証プロバイダーのページにリダイレクトさせます
            // 認証完了後リダイレクトするURLなどは必要に応じて設定してください。たとえば、returnUrlクエリパラメーターがある場合はそのURLを使用するなど
            FederatedAuthentication.WSFederationAuthenticationModule.RedirectToIdentityProvider(string.Empty, "http://scacs/secure.aspx", true);
        }
    }
}

非常にシンプルなソースです。後はログインしているユーザーの情報を確認するためにもう1つサブレイアウトを作成します。今度は、下図のように LoginState という名前でログインしているユーザの名前とロール一覧を出力するサブレイアウトを作成します。

LoginState.ascxの中身は単純に次のようにしました。

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LoginState.ascx.cs" Inherits="ScAcs.Web.layouts.LoginState" %>
<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>
<section>
    こんにちは <asp:Literal runat="server" ID="ltlName"></asp:Literal>さん!!<br />
    <asp:Repeater runat="server" ID="rptRoles" ItemType="String">
        <HeaderTemplate>
            ロール一覧
        </HeaderTemplate>
        <ItemTemplate>
            <asp:Label runat="server" ID="lblRoleName" Text="<%# Item %>"></asp:Label>
        </ItemTemplate>
        <SeparatorTemplate>
            <br />
        </SeparatorTemplate>
    </asp:Repeater>
</section>

コードビハインドファイルでユーザーのロール一覧をRepeaterコントロールにバインドしています。

using Sitecore.Security.Accounts;
using System;
using System.Linq;

namespace ScAcs.Web.layouts
{

    public partial class LoginState : System.Web.UI.UserControl
    {
        private void Page_Load(object sender, EventArgs e)
        {
            ltlName.Text = User.Current.Name;
            rptRoles.DataSource = User.Current.Roles.Select(x => x.Name);
            rptRoles.DataBind();
        }
    }
}

ここまででサブレイアウトの作成は完了です。

あとは、認証トークンを読み取ってユーザー認証を行うプログラムを作成します。通常WIFの機能を使用する場合は SessionAuthenticationModule や WSFederationAuthenticationModule がADFSや Azure ACS などのセキュリティートークサービスから返されたトークンを解析し、クレームから認証されたユーザー情報の作成を自動的に行ってくれます。Sitecore と共存させることを考えた場合の注意点として、 Sitecoreは ASP.NET の BeginRequest でアイテムの解決やユーザー情報の解決やアイテムに対するセキュリティー権限の検証など多くの重要な処理を行うのに対して SessionAutenticationModule など WIF に関係するモジュールが動作するのは AuthenticateRequest といった BeginRequest イベント以降に発生します。

そのため、Sitecoreと共存するには トークンを解析してユーザー情報を構築する処理は 自前で行うとうまくいきます。解析を行う場所は UserResolver パイプラインプロセッサーの前がよいでしょう。UserResolver はUserResolver より前のプロセッサーでユーザー情報が設定されている場合は処理をスキップするので他のフォーム認証などの方式と組み合わせた場合もうまく動作させることができるようになります。

ソリューションのWebアプリケーションプロジェクトで新しい項目を追加します。 新しい項目の追加 ダイアログで 下図のように Sitecore > Pipelines テンプレート一覧から Http Request Processor を選択して、Http Request Processor を作成します。名前は ClaimsUserResolver という名前で作成しました。

この時同時にパッチようのコンフィグファイルが App_Config / Include フォルダーに作成されます。そのファイルを編集して作成するプロセッサーは UserResolver の前に実行されるように編集しました。プロセッサーの名前空間やアセンブリなどは環境に合わせて適宜変更してください。

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="ScAcs.Web.Pipelines.ClaimsUserResolver,ScAcs.Web" patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.UserResolver, Sitecore.Kernel']" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

ClaimsUserResolverのソースプログラム自体は次のように作成しました。ソースは冒頭で紹介した ADFS Authenticator のソースを参考に一部修正しています。サンプルではクレームを使用したユーザー認証が成功していた場合はそのユーザーをもとに VirtualUser を作成し、 extranet\AuthenticatedUsers ロールにユーザーを所属させるようにしています。そのためサンプルソースを動かす場合は AuthenticatedUsers ロールを作成するようにしてください。

using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Security.Authentication;
using System;
using System.Diagnostics;
using System.IdentityModel.Services;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Principal;
using System.Web;


namespace ScAcs.Web.Pipelines
{
    /// <summary>
    /// Sitecore Marketプレースの ADFS AUTHENTICATOR の UserResolver を簡易実装したクラス
    /// 外部サイト用にクレーム認証することが目的のため、オリジナルソースよりシンプルな作りにしています。
    /// </summary>
    public class ClaimsUserResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            if (Sitecore.Context.User != null && Sitecore.Context.User.IsAuthenticated)
                return;
            try
            {
                SessionSecurityToken sessionToken = null;
                FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out sessionToken);
                if (sessionToken != null)
                {
                    try
                    {
                        FederatedAuthentication.SessionAuthenticationModule.AuthenticateSessionSecurityToken(
                            sessionToken, true);
                    }
                    catch
                    {
                        // サインアウト
                        FederatedAuthentication.WSFederationAuthenticationModule.SignOut(false);
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error("Claims::Error parsing token", ex, (object)this);
                return;
            }
            IPrincipal user = HttpContext.Current.User;
            if (user != null)
                Login(user);
        }
        protected virtual void Login(IPrincipal user)
        {
            var identity = user.Identity;

            WriteClaimsInfo(user.Identity as ClaimsIdentity);

            if (!identity.IsAuthenticated)
                return;

            try
            {
                // 現在のドメインとクレームの名前をもとにVirtualUserのアカウント名を作成。必要におうじて名前の付け方はカスタマイズします。
                var userName = string.Format("{0}\\{1}", Sitecore.Context.Domain.Name, identity.Name);
                // バーチャルユーザーの作成
                var virtualUser = AuthenticationManager.BuildVirtualUser(userName, true);
                // ロールに所属させる.参考にしたソースではトークンに含まれるロール(http://schemas.microsoft.com/ws/2008/06/identity/claims/role)の
                // クレーム一覧をもとに、サイトコアのセキュリティロール
                // を割れあてたり、Sitecoreの管理者としてログインする機能を提供していたが、今回は決め打ちでロールに所属させます。
                string roleName = @"extranet\AuthenticatedUsers";
                if (Sitecore.Security.Accounts.Role.Exists(roleName))
                {
                    virtualUser.Roles.Add(Sitecore.Security.Accounts.Role.FromName(roleName));
                }
                Sitecore.Security.Authentication.AuthenticationManager.LoginVirtualUser(virtualUser);
            }
            catch (ArgumentException ex)
            {
                Log.Error("Claims::Login Failed!", ex, this);
            }
        }

        /// <summary>
        /// Writes the claims information.
        /// </summary>
        /// <param name="claimsIdentity">The claims identity.</param>
        [Conditional("DEBUG")]
        private void WriteClaimsInfo(ClaimsIdentity claimsIdentity)
        {
            Log.Info("Writing Claims Info", this);
            foreach (var claim in claimsIdentity.Claims)
                Log.Info(string.Format("Claim : {0} , {1}", claim.Type, claim.Value), this);
        }
    }
}

最後に SessionAutenticationModule を継承して Sitecore のパイプライン上でユーザーが認証されている場合は、 SessionAuthenticationModule が認証トークン情報を使用してユーザーの認証情報を上書きしないように変更を行います。

ソリューションエクスプローラー上のプロジェクトで新しくクラスを作成します。クラス名は CustomSessionAuthenticationModule という名前で作成します。

ソースプログラムはシンプルです。OnAuthenticateRequest を継承して、ユーザーが認証されている場合は処理をスキップするようにしています。

using System;
using System.Collections.Generic;
using System.IdentityModel.Services;
using System.Linq;
using System.Web;


namespace ScAcs.Web.Fx
{
    public class CustomSessionAuthenticationModule : SessionAuthenticationModule
    {
        /// <summary>
        /// UserResolverはASP.NETのBeginRequestイベントで動作するが、AuthenticateRequestイベントは
        /// BeginRequest イベントの後で発生するため、認証の設定を変更しないようにする。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="eventArgs"></param>
        protected override void OnAuthenticateRequest(object sender, EventArgs eventArgs)
        {
            // Sitecoreでユーザーが認証済みの場合は、認証情報を変更しないようにする
            if (Sitecore.Context.User != null && Sitecore.Context.User.IsAuthenticated)
                return;

            base.OnAuthenticateRequest(sender, eventArgs);
        }

    }
}

プロジェクトの準備は完了です。あとは AzureACS を使用するように Web.config の修正とサンプルを動作させるためのアイテムの準備をしていきます。