diff options
Diffstat (limited to 'doc/wiki/Design.AuthProcess.txt')
-rw-r--r-- | doc/wiki/Design.AuthProcess.txt | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/doc/wiki/Design.AuthProcess.txt b/doc/wiki/Design.AuthProcess.txt new file mode 100644 index 0000000..dd4fc89 --- /dev/null +++ b/doc/wiki/Design.AuthProcess.txt @@ -0,0 +1,365 @@ +Authentication process design +============================= + +See <Design.Processes.txt> for an overview of how the authentication process +works. + +There are four major classes in the code: + + * 'struct mech_module': Authentication mechanism + * 'struct password_scheme': Password scheme + * 'struct passdb_module': Password database + * 'struct userdb_module': User database + +There are many implementations for each of these, and it's simple to add more +of them. They can also be added as plugins, although the current plugin loading +code doesn't allow loading authentication mechanisms cleanly, and it's not +possible to add new credentials (see below). + +The code flow usually goes like: + + * Dovecot-auth listens for new authentication client connections (the listener + socket is created by master process and passed in MASTER_SOCKET_FD -> + 'main.c:main_init()' -> + 'auth-master-connection.c:auth_master_listener_add()') + * A new authentication client connects via UNIX socket + ('auth-master-connection.c:auth_master_listener_accept()' -> + 'auth-client-connection.c:auth_client_connection_create()') + * Authentication client begins an authentication + ('auth-client-connection.c:auth_client_input()' -> + 'auth_client_handle_line()' -> + 'auth-request-handler.c:auth_request_handler_auth_begin()' [ -> + 'auth-request.c:auth_request_new()']) + * Authentication mechanism backend handles it ('mech->auth_initial()' and + 'mech->auth_continue()' in 'mech-*.c') + * The mechanism looks up the password from passdb + ('auth-request.c:auth_request_verify_plain()' and + 'auth_request_lookup_credentials()') and the password scheme code to + verifies it ('password-scheme.c:password_verify()' and + 'password_generate()') + * If user is logging in, the user information is looked up from the userdb + ('auth-master-connection.c:master_input()' -> 'master_input_request()' -> + 'auth-request-handler.c:auth_request_handler_master_request()' -> + 'auth-request.c:auth_request_lookup_user()') + * The authentication may begin new authentication requests even before the + existing ones are finished. + +It's also possible to request a userdb lookup directly, for example Dovecot's +<deliver> [LDA.txt] needs that. The code path for that goes +'auth-master-connection.c:master_input()' -> 'master_input_user()' -> +'auth-request.c:auth_request_lookup_user()'. + +Authentication mechanisms +------------------------- + +These are <SASL> [Sasl.txt] authentication mechanism implementations. See +<Authentication.Mechanisms.txt> for a list of mechanisms supported by Dovecot. + +A new mechanism is created by filling a 'struct mech_module' (in 'mech.h') and +passing it to 'mech_register_module()'. The struct fields are: + +mech_name: + The public name of the mechanism. This is shown to clients in the IMAP, POP3 + and SMTP capability lists. If you create a new non-standard mechanism, please + prefix it with "X-". + +flags: + Describes how secure the mechanism is. Also 'MECH_SEC_PRIVATE' flag specifies + that the mechanism shouldn't be advertised in the capability list. This is + currently used only for APOP mechanism, which is defined by the POP3 protocol + itself. + +passdb_need_plain: + This mechanism uses passdb's 'verify_plain()' function to verify the + password's validity. This means that the mechanism has access to the + plaintext password. This is true only for plaintext mechanisms such as PLAIN + and LOGIN. The main purpose of this flag is to make dovecot-auth complain at + startup if there are no passdbs defined in the configuration file. Note that + a configuration without any passdbs is valid with eg. GSSAPI mechanism which + doesn't need a passdb at all. + +passdb_need_credentials: + This mechanism uses passdb's 'lookup_credentials()' function. See below for + description of the credentials. + +auth_new(): + Allocates a new 'struct auth_request'. Typically with more complex mechanisms + it really allocates a 'struct <mech>_auth_request' which contains 'struct + auth_request' as the first field, followed by mechanism-specific fields. + +auth_initial(request, data, data_size): + This begins the authentication, data and data_size containing the initial + response sent by the client (decoded, not in base64). Call + 'request->callback()' once you're done (see below). + +auth_continue(request, data, data_size): + Continues the authentication. Works the same as 'auth_initial()'. + +auth_free(): + Free the request. Usually all the memory allocations for the request should + be allocated from 'request->pool', so you can use 'mech_generic_auth_free()' + which simply frees the pool. + +'auth_initial()' and 'auth_continue()' continue or finish the authentication by +calling 'request->callback()': + +---%<------------------------------------------------------------------------- +typedef void mech_callback_t(struct auth_request *request, + enum auth_client_result result, + const void *reply, size_t reply_size); +---%<------------------------------------------------------------------------- + +The 'reply' and 'reply_size' contain the server's mechanism-specific reply to +the client. If there is no need to return anything (which is usually the case +with the "success" reply), the 'reply_size' can be 0. The 'result' parameter is +one of: + + * AUTH_CLIENT_RESULT_CONTINUE: Client can continue the authentication. The + reply contains the mechanism-specific reply sent to the client. + * AUTH_CLIENT_RESULT_SUCCESS: Authentication successful. The reply is usually + empty. + * AUTH_CLIENT_RESULT_FAILURE: Authentication failed. The reply is always + ignored. + +The 'request->callback()' should actually be called directly only for +continuation requests (a new function should probably be added for this as +well). For success and failure replies, you should instead use one of these +functions: + + * 'auth_request_success()' + * 'auth_request_fail()' + * 'auth_request_internal_failure()': Use this if you couldn't figure out if + the authentication succeeded or failed, for example because passdb lookup + returned internal failure. + +SASL authentication in general works like: + + 1. Client begins the authentication, optionally sending an "initial response", + meaning some data that the mechanism sees in 'auth_initial()'. + * Note that not all protocols support the initial response. For example + IMAP supports it only if the server implements SASL-IR extension. + Because of this mechanisms, such as PLAIN, support doing the + authentication either in 'auth_initial()' or in 'auth_continue()'. + * If the client initiates the authentication (ie. server's initial reply + is empty, such as with PLAIN mechanism) you can use + 'mech_generic_auth_initial()' instead of implementing your own. + 2. Server processes the authentication request and replies back with + 'request->callback()'. + * If the authentication failed, it's placed into 'auth_failures_buf' + unless 'request->no_failure_delay=TRUE'. The failures are flushed from + the buffer once every 2 seconds to clients and 'mechanism->auth_free()' + is called. + * If the authentication succeeded and + * there is a master connection associated with the request (IMAP/POP3 + login), the authentication now waits for master connection to do a + verification request. If this for some reason doesn't happen in + 'AUTH_REQUEST_TIMEOUT' seconds (3,5 mins), it's freed. + * there isn't a master connection (SMTP AUTH), the authentication is + freed immediately. + 3. Client processes the reply: + * If the authentication continues, it sends back more data which is + processed in 'auth_continue()'. Goto 2. + * If the authentication failed, it's done. + 4. If the authentication succeeded, the client requests a login from the + master process, which in turn requests verification from the auth process. + * Besides verifying the authentication, dovecot-auth also does a userdb + lookup to return the userdb information to master. + * If the verification fails (normally because userdb lookup fails), the + client gets "internal authentication failure" + * If the verification succeeds, the user is now logged in + * In either case, 'mechanism->auth_free()' is called now. + +Credentials +----------- + +Most of the non-plaintext mechanisms need to verify the authentication by using +a special hash of the user's password. So either the passdb credentials lookup +returns a plaintext password from which the hash can be created, or the hash +directly. The plaintext to hash conversion is done by calling +'password_generate' function of the password scheme. + +Unfortunately the list of allowed credentials is currently hardcoded in 'enum +passdb_credentials'. The enum values are mapped to password scheme strings in +'passdb_credentials_to_str()'. Some day the enum will be removed so plugins can +add new mechanisms. Besides the mechanism-specific credentials, the enum +contains: + +_PASSDB_CREDENTIALS_INTERNAL: + I don't remember why this really exists. It should probably be called + _PASSDB_CREDENTIALS_INVALID or something and used only by some asserts.. + +PASSDB_CREDENTIALS_PLAINTEXT: + Request a plaintext password. + +PASSDB_CREDENTIALS_CRYPT: + Request the password in any scheme. This is especially useful if you only + want to verify a user's existence in a passdb. Used by <static userdb> + [UserDatabase.Static.txt] in userdb lookups. + +Password schemes +---------------- + +'struct password_scheme' has fields: + +name: + Name of the scheme. This only shows up in configuration files and maybe in + the passwords stored in passdb ("{scheme_name}password_hash"). + +password_verify(plaintext, password, user): + Returns TRUE if 'password' hash matches the plaintext password given in + 'plaintext' parameter. If the password hash depends on the username (eg. with + DIGEST-MD5), the 'user' parameter can also be used. + +password_generate(plaintext, user): + Returns the password hash for given plaintext password and username. + +You can create a new password scheme by simply creating a 'struct +password_scheme' named '<module_name>_scheme', compiling a shared object and +placing it to '$moduledir/auth/' directory. + +Password databases +------------------ + +See <PasswordDatabase.txt> for a description of passdbs and a list of already +implemented ones. + +'struct passdb_module' contains fields: + +cache_key: + A string containing <variables> [Variables.txt]. When expanded, it uniquely + identifies a passdb lookup. This is '%u' when the passdb lookup validity + depends only on the username. With more complex databases such as SQL and + LDAP this is created dynamically based on the password query in the + configuration file. If there are multiple variables, they should be separated + so that their contents don't get mixed, for example '%u<TAB>%r<TAB>%l'. + 'auth_cache_parse_key()' can be used to easily create a cache key from a + query string. + +default_pass_scheme: + The default scheme to use when it's not explicitly specified with a + "{scheme}" prefix. + +blocking: + If TRUE, the lookup is done in dovecot-auth worker process. This should be + used if the lookup may block. + +iface.preinit(auth_passdb, args): + Allocate 'struct passdb_module' and return it. This function is called before + chrooting and before privileges are dropped from dovecot-auth process, so if + should do things like read the configuration file.'auth_passdb' is typically + used for getting a memory pool and looking up some global settings such as + 'auth_passdb->auth->verbose_debug'. 'args' contains the args parameter in + configuration file. + +iface.init(module, args): + The privileges have been dropped before calling this. 'module' contains the + structure returned by 'preinit()'. 'args' is the same as in 'preinit()'. + Typically this function will do things like connect to the database. + +iface.deinit(module): + Close the connection to the password database and free all the memory you + used. + +iface.verify_plain(auth_request, password, callback): + Check if the given plaintext password matches. 'auth_request->credentials = + -1' always. When the verification is done, call the given callback with the + result in 'result' parameter. + +iface.lookup_credentials(auth_request, callback): + Look up the password credentials. 'auth_request->credentials' contains the + credentials that the mechanism wants. When the lookup is finished, call the + given callback with the result in 'result' parameter, and if the lookup was + successful the credentials in 'password' parameter. + +Plaintext authentication mechanisms typically call 'verify_plain()', which is +possible to implement with all the passdbs. Non-plaintext mechanisms typically +call 'lookup_credentials()', which isn't possible to implement always (eg. +PAM). If it's not possible to implement 'lookup_credentials()', you can leave +the pointer to it NULL. + +If the passdb uses connections to external services, it's preferred that they +use non-blocking connections. Dovecot does this whenever possible (PostgreSQL +and LDAP for example). If it's not possible, set 'blocking = TRUE'. + +With both functions 'auth_request->passdb->passdb' contains the passdb_module +returned by your 'preinit()' function. 'auth_request->user' contains the +username whose password we're verifying. You don't need to worry about <master +users> [MasterUsers.txt] here. It's also possible to use any other fields in +'auth_request' to do the lookup, such as 'service', 'local_ip' or 'remote_ip' +if they exist. Often you want to let user to configure the lookup with +<variables> [Variables.txt] (eg. SQL query). In that case you can use +'auth_request_get_var_expand_table()' to retrieve the variable table for +'var_expand()'. + +The passdb lookup can return one of the following results: + +PASSDB_RESULT_INTERNAL_FAILURE: + The lookup failed. For example SQL server is down. + +PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + 'lookup_credentials()' requested a scheme which isn't in the passdb + +PASSDB_RESULT_USER_UNKNOWN: + The user doesn't exist in the database. + +PASSDB_RESULT_USER_DISABLED: + The user is disabled either entirely, or for this specific login (eg. only + POP3 logins allowed). This isn't commonly implemented in passdbs. + +PASSDB_RESULT_PASS_EXPIRED: + The user's password had expired. This isn't commonly implemented in passdbs. + +PASSDB_RESULT_PASSWORD_MISMATCH: + The password given in 'verify_plain()' wasn't valid. + +PASSDB_RESULT_OK: + Success. + +User databases +-------------- + +See <UserDatabase.txt> for a description of userdbs and a list of already +implemented ones. + +'struct userdb_module' is very similar to 'struct passdb_module'. The lookup +callback is a bit different though: + +---%<------------------------------------------------------------------------- +typedef void userdb_callback_t(enum userdb_result result, + struct auth_stream_reply *reply, + struct auth_request *request); +---%<------------------------------------------------------------------------- + +'result' contains one of: + +USERDB_RESULT_INTERNAL_FAILURE: + The lookup failed. For example SQL server is down. + +USERDB_RESULT_USER_UNKNOWN: + The user doesn't exist in the database. + +USERDB_RESULT_OK: + Success. + +There is no equivalent for PASSDB_RESULT_USER_DISABLED currently. Practically +the userdb result is used only by Dovecot's <deliver> [LDA.txt] to figure out +if the user exists or not. When logging in with IMAP or POP3, the user's +existence was already checked in passdb lookup, so only in rare conditions when +a user is logging in at the same time as it's being deleted, the userdb result +is USER_UNKNOWN. + +The 'reply' parameter contains the username (it's allowed to be different from +the looked up username) and a list of key=value pairs that were found from the +userdb. The userdb should make sure that at least "uid" and "gid" keys were +returned. Here's an example code based on passwd userdb: + +---%<------------------------------------------------------------------------- +reply = auth_stream_reply_init(auth_request); +auth_stream_reply_add(reply, NULL, pw->pw_name); +auth_stream_reply_add(reply, "uid", dec2str(pw->pw_uid)); +auth_stream_reply_add(reply, "gid", dec2str(pw->pw_gid)); +auth_stream_reply_add(reply, "home", pw->pw_dir); +callback(USERDB_RESULT_OK, reply, auth_request); +---%<------------------------------------------------------------------------- + +(This file was created from the wiki on 2019-06-19 12:42) |