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:
 * 0..n PreAuthenticationFilters
 * 1..n AuthenticationProviders
 * 0..n SecondaryAuthenticationProviders
 * 0..n PostAuthenticationFilters

The entry point that collects authentication data from the user (eg Special:Login) needs to know what type of data it is collecting from the request. Determination of this will be an implementation detail of the authentication entry point but must in the case of an API entry point be well documented in order to allow automated clients to select their preferred authentication method.

The collected authentication data is placed in a typed AuthenticationRequest instance. A UsernamePasswordAuthenticationRequest would be typical, but other forms would be possible and necessary for AuthenticationProviders that used other methods of authentication (eg OAuthAuthenticationRequest, GoogleAuthenticationRequest, X509AuthenticationRequest, etc).

Approval of an AuthenticationRequest will be occur in the AuthManager by allowing each filter and provider an opportunity to vote on the validity of the request. To be eligible to vote, the authn component must declare that it knows something about how to process the specific AuthenticationRequest sub-type currently being evaluated. The votes can be to approve, deny, request additional information (eg 2-factor auth) or abstain (eg no matching account found). Any vote to deny or request additional information is an immediate decision. Approval requires all voting components to approve or abstain from the vote (with at least one approval vote required).

PreAuthenticationFilter
Examples: Captcha checking, or rate limiting. These would also be able to provide their own input requirements and validate them, returning "PASS" or "FAIL".

AuthenticationProvider
An AuthenticationProvider is responsible for validating the credentials provided in a compatible AuthenticationRequest. Examples would be local MediaWiki password auth, CentralAuth-auth, LDAP, OAuth, Google auth, etc. There can be multiple AuthenticationProviders 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). AuthManager will provide a means to discover what authentication options are available based on it's current configuration. This description must be sufficient for both Special:Login and the API action=login method to advertise the options and collect the proper data to create a typed AuthenticationRequest to pass to AuthManager.

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 AuthenticationProvider 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"
 * UI here both applies to API and index.php, and means that there is some interaction needed on the user's part
 * If everything "PASS"es, the session would be finalized and given full access to the account

PostAuthenticationFilter
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 AuthenticationProvider). AuthManager will validate the session by passing the user-token to the AuthenticationProvider.
 * 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.

Password auth with fallback to multiple providers (e.g. CentralAuth → Local auth)
AuthenticationProviders 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 AuthenticationProviders that support a common AuthenticationRequest subclass.

Simple login via web UI

 * 1) User visits Special:UserLogin.
 * 2) SpecialUserLogin asks AuthManager for data on the UI fields required, and somehow renders them.
 * 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 PreAuthenticationFilters. If any fail, a failure result would be returned to SpecialUserLogin.
 * 7) AuthManager finds the configured AuthenticationProvider corresponding to the AuthenticationRequest type.
 * 8) Data is added to the current session to indicate the AuthenticationProvider in use.
 * 9) AuthManager presents the AuthenticationRequest to the AuthenticationProvider for validation.
 * 10) If additional data is needed beyond the current contents of the AuthenticationRequest, the AuthenticationProvider 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 AuthenticationProvider does any server-side calls needed to set up third-party authentication with ExampleSite.
 * 12) The AuthenticationProvider 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 AuthenticationProvider from the session and asks it for status.
 * 18) The AuthenticationProvider gets the ExampleSite user id in whatever manner is appropriate for ExampleSite's third-party login flow.
 * 19) The AuthenticationProvider 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 "PostAuthenticationFilter" step.

Simple login via API

 * 1) User checks API help or paraminfo
 * 2) ApiLogin queries AuthManager for data about UI fields required, and returns appropriate data to the user.
 * 3) User submits a login request with appropriate parameters, including one that selects ExampleSite as the login mechanism.
 * 4) ApiLogin matches the submitted parameters with UI fields and passes the data to AuthManager. The user-application may also have supplied a return-URL.
 * 5) * AuthManager behaves as in steps 2.1.1–2.1.5 above.
 * 6) * In step 2.1.4.1, the AuthenticationProvider 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–2.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 AuthenticationProvider 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 UI fields for linking another account with the current session.
 * 2) AuthManager is provided with user submission data corresponding to the above UI fields, and a return-URL.
 * 3) AuthManager determines the appropriate AuthenticationProvider.
 * 4) AuthManager marks the session as being in the process of linking with that AuthenticationProvider.
 * 5) AuthManager gathers any additional fields the AuthenticationProvider specifies (likely none here) and passes them to the AuthenticationProvider along with the return-URL.
 * 6) The AuthenticationProvider does any server-side calls needed to set up third-party authentication with ExampleSite.
 * 7) The AuthenticationProvider 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 AuthenticationProvider from the session and asks it for status.
 * 12) The AuthenticationProvider gets the ExampleSite user id in whatever manner is appropriate for ExampleSite's third-party login flow.
 * 13) The AuthenticationProvider 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 AuthenticationProviders 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 AuthenticationProvider.
 * 5) AuthManager asks all AuthenticationProviders 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 AuthenticationProvider 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.