spring security, UserDetailsService, authenticationProvider, pass word encoder.. i'm lost - spring

first, i've read/re-read (repeat 10 times), at least 6 books on spring and spring security and have googled my brains out trying to figure it all out.
after working w/ spring for 10 years, i still find that there is so much annotation-defined, injected, component, config annotation magic going on that i have 0 confidence that i understand my applications as i should.
examples online are either xml-config, not complete, done n diff. ways, overly simplistic, using older spring, conflicting and just simply not built to handle a basic realistic use-case.
as an example, the following code is trying to handle a simple logon, authenticated to db table using an encoder for passwords.
form post includes a "client" to which one authenticates to, a persisted IP address and some url path info for deep linking post logon.
(all really basic stuff for today's single-page web apps)
i originally had this working using xml config, but javaConfig has me stuck.
i have no idea how the userDetailsService, AuthenticationManagerBuilder and PasswordEncoder interact in SecurityConfiguration. I get logon data to the service, but am not sure where or when a spring authenticationProvider is applied or if i even need one.
my User implements UserDetails and holds the required fields.
i populate those and granted authorities in my CustomUserDetailsService.
how/when/why do i need an auth.authenticationProvider(authenticationProvider()), if i check db using logon/password in my service?
my UserDetailsService seems to be executing twice now.
how does spring take the submitted password, encode it and compare to that stored in the db?
how does it know to use the same salt as that used when the p/w was created/persisted when the user was created?
why does configureGlobal() define both auth.userDetailsService and auth.authenticationProvider when authenticationProvider() also sets the userDetailsService?
why is my brain so small that i cannot make sense of this ? :)
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private ClientDAO clientDAO;
#Autowired
private UserDAO userDAO;
public UserDetails loadUserByUsername(String multipartLogon) throws UsernameNotFoundException, DataAccessException {
Boolean canAccess = false;
Long clientId = null;
String userLogon = null;
String password = null;
String id = null;
String entryUrl = null;
String ipAddress = null;
String urlParam = null;
String[] strParts = multipartLogon.split(":");
try {
userLogon = strParts[0];
password = strParts[1];
id = strParts[2];
entryUrl = strParts[3];
ipAddress = strParts[4];
urlParam = strParts[5];
} catch(IndexOutOfBoundsException ioob) { }
Client client = new Client();
if (!"".equals(id)) {
clientId = IdUtil.toLong(id);
client = clientDAO.getClient(clientId);
}
//BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//String encodedPassword = passwordEncoder.encode(password);
//String encodedPassword = "$2a$22$6UiVlDEOv6IQWjKkLm.04uN1yZEtkepVqYQ00JxaqPCtjzwIkXDjy";
User user = userDAO.getUserByUserLogonPassword(userLogon, password); //encodedPassword?
user.isCredentialsNonExpired = false;
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (UserRole userRole : userDAO.getUserRolesForUser(user)) {
if (userRole.getRole().getActiveStatus()) {
authorities.add(new SimpleGrantedAuthority(userRole.getRole().getRoleName()));
user.isCredentialsNonExpired = true;
}
}
user.setAuthorities(authorities);
user.setPassword(password); //encodedPassword?
user.setUsername(user.getUserLogon());
user.isAccountNonExpired = false;
user.isAccountNonLocked = false;
List<ClientUser> clientUsers = clientDAO.getClientUsersForUser(user);
for (ClientUser clientUser : clientUsers) {
if (clientUser.getClient().getClientId().equals(client.getClientId())) {
canAccess = true;
break;
}
}
user.isEnabled = false;
if (user.getActiveStatus() && canAccess) {
user.isAccountNonExpired = true;
user.isAccountNonLocked = true;
user.isEnabled = true;
Session session = userDAO.getSessionForUser(user);
if (session == null) { session = new Session(); }
session.setUser(user);
session.setDateLogon(Calendar.getInstance().getTime());
session.setClient(client);
session.setEntryUrl(entryUrl);
session.setUrlParam(urlParam);
session.setIPAddress(ipAddress);
session.setActive(true);
userDAO.persistOrMergeSession(session);
}
return user;
}
}
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
CustomUserDetailsService customUserDetailsService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
auth.authenticationProvider(authenticationProvider());
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/conv/a/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_COURT_ADMIN')")
.antMatchers("/conv/u/**").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN') or hasRole('ROLE_COURT_ADMIN')")
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/conv/common/logon")
.usernameParameter("multipartLogon")
.loginProcessingUrl("/conv/common/logon")
.defaultSuccessUrl("/conv/")
.failureUrl("/conv/common/logon?error=1")
.and()
.logout()
.logoutUrl("/conv/common/logout")
.logoutSuccessUrl("/conv/")
.permitAll()
.and()
.rememberMe()
.key("conv_key")
.rememberMeServices(rememberMeServices())
.useSecureCookie(true);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/common/**")
.antMatchers("/favicon.ico");
}
#Bean
public RememberMeServices rememberMeServices() {
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("conv_key", customUserDetailsService);
rememberMeServices.setCookieName("remember_me_cookie");
rememberMeServices.setParameter("remember_me_checkbox");
rememberMeServices.setTokenValiditySeconds(2678400); //1month
return rememberMeServices;
}
}

my User implements UserDetails and holds the required fields. i
populate those and granted authorities in my CustomUserDetailsService.
how/when/why do i need an
auth.authenticationProvider(authenticationProvider()), if i check db
using logon/password in my service?
I think what you want is:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
The userDetailsService method is a shortcut for creating DaoAuthenticationProvider bean! You should not need both, its just two different ways to configure the same thing. The authenticationProvider method is used for more custom setups.
how does spring take the submitted password, encode it and compare to
that stored in the db? how does it know to use the same salt as that
used when the p/w was created/persisted when the user was created?
If you are using BCrypt, the salt is stored in the encoded password value. The salt is the first 22 characters after the third $ (dollar) sign. The matches method is responsible for checking the password.
why does configureGlobal() define both auth.userDetailsService and
auth.authenticationProvider when authenticationProvider() also sets
the userDetailsService?
See above. This is likely why user details are loaded twice.
Update: It is weird that you get password and other details into your UserDetailsService. This should only load user based on username, something like:
User user = userDAO.getUserByUserLogonPassword(userLogon);
The returned User object should contain the encoded (stored) password, not the entered password. Spring Security does the password checking for you. You should not modify the User object in you UserDetailsService.

Wow, ok that's a lot of questions. I'll speak to this one:
"I have no idea how the userDetailsService, AuthenticationManagerBuilder and PasswordEncoder "
The UserDetailsService sets up the User which you can access from Spring. If you want more user information stored in the context on the user, you need to implement your own user and set that up with your custom user details service. e.g.
public class CustomUser extends User implements UserDetails, CredentialsContainer {
private Long id;
private String firstName;
private String lastName;
private String emailAddress;
....
And then, in your custom UserDetailsService, you set the properties:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
DatabaseEntity databaseUser = this.userRepository.findByUsernameIgnoreCase(username);
customUser customUser = databaseUser.getUserDetails();
customUser.setId(databaseUser.getId());
customUser.setFirstName(databaseUser.getFirstname());
.....
The password encoder, is the mechanism Spring uses to compare the plain-text password to the encrypted hash in your database. You can use the BCryptPasswordEncoder:
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
Aside from passing that to your auth provider, you do need to do any more.
Finally, configureGlobal is where you wire things up. You define your user details service Spring is to use and the authentication provider.
In my case, I use a custom authentication provider to limit failed login attempts:
#Component("authenticationProvider")
public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider {
And then I wire everything up:
#Autowired
#Qualifier("authenticationProvider")
AuthenticationProvider authenticationProvider;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
LimitLoginAuthenticationProvider provider = (LimitLoginAuthenticationProvider)authenticationProvider;
provider.setPasswordEncoder(passwordEncoder);
auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder);
auth.authenticationProvider(authenticationProvider);
}

Related

Requests coming back as 401 unauthorized

I recently asked a question very similar to this one but instead of 401 the error I was getting was 403 (Forbbiden), but I changed the entire code so I decided to post a new one specific to this code and this problem.
I'm trying to create an user logic to my project (for the first time ever) but it has been impossible to implement any kind of security measure. I've been stuck in this for days so if anyone knows where I'm wrong I'd be grateful!
this is my code:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/users/create", "/users/create/**").permitAll()
.and()
.httpBasic();
}
}
#Data
#Component
public class CreateUserRoleDTO {
private Integer idUser;
private List<Integer> idsRoles;
public CreateUserRoleDTO() {
super();
}
public CreateUserRoleDTO(Integer idUser, List<Integer> idsRoles) {
super();
this.idUser = idUser;
this.idsRoles = idsRoles;
}
public Integer getIdUser() {
return idUser;
}
public void setIdUser(Integer idUser) {
this.idUser = idUser;
}
public List<Integer> getIdsRoles() {
return idsRoles;
}
public void setIdsRoles(List<Integer> idsRoles) {
this.idsRoles = idsRoles;
}
}
#Service
public class CreateRoleUserService {
#Autowired
private UserRepository repo;
#Autowired
private CreateUserRoleDTO createUserRoleDTO;
public Users execute(CreateUserRoleDTO createUserRoleDTO) {
Optional<Users> userExists=repo.findById(createUserRoleDTO.getIdUser());
List<Roles> roles=new ArrayList<>();
if (userExists.isEmpty()) {
throw new Error("User does not exist");
}
roles=createUserRoleDTO.getIdsRoles().stream().map(role -> {
return new Roles(role);
}).collect(Collectors.toList());
Users user=userExists.get();
user.setRole(roles);
repo.save(user);
return user;
}
#Entity
#Table(name="users_table")
public class Users implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(unique=true)
private String login;
#Column(unique=true)
private String email;
private String password;
#ManyToMany
private List<Roles> role;
}
(plus the getters and setters and constructors)
data.sql:
INSERT INTO `ROLES`(`ID`, `NAME`) VALUES(1, 'USER');
INSERT INTO `ROLES`(`ID`,`NAME`) VALUES(2, 'ADMIN');
-> the code runs fine, it even gives me the security password, the problem appears when I try to make any kind of requests.
The entire code if I've left anything out: https://github.com/vitoriaacarvalho/backend-challenge-very-useful-tools-to-remember-
An authentication configuration is missing in your SecurityConfig. For example, try adding the following to your configure method:
http.httpBasic();
Additionally, your security configuration is missing a default authorization rule, so authentication is not actually required. You can try adding .anyRequest().authenticated() to test this out.
Here's a configuration which uses the lambda syntax available in the DSL and is ready to be upgraded to Spring Security 6:
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/users/create", "/users/create/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
// Disable CSRF for testing.
// TODO: Delete the following line and learn about CSRF!
http.csrf().disable();
return http.build();
}
#Bean // Automatically injected into Spring Security
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Note: We don't configure a UserDetailsService since it is already
// annotated #Service and therefore already published as an #Bean.
}
Unfortunately, I also spotted a few other mistakes in your application that made it not work.
It looks like you have a mistake in the JPQL used to query the user for the UserDetailsService. The WHERE clause should be where u.login = :username (add a u.).
You also have the if-statement inverted as well. When throwing a UsernameNotFoundException (a better exception than Error for this case), it would look like:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = repo.findByUsernameFetchRoles(username);
if (user == null) {
throw new UsernameNotFoundException("User does not exist!");
}
return UserPrincipal.create(user);
}
Lastly, the constructor of your Users class was not assigning user data from the user parameter. It should be:
public UserPrincipal(Users user) {
this.login = user.getLogin();
this.password = user.getPassword();
...
}
With those changes, authentication works and you're on your way to learning Spring Security!

How to set a custom principal object during or after JWT authentication?

I've changed the way a user is authenticated in my backend. From now on I am receiving JWT tokens from Firebase which are then validated on my Spring Boot server.
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
// Note: "authentication" is a JwtAuthenticationToken
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Jwt jwt = (Jwt) authentication.getPrincipal();
So, after some reading and debugging I found that the BearerTokenAuthenticationFilter essentially sets the Authentication object like so:
// BearerTokenAuthenticationFilter.java
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
// Note: authenticationResult is our JwtAuthenticationToken
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
and as we can see, this on the other hand comes from the authenticationManager which is a org.springframework.security.authentication.ProviderManager and so on. The rabbit hole goes deep.
I didn't find anything that would allow me to somehow replace the Authentication.
So what's the plan?
Since Firebase is now taking care of user authentication, a user can be created without my backend knowing about it yet. I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
Further, a lot of my business logic currently relies on the principal being a user-entity business object. I could change this code but it's tedious work and who doesn't want to look back on a few lines of legacy code?
I did it a bit different than Julian Echkard.
In my WebSecurityConfigurerAdapter I am setting a Customizer like so:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer()
.jwt(new JwtResourceServerCustomizer(this.customAuthenticationProvider));
}
The customAuthenticationProvider is a JwtResourceServerCustomizer which I implemented like this:
public class JwtResourceServerCustomizer implements Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer> {
private final JwtAuthenticationProvider customAuthenticationProvider;
public JwtResourceServerCustomizer(JwtAuthenticationProvider customAuthenticationProvider) {
this.customAuthenticationProvider = customAuthenticationProvider;
}
#Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer jwtConfigurer) {
String key = UUID.randomUUID().toString();
AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(key);
ProviderManager providerManager = new ProviderManager(this.customAuthenticationProvider, anonymousAuthenticationProvider);
jwtConfigurer.authenticationManager(providerManager);
}
}
I'm configuring the NimbusJwtDecoder like so:
#Component
public class JwtConfig {
#Bean
public JwtDecoder jwtDecoder() {
String jwkUri = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken#system.gserviceaccount.com";
return NimbusJwtDecoder.withJwkSetUri(jwkUri)
.build();
}
}
And finally, we need a custom AuthenticationProvider which will return the Authentication object we desire:
#Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtDecoder jwtDecoder;
#Autowired
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
Jwt jwt;
try {
jwt = this.jwtDecoder.decode(token.getToken());
} catch (JwtValidationException ex) {
return null;
}
List<GrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("roles")) {
List<String> rolesClaim = jwt.getClaim("roles");
List<RoleEntity.RoleType> collect = rolesClaim
.stream()
.map(RoleEntity.RoleType::valueOf)
.collect(Collectors.toList());
for (RoleEntity.RoleType role : collect) {
authorities.add(new SimpleGrantedAuthority(role.toString()));
}
}
return new JwtAuthenticationToken(jwt, authorities);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(BearerTokenAuthenticationToken.class);
}
}
This is working fine so far but there's one change which I am not too happy about and it's that the principal-object is now a org.springframework.security.oauth2.jwt.Jwt and not a AppUserEntity, the user-model, like before.
In my application I have circumvented this by rolling my own JwtAuthenticationFilter instead of using BearerTokenAuthenticationFilter, which then sets my User Entity as the principal in the Authentication object. However, in my case this constructs a User barely from the JWT claims, which might be bad practice: SonarLint prompts to use a DTO instead to mitigate the risk of somebody injecting arbitrary data into his user record using a compromised JWT token. I don't know if that is a big deal - if you can't trust your JWTs, you have other problems, IMHO.
I don't know if this is the best way to do it but I intend to simply create a user record in my database once I discover a valid JWT-token of a user which does not exist yet.
Keep in mind that JWTs should be verified by your application in a stateless manner, solely by verifying their signature. You shouldn't hit the database every time you verify them. Therefor it would be better if you create a user record using a method call like
void foo(#AuthenticationPrincipal final Jwt jwt) {
// only invoke next line if reading JWT claims is not enough
final User user = userService.findOrCreateByJwt(jwt);
// TODO method logic
}
once you need to persist changes to the database that involve this user.
Since
SecurityContextHolder.setContext(context);
won't work for
request.getUserPrincipal();
you may create a custom class extending HttpServletRequestWrapper
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class UserPrincipalHttpServletRequest extends HttpServletRequestWrapper {
private final Principal principal;
public UserPrincipalHttpServletRequest(HttpServletRequest request, Principal principal) {
super(request);
this.principal = principal;
}
#Override
public Principal getUserPrincipal() {
return principal;
}
}
then in your filter do something like this:
protected void doFilterInternal(HttpServletRequest request){
. . .
// create user details, roles are required
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("SOME ROLE"));
UserDetails userDetails = new User("SOME USERNAME", "SOME PASSWORD", authorities);
// Create an authentication token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// follow the filter chain, using the new wrapped UserPrincipalHtppServletRequest
chain.doFilter(new UserPrincipalHttpServletRequest(request, usernamePasswordAuthenticationToken), response);
// all filters coming up, will be able to run request.getUserPrincipal()
}
According Josh Cummings answer in issue #7834 make configuration:
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http...
.oauth2ResourceServer(oauth2 -> oauth2.jwt(
jwt -> jwt.jwtAuthenticationConverter(JwtUtil::createJwtUser)))
...
return http.build();
}
and implement factory method, e.g:
public class JwtUtil {
public static JwtUser createJwtUser(Jwt jwt) {
int id = ((Long) jwt.getClaims().get("id")).intValue();
String rawRoles = (String) jwt.getClaims().get("roles");
Set<Role> roles = Arrays.stream(rawRoles.split(" "))
.map(Role::valueOf)
.collect(Collectors.toSet());
return new JwtUser(jwt, roles, id);
}
}
public class JwtUser extends JwtAuthenticationToken {
public JwtUser(Jwt jwt, Collection<? extends GrantedAuthority> authorities, int id) {
super(jwt, authorities);
....
}
}
Take in note, that controller's methods should inject JwtUser jwtUser without any #AuthenticationPrincipal

Customise user login behaviour in OAuth based B2B multi tenant Spring Boot application using Spring Security

I am working on a Spring Boot application, which has two tenants as of now: tenant1 and tenant2 (in the future, I will add more tenants). Each of the tenants has its own authentication providers.
In order to achieve the same, as of now, I have made the following changes in my application:
config changes are as follows:
spring.security.oauth2.client.registration.tenant1.client-id=abcd
spring.security.oauth2.client.registration.tenant1.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant1.authorization-grant-type=authorization_code
myapp.oauth2.path=https://external.authorization.server/services/oauth2/
spring.security.oauth2.client.provider.tenant1.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant1.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant1.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant1.user-name-attribute=name
spring.security.oauth2.client.registration.tenant2.client-id=efgh
spring.security.oauth2.client.registration.tenant2.client-secret=secret
spring.security.oauth2.client.registration.tenant2.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant2.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.tenant2.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant2.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant2.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant2.user-name-attribute=name
As of now, I am fetching client secrets for both tenants from Vault, so I had to define the OAuth2 configuration as follows:
#EnableConfigurationProperties(OAuth2ClientProperties.class)
#Conditional(ClientsConfiguredCondition.class)
#Configuration
public class OAuth2Configuration {
static final String OAUTH2_CLIENT_SECRET_KEY = "oauth2_client_secret";
private static final Logger log = LoggerFactory.getLogger(OAuth2Configuration.class);
private static final String OAUTH2_REGISTRATION_MISSING =
"oAuth2 registration properties are missing";
private final ApplicationSecretProvider applicationSecretProvider;
private final Map<String, ClientAuthenticationMethod> clientAuthenticationMethodMap =
new HashMap<>();
private final String authenticationMethod;
public OAuth2Configuration(
#Value("${spring.security.oauth2.client.registration.tenant1.client-authentication-method}")
final String authenticationMethod,
final ApplicationSecretProvider applicationSecretProvider) {
this.authenticationMethod = authenticationMethod;
this.applicationSecretProvider = applicationSecretProvider;
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.POST.getValue(), ClientAuthenticationMethod.POST);
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.BASIC.getValue(), ClientAuthenticationMethod.BASIC);
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.NONE.getValue(), ClientAuthenticationMethod.NONE);
}
#Bean
public InMemoryClientRegistrationRepository getClientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
//We will have only one client registered for oAuth
if (CollectionUtils.isEmpty(registrations)) {
log.error(OAUTH2_REGISTRATION_MISSING);
throw new IllegalStateException(OAUTH2_REGISTRATION_MISSING);
}
ClientRegistration registration = registrations.get(0);
ClientRegistration.Builder builder = ClientRegistration.withClientRegistration(registration);
ClientAuthenticationMethod clientAuthenticationMethod =
getClientAuthenticationMethod(authenticationMethod);
ClientRegistration completeRegistration = builder
.clientSecret(applicationSecretProvider.getSecretForKey(OAUTH2_CLIENT_SECRET_KEY))
.clientAuthenticationMethod(clientAuthenticationMethod)
.build();
ClientRegistration testRegistration = registrations.get(1);
return new InMemoryClientRegistrationRepository(List.of(completeRegistration, testRegistration));
}
protected ClientAuthenticationMethod getClientAuthenticationMethod(String grantType) {
ClientAuthenticationMethod retValue = clientAuthenticationMethodMap.get(grantType);
if (retValue == null) {
return ClientAuthenticationMethod.NONE;
}
return retValue;
}
}
Then I extended DefaultOAuth2UserService in order to save user details in my application as follows:
#Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private UserRepository userRepository;
private AuthorityRepository authRepository;
#Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Autowired
public void setAuthorityRepository(AuthorityRepository
authorityRepository) {
this.authorityRepository = authorityRepository;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) super.loadUser(userRequest);
Collection<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());
Map<String, Object> attributes = oAuth2User.getAttributes();
...
return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
}
}
Security configuration is as follows:
#EnableWebSecurity
#Import(SecurityProblemSupport.class)
#ConditionalOnProperty(
value = "myapp.authentication.type",
havingValue = "oauth",
matchIfMissing = true
)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customoAuth2UserService;
public SecurityConfiguration(CustomOAuth2UserService customoAuth2UserService) {
this.customoAuth2UserService = customoAuth2UserService;
}
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/manage/**").permitAll()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.anyRequest().authenticated()
//.and().oauth2ResourceServer().jwt()
.and()
//.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri("/oauth2**")
.and()
.failureUrl("/api/redirectToHome")
.userInfoEndpoint().userService(customoAuth2UserService);
http.cors().disable();
}
}
With this on /login page, users from both the tenants are able to see both the login links.
I have following question in this regard:
(1) Instead of showing multiple links on the login page, would like to have a common entry page for all users, where users can enter the email and based on tenant id (derived from email), user can be redirected to the appropriate authentication provider and post successful authentication, authenticated user details can be saved in application database as done in CustomOAuth2UserService. How would I achieve this?
In this regard, I have gone through several articles/posts but did not get any concrete idea regarding what changes should I do in the existing code base to achieve this.

Using JWT with #PathVariable but only allow access url for spesific user

I am creating simple Rest social media application with Spring Boot. I use JWT for authentication in application.
In my mobile application when users register, i am getting some information from users and create account and profile of the user.
By the way, you can see (simplified) database object of account and profile. I use Mongo DB for database.
account:
{
“_id”: “b6164102-926e-47d8-b9ff-409c44dc47c0“,
“email”: “xxx#yy.com”
….
}
profile:
{
“_id”: “35b06171-c16a-4559-90f3-df81ace6d64a“,
“accountId”: “b6164102-926e-47d8-b9ff-409c44dc47c0”,
profileImages: [
{
“imageId”: “1431b0bc-feb7-436d-9d3a-7b9094547bf6”,
“imageLink”: “https://this_is_some_link_to_image.com
}
….
]
….
}
When user login to app, i add accountId to JWT and then in my mobile app i call below endpoint to get profile information of user. I take accountId from jwt and find profile of that account id.
#GetMapping("/profiles")
public ResponseEntity<BaseResponse> getUserProfile(#AuthenticationPrincipal AccountId accountId) {
var query = new Query(accountId);

 var presenter = new GetUserProfilePresenter();


 useCase.execute(query, presenter);


 return presenter.getViewModel();
}
In the app, users can upload photo to their profile using below endpoint;
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {

......
}
Everything works fine but the problem is someone can use their token to call this url with another person’s profileId. Because profileId is not a hidden id. In my mobile app users can shuffle and see other users profile using below url.
This url is accessible by any authenticated users.
#GetMapping(path = "/profiles/{profileId}")
public ResponseEntity<BaseResponse> getProfile(#PathVariable("profileId") UUID profileId) {
......
}
Now, my question is how can i make "/profiles/{profileId}/images" this url is only accesible for user of this profile without changing path format.
For exampe;
User A - Profile Id = XXX
User B - Profile Id = YYY
I want that if User A calls this url with own JWT Token, uploads image only to own profile not another one profile.
I have come up with some solutions but these solutions cause me to change the url path;
Solution 1:
I can use accountId in the jwt. Find profile of user with this accountId so that, every call to this url guaranteed upload image only to profile of token user.
But this solution change url path like below because i dont need to get any profileId from path.
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(

 #AuthenticationPrincipal AccountId accountId, #RequestParam("image") MultipartFile image) throws IOException {


 ......
}
Solution 2:
This is very similar to first solution only different is when i create jwt for user. I will put profileId of user to inside of JWT. So when the user calls the url i will get profileId from jwt and put inside of Authentication object. And in the controller i will get this profileId for using to find profile of user then upload image to this spesific profile.
But also, this solution change url path format because i dont need to get profileId from url path.
So if i back to my main question. What is the best practices and solutions for these kinda problems and situations?
~~~EDIT~~~
For those whose wonder, i didn't change my path. Actually i implemented solution 1 with a twist.
Now i use accountId from JWT and profileId at the same time so when i want to find a profile of exactly that user i search the database using accountId and profileId together.
With this change, i didn't need to change other paths.
For example; (GET) /profiles/{profileId} this path still meaningful for all authenticated users.
But (POST) /profiles/{profileId}/images this path only meaningful for that spesific (owner of token) user.
By the way, i starts paths with "api/admin/**" prefix for my admin role operations.
Final code (Controller);
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal AccountId accountId,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
....
}
Final code (Repository);
#Repository
public interface ProfileJpaRepository extends MongoRepository<ProfileDto, String> {
Optional<ProfileDto> findByAccountId(String accountId);
Optional<ProfileDto> findByIdAndAccountId(String profileId, String accountId);
}
The best practice to handle this kind of scenarios is to have two endpoints, each needing different kind of permissions:
"/profiles/{profileId}/images" will be available for admins, so that if an admin wants to change another user's profile image, they can do so by calling this endpoint.
"/profiles/images" will be responsible for changing the most generic users with the lowest privileges.
So, in both scenarios you need to extract the AccountId from the JWT and you should not get the AccountId from the user directly, unless for administration purposes where you check the privileges to authorize the user.
Now, the best way to implement such a system, is to use Spring Security and to create a custom AuthenticationToken, then to customize AbstractUserDetailsAuthenticationProvider, * AbstractAuthenticationProcessingFilter* and UsernamePasswordAuthenticationToken.
After doing so, you can then configure Spring to use the custom provider for authentication.
UsernamePasswordAuthenticationToken
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
private Payload payload; // Payload can be any model class that encapsulates the payload of the JWT.
private boolean creationAllowed;
public JwtAuthenticationToken(String jwtToken) throws Exception {
super(null, jwtToken);
// Verify JWT and get the payload
this.payload = // set the payload
}
public JwtAuthenticationToken(String principal, JwtAuthenticationToken authToken, Collection<? extends GrantedAuthority> authorities) {
super(principal, authToken.getCredentials(), authorities);
this.payload = authToken.payload;
authToken.eraseCredentials(); // not sure if this is needed
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public Payload getPayload() {
return this.firebaseToken;
}
public boolean isCreationAllowed() {
return creationAllowed;
}
public void setCreationAllowed(boolean creationAllowed) {
this.creationAllowed = creationAllowed;
}
}
AbstractUserDetailsAuthenticationProvider
#Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
AppUserService appUserService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(JwtAuthenticationToken.class, authentication, () ->
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JwtAuthenticationToken is supported")
);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;
String principal;
try {
principal = jwtAuthToken.getPayload().getEmail(); // Here I'm using email as the user identifier, this can be anything, for example AccountId
} catch (RuntimeException re) {
throw new AuthenticationException("Could not extract user's email address.");
}
AppUser user = (AppUser) this.retrieveUser(principal, jwtAuthToken);
return this.createSuccessAuthentication(principal, jwtAuthToken, user);
}
#Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
JwtAuthenticationToken result = new JwtAuthenticationToken((String) principal, (JwtAuthenticationToken) authentication, user.getAuthorities());
result.setDetails(user);
return result;
}
#Override
public UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
UserDetails userDetails = appUserService.loadUserByUsername(s);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) usernamePasswordAuthenticationToken;
if (userDetails != null)
return userDetails; // You need to create an UserDetails which will be set by the framework to the Security Context as the authenticated user, this will be useful later when you want to check the privileges.
else
throw new AuthenticationException("Creating the user details is not allowed.");
}
#Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
#Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
AbstractAuthenticationProcessingFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/**"); // The path that this filter needs to process, use "/**" to make sure all paths must be proessed.
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true; // Here I am returning true to require authentication for all requests.
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer "))
throw new AuthenticationException("No JWT token found in request headers");
String authToken = authorization.substring(7);
JwtAuthenticationToken token = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// Authentication process succeed, filtering the request in.
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
// Authentication process failed, filtering the request out.
}
}
UserDetails
public class AppUser implements UserDetails {
// A class to be used as a container for user details, you can add more details specific to your application here.
}
Finally, you need to configure Spring boot to use this classes:
SecurityConfig
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
// -- public paths, for example: swagger ui paths
new AntPathRequestMatcher("/swagger-ui.html"),
new AntPathRequestMatcher("/swagger-resources/**"),
new AntPathRequestMatcher("/v2/api-docs"),
new AntPathRequestMatcher("/webjars/**")
);
private JwtAuthenticationProvider provider;
public SecurityConfig(JwtAuthenticationProvider provider) {
this.provider = provider;
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS) // Allowing browser pre-flight
.requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet authenticated
//.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.authenticationEntryPoint(forbiddenEntryPoint())
.and()
.authenticationProvider(this.provider)
.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
}
#Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
final JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(this.authenticationManager());
filter.setAuthenticationSuccessHandler(this.successHandler());
filter.setAuthenticationFailureHandler(this.failureHandler());
return filter;
}
#Bean
JwtAuthenticationSuccessHandler successHandler() {
return new JwtAuthenticationSuccessHandler();
}
#Bean
JwtAuthenticationFailureHandler failureHandler() {
return new JwtAuthenticationFailureHandler();
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(JwtAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
AuthenticationFailureHandler
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> data = new HashMap<>();
data.put("exception", e.getMessage());
httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(data));
}
}
AuthenticationSuccessHandler
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
}
OKAY!
Now that you have implemented the security correctly, you can access user details and privileges from anywhere using the last piece:
UserDetailsService
#Service
public class AppUserService implements UserDetailsService {
#Autowired
private AppUserRepository appUserRepository;
public AppUser getCurrentAppUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null)
return (AppUser) authentication.getDetails();
return null;
}
public String getCurrentPrincipal() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<AppUser> appUserOptional = this.appUserRepository.findByEmailsContains(new EmailEntity(s)); // This should be changed in your case if you are using something like AccountId
appUserOptional.ifPresent(AppUser::loadAuthorities);
return appUserOptional.orElse(null);
}
}
Great.
Let's see how to use it in your Controllers:
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(#RequestParam("image") MultipartFile image) throws IOException {

AppUser user = this.appUserService.getCurrentAppUser();
Long id = user.getAccountId(); // Or profile id or any other identifier that you needed and extracted from the JWT after verification.
// set the profile picture.
// save changes of repository and return.
}
For admin purposes:
#PreAuthorize ("hasRole('ROLE_ADMIN')")
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {

AppUser user = this.appUserService.getCurrentAppUser();
// set the profile picture using profileId parameter
// save changes of repository and return.
}
The only remaining task is to assign the ROLE_ADMIN to the right user when loading it from the database. To do this, there are a lot of different approaches and it totally depends on your requirements. Overall, you can save a role in the database and relate it to a specific user and simply load it using an Entity.
Let's get few things right here , I am assuming that you have like two entities - Account and Profile and you wish to upload/update new profile image using same API -
#PostMapping(path = "/profiles/{profileId}/images
If ADMIN role , update profile image for #PathVariable("profileId") OR if USER role update their own profile image using #PathVariable("profileId") and not any other Profile entity image using ProfileId if current user is authenticated.
Please check this link for Role-Permission Authentication
Spring Boot : Custom Role - Permission Authorization using SpEL
User Principal
#Getter
#Setter
#Builder
public class UserPrincipal implements UserDetails {
/**
* Generated Serial ID
*/
private static final long serialVersionUID = -8983688752985468522L;
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Collection<? extends GrantedAuthority> permissions;
public static UserPrincipal createUserPrincipal(Account account) {
if (userDTO != null) {
List<GrantedAuthority> authorities = userDTO.getRoles().stream().filter(Objects::nonNull)
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
List<GrantedAuthority> permissions = account.getRoles().stream().filter(Objects::nonNull)
.map(Role::getPermissions).flatMap(Collection::stream)
.map(permission -> new SimpleGrantedAuthority(permissionDTO.getName().name()))
.collect(Collectors.toList());
return UserPrincipal.builder()
.id(account.getId())
.email(account.getEmail())
.authorities(authorities)
.permissions(permissions)
.build();
}
return null;
}
AuthenticationFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwtToken = getJwtTokenFromHttpRequest(request);
if (StringUtils.isNotBlank(jwtToken) && jwtUtils.validateToken(jwtToken)) {
Long accountId = jwtUtils.getAccountIdFromJwtToken(jwtToken);
UserDetails userDetails = customUserDetailsService.loadUserByUserId(accountId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails
.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
}
filterChain.doFilter(request, response);
}
private String getJwtTokenFromHttpRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
AuthUtil
#UtilityClass
public class AuthUtils {
public boolean isAdmin(UserPrincipal userPrincipal){
if(CollectionUtils.isNotEmpty(userPrincipal.getAuthorities())){
return userPrincipal.getRoles().stream()
.filter(Objects::nonNull)
.map(GrantedAuthority::getName)
.anyMatch(role -> role.equals("ROLE_ADMIN"));
}
return false;
}
}
Profile Service
#Service
public class ProfileService {
#Autowired
private ProfileRepository profileRepository;
public Boolean validateProfileIdForAccountId(Integer profileId, Long accountId) throws NotOwnerException,NotFoundException {
Profile profile = profileRepository.findByAccountId(profileId,accountId);
if(profile == null){
throw new NotFoundException("Profile does not exists for this account");
} else if(profile.getId() != profileId){
throw new NotOwnerException();
}
return true;
}
}
ProfileController
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
#PostMapping(path = "/profiles/{profileId}/images", consumes =
MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal UserPrincipal currentUser,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
if(!AuthUtils.isAdmin(currentUser)){
profileService.validateProfileIdForAccountId(profileId, currentUser.getId());
}
}
Now you can validate whether the #PathVariable("profileId") does indeed belong to the authenticated CurrentUser, you are also checking if the CurrentUser is ADMIN.
You can also add & check any specific permission for ROLES for facilitating UPLOAD/UPDATE
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') or hasPermission('UPDATE')")

Jwt token gets invalidated on each restart of spring boot application

I have a spring boot application which generates a jwt token on successful login. and the token is returned to the user, but on each restart of the application the token is invalidated. I have a secret key stored on the properties file for now to test. here is my code to generate it,
public String createToken(String username, String role) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", role);
claims.put("test", "test");
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();
}
I would like to have the token validated even though the application is restarted. Any suggestions on what I could be doing wrong is appreciated.
My Config class
public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
public void configure(HttpSecurity http) throws Exception {
JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}

Resources