Spring Security e OpenID Connect
1. Visão geral
Neste tutorial rápido, vamos nos concentrar na configuração do OpenID Connect com uma implementação Spring Security OAuth2.
OpenID Connect é uma camada de identidade simples construída sobre o protocolo OAuth 2.0.
E, mais especificamente, aprenderemos como autenticar usuários usandoOpenID Connect implementation fromGoogle.
2. Configuração do Maven
Primeiro, precisamos adicionar as seguintes dependências ao nosso aplicativo Spring Boot:
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
3. The Id Token
Antes de nos aprofundarmos nos detalhes da implementação, vamos dar uma olhada rápida em como o OpenID funciona e como iremos interagir com ele.
Neste ponto, é claro, é importante já ter uma compreensão do OAuth2, uma vez que o OpenID é construído em cima do OAuth.
Primeiro, para usar a funcionalidade de identidade, usaremos um novo escopo OAuth2 chamadoopenid. This will result in an extra field in our Access Token – “id_token“.
Oid_token é um JWT (JSON Web Token) que contém informações de identidade sobre o usuário, assinadas pelo provedor de identidade (no nosso caso, Google).
Finalmente, ambos os fluxosserver(Authorization Code)eimplicit são as formas mais comumente usadas de obterid_token, em nosso exemplo, usaremosserver flow.
3. Configuração de cliente OAuth2
A seguir, vamos configurar nosso cliente OAuth2 - da seguinte maneira:
@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);
}
}
E aqui está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
Observe que:
-
Primeiro, você precisa obter credenciais OAuth 2.0 para seu aplicativo da web do Google deGoogle Developers Console.
-
Usamos o escopoopenid para obterid_token.
-
também usamos um escopo extraemail para incluir o e-mail do usuário nas informações de identidade deid_token.
-
O URI de redirecionamentohttp://localhost:8081/google-login é o mesmo usado em nosso aplicativo da web do Google.
4. Filtro de OpenID Connect personalizado
Agora, precisamos criar nosso próprioOpenIdConnectFilter personalizado para extrair a autenticação deid_token - da seguinte maneira:
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);
}
}
}
E aqui está nossoOpenIdConnectUserDetails simples:
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;
}
}
Observe que:
-
Spring SecurityJwtHelper para decodificarid_token.
-
id_token sempre contém o campo “sub” que é um identificador exclusivo para o usuário.
-
id_token também conterá o campo “email” conforme adicionamos o escopoemail à nossa solicitação.
4.1. Verificando o token de ID
No exemplo acima, usamos o métododecodeAndVerify() deJwtHelper para extrair informações deid_token,, mas também para validá-las.
A primeira etapa para isso é verificar se ele foi assinado com um dos certificados especificados no documentoGoogle Discovery.
Eles mudam cerca de uma vez por dia, então usaremos uma biblioteca de utilitários chamadajwks-rsa para lê-los:
com.auth0
jwks-rsa
0.3.0
Vamos adicionar o URL que contém os certificados ao arquivoapplication.properties:
google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs
Agora podemos ler esta propriedade e construir o objetoRSAVerifier:
@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());
}
Por fim, também verificaremos as declarações no token de id decodificado:
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");
}
}
O métodoverifyClaims() verifica se o token de id foi emitido pelo Google e se não expirou.
Você pode encontrar mais informações sobre isso emGoogle documentation.
5. Configuração de segurança
A seguir, vamos discutir nossa configuração de segurança:
@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();
}
}
Observe que:
-
Adicionamos nossoOpenIdConnectFilter personalizado apósOAuth2ClientContextFilter
-
Usamos uma configuração de segurança simples para redirecionar os usuários para “/google-login” para serem autenticados pelo Google
6. Controlador de Usuário
Em seguida, aqui está um controlador simples para testar nosso aplicativo:
@Controller
public class HomeController {
@RequestMapping("/")
@ResponseBody
public String home() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return "Welcome, " + username;
}
}
Resposta de amostra (após redirecionar para o Google para aprovar autoridades do aplicativo):
Welcome, [email protected]
7. Exemplo de processo de conexão OpenID
Por fim, vamos dar uma olhada em um exemplo de processo de autenticação do OpenID Connect.
Primeiro, vamos enviar umAuthentication 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
A resposta (after user approval) é um redirecionamento para:
http://localhost:8081/google-login?state=abc&code=xyz
A seguir, vamos trocar ocode por um token de acesso eid_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
Aqui está um exemplo de resposta:
{
"access_token": "SampleAccessToken",
"id_token": "SampleIdToken",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "SampleRefreshToken"
}
Finalmente, aqui está a aparência das informações dosid_token reais:
{
"iss":"accounts.google.com",
"at_hash":"AccessTokenHash",
"sub":"12345678",
"email_verified":true,
"email":"[email protected]",
...
}
Assim, você pode ver imediatamente o quão útil as informações do usuário dentro do token são para fornecer informações de identidade para nosso próprio aplicativo.
8. Conclusão
Neste tutorial de introdução rápida, aprendemos como autenticar usuários usando a implementação do OpenID Connect do Google.
E, como sempre, você pode encontrar o código-fonteover on GitHub.