I have two applications on IIS for development. First WCF application that contains all the logic and communication with database (we call this Server). And another ASP.NET MVC 3 application that has reference to WCF application (we call this Client).
I have issue connected with mixing WCF web.config configuration with Unity IoC custom service host and custom behavior.
When all configuration is done by Unity it creates simple BasicHttpBinding, but my requirement is to make it secure with Certificate Authorization, so I need wsHTTPBinding.
------------- Configuring for BasicHttpBinding ------------
At the beginning look at common Unity implementation for WCF:
internal class UnityInstanceProvider : IInstanceProvider
{
private readonly IUnityContainer container;
private readonly Type contractType;
public UnityInstanceProvider(
[NotNull] IUnityContainer container,
[NotNull] Type contractType)
{
this.container = container;
this.contractType = contractType;
}
#region IInstanceProvider Members
public object GetInstance(InstanceContext instanceContext)
{
return GetInstance(instanceContext, null);
}
public object GetInstance(InstanceContext instanceContext, Message message)
{
return container.Resolve(contractType);
}
public void ReleaseInstance(InstanceContext instanceContext, object instance)
{
container.Teardown(instance);
}
}
internal class UnityServiceBehavior : IServiceBehavior
{
private readonly IUnityContainer container;
public UnityServiceBehavior(
[NotNull] IUnityContainer container)
{
this.container = container;
}
#region IServiceBehavior Members
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
{
if (endpointDispatcher.ContractName != "IMetadataExchange")
{
endpointDispatcher.DispatchRuntime.InstanceProvider = new UnityInstanceProvider(container, serviceDescription.ServiceType);
}
}
}
}
#endregion
}
public class UnityServiceHost : ServiceHost
{
private readonly IUnityContainer container;
public UnityServiceHost(
[NotNull] IUnityContainer container,
[NotNull] Type serviceType,
Uri[] baseAddresses)
: base(serviceType, baseAddresses)
{
this.container = container;
}
protected override void OnOpening()
{
base.OnOpening();
if (Description.Behaviors.Find<UnityServiceBehavior>() == null)
{
Description.Behaviors.Add(new UnityServiceBehavior(container));
}
}
}
public class UnityServiceHostFactory : ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
IUnityContainer container = new UnityContainer();
UnityContainerConfigurator.Configure(container);
return new UnityServiceHost(container, serviceType, baseAddresses);
}
}
WCF application web.config contains only basic information:
None endpoints, none service definition.
Now imagine that we have SecurityService with definition:
<%# ServiceHost Language="C#" Debug="true"
Service="myNamespace.SecurityService"
Factory="myNamespace.UnityServiceHostFactory" %>
Now I can add service reference to SecurityService to my Client.
A this step it generates in client web.config:
<basicHttpBinding>
<binding name="BasicHttpBinding_ISecurityService" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
<endpoint address="http://localhost/wcf-app/SecurityService.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_ISecurityService"
contract="SecurityServiceReference.ISecurityService" name="BasicHttpBinding_ISecurityService" />
At this point I configure this for Unity:
container.RegisterType<SecurityServiceClient>(new InjectionConstructor());
And in Client application I can use it simply by (I don't mention here constructor injection):
var securityService = DependencyResolver.Current.GetService<SecurityServiceClient>();
And this all works! But It don't if I want to use wsHTTPBinding ...
------------- Configuring for wsHTTPBinding ------------
To enable wsHTTPBinding I configured it inside web.config of WCF Application. As a remainder for BasicHttpBinding it didn't contained any information concerning binding, endpoin, etc.
But now for wsHTTPBinding I added:
<bindings>
<wsHttpBinding>
<binding name="wsHttpEndpointBinding">
<security>
<message clientCredentialType="Certificate" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<services>
<service behaviorConfiguration="ServiceBehavior" name="myNamespace.SecurityService">
<endpoint address="" binding="wsHttpBinding"
bindingConfiguration="wsHttpEndpointBinding"
name="wsHttpEndpoint" contract="myNamespace.ISecurityService">
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
<serviceCredentials>
<serviceCertificate findValue="CN=myClientCert" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
And after adding service reference to Client application it generates:
<wsHttpBinding>
<binding name="wsHttpEndpoint" closeTimeout="00:01:00" openTimeout="00:01:00"
receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false"
transactionFlow="false" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text"
textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSession ordered="true" inactivityTimeout="00:10:00"
enabled="false" />
<security mode="Message">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="Certificate" negotiateServiceCredential="true"
algorithmSuite="Default" />
</security>
</binding>
I manually added behaviorConfiguration="CertBehavior", that is:
<behaviors>
<endpointBehaviors>
<behavior name="CertBehavior">
<clientCredentials>
<clientCertificate findValue="CN=myClientCert"/>
</clientCredentials>
</behavior>
</endpointBehaviors>
And now when I want to resolve it using Unity:
var securityService = DependencyResolver.Current.GetService<SecurityServiceClient>();
I get always null...
What is funny when I create simply instance by:
var client = new SecurityServiceReference.SecurityServiceClient();
It works fine... So for sure issue is not connected with wrong wsHttpBinding configuration but rather combining Unity + wsHttpBinding from web.config...
Can any one help me with this issue?
Daniel
Ok, I figured it out.
Ladislav you were right that it should show an exception.
UnityDependencyResolver was simply catching it.
internal class UnityDependencyResolver : IDependencyResolver
{
private readonly IUnityContainer container;
public UnityDependencyResolver([NotNull] IUnityContainer container)
{
this.container = container;
}
#region IDependencyResolver Members
public object GetService(Type serviceType)
{
try
{
return container.Resolve(serviceType);
}
catch
{
return null;
}
}
public IEnumerable<object> GetServices(Type serviceType)
{
try
{
return container.ResolveAll(serviceType);
}
catch
{
return new List<object>();
}
}
I also had to explicitly set certificate locations for the certificates:
<serviceCertificate findValue="CN=myClientCert"
storeName="My"
x509FindType="FindBySubjectDistinguishedName"
storeLocation="LocalMachine"
/>
<clientCertificate findValue="CN=myClientCert" storeName="My"
x509FindType="FindBySubjectDistinguishedName"
storeLocation="LocalMachine"
/>
Now it works fine.
Related
I have problem with resolving component with Unity in my .NET MVC 5 app.
In my App_Start folder I have generated UnityConfig.cs and UnityMvcActivator.cs, which looks like this:
public static class UnityConfig
{
#region Unity Container
private static Lazy<IUnityContainer> container =
new Lazy<IUnityContainer>(() =>
{
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
public static IUnityContainer Container => container.Value;
#endregion
public static void RegisterTypes(IUnityContainer container)
{
// NOTE: To load from web.config uncomment the line below.
// Make sure to add a Unity.Configuration to the using statements.
// container.LoadConfiguration();
container.AddNewExtension<Interception>();
container.RegisterType<ILogger, EntLibFileLogger>(new ContainerControlledLifetimeManager(),
new InjectionConstructor(ConfigurationManager.AppSettings["logTrace"],
int.Parse(ConfigurationManager.AppSettings["logSize"])));
container.RegisterType<IUserRepository, UserRepository>(new Interceptor<InterfaceInterceptor>(), new InterceptionBehavior<LoggingInterceptionBehavior>());
container.RegisterType<IUserProvider, UserProvider>(new Interceptor<InterfaceInterceptor>(), new InterceptionBehavior<LoggingInterceptionBehavior>());
...
}
}
public static class UnityMvcActivator
{
/// <summary>
/// Integrates Unity when the application starts.
/// </summary>
public static void Start()
{
FilterProviders.Providers.Remove(FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().First());
FilterProviders.Providers.Add(new UnityFilterAttributeFilterProvider(UnityConfig.Container));
DependencyResolver.SetResolver(new UnityDependencyResolver(UnityConfig.Container));
// TODO: Uncomment if you want to use PerRequestLifetimeManager
// Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(UnityPerRequestHttpModule));
}
/// <summary>
/// Disposes the Unity container when the application is shut down.
/// </summary>
public static void Shutdown()
{
UnityConfig.Container.Dispose();
}
}
Interception look like this:
class LoggingInterceptionBehavior : IInterceptionBehavior
{
private readonly ILogger _logger;
public LoggingInterceptionBehavior(ILogger logger)
{
_logger = logger;
}
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
var methodDesc = $"{input.MethodBase.DeclaringType.Name}.{input.MethodBase.Name}";
var args = input.Arguments == null || input.Arguments.Count == 0
? "null"
: JsonConvert.SerializeObject(input.Arguments);
// Before invoking the method on the original target.
_logger.Log($"{methodDesc} start. Arguments: {args}", LogLevel.Info);
// Invoke the next behavior in the chain.
var result = getNext()(input, getNext);
// After invoking the method on the original target.
if (result.Exception != null)
{
_logger.Log($"{methodDesc} threw exception. {result.Exception.GetBaseException()}", LogLevel.Error);
}
else
{
_logger.Log($"{methodDesc} end", LogLevel.Info);
}
return result;
}
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
public bool WillExecute
{
get { return true; }
}
}
I have also customized authorization filter, where is following code:
public class SayDoAuthorizeAttribute : AuthorizeAttribute
{
private readonly string[] allowedRoles;
public SayDoAuthorizeAttribute() : base()
{
}
public SayDoAuthorizeAttribute(params string[] roles)
{
allowedRoles = roles;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var userRoles = SayDoAuthorizeHelper.GetRolesAndCacheThem(httpContext);
bool authorize = false;
foreach (var role in allowedRoles)
{
if (userRoles.Contains(role))
{
authorize = true;
break;
}
}
return authorize;
}
...
}
The problem is that UnityConfig.Resolve fails on resolving IUserProvider component in following SayDoAuthorizeHelper.GetRolesAndCacheThem method. (I want to have this class static, because IsUserInRole and IsUserInRoles mehthods are also used in razor views.
public static class SayDoAuthorizeHelper
{
public static IEnumerable<string> GetRolesAndCacheThem(HttpContextBase httpContext)
{
var nameFromSession = httpContext.Session[Constants.SessionKeyUserPreselectSession];
var userName = (nameFromSession != null) ? (string)nameFromSession : httpContext.User.Identity.Name;
var rolesString = (string)httpContext.Cache.Get(userName);
if (string.IsNullOrEmpty(rolesString))
{
var userProvider = UnityConfig.Container.Resolve<IUserProvider>();
var roles = userProvider.GetUserRolesNames(userName);
rolesString = string.Join(",", roles);
//set absolute cache
var minutesToCacheRoles = double.Parse(ConfigurationManager.AppSettings[Constants.KeyCacheUserRolesInMinutes]);
httpContext.Cache.Insert(userName, rolesString, null, DateTime.Now.AddMinutes(minutesToCacheRoles), Cache.NoSlidingExpiration);
return roles;
}
return rolesString.Split(',').ToList();
}
public static bool IsUserInRole(System.Security.Principal.IPrincipal userPrincipal, string roleName)
{
var userRoles = GetRolesAndCacheThem(new HttpContextWrapper(HttpContext.Current));
return userRoles.Contains(roleName);
}
public static bool IsUserInRoles(System.Security.Principal.IPrincipal userPrincipal, string[] roleNames)
{
var userRoles = GetRolesAndCacheThem(new HttpContextWrapper(HttpContext.Current));
foreach (var roleName in roleNames)
{
if (userRoles.Contains(roleName))
{
return true;
}
}
return false;
}
}
When I have breakpoint set on line
var userProvider = UnityConfig.Container.Resolve();
then no exception is thrown, and code still continue, but I see in local variables that userProvider threw an exception of type 'System.IO.FileNotFoundException' Abb.Czopc.SayDo.BusinessLogic.Interface.IUserProvider {System.IO.FileNotFoundException}
with message "Cannot load assembly '15bd37ce9f8a41d9b9916a1ebadc536a'.". But in output window I see that assembly was loaded.
'iisexpress.exe' (CLR v4.0.30319: /LM/W3SVC/2/ROOT-1-131699797337577440): Loaded '15bd37ce9f8a41d9b9916a1ebadc536a'.
When I have registration in UnityConfig without Interception, everything works good.
container.RegisterType<IUserRepository, UserRepository>();
container.RegisterType<IUserProvider, UserProvider>();
I also tried replace InterfaceInterceptor with VirtualMethodInterceptor, but with same result.
Can anyone help? I dont know what is wrong. Thanks for help.
Solution is written in VS 2015, list of nuget packages is here:
<package id="Abb.Czopc.Common.Log" version="1.0.1" targetFramework="net451" />
<package id="Abb.Czopc.Common.Log.EntLibLogger" version="1.0.1" targetFramework="net451" />
<package id="Antlr" version="3.5.0.2" targetFramework="net451" />
<package id="AutoMapper" version="6.2.2" targetFramework="net451" />
<package id="EnterpriseLibrary.Common" version="6.0.1304.0" targetFramework="net451" />
<package id="EnterpriseLibrary.Logging" version="6.0.1304.0" targetFramework="net451" />
<package id="jQuery" version="3.3.1" targetFramework="net451" />
<package id="jQuery.Validation" version="1.17.0" targetFramework="net451" />
<package id="Microsoft.AspNet.Mvc" version="5.2.4" targetFramework="net451" />
<package id="Microsoft.AspNet.Razor" version="3.2.4" targetFramework="net451" />
<package id="Microsoft.AspNet.Web.Optimization" version="1.1.3" targetFramework="net451" />
<package id="Microsoft.AspNet.WebPages" version="3.2.4" targetFramework="net451" />
<package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.8" targetFramework="net451" />
<package id="Microsoft.jQuery.Unobtrusive.Ajax" version="3.2.5" targetFramework="net451" />
<package id="Microsoft.jQuery.Unobtrusive.Validation" version="3.2.9" targetFramework="net451" />
<package id="Microsoft.Net.Compilers" version="2.7.0" targetFramework="net451" developmentDependency="true" />
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net451" />
<package id="Modernizr" version="2.8.3" targetFramework="net451" />
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net451" />
<package id="Respond" version="1.4.2" targetFramework="net451" />
<package id="Unity.Abstractions" version="3.3.0" targetFramework="net451" />
<package id="Unity.Container" version="5.8.5" targetFramework="net451" />
<package id="Unity.Interception" version="5.5.1" targetFramework="net451" />
<package id="Unity.Mvc" version="5.0.13" targetFramework="net451" />
<package id="WebActivatorEx" version="2.2.0" targetFramework="net451" />
<package id="WebGrease" version="1.6.0" targetFramework="net451" />
I have a Spring Security configured in XML that works just fine. Now, I'm trying to have it expressed in JavaConfig only so as to get rid of the XML configuration altogether.
I've looked at the reference documentation, and at many blogs and support requests, but I still cannot find the solution.
It gives me the following exception:
Could not autowire field: private org.springframework.security.web.FilterChainProxy
com.thalasoft.learnintouch.rest.config.WebTestConfiguration.springSecurityFilterChain;
Pitifully I resorted to post my own request here...
The code:
#Configuration
#ComponentScan(basePackages = { "com.thalasoft.learnintouch.rest" })
public class WebTestConfiguration {
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private FilterChainProxy springSecurityFilterChain;
}
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}
public class WebInit implements WebApplicationInitializer {
private static Logger logger = LoggerFactory.getLogger(WebInit.class);
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
registerListener(servletContext);
registerDispatcherServlet(servletContext);
registerJspServlet(servletContext);
}
private void registerListener(ServletContext servletContext) {
// Create the root application context
AnnotationConfigWebApplicationContext appContext = createContext(ApplicationConfiguration.class, WebSecurityConfiguration.class);
// Set the application display name
appContext.setDisplayName("LearnInTouch");
// Create the Spring Container shared by all servlets and filters
servletContext.addListener(new ContextLoaderListener(appContext));
}
private void registerDispatcherServlet(ServletContext servletContext) {
AnnotationConfigWebApplicationContext webApplicationContext = createContext(WebConfiguration.class);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(webApplicationContext));
dispatcher.setLoadOnStartup(1);
Set<String> mappingConflicts = dispatcher.addMapping("/");
if (!mappingConflicts.isEmpty()) {
for (String mappingConflict : mappingConflicts) {
logger.error("Mapping conflict: " + mappingConflict);
}
throw new IllegalStateException(
"The servlet cannot be mapped to '/'");
}
}
private void registerJspServlet(ServletContext servletContext) {
}
private AnnotationConfigWebApplicationContext createContext(final Class... modules) {
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(modules);
return appContext;
}
}
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
CustomAuthenticationProvider customAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
#Bean
public DelegatingFilterProxy springSecurityFilterChain() {
DelegatingFilterProxy filterProxy = new DelegatingFilterProxy();
return filterProxy;
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").hasRole("ROLE_ADMIN").and().httpBasic();
http.authorizeRequests().antMatchers("/admin/login", "/admin/logout", "/admin/denied").permitAll()
.antMatchers("/admin/**").hasRole("ROLE_ADMIN")
.and()
.formLogin()
.loginPage("/admin/login")
.defaultSuccessUrl("/admin/list")
.failureUrl("/admin/denied?failed=true")
.and()
.rememberMe();
http.logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin/login").deleteCookies("JSESSIONID");
}
}
The XML configuration that I hope to get rid of:
<!-- A REST authentication -->
<http use-expressions="true" pattern="/admin/**">
<intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')" />
<http-basic entry-point-ref="restAuthenticationEntryPoint" />
<logout />
</http>
<!-- A form based browser authentication -->
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/admin/login" access="permitAll" />
<intercept-url pattern="/admin/logout" access="permitAll" />
<intercept-url pattern="/admin/denied" access="permitAll" />
<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
<form-login
login-page="/admin/login"
default-target-url="/admin/list"
authentication-failure-url="/admin/denied?failed=true"
always-use-default-target="true" />
<logout logout-success-url="/admin/login" />
<logout delete-cookies="JSESSIONID" />
</http>
<!-- A custom authentication provider on legacy data -->
<authentication-manager>
<authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>
UPDATE:
I added a Configuration directive:
#Configuration
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}
and an explicit import directive:
#Import({ SecurityWebApplicationInitializer.class })
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
}
But the exception remained the exact same.
I'm running Spring Security 3.2.4.RELEASE and Spring 3.2.9.RELEASE
If you have any suggestion, it is welcomed.
I removed this bean definition from the security configuration and it seems to have solved the issue
#Bean
public DelegatingFilterProxy springSecurityFilterChain() {
DelegatingFilterProxy filterProxy = new DelegatingFilterProxy();
return filterProxy;
}
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.
I've got my claims set-up with MVC3 using azure and everything is going well.
What I need to do now is extend the Claims Identity that's in the current thread / http context and add my own information (DOB, Address.. that sort of stuff)
so my question is - where is the best place to do this? any examples would be great..
I presume that when the user is authenticated id then have to go to the DB and pull back the relevant record for the user then add it to the custom Claims Identity object?
Typically you will have a httpmodule that will inspect the cookies and once the FedAuth token is found, you have a hook to build your Claims Principal and identities.
You don't typically need to store the user's entire profile, just useful things that wont typically change that often. I do this inside of an actionfilter.
Here is the code I found that does all of this.
https://github.com/wcpro/ScaffR/tree/master/src/ScaffR.Security/content/CodeTemplates/Scaffolders/ScaffR.Security
You may have to do a little digging but its all there.
Here is the code for the http module
public class ClaimsTransformationHttpModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.PostAuthenticateRequest += context_PostAuthenticateRequest;
}
void context_PostAuthenticateRequest(object sender, EventArgs e)
{
var context = ((HttpApplication) sender).Context;
if (FederatedAuthentication.SessionAuthenticationModule != null &&
FederatedAuthentication.SessionAuthenticationModule.ContainsSessionTokenCookie(context.Request.Cookies))
{
return;
}
var transformer = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
if (transformer != null)
{
var transformedPrincipal = transformer.Authenticate(context.Request.RawUrl, context.User as ClaimsPrincipal);
context.User = transformedPrincipal;
Thread.CurrentPrincipal = transformedPrincipal;
}
}
public void Dispose() { }
}
Here is the Claims Transformer
public partial class ClaimsTransformer : ClaimsAuthenticationManager
{
partial void SetCustomPrincipalClaims(IUserService userService, ref ClaimsPrincipal principal);
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return incomingPrincipal;
}
var newPrincipal = Transform(incomingPrincipal);
EstablishSession(newPrincipal);
return newPrincipal;
}
ClaimsPrincipal Transform(ClaimsPrincipal incomingPrincipal)
{
var nameClaim = incomingPrincipal.Identities.First().FindFirst(ClaimTypes.Name);
var userService = DependencyResolver.Current.GetService<IUserService>();
var user = userService.GetByUsername(nameClaim.Value);
var id = new ApplicationIdentity(user);
var principal = new ClaimsPrincipal(id);
SetCustomPrincipalClaims(userService, ref principal);
return principal;
}
private void EstablishSession(ClaimsPrincipal principal)
{
if (HttpContext.Current != null)
{
var sessionToken = new SessionSecurityToken(principal);
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
}
}
}
Then here is the configuration
<?xml version="1.0" encoding="utf-8"?>
<system.identityModel>
<identityConfiguration>
<claimsAuthenticationManager type="Barbarella.Core.Common.Security.ClaimsTransformer, Barbarella.Core" />
</identityConfiguration>
</system.identityModel>
And this...
<system.identityModel.services>
<federationConfiguration>
<cookieHandler mode="Default" requireSsl="false" />
</federationConfiguration>
</system.identityModel.services>
And this...
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true">
<add name="ClaimsTransformationModule" type="Barbarella.Core.Common.Security.ClaimsTransformationHttpModule, Barbarella.Core" />
<add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</modules>
Dont forget to add your configuration sections
<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
Here is my code for the ApplicationIdentity (overrides ClaimsIDentity)... This is the code that answers your question really...
public sealed partial class ApplicationIdentity : ClaimsIdentity
{
partial void SetCustomIdentityClaims(User user);
private readonly User _user;
public ApplicationIdentity(User user) : base("Application")
{
_user = user;
AddClaim(new Claim(ClaimTypes.Name, user.Username));
AddClaim(new Claim(ApplicationClaimTypes.UserId, user.Id.ToString(CultureInfo.InvariantCulture)));
AddClaim(new Claim(ApplicationClaimTypes.FirstName, user.FirstName));
AddClaim(new Claim(ApplicationClaimTypes.LastName, user.LastName));
AddClaim(new Claim("Time", DateTime.Now.ToString()));
SetCustomIdentityClaims(_user);
}
public User User
{
get { return _user; }
}
public int UserId
{
get { return int.Parse(FindFirst(ApplicationClaimTypes.UserId).Value); }
}
public string Username
{
get { return FindFirst(ClaimTypes.Name).Value; }
}
}
How to inject IServiceLocator to my class constructor?
When I tried to do this via my config, described above I got an Exception that it could not to create a RequestHandlersFactory class because unity could't find the constructor with serviceLocator and assemblyName.
I got two interfaces
public interface IPublicService
{
[OperationContract]
[ServiceKnownType("GetKnownTypes", typeof(KnownTypeProvider))]
Response Handle(Request request);
}
public interface IRequestHandlersFactory
{
IRequestHandler GetHandler(Type requestType);
IRequestHandler GetHandler<T>()
where T : Request;
IRequestHandler<T, TK> GetHandler<T, TK>()
where T : Request
where TK : Response;
}
and two classes:
public sealed class PublicService: IPublicService
{
private readonly IRequestHandlersFactory _requestHandlersFactory;
public PublicService(IRequestHandlersFactory requestHandlersFactory)
{
_requestHandlersFactory = requestHandlersFactory;
}
public Response Handle(Request request)
{
var handler = _requestHandlersFactory.GetHandler(request.GetType());
return handler.Handle(request);
}
}
public sealed class RequestHandlersFactory : IRequestHandlersFactory
{
private readonly IServiceLocator _serviceLocator;
private RequestHandlersFactory(IServiceLocator serviceLocator)
{
_serviceLocator = serviceLocator;
...
}
public RequestHandlersFactory(IServiceLocator serviceLocator, String assemblyName) : this(serviceLocator)
{
AddHandlersFromAssembly(Assembly.Load(assemblyName));
}
public RequestHandlersFactory(IServiceLocator serviceLocator, Assembly assembly) : this(serviceLocator)
{
AddHandlersFromAssembly(assembly);
}
...
}
Now I want to create unity config file:
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<alias alias="IPublicService" type="MyAssembly.IPublicService, MyAssembly"/>
<alias alias="PublicService" type="MyAssembly.PublicService, MyAssembly"/>
<alias alias="IRequestHandlersFactory" type="MyAssembly.IRequestHandlersFactory, MyAssembly"/>
<alias alias="RequestHandlersFactory" type="MyAssembly.RequestHandlersFactory, MyAssembly"/>
<container>
<register type="IPublicService" mapTo="PublicService">
<lifetime type="singleton"/>
</register>
<register type="IRequestHandlersFactory" mapTo="RequestHandlersFactory">
<lifetime type="singleton"/>
<constructor>
<param name="assemblyName">
<value value="MyAssemblyWithHandlers" />
</param>
<param name="serviceLocator" dependencyName="WcfServiceLocator" dependencyType="Microsoft.Practices.ServiceLocation.IServiceLocator, Microsoft.Practices.ServiceLocation"/>
</constructor>
</register>
</container>
My config code:
var container = new UnityContainer();
//configure container
var unitySection = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
var serviceLocator = new UnityServiceLocator(container );
container.RegisterInstance<IServiceLocator>("WcfServiceLocator", serviceLocator, new ContainerControlledLifetimeManager());
unitySection.Configure(container);
Try swapping the order of the constructor parameters in the config file so they line up with the actual parameter list in the class.