Duende IdentityServer
認証ライブラリ
Duende IdentityServer
概要
Duende IdentityServerは、ASP.NET Core向けの最も柔軟で標準準拠のOpenID ConnectおよびOAuth 2.0フレームワークです。2025年現在、バージョン7.2.4が最新リリースとなっており、OpenID Foundation認定を受けた完全な標準準拠実装を提供しています。IdentityServer4の後継として開発され、エンタープライズグレードの認証・認可ソリューションを提供します。商用利用には有償ライセンスが必要ですが、非営利団体や適格企業向けには無償のCommunity Editionが用意されています。
詳細
Duende IdentityServerは、複雑なカスタムセキュリティシナリオに対応できる深い柔軟性を持つ認証フレームワークです。主な特徴:
- 完全な標準準拠: OpenID Connect 1.0およびOAuth 2.0/2.1の完全実装
- ASP.NET Core統合: ASP.NET Core Identityとのネイティブ統合
- エンタープライズ機能: Token Exchange、Audience Constrained Tokens、Federation対応
- 多様なデプロイメント: オンプレミス、クラウド、Docker、Kubernetes対応
- BFFパターン: ブラウザベースJavaScriptアプリケーション向けのBackend for Frontendアーキテクチャ
- 豊富な統合オプション: Entity Framework、外部IDプロバイダー、カスタムストア対応
メリット・デメリット
メリット
- .NETエコシステムで最も成熟した認証ソリューション
- OpenID Foundation認定による高い信頼性と標準準拠
- 柔軟な設定とカスタマイズが可能で複雑な要件に対応
- エンタープライズグレードのセキュリティ機能を標準搭載
- 豊富なドキュメントとコミュニティサポート
- オンプレミスからクラウドまで柔軟なデプロイメント選択肢
デメリット
- 商用利用時は有償ライセンスが必要(年間$1,500〜)
- .NET/ASP.NET Coreエコシステムに限定される
- 初期セットアップが複雑で学習コストが高い
- 小規模アプリケーションには過剰な機能
- ライセンス条件の理解と管理が必要
- Community Editionには機能制限がある
参考ページ
- Duende IdentityServer 公式サイト
- Duende IdentityServer ドキュメント
- GitHub - DuendeSoftware/IdentityServer
- NuGet - Duende.IdentityServer
書き方の例
基本的なセットアップとインストール
# NuGetパッケージのインストール
dotnet add package Duende.IdentityServer
dotnet add package Duende.IdentityServer.AspNetIdentity
dotnet add package Duende.IdentityServer.EntityFramework
# テンプレートからプロジェクト作成
dotnet new isaspid -n MyIdentityServer
基本設定(Program.cs / Startup.cs)
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Entity Framework設定
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// ASP.NET Core Identity設定
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>();
// IdentityServer設定
builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// 本番環境では適切な証明書を設定
options.KeyManagement.Enabled = true;
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddAspNetIdentity<IdentityUser>();
var app = builder.Build();
// パイプライン設定
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
クライアント、リソース、スコープの設定
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("api1", "My API"),
new ApiScope("api2", "Another API"),
new ApiScope("read", "Read access"),
new ApiScope("write", "Write access")
};
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("api1", "My API")
{
Scopes = new List<string> { "api1", "read", "write" },
UserClaims = new List<string> { "role", "email" }
}
};
public static IEnumerable<Client> Clients =>
new Client[]
{
// SPAクライアント (React/Angular/Vue)
new Client
{
ClientId = "spa-client",
ClientName = "SPA Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RequireConsent = false,
RedirectUris = { "https://localhost:3000/callback" },
PostLogoutRedirectUris = { "https://localhost:3000" },
AllowedCorsOrigins = { "https://localhost:3000" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1"
},
AccessTokenLifetime = 3600,
AllowAccessTokensViaBrowser = true
},
// Webアプリケーション
new Client
{
ClientId = "web-client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RequireConsent = false,
RedirectUris = { "https://localhost:5001/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5001/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
},
// モバイルアプリ/ネイティブアプリ
new Client
{
ClientId = "mobile-client",
ClientName = "Mobile Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { "myapp://callback" },
PostLogoutRedirectUris = { "myapp://logout" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
},
AllowOfflineAccess = true,
AccessTokenLifetime = 3600,
RefreshTokenUsage = TokenUsage.ReUse
}
};
}
APIの保護とスコープベース認可
// API Controller
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class WeatherController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "ApiScope")]
public IActionResult Get()
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
var claims = identity?.Claims?.Select(c => new { c.Type, c.Value }).ToArray();
return Ok(new
{
Message = "天気データ",
User = User.Identity.Name,
Claims = claims,
Scopes = User.Claims.Where(c => c.Type == "scope").Select(c => c.Value)
});
}
[HttpPost]
[Authorize(Policy = "WriteAccess")]
public IActionResult Post([FromBody] WeatherData data)
{
// 書き込み権限が必要な操作
return Ok("天気データが更新されました");
}
}
// Program.cs でのAPI認証設定
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
options.AddPolicy("WriteAccess", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "write");
});
});
カスタムプロファイルサービス
public class CustomProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<IdentityUser> _claimsFactory;
private readonly UserManager<IdentityUser> _userManager;
public CustomProfileService(UserManager<IdentityUser> userManager,
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory)
{
_userManager = userManager;
_claimsFactory = claimsFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
// カスタムクレームの追加
var roles = await _userManager.GetRolesAsync(user);
claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, role)));
// 組織情報やカスタムプロパティの追加
claims.Add(new Claim("department", "Engineering"));
claims.Add(new Claim("employee_id", user.Id));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
// Program.cs での登録
builder.Services.AddTransient<IProfileService, CustomProfileService>();
Token Exchange の実装
// Token Exchange設定
public static IEnumerable<Client> TokenExchangeClients =>
new Client[]
{
new Client
{
ClientId = "token-exchange-client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = { "urn:ietf:params:oauth:grant-type:token-exchange" },
AllowedScopes = new List<string>
{
"api1", "api2"
},
// Token Exchange特有の設定
AllowedTokenExchangeSubjectTokenTypes =
{
"urn:ietf:params:oauth:token-type:access_token"
},
AllowedTokenExchangeActorTokenTypes =
{
"urn:ietf:params:oauth:token-type:access_token"
}
}
};
// Token Exchange使用例
public class TokenExchangeService
{
private readonly HttpClient _httpClient;
public async Task<TokenResponse> ExchangeTokenAsync(string subjectToken, string audience)
{
var request = new TokenExchangeTokenRequest
{
Address = "https://localhost:5001/connect/token",
ClientId = "token-exchange-client",
ClientSecret = "secret",
SubjectToken = subjectToken,
SubjectTokenType = "urn:ietf:params:oauth:token-type:access_token",
Audience = audience,
Scope = "api2"
};
return await _httpClient.RequestTokenExchangeTokenAsync(request);
}
}
外部IDプロバイダーとの統合
// Google OAuth設定
builder.Services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
options.CallbackPath = "/signin-google";
options.Scope.Add("email");
options.Scope.Add("profile");
options.Events.OnCreatingTicket = context =>
{
// カスタムクレーム処理
var email = context.Principal.FindFirst(ClaimTypes.Email)?.Value;
if (email?.EndsWith("@mycompany.com") == true)
{
context.Identity.AddClaim(new Claim("department", "internal"));
}
return Task.CompletedTask;
};
})
.AddMicrosoft("Microsoft", options =>
{
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
options.CallbackPath = "/signin-microsoft";
});