In order to get my account I have a external spring application that I need to login at. Why I need it is not important but in order to do a /login call on the API I need to get the password in the UserDetailsServiceMethod. Here is my security setup:
//https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
//Constructor gets authLogic for external authentication
@Autowired
public WebSecurity(@Qualifier("authLogic") UserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security", "/swagger-ui.html", "/webjars/**", "/swagger-resources/configuration/ui", "/swagger-resources/configuration/security").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(BANK_API, INVENTORY_API, MARKET_API)); //TODO: is dit correct??
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("X-Auth-Token","Authorization","Access-Control-Allow-Origin","Access-Control-Allow-Credentials"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
My UserDetailsServiceMethod implementation:
@Service
public class AuthLogic implements UserDetailsService {
private HttpServletRequest request;
private IAccountRepository accountRepository;
private RestCallLogic restCall;
@Autowired
public AuthLogic(HttpServletRequest request, IAccountRepository accountRepository, RestCallLogic restCall){
this.request = request;
this.accountRepository = accountRepository;
this.restCall = restCall;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//get password
//make restcall to external login
}
}
Is there a way I can get the password while using the spring security implementation. Because I could easily make my own class and do the login from there but it would be nice to use Spring security for it. Also the login returns a token that I can reform to a User. Maybe i'm just overthinking...
In order to make a API call i needed to write a custom AuthenticationProvider:
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails principal = new User(username, password, new ArrayList<>());
return new UsernamePasswordAuthenticationToken(principal, password, new ArrayList<>());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
After a week I finally got what I wanted. So i made a custom authentication provider that will make a REST call to my authentication API. If the username and password I gave are correct I'll get a JWT-token back that contains a username, roles and a ID. after that I just call a custom Authentication service that checks if the user id already exists in its database. If that isn't the case than I'll create a new user with the given id from the JWT-token.
Here is my custom authentication provider:
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//custom authentication service
private AuthLogic userDetailsImpl;
public JwtAuthenticationProvider(AuthLogic userDetailsImpl) {
this.userDetailsImpl = userDetailsImpl;
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//JWTUser is a custom class that extends the UserDetails class from spring
JwtUser user = (JwtUser) userDetails;
//call the custom auth service to check if the user exists in the database
userDetailsImpl.loadUserByUsername(user.getUserID(), user.getUsername());
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//get the token from a external authentication API
String token = retrieveAccountData(new LoginWrapper(username, authentication.getCredentials().toString()));
Claims claims = Jwts.parser()
.setSigningKey(JWTKEY)
.parseClaimsJws(token)
.getBody();
List<String> scopes = (List<String>) claims.get("scopes");
int UserId = (int) claims.get("userID");
List<GrantedAuthority> authorities = scopes.stream()
.map(authority -> new SimpleGrantedAuthority(authority))
.collect(Collectors.toList());
//return the User
return new JwtUser(UserId, username, authentication.getCredentials().toString(), authorities);
}
private String retrieveAccountData(LoginWrapper loginWrapper){
URI uri = UriComponentsBuilder.fromUriString(BANK_LOGIN).build().toUri();
Gson gson = new GsonBuilder().create();
RequestEntity<String> request = RequestEntity
.post(uri)
.accept(MediaType.APPLICATION_JSON)
.body(gson.toJson(loginWrapper));
//post call
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(request, String.class);
//check if status code is correct
if(response.getStatusCode() != HttpStatus.OK) {
throw new UsernameNotFoundException(loginWrapper.getUsername());
}
//convert to LoginWrapper
return gson.fromJson(response.getBody(), TokenWrapper.class).getToken();
}
}
And here is my custom authentication service:
@Service
public class AuthLogic {
private IAccountRepository accountRepository;
@Autowired
public AuthLogic(IAccountRepository context) {
this.accountRepository = context;
}
trough with the jwt token)
public UserDetails loadUserByUsername(int userId, String username) throws UsernameNotFoundException {
Optional<Account> foundAccount = accountRepository.findById(userId);
Account account;
//check if user has logged in to our inventory API before, if not create new account
if (!foundAccount.isPresent()) {
account = accountRepository.save(new Account(userId, username));
} else {
account = foundAccount.get();
}
return new JwtUserPrincipal(account);
}
}
In order to call the service from the provider you need to configure your WebSecurityConfigurerAdapter properly:
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private JwtAuthenticationProvider authenticationProvider;
@Autowired
public WebSecurity(@Qualifier("authLogic") AuthLogic userDetailsImpl) {
this.authenticationProvider = new JwtAuthenticationProvider(userDetailsImpl);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
}
I hope this answer helps.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments