I'm making an Angular application as the frontend with a Springboot backend. I have set up SpringSecurity to handle logins and if I try it using Postman everything works, but when I try it using Angulars login, the "request.getParameter" calls always return me "null". I have tried changing it in several ways but the result is always the same.
The successful login return information about the user and a token.
Here is the Java part:
#Configuration
#EnableWebSecurity
#ComponentScan
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Lazy
#Autowired
CurrentUserDetailsServiceImpl userDetailsService;
#Autowired
TokenAuthenticationService tokenAuthService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().cacheControl().disable();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.headers().xssProtection();
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/auth/**").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(
new LoginFilter("/auth/login", authenticationManager(), tokenAuthService),
UsernamePasswordAuthenticationFilter.class)
// Custom Token based authentication based on the header previously given to the
// client
.addFilterBefore(new JWTFilter(tokenAuthService), UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
// Allow anonymous resource requests
.antMatchers("/favicon.ico");
}
}
The LoginFilter does quite some things, but this is the minimum:
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
private TokenAuthenticationService tokenAuthenticationService;
public LoginFilter(String urlMapping, AuthenticationManager authenticationManager, TokenAuthenticationService tokenAuthenticationService) {
super(new AntPathRequestMatcher(urlMapping));
setAuthenticationManager(authenticationManager);
this.tokenAuthenticationService = tokenAuthenticationService;
}
#Override
#Transactional
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("USERNAME: " + username + " - PASSWORD: " + password);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
CurrentUser loggedUser = (CurrentUser) authentication.getPrincipal();
...
}
}
Finally, the Angular form does some validations and calls to a service:
#Injectable({
providedIn: 'root'
})
export class LoginService {
apiUrl: string = environment.API_URL;
constructor(private http: HttpClient) {}
login(data: LoginInterface): Observable<LoginResult> {
const formData: string =
'username=' + data.username + '&password=' + data.password;
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
})
};
return this.http.post<LoginResult>(
this.apiUrl + '/auth/login',
formData,
httpOptions
);
}
}
What am I doing wrong? Why is it working with Postman?
Thanks!
Edit: As requested, here are the console details for the call in Chrome
Headers:
Body:
Postman headers:
Postman data:
And here is Eclipse showing the null value that arrived:
Thanks!
You can probably do it by using HttpParams as the request body:
login(data: LoginInterface): Observable<LoginResult> {
const params = new HttpParams({
fromObject: {
username: data.username,
password: data.password,
},
});
return this.http.post<LoginResult>(this.apiUrl + '/auth/login', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
}
Related
I am using Spring Boot + Spring Security OAUTH 2 for my app. I want to use Spring OAUTH2 to "Login with Discord" and then parse the information that discord gives on an authentication success into my own JPA Entity which implements UserDetails. Also, I want to implement my own UserDetailsService to return that custom user entity that I created.
Basically, I only want Discord to provide unique information about a user to I can construct my own Account entity based on the response.
I've read the following articles but I still don't understand how to do it:
https://www.baeldung.com/spring-security-oauth-principal-authorities-extractor
https://www.devglan.com/spring-security/spring-oauth2-role-based-authorization
Also, here is my OAuth2 Setup:
Security Config (WebSecurityConfigurerAdapter):
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private AuthSuccessHandler authSuccessHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers(HttpMethod.GET, "/favicon.ico").permitAll()
.antMatchers(HttpMethod.GET, "/web/**").permitAll()
.antMatchers(HttpMethod.GET, "/oauth2/authorization/discord").anonymous()
.antMatchers(HttpMethod.GET, "/login").anonymous()
.antMatchers(HttpMethod.GET, "/logout").permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.successHandler(authSuccessHandler)
.tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient())
.and()
.userInfoEndpoint().userService(userService());
}
#Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
client.setRequestEntityConverter(new OAuth2AuthorizationCodeGrantRequestEntityConverter() {
#Override
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest oauth2Request) {
return addUserAgentHeader(Objects.requireNonNull(super.convert(oauth2Request)));
}
});
return client;
}
#Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> userService() {
DefaultOAuth2UserService service = new DefaultOAuth2UserService();
service.setRequestEntityConverter(new OAuth2UserRequestEntityConverter() {
#Override
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
return addUserAgentHeader(
Objects.requireNonNull(super.convert(userRequest)));
}
});
return service;
}
private RequestEntity<?> addUserAgentHeader(RequestEntity<?> request) {
HttpHeaders headers = new HttpHeaders();
headers.putAll(request.getHeaders());
headers.add(HttpHeaders.USER_AGENT, "Mozilla/5.0 (X11; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0");
return new RequestEntity<>(request.getBody(), headers, request.getMethod(), request.getUrl());
}
}
AuthSuccessHandler (AuthenticationSuccessHandler)
As you can see for this I am trying to work around the problem by saving a JPA entity based on the information given by discord and only pulling from the database as opposed to getting the current user, which prevents me from using roles and authorities.
#Component
#RequiredArgsConstructor
#Slf4j
public class AuthSuccessHandler implements AuthenticationSuccessHandler {
private final AccountRepository accountRepository;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
}
#Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
OAuth2User oAuth2User = ((OAuth2AuthenticationToken) authentication).getPrincipal();
// oAuth2User.getAttributes().forEach((s, o) -> {
// System.out.println(s + " : " + o);
// });
Long discordId = Long.parseLong(Objects.requireNonNull(oAuth2User.getAttribute("id")));
Optional<Account> accountOptional = accountRepository.findAccountByDiscordId(discordId);
if (!httpServletResponse.isCommitted()) {
if (accountOptional.isEmpty()) {
accountRepository.save(getAccountFromOAuth2User(oAuth2User));
}
redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, "/dashboard");
} else {
log.warn("Response committed");
}
}
private Account getAccountFromOAuth2User(OAuth2User oAuth2User) {
return new Account(Long.parseLong(oAuth2User.getAttribute("id")),oAuth2User.getAttribute("username"), oAuth2User.getAttribute("discriminator"));
}
}
application.properties
spring.security.oauth2.client.registration.discord.client-name=Discord
spring.security.oauth2.client.registration.discord.client-id=**CLIENT**
spring.security.oauth2.client.registration.discord.client-secret=**SECRET**
spring.security.oauth2.client.registration.discord.clientAuthenticationMethod=post
spring.security.oauth2.client.registration.discord.authorizationGrantType=authorization_code
spring.security.oauth2.client.registration.discord.redirect-uri=http://localhost:8081/login/oauth2/code/discord
spring.security.oauth2.client.registration.discord.scope=identify
spring.security.oauth2.client.provider.discord.authorization-uri=https://discordapp.com/api/oauth2/authorize
spring.security.oauth2.client.provider.discord.token-uri=https://discordapp.com/api/oauth2/token
spring.security.oauth2.client.provider.discord.user-info-uri=https://discordapp.com/api/users/#me
spring.security.oauth2.client.provider.discord.user-name-attribute=username
Any help on this would be appreciated.
Thanks in advance!
EDIT:
Most of the code used is from this GitHub repository:
https://github.com/Samurus/spring-boot-discord-oauth-example
I solved this by using CustomUserTypesOAuth2UserService which lets me use a custom class that implements OAuth2User
I'm implementing Spring Security on API-REST, of we are developing.
My product manager told us to use the same url to return data and to login.
This url, as a POST, and that wait a JSON.
I can't configure the environment for do that, is possible? or always we need have a different url to login?
Thank you!!
This is the JSON on JAVA
#Getter
#Setter
public class CheckCloudUsersJSON implements Serializable {
private String tvAdmin;
private String ip;
}
#PostMapping(value = "/check-cloudusers", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity checkCloudUsers(#RequestBody CheckCloudUsersJSON checkCloudUsersJSON) {
#EnableWebSecurity
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
JWTAuthenticationFilter authenticationFilter = new JWTAuthenticationFilter(authenticationManager());
authenticationFilter.setFilterProcessesUrl("/mobile/login");
httpSecurity
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, VERSION_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl(AUTH_LOGIN_URL);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
TVUser credenciales = new ObjectMapper().readValue(request.getInputStream(), TVUser.class);
return new UsernamePasswordAuthenticationToken(
credenciales.getTvUserId(), null);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
String userName = auth.getPrincipal().toString();
byte[] signingKey = SECRET_KEY.getBytes();
String token = Jwts.builder()
.signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512)
.setHeaderParam("typ", TOKEN_TYPE)
.setIssuer(TOKEN_ISSUER)
.setAudience(TOKEN_AUDIENCE)
.setSubject(userName)
.setExpiration(new Date(System.currentTimeMillis() + 900000))
.compact();
response.addHeader(TOKEN_HEADER, TOKEN_PREFIX + token);
}
}
Where does the user and password go to the authentication? In the JSON checkCloudUsers JSON?
And the endpoint will return a 200 Ok Code and the JWT Token in the authorization header.
Please elaborate from what you are trying to do, but anyways from what i understand ,since you are using spring security we need to authorise the login request and return the JSON response from the login webservice as a part of the response body.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
http
.authorizeRequests()
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated();
}
#PostMapping("/login")
public JsonResponse login() {
// Point to the login page
// Return the json in the response with a 200 OK
}
I am doing basic authentication in spring boot . Also i have a filter which does the header filtering and throws error if i dint pass required values in header . Authentication and header filtering are working fine if implemented separately. But if we implement both , i am getting the same response for both the validations ( filter and basic auth ). My guess is as filter response is generated first , it is getting replaced by the authentication response later.
PS: Used ** in code below to tell the issue location .
Any experts please advice . Thanks
#Slf4j
#Component
#Order(Ordered.HIGHEST_PRECEDENCE+2000)
#WebFilter
public class ValidTenantFilter extends OncePerRequestFilter {
#Autowired
private ClientRepository clientRepository;
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
AntPathMatcher pathMatcher = new AntPathMatcher();
return Constant.TENANT_FILTER_URL_LIST.stream()
.anyMatch(p -> pathMatcher.match(p, request.getServletPath()));
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("Inside Tenant Checker filter for path {} with method {} ",request.getServletPath(),request.getMethod());
if(!this.isValidTenant(request)) {
** response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid "+ Constant.X_COMPANY_ID+" and/or "+Constant.X_OPERATOR_ID+ " are passed. Please validate the request."); **
}
else {
filterChain.doFilter(request, response);
}
}
private boolean isValidTenant(HttpServletRequest request) {
// Getting company id and operator id from the header , earlier we were using the tenant id
String companyId = request.getHeader(Constant.X_COMPANY_ID);
String operatorId=request.getHeader(Constant.X_OPERATOR_ID);
if(StringUtils.isNotEmpty(companyId) && StringUtils.isAlphanumeric(companyId)
&& StringUtils.isNotEmpty(operatorId) && StringUtils.isAlphanumeric(operatorId)) {
Client client = clientRepository.findByIdAndOperatorId(companyId, operatorId);
//Only active clients request are entertained. // PRODUCT FIX
if(client!=null && client.getId()!=null && client.isActive()) {
MDC.put(MDC_CLIENT_ID, client.getId().toString());
TenantContext.setCurrentTenant(client.getId().toString());
return true;
}
}
return false;
}
}
And below is the code for the authentication part :
#Configuration
#EnableWebSecurity
#Slf4j
public class SomeConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationEntryPointImpl authEntryPoint;
#Autowired
private ApplicationClients application;
#Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
return new PropertySourcesPlaceholderConfigurer();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable()
.cors().disable();
//http.requiresChannel().antMatchers("/*").requiresSecure();
http.authorizeRequests()
// .antMatchers("/**").hasRole("ADMIN")
//.antMatchers("/user").hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and().httpBasic();
http.headers().defaultsDisabled().cacheControl().and().contentTypeOptions()
.and().frameOptions().deny().xssProtection().block(false)
.and().httpStrictTransportSecurity().includeSubDomains(true).maxAgeInSeconds(31536000);
// Use AuthenticationEntryPoint to authenticate user/password
http.httpBasic().authenticationEntryPoint(authEntryPoint);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources",
"/swagger-resources/configuration/**", "/swagger-ui.html", "/webjars/**");
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
final InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
log.info("Importing {} clients:", application.getClients().size());
application.getClients().forEach(client -> {
String encrytedPassword = this.passwordEncoder().encode(client.getPassword());
manager.createUser(User.withUsername(client.getUsername()).password(encrytedPassword).roles(client.getRoles()).build());
log.info("Imported client {}", client.toString());
});
return manager;
}
}
Code for authentication entry point :
#Component
public class AuthenticationEntryPointImpl extends BasicAuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
//This is invoked when a user tries to access a secured REST resource without supplying any credentials
//We should just add a 401 Unauthorized response because there is no 'login page' to redirect to
** response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{\"status\": " + HttpServletResponse.SC_UNAUTHORIZED + ", \"message\": \"" + authEx.getMessage() + "\" }");**
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("api-services");
super.afterPropertiesSet();
}
}
Getting below Response in POSTMAN :
1. If i dont pass any credentials in postman i am getting below response
{
"status": 401,
"message": "Full authentication is required to access this resource"
}
If i dont pass the headers , ideally i should get below response :
{
"timestamp": 1557753285553,
"status": 403,
"error": "Forbidden",
"message": "Invalid X-COMPANY-ID and/or X-OPERATOR-ID are passed. Please validate the request.",
"path": "/apa/invoices"
}
But instead of this i am getting below error :
{
"status": 401,
"message": "Full authentication is required to access this resource"
}
I have the simplest oauth2 client:
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#RestController
public class ClientApplication {
#RequestMapping("/")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response) throws ServletException {
return "Hello " + user.getName();
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
I also have the following application.yml:
server:
port: 9999
servlet:
context-path: /client
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
user-info-uri: http://localhost:8080/me
logging:
level:
org.springframework.security: DEBUG
org.springframework.web: DEBUG
It is the full code. I don't have any additional source code. It works properly.
But now I want to add a logout feature. I've added an endpoint but it doesn't work. I tried to do the following:
#RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.setAuthenticated(false);
new SecurityContextLogoutHandler().logout(request,response,authentication);
SecurityContextHolder.clearContext();
request.logout();
request.getSession().invalidate();
}
But I am still logged in and can access / url and it responds to me with the username.
Can you help me fix this issue?
Update
I tried the approach described here https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_logout :
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#Controller
public class ClientApplication extends WebSecurityConfigurerAdapter {
private Logger logger = LoggerFactory.getLogger(ClientApplication.class);
#RequestMapping("/hello")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response, Model model) throws ServletException {
model.addAttribute("name", user.getName());
return "hello";
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
and on FE I wrote:
<script type="text/javascript">
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/
.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
var logout = function () {
$.post("/client/logout", function () {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
};
$(function() {
$("#logoutButton").on("click", function () {
logout();
});
});
</script>
and
<input type="button" id="logoutButton" value="Logout"/>
But it still doesn't work. It results in the following behavior:
Post http://localhost:9999/client/logout redirects to the http://localhost:9999/client but this page doesn't exist
source code on gitub:
client - https://github.com/gredwhite/logour_social-auth-client (use localhost:9999/client/hello url)
server - https://github.com/gredwhite/logout_social-auth-server
You can delete the refresh token as well as access token from database to save space.
#PostMapping("/oauth/logout")
public ResponseEntity<String> revoke(HttpServletRequest request) {
try {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenValue = authorization.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
//OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(tokenValue);
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid access token");
}
return ResponseEntity.ok().body("Access token invalidated successfully");
}
The URL to logout will be : http://localhost:9999/oauth/logout
Also, pass the access token in the Authorization header, as
Authorization: Bearer 0cb72897-c4f7-4f01-aed9-2f3f79a75484
where, 0cb72897-c4f7-4f01-aed9-2f3f79a75484 is the access token.
Since, its Spring security, don't forget to bypass /oauth/logout url from authorize access, as
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello", "/oauth/logout");
}
Hope, it will solve your logout problem in Springboot2+Oauth2. Its working for me.
Add following code snippet to your ClientApplication class. This will also clear your session details.
Replace below code with the configure method of your web security adapter class.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().invalidateHttpSession(true)
.clearAuthentication(true).logoutSuccessUrl("/login?logout").deleteCookies("JSESSIONID").permitAll().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
You probably want to use the Spring Security built-in support for the /logout endpoint which will do the right thing (clear the session and invalidate the cookie). To configure the endpoint extend the existing configure() method in our WebSecurityConfigurer:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.and().logout().logoutSuccessUrl("/").permitAll();
}
You can change Post to
Get http://localhost:9999/client/logout
it works for me
Try to add logout url to your security configuration.
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll();
I am developing a web service including three web servers with spring boot:
1. localhost:8080 - ui
2. localhost:9999 - uaa (authserver)
3. localhost:9000 - ressource
I took this example repository https://github.com/spring-guides/tut-spring-security-and-angular-js/tree/master/oauth2 and modified it with a UserDetailsService and a custom authenticationSuccessHandler / authenticationFailureHandler for the login process.
I would like to have my login page at the localhost:8080 and make a ajax / angularjs request to the localhost:9999 to login. This works fine but I do not get the jsessionid issued by the uaa server in the response header and therefore I can not make any secured requests.
How do I have to modify my authserver to send the jsessionid as a cookie back to my ui server?
This is the response header, if I would have the template on the authserver like the example repository and make the request:
And this is the response header, if I make the request by the localhost:8080 to the authserver:
Source Code:
-Authserver (localhost:9999)
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// #formatter:off
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter());
// #formatter:on
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// #formatter:off
clients.inMemory()
.withClient("acme").secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.scopes("openid")
.autoApprove(true);
// #formatter:on
}
}
#Configuration
#EnableWebSecurity
#Order(-20)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
public WebSecurityConfig(){
super();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/signup", "/forgotPassword", "/user/updatePassword**", "/user/registration", "/user/registrationConfirm**", "/user/resendRegistrationToken**", "/user/resetPassword**", "/user/changePassword**", "/user/savePassword").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.formLogin().loginPage("/login").successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).permitAll()
.and()
.requestMatchers().antMatchers("/login**", "/signup", "/forgotPassword", "/user/updatePassword**", "/user/registration", "/user/registrationConfirm**", "/user/resendRegistrationToken**", "/user/resetPassword**", "/user/changePassword**", "/user/savePassword", "/oauth/authorize", "/oauth/confirm_access");
// #formatter:on
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
//Beans
#Bean
public DaoAuthenticationProvider authProvider() {
final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(11);
}
}
#Component
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
clearAuthenticationAttributes(request);
}
}
#Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
super.onAuthenticationFailure(request, response, exception);
}
}
#Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
-ui-Server (localhost:8080)
#Configuration
#Order(-20)
#EnableZuulProxy
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/").deleteCookies("XSRF-TOKEN").deleteCookies("JSESSIONID").invalidateHttpSession(true)
.and()
.antMatcher("/**").authorizeRequests()
.antMatchers(
"/",
"/signin",
"/uaa/login",
"/pix/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
//#formatter:on
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
application.yml
server:
port: 8080
debug: true
spring:
aop:
proxy-target-class: true
security:
user:
password: none
oauth2:
client:
accessTokenUri: http://localhost:9999/uaa/oauth/token
userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
clientId: acme
clientSecret: acmesecret
resource:
jwt:
keyValue: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGp/Q5lh0P8nPL21oMMrt2RrkT9AW5jgYwLfSUnJVc9G6uR3cXRRDCjHqWU5WYwivcF180A6CWp/ireQFFBNowgc5XaA0kPpzEtgsA5YsNX7iSnUibB004iBTfU9hZ2Rbsc8cWqynT0RyN4TP1RYVSeVKvMQk4GT1r7JCEC+TNu1ELmbNwMQyzKjsfBXyIOCFU/E94ktvsTZUHF4Oq44DBylCDsS1k7/sfZC2G5EU7Oz0mhG8+Uz6MSEQHtoIi6mc8u64Rwi3Z3tscuWG2ShtsUFuNSAFNkY7LkLn+/hxLCu2bNISMaESa8dG22CIMuIeRLVcAmEWEWH5EEforTg+QIDAQAB
-----END PUBLIC KEY-----
zuul:
routes:
resource:
path: /resource/**
url: http://localhost:9000/resource
user:
path: /uaa/**
url: http://localhost:9999/uaa
logging:
level:
org.springframework.security: DEBUG
signin.js
'use strict';
angular.module('loginUser').controller('LoginViewController', function($scope, $http) {
$scope.login = function() {
$http({
method: 'POST',
url: '/uaa/login',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
transformRequest: function(obj) {
var str = [];
for(var p in obj)
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
return str.join("&");
},
data: {"username": $scope.user.username, "password": $scope.user.password}
})
.success(function (data) {
console.log(data);
})
.error(function(data, status) {
console.log(data);
console.log(status);
});
}
});
angular.module('loginUser').run(function run($http, $cookies){
$http.defaults.headers.post['X-XSRF-TOKEN'] = $cookies['XSRF-TOKEN'];
$http.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
});