Search code examples
spring-bootjwtgrpc

Any idea to have the jwt in spring boot grpc project


Currently, I am using yidongnan/grpc-spring-boot-starter project to implement the grpc in spring-boot project.

Any idea to have the jwt in the grpc for authentication?

I have the following code, this only work when the client does not have the authentication.

Not sure how to revise it.

Clent Configuration

@Slf4j
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {

    @Value("${auth.token}")
    private String token;

    @Bean
    public StubTransformer call() {
        return (name, stub) -> {
            if ("local-grpc-server".equals(name)) {
                return stub.withCallCredentials(CallCredentialsHelper.bearerAuth(token)); // remove this line, the request be passed.
            }
            return stub;
        };
    }

}

Server Configuration & Impementation

@Slf4j
@Component
public class JwtTokenProvider {

    private static final String AUTHORITIES_KEY = "roles";

    private final JwtProperties jwtProperties;

    @Getter
    private NimbusJwtDecoder jwtDecoder;

    private String secretKey;

    public JwtTokenProvider(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
        secretKey = Base64.getEncoder().encodeToString(jwtProperties.getSignKey().getBytes());
        this.jwtDecoder = NimbusJwtDecoder.withSecretKey(stringToSecretKey(secretKey)).build();
    }

    public SecretKey stringToSecretKey(String secret) {
        byte[] decodedKey = Base64.getDecoder().decode(secret.getBytes(StandardCharsets.UTF_8));
        return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
    }

    public String createToken(Authentication authentication) {
        String username = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Claims claims = Jwts.claims().setSubject(username);
        if (!authorities.isEmpty()) {
            claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        }

        Date now = new Date();
        Date validity = new Date(now.getTime() + this.jwtProperties.getExpireTimeAsSec() * 1000);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(SignatureAlgorithm.HS256, this.secretKey)
                .compact();

    }

    public Authentication getAuthentication(String token) {
        Jwt jwt = null;
        try {
            jwt = jwtDecoder.decode(token);
        } catch (JwtException e) {
            throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
        }

        Object authoritiesClaim = jwt.getClaims().get(AUTHORITIES_KEY);
        Collection<? extends GrantedAuthority> authorities = authoritiesClaim == null ? AuthorityUtils.NO_AUTHORITIES
                : AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesClaim.toString());

        return new JwtAuthenticationToken(jwt, authorities);
    }

    public boolean validateToken(Jwt token) {
        try {
            OAuth2TokenValidatorResult result = JwtValidators.createDefault().validate(token);
            return !result.hasErrors();
        } catch (JwtException | IllegalArgumentException e) {
            throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
        }
    }

}

createToken: generate the token.

@Repository
public class UserRepository {

    private static final Map<String, UserDetails> allUsers = new HashMap<>();

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    protected void init() {
        allUsers.put("pkslow", new UserDetailsImpl("pkslow", passwordEncoder.encode("123456"), new SimpleGrantedAuthority("ROLE_ADMIN")));
        allUsers.put("user", new UserDetailsImpl("user", passwordEncoder.encode("123456"), new SimpleGrantedAuthority("ROLE_USER")));
    }

    public Optional<UserDetails> findByUsername(String username) {
        return Optional.ofNullable(allUsers.get(username));
    }
}
@RequiredArgsConstructor
@Component
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository users;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found"));
    }
}
@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true)
public class SecurityConfiguration {

    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider(
            final CustomUserDetailsService userDetailsService,
            final PasswordEncoder passwordEncoder) {

        final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }

    @Bean
    AuthenticationManager authenticationManager(final DaoAuthenticationProvider daoAuthenticationProvider) {
        final List<AuthenticationProvider> providers = new ArrayList<>();
        providers.add(daoAuthenticationProvider);
        return new ProviderManager(providers);
    }

    @Bean
    GrpcAuthenticationReader authenticationReader(final JwtTokenProvider jwtTokenProvider) {
        final List<GrpcAuthenticationReader> readers = new ArrayList<>();
        BearerAuthenticationReader reader = new BearerAuthenticationReader(accessToken -> jwtTokenProvider.getAuthentication(accessToken));
        readers.add(reader);
        return new CompositeGrpcAuthenticationReader(readers);
    }
}

Solution

  • Finally, create a JwtGrantedAuthoritiesConverter and replace DaoAuthenticationProvider with JwtAuthenticationConverter.

    It's work for me!

    @Slf4j
    @Configuration(proxyBeanMethods = false)
    @EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true)
    public class SecurityConfiguration {
    
        @Bean
        JwtAuthenticationConverter jwtAuthenticationConverter(final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter) {
    
            final JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
            converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
            return converter;
        }
    
        @Bean
        JwtAuthenticationProvider jwtAuthenticationProvider(final JwtAuthenticationConverter jwtAuthenticationConverter, final JwtDecoder jwtDecoder) {
    
            final JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder);
            provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
            return provider;
        }
    
        @Bean
        AuthenticationManager authenticationManager(final JwtAuthenticationProvider jwtAuthenticationProvider) {
            final List<AuthenticationProvider> providers = new ArrayList<>();
            providers.add(jwtAuthenticationProvider);
            return new ProviderManager(providers);
        }
    
        @Bean
        GrpcAuthenticationReader authenticationReader() {
            return new BearerAuthenticationReader(BearerTokenAuthenticationToken::new);
        }
    
        @Bean
        JwtDecoder jwtDecoder(final JwtProperties jwtProperties) {
            SecretKey originalKey = new SecretKeySpec(jwtProperties.getSignKey().getBytes(), SignatureAlgorithm.HS256.getJcaName());
            return NimbusJwtDecoder.withSecretKey(originalKey).build();
        }
    
    }
    
    @Slf4j
    @Service
    public class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
    
        private static final String AUTHORITIES_KEY = "roles";
    
        @Override
        public Collection<GrantedAuthority> convert(Jwt jwt) {
            Object authoritiesClaim = jwt.getClaims().get(AUTHORITIES_KEY);
    
            return authoritiesClaim == null ? AuthorityUtils.NO_AUTHORITIES : AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesClaim.toString());
        }
    
    }
    
    @Slf4j
    @Component
    public class JwtTokenProvider {
    
        private static final String AUTHORITIES_KEY = "roles";
    
        private final JwtProperties jwtProperties;
    
        private final String secretKey;
    
        public JwtTokenProvider(JwtProperties jwtProperties) {
            this.jwtProperties = jwtProperties;
            secretKey = Base64.getEncoder().encodeToString(jwtProperties.getSignKey().getBytes());
    
        }
    
        public String createToken(Authentication authentication) {
    
            String username = authentication.getName();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            Claims claims = Jwts.claims().setSubject(username);
            if (!authorities.isEmpty()) {
                claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(joining(",")));
            }
    
            Date now = new Date();
            Date validity = new Date(now.getTime() + this.jwtProperties.getExpireTimeAsSec());
    
            return Jwts.builder()
                    .setClaims(claims)
                    .setIssuedAt(now)
                    .setExpiration(validity)
                    .signWith(SignatureAlgorithm.HS256, this.secretKey)
                    .compact();
    
        }
    
    
    }