Spring Security authenticate user via post - java

I have a react app running on a separate port (localhost:3000) that i want to use to authenticate users with, currently i have a proxy setup to my Spring backend (localhost:8080).
Can I somehow manually authenticate instead of http.httpBasic() by sending a POST request to my backend and getting back a session cookie then include the cookie with every request? It would simplify the auth process on iOS side aswell (using this process i could only store the session cookie value in keychain and pass it with every request made to my api)
How would I disable csrf for non-browser requests?
Is there a better approach to this? Diffrent paths for browser and mobile auth?
{
"username": "user",
"password": "12345678"
}
Handle the request in spring controller
#PostMapping(path = "/web")
public String authenticateUser() {
//Receive the auth data here... and perform auth
//Send back session cookie
return "Success?";
}
My WebSecurityConfigurerAdapter.java
#Configuration
#EnableWebSecurity
public class WebsecurityConfig extends WebSecurityConfigurerAdapter {
private final DetailService detailService;
public WebsecurityConfig(DetailService detailService) {
this.detailService = detailService;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/new").permitAll()
.antMatchers(HttpMethod.POST,"/api/v1/authenticate/web").permitAll()
.anyRequest().authenticated();
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST").allowedOrigins("http://localhost:8080");
}
};
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(14);
}
}

You can create an endpoint that takes user's credentials in a request body, perform authentication and then set tokens, and other required parameters in HttpOnly cookies.
After setting cookies, subsequent requests can read access/refresh token from cookies and add it in requests, you can then use custom CheckTokenEndpoint to verify tokens.
In the following example TokenParametersDto is a POJO that has username and password properties.
For issuing token (by verifying credentials) you can delegate call to TokenEndpoint#postAccessToken(....) or use its logic to your own method.
#PostMapping(path = "/oauth/http/token", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> issueToken(#RequestBody final #Valid #NotNull TokenParametersDto tokenParametersDto,
final HttpServletResponse response) {
final OAuth2AccessToken token = tokenService.issueToken(tokenParametersDto);
storeTokenInCookie(token, response);
return new ResponseEntity<>(HttpStatus.OK);
}
private void storeTokenInCookie(final OAuth2AccessToken token, final HttpServletResponse response) {
final Cookie accessToken = new Cookie("access_token", token.getValue());
accessToken.setHttpOnly(true);
accessToken.setSecure(sslEnabled);
accessToken.setPath("/");
accessToken.setMaxAge(cookieExpiration);
final Cookie tokenType = new Cookie("token_type", token.getTokenType());
tokenType.setHttpOnly(true);
tokenType.setSecure(sslEnabled);
tokenType.setPath("/");
tokenType.setMaxAge(cookieExpiration);
// Set Refresh Token and other required cookies.
response.addCookie(accessToken);
response.addCookie(tokenType);
}
Check this answer for disabling CSRF for a specific URL section.

Related

Set custom response headers on Spring Security authentication success URL

After a successful SAML 2.0 login on a Spring Security application acting as service provider, I can redirect the user to a static success URL using:
#Configuration
#EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.saml2Login(saml2Login -> saml2Login.defaultSuccessUrl(mySuccessUrl);
}
}
How can I (dynamically) set these other pieces of information?
URL parameters
Response headers
Cookies
So far, I found a (rather cumbersome) solution: set an AuthenticationSuccessHandler with a custom RedirectStrategy.
SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
handler.setDefaultTargetUrl(relayState);
handler.setRedirectStrategy(new DefaultRedirectStrategy() {
#Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
// do stuff ...
// set response headers ...
// set cookies ...
// add a query string to the redirect URL ...
super.sendRedirect(request, response, url);
}
});
http.saml2Login(saml2Login -> saml2Login.successHandler(handler));
Are there better alternatives?
I am using Spring Boot 2.3 and Spring Security 5.2.

Spring boot - google oauth2, store refresh token in database

i'm trying to get the refresh token from the user logged in my system, and store it in a database. So a different system in my ecosystem can access the stored refresh token, generate an access token with it and use the google calendar api with the user credentials.
So far i have managed to do the login with
#Configuration
public class AppConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientRegistrationRepository clientRegistrationRepository;
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
this.clientRegistrationRepository))
.and()
.and()
.rememberMe();
}
}
And
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;
public CustomAuthorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
this.defaultAuthorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest authorizationRequest =
this.defaultAuthorizationRequestResolver.resolve(request);
return authorizationRequest != null ?
customAuthorizationRequest(authorizationRequest) :
null;
}
#Override
public OAuth2AuthorizationRequest resolve(
HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest authorizationRequest =
this.defaultAuthorizationRequestResolver.resolve(
request, clientRegistrationId);
return authorizationRequest != null ?
customAuthorizationRequest(authorizationRequest) :
null;
}
private OAuth2AuthorizationRequest customAuthorizationRequest(
OAuth2AuthorizationRequest authorizationRequest) {
Map<String, Object> additionalParameters = new LinkedHashMap<>(authorizationRequest.getAdditionalParameters());
additionalParameters.put("access_type", "offline");
return OAuth2AuthorizationRequest.from(authorizationRequest)
.additionalParameters(additionalParameters)
.build();
}
}
how and where can i access the refresh token of the logged user?
I answered a similar question here, but it is in kotlin, so I'll add a java version for you.
These are two approaches to get the refresh token (or rather OAuth2AuthorizedClient, from which you can get the refresh token). Which one you use depend on your needs.
Inject and OAuth2AuthorizedClient representing the requesting user into an endpoint method:
#GetMapping("/foo")
void foo(#RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient user) {
OAuth2RefreshToken refreshToken = user.getRefreshToken();
}
Outside the context of a request, you can inject OAuth2AuthorizedClientService into a managed component, and get the needed OAuth2AuthorizedClient instance with the client registration id and principal name:
#Autowired
private OAuth2AuthorizedClientService clientService;
public void foo() {
OAuth2AuthorizedClient user = clientService.loadAuthorizedClient("google", "principal-name");
OAuth2RefreshToken refreshToken = user.getRefreshToken();
}

Updated password not picked up after InMemoryUserDetailsManager updateUser call

Hi I have a Rest WS using WebSecurityConfigurerAdapter to implement HTTP Basic auth.
The password is allowed to be updated and I need to let the WS to pick up updated password without restarting server
Following are the codes:
SecurityConfig
// init a user with credentials admin/password
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
//disable csrf
.csrf().disable()
//authentic all requests
.authorizeRequests().anyRequest().authenticated().and().httpBasic()
//disable session
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
Properties users = new Properties();
users.put("admin", "password,USER,enabled");
return new InMemoryUserDetailsManager(users);
}
}
The controller that will update password
#RestController
public class someController{
#Autowired
public InMemoryUserDetailsManager inMemoryUserDetailsManager;
// update password from password -> pass
#RequestMapping(...)
public updatePass(){
ArrayList<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
grantedAuthoritiesList.add(new SimpleGrantedAuthority("USER"));
this.inMemoryUserDetailsManager.updateUser(new User("admin", "pass", grantedAuthoritiesList));
}
// another way that also doesn’t work
#RequestMapping(...)
public newUpdate(){
ArrayList<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
grantedAuthoritiesList.add(new SimpleGrantedAuthority("USER"));
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("admin", "pass",
grantedAuthoritiesList);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
}
}
After calling updatePass() with credential admin/password for the first time, I can see that the password has been updated to "pass" in debugger
I assume that if I'm to call updatePass() again, I should use admin/pass. However it turned out to be still using the old admin/password.
Sources I referred to when writing this code source1 source2
*I'm using Advance Rest Client to make the calls
When you update the password, you have to set the UserDetails in springSecurityContext object if the user is authenticated.
instead of using SecurityContext, I overwrote function loadUserByUsername of interface UserDetailsService to let spring security always pick up the latest pwd from DB.

Spring Security logout does not work - does not clear security context and authenticated user still exists

I know, there are many articles about this topic, but I have a problem and I can't find any solution.
I have a classic spring security java config:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuctionAuthenticationProvider auctionAuthenticationProvider;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(auctionAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequest = http.authorizeRequests();
configureAdminPanelAccess(authorizeRequest);
configureFrontApplicationAccess(authorizeRequest);
configureCommonAccess(authorizeRequest);
http.csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
http.logout()
.clearAuthentication(true)
.invalidateHttpSession(true);
}
...
}
Also, I have two controller methods, where I login/logout from my web application by AJAX.
When I would like to logout, I first call this method, which I expect to clear user sessions and clear everything from the security context.
#Override
#RequestMapping(value = "/logout", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Boolean> logout(final HttpServletRequest request, final HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return new ResponseEntity<>(Boolean.TRUE, HttpStatus.OK);
}
After this I reload my client web application and each time, when it is reloaded, I check whether the user is authenticated by calling the following controller method:
#Override
#RequestMapping(value = "/user", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<UserDetails> user() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return new ResponseEntity<>((UserDetails) principal, HttpStatus.OK);
}
return null;
}
And here I aways receive the last authenticated user. It seems that in the previous logout method, Spring logout doesn't work.
Keep in mind that I tried to logout with the following code, without any success:
#Override
#RequestMapping(value = "/logout", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Boolean> logout(final HttpServletRequest request) {
try {
request.logout();
return new ResponseEntity<>(Boolean.TRUE, HttpStatus.OK);
} catch (ServletException ex) {
if (LOG.isDebugEnabled()) {
LOG.debug("There is a problem with the logout of the user", ex);
}
}
Are you have any idea what I miss in my config and the logout process?
From your question, I see you are trying to create your own logout and you also trying to use the default Spring logout. I advise that you should choose one method and not mix them both. There are two I recommend to logout from Spring:
First: Default spring security logout
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/logout.done").deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
From the example above, you should only need to call the /logout URL whenever you want to logout the user. No need to create any #Controller to handle that logout instead Spring will help to log the user out. You also can add other thing you want to invalidate here.
Second: Programmatically logout
#RequestMapping(value = {"/logout"}, method = RequestMethod.POST)
public String logoutDo(HttpServletRequest request,HttpServletResponse response){
HttpSession session= request.getSession(false);
SecurityContextHolder.clearContext();
session= request.getSession(false);
if(session != null) {
session.invalidate();
}
for(Cookie cookie : request.getCookies()) {
cookie.setMaxAge(0);
}
return "logout";
}
If you are using this logout approach, you don't need to include the first method in ht eSpring security config. By using this method, you can add an extra action to perform before and after logout done. BTW, to use this logout, just call the /logout url and the user will be logged out manually. This method will invalidate the session, clear Spring security context and cookies.
In addition for the second method, if you are using RequestMethod.POST, you need to include the CSRF key on the POST request. The alternative way is to create a form with a hidden input CSRF key. This is some example of auto generated logout link with jQuery :
$("#Logout").click(function(){
$form=$("<form>").attr({"action":"${pageContext.request.contextPath}"+"/logout","method":"post"})
.append($("<input>").attr({"type":"hidden","name":"${_csrf.parameterName}","value":"${_csrf.token}"}))
$("#Logout").append($form);
$form.submit();
});
You just need to create a hyperlink <a id="Logout">Logout</a> to use it.
If you are using RequestMethod.GET,just include a CSRF key as a parameter in you link like this:
Logout
Thats all, hope it helps.
Just a heads up, there is Clear Site Data HTTP header as shown below
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
I also helped add support for Clear-Site-Data header into Spring-Security 5.2 project. For more details around the implementation, see the PR.
Here is a sample of how it is going to work
#EnableWebSecurity
static class HttpLogoutConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.addLogoutHandler(new HeaderWriterLogoutHandler(
new ClearSiteDataHeaderWriter(SOURCE)));
}
}
Where SOURCE is a vararg of one or more of the following
"*" Clear everything
One or more of "cache", "cookies", "storage", "executionContexts"
For more details see the sample test in the LogoutConfigurerClearSiteDataTests.java.
This will help, i think clearAuthentication(true) is enough:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
....
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.httpBasic()
.and()
.logout().clearAuthentication(true)
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
I solved my problem similarly by adding the following parameter to the application.properties file
spring.cache.type=NONE
Just change logout URL from "/logout" to "war or snapshot name/logout"

AngularJS and Spring MVC security

I have a front end written in AngularJS, and a Spring MVC backend. The idea I had was to only secure the REST API services and use an interceptor in AngularJS to redirect the user to the login page when an unauthorized service call is made. The problem I'm facing now is that, while a service is called, the page is briefly displayed before the user is redirected. Is there anything I can do about that? Or is this approach fundamentally flawed?
This is the interceptor:
$httpProvider.interceptors.push(function ($q, $location) {
return {
'responseError': function(rejection) {
var status = rejection.status;
if (status == 401 || status == 403) {
$location.path( "/login" );
} else {
}
return $q.reject(rejection);
}
};});
My security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.authorizeRequests()
.anyRequest().authenticated();
}
#Bean(name="myAuthenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
The login controller:
#RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
#ResponseBody
public String login(#RequestBody User user) {
JSONObject result = new JSONObject();
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
try {
Authentication auth = authenticationManager.authenticate(token);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(auth);
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
result.put("isauthenticated", true);
} catch (BadCredentialsException e) {
result.put("isauthenticated", false);
}
return result.toString();
}
I think this approach is OK, but you may have to live with the page flash, which in turn means you will have to handle it gracefully.
I guess the page flush happens roughly as follows:
A navigation takes place, rendering a template and activating a controller for the new route
The controller calls a service; this is asynchronous, so the page without any data is displayed
The service returns 401/403, it is intercepted and the new navigation to the login page occurs
You may want to try:
Collecting all data required by the page in the resolve configuration of the route (supported both by ngRoute and angular-ui-router), so that the navigation will not complete before all data is fetched.
Handle it from within the page: while the service call is still pending, display a spinner/message whatever to let the user know that some background activity is going on.
When the interceptor catches a 401/403, have it open a modal popup, explaining the situation and offering the user to login or navigate to the login page as a single option. Combine this with the spinner/message.

Categories