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

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

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);
}

Spring Security Google OAuth2 authorizationUri became domain after deployed on aws

So I am current testing with spring securtiy with google oauth.
it works fine, when trying to login with google through
/oauth2/authorization/google
on localhost
but when i deployed the application on tomcat on aws ec2 instance,
the authorizationuri became sometime like below
when running on localhost:
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount? ............
when running on aws ec2 domain:
http://{domain}:{port}/o/oauth2/v2/auth? ..........
but when i manually replace the http domain and port with https://accounts.google.com/
it directs me to google login page and are able to complete the login successfully.
So i wonder if there is any part of the setting ive being missing or did wrong that cause it to happen.
Thanks in advance.
the complete code for spring security setting is like below
#Configuration
#EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
private lateinit var oidUserSer:OidUserService
#Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(googleClientRegistration())
}
private fun googleClientRegistration(): ClientRegistration {
return ClientRegistration.withRegistrationId("google")
.clientId("clientId")
.clientSecret("secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://{domain:port}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build()
}
#Bean
fun customAuthorizationRequestResolver(): CustomAuthorizationRequestResolver {
val repo = InMemoryClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientName("Google")
.clientId("61770666483-dj64uabnia7tq2g0kri2ajrb7sl21r3t.apps.googleusercontent.com")
.clientSecret("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")
.redirectUri("https://{domain:port}/login/oauth2/code/{registrationId}")
.build()
)
val baseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
val customizeAuthorizationRequest= CustomAuthorizationRequestResolver(repo,baseUri)
return customizeAuthorizationRequest
}
/**
* セキュリティの有効範囲設定
*/
#Override
override fun configure(web: WebSecurity) {
// org.springframework.security.web.firewall.RequestRejectedException:
// The request was rejected because the URL contained a potentially malicious String ";"
// というエラーログがコンソールに出力されるため、下記を追加
val firewall = DefaultHttpFirewall()
web.httpFirewall(firewall)
web.ignoring().antMatchers(
"/img/**",
"/css/**",
"/js/**",
"/libs/**"
)
}
#Override
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.exceptionHandling()
.authenticationEntryPoint(LoginUrlAuthenticationEntryPoint("/login"));
http.oauth2Login()
.loginPage("/login")
.defaultSuccessUrl("/login-success", true)
.userInfoEndpoint().oidcUserService(oidUserSer).
and()
.failureUrl("/login?error")
.authorizationEndpoint()
.authorizationRequestResolver(customAuthorizationRequestResolver())
http.logout()
//.logoutRequestMatcher(AntPathRequestMatcher("/logout**"))
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.logoutSuccessHandler(CustomLogoutSuccessHandler())
//.invalidateHttpSession(true)
//セッション設定
http.sessionManagement().
invalidSessionUrl("/login?timeout=true")
}
}
class CustomAuthorizationRequestResolver(repo: ClientRegistrationRepository?,
authorizationRequestBaseUri: String?):OAuth2AuthorizationRequestResolver{
private var defaultResolver:DefaultOAuth2AuthorizationRequestResolver? = DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri)
override fun resolve(request: HttpServletRequest): OAuth2AuthorizationRequest? {
val authorizationRequest:OAuth2AuthorizationRequest? =
this.defaultResolver?.resolve(request)
return customAuthorizationRequest(authorizationRequest)
}
override fun resolve(request: HttpServletRequest?, clientRegistrationId: String?): OAuth2AuthorizationRequest? {
val authorizationRequest = this.defaultResolver?.resolve(
request, clientRegistrationId);
return authorizationRequest?.let { customAuthorizationRequest(it) }
}
private fun customAuthorizationRequest(authorizationRequest: OAuth2AuthorizationRequest?): OAuth2AuthorizationRequest? {
var param:OAuth2AuthorizationRequest? = null
if(authorizationRequest!=null){
val additionalParameter: LinkedHashMap<String,Any> = LinkedHashMap(authorizationRequest!!.additionalParameters)
additionalParameter.put("prompt","select_account+consent")
param = OAuth2AuthorizationRequest.from(authorizationRequest).additionalParameters(additionalParameter).build()
}
return param
}
}

How to add a custom OpenId Filter in a Spring boot application?

I am trying to implement the backend side of an OpenId Connect authentication. It is a stateless API so I added a filter that handles the Bearer token.
I have created the OpenIdConnect Filter that handles the Authentication and added it in a WebSecurityConfigurerAdapter.
public class OpenIdConnectFilter extends
AbstractAuthenticationProcessingFilter {
#Value("${auth0.clientId}")
private String clientId;
#Value("${auth0.issuer}")
private String issuer;
#Value("${auth0.keyUrl}")
private String jwkUrl;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
public OpenIdConnectFilter() {
super("/connect/**");
setAuthenticationManager(new NoopAuthenticationManager());
}
#Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
Authentication authentication = tokenExtractor.extract(request);
String accessToken = (String) authentication.getPrincipal();
String kid = JwtHelper.headers(accessToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
int expires = (Integer) authInfo.get("exp");
OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Here is security configuration:
#Configuration
#EnableWebSecurity
public class OpenIdConnectWebServerConfig extends
WebSecurityConfigurerAdapter {
#Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter();
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.antMatcher("/connect/**").authorizeRequests()
.antMatchers(HttpMethod.GET, "/connect/public").permitAll()
.antMatchers(HttpMethod.GET, "/connect/private").authenticated()
.antMatchers(HttpMethod.GET, "/connect/private-
messages").hasAuthority("read:messages")
.antMatchers(HttpMethod.GET, "/connect/private-
roles").hasAuthority("read:roles")
.and()
.addFilterBefore(myFilter(),
UsernamePasswordAuthenticationFilter.class);
}
Rest endpoints looks like following:
#RequestMapping(value = "/connect/public", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String publicEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You DO NOT need to be authenticated to
call /api/public.")
.toString();
}
#RequestMapping(value = "/connect/private", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public String privateEndpoint() throws JSONException {
return new JSONObject()
.put("message", "All good. You can see this because you are
Authenticated.")
.toString();
}
If I remove completely the filter for configuration and also the #Bean definition, the configuration works as expected: /connect/public is accessible, while /connect/private is forbidden.
If I keep the #Bean definition and add it in filter chain the response returns a Not Found status for requests both on /connect/public and /connect/private:
"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/
When debugging I noticed that filter is processing the token and returns an implementation of Authentication.
Is the filter properly added in filter chain and in correct position?
Why is the filter invoked also on /connect/public path when this is supposed to be public. Is it applied to all paths matching super("/connect/**") call?
Why is it returning the path as "/" when the request is made at /connect/private
Seems that is something wrong with the filter, cause every time it is applied, the response is messed up.

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

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.

Spring security token persistence storage not working

The problem is that the login and all things are working great except the remember me logic. The cookie is not set and there is no rows are inserted in the database.
This is the security configuration class.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* Spring security configurations.
*/
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// Authorize all requests
.authorizeRequests()
// Allow only admins to access the administration pages
.antMatchers("/admin/**").access("hasRole('ADMIN')")
// Allow any one to access the register and the main pages only alongside
// the resources files that contains css and javascript files
.antMatchers("/resources/**", "/register", "/").permitAll()
// Authenticate any other request
.anyRequest().authenticated()
.and()
// Set up the login form.
.formLogin()
//.successHandler(successHandler())
.loginPage("/login")
.usernameParameter("email").passwordParameter("password")
.permitAll()
.and()
// Enable remember me cookie and persistence storage
.rememberMe()
// Database token repository
.tokenRepository(persistentTokenRepository())
// Valid for 20 days
.tokenValiditySeconds(20 * 24 * 60 * 60)
.rememberMeParameter("remember-me")
.and()
// Log out handler
.logout()
.permitAll()
.and()
// Enable Cross-Site Request Forgery
.csrf();
}
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// Provide database authentication and swl queries to fetch the user's data..
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select email, password, enabled from users where email=?")
.authoritiesByUsernameQuery("select us.email, ur.role from users us, " +
" roles ur where us.role_id=ur.id and us.email=?");
}
}
and this is the database table for token persistence
CREATE TABLE persistent_logins (
username VARCHAR(254) NOT NULL,
series VARCHAR(64) NOT NULL,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL,
PRIMARY KEY (series)
);
Spring Security comes with 2 implementation of PersistentTokenRepository : JdbcTokenRepositoryImpl and InMemoryTokenRepositoryImpl.
I'm using Hibernate in my application, i create a custom implementation using Hibernate instead of using JDBC.
#Repository("tokenRepositoryDao")
#Transactional
public class HibernateTokenRepositoryImpl extends AbstractDao<String, PersistentLogin>
implements PersistentTokenRepository {
static final Logger logger = LoggerFactory.getLogger(HibernateTokenRepositoryImpl.class);
#Override
public void createNewToken(PersistentRememberMeToken token) {
logger.info("Creating Token for user : {}", token.getUsername());
PersistentLogin persistentLogin = new PersistentLogin();
persistentLogin.setUsername(token.getUsername());
persistentLogin.setSeries(token.getSeries());
persistentLogin.setToken(token.getTokenValue());
persistentLogin.setLast_used(token.getDate());
persist(persistentLogin);
}
#Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
logger.info("Fetch Token if any for seriesId : {}", seriesId);
try {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("series", seriesId));
PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult();
return new PersistentRememberMeToken(persistentLogin.getUsername(), persistentLogin.getSeries(),
persistentLogin.getToken(), persistentLogin.getLast_used());
} catch (Exception e) {
logger.info("Token not found...");
return null;
}
}
#Override
public void removeUserTokens(String username) {
logger.info("Removing Token if any for user : {}", username);
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("username", username));
PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult();
if (persistentLogin != null) {
logger.info("rememberMe was selected");
delete(persistentLogin);
}
}
#Override
public void updateToken(String seriesId, String tokenValue, Date lastUsed) {
logger.info("Updating Token for seriesId : {}", seriesId);
PersistentLogin persistentLogin = getByKey(seriesId);
persistentLogin.setToken(tokenValue);
persistentLogin.setLast_used(lastUsed);
update(persistentLogin);
}
}
I've reproduced the same issue. Using debug I've checked loginSuccess() method for AbstractRememberMeServices class.
Internal logic was like:
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
it turned out that I hadn’t been tagged Remember Me tag while the user was logging in, so I couldn't call onLoginSuccess() method and fell into the block if instead of else.
After I marked the flag, I was able to persist the token and cookies.
Notice: logic can be taken from the answer mentioned by #FuSsA.
Right, so actually, cookie is written in the login success hanlder, so DB persitance error can cause a cookie tofail to be written. My issue was data type on date in persistant table in ms sql:
CREATE TABLE [dbo].[persistent_logins](
--[id] [bigint] IDENTITY(1,1) NOT NULL,
[username] [varchar](64) NOT NULL,
[series] [varchar](64) NOT NULL,
[token] [varchar](64) NOT NULL,
[last_used] [datetime] NOT NULL default CURRENT_TIMESTAMP);
From there SecSecurityConfig is:
.rememberMe()
// Valid for 20 days
.tokenValiditySeconds(20 * 24 * 60 * 60)
.rememberMeParameter("remember-me")
.key("yourPrivateKeyOfChoice")
.tokenRepository(persistentTokenRepository())
.rememberMeServices(rememberMeServices())
...
#Bean
public RememberMeServices rememberMeServices() {
CustomRememberMeServices rememberMeServices = new
CustomRememberMeServices("againThePrivateKey",
userDetailsService, persistentTokenRepository());
return rememberMeServices;
}
#Autowired
#Qualifier("dataSourceEwoForSap")
DriverManagerDataSource dataSource;
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setCreateTableOnStartup(false);
db.setDataSource(dataSource);
return db;
}
...then custom remember me has
public CustomRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
this.tokenRepository = tokenRepository;
this.key = key;
}
#Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = ((User) successfulAuthentication.getPrincipal()).getEmail();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
} catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}

Resources