Sunday, January 2, 2011

Spring Security - MVC: Using an LDAP Authentication Provider

In this tutorial we will setup a simple Spring MVC 3 application, secured by Spring Security. Our users will be authenticated against an LDAP provider. We'll explore how we can configure an LDAP authentication provider. For this tutorial we will leveraged on our existing tutorials to lessen the repetition of steps. The overall structure of our application will be based on Spring Security 3 - MVC: Using a Simple User-Service Tutorial. This is a good exercise to show how easy we can change providers without disrupting the whole flow of our existing system. The LDAP server and entries will be based on LDAP - Apache Directory Studio: A Basic Tutorial

What is LDAP?
The Lightweight Directory Access Protocol (LDAP) is an application protocol for reading and editing directories over an IP network. A directory is an organized set of records. For example, the telephone directory is an alphabetical list of persons and organizations, with each record having an address and phone number. A directory information tree often follows political, geographic, or organizational boundaries. LDAP directories often use Domain Name System (DNS) names for the highest levels. Deeper inside the directory might appear entries for people, departments, teams, printers, and documents.

Source: http://en.wikipedia.org/wiki/LDAP
If this is your first time to LDAP, you might be wondering how is this different from an RDBMS. I suggest my readers to visit the following article Should I Use a Directory, a Database, or Both?

We'll start immediately with the spring-security.xml configuration.

spring-security.xml

Honestly, this is the only file that you need to change from the Spring Security 3 - MVC: Using a Simple User-Service Tutorial.

Actually, we just deleted a couple of entries. The old configuraiton contains an in-memory user-service provider:

The new configuration contains an LDAP authentication provider:

The real tricky part here is ensuring that you can connect to your LDAP server and ensuring that you've mapped correctly the attribute names from your ldap-authentication-provider to the LDAP Directory Information Tree.

To get a better understanding, let's examine the directory structure of the server. You will gain better insight if you've read first LDAP - Apache Directory Studio: A Basic Tutorial.

Here's server's structure:
mojo
 |
 |--groups
 |    |
 |    |--Admin
 |    |--User
 |
 |--users
      |
      |--guy1
      |--guy2
      |--guy3
      |--guy4
Here's a screenshot of the server's directory:

Let's focus on the elements of the ldap-authentication-provider

The attribute value of the user-search-filter="(uid={0})" corresponds to the attribute we've declared for the users
The {0} in the (uid={0}) will be replaced by the username entered in the form.

The value of the user-search-base="ou=users" corresponds to the attribute we've declared in the directory tree

The attribute value of the group-search-filter="(uniqueMember={0})" corresponds to the attribute we've declared for the groups
The {0} in the (uniqueMember={0}) represents the Distinguished Name (DN) of the user. On our sample data, the dn for Hugo Williams is cn=Hugo Williams,ou=users,o=mojo. The DN is similar with the primary key in relational databases.

The value of the group-search-base="ou=groups" corresponds to the attribute we've declared in the directory tree

The attribute value of the group-role-attribute="cn" corresponds to the attribute we've declared for the groups. Spring Security uses this value of this attribute to determine the authorization level of the user.

The value of the role-prefix="ROLE_" is used to indicate what prefix should be added on the values received from group-role-attribute="cn". If you examine carefully the server's structure, we have two roles declared as cn=Admin and cn=User
The values that will be returned are Admin and User. With role-prefix="ROLE_", they will become ROLE_ADMIN and ROLE_USER respectively.

The LDAP Server
Let's examine ldap-server tag.


The url ldap://localhost:10389/o=mojo is composed of the server's url and port number ldap://localhost:10389/ and the base parent path o=mojo

The attribute manager-dn="uid=admin,ou=system" is based on the Distinguished Name of the admin. Normally, you will be provided by your administrator with a custom access, but for this tutorial we're relying on the default values to make things simple.

That's it. We've setup a simple Spring MVC 3 application, secured by Spring Security. Our users are authenticated against an LDAP provider. We've also explored how the various attributes map to an existing directory structure. This also means we can customize our mappings and assign different attribute names.

The best way to learn further is to try the actual application.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-security-ldap/

You can download the project as a Maven build. Look for the spring-security-ldap.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security - MVC: Using an LDAP Authentication Provider ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

126 comments:

  1. hi, thank you about this article. can you demo to authenticate user data from mysql database by using hibernate.

    ReplyDelete
  2. @kanjon, this should be simple enough. If you look on the Spring 3 MVC - Hibernate 3: Using Annotations Integration Tutorial at http://krams915.blogspot.com/2010/12/spring-3-mvc-hibernate-3-using.html. All you need to do is edit the properties file and the hibernate language to MySQL. I'm gonna try making a demo for that. Thanks :)

    ReplyDelete
    Replies
    1. Thanks so much for your excellent doc. It's really helpful ...

      Cheers,
      HQ

      Delete
  3. Nice post but the download repo is empty

    ReplyDelete
  4. Hi, I ran the project via Maven on Tomcat, the log suggests it deployed correctly (no errors) but all I see is a 404 when I browse to http://localhost:8080/spring-security-ldap. What could I be doing wrong?

    ReplyDelete
  5. My apologies, I didn't realise I had to browse to http://localhost:8080/spring-security-ldap/krams/auth/login. Thanks for the tutorial.

    ReplyDelete
    Replies
    1. Thank you! I was having the same issue! Then I decided since I don't know that much about where to point to for webapps I decided to gasp, read the comments.

      Delete
    2. Same here...I am trying to make changes and modified web.xml and all the paths by deleting krams. so I assume http://localhost:8080/spring-security-ldap/auth/login should work the same way, but for some reason I get 404 error.

      Delete
  6. Good Article. But most of the time, You want to authenticate to the LDAP with out manager-dn user/password. Here it assumes you have the manager-dn password. Could you please give an example which just authenticates the LDAP and also how to write our own implementation of assigning a role. something like overriding like and I can map my own role for a given user. This is more practical for most if us.

    Vam

    ReplyDelete
  7. Nice article. Could you please provide an example of how to authenticate a login user against Active Directory using LDAP protocol and assign the roles dynamically from database with out having to map the roles in XML configuration. Is it possible? Could you please guide me, I am new to Spring Security...

    ReplyDelete
  8. This is simply best and most detailed article man. I just need to authenticate instead of authorization can I remove group search filter and other stuff ?

    ReplyDelete
  9. One major point to note while authenticating against active directory is that it doesn't 't handle referral and you will get PartialResultException: Unprocessed Continuation Reference(s), to avoid this problem setIgnorePartialResultException(true), check Ldap authentication using spring security example for more details.Also I suggest using spring security framework for ldap authentication.

    ReplyDelete
  10. Hi,
    Will you be able to post a tutorial about SSO with CAS and LDAP?

    Thanks

    ReplyDelete
  11. amazing tutorial ..... helped me allow users of certain roles to be authenticated to a url.

    ReplyDelete
  12. hi krams
    how can we authenticate a user in spring mvc.
    in a special application there is a sha512 password encoding +custom password encoding and that password is inserted to database.
    On user login the password has to convert to that old password encoding and validate with those in database.
    in applicationcontex-security.xml










    the CustomUserDetailsService class implements AuthenticationProvider its getting the password we typing in password field ,i converted this password with passwordencoding ,now i need to authenticate with database and logged in.so what things i need to do extra in the customclass to validate with custompassword.

    please help me
    thanks in advance

    ReplyDelete
  13. Hi krams,

    Thanks for amazing tutorial. LDAP can also store some user specific information. e.g. Phone Number. Can you please let me know how to retieve this information for logged in user.

    Thanks
    Shirish

    ReplyDelete
  14. Very very helpfull tutorial, TKS very much, but....if I want to get the roles from a database after login, what I need to set in the security:ldap-authentication-provider tag to it access my class, and what I need to return in the class? Have you an example?

    ReplyDelete
  15. Krams. You are awesome. That concludes this message.

    ReplyDelete
  16. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  17. what is the user name and password

    ReplyDelete
  18. great tutorial. Very straight forward. But I'm having an error when executing mvn jetty:run

    2012-08-16 21:54:08.963:/spring-security-ldap-embedded:INFO: Initializing Spring root WebApplicationContext
    [ERROR] [main 09:54:12] (DefaultAttributeTypeRegistry.java:lookup:192) attributeType w/ OID 2.5.4.16 not registered!
    [ERROR] [main 09:54:14] (ApacheDSContainer.java:start:180) Failed to create dc entry
    java.lang.IllegalArgumentException: [Assertion failed] - this expression must be true
    at org.springframework.util.Assert.isTrue(Assert.java:65)

    ReplyDelete
    Replies
    1. Hi Warner, I get the same error. Could you figure out the fix for this?

      Delete
  19. Hi Krams,

    The example is really good. But I am facing a problem. In my example the authentication is happening successfully but authorization means the ROLE_ + String retreived from LDAP group is not working fine I quess. Thats why even though I am having correct users and groups it is always going to accessDenied page...

    ReplyDelete
  20. Ele ejemplo esta muy bueno pero al levantarlo en tomcat me arroja un error 404 al desplegar que haria para probar el proyecto a la perfeccion

    ReplyDelete
  21. thanks for ur tutorial ..., same as how we can apply the cache to the application same spring-ldap...
    its really help for me

    ReplyDelete
  22. excellent material for beginners..

    ReplyDelete
    Replies
    1. what is username/password for this application

      Delete
  23. The example is really good. But I am facing a problem. In my example the authentication is happening successfully but authorization means the ROLE_ + String retreived from LDAP group is not working fine I quess. Thats why even though I am having correct users and groups it is always going to accessDenied page...

    ReplyDelete
  24. Hi ,

    In the debug mode , i got this ,

    Got LDAP context on server ldap://testldap:389/dc=ab,dc=cd,dc=ef

    also
    searching forSearching for user 'xxxxx', with user search [ searchFilter: '(&(o
    ry=Person)(sAMAccountName={0}))', searchBase: 'DC=ab,DC=cd,DC=ef',scope: subtree, searchTimeLimit: 0, derefLinkFlag: false ]

    i am not able to authenticate still...

    what could be the error ?Please help..

    ReplyDelete
  25. Hi ,

    In the debug mode , i got this ,

    Got LDAP context on server ldap://testldap:389/dc=ab,dc=cd,dc=ef

    also
    searching forSearching for user 'xxxxx', with user search [ searchFilter: '(&(objectCategory=Person)(sAMAccountName={0}))', searchBase: 'DC=ab,DC=cd,DC=ef',scope: subtree, searchTimeLimit: 0, derefLinkFlag: false ]

    i am not able to authenticate stil...

    what could be the error ?Please help..

    ReplyDelete
  26. hi i m adarsh
    i m getting this exception
    [ERROR] [Scanner-2 09:36:38] (DefaultAttributeTypeRegistry.java:lookup:192) attributeType w/ OID 2.5.4.16 not registered!
    [ERROR] [Scanner-2 09:36:39] (ApacheDSContainer.java:start:182) Failed to create dc entry
    java.lang.IllegalArgumentException: [Assertion failed] - this expression must be true

    plz any one can let me know where i m wrong .

    ReplyDelete
    Replies
    1. Hi Adarsh, I get the same error. Could you figure out the fix for this?

      Delete
  27. Hi, how can one reference an odjectClass in authentication manager?

    ReplyDelete
  28. Hi,
    Great article however I would like to know how in the ldap server I could give a specific user permissions and then use it in the Spring security. for example 'view_all_mails' permissions or 'delete_mail' permission

    ReplyDelete
  29. Hi,

    I deployed code in tomcat t.i am getting error. java.lang.Noclass found error. org/springframework/core/envEnvoirnmentCapble.

    I add spring-core.3.0.5 .i am getting error.Please help me resolve this issue.

    Regards,
    Venkat.

    ReplyDelete
  30. Good article! Congratulations guy!!

    ReplyDelete
    Replies
    1. what is username/password for this application. Thanks

      Delete
  31. What is the username and password for logging into LDAP server successfully Login ?

    ReplyDelete
  32. Hi i am getting below error,

    attributeType w/ OID 2.5.4.16 not registered!

    Please help me to get away from this,

    Thanks,
    Kiran

    ReplyDelete
  33. Are we supposed to have any LDIF file?

    ReplyDelete
    Replies
    1. nope, you are pointing to a ldap server. If you don't have an ldap then setup a test ldap.
      Seems you are new, so if you don't want to use ldap, try using inmemory auth first. replace the ldap with








      Delete
  34. Hi,

    Iam able to authenticate user from ldap using spring security......... Can someone tell me what will be the best approach for the below scenario.

    We have two spring security implemented web application and like to do seamless integration between these applications... Basically, I want to implement SSO so that i can login once through app1 and get access to app2 without changeling login page of app2...

    ReplyDelete
  35. This is a great tutorial. It helps me to find the answer that I was looking for for a long time.
    Thank you so much.

    ReplyDelete
  36. How to know user account is disabled or locked

    ReplyDelete
  37. Thank you for that information you article
    Signature:
    download free descargar whatsapp and download baixar whatsapp online and descargar whatsapp gratis , baixar whatsapp gratis

    ReplyDelete
  38. Thanks for all your information, Website is very nice and informative content.
    Signature:
    download descarga facebook gratis para Android celular and download free descargar facebook gratis and descargar facebook gratis , descarga facebook

    ReplyDelete
  39. Posts shared useful information and meaningful life, I'm glad to be reading this article and hope to soon learn the next article. thank you
    kids games online
    friv 2
    unblocked games
    juegos de un show mas

    ReplyDelete
  40. I'm constantly getting a 403 access denied error, I assume the authentication part is working fine, but i do not know what is going wrong, any suggestions?

    ReplyDelete
    Replies
    1. I could debug further to find that the access having role admin fails, basically because it says no granted authorities to the user

      Delete
    2. Ok. I could solve it myself finally.was a problem with uniqueMember mapping. where ou=parent, I had ou=child itself.

      Delete
  41. This information which you provided is very much useful for us.It was very interesting and useful for JAVA.we also provide struts see below.
    Regards...
    struts offline training in USA

    ReplyDelete
  42. Thank you for such a great tutorial Krams. But I'm getting j_spring_security_check 404 error, I'm using spring 4 and spring security 3.2.0 . I'm stuck and not logged in at all. Any luck? I'm struggling with this for weeks now. Thanks in advance

    ReplyDelete
  43. Hi what is username/password for this application

    ReplyDelete
    Replies
    1. check the other tutorials, it should be 'pass' for every user.

      Delete
  44. best article for ldap with spring

    ReplyDelete
  45. Great tutorial !!

    Greetings

    java books

    ReplyDelete
  46. Hi kram,
    simply suberb..
    Nicely explained.
    It will be really great, If you add few more roles and authentication in future explanation

    ReplyDelete
  47. Hi downloaded the zip file and when I try to run I am getting 404 error.. please help me to solve this problem.

    ReplyDelete
  48. Great Article… I love to read your articles because your writing style is too good,
    its is very very helpful for all of us and I never get bored while reading your article because,
    they are becomes a more and more interesting from the starting lines until the end.
    Java training in Chennai

    Java training in Bangalore

    Java online training

    Java training in Pune





    ReplyDelete
  49. Nice Article… I love to read your articles because your writing style is too good, its is very very helpful for all of us and I never get bored while reading your article because, they are becomes a more and more interesting from the starting lines until the end.

    Check out : best hadoop certification in chennai
    best hadoop training institute in chennai with placement
    best bigdata hadoop training in chennai
    big data training in chennai velachery

    ReplyDelete
  50. Great to share this information thanks. I am really happy to say it’s an interesting post to read. I learn new information from your blog.best fashion photographer in jalandhar

    ReplyDelete
  51. This comment has been removed by the author.

    ReplyDelete
  52. The article was up to the point and described the information very effectively. Thanks to blog author for wonderful and informative post.
    Security Service

    ReplyDelete
  53. This comment has been removed by the author.

    ReplyDelete
  54. This site was... eviews how do I say it? Relevant!! Finally I have found something that helped me. Thank you!

    ReplyDelete

  55. Well thats a nice article.The information You providied is good . Here is i want to share about dell boomi training videos and Mulesoft Training videos . Expecting more articles from you .

    ReplyDelete
  56. With the help of creative designing team TSS advertising company provides different branding and marketing strategies in advertising industry...
    https://www.tss-adv.com/branding-and-marketing

    ReplyDelete
  57. Thanks for the informative article About Java.This is one of the best resources I have found in quite some time. Nicely written and great info. I really cannot thank you enough for sharing.
    Java training in chennai | Java training in annanagar | Java training in omr | Java training in porur | Java training in tambaram | Java training in velachery

    ReplyDelete
  58. Effective blog with a lot of information. I just Shared you the link below for Courses .They really provide good level of training and Placement,I just Had Cloud Computing Classes in this institute , Just Check This Link You can get it more information about the Cloud Computing course.


    Java training in chennai | Java training in annanagar | Java training in omr | Java training in porur | Java training in tambaram | Java training in velachery

    ReplyDelete
  59. I am really happy to say it is an interesting post, thank you for sharing.

    https://nareshit.com/advanced-java-online-training/

    ReplyDelete
  60. very nice blogs!!! i have to learning for lot of information for this sites...Sharing for wonderful information.Thanks for sharing this valuable information to our vision. You have posted a trust worthy blog keep sharing.

    Azure Training in Chennai

    Azure Training in Bangalore

    Azure Training in Hyderabad

    Azure Training in Pune

    Azure Training | microsoft azure certification | Azure Online Training Course

    Azure Online Training

    ReplyDelete
  61. I`m always search this relevant content.I think you are the right person because you provide many useful information.

    ReplyDelete
  62. It was so nice content.I was really satisfied by seeing this content.
    sap wm training in bangalore

    ReplyDelete
  63. Very Informative and useful... Keep it up the great work. I really appreciate your post.
    Primavera Course in Chennai | primavera online training

    ReplyDelete
  64. This is a great post. Your Blog the very informative .
    Visit us: RPA Ui Path Online Training
    Visit us: Ui Path Course Online

    ReplyDelete
  65. Nice Blog,Thabk you for sharing this useful blog with us.
    keep doing your best

    ReplyDelete
  66. Veryniceblog,Keep Updating more Posts.
    Thank you

    Mulesoft Training

    ReplyDelete
  67. Great deals of important information and also a great article. I am currently following for your blog site and I am bookmarking it future reference. thanks for sharing!

    Top CRM Software

    ReplyDelete
  68. It’s in reality a nice and useful piece of info. I’m satisfied that you shared this useful information with us. Please stay us informed like this. Thank you for sharing.

    Jokergaming123
    Slot007
    Joker88 Slot
    Daftar Jokergaming123
    Slotwin777

    ReplyDelete
  69. Магазин питания для спорта, официальный интернет-сайт которого доступен по адресу: SportsNutrition-24.Com, реализует большой выбор товаров, которые принесут пользу и достижения как проф спортсменам, так и любителям. Интернет-магазин производит свою деятельность уже многие годы, предоставляя клиентам со всей России высококачественное питание для спорта, а также витамины и особые препараты - https://sportsnutrition-24.com/. Спортпит представляет собой категорию продуктов, которая призвана не только лишь сделать лучше спортивные достижения, но и благоприятно влияет на здоровье организма. Схожее питание вводится в повседневный рацион с целью получения микро- и макроэлементов, витаминов, аминокислот и белков, а помимо этого прочих недостающих веществ. Не секрет, что организм спортсмена в процессе наращивания мышечной массы и адаптации к повышенным нагрузкам, остро нуждается в должном количестве полезных веществ. При этом, даже правильное питание и употребление растительной, а также животной пищи - не гарантирует того, что организм получил нужные аминокислоты или белки. Чего нельзя сказать о высококачественном спортивном питании. Об наборе товаров Интернет-магазин "SportsNutrition-24.Com" продает качественную продукцию, которая прошла ряд проверок и получила сертификаты качества. Посетив магазин, клиенты смогут подобрать для себя товары из следующих категорий: - L-карнитинг (Л-карнитин) представляет собой вещество, родственное витамину B, синтез которого осуществляется в организме; - гейнеры, представляющие из себя, белково-углеводные консистенции; - BCAA - средства, содержащие в собственном составе три важнейшие аминокислоты, стимулирующие рост мышечной массы; - протеин - чистый белок, употреблять который вы можете в виде коктейлей; - различные аминокислоты; - а кроме этого ряд многих других товаров (нитробустеры, жиросжигатели, специальные препараты, хондропротекторы, бустеры гормона роста, тестобустеры и все остальное). Об оплате и доставке Интернет-магазин "SportsNutrition-24.Com" предлагает большое обилие товаров, которое в полной мере способно удовлетворить профессиональных и начинающих любителей спорта, включая любителей. Большой опыт дозволил организации сделать связь с крупнейшими поставщиками и производителями спортивного питания, что позволило сделать ценовую политику гибкой, а цены - демократичными! К примеру, аминокислоты либо гейнер приобрести можно по цене, которая на 10-20% ниже, чем у конкурентов. Оплата возможна как наличным, так и безналичным расчетом. Магазин предлагает широкий выбор способов оплаты, включая оплату разными электронными платежными системами, а кроме этого дебетовыми и кредитными картами. Главный кабинет фирмы расположен в Санкт-Петербурге, однако доставка товаров осуществляется во все населенные пункты РФ. Кроме самовывоза, получить товар можно с помощью любой транспортной фирмы, выбрать которую каждый клиент может в индивидуальном порядке.

    ReplyDelete
  70. http://krams915.blogspot.com/2011/01/spring-security-mvc-using-ldap.html
    http://blog.onsongapp.com/2017/12/dealing-with-large-backups.html?commentPage=2
    http://sqlage.blogspot.com/2015/05/how-to-use-variables-in-script-task-in.html
    http://findplanetnine.blogspot.com/2016/01/the-long-and-winding-history-of-planet-x.html
    http://all-best-study-material-marketing.blogspot.com/2014/01/promote-your-blog-with-social-media.html

    ReplyDelete
  71. Thanks for Sharing This Article. It is very so much valuable content. I hope these Commenting lists will help to my websiteEmbedded Systems Course in Hyderabad

    ReplyDelete
  72. wordpress design agency in united states Need professional WordPress Web Design Services? We're experts in developing attractive mobile-friendly WordPress websites for businesses. Contact us today!

    ReplyDelete
  73. Build natural individual modern million station. Every ask guess soldier strong nor break although.technology

    ReplyDelete
  74. Overall, your tutorial is informative and well-structured. It provides valuable information for developers looking to implement LDAP authentication in their Spring MVC applications. It would be beneficial for anyone seeking to understand LDAP integration with Spring Security.

    https://jewelorchard.com/wedding-destination-in-hyderabad/
    https://jewelorchard.com/places-for-birthday-celebration-in-hyderabad/
    https://jewelorchard.com/wedding-venue-in-hyderabad/
    https://jewelorchard.com/pre-wedding-shoot-places-in-hyderabad/
    https://jewelorchard.com/best-convention-hall-in-hyderabad/

    ReplyDelete
  75. Overall, your tutorial is informative and well-structured. It provides valuable information for developers looking to implement LDAP authentication in their Spring MVC applications. It would be beneficial for anyone seeking to understand LDAP integration with Spring Security.
    https://sterlingsystems.in/home-theatre-dealers-in-hyderabad/
    https://sterlingsystems.in/projector-screens-in-hyderabad/
    https://sterlingsystems.in/home-automation-in-hyderabad/

    ReplyDelete
  76. Overall, your tutorial is informative and well-structured. It provides valuable information for developers looking to implement LDAP authentication in their Spring MVC applications. It would be beneficial for anyone seeking to understand LDAP integration with Spring Security
    digital marketing trainer
    digital marketing course in hyderabad

    ReplyDelete
  77. "Great article, felt good after reading, worth it.
    i would like to read more from you.
    keep posting more.
    also follow Mern Stack course in hyderabad"

    ReplyDelete
  78. This is a fantastic read. I appreciate the thorough research and attention to detail. It’s always refreshing to find well-written content that’s both informative and engaging

    Mysore Ooty Coorg Tour Package
    Mysore Ooty Kodaikanal Tour Package
    Manali Rohtang Tour Package

    ReplyDelete
  79. This is a really interesting post. I appreciate the depth of your research and the clarity of your writing. Looking forward to reading more

    Khajuraho Tour Packages
    Western Group of Temples in Khajuraho

    ReplyDelete
  80. This post was exactly what I needed to read today. It’s very well-written and provides practical advice. Keep up the great work

    Destination Wedding Planner Packages
    Mice Tour Operators
    Corporate Event Planner

    ReplyDelete