Customise oath2 token request to accept extra data - spring-security

I am using jersey and spring-oauth2 with spring security. My app is working fine with end points "/oauth/token".
I want to change the endpoints to accept more data. The requirement is, I want to send more details to the token API (i.e. the device details OS, phone/tablet/web etc.). So, I want to override the endpoint and if authentication is successful, I want to store that extra information in database.
I could not find anything related to changing the API in such a way.
Can someone help?

I have found a solution by writing a wrapper controller and assigning default tokenEndpoint bean
#FrameworkEndpoint
public class LoginContrller{
private static Logger logger = org.slf4j.LoggerFactory.getLogger(LoginContrller.class);
private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
#Autowired
private UserManager userManager;
#Autowired
TokenEndpoint tokenEndPoint;
#RequestMapping(value = "/user/login", method=RequestMethod.POST,consumes=MediaType.APPLICATION_JSON)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, #RequestParam
Map<String, String> parameters,#RequestBody(required=false) LoginModel loginModel) throws HttpRequestMethodNotSupportedException {
ResponseEntity<OAuth2AccessToken> response = tokenEndPoint.postAccessToken(principal, parameters);
if(!isRefreshTokenRequest(parameters)){
if(loginModel!=null){
loginModel.setUsername(parameters.get("username"));
try {
userManager.loginUser(loginModel);
} catch (UserNotFoundException e) {
logger.warn("Exception in custom login {} ",e);
}
}
}
return response;
}
private boolean isRefreshTokenRequest(Map<String, String> parameters) {
return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
}
private boolean isAuthCodeRequest(Map<String, String> parameters) {
return "authorization_code".equals(parameters.get("grant_type")) && parameters.get("code") != null;
}
#ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public void handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
throw e;
}
#ExceptionHandler(Exception.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
return getExceptionTranslator().translate(e);
}
#ExceptionHandler(ClientRegistrationException.class)
public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
return getExceptionTranslator().translate(new BadClientCredentialsException());
}
#ExceptionHandler(OAuth2Exception.class)
public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
return getExceptionTranslator().translate(e);
}
private WebResponseExceptionTranslator getExceptionTranslator() {
return providerExceptionHandler;
}
}
Change in web.xml : just replace the URL with new one
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/user/login</url-pattern>
</servlet-mapping>
And finally create bean with logincontroller class and change the URL in spring-security.xml.
Change the oauth token url and url of clientCredentialsTokenEndpointFilter as mentioned below.
<sec:http pattern="/user/login" create-session="stateless" authentication-manager-ref="clientAuthenticationManager" use-expressions="true" >
<sec:intercept-url pattern="/user/login" access="isFullyAuthenticated()"/>
<sec:csrf disabled="true"/>
<sec:anonymous enabled="false" />
<sec:http-basic entry-point-ref="clientAuthenticationEntryPoint" />
<sec:custom-filter ref="clientCredentialsTokenEndpointFilter" after="BASIC_AUTH_FILTER" />
</sec:http>
<bean id="clientCredentialsTokenEndpointFilter" class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
<constructor-arg value="/user/login"></constructor-arg>
<property name="authenticationManager" ref="clientAuthenticationManager" />
<property name="filterProcessesUrl" value="/user/login" />
</bean>
<bean class="com.oauth2.provider.endpoint.LoginContrller" />

Related

Accessing CAS Released Attributes Using Spring Security

I'm having difficulty figuring out just how exactly one would access CAS released attributes in a servlet using Spring Security and Spring MVC. Traditionally, in a Spring-less implementation, I'd do something like this
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
// Gets the user ID from CAS
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
final Map<String, Object> attributes = principal.getAttributes();
String userId = (String) attributes.get("userid");
// ...
}
When creating a servlet using Spring MVC, but without Spring Security, there seemed to be basically no difference in accessing the attributes:
#RequestMapping("/")
public String welcome(HttpServletRequest request)
{
// Get the user ID from CAS
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();;
final Map<String, Object> attributes = principal.getAttributes();
userId = (String) attributes.get("userid");
// ...
}
However, after implementing Spring Security, request.getUserPrincipal() returns a CasAuthenticationToken rather than an AttributePrincipal. From what I noticed, none of the retrievable objects and data from this contained any of the CAS released attributes.
After a bit of looking around, I did notice something with mentioning the GrantedAuthorityFromAssertionAttributesUserDetailsService class, so I changed my security context .xml from
<security:user-service id="userService">
<security:user name="user" password="user" authorities="ROLE_ADMIN,ROLE_USER" />
</security:user-service>
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<constructor-arg ref="userService" />
</bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Saml11TicketValidator">
<constructor-arg value="https://localhost:8443/cas" />
</bean>
</property>
<property name="key" value="casAuthProviderKey" />
</bean>
to
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
<bean class="org.springframework.security.cas.userdetails.GrantedAuthorityFromAssertionAttributesUserDetailsService">
<constructor-arg>
<list>
<value>userid</value>
</list>
</constructor-arg>
</bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Saml11TicketValidator">
<constructor-arg value="https://localhost:8443/cas" />
</bean>
</property>
<property name="key" value="casAuthProviderKey" />
</bean>
Then, through a considerably more roundabout method, I could access the userid attribute by doing something like this:
#RequestMapping("/")
public String welcome(HttpServletRequest request)
{
CasAuthenticationToken principal = (CasAuthenticationToken) request.getUserPrincipal();
UserDetails userDetails = principal.getUserDetails();
Collection<SimpleGrantedAuthority> authorities = (Collection<SimpleGrantedAuthority>) userDetails.getAuthorities();
Iterator<SimpleGrantedAuthority> it = authorities.iterator();
String userid = it.next().getAuthority();
// ...
}
However, besides being a little more lengthy than previous implementations, it doesn't seem possible to support map multiple attributes from CAS (say, if CAS were also releasing firstName and lastName attributes).
Is there a better way of setting up the security context .xml to allow easier access of these attributes, especially if there are multiples that I want to use in a web app?
I think I figured it out. Outside of setting the attributes as authorities, which may be useful if you're using those to determine permission (i.e. hasAuthority('username')), it seems like the only other way is to construct your own UserDetails and UserDetailsService classes.
For example, MyUser:
package my.custom.springframework.security.userdetails;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
public class MyUser extends User
{
private static final long serialVersionUID = 1L;
private String id;
private String lastName;
private String firstName;
public MyUser(
String username,
String password,
String id,
String lastName,
String firstName,
Collection<? extends GrantedAuthority> authorities)
{
super(username, password, authorities);
this.id = id;
this.lastName = lastName;
this.firstName = firstName;
}
public String getId()
{
return id;
}
public String getLastName()
{
return lastName;
}
public String getFirstName()
{
return firstName;
}
}
Then, borrowing some of the structure of GrantedAuthorityFromAssertionAttributesUserDetailsService and JdbcDaoImpl, I created a MyUserDetailsService:
package my.custom.springframework.security.userdetails;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.sql.DataSource;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.cas.userdetails.AbstractCasAssertionUserDetailsService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
public final class MyUserDetailsService extends AbstractCasAssertionUserDetailsService
{
public static final String DEF_USERS_BY_ID_QUERY = "select ?, id, last_name, first_name " +
"from users " + "where id = ?";
public static final String DEF_AUTHORITIES_BY_ID_QUERY = "select role " +
"from roles join users on users.username = roles.username " +
"where users.id = ?";
private static final String NON_EXISTENT_PASSWORD_VALUE = "NO_PASSWORD";
private JdbcTemplate jdbcTemplate;
private String usersByIdQuery;
private String authoritiesByIdQuery;
public MyUserDetailsService(DataSource dataSource)
{
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.usersByIdQuery = DEF_USERS_BY_ID_QUERY;
this.authoritiesByIdQuery = DEF_AUTHORITIES_BY_ID_QUERY;
}
protected MyUser loadUserDetails(Assertion assertion)
{
AttributePrincipal attributePrincipal = assertion.getPrincipal();
String username = attributePrincipal.getName();
String id = (String) attributePrincipal.getAttributes().get("userid");
MyUser user = loadUser(username, id);
Set<GrantedAuthority> dbAuthsSet = new HashSet<GrantedAuthority>();
dbAuthsSet.addAll(loadUserAuthorities(id));
List<GrantedAuthority> dbAuths = new ArrayList<GrantedAuthority>(dbAuthsSet);
return createMyUser(username, user, dbAuths);
}
protected MyUser loadUser(String username, String id)
{
return jdbcTemplate.queryForObject(usersByIdQuery, new String[] { username, id },
new RowMapper<MyUser>()
{
public MyUser mapRow(ResultSet rs, int rowNum) throws SQLException
{
String username = rs.getString(1);
String id = rs.getString(2);
String lastName = rs.getString(3);
String firstName = rs.getString(4);
return new MyUser(username, NON_EXISTENT_PASSWORD_VALUE, id, lastName, firstName,
AuthorityUtils.NO_AUTHORITIES);
}
});
}
protected List<GrantedAuthority> loadUserAuthorities(String id)
{
return jdbcTemplate.query(authoritiesByIdQuery, new String[] { id },
new RowMapper<GrantedAuthority>()
{
public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException
{
// TODO Replace with rolePrefix variable
String roleName = "ROLE_" + rs.getString(1);
return new SimpleGrantedAuthority(roleName);
}
});
}
protected MyUser createMyUser(String username,
MyUser userFromUserQuery, List<GrantedAuthority> combinedAuthorities)
{
return new MyUser(username, userFromUserQuery.getPassword(),
userFromUserQuery.getId(), userFromUserQuery.getLastName(), userFromUserQuery.getFirstName(),
combinedAuthorities);
}
}
Finally, I set the authenticationUserDetailsService in my casAuthenticationProvider to use this class, passing in a global datasource from my container (Tomcat 6 in this case):
...
<property name="authenticationUserDetailsService">
<bean class="my.custom.springframework.security.userdetails.MyUserDetailsService">
<constructor-arg>
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/my/conn"/>
</constructor-arg>
</bean>
</property>
...

how to delete refreshtoken and access token when user logout oauth 2.0?

i tried with ....
<sec:logout invalidate-session="true" logout-success-url="/logoutsuccess" logouturl="/logout/>
but it is not working properly....
i want to clear everything like refresh token and access token session , cookies when user logout....
my security-servlet.xml looks like this
<!-- Protected resources -->
<sec:http create-session="never" entry-point-ref="oauthAuthenticationEntryPoint"
access-decision-manager-ref="accessDecisionManager"
xmlns="http://www.springframework.org/schema/security">
<sec:anonymous enabled="false" />
<sec:intercept-url pattern="/data/user/*"
access="IS_AUTHENTICATED_FULLY" />
<sec:logout delete-cookies="JSESSIONID" invalidate-session="true" />
<sec:custom-filter ref="resourceServerFilter"
before="PRE_AUTH_FILTER" />
<sec:access-denied-handler ref="oauthAccessDeniedHandler" />
</sec:http>
In Spring-boot application I will:
1. get OAuth2AccessToken
2. using it will delete OAuth2RefreshToken
3. and then delete itself
#Component
public class CustomLogoutSuccessHandler
extends AbstractAuthenticationTargetUrlRequestHandler
implements LogoutSuccessHandler {
private static final String BEARER_AUTHENTICATION = "Bearer ";
private static final String HEADER_AUTHORIZATION = "authorization";
#Autowired
private TokenStore tokenStore;
#Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
String token = httpServletRequest.getHeader(HEADER_AUTHORIZATION);
if (token != null && token.startsWith(BEARER_AUTHENTICATION)) {
String accessTokenValue = token.split(" ")[1];
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessTokenValue);
if (oAuth2AccessToken != null) {
OAuth2RefreshToken oAuth2RefreshToken = oAuth2AccessToken.getRefreshToken();
if (oAuth2RefreshToken != null)
tokenStore.removeRefreshToken(oAuth2RefreshToken);
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
}
}
you can do these things into sessionDestroyedListener...almost look like this..
In this code i am updating lastLogout date ..you can do what you want
#Component("sessionDestroyedEventListener")
public class SessionDestroyedEventListener implements ApplicationListener<SessionDestroyedEvent>{
// private static Logger logger = BaseLogger.getLogger(AuthenticationEventListener.class);
#Autowired
private AuthenticationService authenticationService;
public void setAuthenticationService(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
/**
* Capture sessionDestroyed event and update lastLogout date after session destroyed of particular user.
*/
#Override
public void onApplicationEvent(SessionDestroyedEvent appEvent) {
SessionDestroyedEvent event = (SessionDestroyedEvent) appEvent;
Object obj = null;
UserInfo userInfo = null;
ArrayList<SecurityContext> sc = (ArrayList<SecurityContext>) event.getSecurityContexts();
Iterator<SecurityContext> itr = sc.iterator();
while (itr.hasNext()) {
obj = itr.next().getAuthentication().getPrincipal();
if (obj instanceof UserInfo) {
userInfo = (UserInfo) obj;
} else {
String userCode = (String) obj;
if (userCode == null || "".equals(userCode)) {
userCode = "UnDefinedUser";
}
userInfo = new UserInfo(userCode);
}
//authenticationService.updateLastLogoutDate(userInfo.getUsername());
}
}
}

Ways to toggle spring security SAML on and off

I have a pretty standard implementation of spring security saml into my application in addition to other authentication mechanisms. Out of the box SAML will not be configured but can be configured through a form, so by default SAML should be disabled. I'd like to easily be able to toggle SAML on / off but am not sure what the best way to do this would be.
It seems like one approach would be to do a custom FilterChainProxy where if I check if saml is enabled and if so to ignore the samlFilter chain(How to delete one filter from default filter stack in Spring Security?) and also do a similar implementation for the Metadata Generator Filter.
Any advice would be great.
Here is my config:
<http auto-config="false" use-expressions="true"
access-decision-manager-ref="webAccessDecisionManager"
disable-url-rewriting="false"
create-session="never"
authentication-manager-ref="authenticationManager">
<custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
<custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
</http>
Metadata Generator Filter:
<beans:bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.saml.metadata.MetadataGenerator">
<beans:property name="entityId" value="${saml.entityId}"/>
<beans:property name="signMetadata" value="${saml.signMetadata}"/>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
Saml Filter:
<beans:bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
<filter-chain-map request-matcher="ant">
<filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
<filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter"/>
<filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter"/>
<filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
<filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter"/>
<filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter"/>
<filter-chain pattern="/saml/discovery/**" filters="samlIDPDiscovery"/>
</filter-chain-map>
</beans:bean>
EDIT: Here is my implementation, it is a bit hackish and relies on a deprecated method but it works
The below snippet disables MetadataGeneratorFilter:
public class MyMetadataGeneratorFilter extends MetadataGeneratorFilter {
private boolean isActive = false;
public MyMetadataGeneratorFilter(MetadataGenerator generator) {
super(generator);
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (isActive) {
processMetadataInitialization((HttpServletRequest) request);
}
chain.doFilter(request, response);
}
public void setActive(boolean active) {
isActive = active;
}
}
There is also the samlFilter / FilterChainMap which is autowired. If saml is enabled, I leave this chain as is, if it is disabled, I set the chain to an empty map in my service which enables / disables saml.
Upon initialization, I get the filterchainmap values:
private Map<RequestMatcher, List<Filter>> map;
#Override
public void init() throws ServiceException, MetadataProviderException {
SamlConfig samlConfig = getConfig();
map = samlFilter.getFilterChainMap();
applySamlConfig(samlConfig);
}
In the below method, I set the filter chain map to either the original map provided in the spring xml(if enabled) or an empty map (if disabled).
public void applySamlConfig(SamlConfig samlConfig) throws ServiceException, MetadataProviderException {
if (!samlConfig.isEnabled()) {
Map<RequestMatcher, List<Filter>> emptyMap = samlFilter.getFilterChainMap();
emptyMap.clear();
samlFilter.setFilterChainMap(emptyMap);
return;
}
samlFilter.setFilterChainMap(map);
i added a custom filter in the entry-point-ref definition. This filter skips all following filters if the feature is not enabled.
<security:http entry-point-ref="samlEntryPoint">
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" />
<!-- This filter checks if the SSO-Feature is enabled - otherwise all following security filters will be skipped -->
<security:custom-filter before="BASIC_AUTH_FILTER" ref="ssoEnabledFilter"/>
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter" />
<security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter" />
The ssoEnabledFilter:
public class SsoEnabledFilter extends GenericFilterBean {
#Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain) throws IOException, ServletException {
boolean ssoEnabled = isSsoEnabled();
if (ssoEnabled) {
filterChain.doFilter(request, response);
} else {
request.getRequestDispatcher(((HttpServletRequest) request).getServletPath()).forward(request, response);
}
}
}
So far I've been implementing this using a custom Spring namespace which includes or skips certain beans based on the backend configuration and reloading of the Spring context in case the backend configuration changes.
Edit : fixed error signaled by TheTurkish
If you want to be able to switch the use of SAML on a running application, the simpler would be to use a wrapper around samlFilter. For example
public class FilterWrapper extends GenericFilterBean {
private Filter inner;
private boolean active;
private boolean targetFilterLifeCycle = false;
public Filter getInner() {
return inner;
}
public void setInner(Filter inner) {
this.inner = inner;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
#Override
public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
if (active) {
inner.doFilter(sr, sr1, fc);
}
else {
fc.doFilter(str,sr1);
}
}
#Override
protected void initFilterBean() throws ServletException {
super.initFilterBean();
if (inner == null) {
throw new ServletException("Inner cannot be null");
}
if (targetFilterLifeCycle) {
inner.init(getFilterConfig());
}
}
#Override
public void destroy() {
super.destroy();
if (inner != null && targetFilterLifeCycle) {
inner.destroy();
}
}
}
You can use it that way :
<bean id="samlFilter" class="...FilterWrapper" p:active="false">
<property name=inner>
<!-- the real samlFilter bean -->
<bean class="org.springframework.security.web.FilterChainProxy">
...
</bean>
</property>
</bean>
As it is a bean, you inject it where you want to activate/deactivate Saml and simple call :
samlFilter.setActive(active);

Is it possible to use Spring Security (3.1.X) to get LDAP information on a user other that the one authenticated against?

I use Spring Security to authenticate a user against an Active Directory server. A CustomUserContext is also injected into the ldapAuthenticationProvider bean to provide access to additional LDAP attributes. Everything works quite well. I have no problem pulling whatever I want from the authenticated user.
The issue I have is that I want to retrieve some attributes, most specifically the email address, from the Active Directory server on a user other than the user that is logged in. Is it possible to achieve this by leveraging what I already have, or is my only option to use a totally separate method to access LDAP attributes from a different user?
[edit]
Configuration follows
security-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
<property name="url" value="ldap://xxxx.xxxx.xxx:389" />
<property name="base" value="dc=corp,dc=global,dc=xxxxx,dc=com" />
<property name="userDn" value="CN=lna.authquery,OU=LDAPGroups,OU=NorthAmerica,DC=corp,DC=global,DC=xxxxx,DC=com" />
<property name="password" value="xxxxxxx" />
<property name="pooled" value="true" />
<!-- AD Specific Setting for avoiding the partial exception error -->
<property name="referral" value="follow" />
</bean>
<bean id="ldapAuthenticationProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider" >
<constructor-arg>
<bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
<constructor-arg ref="contextSource" />
<property name="userSearch">
<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value="" />
<constructor-arg index="1" value="(sAMAccountName={0})" />
<constructor-arg index="2" ref="contextSource" />
</bean>
</property>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="contextSource" />
<constructor-arg value="" />
<property name="groupSearchFilter" value="(member={0})" />
<property name="searchSubtree" value="true" />
<!-- Settings below convert the adds the prefix ROLE_ to roles returned from AD -->
</bean>
</constructor-arg>
<property name="userDetailsContextMapper">
<bean class="net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper" />
</property>
</bean>
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<list>
<ref local="ldapAuthenticationProvider" />
</list>
</constructor-arg>
</bean>
<sec:http pattern="/css/**" security="none"/>
<sec:http pattern="/images/**" security="none"/>
<sec:http auto-config="true" authentication-manager-ref="authenticationManager" >
<sec:intercept-url pattern="/login.jsp*" requires-channel="https" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/**" requires-channel="https" access="IS_AUTHENTICATED_FULLY"/>
<sec:form-login login-page='/login.jsp'
default-target-url="/home.html"
authentication-failure-url="/login.jsp" />
</sec:http>
CustomeUserDetails.java
package net.xxxx.xxxx.utilities;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
public class CustomUserDetails extends User {
private static final long serialVersionUID = 1416132138315457558L;
// extra instance variables
final String fullname;
final String email;
final String title;
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities, String fullname,
String email, String title) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired,
accountNonLocked, authorities);
this.fullname = fullname;
this.email = email;
this.title = title;
}
public String getFullname() {
return this.fullname;
}
public String getEmail() {
return this.email;
}
public String getTitle() {
return this.title;
}
}
CustomUserDetailsContextMapper.java
package net.xxxx.xxxxx.utilities;
import java.util.Collection;
public class CustomUserDetailsContextMapper implements UserDetailsContextMapper {
public UserDetails mapUserFromContext(DirContextOperations ctx,
String username, Collection<? extends GrantedAuthority> authorities) {
String fullname = "";
String email = "";
String title = "";
Attributes attributes = ctx.getAttributes();
try {
fullname = (String) attributes.get("displayName").get();
email = (String) attributes.get("mail").get();
title = (String) attributes.get("title").get();
} catch (NamingException e) {
e.printStackTrace();
}
CustomUserDetails details = new CustomUserDetails(username, "", true, true, true, true, authorities, fullname, email, title);
return details;
}
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
}
}
I finally did end up figuring out how to do this. I'm answering this in case it helps someone else who needs to do this. I'd be surprised if I'm the only one.
First I had to move my security-config.xml file out of the WEB-INF structure and put it under the spring resources directory. The contextSource bean I was able to reuse. However I could not reuse the CustomUserDetailsContextMapper.java nor the CustomUserDetails.java class as they were too specific to Spring security and not to just retrieving LDAP data from an unauthenticated user.
I ended up writing a separate class for the LDAP access that had the common contextSource autowired in. That class is below.
LdapDao.java
package net.xxxxx.xxx.dao;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javax.naming.directory.Attributes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.stereotype.Component;
#Component
public class LdapDao {
LdapTemplate template;
#Autowired
public LdapDao(LdapContextSource contextSource) {
template = new LdapTemplate(contextSource);
}
#SuppressWarnings("unchecked")
public Map<String, String> getUserAttributes(String username) {
Map<String, String> results = new HashMap<String, String>();
String objectClass = "samAccountName=" + username;
LinkedList<Map<String, String>> list = (LinkedList<Map<String, String>>) template.search("", objectClass, new UserAttributesMapper());
if (!list.isEmpty()) {
// Should only return one item
results = list.get(0);
}
return results;
}
private class UserAttributesMapper implements AttributesMapper {
#Override
public Map<String, String> mapFromAttributes(Attributes attributes) throws javax.naming.NamingException {
Map<String, String> map = new HashMap<String, String>();
String fullname = (String) attributes.get("displayName").get();
String email = (String) attributes.get("mail").get();
String title = (String) attributes.get("title").get();
map.put("fullname", fullname);
map.put("email", email);
map.put("title", title);
return map;
}
}
}
#Bill what you've done is great, though there is actually an easier way. Instead of resorting to the LdapTemplate, just use the beans you've already registered for DefaultLdapAuthoritiesPopulator and FilterBasedLdapUserSearch. This way you can get the same UserDetails object which also has the authorities populated and reuses your existing code for your net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper.
Here's what you need to do:
Split out the beens you need to inject as named beans and use ref attributes for the properties and constructor-args (DefaultLdapAuthoritiesPopulator, FilterBasedLdapUserSearch, net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper).
In your LdapDao inject references to:
FilterBasedLdapUserSearch - userSearch
DefaultLdapAuthoritiesPopulator - authPop
net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper - userMapper
Add the following method to your LdapDao:
.
public UserDetails getUserDetails(final String username) {
try {
DirContextOperations ctx = userSearch.searchForUser(username);
return userMapper.mapUserFromContext(ctx, username,
authPop.getGrantedAuthorities(ctx, username));
} catch (UsernameNotFoundException ex) {
return null;
}
}
Now you can just call getUserDetails(String) to get the same object you do when retrieving the currently logged in context, and can use the same code etc.

RestTemplate - Handling response headers/body in Exceptions (RestClientException, HttpStatusCodeException)

In my restful webservice, in case of bad request (5xx) or 4xx respose codes, I write a custom header "x-app-err-id" to the response.
On the client side, I use exchange method of RestTemplate to make a RestFul web service call. Everything is fine when the response code is 2xx.
ResponseEntity<Component> response = restTemplate.exchange(webSvcURL,
HttpMethod.POST,
requestEntity,
Component.class);
But if there is an exception(HttpStatusCodeException) because of it being a bad request(5xx) or 4xx, in the catch block of HttpStatusCodeException, I get response(see above) as null and so I do not have access to my custom header I set in my web service. How do I get custom headers from the response in case of exceptions in RestTemplate.
One more question is, I set an error object(json) in the reponse body in case of error and I would like to know how to access response body as well in case of exceptions in RestTemplate
I finally did it using ResponseErrorHandler.
public class CustomResponseErrorHandler implements ResponseErrorHandler {
private static ILogger logger = Logger.getLogger(CustomResponseErrorHandler.class);
private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
public void handleError(ClientHttpResponse response) throws IOException {
List<String> customHeader = response.getHeaders().get("x-app-err-id");
String svcErrorMessageID = "";
if (customHeader != null) {
svcErrorMessageID = customHeader.get(0);
}
try {
errorHandler.handleError(response);
} catch (RestClientException scx) {
throw new CustomException(scx.getMessage(), scx, svcErrorMessageID);
}
}
public boolean hasError(ClientHttpResponse response) throws IOException {
return errorHandler.hasError(response);
}
}
And then use this custom response handler for RestTemplate by configuring as shown below
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
<property name="messageConverters">
<list>
<ref bean="jsonConverter" />
</list>
</property>
<property name="errorHandler" ref="customErrorHandler" />
</bean>
<bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="supportedMediaTypes" value="application/json" />
</bean>
<bean id="customErrorHandler " class="my.package.CustomResponseErrorHandler">
</bean>
You shouldn't have to create a custom error handler. You can get the body and headers from the HttpStatusCodeException that gets thrown.
try {
ResponseEntity<Component> response = restTemplate.exchange(webSvcURL,
HttpMethod.POST,
requestEntity,
Component.class);
} catch (HttpStatusCodeException e) {
List<String> customHeader = e.getResponseHeaders().get("x-app-err-id");
String svcErrorMessageID = "";
if (customHeader != null) {
svcErrorMessageID = customHeader.get(0);
}
throw new CustomException(e.getMessage(), e, svcErrorMessageID);
// You can get the body too but you will have to deserialize it yourself
// e.getResponseBodyAsByteArray()
// e.getResponseBodyAsString()
}
If you use a global exception handler add the below method or check this
https://www.javaguides.net/2018/09/spring-boot-2-exception-handling-for-rest-apis.html add below method in GlobalExceptionHandler class
#ExceptionHandler({HttpClientErrorException.class, HttpStatusCodeException.class, HttpServerErrorException.class})
#ResponseBody
public ResponseEntity<Object> httpClientErrorException(HttpStatusCodeException e) throws IOException {
BodyBuilder bodyBuilder = ResponseEntity.status(e.getRawStatusCode()).header("X-Backend-Status", String.valueOf(e.getRawStatusCode()));
if (e.getResponseHeaders().getContentType() != null) {
bodyBuilder.contentType(e.getResponseHeaders().getContentType());
}
return bodyBuilder.body(e.getResponseBodyAsString());
}

Resources