pac4j

認証ライブラリJavaOAuthSAMLOpenID ConnectSpring Boot認証認可

認証ライブラリ

pac4j

概要

pac4jは、Java向けの包括的なセキュリティエンジンで、認証・認可・マルチフレームワーク対応を提供します。

詳細

pac4jは、Javaアプリケーション向けの強力で柔軟なセキュリティフレームワークです。Apache 2ライセンスの下で提供され、OAuth、CAS、SAML、OpenID Connect、LDAP、JWT、MongoDB、CouchDBなど多様な認証メカニズムをサポートしています。ロール、匿名/記憶/完全認証ユーザー、プロファイルタイプと属性チェックなどの認可機能も提供します。Spring Boot、Play Framework、Vert.x、Spark Java、JAX-RS、Dropwizardなど、ほぼ全てのJavaフレームワークと統合可能です。DirectClientとIndirectClientの2つのクライアントタイプを提供し、WebサービスとUIアプリケーション両方の認証に対応します。CSRF保護、セキュリティヘッダー、IPアドレス制限などの高度なセキュリティ機能も内蔵しています。設定ベースのアプローチにより、プロパティファイルから簡単にクライアントとオーセンティケーターを構築できます。

メリット・デメリット

メリット

  • 多様な認証対応: OAuth、SAML、OpenID Connect、LDAP、JWT等幅広くサポート
  • フレームワーク横断: Spring Boot、Play、Vert.x等ほぼ全Javaフレームワーク対応
  • 統合API: 全認証メカニズムを統一されたAPIで提供
  • 柔軟な認可: ロール、属性、プロファイルタイプによる細かい認可制御
  • セキュリティ機能: CSRF、セキュリティヘッダー、セッション保護内蔵
  • 設定の外部化: プロパティファイルによる設定管理
  • 活発なコミュニティ: 継続的な開発とサポート

デメリット

  • 学習コスト: 多機能ゆえの概念習得が必要
  • 設定複雑さ: 高度な機能には詳細な設定が必要
  • Java限定: Java生態系外では使用不可
  • ドキュメント: 一部の統合で詳細ドキュメントが不足
  • デバッグ複雑さ: 多層的な認証フローのデバッグが困難
  • 依存関係: 各プロバイダーごとに追加依存関係が必要

主要リンク

書き方の例

基本的なSpring Boot統合

// application.properties
pac4j.callbackUrl=http://localhost:8080/callback
pac4j.properties.oidc.id=your-client-id
pac4j.properties.oidc.secret=your-client-secret
pac4j.properties.oidc.issuer=https://accounts.google.com
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public Config config() {
        // Google OpenID Connect設定
        OidcConfiguration oidcConfig = new OidcConfiguration();
        oidcConfig.setClientId("your-client-id");
        oidcConfig.setSecret("your-client-secret");
        oidcConfig.setIssuer("https://accounts.google.com");
        
        OidcClient oidcClient = new OidcClient(oidcConfig);
        oidcClient.setName("Google");

        // クライアント群設定
        Clients clients = new Clients("http://localhost:8080/callback", oidcClient);

        // 設定組み立て
        return new Config(clients);
    }

    @Bean
    public SecurityInterceptor securityInterceptor(Config config) {
        return new SecurityInterceptor(config, "Google");
    }
}

複数認証プロバイダーの設定

@Configuration
public class AuthenticationConfig {

    @Bean
    public Config multiProviderConfig() {
        // Google OpenID Connect
        OidcConfiguration googleConfig = new OidcConfiguration();
        googleConfig.setClientId("google-client-id");
        googleConfig.setSecret("google-client-secret");
        googleConfig.setIssuer("https://accounts.google.com");
        OidcClient googleClient = new OidcClient(googleConfig);
        googleClient.setName("Google");

        // Facebook OAuth
        FacebookConfiguration facebookConfig = new FacebookConfiguration();
        facebookConfig.setKey("facebook-app-id");
        facebookConfig.setSecret("facebook-app-secret");
        facebookConfig.setScope("email,user_profile");
        FacebookClient facebookClient = new FacebookClient(facebookConfig);

        // SAML設定
        SAML2Configuration samlConfig = new SAML2Configuration();
        samlConfig.setKeystorePath("samlKeystore.jks");
        samlConfig.setKeystorePassword("password");
        samlConfig.setPrivateKeyPassword("password");
        samlConfig.setIdentityProviderMetadataPath("idp-metadata.xml");
        SAML2Client samlClient = new SAML2Client(samlConfig);

        // JWT設定
        SecretSignatureConfiguration jwtConfig = 
            new SecretSignatureConfiguration("myJwtSecret");
        JwtAuthenticator jwtAuthenticator = 
            new JwtAuthenticator(jwtConfig);
        HeaderClient jwtClient = new HeaderClient("Authorization", 
            "Bearer ", jwtAuthenticator);

        Clients clients = new Clients("http://localhost:8080/callback",
            googleClient, facebookClient, samlClient, jwtClient);

        return new Config(clients);
    }
}

セキュリティ制御とオーソライザー

@RestController
@RequestMapping("/api")
public class ApiController {

    // 認証必須
    @GetMapping("/secure")
    @RequiresAuthentication
    public String secureEndpoint(HttpServletRequest request) {
        ProfileManager manager = new ProfileManager(
            new JEEContext(request, null));
        Optional<UserProfile> profile = manager.get(true);
        
        return "Hello " + profile.get().getDisplayName();
    }

    // 特定ロール必須
    @GetMapping("/admin")
    @RequiresRoles("ADMIN")
    public String adminEndpoint() {
        return "Admin area";
    }

    // カスタム認可
    @GetMapping("/custom")
    @RequiresAuthorization("customAuthorizer")
    public String customEndpoint() {
        return "Custom authorized content";
    }
}

// カスタムオーソライザー
@Component("customAuthorizer")
public class CustomAuthorizer implements Authorizer<UserProfile> {
    
    @Override
    public boolean isAuthorized(WebContext context, 
                               List<UserProfile> profiles) {
        if (profiles.isEmpty()) {
            return false;
        }
        
        UserProfile profile = profiles.get(0);
        return profile.containsAttribute("department") &&
               "IT".equals(profile.getAttribute("department"));
    }
}

LDAP認証設定

@Configuration
public class LdapAuthConfig {

    @Bean
    public Config ldapConfig() {
        // LDAP設定
        LdapProfile ldapProfile = new LdapProfile();
        ldapProfile.setId("cn=admin,dc=example,dc=com");
        ldapProfile.setPassword("admin_password");

        LdaptiveAuthenticator ldapAuthenticator = 
            new LdaptiveAuthenticator("ldap://localhost:389",
                                     "dc=example,dc=com",
                                     "cn=%s,ou=users,dc=example,dc=com");

        UsernamePasswordCredentials credentials = 
            new UsernamePasswordCredentials("admin", "password");
        
        IndirectBasicAuthClient ldapClient = 
            new IndirectBasicAuthClient(ldapAuthenticator);

        Clients clients = new Clients("http://localhost:8080/callback", 
                                     ldapClient);
        
        return new Config(clients);
    }
}

プロファイル属性管理

@Service
public class ProfileService {

    public void handleUserProfile(HttpServletRequest request) {
        JEEContext context = new JEEContext(request, null);
        ProfileManager manager = new ProfileManager(context);
        
        Optional<UserProfile> profile = manager.get(true);
        if (profile.isPresent()) {
            UserProfile userProfile = profile.get();
            
            // 基本情報取得
            String userId = userProfile.getId();
            String displayName = userProfile.getDisplayName();
            String email = (String) userProfile.getAttribute("email");
            
            // ロール確認
            Set<String> roles = userProfile.getRoles();
            boolean isAdmin = roles.contains("ADMIN");
            
            // 権限確認
            Set<String> permissions = userProfile.getPermissions();
            boolean canRead = permissions.contains("READ");
            
            // カスタム属性
            String department = (String) userProfile.getAttribute("department");
            
            // ビジネスロジック処理
            processUserData(userId, displayName, email, department, isAdmin);
        }
    }

    private void processUserData(String userId, String displayName, 
                               String email, String department, boolean isAdmin) {
        // ユーザーデータ処理ロジック
    }
}

セッション管理とログアウト

@Controller
public class AuthController {

    @RequestMapping("/login")
    public String login(HttpServletRequest request, 
                       @RequestParam(required = false) String client) {
        JEEContext context = new JEEContext(request, null);
        SecurityLogic<Object, JEEContext> securityLogic = 
            DefaultSecurityLogic.INSTANCE;
        
        if (client != null) {
            // 特定クライアントでログイン
            return "redirect:/callback?client_name=" + client;
        }
        
        return "login";
    }

    @RequestMapping("/logout")
    public String logout(HttpServletRequest request, 
                        HttpServletResponse response) {
        JEEContext context = new JEEContext(request, response);
        ProfileManager manager = new ProfileManager(context);
        
        // ローカルログアウト
        manager.logout();
        
        // セッション無効化
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        
        return "redirect:/";
    }

    @RequestMapping("/profile")
    public String profile(HttpServletRequest request, Model model) {
        JEEContext context = new JEEContext(request, null);
        ProfileManager manager = new ProfileManager(context);
        
        Optional<UserProfile> profile = manager.get(true);
        if (profile.isPresent()) {
            model.addAttribute("profile", profile.get());
            model.addAttribute("attributes", profile.get().getAttributes());
        }
        
        return "profile";
    }
}