Save user after success authentication [duplicate] - java

This question already has answers here:
Filter invoke twice when register as Spring bean
(3 answers)
Closed 6 months ago.
I have set up keycloak with a google identity provider. And I have set up a simple reactive spring-boot web application with spring security and MongoDB. I want to save users after they successfully pass the authorization filter. Here is my security configuration:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
#Slf4j
#RequiredArgsConstructor
public class SecurityConfiguration {
private final UserSavingFilter userSavingFilter;
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange().authenticated()
.and()
.addFilterAfter(userSavingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
And here is my filter for saving users:
public class UserSavingFilter implements WebFilter {
private final ObjectMapper objectMapper;
private final UserService userService;
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Base64.Decoder decoder = Base64.getUrlDecoder();
var authHeader = getAuthHeader(exchange);
if (authHeader == null) {
return chain.filter(exchange);
}
var encodedPayload = authHeader.split("Bearer ")[1].split("\\.")[1];
var userDetails = convertToMap(new String(decoder.decode(encodedPayload)));
saveUserIfNotPresent(userDetails);
return chain.filter(exchange);
}
#SneakyThrows
private void saveUserIfNotPresent(Map<String, Object> map) {
var userEmail = String.valueOf(map.get("email"));
var userPresent = userService.existsByEmail(userEmail).toFuture().get();
if (userPresent) {
return;
}
log.info("Saving new user with email: {}", userEmail);
var user = new User();
user.setEmail(userEmail);
user.setFirstName(String.valueOf(map.get("given_name")));
user.setLastName(String.valueOf(map.get("family_name")));
userService.save(user).subscribe(User::getId);
}
#SuppressWarnings("java:S2259")
private String getAuthHeader(ServerWebExchange exchange) {
var authHeaders = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION);
if (authHeaders == null) {
return null;
}
return authHeaders.get(0);
}
#SneakyThrows
private Map<String, Object> convertToMap(String payloadJson) {
return objectMapper.readValue(payloadJson,Map.class);
}
}
Problems:
For some reason, my filter executes twice per request. I can see 2 log messages about saving new user.
When I call getAll() endpoint, it does not return the user saved in this request in the filter.
Probably it is not the best way to save users, but I could not find an alternative to successHandler for the resource server with jwt. Please suggest how can I solve those two problems.

By any chance is your filter annotated with #Component ? This could explain why it is called twice, as Spring Boot automatically registers any bean that is a Filter with the servlet container (see documentation).
So you can setup a registration bean to disable it :
#Bean
public FilterRegistrationBean<UserSavingFilter> disableUserSavingFilter(final UserSavingFilter filter) {
final FilterRegistrationBean<UserSavingFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setEnabled(false);
return filterRegistrationBean;
}
By default, custom filter beans without information of ordering are automatically added to the main servlet filter chain at the very last position (actually with the lowest precedence, as if you would apply default order annotation #Order(Ordered.LOWEST_PRECEDENCE), see Order).
In debug level you should see in the logs the position of the filters when they are added to the chain, something like :
... at position 4 of 4 in additional filter chain; firing Filter:
UserSavingFilter
About your second problem, if you are sure the user is actually saved (i.e. you find it into the database afterwards) then indeed it may just be because your getAll() method gets executed before your future call is completed.

Related

Trying to access the end point of a json rpc service from a java spring API by using axios in react js

I'm actually trying to access the method at that endpoint but I keep getting the classic CORS error
"Access to XMLHttpRequest at 'http://localhost:8080/rpc-measurements' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status."
#JsonRpcService("rpc-measurements")
public interface JsonRpcAPI {
public List<Measurements> getMeasurementsByDays(String username, int days);
public Float getAvgEnergyConsumption();
public String getUsername(String username);
}
The implementation:
#Service
#AutoJsonRpcServiceImpl
public class JsonRpcAPIimpl implements JsonRpcAPI {
#Autowired
private MeasurementsRepository measurementsRepository;
#Autowired
private DeviceRepository deviceRepository;
#Autowired
private PersonRepository personRepository;
#Override
public List<Measurements> getMeasurementsByDays(String username, int days) {
Person person = personRepository.findByName(username);
List<Device> devices = deviceRepository.findByPersonId(person.getId());
List<Measurements> measurements = new ArrayList<>();
devices.forEach(x -> {
measurements.addAll(measurementsRepository.getEnergyConsumptionForDays(UuidAdapter.getBytesFromUUID(x.getId()), days));
});
return measurements;
}
#Override
public Float getAvgEnergyConsumption() {
return 0.0f;
}
#Override
public String getUsername(String username) {
return username;
}
}
The security configuration file:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Resource(name = "userService")
private CustomUserService userService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
/*http.csrf().disable().authorizeRequests()
.anyRequest().permitAll();*/
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
#Autowired
private UnauthorizedEntryPoint unauthorizedEntryPoint;
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
}
And finally the react js function that calls the API:
function handleValueSelect(event) {
axios.post(REST_API_URL + '/rpc-measurements', {
id: "1",
jsonrpc: "2.0",
method:"getAvgEnergyConsumption",
params:[]
},{
headers: {
Authorization: "Bearer " + localStorage.getItem('userToken')
}
}).then(res => {
setDataP(res.data.map((data) => ({
time: ((data.time.split('T')[1])
.split('+')[0])
.split('.')[0],
value: data.value
})));
});
};
I have been wasting half a day trying to find some solutions, most of the problems related to the CORS policy error that I found on the internet were solved by either adding the #CrossOrigin annotation to a controller or by adding http.cors() in the security configuration file, the thing is that I am not trying to access an endpoint from a controller I am trying to access an endpoint from a json rpc service in order to use a method from there and I have no idea if I have to set up cross origin for that particular path in a different way compared to a controller (where you just use the annotation, and yes I already tried adding it to the service).
The weirdest thing is that I tried accessing the URL in postman with exactly the same body as in the axios request and it worked, but when I tried in the web client the CORS error kept popping up. I even tried creating a Rest Controller and using JsonRpcRestClient to invoke the method but it didn't work, it freezes my spring app, I don't even get an error or an exception in the console, it just stops responding.
So I wanted to ask if there's someone who's been in a similar situation or has some ideas on how to deal with the CORS policy error taking into account that I am trying to access an end point from a Service and not a controller.
I have faced the same issue with react and spring.
In this case, browser is blocking your request.
put the reactjs build file inside template folder and run spring boot sends index.html then reactjs start works and cros error not come but it requires restController for all other requests.

How to secure communication between 2 Spring Boot microservices using OAuth2?

I'm learning about securing microservices with Basic Authentication and OAuth2 JWT Token Authentication. I implemented it using Basic Authentication and now I want to transform it in OAuth2 Authentication.
This is the implementation for securing the communication between these 2 microservices using Basic Auth.
Microservice 1 - REST API
#Configuration
#Getter
public class DemoApiConfiguration {
#Value("${demo.api.credentials.username}")
private String username;
#Value("${demo.api.credentials.password}")
private String password;
}
SecurityConfigurer class:
#Configuration
#RequiredArgsConstructor
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
private final DemoApiConfiguration apiConfig;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
#Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails theUser = User.withUsername(apiConfig.getUsername())
.password(passwordEncoder.encode(apiConfig.getPassword())).roles("USER").build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(theUser);
return userDetailsManager;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Controller class:
#RestController
#RequestMapping("/rest/api/v1")
public class HomeController {
#GetMapping("/products")
public String home() {
return "These are products!";
}
}
application.yml:
demo:
api:
credentials:
username: ${demo_api_username:john}
password: ${demo_api_password:test}
Microservice 2 - REST Consumer
#Configuration
#Getter
public class DemoApiConfiguration {
#Value("${demo.api.credentials.username}")
private String username;
#Value("${demo.api.credentials.password}")
private String password;
#Value("${demo.api.credentials.basePath}")
private String basePath;
}
WebConfigurer class:
#Configuration
#RequiredArgsConstructor
public class WebConfigurer {
private final DemoApiConfiguration apiConfig;
#Bean
public ApiClient restTemplate() {
RestTemplate restTemplate = new RestTemplate();
ApiClient apiClient = new ApiClient(restTemplate);
apiClient.setBasePath(apiConfig.getBasePath());
return apiClient;
}
public String getAuthorization() {
return (!StringUtils.isEmpty(apiConfig.getUsername()) &&
!StringUtils.isEmpty(apiConfig.getPassword())) ?
"Basic " + Base64Utils.encodeToString((
apiConfig.getUsername() + ":" + apiConfig.getPassword())
.getBytes()) :
null;
}
}
ApiClient class:
#Getter
#RequiredArgsConstructor
#Slf4j
public class ApiClient {
private static final String AUTHORIZATION_HEADER = "Authorization";
private final RestTemplate restTemplate;
private String basePath;
public ApiClient setBasePath(String basePath) {
this.basePath = basePath;
return this;
}
public String invokeApi(String path, String credentials) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(basePath).path(path);
RequestEntity.BodyBuilder requestBuilder =
RequestEntity.method(HttpMethod.GET, builder.build().toUri());
requestBuilder.contentType(MediaType.APPLICATION_JSON);
requestBuilder.header(AUTHORIZATION_HEADER, credentials);
RequestEntity<Object> requestEntity = requestBuilder.body(null);
return restTemplate
.exchange(requestEntity, String.class).getBody();
}
}
ConsumeController class:
#RestController
#RequiredArgsConstructor
public class ConsumeController {
private static final String PATH = "/rest/api/v1/products";
private final WebConfigurer webConfigurer;
private final ApiClient apiClient;
#GetMapping(value = "/products-client")
public String getProductList() {
return apiClient.invokeApi(PATH, webConfigurer.getAuthorization());
}
}
application.yml:
server:
port: 8090
demo:
api:
credentials:
username: ${demo_api_username:john}
password: ${demo_api_password:test}
basePath: ${demo_api_path:http://localhost:8080}
So the first microservice is a REST API and the second microservice is a REST consumer and the communication is secured using Basic Auth.
Now I want to implement using OAuth2, and I want to ask you how can I secure the communication using OAuth2? So I want to add another endpoint like "/access-token", and the client first will do a request at this endpoint with username and password and will get a jwt token. After that will do a request for "/products" endpoint with Authorization header using this jwt token. Can you help me to do this kind of implementation? Thank you!
Overview
You will need client credential grant type flow to communicate between apps. Spring has built in support for well known providers like facebook, google and so on. In our case we provide our own authorization server.
Note - Client credential doesn't return a refresh token as per spec - so make sure you ask for new access token when the current access token is expired.
Client
application properties
security.basic.enabled=false
server.port=8082
spring.security.oauth2.client.registration.server.client-id=first-client
spring.security.oauth2.client.registration.server.client-secret=noonewilleverguess
spring.security.oauth2.client.registration.server.client-authentication-method=basic
spring.security.oauth2.client.registration.server.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.server.scope=read
spring.security.oauth2.client.provider.server.token-uri=http://server:8080/oauth/token
main class
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
#Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
credential client grant flow configuration
#Configuration
public class OauthClientCredentialConfig {
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository) {
OAuth2AuthorizedClientService service =
new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, service);
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
pom dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
rest client
#Getter
#RequiredArgsConstructor
#Slf4j
#Component
public class ApiClient {
private static final String AUTHORIZATION_HEADER = "Authorization";
private final RestTemplate restTemplate;
private final OAuth2AuthorizedClientManager authorizedClientManager;
public String invokeApi(String path) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://server:8080").path(path);
RequestEntity.BodyBuilder requestBuilder =
RequestEntity.method(HttpMethod.GET, builder.build().toUri());
requestBuilder.contentType(MediaType.APPLICATION_JSON);
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthorizeRequest oAuth2AuthorizeRequest =
OAuth2AuthorizeRequest.withClientRegistrationId("server")
.principal(principal.getName())
.build();
requestBuilder.header(AUTHORIZATION_HEADER, "Bearer " + authorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken().getTokenValue());
RequestEntity<Object> requestEntity = requestBuilder.body(null);
return restTemplate.exchange(requestEntity, String.class).getBody();
}
}
Authorization and Resource Server
Note for authorization and resource server we are using legacy version as there is no support to create authorization server in new spring security oauth2 module.
Configuration
#EnableWebSecurity
public class Security extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/oauth/token")
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
#EnableAuthorizationServer
#EnableResourceServer
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Auth Server Config
#Import(AuthorizationServerEndpointsConfiguration.class)
#Configuration
#Order(2)
#RequiredArgsConstructor
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
private final TokenStore tokenStore;
private final AccessTokenConverter accessTokenConverter;
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("first-client")
.secret(passwordEncoder().encode("noonewilleverguess"))
.scopes("read")
.authorizedGrantTypes("client_credentials")
.scopes("resource-server-read", "resource-server-write");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.accessTokenConverter(accessTokenConverter)
.tokenStore(tokenStore);
}
}
Jwt Config
#Configuration
public class JwtTokenConfig {
#Bean
public KeyPair keyPair() throws NoSuchAlgorithmException {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KeyPair keyPair = gen.generateKeyPair();
return keyPair;
}
#Bean
public TokenStore tokenStore() throws NoSuchAlgorithmException {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() throws NoSuchAlgorithmException {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
}
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.6</version>
</dependency>
I've added a working example at
https://github.com/saagar2000/oauth2_server
https://github.com/saagar2000/oauth2_client
Response with valid access token
More explanation can be found here
It is necessary differentiate between JWT token based authentication, it seems what you are trying to achieve, and OAuth2 authentication, a more complex subject.
For OAuth2 authentication, Spring framework provides support with the Spring Security OAuth project, but my best advice is that, if you actually need OAuth2 in your project, it is better use a third party OAuth2 provider, like Okta or Auth0, or one of the providers offered in the cloud - for instance, GCP OAuth clients, AWS Cognito, Azure AD applications, etcetera, or a product like Keycloak. All these products will provide you a robust OAuth2 implementation and libraries and mechanisms that will help you to integrate with them.
But it seems for the last paragraphs of your question that what you actually need is authenticate your microservices with JWT tokens.
Let's talk about the server side requirements first.
To accomplish this task, the first thing you need is a service that generates and validates JWT tokens. Maybe something like:
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
// ...
#Component
public class JWTService {
// Get itfrom a configuration property, for instance
#Value("${secretKey}")
private String secretKey;
#Value("${tokenValidityInMillis}")
private Long tokenValidityInMillis;
public String createToken(Authentication authentication) {
long now = (new Date()).getTime();
Date validity = new Date(now + this.tokenValidityInMillis);
// Modify it as per your needs, defining claims, etcetera. For instance
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim("authorities", authorities)
// The signature algorithm you consider appropriate
.signWith(SignatureAlgorithm.HS256, secretKey)
.setExpiration(validity)
.compact();
}
public Authentication getAuthentication(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
// Get the authorities back
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new PreAuthenticatedAuthenticationToken(principal, token, authorities);
} catch (Exception e) {
// Handle exceptions (expiration, invalid signature, etcetera) as you wish
}
return null;
}
}
You have several libraries for handling the actual JWT token stuff. The example is using jjwt.
Then, define a Controller that swap the provided credentials for an access token:
import org.springframework.security.authentication.AuthenticationManager;
//...
#RestController
public class AuthController {
private final JWTService jwtService;
private final AuthenticationManager authenticationManager;
public AuthRestController(final JWTService jwtService, final AuthenticationManager authenticationManager) {
this.jwtService = jwtService;
this.authenticationManager = authenticationManager;
}
#PostMapping("/access-token")
public ResponseEntity<JWTToken> swapAccessToken(#RequestBody LoginDTO loginDTO) {
// Note we are passing a JSON object with two fields, username and password,
// not actual HTTP parameters. Modify it according to your needs
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
String jwt = jwtService.createToken(authentication);
return new ResponseEntity.ok(new JWTToken(jwt));
}
}
Where LoginDTO is a simple POJO for storing the username and password:
public class LoginDTO {
private String username;
private String password;
// Getters and setters omitted for brevity
}
And JWTToken is just a convenient way to return the generated token as JSON instead of plain text:
public class JWTToken {
private String idToken;
JWTToken(String idToken) {
this.idToken = idToken;
}
#JsonProperty("id_token")
String getIdToken() {
return idToken;
}
}
The next thing you need is some mechanism that will validate the tokens when necessary. I think the best way you can achieve this is implementing a custom filter that performs the user authentication by inspecting the JWT token. For example:
public class JWTFilter extends GenericFilterBean {
private final JWTService jwtService;
public JWTFilter(final JWTService jwtService) {
this.jwtService = jwtService;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = getTokenFromHttpRequest(httpServletRequest);
if (jwt != null) {
// We have a token, perform actual authentication
Authentication authentication = this.jwtService.getAuthentication(jwt);
// If success
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// Unsuccesful authentication, let the spring security chain continue and fail if necessary
filterChain.doFilter(servletRequest, servletResponse);
}
// Look for token in an Authorization Bearer header
private String getTokenFromHttpRequest(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
All this components must be configured for the Spring Security. It probably need to be further adapted, but please, get the idea:
#Configuration
#RequiredArgsConstructor
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
private final DemoApiConfiguration apiConfig;
private final JWTService jwtService;
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
// Probably you need to handle more stuff like configuring exception
// handling endpoints for access denied, stateless sessions, CORS, think about it...
http
.csrf().disable()
.authorizeRequests()
// Allow to swap the credentials for access token
.antMatchers("/access-token").permitAll()
// Require authentication for the rest of your API
.anyRequest().authenticated();
// Include your filter somewhere the Spring Security filter chain
final JWTFilter jwtFilter = new JWTFilter(jwtService);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
// This is an important step: as we are providing both username an
// password and preauthenticated credentials, so we need to configure
// AuthenticationManager that actually supports both authentication types
// It will use your userDetailsService for validating
// the original provided credentials
#Bean
#Override
public AuthenticationManager authenticationManager() {
// Username and password validation
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(userDetailsService());
PreAuthenticatedAuthenticationProvider preAuthProvider = new PreAuthenticatedAuthenticationProvider();
preAuthProvider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService()));
return new ProviderManager(Arrays.<AuthenticationProvider> asList(daoAuthenticationProvider, preAuthProvider));
}
#Bean
public UserDetailsService userDetailsService() {
if (userDetailsService == null) {
userDetailsService = this.initUserDetailsService(passwordEncoder());
}
return userDetailsService;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private UserDetailsService initUserDetailsService(PasswordEncoder passwordEncoder) {
UserDetails theUser = User.withUsername(apiConfig.getUsername())
.password(passwordEncoder.encode(apiConfig.getPassword())).roles("USER").build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(theUser);
return userDetailsManager;
}
}
Your client microservice only need to swap the configured credentials for an access token, and use the returned JWT as the value of a Bearer HTTP Authorization header when you invoke a protected endpoint. It should be straightforward but let me know if you need further help on this.
Microservice Architecture
The ideal way or commonly preferred way is the API Gateway Pattern for the microservices however it may change according to the projects and requirements. Let's consider the following components
Config Server:
Responsible to manage the configurations for the microservices and we may change the configurations dynamically using spring cloud features with a common bus interface with Kafka or RabbitMQ
API Gateway:
This will be the common entry point to manage the REST request for other services. We can manage the requests using a load balancer here. Also, we can serve the UI from the API Gateway.
Authentication Service (UAA):
This should be responsible for managing the user management and related activity. This is where you will add #EnableAuthorizationServer and extend AuthorizationServerConfigurerAdapter
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
int accessTokenValidity = uaaProperties.getWebClientConfiguration().getAccessTokenValidityInSeconds();
accessTokenValidity = Math.max(accessTokenValidity, MIN_ACCESS_TOKEN_VALIDITY_SECS);
int refreshTokenValidity = uaaProperties.getWebClientConfiguration().getRefreshTokenValidityInSecondsForRememberMe();
refreshTokenValidity = Math.max(refreshTokenValidity, accessTokenValidity);
/*
For a better client design, this should be done by a ClientDetailsService (similar to UserDetailsService).
*/
clients.inMemory()
.withClient(uaaProperties.getWebClientConfiguration().getClientId())
.secret(passwordEncoder.encode(uaaProperties.getWebClientConfiguration().getSecret()))
.scopes("openid")
.autoApprove(true)
.authorizedGrantTypes("implicit","refresh_token", "password", "authorization_code")
.accessTokenValiditySeconds(accessTokenValidity)
.refreshTokenValiditySeconds(refreshTokenValidity)
.and()
.withClient(applicationProperties.getSecurity().getClientAuthorization().getClientId())
.secret(passwordEncoder.encode(applicationProperties.getSecurity().getClientAuthorization().getClientSecret()))
.scopes("web-app")
.authorities("ROLE_GA")
.autoApprove(true)
.authorizedGrantTypes("client_credentials")
.accessTokenValiditySeconds((int) jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSeconds())
.refreshTokenValiditySeconds((int) jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe());
}
Service 1, Service 2...
This will be the microservice to manage the business logic and requirements which is commonly known as Resource Server which can be configured with ResourceServerConfigurerAdapter
Diagram
Managing Access and Refresh Tokens
As mentioned API Gateway is the common entry point for the requests. We can manage the login/logout API in the API Gateway. When the user performs the log in and we can manage the authorization grant type using authentication service and OAuth2TokenEndpointClient from org.springframework.security.oauth2.common.OAuth2AccessToken using OAuth2AccessToken sendPasswordGrant(String username, String password); and OAuth2AccessToken sendRefreshGrant(String refreshTokenValue); methods.
The authentication service will provide the OAuth2AccessToken based on the configurations and login users. Inside OAuth2AccessToken you will get access_token, refresh_token, OAuth2, expires_in, scope.
At the time of authentication, two JWTs will be created - access token and refresh token. Refresh token will have longer validity. Both the tokens will be written in cookies so that they are sent in every subsequent request.
On every REST API call, the tokens will be retrieved from the HTTP header. If the access token is not expired, check the privileges of the user and allow access accordingly. If the access token is expired but the refresh token is valid, recreate new access token and refresh token with new expiry dates and sent back through Cookies
/**
* Authenticate the user by username and password.
*
* #param request the request coming from the client.
* #param response the response going back to the server.
* #param loginVM the params holding the username, password and rememberMe.
* #return the {#link OAuth2AccessToken} as a {#link ResponseEntity}. Will return {#code OK (200)}, if successful.
* If the UAA cannot authenticate the user, the status code returned by UAA will be returned.
*/
public ResponseEntity<OAuth2AccessToken> authenticate(HttpServletRequest request, HttpServletResponse response,
LoginVM loginVM) {
try {
String username = loginVM.getUsername();
String password = loginVM.getPassword();
boolean rememberMe = loginVM.isRememberMe();
OAuth2AccessToken accessToken = authorizationClient.sendPasswordGrant(username, password);
OAuth2Cookies cookies = new OAuth2Cookies();
cookieHelper.createCookies(request, accessToken, rememberMe, cookies);
cookies.addCookiesTo(response);
if (log.isDebugEnabled()) {
log.debug("successfully authenticated user {}", username);
}
return ResponseEntity.ok(accessToken);
} catch (HttpStatusCodeException in4xx) {
throw new UAAException(ErrorConstants.BAD_CREDENTIALS);
}
catch (ResourceAccessException in5xx) {
throw new UAAException(ErrorConstants.UAA_APPLICATION_IS_NOT_RESPONDING);
}
}
/**
* Try to refresh the access token using the refresh token provided as cookie.
* Note that browsers typically send multiple requests in parallel which means the access token
* will be expired on multiple threads. We don't want to send multiple requests to UAA though,
* so we need to cache results for a certain duration and synchronize threads to avoid sending
* multiple requests in parallel.
*
* #param request the request potentially holding the refresh token.
* #param response the response setting the new cookies (if refresh was successful).
* #param refreshCookie the refresh token cookie. Must not be null.
* #return the new servlet request containing the updated cookies for relaying downstream.
*/
public HttpServletRequest refreshToken(HttpServletRequest request, HttpServletResponse response, Cookie
refreshCookie) {
//check if non-remember-me session has expired
if (cookieHelper.isSessionExpired(refreshCookie)) {
log.info("session has expired due to inactivity");
logout(request, response); //logout to clear cookies in browser
return stripTokens(request); //don't include cookies downstream
}
OAuth2Cookies cookies = getCachedCookies(refreshCookie.getValue());
synchronized (cookies) {
//check if we have a result from another thread already
if (cookies.getAccessTokenCookie() == null) { //no, we are first!
//send a refresh_token grant to UAA, getting new tokens
String refreshCookieValue = OAuth2CookieHelper.getRefreshTokenValue(refreshCookie);
OAuth2AccessToken accessToken = authorizationClient.sendRefreshGrant(refreshCookieValue);
boolean rememberMe = OAuth2CookieHelper.isRememberMe(refreshCookie);
cookieHelper.createCookies(request, accessToken, rememberMe, cookies);
//add cookies to response to update browser
cookies.addCookiesTo(response);
} else {
log.debug("reusing cached refresh_token grant");
}
//replace cookies in original request with new ones
CookieCollection requestCookies = new CookieCollection(request.getCookies());
requestCookies.add(cookies.getAccessTokenCookie());
requestCookies.add(cookies.getRefreshTokenCookie());
return new CookiesHttpServletRequestWrapper(request, requestCookies.toArray());
}
}
Secured Communication between Microservices
We can communicate between the service using the FeignClient and can secure the communication by customizing the configurations. See Class<?>[] configuration() default OAuth2UserClientFeignConfiguration.class;
Here we have enhanced default #FeignClient with AuthorizedUserFeignClient interface which consists of custom configuration as OAuth2UserClientFeignConfiguration which consists of #Bean for UserFeignClientInterceptor which manage the autehication using the headers
AuthorizedUserFeignClient.java
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#Documented
#FeignClient
public #interface AuthorizedUserFeignClient {
#AliasFor(annotation = FeignClient.class, attribute = "name")
String name() default "";
/**
* A custom {#code #Configuration} for the feign client.
*
* Can contain override {#code #Bean} definition for the pieces that make up the client, for instance {#link
* feign.codec.Decoder}, {#link feign.codec.Encoder}, {#link feign.Contract}.
*
* #see FeignClientsConfiguration for the defaults.
*/
#AliasFor(annotation = FeignClient.class, attribute = "configuration")
Class<?>[] configuration() default OAuth2UserClientFeignConfiguration.class;
/**
* An absolute URL or resolvable hostname (the protocol is optional).
*/
String url() default "";
/**
* Whether 404s should be decoded instead of throwing FeignExceptions.
*/
boolean decode404() default false;
/**
* Fallback class for the specified Feign client interface. The fallback class must implement the interface
* annotated by this annotation and be a valid Spring bean.
*/
Class<?> fallback() default void.class;
/**
* Path prefix to be used by all method-level mappings. Can be used with or without {#code #RibbonClient}.
*/
String path() default "";
}
UserFeignClientInterceptor.java
public class UserFeignClientInterceptor implements RequestInterceptor{
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_TOKEN_TYPE = "Bearer";
#Override
public void apply(RequestTemplate template) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if (authentication != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, details.getTokenValue()));
}
}
}
Might be helpful
Architecture Overview
Managing the authentication service

How to make Basic Authentication work as an alternative for keycloak in a Angular JS/Spring boot app

We have migrated from Basic Authentication to Keycloak method in our project in the production environment. However we would like continue using Basic Authentication, for local development, standalone and demo instalations, which could be triggered by a profile or something like this.
In this project we have REST APIs developed with Java/Spring boot and an AngularJS application which consumes these APIs. We are using Keycloak to protect both AngularJS app and the APIs.
The problem is how to make Spring Security and Keycloak to work "together" in the same application with different profiles. The solution I found so far, was to configure both Spring Security and Keycloak, and made a workaround with properties files, as described below:
application-keycloak.properties
#Unactivate Basic Authentication
security.ignored=/**
application-local-auth.properties
#Unactivate Keycloak
spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration
When I wanto to use keycloak, I have to ignore security in order to not have problems and when I want to use basic authentication I have to exclude Keycloak configuration in order to also prevent conflicts.
This is my Security Configuration class:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and()
.authorizeRequests()
.antMatchers("/","/scripts/**","/keycloak/isActive","/keycloak/config","/bower_components/**","/views/**","/fonts/**",
"/views/inventory/dialogs/**", "/services/**","/resources/**","/styles/**", "/info")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}
And this is my Keycloak Spring Boot configuration:
# Keycloak
keycloak.realm=local
keycloak.realmKey=MIIBIjANBgkqhkiG9wsIIBCgKCAQEAuJYmaWvF3YhifflJhspXOs8RJn74w+eVD8PtpVbu2cYG9OIa49P8SwqVn/kyJQr7kT3OlCq3XMZWBHe+JSzSz7KttKkhfFSfzISdKDKlkPena2H/i3FKlRZIldbeeuQNYdD6nMpzU6QWLwGF1cUAo1M11f2p99QI1FOhVPJSErWsjDsKpWqG+rMMjT1eos0QCNP7krx/yfMdlUyaJCYiDvpOAoec3OWXvDJovEajBNAZMWVXgJF90wAVPRF6szraA2m7K2gG9ozaCNWB0v4Sy6czekbKjqEBPJo45uEmGHd92V//uf/WQG4HSiuv8CTV+b6TQxKtZCpQpqp2DyCLewIDAQAB
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.ssl-required=none
keycloak.resource=App-backend
keycloak.bearer-only=true
keycloak.credentials.secret=a714aede-5af9-4560-8c9d-d655c831772f
keycloak.securityConstraints[0].securityCollections[0].name=Secured API
keycloak.securityConstraints[0].securityCollections[0].authRoles[0]=ROLE_USER
keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/api/*
It is working, however I think it is not an elegant solution. I have tried to implement this using the Keycloak property enable-basic-auth, but I could not understand how it works but it seems that it is just to protect Rest APIs, it does not allow the browser to create a session and use it for all the other requests.
Have someone ever had to implement something like this and can give me some better idea?
I managed to solve this. However, how beautiful my solution is is up for debate.
My use case is that I need to secure most of my endpoints using Keycloak but some (for batch processing) should just use Basic Auth. Configuring both has the downside that Keycloak tries to validate the Authorization Header even if it is Basic Auth so I needed to do three things.
Deactivate all automatic security for my batch route.
Write a custom request filter which secures the batch route.
Manipulate the servlet request object such that the zealous keycloak filter doesn't trip on it.
My security configuration.
#EnableWebSecurity
#EnableResourceServer
public class SecurityConfiguration extends KeycloakWebSecurityConfigureAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
// usual configuration ...
.antMatchers("/api/v1/batch/**").permitAll() // decouple security for this route
.anyRequest().denyAll();
}
}
My custom request filter (needs to run before the spring security filter, thus the ordering annotation):
#Component
#Slf4j
#Order(Ordered.HIGHEST_PRECEDENCE + 2)
public class BasicAuthRequestFilter extends OncePerRequestFilter {
#Value("${batch.user}")
private String user;
#Value("${batch.password}")
private String password;
#Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if (isBatchRequest(request)) {
SimpleHttpFacade facade = new SimpleHttpFacade(request, response);
if (AuthOutcome.AUTHENTICATED.equals(auth(facade))) {
filterChain.doFilter(new AuthentifiedHttpServletRequest(request), response);
}
log.debug("Basic auth failed");
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate with basic authentication");
return;
}
filterChain.doFilter(request, response);
}
private boolean isBatchRequest(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/v1/batch/");
}
private AuthOutcome auth(HttpFacade exchange) {
return extractToken(exchange.getRequest().getHeaders(HttpHeaders.AUTHORIZATION))
.map(token -> extractUserPw(token)
.filter(userpw -> verify(userpw.getFirst(), userpw.getSecond()))
.map(userpw -> AuthOutcome.AUTHENTICATED)
.orElse(AuthOutcome.FAILED))
.orElse(AuthOutcome.NOT_ATTEMPTED);
}
private Optional<String> extractToken(List<String> authHeaders) {
return authHeaders == null ? Optional.empty() : authHeaders.stream().map(authHeader -> authHeader.trim().split("\\s+"))
.filter(split -> split.length == 2)
.filter(split -> split[0].equalsIgnoreCase("Basic"))
.map(split -> split[1])
.findFirst();
}
private Optional<Pair<String, String>> extractUserPw(String token) {
try {
String userpw = new String(Base64.decode(token));
String[] parts = userpw.split(":");
if (parts.length == 2) {
return Optional.of(Pair.of(parts[0], parts[1]));
}
} catch (Exception e) {
log.debug("Basic Auth Token formatting error", e);
}
return Optional.empty();
}
private boolean verify(String user, String password) {
return (this.user.equals(user) && this.password.equals(password));
}
}
And finally the wrapped ServletRequest (as you cannot remove Headers from the request):
public class AuthentifiedHttpServletRequest extends HttpServletRequestWrapper {
public AuthentifiedHttpServletRequest(HttpServletRequest request) {
super(request);
}
#Override
public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
return true;
}
#Override
public String getAuthType() {
return "Basic";
}
#Override
public String getHeader(String name) {
if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
return super.getHeader(name);
}
return null;
}
#Override
public Enumeration<String> getHeaders(String name) {
if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
return super.getHeaders(name);
}
return Collections.enumeration(Collections.emptyList());
}
#Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(EnumerationUtils.toList(super.getHeaderNames())
.stream()
.filter(s -> !HttpHeaders.AUTHORIZATION.equalsIgnoreCase(s))
.collect(Collectors.toList()));
}
#Override
public int getIntHeader(String name) {
if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
return super.getIntHeader(name);
}
return -1;
}
}
Not quite sure whether this is still relevant or not, but maybe someone will find it helpful.
By default, Keycloak is overwriting plenty of configurations. It's intercepting all Auth request (OAuth2, BasicAuth etc.)
Fortunately, with Keycloak, it's possible to enable authentication both with OAuth2 and BasicAuth in parallel, which I assume is what you want to enable in your dev/localhost environments.
In order to do that, you first need to add the following property to your
application-local-auth.properties:
keycloak.enable-basic-auth=true
This property will enable Basic Auth in your dev environment. However, you also need to enable Basic Auth at your client in Keycloak.
You can accomplish that by connecting to the Keycloak Admin Console on your local Keycloak server and enabling the Direct Access Grant for your client:
Enabling Basic Auth in Keycloak
After that you can authenticate both with Bearer Token and Basic Auth.

Spring security : handling SSO and DB users

I have two different types of users.
SSO users
DB users.
SSO users would have already been authenticated by different system and DB users should be authenticated by our system. Can i configure Spring security to handle this scenario where by i can say prompt login page for some users and don't prompt for some.
Lets say for SSO users i can get there users ID in request headers while DB when they access the application there is no user id present in request header .How can i handle this scenario ?
Can i override authenticate method of DaoAuthenticationProvider by extending it and then page on some parameter decide to authenticate user or is there any other means ? can i add any information to Authentication class
This is What i have tried to Far
Security Config java
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
DataSource dataSource;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder builder) throws Exception {
builder.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder())
.usersByUsernameQuery("select username,password, enabled from users where username=?")
.authoritiesByUsernameQuery("select username, role from user_roles where username=?");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic()
.and().addFilterBefore(new UserTypeFilter(), BasicAuthenticationFilter.class);
}
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
/*
* #Bean public MethodSecurityInterceptor methodSecurityService() { return
* new MethodSecurityInterceptor(); }
*/
#Bean(name="myAuthenticationManager")
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public ExceptionTranslationFilter exceptionTranslationFilter() {
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
new Http403ForbiddenEntryPoint());
AccessDeniedHandlerImpl accessDeniedHandlerImpl = new AccessDeniedHandlerImpl();
accessDeniedHandlerImpl.setErrorPage("/exception");
exceptionTranslationFilter
.setAccessDeniedHandler(accessDeniedHandlerImpl);
exceptionTranslationFilter.afterPropertiesSet();
return exceptionTranslationFilter;
}
#Bean
public UserTypeFilter authenticationFilter() throws Exception {
UserTypeFilter authFilter = new UserTypeFilter();
authFilter.setAuthenticationManager(authenticationManager());
return authFilter;
}
}
My Custom AbstractAuthenticationProcessingFilter
public class UserTypeFilter extends AbstractAuthenticationProcessingFilter {
private static final String INTERCEPTOR_PROCESS_URL = "/index";
#Autowired
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
public UserTypeFilter() {
super(INTERCEPTOR_PROCESS_URL);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
System.out.println(" BASIC AUTHENTICATION FILTER");
String userId = request.getHeader("USERID");
if (userId == null) {
System.out.println(" THROWING EXCEPTION FILTER");
throw new PreAuthenticatedCredentialsNotFoundException("USERID param not found");
}
return null;
}
}
My Controller
#Controller
public class MainController {
#RequestMapping(value = { "/index" }, method = RequestMethod.GET)
public ModelAndView index() {
ModelAndView model = new ModelAndView();
model.addObject("message", "This is test page!");
model.setViewName("dummy");
return model;
}
}
The control goes to My Custom filter and then when the exception is thrown but ExceptionTranslationFilter is not getting called
Have i configured httpsecurity correctly
Have i configured My custom filter correctly
Have i configured ExceptionTranslation Filter
Am i missing anything
This is a pretty standard use case for spring security. You will need to provide an Authentication object into the security context before any security interceptor is encountered.
Typically you would have some kind of filter which extracted SSO parameters from the request, authenticated those parameters against the SSO service, and then create an Authentication object and put it into the security context. The type of filter and configuration of the filter will depend on what SSO technology you are using.
There would often also be a filter (usually an ExceptionTranslationFilter) which will send unauthenticated requests to a login page.
There would also be filters to receive the parameters from the login form and store them in the security context.
Putting it all together I would expect one possible workflow to be:
User logs in with SSO parameters
Request comes in prepopulated with credentials
Some filter extracts those credentials, verifies them, creates an Authentication object, places the object in the security context.
The security interceptor finds the Authentication object in the security context, verifies the user is allowed access to the particular function, and passes the request on.
User logs in without SSO parameters (needs login page)
Request comes in with no credentials
The security interceptor finds no Authentication object and throws an exception.
The ExceptionTranslationFilter turns the exception into a redirect to a login page.
User logs in with filled out login form (e.g. for DB login)
Request comes in with a login form as the entity body
Some filter (e.g. UsernamePasswordAuthenticationFilter) extracts the credentials from the login form and defers to an authentication provider (e.g. your DAO authentication provider) to query the database and verify the user. If verified this filter will create an Authentication object and place it in the security context.
The security interceptor finds the Authentication object in the security context, verifies the user is allowed access to the particular function, and passes the request on.

How to implement basic Spring security (session management) for Single Page AngularJS application

I am currently building a single page AngularJS application which communicates via REST to a backend. The structure is as follow:
One Spring MVC WebApp project which contains all AngularJS pages and resources and all REST controllers.
A true backend which has services and repositories for backend communication, an API if you will. The REST calls will talk to these service (the second project is included as a dependency of the first one).
I have been thinking about this a lot but I can't seem to find anything that can help me. Basically I just need some security on this application. I'd like some kind of session management which is extremely simple:
user logs in, session id is created and stored in JS/cookie on
website
when user would reload page/ come back later a check needs to be done to see if the session id is still valid
no calls should reach the controllers if the session id is not valid
This is the general idea of basic session managament, what would be the easiest way to get this implemented in a Spring MVC webapp (no JSP's, just angular and REST controllers).
Thanks in advance!
You have 2 options for the rest API: stateful or stateless.
1st option: HTTP session authentication - the "classical" Spring Security authentication mechanism. If you plan to scale your application on multiple servers, you need to have a load balancer with sticky sessions so that each user stays on the same server (or use Spring Session with Redis).
2nd option: you have the choice of OAuth or token-based authentication.
OAuth2 is a stateless security mechanism, so you might prefer it if you want to scale your application across several machines. Spring Security provides an OAuth2 implementation. The biggest issue with OAuth2 is that requires to have several database tables in order to store its security tokens.
Token-based authentication, like OAuth2, is a stateless security mechanism, so it's another good option if you want to scale on several different servers. This authentication mechanism doesn't exist by default with Spring Security. It is easier to use and implement than OAuth2, as it does not require a persistence mechanism, so it works on all SQL and NoSQL options. This solution uses a custom token, which is a MD5 hash of your user name, the expiration date of the token, your password, and a secret key. This ensures that if someone steals your token, he should not be able to extract your username and password.
I recommend you to look into JHipster. It will generate a web app skeleton for you with REST API using Spring Boot and the front end using AngularJS. When generating the application skeleton it will ask you to choose between the 3 authentication mechanisms that I described above. You can reuse the code that JHipster will generate in your Spring MVC application.
Here is an example of TokenProvider generated by JHipster:
public class TokenProvider {
private final String secretKey;
private final int tokenValidity;
public TokenProvider(String secretKey, int tokenValidity) {
this.secretKey = secretKey;
this.tokenValidity = tokenValidity;
}
public Token createToken(UserDetails userDetails) {
long expires = System.currentTimeMillis() + 1000L * tokenValidity;
String token = userDetails.getUsername() + ":" + expires + ":" + computeSignature(userDetails, expires);
return new Token(token, expires);
}
public String computeSignature(UserDetails userDetails, long expires) {
StringBuilder signatureBuilder = new StringBuilder();
signatureBuilder.append(userDetails.getUsername()).append(":");
signatureBuilder.append(expires).append(":");
signatureBuilder.append(userDetails.getPassword()).append(":");
signatureBuilder.append(secretKey);
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No MD5 algorithm available!");
}
return new String(Hex.encode(digest.digest(signatureBuilder.toString().getBytes())));
}
public String getUserNameFromToken(String authToken) {
if (null == authToken) {
return null;
}
String[] parts = authToken.split(":");
return parts[0];
}
public boolean validateToken(String authToken, UserDetails userDetails) {
String[] parts = authToken.split(":");
long expires = Long.parseLong(parts[1]);
String signature = parts[2];
String signatureToMatch = computeSignature(userDetails, expires);
return expires >= System.currentTimeMillis() && signature.equals(signatureToMatch);
}
}
SecurityConfiguration:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
#Inject
private UserDetailsService userDetailsService;
#Inject
private TokenProvider tokenProvider;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/scripts/**/*.{js,html}");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/protected/**").authenticated()
.and()
.apply(securityConfigurerAdapter());
}
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
}
private XAuthTokenConfigurer securityConfigurerAdapter() {
return new XAuthTokenConfigurer(userDetailsService, tokenProvider);
}
/**
* This allows SpEL support in Spring Data JPA #Query definitions.
*
* See https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
*/
#Bean
EvaluationContextExtension securityExtension() {
return new EvaluationContextExtensionSupport() {
#Override
public String getExtensionId() {
return "security";
}
#Override
public SecurityExpressionRoot getRootObject() {
return new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};
}
};
}
}
And the respective AngularJS configuration:
'use strict';
angular.module('jhipsterApp')
.factory('AuthServerProvider', function loginService($http, localStorageService, Base64) {
return {
login: function(credentials) {
var data = "username=" + credentials.username + "&password="
+ credentials.password;
return $http.post('api/authenticate', data, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
}
}).success(function (response) {
localStorageService.set('token', response);
return response;
});
},
logout: function() {
//Stateless API : No server logout
localStorageService.clearAll();
},
getToken: function () {
return localStorageService.get('token');
},
hasValidToken: function () {
var token = this.getToken();
return token && token.expires && token.expires > new Date().getTime();
}
};
});
authInterceptor:
.factory('authInterceptor', function ($rootScope, $q, $location, localStorageService) {
return {
// Add authorization token to headers
request: function (config) {
config.headers = config.headers || {};
var token = localStorageService.get('token');
if (token && token.expires && token.expires > new Date().getTime()) {
config.headers['x-auth-token'] = token.token;
}
return config;
}
};
})
Add authInterceptor to $httpProvider:
.config(function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
})
Hope this is helpful!
This video from SpringDeveloper channel may be useful too: Great single page apps need great backends. It talks about some best practices (including session management) and demos working code examples.
Take a look from what has been done in JHipster https://jhipster.github.io/. You can even use it.
Jhipster is a spring boot + angular/angularjs generator. I often use it to inspire me and learn best practice.

Categories

Resources