java 使用cognito作为Spring授权服务器的IDP

sg3maiej  于 2023-04-28  发布在  Java
关注(0)|答案(2)|浏览(128)

bounty还有2天到期。回答此问题可获得+50声望奖励。Sercan Ozdemir希望引起更多关注这个问题。

我知道Cognito本身通常是Oauth2流中的一个授权服务器角色。但是根据我的定制需求,我想使用spring authorization server和Cognito,基本上:

  • 客户端使用其客户端ID访问oauth2/authorize端点
  • 重定向到登录页面,spring将知道哪个客户端正在尝试用户登录。
  • 客户端在成功登录后从spring收到一个auth代码(通过Cognito,所以auth代码来自spring,但登录应该通过cognito)
  • 然后向Spring的token端点发送POST请求,以从Cognito接收access/ID/refresh token。
  • 所有应用程序客户端将从Cognito用户池中获取。

我看到了JDBC的例子,但不能真正让它与Cognito一起工作,任何帮助都将不胜感激。

ttcibm8c

ttcibm8c1#

在这方面,我发现来自Ryosuke Uchitate的演示文稿“Want to authenticate using Amazon Cognito? Then use Spring Security!”很有启发性,关于Amazon Cognito与Spring的集成。
你可以在b1a9id/spring-security-with-cognito(2018)找到他的实现

一般的想法是使用CognitoServiceAutoWired annotation来提供AbstractUserDetailsAuthenticationProvider

@Component
public class CustomCognitoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private CognitoService cognitoService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // Perform additional authentication checks if needed.
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        String password = authentication.getCredentials().toString();

        // Call your CognitoService to authenticate the user with the provided credentials.
        return cognitoService.authenticate(username, password);
    }
}

对于Cognito服务,您需要将AWS Java SDK for Cognito Identity Provider添加到项目的依赖项中。如果您使用的是Maven,请将以下内容添加到pom.xml

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-cognitoidp</artifactId>
    <version>1.12.454</version>
</dependency>

服务本身需要适当的AWS凭证、区域和Amazon Cognito配置(客户端ID、客户端密码):

@Service
public class CognitoService {

    @Value("${cognito.clientId}")
    private String clientId;

    @Value("${cognito.clientSecret}")
    private String clientSecret;

    @Value("${cognito.userPoolId}")
    private String userPoolId;

    private AWSCognitoIdentityProvider cognitoIdentityProvider;

    @PostConstruct
    public void init() {
        // Initialize your Amazon Cognito Identity Provider with your AWS credentials.
        // Consider using a proper credential provider to avoid hardcoding credentials.
        AWSCredentials awsCredentials = new BasicAWSCredentials("YOUR_AWS_ACCESS_KEY", "YOUR_AWS_SECRET_KEY");
        AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);

        // Create the Amazon Cognito client.
        this.cognitoIdentityProvider = AWSCognitoIdentityProviderClientBuilder.standard()
                .withCredentials(credentialsProvider)
                .withRegion(Regions.YOUR_AWS_REGION)
                .build();
    }

    public UserDetails authenticate(String username, String password) {
        // Create the authentication request.
        AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest()
                .withAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH)
                .withClientId(clientId)
                .withUserPoolId(userPoolId)
                .withAuthParameters(authParameters(username, password));

        try {
            // Perform the authentication request.
            AdminInitiateAuthResult authResult = cognitoIdentityProvider.adminInitiateAuth(authRequest);

            // If the user is authenticated, return a UserDetails object.
            return new User(username, password, new ArrayList<>());
        } catch (NotAuthorizedException | UserNotFoundException e) {
            throw new BadCredentialsException("Invalid username or password", e);
        } catch (Exception e) {
            throw new AuthenticationServiceException("Error during authentication", e);
        }
    }

    private Map<String, String> authParameters(String username, String password) {
        Map<String, String> authParameters = new HashMap<>();
        authParameters.put("USERNAME", username);
        authParameters.put("PASSWORD", password);
        authParameters.put("SECRET_HASH", calculateSecretHash(clientId, clientSecret, username));

        return authParameters;
    }

    private String calculateSecretHash(String clientId, String clientSecret, String username) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);

            String data = username + clientId;
            byte[] digest = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(digest);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new AuthenticationServiceException("Error calculating the secret hash", e);
        }
    }
}

然后您可以配置您的Spring Security以使用自定义身份验证提供程序并设置授权服务器。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomCognitoAuthenticationProvider customCognitoAuthenticationProvider;

    @Autowired
    private OAuth2AuthorizationServerConfiguration oAuth2AuthorizationServerConfiguration;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authenticationProvider(customCognitoAuthenticationProvider)
            .apply(oAuth2AuthorizationServerConfiguration)
            .and()
            .authorizeRequests()
            .antMatchers("/oauth2/authorize").authenticated()
            .anyRequest().permitAll();
    }
}

但是:如documented here
Spring Security 5.7.0-M2(2月2022)我们弃用了WebSecurityConfigurerAdapter,因为我们鼓励用户转向基于组件的安全配置。
Spring Security 5.4(Sep.2020)我们引入了通过创建SecurityFilterChain bean来配置HttpSecurity的功能。
这意味着更现代的实现将使用SecurityFilterChain bean:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

您需要将原始参数(如.authenticationProvider(customCognitoAuthenticationProvider))传递给新的HttpSecurity http,如“WebSecurityConfigurerAdapter Deprecated in Spring Boot”或“Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated”中所示。
然后,使用自定义令牌存储(使用Amazon Cognito的access/ID/refresh令牌)配置Spring Authorization Server(OAuth2AuthorizationServerConfiguration)。

@Configuration
public class OAuth2AuthorizationServerConfiguration {

    @Autowired
    private CustomTokenStore customTokenStore;

    public OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServer() {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
        authorizationServerConfigurer
            .tokenStore(customTokenStore)
            .authorizationEndpoint()
            .baseUri("/oauth2/authorize");

        return authorizationServerConfigurer;
    }
}

这意味着要实现一个定制的TokenStore,它应该被设计为处理OAuth2令牌(访问令牌、ID令牌和刷新令牌)的存储和检索。
在您的案例中:由Amazon Cognito发行的代币。
内存中的(不适合生产)将以以下开头:

public class CustomTokenStore implements TokenStore {

    private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, OAuth2RefreshToken> refreshTokenStore = new ConcurrentHashMap<>();

    @Override
    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        return accessTokenStore.values()
                .stream()
                .filter(token -> authentication.equals(token.getOAuth2Authentication()))
                .findFirst()
                .orElse(null);
    }
    ...

正如“TokenStore in Spring Security 5.x after removal of Spring Security OAuth 2.x”中所指出的,您需要检查这是否与Spring Security 5的最新版本兼容。X/6.0+。
根据我的理解(但无法正确测试),使用您的步骤:
1.客户端使用其客户端ID访问/oauth2/authorize端点:
SecurityConfig.java中:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/cognito/callback", "/login**").permitAll()
            .anyRequest().authenticated()
        .and()
        .oauth2Login()
            .authorizationEndpoint()
                .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                .baseUri("/oauth2/authorize")
                ...
}

如上所述,如果您使用的是Spring Security 5,则应该在SecurityFilterChain中完成此操作。X/6.x。
1.重定向到登录页面,Spring将知道哪个客户端正在尝试用户登录:
application.yml中:

spring:
  security:
    oauth2:
      client:
        registration:
          cognito:
            client-id: your-cognito-app-client-id
            client-secret: your-cognito-app-client-secret
            ...
        provider:
          cognito:
            issuer-uri: https://cognito-idp.your-aws-region.amazonaws.com/your-cognito-user-pool-id

1.客户端在成功登录后从Spring收到一个auth代码(通过Cognito,所以auth代码来自Spring,但登录应该通过Cognito):
SecurityConfig.java中:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        ...
        .oauth2Login()
            ...
            .redirectionEndpoint()
                .baseUri("/cognito/callback")
                ...
}

在SecurityConfig中配置的重定向端点将负责在成功登录后处理来自Amazon Cognito的回调。验证代码实际上来自Amazon Cognito,而不是Spring。
含义:

  • 用户通过Amazon Cognito登录页面登录。
  • 成功登录后,Amazon Cognito会将用户发送回配置的回调URL(例如:例如,/cognito/callback)沿着作为查询参数的授权码。
  • Spring Security处理对/cognito/callback端点的请求,并从查询参数中提取授权代码。

1.然后向Spring的令牌端点发送POST请求,以接收Cognito的access/ID/refresh令牌:
CognitoOAuth2LoginSuccessHandler.java中:

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    OAuth2AuthenticationToken oAuth2Authentication = (OAuth2AuthenticationToken) authentication;

    // Retrieve the authorized client.
    OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(oAuth2Authentication.getAuthorizedClientRegistrationId(), oAuth2Authentication.getName());

    // Extract access token, ID token, and refresh token from the authorized client.
    OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
    OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
    String idToken = authorizedClient.getAdditionalParameters().get("id_token").toString();

    // Store tokens or use them as needed.

    // Redirect the user to the desired page after successful login.
    getRedirectStrategy().sendRedirect(request, response, "/success-page");
}

1.所有应用程序客户端将从Cognito用户池中获取:
CognitoOAuth2UserService.java中:

@Service
public class CognitoOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // Extract user attributes from the OAuth2UserRequest.
        Map<String, Object> userAttributes = userRequest.getUserInfo().getAttributes();
        return new DefaultOAuth2User(Collections.singleton(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()), userAttributes, userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
    }
}

关于OAuth2AuthorizationService:应该不需要直接替换OAuth2AuthorizationService
Spring Security对OAuth2的内置支持应该负责管理授权代码流,包括将授权代码发送到Amazon Cognito的令牌端点,并将其交换为访问、ID和刷新令牌。
CognitoOAuth2UserService负责从Amazon Cognito加载用户详细信息,而CognitoOAuth2LoginSuccessHandler处理成功的身份验证并提供对令牌的访问。

fnvucqvd

fnvucqvd2#

我想我应该在这里添加一些关于OAuth架构的注解,以及我是如何看待它的,因为你的问题有几点看起来不太正确。我不能帮助你在Spring的细节。

角色

  • Client:通过Spring AS触发用户登录
  • 授权服务器:向客户端发出令牌
  • 身份提供者:多种可能的登录方法之一

客户端在AS处实现代码流。AS向IDP运行另一个代码流。将这些系统链接在一起是非常标准的,应该只需要配置,客户机中的代码更改为零。

注册

  • 客户端仅在Spring AS中注册
  • Spring AS在AWS Cognito中注册为客户端
  • AWS Cognito在Spring AS中注册为身份验证方法(IDP)
    已发行代币

客户端总是从AS而不是IDP接收令牌。AS发出令牌来保护您的业务数据。它使您能够发出锁定令牌所需的任何范围和声明。

上游代币

客户端和API通常不需要处理来自IDP的令牌。有时会有例外,例如也使用AWS令牌来访问用户的AWS资源。
如果这是您的要求,请使用嵌入式令牌。这意味着SpringAS发布IDP令牌作为AS令牌的自定义声明。这使您的API能够继续正确授权,同时还能够在需要时访问AWS资源。

相关问题