I'm trying to get the the Principal user name from Spring websocket SessionConnectEvent but it is null on every listener. What I can be doing wrong?
To implement it I followed the answers you will find here: how to capture connection event in my webSocket server with Spring 4?
#Slf4j
#Service
public class SessionEventListener {
#EventListener
private void handleSessionConnect(SessionConnectEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
String sessionId = headers.getSessionId();
log.debug("sessionId is " + sessionId);
String username = headers.getUser().getName(); // headers.getUser() is null
log.debug("username is " + username);
}
#EventListener
private void handleSessionConnected(SessionConnectEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
String sessionId = headers.getSessionId();
log.debug("sessionId is " + sessionId);
String username = headers.getUser().getName(); // headers.getUser() is null
log.debug("username is " + username);
}
#EventListener
private void handleSubscribeEvent(SessionSubscribeEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
String sessionId = headers.getSessionId();
log.debug("sessionId is " + sessionId);
String subscriptionId = headers.getSubscriptionId();
log.debug("subscriptionId is " + subscriptionId);
String username = headers.getUser().getName(); // headers.getUser() is null
log.debug("username is " + username);
}
#EventListener
private void handleUnsubscribeEvent(SessionUnsubscribeEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
String sessionId = headers.getSessionId();
log.debug("sessionId is " + sessionId);
String subscriptionId = headers.getSubscriptionId();
log.debug("subscriptionId is " + subscriptionId);
String username = headers.getUser().getName(); // headers.getUser() is null
log.debug("username is " + username);
}
#EventListener
private void handleSessionDisconnect(SessionDisconnectEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
log.debug("sessionId is " + event.getSessionId());
String username = headers.getUser().getName(); // headers.getUser() is null
log.debug("username is " + username);
}
}
This is my security config:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.permitAll()
.and().csrf().disable();
}
}
As I'm not implementing an authentication mechanisim, Spring has no enough information to provide a Principal user name. So what I had to do is to configure a HandshakeHandler that generates the Principal.
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public static final String ENDPOINT_CONNECT = "/connect";
public static final String SUBSCRIBE_USER_PREFIX = "/private";
public static final String SUBSCRIBE_USER_REPLY = "/reply";
public static final String SUBSCRIBE_QUEUE = "/queue";
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker(SUBSCRIBE_QUEUE, SUBSCRIBE_USER_REPLY);
registry.setUserDestinationPrefix(SUBSCRIBE_USER_PREFIX);
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(ENDPOINT_CONNECT)
// assign a random username as principal for each websocket client
// this is needed to be able to communicate with a specific client
.setHandshakeHandler(new AssignPrincipalHandshakeHandler())
.setAllowedOrigins("*");
}
}
/**
* Assign a random username as principal for each websocket client. This is
* needed to be able to communicate with a specific client.
*/
public class AssignPrincipalHandshakeHandler extends DefaultHandshakeHandler {
private static final String ATTR_PRINCIPAL = "__principal__";
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
final String name;
if (!attributes.containsKey(ATTR_PRINCIPAL)) {
name = generateRandomUsername();
attributes.put(ATTR_PRINCIPAL, name);
} else {
name = (String) attributes.get(ATTR_PRINCIPAL);
}
return new Principal() {
#Override
public String getName() {
return name;
}
};
}
private String generateRandomUsername() {
RandomStringGenerator randomStringGenerator =
new RandomStringGenerator.Builder()
.withinRange('0', 'z')
.filteredBy(CharacterPredicates.LETTERS, CharacterPredicates.DIGITS).build();
return randomStringGenerator.generate(32);
}
}
Looking into the implementation of AbstractSubProtocolEvent (the superclass of all the events you're interested in) you can see that the user is hold in a seperate field. So you can simply access the user by calling event.getUser(). You don't need to get it from the message.
E.g. for the SessionConnectedEvent you can see that the user gets populated in the event but not the message.
Update:
You can only access the user when you authenticated the http upgrade. So you need to have a WebSecurityConfigurerAdapter that configures something like:
#Configuration
public static class UserWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers(WebsocketPaths.WEBSOCKET_HANDSHAKE_PREFIX); //You configured the path in WebSocketMessageBrokerConfigurer#registerStompEndpoints
http
.authorizeRequests()
.anyRequest().authenticated();
}
}
Related
In a rest API, i implemented 2 event listners to handle Authentication success and failure. It works fine and I do have a 403 error but i want to return a JSON Message.
For my login I implemented, the following :
#PostMapping("/login")
public ResponseEntity<UserResponse> loadUserByUsername(#RequestBody UserDetailsRequestModel userDetails) {
if(userDetails.getEmail().isEmpty() || userDetails.getPassword().isEmpty()) {
throw new UserServiceException(ErrorMessages.MISSING_REQUIRED_FIELD.getErrorMessage());
}
authenticate(userDetails.getEmail(), userDetails.getPassword());
UserResponse userRestResponseModel = new UserResponse();
ModelMapper modelMapper = new CustomMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STANDARD);
UserDto loggedInUser = userService.getUser(userDetails.getEmail());
userRestResponseModel = modelMapper.map(loggedInUser, UserResponse.class);
// retrieve authorities manually
for(RoleDto roleDto: loggedInUser.getRoles()) {
Collection<AuthorityDto> authorityDtos = authorityService.getRoleAuthorities(roleDto);
roleDto.setAuthorities(authorityDtos);
}
UserPrincipalManager userPrincipal = new UserPrincipalManager(modelMapper.map(loggedInUser, UserEntity.class));
// authorities are not fetched ... so we'll fetch them manually
HttpHeaders jwtHeader = getJwtHeader(userPrincipal);
ResponseEntity<UserResponse> returnValue =
new ResponseEntity<>(userRestResponseModel, jwtHeader, HttpStatus.OK);
return returnValue;
}
private void authenticate(String userName, String password) {
AuthenticationManager authenticationManager =
(AuthenticationManager) SpringApplicationContext.getBean("authenticationManager");
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
}
private HttpHeaders getJwtHeader(UserPrincipalManager userPrincipal) {
HttpHeaders headers = new HttpHeaders();
String token = jwtTokenProvider.generateJwtToken(userPrincipal);
headers.add(SecurityConstants.TOKEN_PREFIX, token);
return headers;
}
#Component
public class AuthenticationFailureListener {
private final LoginAttemptService loginAttemptService;
#Autowired
public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) throws ExecutionException {
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.addUserToLoginAttemptCache(username);
}
}
}
In my loginAttemptService I try to prepare a return to a rest response.
#Override
public void addUserToLoginAttemptCache(String username) {
int attempts = 0;
try {
attempts = SecurityConstants.AUTH_ATTEMPT_INCREMENT + loginAttemptCache.get(username);
loginAttemptCache.put(username, attempts);
String message = "";
if(!errorContext.isHasExceededMaxAttempts()) {
message = "Invalid email or password. You tried : " + attempts + "/" + SecurityConstants.MAX_AUTH_ATTEMPTS;
} else {
message = "You reached " + attempts + " attempts. Account is now locked for " + SecurityConstants.LOCK_DURATION + " min";
}
throw new SecurityServiceException(message);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
My issue is the following: using ControllerAdvice won't work because the error is handled before it could reach it. How can I then return a JSON response to the client ?
I did find a trick for this issue. I created a ManagedBean class
#Data
#Builder
#AllArgsConstructor #NoArgsConstructor
#ManagedBean #ApplicationScope
public class ServletContext {
private HttpServletRequest request;
private HttpServletResponse response;
}
I inject it in my AuthenticationFilter custom class. Here in my attemptAuthentication method I can get access to HttpServletRequest and HttpServletResponse objects. I just have to set my ServletContext object with the request and the response.
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// we may need to pass request and response object if we fail authentication,
servletContext.setRequest(request);
servletContext.setResponse(response);
// spring tries to authenticate user
try {
UserLoginRequestModel creds = new ObjectMapper()
.readValue(request.getInputStream(), UserLoginRequestModel.class);
// we return authentication with email and password
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
new ArrayList<>()
)
);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
Now in my AuthenticationFailureListener, I also inject my ServletContext class and retrieve the values in the method that handle onAuthenticationFailure:
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) throws ExecutionException, IOException {
System.out.println(event);
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.addUserToLoginAttemptCache(username);
int attempts = loginAttemptService.getLoginAttempts(username);
String message;
if(!loginAttemptService.hasExceededMaxAttempts(username)) {
message = "Invalid email or password. You tried : " + attempts + "/" + SecurityConstants.MAX_AUTH_ATTEMPTS;
} else {
message = "You reached " + attempts + " attempts. Account is now locked for " + SecurityConstants.LOCK_DURATION + " min";
}
ErrorMessageResponse errorMessageResponse = new ErrorMessageResponse(new Date(), message);
HttpServletResponse response = servletContext.getResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), errorMessageResponse);
}
}
At this stage, I do have HttpServletResponse object and I can use it to write value. I do believe there may be more elegant ways to handle this, but it works fine.
You can use .accessDeniedHandler at your HttpSecurity in you Security Config.
Below Simple way de return JSON For 403 error :
Define a private method in Config Security like this :
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
.....
private void writeResponse(HttpServletResponse httpResponse, int code, String message) throws IOException {
httpResponse.setContentType("application/json");
httpResponse.setStatus(code);
httpResponse.getOutputStream().write(("{\"code\":" + code + ",").getBytes());
httpResponse.getOutputStream().write(("\"message\":\"" + message + "\"}").getBytes());
httpResponse.getOutputStream().flush();
}
}
Add exceptionHandling at your HttpSecurity Config :
#Override
protected void configure(HttpSecurity http) throws Exception {
....
http = http.exceptionHandling()
.accessDeniedHandler((request, response, accessDeniedException) -> {
this.writeResponse(response,HttpServletResponse.SC_FORBIDDEN,accessDeniedException.getMessage());
}
)
.and();
....
}
I am trying to obtain the currently authenticated user in the controller for websockets. The problem is, I cannot access the user using SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId().
I have tried to give Principal as a parameter to the method but it returns principal null.
Security configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connect").setAllowedOrigins("*");
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic/messages");
registry.setApplicationDestinationPrefixes("/ws");
}
}
Controller for websocket:
#Controller
public class MessageController {
#Autowired
private Consumer consumer;
#Autowired
private Utils utils;
#Autowired
private PersonService personService;
#Autowired
SimpMessagingTemplate simpMessagingTemplate;
String destination = "/topic/messages";
ExecutorService executorService =
Executors.newFixedThreadPool(1);
Future<?> submittedTask;
#MessageMapping("/start")
public void startTask(Principal principal){
// Here, I would like to get the logged in user
// If I use principal like this: principal.getName() => NullPointerException
if ( submittedTask != null ){
simpMessagingTemplate.convertAndSend(destination,
"Task already started");
return;
}
simpMessagingTemplate.convertAndSendToUser(sha.getUser().getName(), destination,
"Started task");
submittedTask = executorService.submit(() -> {
while(true){
simpMessagingTemplate.convertAndSend(destination,
// "The calculated value " + val + " is equal to : " + max);
}
});
}
How can I get the authenticated user? I needed it to check when to start the task for the web socket
Try to implement ChannelInterceptor, that need to be registrated in Config file (class that implements WebSocketMessageBrokerConfigurer)
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ChannelInterceptor serverPushInBoundInterceptor;
#Autowired
public WebSocketConfig(#Qualifier("serverPushInBoundInterceptor") ChannelInterceptor serverPushInBoundInterceptor) {
this.serverPushInBoundInterceptor = serverPushInBoundInterceptor;
}
....
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(serverPushInBoundInterceptor);
}
}
#Component("serverPushInBoundInterceptor")
public class ServerPushInBoundInterceptor implements ChannelInterceptor {
private static final Logger log = LoggerFactory.getLogger(ServerPushInBoundInterceptor.class);
#Override
#SuppressWarnings("NullableProblems")
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(Objects.requireNonNull(accessor).getCommand())) {
List<String> authorization = accessor.getNativeHeader("Authorization");
if (authorization != null && !authorization.isEmpty()) {
String auth = authorization.get(0).split(" ")[1];
System.out.println(auth);
try {
// find Principal
Principal principal = ...
accessor.setUser(new UsernamePasswordAuthenticationToken(principal, principal.getCredentials(), principal.getAuthorities()));
} catch (Exception exc) {
log.error("preSend", exc);
}
}
}
return message;
}
}
I have a spring application.
I need to put a value to the initial handshake.
The url looks like: ws://localhost:8080/chat?key=value
I need this key=value in my Websocket Handler.
How can I access it?
Websocket Configuration:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// alle origins erlauben
registry.addHandler(chatWebSocketController(), "/chat").addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*");
}
#Bean
public ChatWebSocketController chatWebSocketController() {
return new ChatWebSocketController();
}
}
Websocket Handler method:
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
if (session.getAttributes().containsKey("key")) {
List<String> userMap = session.getHandshakeHeaders().get("key");
JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();
String token = userMap.get(0);
if (jwtTokenUtil.validateToken(token)) {
User userToStore = new User(jwtTokenUtil.getUsernameFromToken(token));
userUsernameMap.put(session, userToStore);
LOGGER.info("User with name " + jwtTokenUtil.getUsernameFromToken(token) + "and IP "
+ session.getRemoteAddress() + " successfully connected");
sendConnectMessage(session, userToStore);
}
} else {
session.close(CloseStatus.POLICY_VIOLATION);
}
}
Found the solution by myself. You have to write your own HandshakeInterceptor, there you have access to the http parameter. so you can put this to your attribbutes map.
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", session.getId());
attributes.put("key", servletRequest.getServletRequest().getParameterMap().get("key"));
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
//nothing to do
}
}
I'am trying to implement a Spring Security LDAP authentication using WebSecurityConfigurerAdapter.
So far it works fine, but the problem in my case is that I don't want the username and password of context to be hard coded. It must be the login and password of the user, so my question is how can I build the context and setting of the username and password from the login form?
This is the code I'm working with:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.formLogin();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchFilter("(sAMAccountName={0})")
.contextSource(contextSource());
}
#Bean
public BaseLdapPathContextSource contextSource() {
LdapContextSource bean = new LdapContextSource();
bean.setUrl("ldap://10.10.10.10:389");
bean.setBase("DC=myDomaine,DC=com");
//instead of this i want to put here the username and password provided by the user
bean.setUserDn("myDomaine\\username");
bean.setPassword("password");
bean.setPooled(true);
bean.setReferral("follow");
bean.afterPropertiesSet();
return bean;
}
}
Thank you!
Save yourself time by not rewriting your own LdapAuthenticationProvider, there is an existing ActiveDirectoryLdapAuthenticationProvider that will use the received credentials for authentication to LDAP and you can also add a searchFilter if you want to do more (e.g. see if user also belongs to a specific group)
Related docs:
https://docs.spring.io/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ldap-active-directory
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.html
Sample snippet:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private LdapProperties ldapProperties;
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider authenticationProvider =
new ActiveDirectoryLdapAuthenticationProvider(ldapProperties.getDomain(), ldapProperties.getProviderUrl());
authenticationProvider.setConvertSubErrorCodesToExceptions(true);
authenticationProvider.setUseAuthenticationRequestCredentials(true);
//if you're not happy on the default searchFilter, you can set your own. See https://docs.spring.io/spring-security/site/docs/4.2.18.RELEASE/apidocs/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.html#setSearchFilter-java.lang.String-
authenticationProvider.setSearchFilter("(&(objectClass=user)(cn={1}))");
return authenticationProvider;
}
...
}
Your code should work perfectly fine. The hardcoded username and password is used only to create a bind with the ldap server. The username and password provided in login form is only being authenticated using your code.
I use the following code to perform ldap authentication.
public void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userSearchFilter("sAMAccountName={0}").contextSource().url(this.ldapUrl).managerDn(this.managerDn)
.managerPassword(this.managerPassword);
}
Where the manager is the ldap account used to create a bind with the server.
the userDN and password parameter in contextSource is a required parameter. It is like admin username and password for you to be able to acquire or create initial connection to the ldap server.
For you to be able to authenticate the username and password from login form. You can use the ldapTemplate:
#Bean
public BaseLdapPathContextSource contextSource() {
LdapContextSource bean = new LdapContextSource();
bean.setUrl("ldap://10.10.10.10:389");
bean.setBase("DC=myDomaine,DC=com");
//instead of this i want to put here the username and password provided by the user
bean.setUserDn("myDomaine\\username");
bean.setPassword("password");
bean.setPooled(true);
bean.setReferral("follow");
bean.afterPropertiesSet();
return bean;
}
#Bean
public LdapTemplate ldapTemplate() {
LdapTemplate template = new LdapTemplate(contextSource());
return template;
}
Then use this in your service class implementation:
#Service
public class LdapUserServiceImpl implements LdapUserService, BaseLdapNameAware {
#Autowired
protected ContextSource contextSource;
#Autowired
protected LdapTemplate ldapTemplate;
#Override
public boolean authenticate(String userDn, String credentials) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("sAMAccountName", userDn));
return ldapTemplate.authenticate("", filter.toString(), credentials);
}
}
Then call this service passing the username and password from login form like this:
boolean isAuthenticated = ldapUserService.authenticate(loginForm.getUsername(), loginForm.getPassword());
You can use a customized Authentication Provider to do the authentication.
in your security Configuration, you can auto wire a CustomAuthenticationProvider, which can get the username and password from login form:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
#Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(customAuthProvider);
}
}
And Now just implement the CustomAuthenticationProvider
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOG = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
#Value("${ldap.host}")
String ldapHost;
#Value("${ldap.port}")
String ldapPort;
#Value("${ldap.base-dn}")
String baseDomainName;
#Value("${ldap.domain-prefix}")
String domainPrefix;
#Value("${ldap.read.timeout}")
String timeout;
private static final String DEFAULT_JNDI_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//user and password are from user's input from login form...
String user= authentication.getName();
String password = authentication.getCredentials().toString();
try {
Set<String> roles = authenticate(user, password);
if (CollectionUtils.isEmpty(roles))
return null;
List<GrantedAuthority> authorityList = new ArrayList<>();
for(String role: roles){
authorityList.add(new SimpleGrantedAuthority(role));
}
return new UsernamePasswordAuthenticationToken(user, password, authorityList);
} catch (NamingException ex) {
LOG.info("Naming Exception",ex);
}
return null;
}
public Set<String> authenticate(final String username, final String password) throws NamingException {
InitialLdapContext ctx = null;
NamingEnumeration<SearchResult> results = null;
try {
final Hashtable<String, String> ldapEnvironment = new Hashtable<>();
ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY, DEFAULT_JNDI_CONTEXT_FACTORY);
ldapEnvironment.put(Context.PROVIDER_URL, "ldap://" + ldapHost + ":" + ldapPort + "/" + baseDomainName);
ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password);
ldapEnvironment.put(Context.SECURITY_PRINCIPAL, domainPrefix + '\\' + username);
ldapEnvironment.put("com.sun.jndi.ldap.read.timeout", timeout);
ldapEnvironment.put("com.sun.jndi.ldap.connect.timeout", timeout);
ctx = new InitialLdapContext(ldapEnvironment, null);
final SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
constraints.setReturningAttributes(new String[]{"memberOf"});
constraints.setReturningObjFlag(true);
results = ctx.search("", "(sAMAccountName=" + username + ")", constraints);
if (!results.hasMore()) {
LOG.warn(".authenticate(" + ldapHost + "," + username + "): unable to locate " + username);
return null;
}
final Set<String> adGroups = new TreeSet<>();
final SearchResult entry = results.next();
for (NamingEnumeration valEnum = entry.getAttributes().get("memberOf").getAll(); valEnum.hasMoreElements(); ) {
String dn = (String) valEnum.nextElement();
int i = dn.indexOf(",");
if (i != -1) {
dn = dn.substring(0, i);
}
if (dn.startsWith("CN=")) {
dn = dn.substring("CN=".length());
}
adGroups.add(dn);
}
return adGroups;
}
finally {
try {
if (null != results)
results.close();
}
catch (Throwable ignored) {
}
try {
if (null != ctx)
ctx.close();
}
catch (Throwable ignored) {
}
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
I'm using spring boot and i need to implement spring security with 3 fields authentication process username, password and corporate identifier as a hidden input in a form.
I implemented a custom usernamepasswordauthenticationfilter but it not seems to be enough to setup the security config.
EDIT :
Users don't seem to be authenticated ! because a can access to authenticated request defined in web config
EDIT 2 :
in my custom filter when a enter a valid user it's do execute on succesfulAuthentication. What i'm missing please provide me any help :(
Here were i am
#Repository
public class AuthenticationUserDetailsService implements UserDetailsService {
private static final Logger LOGGER = Logger.getLogger(AuthenticationUserDetailsService.class);
#Autowired
private UserRepository users;
private org.springframework.security.core.userdetails.User userdetails;
#Override
public UserDetails loadUserByUsername(String input) throws UsernameNotFoundException {
// TODO Auto-generated method stub
System.out.println(input);
String[] split = input.split(":");
if (split.length < 2) {
LOGGER.debug("User did not enter both username and corporate domain.");
throw new UsernameNotFoundException("no corporate identifier is specified");
}
String username = split[0];
String corporateId = split[1];
System.out.println("Username = " + username);
System.out.println("Corporate identifier = " + corporateId);
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
com.ubleam.corporate.server.model.User user;
user = checkUserDetail(username, corporateId);
if (user == null)
throw new NotAuthorizedException("Your are not allowed to access to this resource");
LOGGER.info("User email : " + user.getEmail() + "#User corporate : " + user.getCorporateId());
userdetails = new User(user.getEmail(), user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities("ROLE_USER"));
return userdetails;
}
/**
*
* #param roles
* roles granted for user
* #return List of granted authorities
*
*/
public List<GrantedAuthority> getAuthorities(String roles) {
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
authList.add(new SimpleGrantedAuthority(roles));
return authList;
}
/**
* User authentication details from database
*
* #param username
* to use for authentication
* #param coporateId
* corporate identifier of user
* #return found user in database
*/
private com.ubleam.corporate.server.model.User checkUserDetail(String username, String corporateId) {
com.ubleam.corporate.server.model.User user = users.findByEmailAndCorporateId(username, corporateId);
return user;
}
My custom filter :
public class PlatformAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger LOGGER = Logger.getLogger(PlatformAuthenticationFilter.class);
private static final String LOGIN_SUCCESS_URL = "{0}/bleamcards/{1}/home";
private static final String LOGIN_ERROR_URL = "{0}/bleamcards/{1}/login?error";
private String parameter = "corporateId";
private String delimiter = ":";
private String corporateId;
#Override
protected String obtainUsername(HttpServletRequest request) {
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getParameter());
String combinedUsername = username + getDelimiter() + extraInput;
setCorporateId(extraInput);
LOGGER.info("Combined username = " + combinedUsername);
return combinedUsername;
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_SUCCESS_URL, contextPath, corporateId);
response.sendRedirect(url);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_ERROR_URL, contextPath, corporateId);
response.sendRedirect(url);
}
public String getParameter() {
return parameter;
}
public void setParameter(String corporateId) {
this.parameter = corporateId;
}
public String getDelimiter() {
return delimiter;
}
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
public String getCorporateId() {
return corporateId;
}
public void setCorporateId(String corporateId) {
this.corporateId = corporateId;
}
}
And finally the web security config :
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Inject
private AuthenticationManagerBuilder auth;
#Inject
private UserDetailsService userDS;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/bleamcards/**/login", "/bleamcards/**/forgetpassword", "/bleamcards/**/register", "/css/**", "/js/**", "/images/**", "/webjars/**")
.permitAll().anyRequest().authenticated().and().addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class).formLogin().loginPage("/login")
.defaultSuccessUrl("/").permitAll().and().logout().permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false);
auth.userDetailsService(userDS).passwordEncoder(new BCryptPasswordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManager() throws Exception {
return auth.build();
}
#Bean
public PlatformAuthenticationFilter authenticationFilter() throws Exception {
PlatformAuthenticationFilter authFilter = new PlatformAuthenticationFilter();
authFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
authFilter.setAuthenticationManager(authenticationManager());
authFilter.setUsernameParameter("username");
authFilter.setPasswordParameter("password");
authFilter.setParameter("corporateId");
return authFilter;
}
#Override
protected UserDetailsService userDetailsService() {
return userDS;
}
I want users to be able to connect only to /login /register /forgetpasswod urls for their respective corporate platforms
Actually i manage to find a solution to my issue.
I added successHandler on successfulAuthentication was missing ! And a failureHandler too on unsuccessfulAuthentication methods.
Here is my new Authentication filter :
public class TwoFactorAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final String LOGIN_SUCCESS_URL = "{0}/bleamcards/{1}/home";
private static final String LOGIN_ERROR_URL = "{0}/bleamcards/{1}/login?error";
private String parameter = "corporateId";
private String delimiter = ":";
private String corporateId;
#Override
protected String obtainUsername(HttpServletRequest request) {
String username = request.getParameter(getUsernameParameter());
String extraInput = request.getParameter(getParameter());
String combinedUsername = username + getDelimiter() + extraInput;
setCorporateId(extraInput);
System.out.println("Combined username = " + combinedUsername);
return combinedUsername;
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain , Authentication authResult) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_SUCCESS_URL, contextPath, corporateId);
setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler(url));
super.successfulAuthentication(request, response, chain, authResult);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String contextPath = request.getContextPath();
String url = MessageFormat.format(LOGIN_ERROR_URL, contextPath, corporateId);
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(url));
super.unsuccessfulAuthentication(request, response, failed);
}
public String getParameter() {
return parameter;
}
public void setParameter(String corporateId) {
this.parameter = corporateId;
}
public String getDelimiter() {
return delimiter;
}
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
public String getCorporateId() {
return corporateId;
}
public void setCorporateId(String corporateId) {
this.corporateId = corporateId;
}
}
Did you check that your AuthenticationUserDetailsService code is actually been executed? If the framework is not invoking it this means that your configuration is not properly hooking that UserDetailsService. In your WebSecurityConfig I think you need to have this:
#Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean(); //not return auth.build();
}
I suggest you to take a look at this branch from Stormpath. There, they are configuring Spring Boot to use a custom AuthenticationProvider (similar to an UserDetailsService). That module uses and depends on this other Spring Security module.
Then, this sample Spring Security Example (note that it is not Spring Boot, but just Spring) will give you a complete example of the way the Spring Security Java Config is done. Please note that this Java Config extends this one which actually hides much of the actual internal configuration.
Disclaimer, I am an active Stormpath contributor.