Spring Security и OpenID Connect

Spring Security и OpenID Connect

1. обзор

В этом кратком руководстве мы сосредоточимся на настройке OpenID Connect с реализацией Spring Security OAuth2.

OpenID Connect - это простой уровень идентификации, построенный на основе протокола OAuth 2.0.

И, более конкретно, мы узнаем, как аутентифицировать пользователей с помощьюOpenID Connect implementation fromGoogle.

2. Конфигурация Maven

Во-первых, нам нужно добавить следующие зависимости в наше приложение Spring Boot:


    org.springframework.boot
    spring-boot-starter-security


    org.springframework.security.oauth
    spring-security-oauth2

3. Идентификационный токен

Прежде чем мы углубимся в детали реализации, давайте кратко рассмотрим, как работает OpenID и как мы будем с ним взаимодействовать.

На этом этапе, конечно, важно уже иметь представление о OAuth2, поскольку OpenID построен поверх OAuth.

Во-первых, чтобы использовать функцию идентификации, мы воспользуемся новой областью действия OAuth2 под названиемopenid. This will result in an extra field in our Access Token – “id_token“.

id_token - это JWT (веб-токен JSON), который содержит идентификационную информацию о пользователе, подписанный поставщиком удостоверений (в нашем случае Google).

Наконец, потокиserver(Authorization Code) иimplicit являются наиболее часто используемыми способами полученияid_token, в нашем примере мы будем использоватьserver flow.

3. Конфигурация клиента OAuth2

Затем давайте настроим нашего клиента OAuth2 следующим образом:

@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {
    @Value("${google.clientId}")
    private String clientId;

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

    @Value("${google.accessTokenUri}")
    private String accessTokenUri;

    @Value("${google.userAuthorizationUri}")
    private String userAuthorizationUri;

    @Value("${google.redirectUri}")
    private String redirectUri;

    @Bean
    public OAuth2ProtectedResourceDetails googleOpenId() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setScope(Arrays.asList("openid", "email"));
        details.setPreEstablishedRedirectUri(redirectUri);
        details.setUseCurrentUri(false);
        return details;
    }

    @Bean
    public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(googleOpenId(), clientContext);
    }
}

А вотapplication.properties:

google.clientId=
google.clientSecret=
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login

Обратите внимание, что:

  • Сначала вам нужно получить учетные данные OAuth 2.0 для вашего веб-приложения Google изGoogle Developers Console.

  • Мы использовали область видимостиopenid, чтобы получитьid_token.

  • мы также использовали дополнительную областьemail, чтобы включить адрес электронной почты пользователя в идентификационную информациюid_token.

  • URI перенаправленияhttp://localhost:8081/google-login тот же, что и в нашем веб-приложении Google.

4. Пользовательский фильтр OpenID Connect

Теперь нам нужно создать наш собственныйOpenIdConnectFilter для извлечения аутентификации изid_token - следующим образом:

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

    public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
    }
    @Override
    public Authentication attemptAuthentication(
      HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException {
        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            throw new BadCredentialsException("Could not obtain access token", e);
        }
        try {
            String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
            String kid = JwtHelper.headers(idToken).get("kid");
            Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
            Map authInfo = new ObjectMapper()
              .readValue(tokenDecoded.getClaims(), Map.class);
            verifyClaims(authInfo);
            OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        } catch (InvalidTokenException e) {
            throw new BadCredentialsException("Could not obtain user details from token", e);
        }
    }
}

А вот и наш простойOpenIdConnectUserDetails:

public class OpenIdConnectUserDetails implements UserDetails {
    private String userId;
    private String username;
    private OAuth2AccessToken token;

    public OpenIdConnectUserDetails(Map userInfo, OAuth2AccessToken token) {
        this.userId = userInfo.get("sub");
        this.username = userInfo.get("email");
        this.token = token;
    }
}

Обратите внимание, что:

  • Spring SecurityJwtHelper для декодированияid_token.

  • id_token всегда содержит поле «sub”, которое является уникальным идентификатором пользователя.

  • id_token также будет содержать поле «email», поскольку мы добавили областьemail к нашему запросу.

4.1. Проверка идентификатора токена

В приведенном выше примере мы использовали методdecodeAndVerify() дляJwtHelper для извлечения информации изid_token,, а также для ее проверки.

Первым шагом для этого является проверка того, что он был подписан одним из сертификатов, указанных в документеGoogle Discovery.

Они меняются примерно раз в день, поэтому мы будем использовать служебную библиотекуjwks-rsa для их чтения:


    com.auth0
    jwks-rsa
    0.3.0

Добавим URL, содержащий сертификаты, в файлapplication.properties:

google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs

Теперь мы можем прочитать это свойство и построить объектRSAVerifier:

@Value("${google.jwkUrl}")
private String jwkUrl;

private RsaVerifier verifier(String kid) throws Exception {
    JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
    Jwk jwk = provider.get(kid);
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}

Наконец, мы также проверим утверждения в декодированном токене идентификатора:

public void verifyClaims(Map claims) {
    int exp = (int) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();
    if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
      !claims.get("aud").equals(clientId)) {
        throw new RuntimeException("Invalid claims");
    }
}

МетодverifyClaims() проверяет, был ли токен идентификатора выпущен Google и не истек ли срок его действия.

Вы можете найти больше информации об этом вGoogle documentation.

5. Конфигурация безопасности

Затем давайте обсудим нашу конфигурацию безопасности:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Bean
    public OpenIdConnectFilter openIdConnectFilter() {
        OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
        filter.setRestTemplate(restTemplate);
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .addFilterAfter(new OAuth2ClientContextFilter(),
          AbstractPreAuthenticatedProcessingFilter.class)
        .addFilterAfter(OpenIdConnectFilter(),
          OAuth2ClientContextFilter.class)
        .httpBasic()
        .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
        .and()
        .authorizeRequests()
        .anyRequest().authenticated();
    }
}

Обратите внимание, что:

  • Мы добавили собственныйOpenIdConnectFilter послеOAuth2ClientContextFilter

  • Мы использовали простую конфигурацию безопасности для перенаправления пользователей на «/google-login» для аутентификации в Google.

6. Пользовательский контроллер

Далее, вот простой контроллер для тестирования нашего приложения:

@Controller
public class HomeController {
    @RequestMapping("/")
    @ResponseBody
    public String home() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Welcome, " + username;
    }
}

Пример ответа (после перенаправления в Google для утверждения полномочий приложения):

Welcome, [email protected]

7. Пример процесса OpenID Connect

Наконец, давайте взглянем на образец процесса аутентификации OpenID Connect.

Сначала мы отправимAuthentication Request:

https://accounts.google.com/o/oauth2/auth?
    client_id=sampleClientID
    response_type=code&
    scope=openid%20email&
    redirect_uri=http://localhost:8081/google-login&
    state=abc

Ответ (after user approval) - это перенаправление на:

http://localhost:8081/google-login?state=abc&code=xyz

Далее мы собираемся обменятьcode на токен доступа иid_token:

POST https://www.googleapis.com/oauth2/v3/token
    code=xyz&
    client_id= sampleClientID&
    client_secret= sampleClientSecret&
    redirect_uri=http://localhost:8081/google-login&
    grant_type=authorization_code

Вот пример ответа:

{
    "access_token": "SampleAccessToken",
    "id_token": "SampleIdToken",
    "token_type": "bearer",
    "expires_in": 3600,
    "refresh_token": "SampleRefreshToken"
}

Наконец, вот как выглядит информация о фактическомid_token:

{
    "iss":"accounts.google.com",
    "at_hash":"AccessTokenHash",
    "sub":"12345678",
    "email_verified":true,
    "email":"[email protected]",
     ...
}

Таким образом, вы можете сразу увидеть, насколько полезна информация пользователя внутри токена для предоставления идентификационной информации нашему приложению.

8. Заключение

В этом кратком вводном руководстве мы узнали, как аутентифицировать пользователей с помощью реализации OpenID Connect от Google.

И, как всегда, вы можете найти исходный кодover on GitHub.