DEV Community

Marcelo
Marcelo

Posted on

Spring boot + Keycloak - protegendo suas APIs (parte 2)

Na primeira parte do tutorial 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:

spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/auth/realms/security-api/protocol/openid-connect/certs
Enter fullscreen mode Exit fullscreen mode

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.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
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:

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();
    }

}
Enter fullscreen mode Exit fullscreen mode

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

Criando os usuários:
usuários

Criando usuário:
usuário

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

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

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
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
forbidden
Perfeito... funcionando!
Já um token do usuário admin que tem a role admin o acesso ao /security/is-admin funciona:
request OK

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 e o código utilizado de exemplo esta no meu github

Grande abraço!

Discussion (0)