Chapter 34 - The plaintext authenticator
The plaintext authenticator can be configured to support the PLAIN and LOGIN authentication mechanisms, both of which transfer authentication data as plain (unencrypted) text (though base64 encoded). The use of plain text is a security risk; you are strongly advised to insist on the use of SMTP encryption (see chapter 43) if you use the PLAIN or LOGIN mechanisms. If you do use unencrypted plain text, you should not use the same passwords for SMTP connections as you do for login accounts.
1. Avoiding cleartext use
The following generic option settings will disable plaintext authenticators when TLS is not being used:
server_advertise_condition = ${if def:tls_in_cipher } client_condition = ${if def:tls_out_cipher}
Note: a plaintext SMTP AUTH done inside TLS is not vulnerable to casual snooping, but is still vulnerable to a Man In The Middle attack unless certificates (including their names) have been properly verified.
2. Plaintext server options
When configured as a server, plaintext uses the following options:
server_condition | Use: authenticators | Type: string† | Default: unset |
This is actually a global authentication option, but it must be set in order to configure the plaintext driver as a server. Its use is described below.
server_prompts | Use: plaintext | Type: string list† | Default: unset |
The contents of this option, after expansion, must be a colon-separated list of prompt strings. If expansion fails, a temporary authentication rejection is given.
3. Using plaintext in a server
When running as a server, plaintext performs the authentication test by expanding a string. The data sent by the client with the AUTH command, or in response to subsequent prompts, is base64 encoded, and so may contain any byte values when decoded. If any data is supplied with the command, it is treated as a list of strings, separated by NULs (binary zeros), the first three of which are placed in the expansion variables $auth1, $auth2, and $auth3 (neither LOGIN nor PLAIN uses more than three strings).
For compatibility with previous releases of Exim, the values are also placed in the expansion variables $1, $2, and $3. However, the use of these variables for this purpose is now deprecated, as it can lead to confusion in string expansions that also use them for other things.
If there are more strings in server_prompts than the number of strings supplied with the AUTH command, the remaining prompts are used to obtain more data. Each response from the client may be a list of NUL-separated strings.
Once a sufficient number of data strings have been received, server_condition is expanded. If the expansion is forced to fail, authentication fails. Any other expansion failure causes a temporary error code to be returned. If the result of a successful expansion is an empty string, “0”, “no”, or “false”, authentication fails. If the result of the expansion is “1”, “yes”, or “true”, authentication succeeds and the generic server_set_id option is expanded and saved in $authenticated_id. For any other result, a temporary error code is returned, with the expanded string as the error text.
Warning: If you use a lookup in the expansion to find the user’s password, be sure to make the authentication fail if the user is unknown. There are good and bad examples at the end of the next section.
4. The PLAIN authentication mechanism
The PLAIN authentication mechanism (RFC 2595) specifies that three strings be sent as one item of data (that is, one combined string containing two NUL separators). The data is sent either as part of the AUTH command, or subsequently in response to an empty prompt from the server.
The second and third strings are a user name and a corresponding password. Using a single fixed user name and password as an example, this could be configured as follows:
fixed_plain: driver = plaintext public_name = PLAIN server_prompts = : server_condition = \ ${if and {{eq{$auth2}{username}}{eq{$auth3}{mysecret}}}} server_set_id = $auth2
Note that the default result strings from if (“true” or an empty string) are exactly what we want here, so they need not be specified. Obviously, if the password contains expansion-significant characters such as dollar, backslash, or closing brace, they have to be escaped.
The server_prompts setting specifies a single, empty prompt (empty items at the end of a string list are ignored). If all the data comes as part of the AUTH command, as is commonly the case, the prompt is not used. This authenticator is advertised in the response to EHLO as
250-AUTH PLAIN
and a client host can authenticate itself by sending the command
AUTH PLAIN AHVzZXJuYW1lAG15c2VjcmV0
As this contains three strings (more than the number of prompts), no further data is required from the client. Alternatively, the client may just send
AUTH PLAIN
to initiate authentication, in which case the server replies with an empty prompt. The client must respond with the combined data string.
The data string is base64 encoded, as required by the RFC. This example,
when decoded, is <NUL>username
<NUL>mysecret
, where <NUL>
represents a zero byte. This is split up into three strings, the first of which
is empty. The server_condition option in the authenticator checks that the
second two are username
and mysecret
respectively.
Having just one fixed user name and password, as in this example, is not very realistic, though for a small organization with only a handful of authenticating clients it could make sense.
A more sophisticated instance of this authenticator could use the user name in $auth2 to look up a password in a file or database, and maybe do an encrypted comparison (see crypteq in chapter 11). Here is a example of this approach, where the passwords are looked up in a DBM file. Warning: This is an incorrect example:
server_condition = \ ${if eq{$auth3}{${lookup{$auth2}dbm{/etc/authpwd}}}}
The expansion uses the user name ($auth2) as the key to look up a password, which it then compares to the supplied password ($auth3). Why is this example incorrect? It works fine for existing users, but consider what happens if a non-existent user name is given. The lookup fails, but as no success/failure strings are given for the lookup, it yields an empty string. Thus, to defeat the authentication, all a client has to do is to supply a non-existent user name and an empty password. The correct way of writing this test is:
server_condition = ${lookup{$auth2}dbm{/etc/authpwd}\ {${if eq{$value}{$auth3}}} {false}}
In this case, if the lookup succeeds, the result is checked; if the lookup fails, “false” is returned and authentication fails. If crypteq is being used instead of eq, the first example is in fact safe, because crypteq always fails if its second argument is empty. However, the second way of writing the test makes the logic clearer.
5. The LOGIN authentication mechanism
The LOGIN authentication mechanism is not documented in any RFC, but is in use in a number of programs. No data is sent with the AUTH command. Instead, a user name and password are supplied separately, in response to prompts. The plaintext authenticator can be configured to support this as in this example:
fixed_login: driver = plaintext public_name = LOGIN server_prompts = User Name : Password server_condition = \ ${if and {{eq{$auth1}{username}}{eq{$auth2}{mysecret}}}} server_set_id = $auth1
Because of the way plaintext operates, this authenticator accepts data supplied with the AUTH command (in contravention of the specification of LOGIN), but if the client does not supply it (as is the case for LOGIN clients), the prompt strings are used to obtain two data items.
Some clients are very particular about the precise text of the prompts. For example, Outlook Express is reported to recognize only “Username:” and “Password:”. Here is an example of a LOGIN authenticator that uses those strings. It uses the ldapauth expansion condition to check the user name and password by binding to an LDAP server:
login: driver = plaintext public_name = LOGIN server_prompts = Username:: : Password:: server_condition = ${if and{{ \ !eq{}{$auth1} }{ \ ldapauth{\ user="uid=${quote_ldap_dn:$auth1},ou=people,o=example.org" \ pass=${quote:$auth2} \ ldap://ldap.example.org/} }} } server_set_id = uid=$auth1,ou=people,o=example.org
We have to check that the username is not empty before using it, because LDAP does not permit empty DN components. We must also use the quote_ldap_dn operator to correctly quote the DN for authentication. However, the basic quote operator, rather than any of the LDAP quoting operators, is the correct one to use for the password, because quoting is needed only to make the password conform to the Exim syntax. At the LDAP level, the password is an uninterpreted string.
6. Support for different kinds of authentication
A number of string expansion features are provided for the purpose of interfacing to different ways of user authentication. These include checking traditionally encrypted passwords from /etc/passwd (or equivalent), PAM, Radius, ldapauth, pwcheck, and saslauthd. For details see section 11.7.
7. Using plaintext in a client
The plaintext authenticator has two client options:
client_ignore_invalid_base64 | Use: plaintext | Type: boolean | Default: false |
If the client receives a server prompt that is not a valid base64 string, authentication is abandoned by default. However, if this option is set true, the error in the challenge is ignored and the client sends the response as usual.
client_send | Use: plaintext | Type: string† | Default: unset |
The string is a colon-separated list of authentication data strings. Each string is independently expanded before being sent to the server. The first string is sent with the AUTH command; any more strings are sent in response to prompts from the server. Before each string is expanded, the value of the most recent prompt is placed in the next $auth<n> variable, starting with $auth1 for the first prompt. Up to three prompts are stored in this way. Thus, the prompt that is received in response to sending the first string (with the AUTH command) can be used in the expansion of the second string, and so on. If an invalid base64 string is received when client_ignore_invalid_base64 is set, an empty string is put in the $auth<n> variable.
Note: You cannot use expansion to create multiple strings, because splitting takes priority and happens first.
Because the PLAIN authentication mechanism requires NUL (binary zero) bytes in the data, further processing is applied to each string before it is sent. If there are any single circumflex characters in the string, they are converted to NULs. Should an actual circumflex be required as data, it must be doubled in the string.
This is an example of a client configuration that implements the PLAIN authentication mechanism with a fixed user name and password:
fixed_plain: driver = plaintext public_name = PLAIN client_send = ^username^mysecret
The lack of colons means that the entire text is sent with the AUTH command, with the circumflex characters converted to NULs.
Note that due to the ambiguity of parsing three consectutive circumflex characters there is no way to provide a password having a leading circumflex.
A similar example that uses the LOGIN mechanism is:
fixed_login: driver = plaintext public_name = LOGIN client_send = : username : mysecret
The initial colon means that the first string is empty, so no data is sent with the AUTH command itself. The remaining strings are sent in response to prompts.