diff --git a/src/memberdb/account.py b/src/memberdb/account.py
index a2fa0ee957b5aff4e5982ca8ffe3247ee5770416..5f911c0f1cd3747cdce27e480f708ca32356af96 100644
--- a/src/memberdb/account.py
+++ b/src/memberdb/account.py
@@ -9,27 +9,26 @@ from formtools.wizard.views import SessionWizardView
 from .models import Member
 from .forms import MyModelForm, MyForm
 from .views import MyUpdateView, MyWizardView
-from memberdb.account_backend import validate_username
+from memberdb.account_backend import validate_username, create_ad_user
 
 
 
 class AccountForm(MyModelForm):
-
 	# form fields
 	username = forms.SlugField(
 		validators=[validate_username],
 		max_length=19,
 	)
 	password = forms.CharField(
-		min_length=10, 
-		max_length=127, 
-		widget=forms.PasswordInput, 
+		min_length=10,
+		max_length=127,
+		widget=forms.PasswordInput,
 		strip=False,
 		help_text="Password must be between 10 and 127 characters long"
-	) 
+	)
 	confirm_password = forms.CharField(
-		min_length=10, 
-		max_length=127, 
+		min_length=10,
+		max_length=127,
 		widget=forms.PasswordInput,
 		strip=False,
 	)
@@ -48,11 +47,11 @@ class AccountForm(MyModelForm):
 
 	def save(self):
 		return
-	
+
 class EmailForm(MyModelForm):
 	forward = forms.BooleanField(required=False)
 	email_address  = forms.EmailField(
-		label='Forwarding address (optional)', 
+		label='Forwarding address (optional)',
 		required=False,
 		help_text="Your club email will be forwarded to this address. Leave blank if email forwarding is not required"
 	)
@@ -60,7 +59,7 @@ class EmailForm(MyModelForm):
 	class Meta:
 		model = Member
 		fields = ['forward', 'email_address']
-	
+
 	def clean(self):
 		if self['forward'].value() == True:
 			try:
@@ -74,16 +73,16 @@ class EmailForm(MyModelForm):
 
 class DispenseForm(MyForm):
 	pin = forms.CharField(
-		min_length=0, 
-		max_length=4, 
-		widget=forms.PasswordInput, 
+		min_length=0,
+		max_length=4,
+		widget=forms.PasswordInput,
 		strip=False,
 		required=False,
-		help_text="PIN must be 4 digits long") 
+		help_text="PIN must be 4 digits long")
 
 	confirm_pin = forms.CharField(
-		min_length=0, 
-		max_length=4, 
+		min_length=0,
+		max_length=4,
 		widget=forms.PasswordInput,
 		required=False,
 		strip=False,
@@ -99,7 +98,7 @@ class DispenseForm(MyForm):
 		except:
 			pass
 		super().clean();
-		
+
 
 class AccountView(MyWizardView):
 	form_list = [AccountForm,EmailForm,DispenseForm]
@@ -110,7 +109,7 @@ class AccountView(MyWizardView):
 		return self.object
 
 	def get_context_data(self, **kwargs):
-		m = self.object 
+		m = self.object
 		context = super().get_context_data(**kwargs)
 		context.update(self.admin.admin_site.each_context(self.request))
 		context.update({
@@ -121,6 +120,12 @@ class AccountView(MyWizardView):
 
 
 	def done(self, form_list, form_dict, **kwargs):
+
+		# create the user and save their username if successfull
+		if create_ad_user(self.get_cleaned_data_for_step('0'), self.object):
+			form_list[0].save()
+
+
 		messages.success(self.request, 'Your membership renewal has been submitted.')
 		return HttpResponseRedirect(reverse("admin:memberdb_membership_changelist"))
 
@@ -130,8 +135,8 @@ class AccountView(MyWizardView):
 def accountProgressView(request, member):
 	return
 
-	
-	
+
+
 
 def accountFinalView():
-	return render(request, 'accountfinal.html', context) 
+	return render(request, 'accountfinal.html', context)
diff --git a/src/memberdb/account_backend.py b/src/memberdb/account_backend.py
index 3f795ed6c41d2a3a82bb054c021b326adf6637dd..cf3162ada40cefea82bcdcfb0006fdf577c5a5eb 100644
--- a/src/memberdb/account_backend.py
+++ b/src/memberdb/account_backend.py
@@ -5,9 +5,12 @@ from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured, ValidationError
 from django.utils.translation import gettext_lazy as _
 
-import ldap
 import re
 import socket
+from ldap3 import Server, Connection, MODIFY_REPLACE,MODIFY_ADD
+from ldap3.core.results import RESULT_SUCCESS
+from ldap3.core.exceptions import *
+
 
 import subprocess
 from subprocess import CalledProcessError, TimeoutExpired
@@ -18,69 +21,94 @@ from squarepay import dispense
 
 
 log = logging.getLogger('ldap')
-# load config
 
+# load config
 ldap_uri = getattr(settings, 'AUTH_LDAP_SERVER_URI')
-ldap_search_dn = getattr(settings, 'LDAP_USER_SEARCH_DN')
+ldap_user_dn = getattr(settings, 'LDAP_USER_SEARCH_DN')
+ldap_base_dn = getattr(settings, 'LDAP_BASE_DN')
 ldap_bind_dn = getattr(settings, 'LDAP_BIND_DN')
 ldap_bind_secret = getattr(settings, 'LDAP_BIND_SECRET')
-ldap_opts = getattr(settings, 'AUTH_LDAP_GLOBAL_OPTIONS')
 
+maxuid_dn = "CN=uccdomayne,CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System,"+ldap_base_dn
 
 #initalise ldap instace
-_ldap_inst = ldap.initialize(ldap_uri)
-for option,value in ldap_opts.items():
-	_ldap_inst.set_option(option,value)
-
-_ldap_inst.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
+_ldap_inst = Connection(
+	Server(ldap_uri),
+	client_strategy='SYNC',
+	user=ldap_bind_dn,
+	password=ldap_bind_secret,
+	raise_exceptions=True,
 
+	)
 
+# get the ldap instance and bind if required
 def get_ldap_instance():
-	try:
-		_ldap_inst.bind(ldap_bind_dn, ldap_bind_secret)
-	except ldap.INVALID_CREDENTIALS:
-		log.error("LDAP: Invalid bind credentials")
-	except ldap.SERVER_DOWN:
-		log.error("LDAP: Cannot Contact LDAP server")
-
+	if not _ldap_inst.bound:
+		try:
+			_ldap_inst.bind()
+		except LDAPInvalidCredentialsResult:
+			log.error("LDAP: Invalid bind credentials")
+			raise
 	return _ldap_inst
 
-def get_user_attrs(username, attrs):
-	# TODO verify bind
+def get_ldap_attrs(dn, filter, limit, attrs):
 	ld = get_ldap_instance()
-	filter = "cn=" + username
 
-	result = ld.search_s(ldap_search_dn, ldap.SCOPE_SUBTREE, filter, attrs)
-	if len(result) > 1:
-		# multiple accounts matched, this is a problem
-		return ldap.NO_UNIQUE_ENTRY
-	if len(result) == 0:
-		return ldap.NO_SUCH_OBJECT
-	return result[0]; 
+	ld.search(dn, filter, size_limit=limit, attributes=attrs)
+	result = ld.result
+	# fetch matched objects on success
+	if (result['result'] == RESULT_SUCCESS):
+		entries = ld.entries
+	else:
+		# otherwise raise an exception
+		raise LDAPOperationResult(
+			result=result['result'],
+			description=result['description'],
+			dn=result['dn'],
+			message=result['message'],
+			response_type=result['type'])
+
+	if len(entries) == 0:
+		raise LDAPNoSuchObjectResult()
+
+	return entries;
+
+def get_user_attrs(username, attrs):
+	# find the user
+	filter = "(cn=" + username + ')'
+
+	result = get_ldap_attrs(ldap_user_dn, filter, 1, attrs)
+
+	return result[0];
+
+def get_maxuid():
+	ld = get_ldap_instance()
+	filter = "(cn=*)"
+	attrs = ['msSFU30MaxUidNumber']
+	result = get_ldap_attrs(maxuid_dn, filter, 1, attrs)
 
+	return result[0]
 
 def get_account_lock_status(username):
 	ld = get_ldap_instance()
 	try:
 		result = get_user_attrs(username, ['userAccountControl'])
-	finally:
-		ld.unbind()
+	# user does not exist
+	except LDAPNoSuchObjectResult:
+		return None
+	# return UAC flag 0x002 ('ACCOUNT_DISABLE')
 	return bool(result[1]['userAccountControl'] & 0x002)
 
 def validate_username(value : str):
 	# note: slug validator ensures that username only contains [a-z0-9_-]
 	# usernames can't begin with a numeric
 	if not value[0].isalpha():
-		raise ValidationError(
-			_('Username must begin with a letter')
-		)
+		raise ValidationError(_('Username must begin with a letter'))
 	# ensure username is lowercase
-	elif not value.islower():
-		raise ValidationError(
-			_('Username cannot contain uppercase characters')
-		)
+	if not value.islower():
+		raise ValidationError(_('Username cannot contain uppercase characters'))
 	# check if the user exists, this test should catch *most* cases
-	if subprocess.call(["id", value]) == 0:
+	if subprocess.call(["id", value], stderr=subprocess.DEVNULL) == 0:
 		raise ValidationError(_('Username already taken (passwd)'))
 
 	# usernames cannot conflict with hostnames
@@ -90,20 +118,14 @@ def validate_username(value : str):
 			_('Username already taken (CNAME)')
 		)
 	except socket.gaierror:
-		pass	
-
+		pass
 	# lookup user in ldap, required because not all users are mapped to *nix users
 	try:
-		if get_user_attrs(value, ['cn']) != ldap.NO_SUCH_OBJECT:
-			raise ValidationError(
-				_('Username already taken (AD)')
-			)
-	except ldap.LDAPError:
-		log.error("Network error, cannot verify username")
-		raise ldap.OPERATIONS_ERROR
-
-
-		
+		get_user_attrs(value, None)
+	except LDAPNoSuchObjectResult:
+		pass
+	else:
+		raise ValidationError(_('Username already taken (AD)'))
 
 
 # locks the specified User Account by performing the following actions:
@@ -117,19 +139,20 @@ def lock_account(username):
 	try:
 		# fetch current uac
 		result = get_user_attrs(username, ['userAccountControl'])
-		
-		dn = result[0]
-		uac = result[1]['userAccountControl'] | 0x002 # set ACCOUNTDISABLE
-		actions = [
-			(ldap.MOD_REPLACE, "userAccountControl", uac),
-			(ldap.MOD_REPLACE, "userShell", "/etc/locked" + str(today.year))
-		]
+
+		dn = result.entry_dn
+		uac = result['userAccountControl'] | 0x002 # set ACCOUNTDISABLE
+		actions = {
+			"userAccountControl": [(MODIFY_REPLACE,[uac])],
+			"userShell": [(MODIFY_REPLACE,["/etc/locked"+str(today.year)])]
+			}
 		# write record
 		ld.modify(dn, actions)
-
+	except LDAPOperationResult:
+		raise
 	finally:
 		ld.unbind()
-	
+
 	reason = "account locked by uccportal on %s" % str(today)
 	dispense.set_dispense_flag(username, 'disabled', reason)
 
@@ -140,31 +163,108 @@ def unlock_account(username):
 	try:
 		# fetch current uac
 		result = get_user_attrs(username, ['userAccountControl'])
-		
+
 		dn = result[0]
 		uac = result[1]['userAccountControl'] & ~0x002 # clear ACCOUNTDISABLE
-		actions = [
-			(ldap.MOD_REPLACE, "userAccountControl",uac),
-			(ldap.MOD_REPLACE, "userShell", "/bin/zsh")
-		]
+		actions = {
+			"userAccountControl": [(MODIFY_REPLACE,[uac])],
+			"userShell": [(MODIFY_REPLACE,["/bin/zsh"])]
+			}
 		# write record
 		ld.modify(dn, actions)
-
+	except LDAPOperationResult:
+		raise
 	finally:
 		ld.unbind()
-	
 	reason = "account unlocked by uccportal on %s" % str(today)
 	dispense.set_dispense_flag(username, '!disabled', reason)
 
 # Account creation steps:
-# 
-def create_account(member, passwd):
-	username = "changeme";
-	log.info("I: creating new account for %s (%s %s)")
+#
+def create_ad_user(form_data, member):
+	log.info("I: creating new account for %s (%s)")
+
+	# store user details
+	# TODO add overides
+	username=form_data['username']
+	displayName = member.first_name + ' ' + member.last_name
+	dn = 'CN=' + username +','+ ldap_user_dn
+
+	# enclose password in quotes and convert to utf16 as required:
+	# https://msdn.microsoft.com/en-us/library/cc223248.aspx
+	quotedpass = '"'+ form_data['password']+'"'
+	utf16pass = quotedpass.encode('utf-16-le')
+
+	# generate uid
+	try:
+		result = get_maxuid()
+	except:
+		log.error("LDAP: cannot find base uid")
+		return False
+
+	maxuid = int(result.msSFU30MaxUidNumber.value)
+
+	# gets all uids >= maxuid
+	# this is done so that we don't encounter the 1000 item limit to ad queries
+	entries = get_ldap_attrs(ldap_user_dn,"(uidNumber>=%s)" % maxuid, 100, ['uidNumber'])
+
+	# generate a new uid
+	uids = []
+	for user in entries:
+		uids.append(int(user.uidNumber.value))
+
+	uids.sort()
+	# use max uid if it is free
+	if uids[0] != maxuid:
+		newuid = str(maxuid)
+	else:
+		prev = uids[0]
+		for uid in uids:
+			if uid - prev > 1:
+				newuid = uid + 1
+				break;
+			prev = uid
+		#increment uid
+		newuid = str(prev + 1)
+
+	# sanity check: make sure the uid is free
+	if subprocess.call(["id", newuid], stderr=subprocess.DEVNULL) == 0:
+		log.error("LDAP: uid already taken")
+		return False
+
+	# create the new user struct
+	objclass = ['top','posixAccount','person','organizationalPerson','user']
+	attrs = {
+		'cn' : username,
+		'sAMAccountName' : username,
+		'givenName' : member.first_name,
+		'sn': member.last_name,
+		'displayName': displayName,
+		'userAccountControl' : '512',
+		'unixHomeDirectory' : "/home/ucc/" + username,
+		'loginShell' : '/bin/zsh',
+		'gidNumber' : '20021',
+		'uidNumber' : newuid,
+		'gecos' : displayName,
+		'mail' : username + '@ucc.gu.uwa.edu.au',
+		'unicodePwd': utf16pass
+	}
+
+	# commit the new user to AD
+	ld = get_ldap_instance()
+	result = ld.add(dn, objclass, attrs)
+	if not result:
+		log.error("LDAP: user add failed")
+		return False
+
+	# set maxuid
+	result = ld.modify(maxuid_dn, {'msSFU30MaxUidNumber': [(MODIFY_REPLACE, newuid)]})
+	if not result:
+		log.warning("LDAP: user created but msSFU30MaxUidNumber not update")
 
+	ld.unbind();
+	return True;
 
-	
-	return None;
 def create_homes(member):
 	return
 def set_email_forwarding(member, addr):