SharePoint オンプレミスの ADFS 認証にてユーザー検索、解決処理を拡張するクレーム プロバイダー サンプル コード
こんにちは SharePoint サポートの森 健吾 (kenmori) です。
SharePoint Server オンプレミス版で ADFS 認証を構築すると、ユーザー検索が実行されず、どのようなユーザーを入力しても解決してしまう状況にとまどったことがある方も多いと思います。
今回の投稿では、その状況を回避するためのユーザー検索や解決処理を実装する最小構成のサンプル コードを記載いたします。この規模のサンプルは実運用で使用するには不十分ですが、カスタム クレーム プロバイダーの実装方法を理解するためには有用だと考えています。
参考情報
はじめに参考資料を記載いたします。クレーム プロバイダーは、今回機能拡張するユーザー検索や解決処理以外にも様々な機能を提供します。全体的なモジュールの役割は下記サイトで抑えていただけますと幸いです。
タイトル : SharePoint 2013 のクレーム プロバイダー
アドレス : https://msdn.microsoft.com/ja-jp/library/office/ee535894.aspx
抜粋
信頼された SAML トークン発行者の場合、SharePoint Server は、リストや検索を提供しません。ユーザーが値を入力すると、SharePoint Server はその値を常に解決します。つまり、「adam@contoso.com」と入力すると、ユーザー選択ウィンドウはその値を受け入れます。これは、STS での解決方法、検索の実装方法、またはクレーム値のリスト方法を指定する業界標準が存在しないためです。
ユーザーは組み込みのクレーム プロバイダーをオーバーライドして、カスタム検索、名前の解決、およびリスト機能を実装できます。これは、たとえば信頼された SAML トークン発行者を使用するシナリオでは特に便利です。
タイトル : [方法] SharePoint 2013 でクレーム プロバイダーを作成する
アドレス : https://msdn.microsoft.com/ja-jp/library/office/ee537299.aspx
タイトル : [方法] SharePoint 2013 でクレーム プロバイダーを展開する
アドレス : https://msdn.microsoft.com/ja-jp/library/office/ee535443.aspx
実践
それでは実践に入ります。大まかな開発の流れは以下の通りになります。
1. 以下の情報をあらかじめ確認しておきます。
・全体管理画面の認証プロバイダー設定で指定した SAML 認証のプロバイダー名 (例. saml provider)
確認コマンド : (Get-SPTrustedIdentityTokenIssuer).Name
・ユーザー識別のためのクレーム タイプ (例. https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress)
確認コマンド : (Get-SPTrustedIdentityTokenIssuer).IdentityClaimTypeInformation
・ロール (グループ) の識別情報について
2. Visual Studio を起動し、[SharePoint 2013 - 空のプロジェクト] から SharePoint ファーム ソリューション開発を開始します。
3. ソリューション エクスプローラより [追加] - [新しい項目] – [クラス] を選択し、クラス ライブラリ (例. CustomADFSClaimProvider.cs) を追加します。
4. SPClaimProvider を継承して、基底クラスで abstruct スコープとして定義されているプロパティ、メソッドをあらかじめすべて override します。
(このサンプルで空実装のメソッドが多いのは、この理由です。)
5. SupportsResolve と SupportsSearch プロパティで true を返すよう実装します。
6. FillResolve の 2 メソッドと FillSearch 1 メソッドを実装します。
7. 標準の SAML プロバイダーのクレームを生成する CreateClaim を定義しておきます。
<<実装結果のサンプルは以下です>>
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration.Claims;
using Microsoft.SharePoint.WebControls;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomADFSClaimProvider
{
class CustomADFSClaimProvider : SPClaimProvider
{
public CustomADFSClaimProvider(string displayName)
: base(displayName)
{
}
public override string Name
{
get { return "CustomADFSClaimProvider"; }
}
public override bool SupportsEntityInformation
{
get { return false; }
}
public override bool SupportsResolve
{
// IMPORTANT! (step. 5)
get { return true; }
}
public override bool SupportsSearch
{
// IMPORTANT! (step. 5)
get { return true; }
}
public override bool SupportsHierarchy
{
get { return false; }
}
public override bool SupportsUserSpecificHierarchy
{
get
{
return base.SupportsUserSpecificHierarchy;
}
}
protected override void FillSchema(Microsoft.SharePoint.WebControls.SPProviderSchema schema)
{
}
protected override void FillClaimTypes(List<string> claimTypes)
{
}
protected override void FillClaimValueTypes(List<string> claimValueTypes)
{
}
protected override void FillEntityTypes(List<string> entityTypes)
{
}
protected override void FillClaimsForEntity(Uri context, SPClaim entity, List<SPClaim> claims)
{
}
protected override void FillHierarchy(Uri context, string[] entityTypes, string hierarchyNodeID, int numberOfLevels, Microsoft.SharePoint.WebControls.SPProviderHierarchyTree hierarchy)
{
}
// IMPORTANT! (step 6)
protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List<PickerEntity> resolved)
{
resolved.AddRange(GetPickerEntitiesFromDirectory(resolveInput.Value));
}
// IMPORTANT! (step 6)
protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<PickerEntity> resolved)
{
resolved.AddRange(GetPickerEntitiesFromDirectory(resolveInput));
}
// IMPORTANT! (step 6)
protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree)
{
searchTree.AddEntities(GetPickerEntitiesFromDirectory(searchPattern));
}
// IMPORTANT! (step 7)
protected new SPClaim CreateClaim(string type, string value, string valueType)
{
// specify claim provider name as lower case letter here.
return new SPClaim(type, value, valueType,
SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, "saml provider"));
}
private List<Microsoft.SharePoint.WebControls.PickerEntity> GetPickerEntitiesFromDirectory(string input)
{
List<Microsoft.SharePoint.WebControls.PickerEntity> resolved = new List<PickerEntity>();
SPSecurity.RunWithElevatedPrivileges(delegate()
{
DirectorySearcher searcher = new DirectorySearcher();
// make sure that mail (outside anr) is also queried.
searcher.Filter = string.Format("(|(&(objectCategory=person)(|(anr={0}*)(mail={0})))(&(objectCategory=group)(anr={0}*)))", input);
foreach (SearchResult src in searcher.FindAll())
{
string claimValue;
string objectCategory = TryGetProperties(src, "objectCategory");
PickerEntity entity = CreatePickerEntity();
string claimType = "";
if (objectCategory.IndexOf("CN=Person") >= 0)
{
// get "mail" when IdentityClaimTypeInformation is mail
claimValue = TryGetProperties(src, "mail");
claimType = System.IdentityModel.Claims.ClaimTypes.Email;
entity.EntityType = SPClaimEntityTypes.User;
}
else
{
// get "sAMAccountName" when LDAP Attribute "Token Groups –Unqualified Names" is set to Role on Claim Rule.
claimValue = TryGetProperties(src, "sAMAccountName");
claimType = System.Security.Claims.ClaimTypes.Role;
entity.EntityType = SPClaimEntityTypes.FormsRole;
}
if (claimValue != null)
{
entity.EntityData[PeopleEditorEntityDataKeys.Email] = claimValue;
SPClaim claim = CreateClaim(claimType,
claimValue,
Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
entity.Claim = claim;
entity.Description = claimValue;
entity.DisplayText = claimValue;
entity.HierarchyIdentifier = claimValue;
entity.IsResolved = true;
resolved.Add(entity);
}
}
});
return resolved;
}
private string TryGetProperties(SearchResult src, string prop)
{
foreach (string propname in src.GetDirectoryEntry().Properties.PropertyNames)
{
if (propname == prop)
{
return src.GetDirectoryEntry().Properties[prop].Value.ToString();
}
}
return null;
}
}
}
8. 次に、ソリューション エクスプローラから [Features] を右クリックし、[フィーチャーの追加] をクリックして機能 (例. CustomADFSClaimProviderFeature) を追加します。
9. ダブルクリックして、スコープを "Farm" に指定します。
10. さらに右クリックし、[イベント レシーバーの追加] をクリックします。
11. 派生元のクラスを SPClaimProviderFeatureReceiver に変更します。(これにより FeatureActivated などの必要なイベントは派生元の処理が呼ばれます)
12. 下記のようなコードを追加します。
using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration.Claims;
namespace CustomADFSClaimProvider.Features.CustomADFSClaimProviderFeature
{
public class CustomADFSClaimProviderFeatureEventReceiver : SPClaimProviderFeatureReceiver
{
private string providerDisplayName = "CustomADFSClaimProvider";
private string providerDescription = "CustomADFSClaimProvider";
public override string ClaimProviderAssembly
{
get
{
return typeof(CustomADFSClaimProvider).Assembly.FullName;
}
}
public override string ClaimProviderDisplayName
{
get
{
return providerDisplayName;
}
}
public override string ClaimProviderType
{
get { return typeof(CustomADFSClaimProvider).FullName; }
}
public override string ClaimProviderDescription
{
get { return providerDescription; }
}
public override bool ClaimProviderUsedByDefault
{
get
{
return true;
}
}
}
}
開発内容は以上です。ビルドしてソリューション パッケージ (*.wsp) を実行環境に展開して動作を確認してみましょう。
以下のように信頼関係のあるドメインから指定された名前を含むセキュリティ グループとユーザーが検索でき、権限指定できるようになります。
いかがでしたでしょうか。本投稿の内容でカスタム クレーム プロバイダーに対する抵抗感をなくしていただき、運用環境で求められる ADFS 認証の価値を引き出す様々なビジネス ロジックを実装いただけますと幸いです。
今回の投稿は以上になります。