How to add Jwt Token based security In Microservices - spring-boot

In my microservices, I will try to implement Jwt spring-security, But I don't know how to apply it.
In my microservices, I have used the 2020.0.3 spring cloud version.
In user services, I have connected the department service using the Rest template.
I need help with how to add Jwt security in these microservices.
This is 4 microservices
Server = Eureka Server
service-API-gateway = Spring cloud Apigateway
service-department & services-user = These two microservices connect with Rest template
Microservices Project Structure
: https://i.stack.imgur.com/ajTiX.png

So at a higher level, Spring Security is applied on controller level when using jwt as authentication. First you need to add a Security config that will extend WebSecurityConfigurerAdapter (this is common for http based security) and in that class you need to define configure method like:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable() // IF your clients connect without a cookie based, this will be fine
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/register", "/login","/your_open_endpoints_etc").permitAll()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
Then in the filter class which extends OncePerRequestFilter, you can define the do filter like this, you have to set the UsernamePasswordAuthenticationFilter instance inside the Spring authentication context:
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
logger.info("do filter...");
String token = jwtProvider.getTokenFromRequest((HttpServletRequest) httpServletRequest);
try{
if (token != null && jwtProvider.validateToken(token)) {
String username = jwtProvider.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, jwtProvider.getAuthorities(token));
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
catch (RuntimeException e)
{
// Some general Exception handling that will wrap and send as HTTP Response
}
}
Check on the extending filters further, they might change as per your requirement
finally in rest endpoints you can safe guard like:
#PreAuthorize("hasRole('ROLE_YOURROLE')")
#GetMapping(path = "/your_secured_endpoint", consumes = "application/json",
produces = "application/json")
public ResponseEntity<List<SomePOJOObject>> getAllAppointmentsForPatient()
{
return new ResponseEntity<>(thatSomePOJOObjectListYouWant, HttpStatus.OK);
}

Related

Spring oauth2login oidc grant access based on user info

I'm trying to set up Authentication based on this tutorial: https://www.baeldung.com/spring-security-openid-connect part 7 specifically.
I have filled properties and configured filter chain like this:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin.permitAll());
return http.build();
}
which works, but now all users from oidc can connect log in. I want to restrict access based on userinfo. E.g. add some logic like:
if(principal.getName() == "admin") {
//allow authentication
}
are there any way to do it?
I tried to create customer provider like suggested here: Add Custom AuthenticationProvider to Spring Boot + oauth +oidc
but it fails with exception and says that principal is null.
You can retrieve user info when authentication is successful and do further checks based user info.
Here is sample code that clears security context and redirects the request:
#Component
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if(authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
// OidcUser or OAuth2User
// OidcUser user = (OidcUser) token.getPrincipal();
OAuth2User user = token.getPrincipal();
if(!user.getName().equals("admin")) {
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
redirectStrategy.sendRedirect(request, response, "login or error page url");
}
}
}
}
Are you sure that what you want to secure does not include #RestController or #Controller with #ResponseBody? If so, the client configuration you are referring to is not adapted: you need to setup resource-server configuration for this endpoints.
I wrote a tutorial to write apps with two filter-chains: one for resource-server and an other one for client endpoints.
The complete set of tutorials the one linked above belongs to explains how to achieve advanced access-control on resource-server. Thanks to the userAuthoritiesMapper configured in resource-server_with_ui, you can write the same security expressions based on roles on client controller methods as I do on resource-server ones.

Spring Cloud Gateway with spring-boot-starter-web

I am creating gateway for Spring Boot microservices using Spring Cloud Gateway. Gateway is also responsible for JWT authorization using Spring Security.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
...
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(JwtProperties.HEADER_STRING);
if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
Authentication authentication = getUsernamePasswordAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private Authentication getUsernamePasswordAuthentication(HttpServletRequest request) {
String token = request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, "");
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes())).build().verify(token);
String username = decodedJWT.getSubject();
if (username != null) {
UserPrincipal principal = (UserPrincipal) userPrincipalService.loadUserByUsername(username);
Authentication auth = new UsernamePasswordAuthenticationToken(username, null, principal.getAuthorities());
return auth;
}
return null;
}
}
This filter is registred in configure method like this:
#Configuration
#EnableWebSecurity
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JwtAuthorizationFilter(authenticationManager(), userPrincipalService))
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
...
.anyRequest().authenticated();
}
...
}
As you can see, Spring Security is using HttpServletRequest, HttpServletResponse, FilterChain interfaces which belong to spring-boot-starter-web. But that is main problem beacause it's incompatible with spring cloud gateway.
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
Is there any way to avoid this error or any different solution for implementing jwt authorization filter on gateway?
Thanks!
In the Documentation of spring cloud gateway it is explicitely stated that this product runs on top of Netty and requires webflux, hence it's not compatible with spring MVC.
The filter that you use (JwtAuthorizationFilter) is something that belongs to the non-reactive world so you probably should rewrite it with spring security for web flux building blocks.
Disclaimer, I'm not a spring web flux / spring-security expert, but please consider checking This application - it shows how to define JWT secured application with a reactive version of spring security.
So bottom line you should choose whether you want a reactive application or a traditional one and use the relevant technologies but you can't really mix them.

Spring Security OAuth - how to disable login page?

I want to secure my application with Spring Security, using OAuth 2. However, I don't want the server to redirect incoming unauthorized requests, but instead to respond with HTTP 401. Is it possible?
Example: this code redirects requests to a default login page.
application.properties
spring.security.oauth2.client.registration.google.client-id=...
spring.security.oauth2.client.registration.google.client-secret=...
AuthConfig.java
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
// https://stackoverflow.com/questions/31714585/spring-security-disable-login-page-redirect
// deos not work
// .and()
// .formLogin().successHandler((request, response, authentication) -> {});
}
}
You need to create new authentication entry point and set it in configuration.
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
}
}
public class AuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public AuthenticationEntryPoint() {
super("");
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(401, "Unauthorized");
}
}
You need to set oauth2Login.loginPage in your HttpSecurity config and create a controller mapping to return whatever you want. Here's a simple example.
So in your security config
http
.authorizeRequests()
.antMatchers("/noauth").permitAll()
.oauth2Login()
.loginPage("/noauth")
In a controller
#GetMapping("/noauth")
public ResponseEntity<?> noAuth() {
Map<String, String> body = new HashMap<>();
body.put("message", "unauthorized");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}
You can pass a map or pojo to the body method.
I would like to expand on Petr's answer by explaining that apparently for the time being first of all, the default login page is shown when there are more than one OAuth2 configured providers. I would expect that Spring Boot would have a smart trick to bypass this page easily and choose the right provider automatically, basing e.g. on the existence of the provider's client ID in the original request. I found out the hard way that this is not the case. So the way to do this is.. this not very apparent trick of providing a custom handler for failures - that will REDIRECT the user to the correct OAuth2 endpoint for each provider, based on the original HTTP request URL. I tried this and it works and I spent a whole day trying all manners of other solutions - my original scenario was to pass additional parameters to OAuth2 scheme in order to be able to get them back on successful authentication - they used to do this appending Base64 encoded information to the "state" URL request parameter, but Spring Security does not allow this at the moment. So the only alternative was to call a Spring Security-protected URL with those parameters already there, so when the successful authentication happens, this URL is accessed again automatically with those parameters intact.
Related: Multiple Login endpoints Spring Security OAuth2

AntMatcher seems not to match requested path (Spring Security)

We are trying to use Spring Security to secure our webservice. We are using a customer filter (a class that extends GenericFilterBean) to read a JSON Web Token from the HTTP header. If a token exists, it is stored as PreAuthenticatedAuthenticationToken in the Security Context. Protected resources should use our customer Authentication Provider to verify if the token is valid and to load user info (which includes roles).
The problem is that I don't get it to configure an AntMatcher for a specific resource.
If I use antMatchers("/**").hasRole("USER") the resource is protected, but we don't want all resources to match, so I tried an AntMachter for one resource like this:
.antMatchers(HttpMethod.GET,"/rest/security/v1/currentuser").hasRole("USER")
But this matcher seems not to match the requested resource and so the Authentication Provider is not called. But I don't know why. I tried several combinations of ant pattern but nothing worked yet.
I set a breakpoint in the custom filter to check the current path and when I call servletRequest.getPathInfo() I get excatly what I thought it should be our ant pattern: /rest/security/v1/currentuser
Spring Security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable()
.csrf().disable()
.authorizeRequests()
//.antMatchers("/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/rest/security/v1/currentuser").hasRole("USER")
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(new JwtAuthenticationFilter(), BasicAuthenticationFilter.class);
}
Custom filter:
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
Optional<String> token = Optional.ofNullable(httpRequest.getHeader(HTTP_AUTHENTICATION_HEADER));
if (token.isPresent()) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token, null);
SecurityContextHolder.getContext().setAuthentication(requestAuthentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
Authentication provider:
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authenticationToken = null;
try {
// Token Validation, Loading User Info
} catch (Exception e) {
LOG.error("Failed to authenticate", e);
}
return authenticationToken;
}

Spring Security - Token based API auth & user/password authentication

I am trying to create a webapp that will primarily provide a REST API using Spring, and am trying to configure the security side.
I am trying to implement this kind of pattern: https://developers.google.com/accounts/docs/MobileApps (Google have totally changed that page, so no longer makes sense - see the page I was referring to here: http://web.archive.org/web/20130822184827/https://developers.google.com/accounts/docs/MobileApps)
Here is what I need to accompish:
Web app has simple sign-in/sign-up forms that work with normal spring user/password authentication (have done this type of thing before with dao/authenticationmanager/userdetailsservice etc)
REST api endpoints that are stateless sessions and every request authenticated based ona token provided with the request
(e.g. user logins/signs up using normal forms, webapp provides secure cookie with token that can then be used in following API requests)
I had a normal authentication setup as below:
#Override protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/mobile/app/sign-up").permitAll()
.antMatchers("/v1/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/loginprocess")
.failureUrl("/?loginFailure=true")
.permitAll();
}
I was thinking of adding a pre-auth filter, that checks for the token in the request and then sets the security context (would that mean that the normal following authentication would be skipped?), however, beyond the normal user/password I have not done too much with token based security, but based on some other examples I came up with the following:
Security Config:
#Override protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.addFilter(restAuthenticationFilter())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint()).and()
.antMatcher("/v1/**")
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/mobile/app/sign-up").permitAll()
.antMatchers("/v1/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/loginprocess")
.failureUrl("/?loginFailure=true")
.permitAll();
}
My custom rest filter:
public class RestAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public RestAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
private final String HEADER_SECURITY_TOKEN = "X-Token";
private String token = "";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
this.token = request.getHeader(HEADER_SECURITY_TOKEN);
//If we have already applied this filter - not sure how that would happen? - then just continue chain
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
//Now mark request as completing this filter
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//Attempt to authenticate
Authentication authResult;
authResult = attemptAuthentication(request, response);
if (authResult == null) {
unsuccessfulAuthentication(request, response, new LockedException("Forbidden"));
} else {
successfulAuthentication(request, response, chain, authResult);
}
}
/**
* Attempt to authenticate request - basically just pass over to another method to authenticate request headers
*/
#Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
AbstractAuthenticationToken userAuthenticationToken = authUserByToken();
if(userAuthenticationToken == null) throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
return userAuthenticationToken;
}
/**
* authenticate the user based on token, mobile app secret & user agent
* #return
*/
private AbstractAuthenticationToken authUserByToken() {
AbstractAuthenticationToken authToken = null;
try {
// TODO - just return null - always fail auth just to test spring setup ok
return null;
} catch (Exception e) {
logger.error("Authenticate user by token error: ", e);
}
return authToken;
}
The above actually results in an error on app startup saying: authenticationManager must be specified
Can anyone tell me how best to do this - is a pre_auth filter the best way to do this?
EDIT
I wrote up what I found and how I did it with Spring-security (including the code) implementing a standard token implementation (not OAuth)
Overview of the problem and approach/solution
Implementing the solution with Spring-security
Hope it helps some others..
I believe the error that you mention is just because the AbstractAuthenticationProcessingFilter base class that you are using requires an AuthenticationManager. If you aren't going to use it you can set it to a no-op, or just implement Filter directly. If your Filter can authenticate the request and sets up the SecurityContext then usually the downstream processing will be skipped (it depends on the implementation of the downstream filters, but I don't see anything weird in your app, so they probably all behave that way).
If I were you I might consider putting the API endpoints in a completely separate filter chain (another WebSecurityConfigurerAdapter bean). But that only makes things easier to read, not necessarily crucial.
You might find (as suggested in comments) that you end up reinventing the wheel, but no harm in trying, and you will probably learn more about Spring and Security in the process.
ADDITION: the github approach is quite interesting: users just use the token as a password in basic auth, and the server doesn't need a custom filter (BasicAuthenticationFilter is fine).

Resources