Insufficient scope in resource server with spring webflux - java

I'm having trouble setting up a ResourceServer that uses Webflux in spring-boot 2.1.3.RELEASE. The same token used to authenticate is working fine with a resource server that doesn't use Webflux, and if I set .permitAll() it works too (obviously). Here's my resource server config
The authorization server uses jwt token store.
#EnableWebFluxSecurity
public class ResourceServerConfig {
#Value("${security.oauth2.resource.jwt.key-value}")
private String publicKey;
#Autowired Environment env;
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/availableInspections/**").hasRole("READ_PRIVILEGE")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtDecoder(reactiveJwtDecoder())
;
return http.build();
}
#Bean
public ReactiveJwtDecoder reactiveJwtDecoder() throws Exception{
return new NimbusReactiveJwtDecoder(getPublicKeyFromString(publicKey));
}
public static RSAPublicKey getPublicKeyFromString(String key) throws IOException, GeneralSecurityException {
String publicKeyPEM = key;
publicKeyPEM = publicKeyPEM.replace("-----BEGIN PUBLIC KEY-----\n", "");
publicKeyPEM = publicKeyPEM.replace("-----END PUBLIC KEY-----", "");
byte[] encoded = Base64.getMimeDecoder().decode(publicKeyPEM);
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded));
return pubKey;
}
}
The error I'm getting is
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The token provided has insufficient scope [read] for this request", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", scope="read"
I've verified that the token has the required scope...
What do I need to change/add in order to get my token read correctly?

Spring seems to add only what is under scope in the jwt token, ignoring everything from authorities - so they can't be used in a webflux resource server unless we extend JwtAuthenticationConverter to also add from authorities (or other claims) in the token. In the security config, I've added jwtAuthenticationConverter
http
// path config like above
.oauth2ResourceServer()
.jwt()
.jwtDecoder(reactiveJwtDecoder())
.jwtAuthenticationConverter(converter())
and have overriden JwtAuthenticationConverter to include authorities granted authorities:
public class MyJwtAuthenticationConverter extends JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
Arrays.asList("scope", "scp", "authorities"); // added authorities
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.getScopes(jwt)
.stream()
.map(authority -> SCOPE_AUTHORITY_PREFIX + authority)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private Collection<String> getScopes(Jwt jwt) {
Collection<String> authorities = new ArrayList<>();
// add to collection instead of returning early
for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) {
Object scopes = jwt.getClaims().get(attributeName);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
authorities.addAll(Arrays.asList(((String) scopes).split(" ")));
}
} else if (scopes instanceof Collection) {
authorities.addAll((Collection<String>) scopes);
}
}
return authorities;
}
}

A little bit late, but a simpler solution could be (in kotlin, but easy to translate to java):
class CustomJwtAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
private val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
override fun convert(source: Jwt): AbstractAuthenticationToken {
val scopes = jwtGrantedAuthoritiesConverter.convert(source)
val authorities = source.getClaimAsStringList("authorities")?.map { SimpleGrantedAuthority(it) }
return JwtAuthenticationToken(source, scopes.orEmpty() + authorities.orEmpty())
}
}
and then supply implementation to WebSecurityConfigurerAdapter, like:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Configuration
class ResourceServerConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2ResourceServer {
jwt {
jwtDecoder = reactiveJwtDecoder()
jwtAuthenticationConverter = CustomJwtAuthenticationConverter()
}
}
}
}
}

with springboot+webflux in version 2.7.1 and Keycloak as IdP, I have the same issue
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/availableInspections/**").hasRole("READ_PRIVILEGE")
.and()
.oauth2ResourceServer()
.jwt()
// .jwtDecoder(reactiveJwtDecoder()) not used
;
return http.build();
}
=> Gives the following error : www-authenticate: Bearer error="insufficient_scope",error_description="The request requires higher privileges than provided by the access token.",error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
Then I see that the line ".anyExchange().authenticated()" was missing on my project :
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/availableInspections/**").hasRole("READ_PRIVILEGE")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
// .jwtDecoder(reactiveJwtDecoder()) not used
;
return http.build();
}
=> no more error :) !

Related

Why do I have double request to authentication endpoint when I use Spring Security + WebFlux

I try to create a simple authentication schema with Reactive approach.
I've created a project from scratch with dependencies to reactive components and security.
Introduced Configuration file where I configure authentication manager and security context repository.
The problem is that I notice, that Mono injected into controller initiates double requests to "login" endpoint.
Why does it happens and how to prevent it?
Here is the code of configuration:
#Configuration
#EnableWebFluxSecurity
public class WebFluxSecurityConfiguration {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private SecurityContextRepository securityContextRepository;
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.logout().disable()
.formLogin().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.build();
}
}
Here is the authentication manager
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
private final WebClient webClient;
public AuthenticationManager(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("http://localhost:8080/login")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.map(r->new AuthenticatedUser());
}
}
And here is a security context repository
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private static final String TOKEN_PREFIX = "Bearer ";
private AuthenticationManager authenticationManager;
List<PathPattern> pathPatternList;
public SecurityContextRepository(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
PathPattern pathPattern1 = new PathPatternParser().parse("/login");
pathPatternList = new ArrayList<>();
pathPatternList.add(pathPattern1);
}
#Override
public Mono load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
RequestPath path = request.getPath();
if (pathPatternList.stream().anyMatch(pathPattern -> pathPattern.matches(path.pathWithinApplication()))) {
System.out.println(path.toString() + " path excluded");
return Mono.empty();
}
System.out.println("executing logic for " + path.toString() + " path");
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String authToken = null;
//test
authHeader = "Bearer bla-bla";
//~test
if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) {
authToken = authHeader.replace(TOKEN_PREFIX, "");
}else {
System.out.println("couldn't find bearer string, will ignore the header.");
}
if (authToken != null) {
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authenticationManager.authenticate(auth).map((authentication) -> new SecurityContextImpl(authentication));
} else {
return Mono.empty();
}
}
#Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
return null;
}
}
link to repository of full project
A quick solution to this particular case is to implement caching of WebClient result. So inside of post method to authorization server for retrieval of user profile I just introduced a method cache().
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.cache()
.map(r->new AuthenticatedUser());
That helped to avoid repeating queries to authorization endpoint during the request.

Using spring-security-oauth2-client with custom ClientRegistrationRepository

I'm building a service which is responsible for allowing a user to link external services to their user account.
Authentication of the web app is using a JWT passed in via query string.
I have a Controller that is attempting to use the OAuth2AuthorizedClientManager to initiate the OAuth client workflow to redirect the user to the target external service and authorize the access.
I am struggling to get the Authorization Grant flow to start - with the code below, my OAuth2AuthorizedClient is always null.
I believe I have a disconnect on how to best use the ClientManager.
Controller:
private final OAuth2AuthorizedClientManager authorizedClientManager;
#Autowired
public LinkingController(OAuth2AuthorizedClientManager authorizedClientManager) {
this.authorizedClientManager = authorizedClientManager;
}
#GetMapping("/tenants/{tenantId}/externalsystem/{externalSystemName}/link")
public ModelAndView linking(#PathVariable Long tenantId, #PathVariable String externalSystemName, Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
// ClientRegistrationRepository does not natively support multi-tenancy, so registrations are
// Prefixed with a tenantId
String registrationId = tenantId + "-" + externalSystemName;
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(registrationId)
.principal(authentication) // Confirmed this is the correct authentication from the JWT
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); // Confirmed with debugging that it is using the correct registration repository and finding the correct registration.
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
Map<String, Object> params = new HashMap<>();
params.put("token", accessToken.getTokenValue());
return new ModelAndView("rootView", params);
}
Security Config:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.oauth2Client()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.bearerTokenResolver(getCustomizedTokenResolver())
.jwt();
}
private BearerTokenResolver getCustomizedTokenResolver() {
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
resolver.setAllowUriQueryParameter(true);
return resolver;
}
#Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new BridgeClientRegistrationRepository(externalSystemService);
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(OAuth2AuthorizedClientRepository authorizedClientRepository) {
return new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository(), authorizedClientRepository);
}
}
UPDATE
My code originally defined the OAuth2AuthorizedClientManager as:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(OAuth2AuthorizedClientRepository authorizedClientRepository) {
return new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository(), authorizedClientRepository);
}
It seems that without adding specific providers to the ClientProvider, it would not function. Corrected code is:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}

ReactiveSecurityContextHolder#getContext returns an empty context

I try to authenticate user (it works) and get a user token from the context and it doesn't work.
I have a simple microservice application as a my pet-project and use a WebFlux as a web-framework. I tried to debug ReactiveSecurityContextHolder#getContext and inside flatMap I see my user token inside context, but in my application I steel have an empty context.
SecurityConfiguration.java
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
AuthenticationWebFilter authenticationWebFilter,
ServerHttpSecurity http) {
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.build();
}
#Bean
public AuthenticationWebFilter authenticationWebFilter(
SecurityTokenBasedAuthenticationManager authenticationManager,
TokenAuthenticationConverter tokenAuthenticationConverter) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter);
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationWebFilter;
}
}
ReactiveAuthenticationManager.java
#Slf4j
#Component
#AllArgsConstructor
public class SecurityTokenBasedAuthenticationManager implements ReactiveAuthenticationManager {
private final AuthWebClient authWebClient;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.switchIfEmpty(Mono.defer(this::raiseBadCredentials))
.cast(Authorization.class)
.flatMap(this::authenticateToken)
.map(user ->
new Authorization(user, (AuthHeaders) authentication.getCredentials()));
}
private <T> Mono<T> raiseBadCredentials() {
return Mono.error(new TokenValidationException("Invalid Credentials"));
}
private Mono<User> authenticateToken(Authorization authenticationToken) {
AuthHeaders authHeaders = (AuthHeaders) authenticationToken.getCredentials();
return Optional.of(authHeaders)
.map(headers -> authWebClient.validateUserToken(authHeaders.getAuthToken(), authHeaders.getRequestId())
.doOnSuccess(user -> log
.info("Authenticated user " + user.getUsername() + ", setting security context")))
.orElseThrow(() -> new MissingHeaderException("Authorization is missing"));
}
}
SecurityUtils.java
#Slf4j
public class SecurityUtils {
public static Mono<AuthHeaders> getAuthHeaders() {
return getSecurityContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getCredentials)
.cast(AuthHeaders.class)
.doOnSuccess(authHeaders -> log.info("Auth headers: {}", authHeaders));
}
private static Mono<SecurityContext> getSecurityContext() {
return ReactiveSecurityContextHolder.getContext();
}
}
WebClient building
(...)
WebClient.builder()
.baseUrl(url)
.filter(loggingFilter)
.defaultHeaders(httpHeaders ->
getAuthHeaders()
.doOnNext(headers -> httpHeaders.putAll(
Map.of(
REQUEST_ID, singletonList(headers.getRequestId()),
AUTHORIZATION, singletonList(headers.getAuthToken())))))
.build()
(...)
The main problem is inside a building of my WebClient - I expect, that I'll have complete webclient with requested header, but as I described above - I have an empty context inside SecurityUtils.java
So, after investigating and debugging I found a reason using combining streams into single one. IMHO, this is not quite reason for this issue, but now it works.
(...)
getAuthHeaders()
.flatMap(authHeaders -> buildWebClient(url, authHeaders)
.get()
(...)

Spring app with Keycloak returns 401 error

I'm trying to access a Spring App via Keycloak, but I always get a 401 Unauthorized error. Basically I have a chat module that works fine on its own, but once I add Keycloak I'm unable to access the app due to that 401 error.
I've followed about 3 tutorials that showed similar things to what I've done, and I still have no idea what I've done wrong.
Here's my app's config :
keycloak:
enabled: true
realm: myReal
resource: myReal-api
public-client: true
bearer-only: true
auth-server-url: http://localhost:8080/auth
credentials:
secret: 82eXXXXX-3XXX-4XXX-XXX7-287aXXXXXXXX
principal-attribute: preferred_username
cors: true
From localhost:port/ I have a first interface (with no Keycloak safety) that has a link to my service, which is localhost:port/index/{topicName} . Now when I click on that link, I'm supposed to get the Keycloak authentication screen, but I get a 401 error instead.
I've checked the header of my request, adding a HttpServletRequest as a parameter to my displayMessage method, and I actually could display the access_token and the X-Auth-Token in my IDE's console. But it seems like when I follow that link, it sends the request without the token.
Here are my controller methods (my Controller class is annotated with #Controller:
#GetMapping(path = "/")
public String index() {
return "external";
}
#GetMapping(path = "/index/{topicName}",
produces = MediaType.APPLICATION_JSON_VALUE)
public String displayMessages(Model model,
#PathVariable String topicName) {
//HttpHeaders headers = new HttpHeaders();
//headers.set("Authorization", request.getHeader("Authorization"));
//header = request.getHeader("Authorization");
//System.out.println(" T O K E N "+request.getHeader("X-Auth-Token"));
projectServiceImpl.findByName(topicName);
List<Message> messages = messageServiceImpl.findAllMessagesByProjectName(topicName);
model.addAttribute("topic", topicName);
model.addAttribute("message",messages);
return "index";
}
My Keycloak config file is inspired from the tuto's I've read, so there might be a mistake in there that I don't know about (not sure what the difference between methods access and hasRole is) :
#Configuration
#ComponentScan(
basePackageClasses = KeycloakSecurityComponents.class,
excludeFilters = #ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "org.keycloak.adapters.springsecurity.management.HttpSessionManager"))
#EnableWebSecurity
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
#Bean
public HttpSessionIdResolver httpSessionIdResolver() { //replace HttpSessionStrategy
return HeaderHttpSessionIdResolver.xAuthToken();
}
//Registers the KeycloakAuthenticationProvider with the authentication manager.
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
try {
SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
grantedAuthorityMapper.setPrefix("ROLE_");
grantedAuthorityMapper.setConvertToUpperCase(true);
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider());
} catch(Exception ex) {
logger.error("SecurityConfig.configureGlobal: " + ex);
}
/*try {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}catch(Exception ex){
logger.error("SecurityConfig.configureGlobal: " +ex);
}*/
}
//Load Keycloak properties from service config-file
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
//Defines the session authentication strategy.
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
//Public or Confidential application keycloak/OpenID Connect client
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
//Bearer mode only keycloak/OpenID Connect client without keycloak session -> stateless behavior
//return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http.authorizeRequests()
//BEGIN
//USER -done to be tested
.antMatchers(HttpMethod.GET,"/index**").access("hasAuthority('ADMIN')")
.antMatchers(HttpMethod.GET,"/").access("hasAuthority('ADMIN')")
.antMatchers(HttpMethod.GET,"/").access("hasAnyAuthority('ADMIN','MANAGER','EXPERT','STANDARD')")
.anyRequest().authenticated()
.and()
.cors()
.and()
.csrf().disable()
//BEGIN Login/Logout
.formLogin()
.permitAll()//.successHandler(authenticationSuccessHandler) //
.and()
.logout()//.clearAuthentication(true) //Add .clearAuthentication(true) to logout()
//.logoutUrl("/custom-logout")
.addLogoutHandler(keycloakLogoutHandler())
//.addLogoutHandler(new LogoutHandlerImpl())
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll();
//END Login/Logout
//BEGIN Session
http
.sessionManagement()
//.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) //BY default IF_REQUIRED
.maximumSessions(1)
.maxSessionsPreventsLogin(false) // if true generate an error when user login after reaching maximumSession (SessionAuthenticationStrategy rejected the authentication object / SessionAuthenticationException: Maximum sessions of 1 for this principal exceeded)
//.expiredUrl("/auth/login")
.sessionRegistry(sessionRegistry());
}
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request.getUserPrincipal()).getCredentials()).getToken();
}
///BEGIN session
#Bean
public SessionRegistry sessionRegistry() {
SessionRegistry sessionRegistry = new SessionRegistryImpl();
return sessionRegistry;
}
#Bean
public RegisterSessionAuthenticationStrategy registerSessionAuthStr( ) {
return new RegisterSessionAuthenticationStrategy( sessionRegistry( ) );
}
// Register HttpSessionEventPublisher
#Bean
public static ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
I don't really know what else I should change to make it work, but I believe there has to be something wrong in there. But I think if I can have the Keycloak authentication screen when trying to access my service, this would be alright.
I received the same error, one thing to double check is that auth-server-url is the same for the server, and the client getting the token.
I.e. if one is the dns name, and one is the IP address, it will not authorize. (in my case I had localhost and 127.0.0.1 so authorization failed)
Server, src/main/resources/application.yml
Postman/client:
This solved my issue. I was using within a docker container and had to match both to host.docker.internal

Create JWT Token within UsernamePasswordAuthenticationFilter or Controller?

I try to generate a JWT Token with Spring Security but I don't how to do it correctly (with the best practice).
Do I should "intercept" the authenticate method within UsernamePasswordAuthenticationFilter somewhere and generate token inside it ?
Or it is better to use AuthenticationManager autowired in the controller '/login' ?
I'm afraid to authenticate the user twice if I use the controller mechanism.
I used this tutorial : tutorial Jwt Token
Here is my code :
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserService userService;
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// #Autowired
// private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtTokenFilter jwtTokenFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/css/**", "/login/**", "/register/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
//.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
.formLogin()
.usernameParameter("email")
//.loginPage("http://localhost:4200/login").failureUrl("/login-error")
.and()
.logout()
.permitAll();
http
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public CustomDaoAuthenticationProvider authenticationProvider() {
CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider();
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
authenticationProvider.setUserDetailsService(userService);
return authenticationProvider;
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebConfig() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:4200")
.allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS")
.allowedHeaders("Content-Type", "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method",
"Access-Control-Request-Headers", "Authorization", "Cache-Control",
"Access-Control-Allow-Origin")
.exposedHeaders("Access-Control-Allow-Origin", "Access-Control-Allow-Credentials")
.allowCredentials(true).maxAge(3600);
}
};
}
}
Token Filter
public class JwtTokenFilter extends GenericFilterBean {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(req, res);
}
}
Token Provider
#Component
public class JwtTokenProvider {
#Value("${security.jwt.token.secret-key:secret}")
private String secretKey = "secret";
#Value("${security.jwt.token.expire-length:3600000}")
private long validityInMilliseconds = 3600000; // 1h
#Autowired
private UserDetailsService userDetailsService;
#PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, secretKey)//
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
if (claims.getBody().getExpiration().before(new Date())) {
return false;
}
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
}
}
}
Given your context, the controller is responsible for issuing a new token (after validating credentials) while the filter is responsible for authenticating the user against the given token. The controller should not populate the security context (authenticate user), it is the filter's responsibility.
To better understand the two phases:
Spring uses two filters to authenticate and log in a user.
See UsernamePasswordAuthenticationFilter and SecurityContextPersistenceFilter in a "username/password" scenario, from the Spring Security project: the first one processes an authentication attempt (username/password) while the latter populates the security context from a SecurityContextRepository (from a session in general).

Categories

Resources