SAMLのSPをIISでホストする(IDPはSustainsys のスタブIDPを使用)
Microsoft .NETの webapp でSAMLのSPを作成し、IISでホストする手順。
IDPはSustainsys のスタブIDPを使用し、下図のような構成を作る。
| 使用するライブラリ | ITfoxtec.Identity.SAML2 (https://saml2.sustainsys.com/en/v2/#) |
| 動作環境 | Windows 11 |
| Idp | Sustainsys SAML IDP (https://stubidp.sustainsys.com/) |
| 参考URL | https://qiita.com/urushibata/items/85eb2d0e25b3cd290e14 https://developer.okta.com/blog/2020/10/23/how-to-authenticate-with-saml-in-aspnet-core-and-csharp |
■1. Windows に .NET SDKをインストールする。
以下のURLから、。NET SDKと .NET Core Runtime をダウンロードしてインストールする(バージョンを合わせること)
https://dotnet.microsoft.com/ja-jp/download
■2. webappを作成する
(2.1)以下のコマンドを実行して、Webappを作成する。
dotnet new webapp -o sample cd sample dotnet add package ITfoxtec.Identity.Saml2 --version 4.8.2 dotnet add package ITfoxtec.Identity.Saml2.MvcCore --version 4.8.2
(2.2)以下のソースコードを編集•追加する。(それ以外はデフォルトのまま)
(2.2.1) sample/appsettings.json
(IdpMetadataに、SustainsysのStub IDPを指定している)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Saml2": {
"IdPMetadata": "https://stubidp.sustainsys.com/Metadata",
"Issuer": "http://saml_sample/",
"SignatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"CertificateValidationMode": "None",
"RevocationMode": "NoCheck"
}
}
(2.2.2) sample/ClaimsTransform.cs
using ITfoxtec.Identity.Saml2.Claims;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
namespace sample.Identity
{
public static class ClaimsTransform
{
public static ClaimsPrincipal Transform(ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return incomingPrincipal;
}
return CreateClaimsPrincipal(incomingPrincipal);
}
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
// All claims
claims.AddRange(incomingPrincipal.Claims);
// Or custom claims
//claims.AddRange(GetSaml2LogoutClaims(incomingPrincipal));
//claims.Add(new Claim(ClaimTypes.NameIdentifier, GetClaimValue(incomingPrincipal, ClaimTypes.NameIdentifier)));
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}
private static IEnumerable<Claim> GetSaml2LogoutClaims(ClaimsPrincipal principal)
{
yield return GetClaim(principal, Saml2ClaimTypes.NameId);
yield return GetClaim(principal, Saml2ClaimTypes.NameIdFormat);
yield return GetClaim(principal, Saml2ClaimTypes.SessionIndex);
}
private static Claim GetClaim(ClaimsPrincipal principal, string claimType)
{
return ((ClaimsIdentity)principal.Identity).Claims.Where(c => c.Type == claimType).FirstOrDefault();
}
private static string GetClaimValue(ClaimsPrincipal principal, string claimType)
{
var claim = GetClaim(principal, claimType);
return claim != null ? claim.Value : null;
}
}
}
(2.2.3) sample/Program.cs
using ITfoxtec.Identity.Saml2;
using ITfoxtec.Identity.Saml2.Schemas.Metadata;
using ITfoxtec.Identity.Saml2.MvcCore.Configuration;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.Configure<Saml2Configuration>(builder.Configuration.GetSection("Saml2"));
builder.Services.Configure<Saml2Configuration>(saml2Configuration =>
{
saml2Configuration.AllowedAudienceUris.Add(saml2Configuration.Issuer);
var entityDescriptor = new EntityDescriptor();
entityDescriptor.ReadIdPSsoDescriptorFromUrl(new Uri(builder.Configuration["Saml2:IdPMetadata"]));
if (entityDescriptor.IdPSsoDescriptor != null)
{
saml2Configuration.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
saml2Configuration.SignatureValidationCertificates.AddRange(entityDescriptor.IdPSsoDescriptor.SigningCertificates);
}
else
{
throw new Exception("IdPSsoDescriptor not loaded from metadata.");
}
});
builder.Services.AddSaml2();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection(); // HTTPSを強制したくない場合は、この行をコメントアウト
app.UseStaticFiles();
app.UseRouting();
app.MapDefaultControllerRoute();
app.UseSaml2();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
(2.2.4) sample/Controllers/AuthController.cs
using ITfoxtec.Identity.Saml2;
using ITfoxtec.Identity.Saml2.Schemas;
using ITfoxtec.Identity.Saml2.MvcCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using sample.Identity;
using Microsoft.Extensions.Options;
using System.Security.Authentication;
namespace sample.Controllers
{
[AllowAnonymous]
[Route("Auth")]
public class AuthController : Controller
{
const string relayStateReturnUrl = "ReturnUrl";
private readonly Saml2Configuration config;
public AuthController(IOptions<Saml2Configuration> configAccessor)
{
config = configAccessor.Value;
}
[Route("Login")]
public IActionResult Login(string returnUrl = null)
{
var binding = new Saml2RedirectBinding();
binding.SetRelayStateQuery(new Dictionary<string, string> { { relayStateReturnUrl, returnUrl ?? Url.Content("~/") } });
return binding.Bind(new Saml2AuthnRequest(config)).ToActionResult();
}
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}
[HttpPost("Logout")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
if (!User.Identity.IsAuthenticated)
{
return Redirect(Url.Content("~/"));
}
var binding = new Saml2PostBinding();
var saml2LogoutRequest = await new Saml2LogoutRequest(config, User).DeleteSession(HttpContext);
return Redirect("~/");
}
}
}
(2.2.5) sample/Pages/Claims.cshtml
@page
@model ClaimsModel
@{
ViewData["Title"] = "Home page";
}
<div class="row">
<div class="col-md-12">
<h2>The users Claims (Iteration on User.Claims)</h2>
<p>
@foreach (var claim in User.Claims)
{
<strong>@claim.Type</strong> <br /> <span style="padding-left: 10px">Value: @claim.Value</span> <br />
}
</p>
</div>
</div>
(2.2.6) sample/Pages/Claims.cshtml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
namespace sample.Pages
{
[Authorize]
public class ClaimsModel : PageModel
{
private readonly ILogger<ClaimsModel> _logger;
public ClaimsModel(ILogger<ClaimsModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}
(2.2.7) sample/Pages/Shared/_Layout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - saml_example</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">sample</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
@if (((System.Security.Claims.ClaimsIdentity)User.Identity).IsAuthenticated)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Claims">SAML Claims</a>
</li>
<li>
@if (User.Identity.Name != null)
{
<span class="navbar-text">Hello, @User.Identity.Name!</span>
}
else
{
<span class="navbar-text">Hello</span>
}
</li>
<li>
<form class="form-inline" asp-controller="Auth" asp-action="Logout">
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="Auth" asp-action="Login">Login</a>
</li>
}
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2025 - saml_example - <a asp-area="" asp-page="/Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
■3. ビルドとデプロイ用のリリース物件を作成
(3.1)以下のコマンドを実行し、ビルドとIISにデプロイする物件を作成する。
dotnet build dotnet publish --configuration Release
上記を実行すると、sample¥bin¥Release¥netX.0¥publish 配下にデプロイファイル群一式ができる。
■4. IISにアプリケーションとして追加
(4.1)IISマネージャ起動
(4.2)アプリケーションプールを新規作成(デフォルト設定でOK)
(4.3)Default Web Siteを右クリックして、「アプリケーションの追加」を選択
(4.4)上記■3. で作成したアプリケーションフォルダを選択
■5. 動かし方
エイリアスを、”sapmple”とした場合、WEBブラウザで以下のようにアクセス。
(5.1)https://localhost/sample/
(5.2)下図のようにIDPで認証する。
■6. その他
(6.1) メタデータ取得に失敗する場合
もしこのSPを会社のイントラネット配下で動かした場合に、下図ように、Proxyサーバを経由したmetadataの取得に失敗するかもしれない。
その場合の回避策の1つとして、以下からメタデータをダウンロードし、
https://stubidp.sustainsys.com/Metadata
それを、今回作成したアプリケーションの、wwwroot配下に、metadata.xmlとして保存し、appilcation.jsonを以下のように変更することで回避することが可能。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Saml2": {
"IdPMetadata":"https://localhost/sample/metadata.xml", // "https://stubidp.sustainsys.com/Metadata",
"Issuer": "http://saml_sample/",
"SignatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"CertificateValidationMode": "None",
"RevocationMode": "NoCheck"
}
}
(6.2) 署名のチェック
今回の例では、署名のチェックをしていない。署名のチェックをする場合は、以下のようにCertificateValidationMode を変更する。ただし、その場合はIDPの公開鍵を信頼済みルートCAとして登録する必要がある。(studbidpの公開鍵を信頼済みルートCAに登録する場合は、開発環境にとどめるなど注意が必要。本番環境でやってしまうと、不特定多数の人間が任意のユーザになりすますことができてしまう)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Saml2": {
"IdPMetadata":"https://localhost/sample/metadata.xml", // "https://stubidp.sustainsys.com/Metadata",
"Issuer": "http://saml_sample/",
"SignatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"CertificateValidationMode": "ChainTrust",
"RevocationMode": "NoCheck"
}
}








