Flutter post request not returning with Spring boot server login - spring-boot

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

Related

How to have access to token in header to pass it to thymeleaf to be able to do ajax call

I use spring boot with spring cloud gateway
I have another app with spring boot and thymeleaf
Spring gateway return a token to my thymeleaf app.
#EnableWebFluxSecurity
#Configuration
public class WebFluxSecurityConfig {
#Autowired
private WebFluxAuthManager authManager;
#Bean
protected SecurityWebFilterChain securityFilterChange(ServerHttpSecurity http) throws Exception {
http.authorizeExchange()
// URL that starts with / or /login/
.pathMatchers("/", "/login", "/js/**", "/images/**", "/css/**", "/h2-console/**").permitAll()
.anyExchange().authenticated().and().formLogin()
.authenticationManager(authManager)
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccesHandler("/findAllCustomers"));
return http.build();
}
}
WebFluxAuthManager class
#Component
public class WebFluxAuthManager implements ReactiveAuthenticationManager {
#Value("${gateway.url}")
private String gatewayUrl;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
// return is already authenticated
if (authentication.isAuthenticated()) {
return Mono.just(authentication);
}
String username = authentication.getName();
String password = authentication.getCredentials().toString();
LoginRequest loginRequest = new LoginRequest(username, password);
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
//todo modify to use webclient
HttpPost httpPost = new HttpPost(this.gatewayUrl + "/authenticate");
httpPost.setHeader("Content-type", "application/json");
String jsonReq = converObjectToJson(loginRequest);
StringEntity requestEntity = new StringEntity(jsonReq);
httpPost.setEntity(requestEntity);
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.OK.value()) {
HttpEntity entity = httpResponse.getEntity();
Header encodingHeader = entity.getContentEncoding();
Charset encoding = encodingHeader == null ? StandardCharsets.UTF_8
: Charsets.toCharset(encodingHeader.getValue());
// use org.apache.http.util.EntityUtils to read json as string
String jsonRes = EntityUtils.toString(entity, encoding);
LoginResponse loginResponse = converJsonToResponse(jsonRes);
Collection<? extends GrantedAuthority> authorities = loginResponse.getRoles().stream()
.map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());
return Mono.just(new UsernamePasswordAuthenticationToken(username, password, authorities));
} else {
throw new BadCredentialsException("Authentication Failed!!!");
}
} catch (RestClientException | ParseException | IOException e) {
throw new BadCredentialsException("Authentication Failed!!!", e);
} finally {
try {
if (httpClient != null)
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
In WebFluxAuthManager, I have access to the token, now I search a way to transfert it to a fragment.

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

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 OAuth2- POST request to oauth/token redirects to login and role displays ROLE_ANONYMOUS

I am following the link https://spring.io/blog/2015/02/03/sso-with-oauth2-angular-js-and-spring-security-part-v & the github project https://github.com/spring-guides/tut-spring-security-and-angular-js/tree/master/oauth2. I am able to login in to the OAuth provider and get the authorization code back in the client.
Now I make the following call from the client to get the token from the provider (provider is on port 9999)
HttpHeaders headers = new HttpHeaders();
headers.add("Accept",MediaType.APPLICATION_JSON_VALUE);
List<String> cookies = httpEntity.getHeaders().get("Cookie");
headers.put("Cookie", cookies);
String redirectURL= "http://localhost:9999/oauthprovider/oauth/token" + "?" + "response_type=token" + "&" + "grant_type=authorization_code" + "&" + "client_id=acme"+ "&" + "client_secret=acmesecret"+ "&" + "redirect_uri=http://localhost:8081/callback"+"&" + "code=" + authCode + "&" + "state=" + stateValue;
HttpEntity<String> redirectResponse = template.exchange(
redirectURL,
HttpMethod.POST,
responseentity,
String.class);
result=redirectResponse.toString()
The result variable value has the following.(I have disabled csrf and sending client_secret as a query parameter (for the time being), although they are not recommended)
<302 Found,{X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Location=[http://localhost:9999/oauthprovider/oauthlogin], Content-Length=[0], Date=[Thu, 09 Nov 2017 12:07:37 GMT]}>
In the console I have these
Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 2B669DF59BCE8047849BFBCA148BEE67; Granted Authorities: ROLE_ANONYMOUS
Does I am redirecting back to login(I am getting it in the logs as mentioned before), since the role is ROLE_ANONYMOUS? How can I fix the issue?
Adding more details on the code (Did only minor changes from the sample code provided in the link). Providers's context path is /oauthprovider and with curl call I am getting the token.
#Configuration
#EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends
AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
//................................
//................................
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")
.secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token",
"password").scopes("openid").autoApprove(true).redirectUris("http://localhost:8081/callback");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager).accessTokenConverter(
jwtAccessTokenConverter());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
}
#Configuration
#Order(-20)
protected static class LoginConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.formLogin().loginPage("/oauthlogin").loginProcessingUrl("/login").failureUrl("/login?error=true").permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauthlogin", "/oauth/authorize", "/oauth/token" ,"/oauth/confirm_access")
.and()
.authorizeRequests().anyRequest().authenticated();
// #formatter:on
http.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManager);
}
}
For calling token endpoint you need to send Authorization header with value base64(client_id:client_secret) and in body you should send username , password ,grant_type as FORM_URLENCODED:
String oauthHost = InetAddress.getByName(OAUTH_HOST).getHostAddress();
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
// Basic Auth
String plainCreds = clientId + ":" + clientSecret;
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = org.apache.commons.net.util.Base64.encodeBase64(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
// params
map.add("username", username);
map.add("password", password);
map.add("grant_type", GRANT_TYPE);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map,
headers);
// CALLING TOKEN URL
OauthTokenRespone res = null;
try {
res = restTemplate.postForObject(OAUTH_TOKEN_URL.replace(OAUTH_HOST, oauthHost), request,
OauthTokenRespone.class);
} catch (Exception ex) {
ex.printStackTrace();
}
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class OauthTokenRespone {
private String access_token;
private String token_type;
private String refresh_token;
private String expires_in;
private String scope;
private String organization;
//getter and setter
}
Something went wrong in my last day's testing. I can get access token if my handler is either in client or in provider. If my redirect handler is in provider the user is not anonymous as per the debug logs (may be due to session..?)But looks like I have to use one redirect_url consistently.(Otherwise I get redirect_uri mismatch error..)
Following is the working code that gets json response..
#ResponseBody
#RequestMapping(value = "/clientcallback", method = RequestMethod.GET)
public ResponseEntity<OauthTokenResponse> redirectCallback(#RequestParam (value= "code", defaultValue="") String authCode,#RequestParam (value= "state", defaultValue="") String stateValue,HttpEntity<String> httpEntity)
{
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
String plainCreds = "acme:acmesecret";
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = org.apache.commons.net.util.Base64.encodeBase64(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
List<String> cookies = httpEntity.getHeaders().get("Cookie");
if(cookies != null)
{
headers.put("Cookie", cookies);
}
else
{
cookies = httpEntity.getHeaders().get("Set-Cookie");
headers.put("Set-Cookie", cookies);
}
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map,
headers);
OauthTokenResponse res = null;
try {
res = restTemplate.postForObject("http://localhost:9999/uaa/oauth/token?grant_type=authorization_code&client_id=acme&redirect_uri=http://localhost:8081/clientcallback&code=" + authCode, request,
OauthTokenResponse.class);
} catch (Exception ex) {
ex.printStackTrace();
}
return new ResponseEntity<OauthTokenResponse>(res, HttpStatus.OK);
}
Thanks again for the tips..
I know this is old but I've recently faced a very similar issue so I'm posting my solution.
I used Simle SSO Example as a base for my modifications. I'm using spring security filter which is mapped to / (web root) and spring oauth with endpoints mapped to /auth/*. When I try to access /auth/oauth/token I get redirect to login page. After some debugging I found out the cause:
By using #EnableAuthorizationServer you are importing AuthorizationServerSecurityConfiguration which secures endpoints /oauth/token, /oauth/token_key and /oauth/check_token. Everything is going to work with this default configuration as long as your authorization server is mapped to the web root. In my case, requests to /auth/oauth/token were simply redirected to login page because spring security could not find a rule for this path.
My solution was to manualy secure those endpoints with /auth prefix in my spring security configuration.
Hope this helps.

Resources