Closed kevintanhongann closed 7 months ago
JHipster has completed the sample check
.yo-rc.json
: valid
Entities JDL: blank
Application: successfully generated
Frontend check: success
Backend check: success
E2E check: success
This check uses jhipster info
output from the issue description to generate the sample.
Bug report that does not contain this information will be marked as invalid.
based on my comparison with 8.1.0 and 7.9.4, these 3 important classes were missing.
The renaming of UserJwtController to AuthenticateController was totally unnecessary.
You should get back those 3 classes, and modify the necessary JWT token verification and the assigning the user principal there.
Suggested fix:
@Component
public class TokenProvider {
private final Logger log = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private static final String INVALID_JWT_TOKEN = "Invalid JWT token.";
private final JwtEncoder jwtEncoder;
private final long tokenValidityInMilliseconds;
private final long tokenValidityInMillisecondsForRememberMe;
private final SecurityMetersService securityMetersService;
private final JwtDecoder jwtDecoder;
public TokenProvider(JwtEncoder jwtEncoder, JHipsterProperties jHipsterProperties, SecurityMetersService securityMetersService, JwtDecoder jwtDecoder) {
this.jwtEncoder = jwtEncoder;
this.jwtDecoder = jwtDecoder;
String secret = jHipsterProperties.getSecurity().getAuthentication().getJwt().getBase64Secret();
if (!ObjectUtils.isEmpty(secret)) {
log.debug("Using a Base64-encoded JWT secret key");
} else {
log.warn(
"Warning: the JWT key used is not Base64-encoded. " +
"We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security."
);
//todo where is this supposed to be assigned to since JwtClaimsSet builder doesnt take signing argument
secret = jHipsterProperties.getSecurity().getAuthentication().getJwt().getSecret();
}
this.tokenValidityInMilliseconds = 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSeconds();
this.tokenValidityInMillisecondsForRememberMe =
1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe();
this.securityMetersService = securityMetersService;
}
public String createToken(Authentication authentication, boolean rememberMe) {
String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
Instant now = Instant.now();
Instant validity;
if (rememberMe) {
validity = now.plus(this.tokenValidityInMilliseconds, ChronoUnit.SECONDS);
} else {
validity = now.plus(this.tokenValidityInMillisecondsForRememberMe, ChronoUnit.SECONDS);
}
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuedAt(Instant.now())
.expiresAt(validity)
.subject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.build();
JwsHeader jwsHeader = JwsHeader.with(JWT_ALGORITHM).build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue();
}
public Authentication getAuthentication(String token) {
Jwt jwt = jwtDecoder.decode(token); // decode and verify the token
Map<String, Object> claims = jwt.getClaims();
Collection<? extends GrantedAuthority> authorities = Arrays
.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.filter(auth -> !auth.trim().isEmpty())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(jwt.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String authToken) {
try {
Jwt jwt = jwtDecoder.decode(authToken); // decode and verify the token
return true;
} catch (JwtException e) {
this.securityMetersService.trackTokenMalformed();
log.trace(INVALID_JWT_TOKEN, e);
} catch (IllegalArgumentException e) { // TODO: should we let it bubble (no catch), to avoid defensive programming and follow the fail-fast principle?
log.error("Token validation error {}", e.getMessage());
}
return false;
}
}
the SecurityConfiguration can roughly follow the same format as before.
import com.pdfchatter.security.AuthoritiesConstants;
import com.pdfchatter.security.jwt.JwtConfigurer;
import com.pdfchatter.security.jwt.TokenProvider;
import com.pdfchatter.web.filter.SpaWebFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import tech.jhipster.config.JHipsterProperties;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {
private final JHipsterProperties jHipsterProperties;
private final TokenProvider tokenProvider;
public SecurityConfiguration(JHipsterProperties jHipsterProperties, TokenProvider tokenProvider) {
this.jHipsterProperties = jHipsterProperties;
this.tokenProvider = tokenProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
http
.cors(withDefaults())
.csrf(csrf -> csrf.disable())
.addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class)
.headers(headers ->
headers
.contentSecurityPolicy(csp -> csp.policyDirectives(jHipsterProperties.getSecurity().getContentSecurityPolicy()))
.frameOptions(FrameOptionsConfig::sameOrigin)
.referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
.permissionsPolicy(permissions ->
permissions.policy(
"camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()"
)
)
)
.authorizeHttpRequests(authz ->
// prettier-ignore
authz
.requestMatchers(mvc.pattern("/index.html"), mvc.pattern("/*.js"), mvc.pattern("/*.txt"), mvc.pattern("/*.json"), mvc.pattern("/*.map"), mvc.pattern("/*.css")).permitAll()
.requestMatchers(mvc.pattern("/*.ico"), mvc.pattern("/*.png"), mvc.pattern("/*.svg"), mvc.pattern("/*.webapp")).permitAll()
.requestMatchers(mvc.pattern("/assets/**")).permitAll()
.requestMatchers(mvc.pattern("/content/**")).permitAll()
.requestMatchers(mvc.pattern("/swagger-ui/**")).permitAll()
.requestMatchers(mvc.pattern(HttpMethod.POST, "/api/authenticate")).permitAll()
.requestMatchers(mvc.pattern(HttpMethod.GET, "/api/authenticate")).permitAll()
.requestMatchers(mvc.pattern("/api/register")).permitAll()
.requestMatchers(mvc.pattern("/api/activate")).permitAll()
.requestMatchers(mvc.pattern("/api/account/reset-password/init")).permitAll()
.requestMatchers(mvc.pattern("/api/account/reset-password/finish")).permitAll()
.requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN)
.requestMatchers(mvc.pattern("/api/**")).authenticated()
.requestMatchers(mvc.pattern("/websocket/**")).authenticated()
.requestMatchers(mvc.pattern("/v3/api-docs/**")).hasAuthority(AuthoritiesConstants.ADMIN)
.requestMatchers(mvc.pattern("/management/health")).permitAll()
.requestMatchers(mvc.pattern("/management/health/**")).permitAll()
.requestMatchers(mvc.pattern("/management/info")).permitAll()
.requestMatchers(mvc.pattern("/management/prometheus")).permitAll()
.requestMatchers(mvc.pattern("/management/**")).hasAuthority(AuthoritiesConstants.ADMIN)
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions ->
exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
).apply(securityConfigurerAdapter());
//todo need to clarify what is this for
// .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}
private JwtConfigurer securityConfigurerAdapter() {
return new JwtConfigurer(tokenProvider);
}
}
and the new JWTConfigurer
import org.springframework.security.config.annotation.SecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class JwtConfigurer implements SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
public JwtConfigurer(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void init(HttpSecurity builder) throws Exception {
JwtFilter customFilter = new JwtFilter(tokenProvider);
builder.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(HttpSecurity http) {
JwtFilter customFilter = new JwtFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
and the new AuthenticateController(formerly known as the UserJWTController)
import com.fasterxml.jackson.annotation.JsonProperty;
import com.pdfchatter.security.jwt.TokenProvider;
import com.pdfchatter.web.rest.vm.LoginVM;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
/**
* Controller to authenticate users.
*/
@RestController
@RequestMapping("/api")
public class AuthenticateController {
private final Logger log = LoggerFactory.getLogger(AuthenticateController.class);
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public AuthenticateController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
this.tokenProvider = tokenProvider;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
@PostMapping("/authenticate")
public ResponseEntity<JWTToken> authorize(@Valid @RequestBody LoginVM loginVM) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginVM.getUsername(),
loginVM.getPassword()
);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.createToken(authentication, loginVM.isRememberMe());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(jwt);
return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK);
}
/**
* {@code GET /authenticate} : check if the user is authenticated, and return its login.
*
* @param request the HTTP request.
* @return the login if the user is authenticated.
*/
@GetMapping("/authenticate")
public String isAuthenticated(HttpServletRequest request) {
log.debug("REST request to check if the current user is authenticated");
return request.getRemoteUser();
}
/**
* Object to return as body in JWT Authentication.
*/
static class JWTToken {
private String idToken;
JWTToken(String idToken) {
this.idToken = idToken;
}
@JsonProperty("id_token")
String getIdToken() {
return idToken;
}
void setIdToken(String idToken) {
this.idToken = idToken;
}
}
}
I'll leave it to you guys to revert some changes and modify them accordingly. Lemme know your thoughts on whether my new suggestions are sound. I just tested with another sample project and it works. @mraible
For jwt you should use:
@AuthenticationPrincipal org.springframework.security.oauth2.jwt.Jwt jwt
We moved away from custom jwt handling to spring-boot provided one. We now rely on spring-boot default behavior for jwt, so this is working as expected.
There is nothing to do on JHipster side.
Overview of the issue
@AuthenticationPrincipal failed to get UserDetails
Motivation for or Use Case
This has got to be an important utility to use for security measures, but it's broken.
Reproduce the error
https://github.com/kevintanhongann/jhipster8-broken-auth
start the app, make sure to login as either admin or user, then from the vue ui, navigate to localhost:5173/demo, and that component will call localhost:8080/api/demo that calls the userDetails principal and fails.
Related issues
Suggest a Fix
JHipster Version(s)
8.1.0
JHipster configuration
.yo-rc.json file
Environment and Tools
openjdk version "21.0.2" 2024-01-16 LTS OpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS) OpenJDK 64-Bit Server VM Temurin-21.0.2+13 (build 21.0.2+13-LTS, mixed mode, sharing)
git version 2.40.1
node: v18.19.0 npm: 10.2.3
Docker version 25.0.3, build 4debf41
JDL for the Entity configuration(s)
entityName.json
files generated in the.jhipster
directoryJDL entity definitions
Browsers and Operating System
Firefox and Ubuntu 23.10