Spring boot + Keycloak - protegendo suas APIs (parte 2)
Tags: java, springboot, keycloak
Published: 2021-07-30 16:48:51

Na primeira parte do [tutorial](https://dev.to/mmacorin/spring-boot-keycloak-protegendo-suas-apis-parte-1-436o) foi incluído uma segurança básica nas nossas APIs. Nesta segunda parte vamos adicionar regras de autorização utilizando os authorities do contidos no nosso token JWT.
Em primeiro lugar, vamos alterar nosso *application.yaml* e mudar a propriedade e o endpoint do keycloak utilizado. Agora vai ficar assim:

```yaml
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/auth/realms/security-api/protocol/openid-connect/certs
```
Lembrando que nesse endpoint ***security-api*** é o nome do nosso realm criado no keycloak.

Vamos agora criar uma classe que vai ser responsável por extrair as authorities do token JWT criado pelo Keycloak.
```java
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Classe responsavel por converter o token gerado pelo Keycloak
 * de maneira a atribuir os authorities do jeito que o spring security
 * utiliza.
 */
@Component
@RequiredArgsConstructor
public class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    private final ObjectMapper objectMapper;

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        Collection<GrantedAuthority> authorities = Stream
                .concat(defaultGrantedAuthoritiesConverter.convert(jwt).stream()
                        , extractAuthorities(jwt).stream())
                .collect(Collectors.toSet());
        return new JwtAuthenticationToken(jwt, authorities);
    }

    /**
     * orquestra a extração dos authorities
     * @param jwt
     * @return Collection<GrantedAuthority> contendo as roles e scopes do jwt
     */
    private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
        Set<String> rolesWithPrefix = new HashSet<>();
        rolesWithPrefix.addAll(getRealmRoles(jwt));
        rolesWithPrefix.addAll(getResourceRoles(jwt));
        return AuthorityUtils.createAuthorityList(rolesWithPrefix.toArray(new String[0]));
    }

    /**
     * Busca as roles de acesso ao realm
     * @param jwt
     * @return uma colection contendo as roles
     */
    private Set<String> getRealmRoles(Jwt jwt) {
        Set<String> rolesWithPrefix = new HashSet<>();
        JsonNode json = objectMapper.convertValue(jwt.getClaim("realm_access"), JsonNode.class);
        json.elements().forEachRemaining(
                e -> e.elements().forEachRemaining(r -> rolesWithPrefix.add(createRole(r.asText()))));
        return rolesWithPrefix;
    }

    /**
     * Busca as roles de acesso a determinado resource
     * @param jwt
     * @return uma colection contendo as roles
     */
    private Set<String> getResourceRoles(Jwt jwt) {
        Set<String> rolesWithPrefix = new HashSet<>();
        Map<String, JsonNode> map = objectMapper.convertValue(jwt.getClaim("resource_access"), new TypeReference<Map<String, JsonNode>>(){});
        for (Map.Entry<String, JsonNode> jsonNode : map.entrySet()) {
            jsonNode
                    .getValue()
                    .elements()
                    .forEachRemaining(e -> e
                            .elements()
                            .forEachRemaining(r -> rolesWithPrefix.add(createRole(jsonNode.getKey(), r.asText()))));
        }
        return rolesWithPrefix;
    }

    private String createRole(String... values) {
        StringBuilder role = new StringBuilder("ROLE");
        for (String value : values) {
            role.append("_").append(value.toUpperCase());
        }
        return role.toString();
    }
}
```

Adicionei pra cada role um prefixo *ROLE_* e para as roles de determinado resource o prefixo inclui também o nome do resource.
Agora vamos criar uma classe para configuração do spring security.

```java
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;

    @Override
    public void configure(final HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authz -> authz.antMatchers("/security/**").authenticated())
                .oauth2ResourceServer()
                .jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);
    }
}
```

Aqui, além de habilitar a segurança, habilitei também o ***@PreAuthorize*** e o ***@PostAuthorize***. Estas anotações recebem expressões, escritas em  SpEL (Spring Expression Language), e trabalham validando no token se o cliente da requisição possui permissões para acessar aquele método ou ter acesso ao que ocorreu após a execução do método, como os nomes das anotações sugerem. Você pode encontrar mais sobre *expression-based access* control na documentação do spring neste [link](https://docs.spring.io/spring-security/site/docs/3.0.x/reference/el-access.html)
Nas configurações do spring security, informei que apenas endpoint s iniciados por ***/security*** precisam ser validados e setei a classe que criamos acima como um converter para o token JWT.

Pra testar, vamos criar os endpoints necessários:

```java
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/security")
public class SecurityResource {

    /**
     * endpoint sem nenhuma validação de role
     */
    @GetMapping
    public ResponseEntity<Void> isAuthenticated() {
        return ResponseEntity.ok().build();
    }

    /**
     * endpoint onde o usuario tem que ter a role user
     */
    @GetMapping(value = "/has-role")
    @PreAuthorize("hasAnyAuthority('ROLE_USER')")
    public ResponseEntity<Void> isUser() {
        return ResponseEntity.ok().build();
    }

    /**
     * endpoint onde o usuario tem que ter a role admin
     */
    @GetMapping(value = "/is-admin")
    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    public ResponseEntity<Void> isAdmin() {
        return ResponseEntity.ok().build();
    }

}
```

No nosso projeto esta tudo pronto. Qualquer endpoint que iniciar com ***security*** necessita do token JWT criado pelo nosso Keycloak e conseguimos validar os authorities contidos neste JWT utilizando as anotações do spring.

Agora no Keycloak precisamos criar as roles e alguns usuários para o teste.

Criando as roles:
![roles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2s8p9pxjdeggrzhisnqv.png)

Criando os usuários:
![usuários](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hra657o3tju7nquzx9y2.png)

Criando usuário:
![usuário](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ws4icycudm3iplssgwkz.png)

Depois de criado, na aba *Credentials* adicione uma senha, altere o *Temporary* pra OFF e clique em *Set Password* pra criar a senha do usuário.
![informando a senha](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72kktoczvgmznqj7bc9a.png)

Agora vamos setar a role ao usuário. Criamos 2 roles (admin e user) e criamos 3 usuários (admin e user). Vamos setar a role de acordo com o usuário criado.
Na aba *Role Mappings* selecione a role no quadro *Available Roles* e clique no botão *Add selected* abaixo do quadro:
![setando a role](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dgel6fokc1s9gotc127b.png)

Pronto, vamos testar.
No postman, naquele mesmo request do tutorial anterior, vamos alterar o formato do token que vamos requisitar. Pra isso altere a key *grant_type* para *password*. Além disto, adicione as keys *username* e *password* com seus respectivos valores.
![token de usuário](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zibbic6nfi1327lf1uz0.png)
Neste primeiro exemplo busquei o token com o usuário *user* e este usuário tem apenas a role *user*. Nos endpoints que criamos o usuário com esta role teria acesso ao endpoint */security/has-role* mas não ao */security/is-admin*.
![request OK](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ff9payf37ojms107mvgv.png)
![forbidden](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fjeuaj26mrt9wha8ca7.png)
Perfeito... funcionando!
Já um token do usuário *admin* que tem a role *admin* o acesso ao */security/is-admin* funciona:
![request OK](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q29cou2jep006ghx162k.png)

Bom, é isso!
Configuramos o Keycloak para nos permitir criar tokens, autenticando usuários e atribuindo a eles determinadas permissões. Configuramos nosso projeto spring para validar este token e utilizar os dados contidos no JWT para dar autorizações para os usuários dentro da nossa aplicação.

A primeira parte deste tutorial esta [aqui](https://dev.to/mmacorin/spring-boot-keycloak-protegendo-suas-apis-parte-1-436o) e o código utilizado de exemplo esta no meu [github](https://github.com/msmacorin/spring-boot-keycloak)

Grande abraço!