Authenticating to Cloud Identity's LDAP interface
Secure LDAP is a Cloud Identity feature that lets it emulate an LDAP server. From an application’s perspective, Secure LDAP makes Cloud Identity look somewhat similar to Active Directory – but authentication works a little differently.
Before we can query Active Directory using LDAP, we typically need to authenticate first. We can do that by providing a username and password (simple authentication) or by using NTLM or Kerberos (SASL authentication).
Active Directory doesn’t require us to register LDAP clients in advance. Because clients aren’t registered or known in advance, they also don’t have their own credentials. The way for clients to authenticate to Active Directory is to present user credentials.
LDAP clients often act on an end user’s behalf when accessing an LDAP server. For example, when we’re running ADSI Edit, we want the tool to perform queries as us. So letting the tool use our user credentials makes perfect sense. But sometimes applications don’t act on behalf of an end user and instead act on their own behalf. In Active Directory, the way to deal with such applications is to create a dedicated “service” user account for that application.
Registering clients
Cloud Identity distinguishes between client authentication and user authentication: Each LDAP client first has to authenticate itself before it can (optionally) present user credentials to “impersonate” that user account.
To facilitate client authentication, Cloud Identity requires us to register LDAP clients in advance. In return for registering an LDAP client, we get an X.509 certificate which uniquely identifies the LDAP client (and the Cloud Identity/Workspace account it belongs to). We then have to use this certificate as a mTLS client certificate whenever we connect to Secure LDAP.
Let’s take a look at how this works with Python and ldap3.
Setting up ldap3
To connect to Cloud Identity using ldap3, we first need to configure a Server
object. This entails:
- Setting the endpoint to
ldap.google.com:636
. Note that this endpoint is the same regardless of which Cloud Identity/Workspace account we’re using. - Specifying a modern TLS version (1.2 works fine)
Specifying the client certificate and key that we got when registering the LDAP client.
HOST = 'ldap.google.com' PORT = 636 def get_server_configuration(privateKeyFilePath, certificateFilePath): """ Configure server with TLS client authentication """ tls_configuration = ldap3.Tls( local_private_key_file=privateKeyFilePath, local_certificate_file=certificateFilePath, validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1_2) return ldap3.Server( HOST, port=PORT, connect_timeout=5, use_ssl=True, tls=tls_configuration)
Next, we need to perform an LDAP bind. We have two options to do this:
If our client acts on its own behalf with no end user involved, we can use SASL EXTERNAL authentication. This mechanism lets us tell LDAP “Look, I already authenticated on the TLS layer and I don’t want to authenticate any end user”.
def connect_sasl_external(server): """ Authenticate with certificate only """ connection = ldap3.Connection( server, authentication=ldap3.SASL, sasl_mechanism=ldap3.EXTERNAL, raise_exceptions=True, receive_timeout=20) connection.bind() return connection
If we do want to authenticate an end user, we can use SIMPLE authentication and pass a username and password:
def connect_simple_user(server, user, password): """ Authenticate with certificate and LDAP user """ connection = ldap3.Connection( server, user=user, password=password, authentication=ldap3.SIMPLE, raise_exceptions=True, receive_timeout=20) connection.bind(read_server_info=False) return connection
Notice that we’ve set an extra option
read_server_info=False
. This is necessary to prevent ldap3 from performing an(objectClass=*)
query during the bind which would be doomed to fail with a permission-denied error.
There is a third way to authenticate – but it only makes sense for third-party applications: Suppose we want to let a third-party application access Secure LDAP on its own behalf, but the application doesn’t support SASL EXTERNAL binds. Then we can let the application use a SIMPLE bind in combination with application-specific credentials, which we can generate when registering the client.
def connect_simple_app(server, appUser, appPassword):
""" Authenticate with certificate and LDAP user """
connection = ldap3.Connection(
server,
user=appUser,
password=appPassword,
authentication=ldap3.SIMPLE,
raise_exceptions=True,
receive_timeout=20)
connection.bind()
return connection
Listing naming contexts and performing queries
Cloud Identity uses a separate naming context for each (secondary) domain. In case we’re not sure yet which context to use, we can query the list of naming contexts like this:
def list_naming_contexts(connection):
if connection.search(
search_filter="(objectClass=top)",
search_scope=ldap3.BASE,
search_base="",
attributes=["namingContexts"]):
return connection.entries[0]['namingContexts']
Equipped with the right the naming context, we’re ready to perform a query:
def search(connection, namingContext, query):
if connection.search(
search_filter=query,
search_base=namingContext,
attributes=["*"]):
for e in connection.entries:
print(e)
Notice that by specifying a naming context, we implicitly restrict the search to a single domain. It’s currently not possible to search users and groups across multiple domains (of a single Cloud Identity/Workspace account).