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 auth "modules". It is compromised of the following:

AuthenticationProvider
Something like local MediaWiki password auth, CentralAuth-auth, LDAP, etc. There can be multiple of these installed on a wiki, but only one can be used 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:
 * Is a "link" or "create" type (for account creation):
 * A "link" type 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" type 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)
 * Gets user provided input, returns "PASS" "FAIL" or "REDIRECT"
 * if "REDIRECT", a second call would be made that expects a "PASS" or "FAIL" response

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".

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 (e.g. CentralAuth → Local auth)
We want to be able to support fallback authentication. The main use case for this is where CentralAuth has a user database, but not all users are in it, so we need to fallback to local authentication. To support this we will have a special FallbackAuthenticationProvider base class. AuthenticationProviders must specifically extend this class to support fallback.

The FallbackAuthenticationProvider would be able to specify the normal "PASS" "FAIL" or "REDIRECT", in addition to a special "FALLBACK", which indicates that the fallback provider should be checked. If no fallback is registered, a FALLBACK is equivalent to FAIL.

The fallback AuthenticationProvider could be any AuthenticationProvider and does not need to have any special implementations.

Whether or not a fallback is used is dependent upon the configuration:

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. It also specifies a return-URL pointing to Special:UserLogin/continue.
 * 5) AuthManager runs any configured PreAuthenticationFilters. If any fail, a failure result would be returned to SpecialUserLogin.
 * 6) AuthManager finds the configured AuthenticationProvider corresponding to the "Log in with ExampleSite" button.
 * 7) AuthManager creates an unauthenticated session if no session currently exists, likely setting a cookie.
 * 8) Data is added to the current session to indicate the AuthenticationProvider in use.
 * 9) AuthManager gathers any additional fields the AuthenticationProvider specifies (likely none here) and passes them to the AuthenticationProvider along with the return-URL.
 * 10) The AuthenticationProvider does any server-side calls needed to set up third-party authentication with ExampleSite.
 * 11) The AuthenticationProvider returns a "REDIRECT" response with the appropriate URL at ExampleSite.
 * 12) AuthManager returns that response back to SpecialUserLogin.
 * 13) SpecialUserLogin redirects the user.
 * 14) User is redirected to ExampleSite, does their thing, and eventually gets sent back to Special:UserLogin/continue.
 * 15) SpecialUserLogin tells AuthManager that the current session's login attempt should continue, again specifying Special:UserLogin/continue as a return-URL.
 * 16) AuthManager identifies the correct AuthenticationProvider from the session and asks it for status.
 * 17) The AuthenticationProvider gets the ExampleSite user id in whatever manner is appropriate for ExampleSite's third-party login flow.
 * 18) 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.
 * 19) AuthManager stores this token in the current session.
 * 20) AuthManager processes any configured SecondaryAuthenticationProviders. This may result in more cycles of returning "UI" or "REDIRECT" results to SpecialUserLogin for interaction with the user.
 * 21) Eventually, all SecondaryAuthenticationProviders pass. AuthManager marks the session as fully-authenticated and returns success to SpecialUserLogin.
 * 22) 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.