0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring SecurityとAWS Cognitoの連携

Posted at

Spring SecurityとAWS Cognitoの連携

はじめに

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

基本的な設定

1. 依存関係の追加

<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の設定

@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認証プロバイダーの実装

@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. セキュリティ設定

@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. ユーザー登録

@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. ユーザー認証

@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トークンの検証

@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. ユーザー管理

    • パスワードポリシーの設定
    • 多要素認証の実装
    • アカウントロックアウトの設定

実装例:ログインコントローラー

@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セキュリティガイドライン
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?