Spring SecurityとAWS Cognitoの連携

# Spring SecurityとAWS Cognitoの連携

## はじめに

AWS Cognitoは、ユーザー認証とアクセス制御を提供するマネージドサービスです。この記事では、Spring SecurityとAWS Cognitoを連携させ、安全な認証・認可システムを構築する方法について解説します。

## 基本的な設定

### 1. 依存関係の追加

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-cognitoidp</artifactId>
    <version>1.12.261</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
```

### 2. AWS Cognitoの設定

```java
@Configuration
public class CognitoConfig {
    
    @Value("${aws.cognito.region}")
    private String region;
    
    @Value("${aws.cognito.userPoolId}")
    private String userPoolId;
    
    @Value("${aws.cognito.clientId}")
    private String clientId;
    
    @Bean
    public AWSCognitoIdentityProvider cognitoClient() {
        return AWSCognitoIdentityProviderClientBuilder.standard()
            .withRegion(region)
            .build();
    }
}
```

## 認証の実装

### 1. Cognito認証プロバイダーの実装

```java
@Component
public class CognitoAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private AWSCognitoIdentityProvider cognitoClient;
    
    @Value("${aws.cognito.userPoolId}")
    private String userPoolId;
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        
        try {
            AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest()
                .withUserPoolId(userPoolId)
                .withClientId(clientId)
                .withAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH)
                .withAuthParameters(new HashMap<String, String>() {{
                    put("USERNAME", username);
                    put("PASSWORD", password);
                }});
                
            AdminInitiateAuthResult authResult = cognitoClient.adminInitiateAuth(authRequest);
            
            if (authResult.getAuthenticationResult() != null) {
                return new UsernamePasswordAuthenticationToken(
                    username,
                    password,
                    getAuthorities(authResult.getAuthenticationResult().getIdToken())
                );
            }
        } catch (Exception e) {
            throw new BadCredentialsException("認証に失敗しました", e);
        }
        
        throw new BadCredentialsException("認証に失敗しました");
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(String idToken) {
        // JWTトークンから権限情報を抽出
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
```

### 2. セキュリティ設定

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private CognitoAuthenticationProvider cognitoAuthenticationProvider;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .authenticationProvider(cognitoAuthenticationProvider)
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
            
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthoritiesClaimName("cognito:groups");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}
```

## ユーザー管理

### 1. ユーザー登録

```java
@Service
public class CognitoUserService {
    
    @Autowired
    private AWSCognitoIdentityProvider cognitoClient;
    
    @Value("${aws.cognito.userPoolId}")
    private String userPoolId;
    
    public void registerUser(UserRegistrationDto dto) {
        try {
            AdminCreateUserRequest createUserRequest = new AdminCreateUserRequest()
                .withUserPoolId(userPoolId)
                .withUsername(dto.getUsername())
                .withTemporaryPassword(dto.getPassword())
                .withUserAttributes(
                    new AttributeType().withName("email").withValue(dto.getEmail()),
                    new AttributeType().withName("email_verified").withValue("true")
                );
                
            cognitoClient.adminCreateUser(createUserRequest);
            
            // パスワードを永続化
            AdminSetUserPasswordRequest setPasswordRequest = new AdminSetUserPasswordRequest()
                .withUserPoolId(userPoolId)
                .withUsername(dto.getUsername())
                .withPassword(dto.getPassword())
                .withPermanent(true);
                
            cognitoClient.adminSetUserPassword(setPasswordRequest);
        } catch (Exception e) {
            throw new RuntimeException("ユーザー登録に失敗しました", e);
        }
    }
}
```

### 2. ユーザー認証

```java
@Service
public class CognitoAuthService {
    
    @Autowired
    private AWSCognitoIdentityProvider cognitoClient;
    
    public AuthenticationResult authenticateUser(String username, String password) {
        try {
            AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest()
                .withUserPoolId(userPoolId)
                .withClientId(clientId)
                .withAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH)
                .withAuthParameters(new HashMap<String, String>() {{
                    put("USERNAME", username);
                    put("PASSWORD", password);
                }});
                
            AdminInitiateAuthResult authResult = cognitoClient.adminInitiateAuth(authRequest);
            return authResult.getAuthenticationResult();
        } catch (Exception e) {
            throw new RuntimeException("認証に失敗しました", e);
        }
    }
}
```

## トークン検証

### 1. JWTトークンの検証

```java
@Component
public class CognitoJwtValidator {
    
    @Value("${aws.cognito.userPoolId}")
    private String userPoolId;
    
    @Value("${aws.cognito.region}")
    private String region;
    
    public Jwt validateToken(String token) {
        try {
            String jwksUrl = String.format(
                "https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json",
                region,
                userPoolId
            );
            
            JwkProvider provider = new UrlJwkProvider(new URL(jwksUrl));
            Jwt jwt = Jwts.parserBuilder()
                .setSigningKeyResolver(new SigningKeyResolverAdapter() {
                    @Override
                    public Key resolveSigningKey(JwsHeader header, Claims claims) {
                        try {
                            return provider.get(header.getKeyId()).getPublicKey();
                        } catch (Exception e) {
                            throw new RuntimeException("トークンの検証に失敗しました", e);
                        }
                    }
                })
                .build()
                .parseClaimsJws(token);
                
            return jwt;
        } catch (Exception e) {
            throw new RuntimeException("トークンの検証に失敗しました", e);
        }
    }
}
```

## ベストプラクティス

1. **セキュリティ設定**
   - 適切なCORS設定
   - セキュリティヘッダーの設定
   - トークンの有効期限管理

2. **エラーハンドリング**
   - Cognitoのエラーコードに応じた適切な処理
   - ユーザーフレンドリーなエラーメッセージ

3. **トークン管理**
   - リフレッシュトークンの適切な処理
   - トークンの安全な保存

4. **ユーザー管理**
   - パスワードポリシーの設定
   - 多要素認証の実装
   - アカウントロックアウトの設定

## 実装例：ログインコントローラー

```java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private CognitoAuthService authService;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            AuthenticationResult authResult = authService.authenticateUser(
                request.getUsername(),
                request.getPassword()
            );
            
            return ResponseEntity.ok(new LoginResponse(
                authResult.getIdToken(),
                authResult.getAccessToken(),
                authResult.getRefreshToken()
            ));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(new ErrorResponse("認証に失敗しました"));
        }
    }
}
```

## まとめ

Spring SecurityとAWS Cognitoを連携させることで、安全でスケーラブルな認証・認可システムを構築することができます。適切な設定と実装により、企業レベルのセキュリティを実現できます。

## 参考資料

- AWS Cognito公式ドキュメント
- Spring Security公式ドキュメント
- OWASPセキュリティガイドライン