Why does Spring Security + Angular login have different sessions on AuthenticationSuccessHandler and RestController? - spring

I have a Spring Security configuration and a login page in Angular. After the successfull login, my SimpleAuthenticationSuccessHandler redirects me to a controller that gets the user from session and returns it. When I call the login from Postman, everything goes as expected, but when I call it from Chrome it doesn't work, because the session on SimpleAuthenticationSuccessHandler is different than the session received on controller.
This is the configuration class for the Spring Security:
#Configuration
#EnableWebSecurity
#ComponentScan("backend.configuration")
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableMongoRepositories(basePackages = "backend.repositories")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/user/").authenticated()
.and()
.formLogin()
.usernameParameter("email")
.loginProcessingUrl("/login").
successHandler(authenticationSuccessHandler())
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.and()
.logout();
}
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new SimpleOnSuccessAuthenticationHandler();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
This is the custom authentication success handler:
public class SimpleOnSuccessAuthenticationHandler
implements AuthenticationSuccessHandler {
protected Log logger = LogFactory.getLog(this.getClass());
#Autowired
UserRepository userRepository;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
protected void handle(HttpServletRequest request,
HttpServletResponse response, Authentication
authentication)
throws IOException {
HttpSession session = request.getSession();
ObjectId objectId = ((MongoUserDetails)
authentication.getPrincipal()).getId();
User loggedUser = userRepository.findById(objectId).orElse(null);
UserDto loggedUserDto = UserConverter.convertUserToDto(loggedUser);
session.setAttribute("loggedUser", loggedUserDto);
if (response.isCommitted()) {
logger.debug(
"Response has already been committed. Unable to redirect to "
+ "/loginSuccess");
return;
}
redirectStrategy.sendRedirect(request, response, "/loginSuccess");
}
protected void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
This is the controller that returns the user:
#CrossOrigin
#RestController
public class LoginController {
#Autowired
UserService userService;
#RequestMapping(value = "/loginSuccess", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public ResponseEntity<UserDto> login(HttpServletRequest request) {
UserDto loggedUser= (UserDto)
request.getSession().getAttribute("loggedUser");
System.out.println(request.getSession().getId());
System.out.println(request.getSession().getCreationTime());
return new ResponseEntity<>((UserDto)
request.getSession().getAttribute("loggedUser"), HttpStatus.OK);
}
}
The angular auth.service.ts:
#Injectable({providedIn: 'root'})
export class AuthService {
apiURL = environment.apiUrl;
constructor(private http: HttpClient) {}
login(username: string, password: string) {
let body = new URLSearchParams();
body.set('email', username);
body.set('password', password);
let options = {headers: new HttpHeaders().set('Content-Type',
'application/x-www-form-urlencoded')
};
return this.http.post(this.apiURL + 'login', body.toString(), options);
}
logout() {localStorage.removeItem('currentUser');}
}
And the login.component.ts is :
#Component({selector: 'app-login',templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
user = {} as any;
returnUrl: string;
form: FormGroup;
formSubmitAttempt: boolean;
errorMessage: string = '';
welcomeMessage: string = 'Welcome to CS_DemandResponse Application';
url = '/add_user';
token: string;
constructor(
private fb: FormBuilder,
private authService: AuthService,
private route: ActivatedRoute,
private router: Router
) {
}
ngOnInit() {
this.authService.logout();
this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/';
this.form = this.fb.group({
email: [AppConstants.EMPTY_STRING, Validators.email],
password: [AppConstants.EMPTY_STRING, Validators.required]
});
}
isFieldInvalid(field: string) {
return (
(!this.form.get(field).valid && this.form.get(field).touched) ||
(this.form.get(field).untouched && this.formSubmitAttempt)
);
}
login() {
if (this.form.valid) {
this.authService.login(this.user.email, this.user.password)
.subscribe((currentUser) => {
this.user=currentUser;
if (this.user != null) {
localStorage.setItem('userId', (<User>this.user).id.toString());
if (this.user.autorities.get(0) === 'ROLE_ADMIN' ) {
this.router.navigate(['/admin']);
}
if (this.user.autorities.get(0) === 'ROLE_USER') {
// this.route.params.subscribe((params) => {
// localStorage.setItem('userId', params.id);
// });
this.router.navigate(['/today']);
}
} else {
this.errorMessage = ('Invalid email or password');
this.welcomeMessage = '';
}
});
this.formSubmitAttempt = true;
}
}
}
The /loginSuccess controller returns null so the login.component.ts receives a null on the subscribe.

I assume this is because Spring "exchanges" your session on successfull authentication if you had one, to prevent certain attacks.
Someone could "steal" your session-cookie while unauthenticated and then use it -when you logged in - to also access protected resources, using your now authenticated session.
If you never had a session - eg. when executing the login-request via Postman - there never was a point in the session where you where "unsafe" - so Spring does not have to do this.
You can verify this by requesting your login page in postman, copying the sessionId you get and setting it as session-cookie in your login request. If i am correct you will then be assigned a new session.

Related

Flutter post request not returning with Spring boot server login

I'm writing a Flutter web project with a Spring boot backend and am really battling with getting the authentication stuff to work.
In flutter web I have a "sign_in" method which receives an email and password and passes it to a repository method which sends a post request to the server. See code below. Currently it appears as if the post never returns as the "done with post" line never prints.
Future<String> signIn(String email, String password) async {
authenticationRepository.setStatus(AuthenticationStatus.unknown());
print('signIn user: email: $email pw: $password');
User user = User('null', email, password: password);
//print('user: $user');
var url;
if (ServerRepository.SERVER_USE_HTTPS) {
url = new Uri.https(ServerRepository.SERVER_ADDRESS,
ServerRepository.SERVER_AUTH_LOGIN_ENDPOINT);
} else {
url = new Uri.http(ServerRepository.SERVER_ADDRESS,
ServerRepository.SERVER_AUTH_LOGIN_ENDPOINT);
}
// print('url: $url');
var json = user.toUserRegisterEntity().toJson();
print('Sending request: $json');
// var response = await http.post(url, body: json);
var response = await ServerRepository.performPostRequest(url, jsonBody: json, printOutput: true, omitHeaders: true );
print('Response status: ${response.statusCode}');
print('Response body b4 decoding: ${response.body}');
Map<String, dynamic> responseBody = jsonDecode(response.body);
print('Response body parsed: $responseBody');
if (response.statusCode != 201) {
authenticationRepository
.setStatus(AuthenticationStatus.unauthenticated());
throw FailedRequestError('${responseBody['message']}');
}
User user2 = User(
responseBody['data']['_id'], responseBody['data']['email'],
accessToken: responseBody['accessToken'],
refreshToken: responseBody['refreshToken']);
print('user2 $user2');
authenticationRepository
.setStatus(AuthenticationStatus.authenticated(user2));
return responseBody['data']['_id']; // return the id of the response
}
static Future<Response> performPostRequest(Uri url, {String? accessToken, var jsonBody, bool printOutput = false, bool omitHeaders=false} ) async {
var body = json.encode(jsonBody ?? '');
if(printOutput){
print('Posting to url: $url');
print('Request Body: $body');
}
Map<String, String> userHeader = {
HttpHeaders.authorizationHeader: 'Bearer ${accessToken ?? 'accessToken'}',
"Content-type": "application/json",
};
if(omitHeaders){
userHeader = { };
}
print('performing post: ');
var response = await http.post(
url,
body: body,
headers: userHeader,
);
print('done with post?!');
if(printOutput){
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
Map<String, dynamic> responseBody = jsonDecode(response.body);
print('Response body parsed: $responseBody');
}
return response;
}
My console output is as follows when attempting the request:
signIn user: email: XXXXXX#gmail.com pw: XXxxXXx500!
Sending request: {email: XXXXXX#gmail.com, password: XXxxXXx500!}
Posting to url: http://localhost:8080/auth/login
Request Body: {"email":"XXXXXX#gmail.com","password":"XXxxXXx500!"}
performing post:
So it seems like the response is never sent by the server.
On my server, using Spring boot security the setup is as follows (I based it from this tutorial). Securityconfig:
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JWTUtils jwtTokenUtil;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(jwtTokenUtil, authenticationManagerBean());
customAuthenticationFilter.setFilterProcessesUrl("/auth/login");
http.csrf().disable();
//http.cors(); //tried but still no repsonse
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers( "/auth/**").permitAll(); // no restrictions on this end point
http.authorizeRequests().antMatchers(POST, "/users").permitAll();
http.authorizeRequests().antMatchers(GET, "/users/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(POST, "/users/role/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().anyRequest().authenticated();
http.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
And the filter handling the "/auth/login" end point:
#Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JWTUtils jwtTokenUtil;
private final AuthenticationManager authenticationManager;
#Autowired
public CustomAuthenticationFilter(JWTUtils jwtTokenUtil, AuthenticationManager authenticationManager) {
this.jwtTokenUtil = jwtTokenUtil;
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("attemptAuthentication");
log.info("type "+request.getHeader("Content-Type"));
try {
//Wrap the request
MutableHttpServletRequest wrapper = new MutableHttpServletRequest(request);
//Get the input stream from the wrapper and convert it into byte array
byte[] body;
body = StreamUtils.copyToByteArray(wrapper.getInputStream());
Map<String, String> jsonRequest = new ObjectMapper().readValue(body, Map.class);
log.info("jsonRequest "+jsonRequest);
String email = jsonRequest.get("email");
String password = jsonRequest.get("password");
log.info("jsonRequest username is "+email);
log.info("jsonRequest password is "+password);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
return authenticationManager.authenticate(authenticationToken);
} catch (IOException e) {
e.printStackTrace();
}
//if data is not passed as json, but rather form Data - then this should allow it to work as well
String email = request.getParameter("email");
String password = request.getParameter("password");
log.info("old username is "+email);
log.info("old password is "+password);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
return authenticationManager.authenticate(authenticationToken);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("successfulAuthentication");
User user = (User) authResult.getPrincipal();
String[] tokens = jwtTokenUtil.generateJWTTokens(user.getUsername()
,user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())
, request.getRequestURL().toString() );
String access_token = tokens[0];
String refresh_token = tokens[1];
log.info("tokens generated");
Map<String, String> tokensMap = new HashMap<>();
tokensMap.put("access_token", access_token);
tokensMap.put("refresh_token", refresh_token);
response.setContentType(APPLICATION_JSON_VALUE);
log.info("writing result");
response.setStatus(HttpServletResponse.SC_OK);
new ObjectMapper().writeValue(response.getWriter(), tokensMap);
}
}
When I try the "auth/login" endpoint using postman, I get the correct response with the jwt tokens. See below:
I'm really stuck and have no idea how to fix it. I've tried setting cors on, changing the content-type (which helped making the server see the POST request instead of an OPTIONS request). Any help/explanation would be greatly appreciated.
After lots of trial and error I stumbled across this answer on a JavaScript/ajax question.
It boils down to edge/chrome not liking the use of localhost in a domain. so, if you're using a Spring Boot server, add the following bean to your application class (remember to update the port number):
#Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:56222"));
corsConfiguration.setAllowedHeaders(Arrays.asList("Origin","Access-Control-Allow-Origin",
"Content-Type","Accept","Authorization","Origin,Accept","X-Requested-With",
"Access-Control-Request-Method","Access-Control-Request-Headers"));
corsConfiguration.setExposedHeaders(Arrays.asList("Origin","Content-Type","Accept","Authorization",
"Access-Control-Allow-Origin","Access-Control-Allow-Origin","Access-Control-Allow-Credentials"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET","PUT","POST","DELETE","OPTIONS"));
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}

Cannot access to Main Page after using spring-security, although login is successful

I want to add security part to the project and I am using spring security for providing backend security. When I added custom login filter that extends AbstractAuthenticationProcessingFilter of spring security, I got an error about cross origin problem. Now I added http.cors(); to the WebSecurityConfig and I do not get cross origin errors anymore.
I am sending a request to the backend http://localhost:8081/user/sys-role/verifyTargetUrl. Now, the exact error is Uncaught (in promise) Error: Infinite redirect in navigation guard at eval (vue-router.esm-bundler.js?6c02:2913). So somehow frontend vue-router guards find itself in an infinite loop. I will appreciate any of your help.
UPDATE:
It turned out that I don't get the response code as 200 and that causes the infinite loop in vue-router. My question becomes pure spring-security question because there seems to be no issue with vue-router. I send a post request to http://localhost:8081/user/sys-role/verifyTargetUrl but my request does not enter to the PostMapping in backend. It rather enters CustomAuthenticationEntryPoint shown below and sets the code to 504. But in verifyTargetUrl of backend I set it to 200. Besides, onAuthenticationSuccess of CustomAuthenticationSuccessfulHandler is also called in the backend.
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Message msg=new Message();
msg.setCode(504);
msg.setMsg("authenticate fail");
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
httpServletResponse.getWriter().write(JSON.toJSONString(msg));
}
}
The console of the browser:
config: {url: "http://localhost:8081/user/sys-role/verifyTargetUrl", method: "post", data: "{"userId":1017,"targetUrl":"/Main"}", headers: {…}, transformRequest: Array(1), …} data: {code: 504, msg: "authenticate fail"}
UPDATE 2: More Code
CustomJSONLoginFilter.java
public class CustomJSONLoginFilter extends AbstractAuthenticationProcessingFilter {
private final ISysUserService iUserService;
public CustomJSONLoginFilter(String defaultFilterProcessesUrl, ISysUserService iUserService) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl, HttpMethod.POST.name()));
this.iUserService = iUserService;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
JSONObject requestBody= getRequestBody(httpServletRequest);
String username= requestBody.getString("username");
String password= requestBody.getString("password");
// get user info by username
SysUser sysUser= iUserService.getUserInfoByUsername(username);
//verify password
String encorderType=EncryptionAlgorithm.ENCODER_TYPE.get(1);
PasswordEncoder passwordEncoder =EncryptionAlgorithm.ENCODER_MAP.get(encorderType);
System.out.println(passwordEncoder);
System.out.println(sysUser);
System.out.println(password);
if(sysUser==null){
throw new UsernameNotFoundException("can't find userinfo by username:"+username);
}else if(!passwordEncoder.matches(password,sysUser.getPassword())){
throw new BadCredentialsException("password wrong!");
}else{
List<SysRole> list= iUserService.findRolesByUsername(username);
List<SimpleGrantedAuthority> simpleGrantedAuthorities= new ArrayList<SimpleGrantedAuthority>();
Iterator<SysRole> i=list.iterator();
while(i.hasNext()){
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(i.next().getRoleName()));
}
return new UsernamePasswordAuthenticationToken(username,password,simpleGrantedAuthorities);
}
}
private JSONObject getRequestBody(HttpServletRequest request) throws AuthenticationException{
try {
StringBuilder stringBuilder = new StringBuilder();
InputStream inputStream = request.getInputStream();
byte[] bs = new byte[StreamUtils.BUFFER_SIZE];
int len;
while ((len = inputStream.read(bs)) != -1) {
stringBuilder.append(new String(bs, 0, len));
}
return JSON.parseObject(stringBuilder.toString());
} catch (IOException e) {
System.out.println("get request body error.");
}
throw new AuthenticationServiceException("invalid request body");
}
I would not write a custom security but use Spring Security, they have a strong library and has worked it out for you, it is a matter of configuration!
My Aproach was easy implemented! I have a user class where I store
Kotlin Code
var username: String? = null
var password: String? = null
var active: Boolean = false
var confirmationToken: String? = null // email confirmationToken sent # registration and other admin functions
var token: String? = null // If JWT token exist (not NULL or "") then the Networker is logged in with Client!
var roles: String? = null
var permissions: String? = null
ADD CONSTRUCTORS ....
val roleList: List<String>
get() = if (this.roles?.isNotEmpty()!!) {
listOf(*this.roles?.split(",".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()!!)
} else ArrayList()
val permissionList: List<String>
get() = if (this.permissions?.isNotEmpty()!!) {
listOf(*this.permissions?.split(",".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()!!)
} else ArrayList()
from there I config the securityConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
#Configuration
#EnableWebSecurity
class SecurityConfiguration(private val userPrincipalDetailService: UserPrincipalDetailService) :
WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationProvider())
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
.antMatchers("/index.html").permitAll()
.antMatchers("/security/**").permitAll()
.antMatchers("/profile/**").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN")
.and().formLogin()
.defaultSuccessUrl("/profile/index", true)
.loginProcessingUrl("/security/login")
.loginPage("/security/login").permitAll()
.usernameParameter("username")
.passwordParameter("password")
.and().logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
.logoutRequestMatcher(AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/security/login")
.and()
.rememberMe().tokenValiditySeconds(2592000) // 2592000 = 30 days in Seconds
.rememberMeParameter("rememberMe")
}
private fun authenticationProvider(): DaoAuthenticationProvider {
val daoAuthenticationProvider = DaoAuthenticationProvider()
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder())
daoAuthenticationProvider.setUserDetailsService(this.userPrincipalDetailService)
return daoAuthenticationProvider
}
#Bean
internal fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}
If you want to follow a Course in Spring Security - you can follow this one
Spring Boot Security by Romanian Coder

Spring Security: Custom CSRF Implementation by extending CsrfRepository

I am trying to create a customized CSRF implementation in my Spring Boot application by implementing the CsrfRepository interface provided by Spring Security.
Below is how my custom repository looks like:
public class CustomCookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
#Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.DEFAULT_CSRF_HEADER_NAME, this.DEFAULT_CSRF_PARAMETER_NAME, createNewToken());
}
#Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = token == null ? "" : token.getToken();
Cookie cookie = new Cookie(this.DEFAULT_CSRF_COOKIE_NAME, tokenValue);
cookie.setSecure(request.isSecure());
response.addCookie(cookie);
}
#Override
public CsrfToken loadToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.DEFAULT_CSRF_COOKIE_NAME);
if (cookie == null) {
return null;
}
String token = cookie.getValue();
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.DEFAULT_CSRF_HEADER_NAME, this.DEFAULT_CSRF_PARAMETER_NAME, token);
}
private String createNewToken() {
String unsignedToken = UUID.randomUUID().toString();
return RSAUtil.signMessage(unsignedToken, privateKey);
}
}
QUESTION: As you can see, I want to sign my cookie value using a private key and validate it using a public key. The question is where should this verification logic take place? I am guessing loadToken() method can have the logic to validate the signature. Is this the correct place or should it take place elsewhere?
Can someone provide some snippets or samples on how and where to handle this?
No, the verification logic should be in generateToken(HttpServletRequest request) of your custom CsrfTokenRepository implementation. The saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) should save the token (or delete the saved token when the passed 'token' param is null) and loadToken(HttpServletRequest request) should return the existing saved token (which was saved by saveToken method) for the current request/session;
#Component
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private String cookieName = "USER_INFO";
private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CustomCsrfTokenRepository2.class
.getName().concat(".CSRF_TOKEN");
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
#Override
public CsrfToken generateToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
String cookieValue = cookie.getValue();
String token = cookieValue.split("\\|")[0];
if (!StringUtils.hasLength(token)) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
#Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
if (token == null) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
}
else {
HttpSession session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
#Override
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
private String createNewToken() {
return UUID.randomUUID().toString();
}
}
And you need to set your customCsrfRepoImpl bean in HttpSecurity configuration as shown below
#Configuration
#EnableWebSecurity
public class SecurityConfigurarion extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
#Autowired
private CsrfTokenRepository customCsrfTokenRepository; //your custom csrfToken repository impl class
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().csrfTokenRepository(customCsrfTokenRepository) //set your custom csrf impl in httpSecurity
.and()
.authorizeRequests()
.antMatchers(permittedUrlsArr).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
}
}

Spring Boot Social Login and Local OAuth2-Server

I'm currently working on a Spring Boot-Application with OAuth2-Authentication. I have a local OAuth2-Server where I receive a token when posting username and password of the local database against in my case http://localhost:8080/v1/oauth/token using Spring Boot's UserDetails and UserService. Everything works fine and nice.
But now I want to enhance my program with Facebook social login and want either log in to my local OAuth2-Server or using the external Facebook-Server. I checked out the Spring Boot example https://spring.io/guides/tutorials/spring-boot-oauth2/ and adapted the idea of an SSO-Filter. Now I can login using my Facebook client and secret id, but I cannot access my restricted localhost-sites.
What I want is that the Facebook-Token "behaves" the same way as the locally generated tokens by for instance being part of my local token storage. I checked out several tutorials and other Stackoverflow questions but with no luck. Here is what I have so far with a custom Authorization-Server and I think I'm still missing something very basic to get the link between external Facebook- and internal localhost-Server:
#Configuration
public class OAuth2ServerConfiguration {
private static final String SERVER_RESOURCE_ID = "oauth2-server";
#Autowired
private TokenStore tokenStore;
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
protected class ClientResources {
#NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
#NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
#Configuration
#EnableResourceServer
#EnableOAuth2Client
protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${pia.requireauth}")
private boolean requireAuth;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore).resourceId(SERVER_RESOURCE_ID);
}
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
#Override
public void configure(HttpSecurity http) throws Exception {
if (!requireAuth) {
http.antMatcher("/**").authorizeRequests().anyRequest().permitAll();
} else {
http.antMatcher("/**").authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest().authenticated().and()
.exceptionHandling().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
}
}
#Configuration
#EnableAuthorizationServer
protected class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
#Value("${pia.oauth.tokenTimeout:3600}")
private int expiration;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
// password encryptor
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
configurer.authenticationManager(authenticationManager).tokenStore(tokenStore).approvalStoreDisabled();
configurer.userDetailsService(userDetailsService);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("pia").secret("alphaport").accessTokenValiditySeconds(expiration)
.authorities("ROLE_USER").scopes("read", "write").authorizedGrantTypes("password", "refresh_token")
.resourceIds(SERVER_RESOURCE_ID);
}
}
}
Any help and/or examples covering this issue greatly appreciated! :)
One possible solution is to implement the Authentication Filter and Authentication Provider.
In my case I've implemented an OAuth2 authentication and also permit the user to access some endpoints with facebook access_token
The Authentication Filter looks like this:
public class ServerAuthenticationFilter extends GenericFilterBean {
private BearerAuthenticationProvider bearerAuthenticationProvider;
private FacebookAuthenticationProvider facebookAuthenticationProvider;
public ServerAuthenticationFilter(BearerAuthenticationProvider bearerAuthenticationProvider,
FacebookAuthenticationProvider facebookAuthenticationProvider) {
this.bearerAuthenticationProvider = bearerAuthenticationProvider;
this.facebookAuthenticationProvider = facebookAuthenticationProvider;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
Optional<String> authorization = Optional.fromNullable(httpRequest.getHeader("Authorization"));
try {
AuthType authType = getAuthType(authorization.get());
if (authType == null) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
String strToken = authorization.get().split(" ")[1];
if (authType == AuthType.BEARER) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Bearer method. Token: " + token.get());
processBearerAuthentication(token);
}
} else if (authType == AuthType.FACEBOOK) {
if (strToken != null) {
Optional<String> token = Optional.of(strToken);
logger.debug("Trying to authenticate user by Facebook method. Token: " + token.get());
processFacebookAuthentication(token);
}
}
logger.debug(getClass().getSimpleName() + " is passing request down the filter chain.");
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException) {
SecurityContextHolder.clearContext();
logger.error("Internal Authentication Service Exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
} catch (Exception e) {
SecurityContextHolder.clearContext();
e.printStackTrace();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
private AuthType getAuthType(String value) {
if (value == null)
return null;
String[] basicSplit = value.split(" ");
if (basicSplit.length != 2)
return null;
if (basicSplit[0].equalsIgnoreCase("bearer"))
return AuthType.BEARER;
if (basicSplit[0].equalsIgnoreCase("facebook"))
return AuthType.FACEBOOK;
return null;
}
private void processBearerAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithBearer(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private void processFacebookAuthentication(Optional<String> token) {
Authentication resultOfAuthentication = tryToAuthenticateWithFacebook(token);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticateWithBearer(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateBearer(requestAuthentication);
}
private Authentication tryToAuthenticateWithFacebook(Optional<String> token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token,
null);
return tryToAuthenticateFacebook(requestAuthentication);
}
private Authentication tryToAuthenticateBearer(Authentication requestAuthentication) {
Authentication responseAuthentication = bearerAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by bearer method.");
return responseAuthentication;
}
private Authentication tryToAuthenticateFacebook(Authentication requestAuthentication) {
Authentication responseAuthentication = facebookAuthenticationProvider.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException(
"Unable to Authenticate for provided credentials.");
}
logger.debug("Application successfully authenticated by facebook method.");
return responseAuthentication;
}
}
This, filters Authorization headers, identifies whether they are facebook or bearer and then directs to specific provider.
The Facebook Provider looks like this:
public class FacebookAuthenticationProvider implements AuthenticationProvider {
#Value("${config.oauth2.facebook.resourceURL}")
private String facebookResourceURL;
private static final String PARAMETERS = "fields=name,email,gender,picture";
#Autowired
FacebookUserRepository facebookUserRepository;
#Autowired
UserRoleRepository userRoleRepository;
#SuppressWarnings({ "rawtypes", "unchecked" })
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
Optional<String> token = auth.getPrincipal() instanceof Optional ? (Optional) auth.getPrincipal() : null;
if (token == null || !token.isPresent() || token.get().isEmpty())
throw new BadCredentialsException("Invalid Grants");
SocialResourceUtils socialResourceUtils = new SocialResourceUtils(facebookResourceURL, PARAMETERS);
SocialUser socialUser = socialResourceUtils.getResourceByToken(token.get());
if (socialUser != null && socialUser.getId() != null) {
User user = findOriginal(socialUser.getId());
if (user == null)
throw new BadCredentialsException("Authentication failed.");
Credentials credentials = new Credentials();
credentials.setId(user.getId());
credentials.setUsername(user.getEmail());
credentials.setName(user.getName());
credentials.setRoles(parseRoles(user.translateRoles()));
credentials.setToken(token.get());
return new UsernamePasswordAuthenticationToken(credentials, credentials.getId(),
parseAuthorities(getUserRoles(user.getId())));
} else
throw new BadCredentialsException("Authentication failed.");
}
protected User findOriginal(String id) {
FacebookUser facebookUser = facebookUserRepository.findByFacebookId(facebookId);
return null == facebookUser ? null : userRepository.findById(facebookUser.getUserId()).get();
}
protected List<String> getUserRoles(String id) {
List<String> roles = new ArrayList<>();
userRoleRepository.findByUserId(id).forEach(applicationRole -> roles.add(applicationRole.getRole()));
return roles;
}
private List<Roles> parseRoles(List<String> strRoles) {
List<Roles> roles = new ArrayList<>();
for(String strRole : strRoles) {
roles.add(Roles.valueOf(strRole));
}
return roles;
}
private Collection<? extends GrantedAuthority> parseAuthorities(Collection<String> roles) {
if (roles == null || roles.size() == 0)
return Collections.emptyList();
return roles.stream().map(role -> (GrantedAuthority) () -> "ROLE_" + role).collect(Collectors.toList());
}
#Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
The FacebookUser only makes a reference to the Local User Id and the Facebook Id (this is the link between facebook and our application).
This SocialResourceUtils is used to get the facebook user information via facebook API (using the method getResourceByToken). The facebook resource url is setted on application.properties (config.oauth2.facebook.resourceURL). This method is basically:
public SocialUser getResourceByToken(String token) {
RestTemplate restTemplate = new RestTemplate();
String authorization = token;
JsonNode response = null;
try {
response = restTemplate.getForObject(accessUrl + authorization, JsonNode.class);
} catch (RestClientException e) {
throw new BadCredentialsException("Authentication failed.");
}
return buildSocialUser(response);
}
The Bearer Provider is your local Authentication, you can make your own, or use the springboot defaults, use other authentication methods, idk (I will not put my implementation here, thats by you).
And finally you need to make your Web Security Configurer:
#ConditionalOnProperty("security.basic.enabled")
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private BearerAuthenticationProvider bearerAuthenticationProvider;
#Autowired
private FacebookAuthenticationProvider facebookAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.addFilterBefore(new ServerAuthenticationFilter(bearerAuthenticationProvider,
facebookAuthenticationProvider), BasicAuthenticationFilter.class);
}
}
Notice that it has the annotation ConditionalOnProperty to enable/disable on properties security.basic.enabled. The #EnableGlobalMethodSecurity(prePostEnabled = true) enables the usage of the annotation #PreAuthorize which enables us to protect endpoints by roles for example (using #PreAuthorize("hasRole ('ADMIN')") over an endpoint, to allow acces only to admins)
This code needs many improvements, but I hope I have helped.

Intercept the SSO cookie before the authorization server redirects the page

I have been banging my head for over a week to intercept the SSO cookie before the Authorization server redirects me my app page.
I'm implementing mitreid-connect for openid configuration. I have followed the documentation in the link and configured it using Java Config. Everything works fine, the redirects and etc., but I'm trying to implement AbstractPreAuthenticatedProcessingFilter to intercept the SSO cookie before the authorization server consumes it and generates the IdToken.
Please let me know if this is not right. I'm very new to spring-security and its scraping my scales off and its driving me crazy how to get hold to sso cookie
I have found this link
Please help me
#Configuration
public class filter extends AbstractPreAuthenticatedProcessingFilter {
#Bean(name = "singleSignOnFilter")
public String filter() {
return "PRE_AUTH_FILTER";
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return null;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
Cookie[] cookie = request.getCookies();
for(int i = 0; i < cookie.length; i++) {
System.out.println(cookie[i].getName() + " - " + cookie[i].getValue());
}
return null;
}
}
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Inject
private ClientDetailsEntity client;
#Inject
private String issuer;
#Bean
public ProviderManager providerManager() {
List<AuthenticationProvider> authenticationProvider = new LinkedList<AuthenticationProvider>();
authenticationProvider.add(oidcAuthProvider());
return new ProviderManager(authenticationProvider);
}
#Bean(name = "authenticationProvider")
public AuthenticationProvider oidcAuthProvider() {
return new OIDCAuthenticationProvider();
}
#Bean(name = "authoritiesMapper")
public OIDCAuthoritiesMapper authorityMapper() {
NamedAdminAuthoritiesMapper namedAdminAuthMapper = new NamedAdminAuthoritiesMapper();
namedAdminAuthMapper.setAdmins(admins());
return namedAdminAuthMapper;
}
#Bean(name = "admins")
public Set<SubjectIssuerGrantedAuthority> admins() {
Set<SubjectIssuerGrantedAuthority> admin = new HashSet<SubjectIssuerGrantedAuthority>();
return admin;
}
#Bean(name = "openIdConnectAuthenticationFilter")
public Filter openIdConnectAuthenticationFilter() {
OIDCAuthenticationFilter oidcAuthFilter = new OIDCAuthenticationFilter();
oidcAuthFilter.setAuthenticationManager(providerManager());
oidcAuthFilter.setIssuerService(issuerService());
oidcAuthFilter.setClientConfigurationService(clientConfigurationService());
oidcAuthFilter.setAuthRequestUrlBuilder(authRequestUrlBuilder());
return oidcAuthFilter;
}
#Bean(name = "issuerService")
public IssuerService issuerService() {
StaticSingleIssuerService issuerService = new StaticSingleIssuerService();
issuerService.setIssuer(issuer);
return issuerService;
}
#Bean(name = "clientConfigurationService")
public ClientConfigurationService clientConfigurationService() {
StaticClientConfigurationService clientConfigService = new StaticClientConfigurationService();
clientConfigService.setClients(registeredClient());
return clientConfigService;
}
#Bean(name = "clients")
public Map<String, RegisteredClient> registeredClient() {
Map<String, RegisteredClient> oidcRegClients = new HashMap<String, RegisteredClient>();
oidcRegClients.put(issuer, new RegisteredClient(client));
return oidcRegClients;
}
#Bean(name = "authRequestUrlBuilder")
public AuthRequestUrlBuilder authRequestUrlBuilder() {
return new PlainAuthRequestUrlBuilder();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(openIdConnectAuthenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.formLogin()
.loginPage("/openid_connect_login")
.and()
.logout()
.and()
.authorizeRequests()
.antMatchers("/items")
.authenticated()
.anyRequest()
.permitAll();
}
}
You are on right path. I think, your question is how to extract the information(for e.g. username) from cookie and then use this information to authorize the user. Here are the steps, to clear some air.
Configure a subclass of AbstractPreAuthenticatedProcessingFilter (e.g. below )
public class CustomPreAuthenticatedFilter extends AbstractPreAuthenticatedProcessingFilter {
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// below sample extracts the username from header.You can pull from cookie
String expectedHeaderNameContainingUsername = "abcd";
String username = request.getHeader(expectedHeaderNameContainingUsername);
return username;
}
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
String expectedHeaderNameContainingCredentials = "";
if (StringUtils.isNotBlank(expectedHeaderNameContainingCredentials)) {
return request.getHeader(expectedHeaderNameContainingCredentials);
}
return "N/A";
}
}
Simply register the above filter with HTTP security with
http.addFilter(Filter filter);
It seems your filter is not registered with spring security.
The AbstractPreAuthenticatedProcessingFilter forwards the result of getPreAuthenticatedPrincipal(..) to authmanager to build the principal object.
Hope this helps.

Resources