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