I have a question regarding Apache Shiro.
I´m using permission and role concept.
I have on the left side a menu with many links to my other pages (create an employee, show employee list etc.).
For each menu item I have also security for it to hide it or not (depends on the permission), like:
<pm:menu widgetVar="me"
rendered="#{checkPermissionController.checkPermission(['myprofile:show', 'myprofile:edit'])}">
To check if the user is permitted or not, I have those two functions in my bean:
/**
* Check permission for login User
*
* #return
* #throws IOException
* #throws PermissionGroupNotFoundException
*/
public boolean checkPermission(String permissionName) throws IOException {
if (loginBean.getCurrentUserShiro().isPermitted(permissionName)) {
return true;
}
return false;
}
/**
* If one of the permission is true
*
* #param strings
* #return
*/
public boolean checkPermission(List<String> list) {
int i = list.size();
if (i != 0) {
for (String s : list) {
if (loginBean.getCurrentUserShiro().isPermitted(s)) {
return true;
}
}
}
return false;
}
My question is now more against performance.
Is Apache Shiro execute for each menu entry a request against the database if the user is permitted or not?
Or does Shiro fetch at login time all permission for a user and "hold" it in the "Shiro User" object?
If yes: how can I improve it?
Edit:
Here my LoginBean:
#SessionScoped
#Named
public class LoginBean implements Serializable {
private Subject currentUserShiro;
public String submit() {
LOGGER.info("START submit");
try {
currentUserShiro = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
....
}
////////////////////
// Getter + Setter
////////////////////
public String getUsername() {
return username;
}
public Subject getCurrentUserShiro() {
return currentUserShiro;
}
currentUserShiro.login(token);
I'd recommend using Shiro's filters to handle the actual login logic for you.
https://shiro.apache.org/web.html
Otherwise, you could end up with the Subject not tied to your Session. It looks like your application might be forcing a log in any time getCurrentUserShiro is called. You should let the framework handle this for you.
It's not a JSF, but you can see a basic JSP example here (the Shrio config logic will be the same):
https://github.com/apache/shiro/tree/master/samples/servlet-plugin
You would likely just replace the login page: https://github.com/apache/shiro/blob/master/samples/servlet-plugin/src/main/webapp/login.jsp with your custom page
Related
I have a method on a controller to get a list of all the punishment types for a chat room (Kick, Ban, Warn and Mute). On the first test when I mock the data it works as expected and the test passes.
However, on my second test I provided. I defined what should be returned as an Optional<Punishment> with the attribute of punishmentName set as "mute". I am very confused why this is giving me null. When I run the Spring application outside of testing, the route works fine. For some reason the mock never wants to return the value I specified but only null. Specifically, this is being caught in the test on the line .andExpect(jsonPath("$.punishmentName", Matchers.equalTo("mute"))); as the fields value is null giving the following error:
java.lang.AssertionError: No value at JSON path "$.punishmentName"
For clarity, I have also provided the controller methods and service methods.
Punishment Controller Test:
#WebMvcTest(PunishmentController.class)
#RunWith(SpringRunner.class)
public class PunishmentControllerTest {
#Autowired
private MockMvc mvc;
#MockBean
private PunishmentService punishmentService;
#MockBean
private PunishmentValidator punishmentValidator;
#Test
public void getAllPunishmentTypesReturnsAListOfPunishmentTypes() throws Exception {
List<Punishment> punishments = new ArrayList<>();
punishments.add(new Punishment("mute"));
punishments.add(new Punishment("kick"));
punishments.add(new Punishment("ban"));
punishments.add(new Punishment("warn"));
Mockito.when(punishmentService.getAllPunishmentTypes()).thenReturn(punishments);
mvc.perform(get("/api/punishments"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.hasSize(4)))
.andExpect(jsonPath("$[0].punishmentName", Matchers.equalTo("mute")))
.andExpect(jsonPath("$[1].punishmentName", Matchers.equalTo("kick")))
.andExpect(jsonPath("$[2].punishmentName", Matchers.equalTo("ban")))
.andExpect(jsonPath("$[3].punishmentName", Matchers.equalTo("warn")));
}
#Test
public void getPunishmentTypeReturnsMuteWhenMuteIsSpecified() throws Exception {
Optional<Punishment> mute = Optional.of(new Punishment("mute"));
Mockito.when(punishmentService.getPunishmentType("mute")).thenReturn(mute);
mvc.perform(get("/api/punishments/mute"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.punishmentName", Matchers.equalTo("mute")));
}
Controller methods:
/**
* GET request for all punishment types.
* #return List<Punishment> - When Punishments are found in the database they are returned in a List object.
* Otherwise, an empty list is returned if no records are found or an error occurs.
*/
#GetMapping
public List<Punishment> getAllPunishments() {
return punishmentService.getAllPunishmentTypes();
}
/**
* GET request for one punishment type.
* #param punishmentType String - The type of punishment.
* #return Optional<Punishment> - The rule that gets returned or an empty optional if no rule is found.
*/
#GetMapping(path = "{punishmentType}")
public Optional<Punishment> getPunishment(#PathVariable("punishmentType") String punishmentType) {
boolean isPunishmentTypeValid = punishmentValidator.validatePunishmentName(punishmentType);
if (isPunishmentTypeValid) {
return punishmentService.getPunishmentType(punishmentType);
} else {
return Optional.empty();
}
}
}
Service methods:
/**
* Gets all the punishment types
* #return List<Punishment> - The rules in the community
*/
public List<Punishment> getAllPunishmentTypes() {
return punishmentRepository.findAll();
}
/**
* Gets a specific punishment type.
* #param punishmentType String - The type of punishment.
* #return The punishment retrieved.
*/
public Optional<Punishment> getPunishmentType(String punishmentType) {
return punishmentRepository.findById(punishmentType);
}
I believe it is because you forget to mock the method PunishmentValidator#validatePunishmentName("mute") to return true such that the method that you stub on PunishmentService is never invoked because by default if you do not stub a method , it will return false (see this).
Also it is a known behaviour that #MockBean is configured as lenient stubbing which will not reported error (i.e. throw UnnecessaryStubbingException) if you stub a method but it actually does not get executed.
So change the following should fix your problem :
#Test
public void getPunishmentTypeReturnsMuteWhenMuteIsSpecified() throws Exception {
Optional<Punishment> mute = Optional.of(new Punishment("mute"));
Mockito.when(punishmentService.getPunishmentType("mute")).thenReturn(mute);
Mockito.when(punishmentValidator.validatePunishmentName("mute")).thenReturn(true);
mvc.perform(get("/api/punishments/mute"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.punishmentName", Matchers.equalTo("mute")));
}
I am trying to use Spring LDAP to retrieve and modify user information in an Active Directory server, but I can't retrieve a user record by dn so that I can modify it.
I am able to find the record by username with the LdapTemplate.search method. There is no dn attribute in the record, but distinguishedName looks like it should be correct. When I use LdapTemplate.lookupContext to retrieve the record by dn, however, the server says that it can't find the record by the dn that it just gave me. What am I doing wrong?
It seem wrong that the LdapTemplate search method doesn't give you a handle that you can use without doing a second query from the Active Directory. Is there a better way to do this?
I have created a sample Groovy application to demonstrate the problem. My Spring Boot application creates this class and then invokes the runTest method.
package edu.sunyjcc.gateway.ldap;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
import org.springframework.ldap.core.DirContextOperations;
public class ActiveDirectoryDNSample {
LdapTemplate ldapTemplate;
/** Attributes to fetch from server */
static attributeList = [
"sAMAccountName",
"distinguishedName",
"baseDn",
"userPrincipalName",
];
/** This will represent the record retrieved from Active Directory */
class Person {
/** Raw data from server */
Map attributes = [:];
/** Return the distinguished name */
String getDn() {
attributes?.distinguishedName;
}
String getUsername() {
attributes?.sAMAccountName;
}
String toString() {
"${this.username} <${this.getDn()}>"
}
/** Get a handle to the object from AD so we can modify it. This fails. */
def getContext() {
assert ldapTemplate;
println "in getContext()";
def dn = new LdapName(this.getDn());
println "...dn=$dn"
assert dn;
// The next line throws an exception.
DirContextOperations context = ldapTemplate.lookupContext(dn);
println "...context=$context"
}
}
/** Convert the attributes from AD into a Person object */
class RecordMapper implements AttributesMapper<Person> {
/** Create a Person object from the attribute map */
Person mapFromAttributes(Attributes attributes)
throws NamingException {
assert ldapTemplate;
Person prec = new Person(
ldapTemplate: ldapTemplate
);
attributeList.collect {
[attrName: it, attr: attributes.get(it)]
}.grep {it.attr}.each {
prec.attributes."${it.attrName}" = it.attr.get() as String;
}
return prec;
}
}
/** Get a user from Active Directory */
public List<Person> getByUsername(String username) throws Exception {
assert ldapTemplate;
AttributesMapper attrMapper = new RecordMapper();
assert attrMapper;
List s = ldapTemplate.search(
query().
where("sAMAccountName").is(username),
attrMapper
);
if (s == null) {
System.err.println("s is null");
}
return s?:[];
}
/** Try to fetch a record and get a modify context for it */
public runTest(String username) {
println "In ActiveDirectoryDNSample.runText($username)"
assert ldapTemplate;
def records = getByUsername(username);
println "Retrieved ${records?.size()} records";
records.each {println " $it"}
println "Now try to get the context for the records"
records.each {
person ->
println " getting context for $person";
def context = person.getContext();
println " context=$context"
}
}
public ActiveDirectoryDNSample(LdapTemplate ldapTemplate ) {
this.ldapTemplate = ldapTemplate;
}
}
In ActiveDirectoryDNSample.runText(testuser)
Retrieved 1 records
testuser <CN=Test User,CN=Users,DC=jccadmin,DC=sunyjcc,DC=edu>
Now try to get the context for the records
getting context for testuser <CN=Test User,CN=Users,DC=jccadmin,DC=sunyjcc,DC=edu>
in getContext()
...dn=CN=Test User,CN=Users,DC=jccadmin,DC=sunyjcc,DC=edu
and then it dies with a javax.naming.NameNotFoundException with the following data.
[LDAP: error code 32 - 0000208D: NameErr: DSID-03100238, problem 2001 (NO_OBJECT), data 0, best match of:
'CN=Users,DC=jccadmin,DC=sunyjcc,DC=edu'
\0]
Thanks for any help you can give me.
It turns out that there was, indeed, a better way. Instead of using an org.springframework.ldap.core.AttributesMapper in the search, you use org.springframework.ldap.core.ContextMapper.
In my example, I added a field to the Person class, which will hold a reference to the context.
DirContextOperations context;
Then I created a new class extending org.springframework.ldap.core.support.AbstractContextMapper.
class PersonContextMapper extends AbstractContextMapper {
#Override
protected Object doMapFromContext(DirContextOperations ctx) {
AttributesMapper attrMapper = new RecordMapper();
Person p = attrMapper.mapFromAttributes(ctx.attributes);
p.context = ctx;
return p;
}
}
When I passed it to the ldapTemplate.search method in the place of the AttributeMapper, I was able to use the context to update the Active Directory.
I am trying to customize the code of the spring oauth authorization server.
for now I have just copied the framework authorizationEndpoint code and placed it in another class. I just changed the address mapping to /custom/oauth/authorize. I have also added #Controller before the class declaration otherwise this code will not be used at all:
#Controller
//#Order(Ordered.HIGHEST_PRECEDENCE)
#SessionAttributes("authorizationRequest")
public class AuthorizationEndpointCustom extends AuthorizationEndpoint {
#Autowired
private AuthenticationManager authenticationManager;
private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices();
private RedirectResolver redirectResolver = new DefaultRedirectResolver();
private UserApprovalHandler userApprovalHandler = new DefaultUserApprovalHandler();
private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();
private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();
private String userApprovalPage = "forward:/oauth/confirm_access";
private String errorPage = "forward:/oauth/error";
private Object implicitLock = new Object();
public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
this.sessionAttributeStore = sessionAttributeStore;
}
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
#RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, #RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
System.out.println("\n\ninside custom authorization endpoint");
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
private OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
OAuth2Request storedOAuth2Request) {
OAuth2AccessToken accessToken = null;
// These 1 method calls have to be atomic, otherwise the ImplicitGrantService can have a race condition where
// one thread removes the token request before another has a chance to redeem it.
synchronized (this.implicitLock) {
accessToken = getTokenGranter().grant("implicit",
new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
}
return accessToken;
}
.
.
.
I have also instructed the framework to change the mappring from /oauth/authorize to /custom/oauth/authorize:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
.accessTokenConverter(tokenEnhancer()).pathMapping("/oauth/authorize", "/custom/authorize/");
}
but when I run the code I encounter the following error:
Description:
Field tokenGranter in com.example.demo.controller.AuthorizationEndpointCustom required a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' that could not be found.
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' in your configuration.
the parent class of AuthorizationEndpoint (AbstractEndpoint) declares tokenGranter but it is not instantiated. there is no #autowired for this and other attributes of this class. who does genereate and inject these variable into this class?
how can I get hold of tokenGranter obj and inject it?
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.provider.endpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.util.Assert;
/**
* #author Dave Syer
*
*/
public class AbstractEndpoint implements InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
private TokenGranter tokenGranter;
private ClientDetailsService clientDetailsService;
private OAuth2RequestFactory oAuth2RequestFactory;
private OAuth2RequestFactory defaultOAuth2RequestFactory;
public void afterPropertiesSet() throws Exception {
Assert.state(tokenGranter != null, "TokenGranter must be provided");
Assert.state(clientDetailsService != null, "ClientDetailsService must be provided");
defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(getClientDetailsService());
if (oAuth2RequestFactory == null) {
oAuth2RequestFactory = defaultOAuth2RequestFactory;
}
}
public void setProviderExceptionHandler(WebResponseExceptionTranslator providerExceptionHandler) {
this.providerExceptionHandler = providerExceptionHandler;
}
public void setTokenGranter(TokenGranter tokenGranter) {
this.tokenGranter = tokenGranter;
}
protected TokenGranter getTokenGranter() {
return tokenGranter;
}
protected WebResponseExceptionTranslator getExceptionTranslator() {
return providerExceptionHandler;
}
protected OAuth2RequestFactory getOAuth2RequestFactory() {
return oAuth2RequestFactory;
}
protected OAuth2RequestFactory getDefaultOAuth2RequestFactory() {
return defaultOAuth2RequestFactory;
}
public void setOAuth2RequestFactory(OAuth2RequestFactory oAuth2RequestFactory) {
this.oAuth2RequestFactory = oAuth2RequestFactory;
}
protected ClientDetailsService getClientDetailsService() {
return clientDetailsService;
}
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
I am answering my own question.
I took a good look at the framework code and I found out that AuthorizationServerEndpointsConfiguration class creates an object of type AuthorizationEndpoint and populates it's attributes and then return this object as a bean.
I managed to solve above mentioned problem with TokenGranter by creating a bean of my new AuthorizationEndpointCustom the same way AuthorizationServerEndpointsConfiguration does. this is the code to do so:
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
AuthorizationServerEndpointsConfiguration asec;
#Bean
#Order(value = Ordered.HIGHEST_PRECEDENCE)
#Primary
public AuthorizationEndpoint authorizationEndpoint () throws Exception{
AuthorizationEndpointCustom authorizationEndpoint = new AuthorizationEndpointCustom();
FrameworkEndpointHandlerMapping mapping = asec.getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(asec.getEndpointsConfigurer().getExceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(asec.getEndpointsConfigurer().getTokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(asec.getEndpointsConfigurer().getAuthorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(asec.getEndpointsConfigurer().getOAuth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(asec.getEndpointsConfigurer().getOAuth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(asec.getEndpointsConfigurer().getUserApprovalHandler());
return authorizationEndpoint;
}
private String extractPath(FrameworkEndpointHandlerMapping mapping, String page) {
String path = mapping.getPath(page);
if (path.contains(":")) {
return path;
}
return "forward:" + path;
}
but this did not result in what I hoped to. the new bean does not replace the bean from framework code. this situation with overriding beans led to another question:
how replace framework beans
buttom line, this is not the way to override the framework endpoints. you can simply create a controller with mappings for these endpoints (e.g /oauth/authorize or /oauth/token). automatically these mappings will get precedence over framework endpoints. for more info refer to spring doc
I am new to Spring MicroService i know how to handle session in Springboot monolithic application but can you please tell me how to handle session in microservice when we communicate with another microservice from one, and how to handle session if multiple instance of a microservice is running.
It's a complicated question and needs some extra logic to be written.
So, all methods, that may be called from one service to another should be parameterized and look like this:
public int externalCall(Session session)
{
if(sessionManager.isAliveSession(session)
{
sessionManager.touch(session);
//do some actions
}
else
{
throw new UnknownSessionException();
}
}
Then you should have one more service or module to deal with sessions. In my code I called it sessionManager.
It may possibly have such methods:
public interface SessionManager
{
/**
* to create session object in database, for example,
* with expiration date, create date etc.
*/
public void createSession();
/**
* to set the fact, that this session is
* still used and update its expiration time
*/
public void touch(Session session);
/**
* checks if session with this
* id is not expired.
*/
public boolean isAliveSession(Session session);
}
Here is an example how to call externalCall from the other service.
You will need to create class like this to perform session-based calls:
public class SessionTemplate
{
private SessionManager sessionManager;
private AtomicReference<SessionTO> session = new AtomicReference();
public <T> T execute(Callback<T> callback) {
Session session = this.getSession();
try {
return callback.doInSeance(session);
} catch (UnknownSessionException e) {
// exception may happen in externalCall method
session = this.createSession(session);
return callback.doInSeance(session);
}
}
private Session getSession() {
if (this.session.get() == null) {
synchronized(this) {
if (this.session.get() == null) {
this.session.set(this.createSession());
}
}
}
return (Session)this.session.get();
}
private Session createSession()
{
// create session in your DB
return sessionManager.createSession();
}
}
Now your remote calls will be performed like this:
public int getSmthFromRemote()
{
return sessionTemplate.execute(session -> microService.externalCall(session));
}
I am using Spring Security with LDAP (Active directory), I am able to authenticate user and create my own user detail object by extending LdapUserDetailsMapper.
By default I am getting certain fields and groups and DN.
But I would like to get additional fields, like email, contact number, which are available in Active Directory.
So how to get those information ?
My configuration
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("hmie.co.in", "ldap://1.1.1.1:389/");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(userDetailsContextMapper);
return provider;
}
Custom user detail mapping
#Service
public class MyUserDetailsContextMapper extends LdapUserDetailsMapper implements UserDetailsContextMapper {
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
LdapUserDetailsImpl ldapUserDetailsImpl = (LdapUserDetailsImpl) super.mapUserFromContext(ctx, username, authorities);
MyUserDetails myUserDetails = new MyUserDetails();
myUserDetails.setAccountNonExpired(ldapUserDetailsImpl.isAccountNonExpired());
myUserDetails.setAccountNonLocked(ldapUserDetailsImpl.isAccountNonLocked());
myUserDetails.setCredentialsNonExpired(ldapUserDetailsImpl.isCredentialsNonExpired());
myUserDetails.setEnabled(ldapUserDetailsImpl.isEnabled());
myUserDetails.setUsername(ldapUserDetailsImpl.getUsername());
myUserDetails.setAuthorities(ldapUserDetailsImpl.getAuthorities());
String dn = ldapUserDetailsImpl.getDn();
int beginIndex = dn.indexOf("cn=") + 3;
int endIndex = dn.indexOf(",");
myUserDetails.setEmployeeName(dn.substring(beginIndex, endIndex));
beginIndex = dn.indexOf("ou=") + 3;
endIndex = dn.indexOf(",", beginIndex);
myUserDetails.setDepartment(dn.substring(beginIndex, endIndex));
return myUserDetails;
}
}
To get the complete LDAP Directory attributes and values i did like this. But here i am using inteface org.springframework.ldap.core.AttributesMapper instead of class org.springframework.security.ldap.userdetails.LdapUserDetailsMapper.
ldapTemplate.search("o=XXXXX", new EqualsFilter("uid", userName).encode(),
new AttributesMapper() {
#Override
public Object mapFromAttributes(Attributes attr) throws NamingException {
// TODO Auto-generated method stub
NamingEnumeration<String> namingEnumeration = attr.getIDs();
while (namingEnumeration.hasMoreElements()) {
String attributeName= (String) namingEnumeration.nextElement();
System.out.println(attributeName+" = "+attr.get(attributeName));
}
return null;
}
});
In the above piece of code attr.getIDs() returns the Active directory attributes like CN,DN,SN and mail. attr.get(attribute) returns the value of attribute.
The code in mapUserFromContext is so close! The key detail is that the ctx object passed in to the method already contains the additional Active Directory attributes for the principal. The attribute values are accessible using method ctx.getStringAttribute("attribute-name"). For example, you would access the surname attribute of the principal with ctx.getStringAttribute("sn"). To get the user's email and contact number, you would only need to access the appropriate attributes. In my company's Active Directory, those attributes are mail and phone, respectively. The attributes might be named differently in your system.