Requests for comment/AuthManager

The current authentication and authorization system is very limited and restricted in terms of the allowed customization, for system administrators, core developers, and extension developers.

Current problems
Our current authn system consists of:


 * User::crypt, User::comparePasswords, User::loadFromSession, etc.
 * The AuthPlugin interface and $wgAuth

This combination of completely hard-coded code paths has led to a number of restrictions and problems:
 * Only one external authentication service can be used at a time
 * Users in an external service must be identified by a username and password
 * There is a massive mix of concerns within the User class, which handles model logic as well as authentication and session logic

This RFC hopes to fix just a section of this problem: authentication. This will be done by deprecating the AuthPlugin system and replacing it will a comprehensive authentication layer. For scope, we're defining authentication as "how to get a session cookie for a logged-in session".

Use cases
In preparing this RfC, the following use cases were considered:
 * Local MediaWiki authentication
 * Authentication services like LDAP or CentralAuth
 * Fallback authentication (if user doesn't exist in CentralAuth, look in local database)
 * Federated services like Google, OpenID
 * Additional authentication requirements like two factor
 * Post authentication steps like an audit log (E:AccountAudit)
 * Pre-authentication requirements like rate limiting or captchas
 * Combinations of all of the above!

AuthManager


The AuthManager is a singleton instance that instantiates and maintains a collection of authentication components that collaborate to implement the core business logic of the authentication flow for a request.

The AuthManager is configured with:
 * optional PreAuthenticationProviders
 * at least one PrimaryAuthenticationProvider
 * optional SecondaryAuthenticationProviders
 * optional PostAuthenticationActions

The entry point that collects authentication data from the user (e.g. Special:Login or API action=login) needs to know what type of data it is collecting from the request. The entry point will be able to request a list of supported AuthenticationRequest types from the AuthManager to be able to construct its UI for the user. The AuthManager will be able to supply both AuthenticationRequest types supported for an initial login attempt and all types that might be used at any point in the authentication process.

An AuthenticationRequest is a typed object that both describes the input that must be collected for a particular type of authentication to be attempted and carries that input from the entry point to the AuthManager. A UsernamePasswordAuthenticationRequest would be typical, but other forms would be possible and necessary for PrimaryAuthenticationProviders that used other methods of authentication (e.g. OAuthAuthenticationRequest, GoogleAuthenticationRequest, X509AuthenticationRequest, etc).

Functionality of an AuthenticationRequest includes:
 * Statically providing specification of UI fields needed to create the request:
 * field name (i.e. POST parameter name)
 * field type (e.g. string, password, HTTP header, ???)
 * message keys for field labels and API help
 * Holding the values of the UI fields.
 * Holding the return-URL in case of a 'REDIRECT' response.

The entry point will create one or more AuthenticationRequest instances based on the user submission and pass them to the AuthManager for validation. AuthManager will dispatch the AuthenticationRequest to the PreAuthenticationProviders, PrimaryAuthenticationProviders, and SecondaryAuthenticationProviders as appropriate for the current state of the authentication process. At any point AuthManager may tell the entry point to redirect elsewhere or to request further AuthenticationRequest instances from the user; the entry point would do so, and later tell the AuthManager to resume the process.
 * First, all PreAuthenticationProviders will be asked to respond "PASS" or "FAIL". Assuming all pass,
 * Then all PrimaryAuthenticationProviders will be asked to respond. Eventually one will respond "PASS" or "FAIL", or all will "ABSTAIN". Assuming one passes,
 * At this point there are three possibilities:
 * The identity of the local user is known. Authentication continues as below.
 * The user name the local user should have is known, but that user doesn't exist (e.g. CentralAuth with no local account). If possible, the user may be created and authentication continue as below, otherwise the entry point may be instructed to transition to the account creation process.
 * The authentication is valid, but there is no local user known (e.g. successful Google auth with no local account mapping). The entry point will be instructed to either transition to the account creation process or to restart the authentication process.
 * Then all SecondaryAuthenticationProviders will be asked to respond in turn. Once all "PASS", the authenticated session is created and the user is logged in.
 * Then the PostAuthenticationActions will have opportunity to do their thing.

The responses referred to above will be in the form of an AuthenticationResponse, which can contain the following data:
 * A status field with values "PASS", "FAIL", "ABSTAIN", "REDIRECT", "UI".
 * "PASS" and "FAIL" indicate a final response.
 * "ABSTAIN" is a non-response. For PrimaryAuthenticationProviders it's effectively a soft fail, while for others it would be equivalent to a "PASS" (unless we decide it should be an invalid response and throw an exception).
 * "REDIRECT" and "UI" are requests for further user input. After one of these responses, the PrimaryAuthenticationProvider is not allowed to later "ABSTAIN".
 * Status message and a new AuthenticationRequest subtype, in case of a "UI" response.
 * Target URL, in case of a "REDIRECT" response.
 * Error message, in case of a "FAIL" response.
 * Opaque data sufficient to revalidate the authentication, in case of a "PASS" response from an PrimaryAuthenticationProvider.
 * Local user name or ID, in case of a "PASS" response from an PrimaryAuthenticationProvider. This might be omitted to indicate a successful federated authentication that is not yet mapped to any local user.

AuthManager will likely use the same AuthenticationResponse data structure for responses to the entry point.

PreAuthenticationProvider
Examples: Captcha checking, or rate limiting.

These would also be able to provide their own AuthenticationRequest types and validate them, returning "PASS" or "FAIL" responses.

PrimaryAuthenticationProvider
An PrimaryAuthenticationProvider is responsible for validating the credentials provided in a compatible AuthenticationRequest. Examples would be local MediaWiki password auth, CentralAuth-auth, LDAP, OAuth against a remote site, Google auth, etc. There can be multiple PrimaryAuthenticationProviders configured for a wiki, but only one can be authoritative per authentication request.

If there are multiple primary modules installed, the login form will provide options on which authentication method the user wishes to use (see Phabricator or StackOverflow for an example).

Each primary authentication module:
 * Has an account creation type of "link" or "create":
 * A "link" provider would have the user authenticate against the backend service, and then store a mapping from the backend-service user to the created User.
 * A "create" provider would create a new account in its backend store under the provided username. Would probably need to somehow specify UI fields such as "password".
 * Probably any "create" module could declare itself mandatory, and the UI fields for all mandatory "create" modules would be required. If there are no mandatory "create" modules, the user would have to pick one module ("create" or "link" types).
 * Once the new User is created, offer to link additional modules? Or let the user do that in Special:Preferences later?
 * Provides input requirements (for login form or API action=login)
 * When presented a compatible AuthenticationRequest: returns "PASS", "FAIL", "REDIRECT" or "ABSTAIN"
 * if "REDIRECT", a second call would be made following the redirect action that expects a "PASS" or "FAIL" response. This would be used for OAuth, Google and other external authn mechanisms that require interaction via HTTP with the requesting client.
 * if "PASS", this PrimaryAuthenticationProvider becomes responsible for providing details to construct a MediaWiki User object matching the validated credentials.

SecondaryAuthenticationProvider
Examples: two-factor, password expiry/reset, any kind of post-login notification.


 * These are in an ordered list.
 * At this point the user would have a partial session, we think we know who they are, but they can't do anything until they pass these modules.
 * Respond with "PASS", "FAIL", "UI", "REDIRECT".
 * If everything "PASS"es, the session would be finalized and given full access to the account.

PostAuthenticationAction
Probably just a hook that allows extensions to control the redirect flow (eg: GettingStarted).

Requiring re-authentication
Some special pages like Special:ChangeEmail or Special:ResetPassword require re-authentication. The user would go through the normal login flow, except at the end we would check they are the same user. We could implement this by storing in the session when the user last re-authenticated and if it's not recent enough ask them to re-do it. The API would then respond with a "needsReauthentication" error.

Session management

 * Have a stack of "session providers"
 * Near the beginning of each request, go through each one asking if it will provide a session-key
 * Providers can return some indication of priority, including "Use me or throw an error".
 * Then MW can look up the session in the already-existing session storage backend.
 * An authenticated session would contain a "user-token" (and likely a reference to the corresponding PrimaryAuthenticationProvider). AuthManager will validate the session by passing the user-token to the PrimaryAuthenticationProvider.
 * An authenticated session would have a "last-authenticated" timestamp. Certain operations (e.g. change password, change email, link a new third-party authentication provider) would check that this timestamp was recent enough.
 * For web UI, "failing" the check would send the user through Special:UserLogin again (or something very much like it).
 * For the API, "failing" would return an error response instructing the user-application to go through action=login again (or something very much like it).
 * The session provider that was used will control whether certain session management functions can succeed. For example, a session provided via OAuth wouldn't allow for destroying the session or creating a different session.

Backwards compatibility
To avoid forcing everyone to rewrite their AuthPlugin things immediately, our current plan is to deprecate $wgAuth as follows: This seems more workable than trying to provide an AuthPlugin that proxies to AuthManager, since existing AuthPlugin-implementing extensions are using the $wgAuth global as the primary way to access themselves.
 * If $wgAuth is set, AuthManager will log deprecation and proxy to $wgAuth.
 * If $wgAuth is null, AuthManager does its own thing.

We haven't decided what exactly to do about the 19 hooks in Special:Login, or other similar hooks, where those become deprecated.

Password auth with fallback to multiple providers (e.g. CentralAuth → Local auth)
PrimaryAuthenticationProviders supporting compatible AuthenticationRequest types can "chain" to authenticate a provided set of credentials. Imagine a wiki that allows authentication via CentralAuth, LDAP and local password auth. A UsernamePasswordAuthenticationRequest would be compatible with all three of these providers. If the username provided was not found in CentralAuth it would abstain from voting and control would pass next to LDAP which could also abstain and finally the request could be authenticated with a local username and password match.

This type of chained or cascading authorization behavior will be native to AuthManager and will only require the addition of multiple PrimaryAuthenticationProviders that support a common AuthenticationRequest subclass.

Simple login via web UI

 * 1) User visits Special:UserLogin.
 * 2) SpecialUserLogin asks AuthManager for data on the AuthenticationRequest types supported, and somehow renders UI necessary to select one and create an instance.
 * 3) User clicks "Log in with ExampleSite" (e.g. Google, Facebook, Stack Exchange, whatever).
 * 4) SpecialUserLogin passes the submitted form data to AuthManager in a AuthenticationRequest. The request also specifies a return-URL pointing to Special:UserLogin/continue.
 * 5) AuthManager creates an unauthenticated session if no session currently exists, likely setting a cookie.
 * 6) AuthManager runs any configured PreAuthenticationProviders. If any fail, a failure result would be returned to SpecialUserLogin.
 * 7) AuthManager finds the configured PrimaryAuthenticationProvider(s) supporting the AuthenticationRequest type.
 * 8) Data is added to the current session to indicate the PrimaryAuthenticationProvider in use.
 * 9) AuthManager presents the AuthenticationRequest to the PrimaryAuthenticationProvider for validation.
 * 10) If additional data is needed beyond the current contents of the AuthenticationRequest, the PrimaryAuthenticationProvider returns a "UI" response with instructions that can be used by SpecialUserLogin to collect the desired data (or a custom Special page?).
 * 11) If the AuthenticationRequest is complete, the PrimaryAuthenticationProvider does any server-side calls needed to set up third-party authentication with ExampleSite.
 * 12) The PrimaryAuthenticationProvider returns a "REDIRECT" response with the appropriate URL at ExampleSite.
 * 13) AuthManager returns that response back to SpecialUserLogin.
 * 14) SpecialUserLogin redirects the user.
 * 15) User is redirected to ExampleSite, does their thing, and eventually gets sent back to Special:UserLogin/continue.
 * 16) SpecialUserLogin tells AuthManager that the current session's login attempt should continue, again specifying Special:UserLogin/continue as a return-URL.
 * 17) AuthManager identifies the correct PrimaryAuthenticationProvider from the session and asks it for status.
 * 18) The PrimaryAuthenticationProvider gets the ExampleSite user id in whatever manner is appropriate for ExampleSite's third-party login flow.
 * 19) The PrimaryAuthenticationProvider looks in its mapping from ExampleSite user ids to local wiki users, and returns a token of some sort indicating the local wiki user.
 * 20) AuthManager stores this token in the current session.
 * 21) AuthManager processes any configured SecondaryAuthenticationProviders. This may result in more cycles of returning "UI" or "REDIRECT" results to SpecialUserLogin for interaction with the user.
 * 22) Eventually, all SecondaryAuthenticationProviders pass. AuthManager marks the session as fully-authenticated and returns success to SpecialUserLogin.
 * 23) SpecialUserLogin calls any hooks for the "PostAuthenticationAction" step.

Simple login via API

 * 1) User checks API help or paraminfo
 * 2) ApiLogin asks AuthManager for data on the AuthenticationRequest types supported, and somehow converts this into help or paraminfo output.
 * 3) User submits a login request with appropriate parameters, including one that selects ExampleSite as the login mechanism.
 * 4) ApiLogin passes the submitted form data to AuthManager in a AuthenticationRequest. The user-application may also have supplied a return-URL.
 * 5) * AuthManager behaves as in steps 2.1.1–2.1.6 above.
 * 6) * In step 2.1.5.2, the PrimaryAuthenticationProvider should be prepared to provide a default return-URL that merely displays a message telling the user to go back to their application now.
 * 7) ApiLogin returns a response indicating that the client should proceed to the ExampleSite URL.
 * 8) The user application does whatever it needs to do, e.g. opening a web browser with the provided URL. The user application eventually calls ApiLogin with a "continue" parameter.
 * 9) ApiLogin tells AuthManager that the current session's login attempt should continue. The user-application may also have supplied a return-URL.
 * 10) * AuthManager behaves as in steps 3.1.1–3.1.4 above.
 * 11) ApiLogin indicates success to the user application.

Attempted login with no local user

 * Same as the above flows up to step 3.1.1.2. At that point the PrimaryAuthenticationProvider would return a token indicating that login was successful but no local account exists. AuthManager would save the token for later linking, then return to SpecialUserLogin/ApiLogin.
 * SpecialUserLogin should offer to create a local account (which feeds into the CreateAccount flow) or to link to an existing account (which would start the login flow over; a core-provided SecondaryAuthenticationProvider during the subsequent login might prompt to link that ExampleSite account for future logins).
 * ApiLogin would return a response indicating no local account exists, leaving it to the client app to decide how to follow up.

Linking to an existing account

 * This might be initiated by the CreateAccount flow, or by some Special:LinkAccount page or corresponding API module.
 * 1) AuthManager is asked for and returns PrimaryAuthenticationProviders and AuthenticationRequest types for linking another account with the current session.
 * 2) AuthManager is provided with user submission data specifying an PrimaryAuthenticationProvider and data for creating an AuthenticationRequest, and a return-URL.
 * 3) AuthManager determines the appropriate PrimaryAuthenticationProvider.
 * 4) AuthManager marks the session as being in the process of linking with that PrimaryAuthenticationProvider.
 * 5) AuthManager creates the AuthenticationRequest subclass and passes them to the PrimaryAuthenticationProvider along with the return-URL.
 * 6) The PrimaryAuthenticationProvider does any server-side calls needed to set up third-party authentication with ExampleSite.
 * 7) The PrimaryAuthenticationProvider returns a "REDIRECT" response with the appropriate URL at ExampleSite.
 * 8) AuthManager returns that response back to its caller
 * 9) The caller does whatever it needs to do, much as described in the login flows above.
 * 10) Eventually AuthManager is told the linking attempt should continue.
 * 11) AuthManager identifies the correct PrimaryAuthenticationProvider from the session and asks it for status.
 * 12) The PrimaryAuthenticationProvider gets the ExampleSite user id in whatever manner is appropriate for ExampleSite's third-party login flow.
 * 13) The PrimaryAuthenticationProvider adds a mapping from the ExampleSite user id to the current session user. Ideally it would support multiple ExampleSite users to map to one local user, and (if we make AuthManager support it) multiple local users for one ExampleSite user.
 * 14) AuthManager returns the success/failure (or request for confirmation that the user really intends to link one ExampleSite account to multiple local accounts) to its caller.

Unlinking an existing account

 * Likely initiated by some Special:UnlinkAccount page or corresponding API module.
 * 1) AuthManager is asked for and returns a list of linked accounts for the current session user.
 * 2) AuthManager asks its PrimaryAuthenticationProviders for a list of linked accounts for the current session user, and combines these as provider+account tuples.
 * 3) AuthManager is given a provider+account tuple to unlink.
 * 4) AuthManager determines the appropriate PrimaryAuthenticationProvider.
 * 5) AuthManager asks all PrimaryAuthenticationProviders if they could authenticate the current session user. When asking the identified provider, it adds the caveat "without using the selected account". If none say yes (and the current submission doesn't have an override), returns a failure response pointing out that the unlinking would leave the account unusable.
 * 6) AuthManager instructs the selected PrimaryAuthenticationProvider to delete the mapping between the current session user and the selected account.

Evaluation of Symfony2 Security
"Security provides an infrastructure for sophisticated authorization systems, which makes it possible to easily separate the actual authorization logic from so called user providers that hold the users credentials. It is inspired by the Java Spring framework."

Based on MediaWiki's current PHP version requirements, we would need to use the 2.6.x versions of the library (currently 2.6.4). The 2.7.x and 3.0.x branches require PHP versions newer than our current  restriction.

The library includes many components that are similar to the work being proposed for this project. It also includes a parallel framework for authorization management that will probably have similarities to eventual work in that area. These basic building blocks are largely a copy of the patterns from the Spring Security/ACEGI Java authn/z framework.

The concrete classes provided by the library are of less use for MediaWiki and would need to be augmented with MediaWiki specific subclasses or additional adapters to make use of existing MediaWiki functionality like our caching, session storage and database layers. It also would still require creation of a non-trivial amount of business logic to handle the specific use cases of the various authentication flows documented in this RFC.

This Symfony library seems like a great resource to mine for interface patterns and naming suggestions, but today the inclusion of such a large amount of code primarily to get a small number of narrow interfaces seems like a net loss.