From 151ae2558fa028b484834018705685a16cafec10 Mon Sep 17 00:00:00 2001 From: mountford Date: Thu, 3 Aug 2017 14:19:46 -0700 Subject: [PATCH] Document the framework for authentication and authorization ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164181067 --- docs/authentication-framework.md | 274 +++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 docs/authentication-framework.md diff --git a/docs/authentication-framework.md b/docs/authentication-framework.md new file mode 100644 index 000000000..dd5180a0a --- /dev/null +++ b/docs/authentication-framework.md @@ -0,0 +1,274 @@ +# Authentication framework + +Nomulus performs authentication and authorization on a per-request basis. Each +endpoint action defined has an `@Action` annotation with an `auth` attribute +which determines the ways a request can authenticate itself, as well as which +requests will be authorized to invoke the action. + +## Authentication and authorization properties + +The `auth` attribute is an enumeration. Each value of the enumeration +corresponds to a triplet of properties: + +* the *authentication methods* allowed by the action +* the *minimum authentication level* which is authorized to run the action +* the *user policy* for the action + +### Authentication methods + +Authentication methods are ways whereby the request can authenticate itself to +the system. In the code, an *authentication mechanism* is a class which handles +a particular authentication method. There are currently three methods: + +* `INTERNAL`: used by requests generated from App Engine task queues; these + requests do not have a user, because they are system-generated, so + authentication consists solely of verifying that the request did indeed + come from a task queue + +* `API`: authentication using an API; the Nomulus release ships with one API + authentication mechanism, OAuth 2, but you can write additional custom + mechanisms to handle other protocols if needed + +* `LEGACY`: authentication using the standard App Engine `UserService` API, + which authenticates based on cookies and XSRF tokens + +The details of the associated authentication mechanism classes are given later. + +### Authentication levels + +Each authentication method listed above can authenticate at one of three levels: + +* `NONE`: no authentication was found +* `APP`: the request was authenticated, but no user was present +* `USER`: the request was authenticated with a specific user + +For instance, `INTERNAL` authentication never returns an authentication level of +`USER`, because internal requests generated from App Engine task queues do not +execute as a particular end user account. `LEGACY` authentication, on the other +hand, never returns an authentication level of `APP`, because authentication is +predicated on identifying the user, so the only possible answers are `NONE` and +`USER`. + +Each action has a minimum request authentication level. Some actions are +completely open to the public, and have a minimum level of `NONE`. Some require +authentication but not a user, and have a minimum level of `APP`. And some +cannot function properly without knowing the exact user, and have a minimum +level of `USER`. + +### User policy + +The user policy indicates what kind of user is authorized to execute the action. +There are three possible values: + +* `IGNORED`: the user information is ignored +* `PUBLIC`: an authenticated user is required, but any user will do +* `ADMIN`: there must be an authenticated user with admin privileges + +Note that the user policy applies only to the automatic checking done by the +framework before invoking the action. The action itself may do more checking. +For instance, the registrar console's main page has no authentication at all, +and all requests are permitted. However, the first thing the code does is check +whether a user was found. If not, it issues a redirect to the login page. + +Likewise, other pages of the registrar console have a user policy of `PUBLIC`, +meaning that any logged-in user can access the page. However, the code then +looks up the user to make sure he or she is associated with a registrar. + +Also note that the user policy only applies when there is actually a user. Some +actions can be executed either by an admin user or by an internal request coming +from a task queue, which will not have a defined user at all. So rather than +determining the minimum user level, this setting should be thought of as +determining the minimum level a user must have *if there is a user at all*. To +require that there be a user, set the minimum authentication level to `USER`. + +### Allowed authentication and authorization values + +Not all triplets of the authentication method, minimum level and user policy +make sense. A master enumeration lists all the valid triplets. They are: + +* `AUTH_PUBLIC_ANONYMOUS`: Allow all access, and don't attempt to authenticate. + The only authentication method is `INTERNAL`, with a minimum level of + `NONE`. Internal requests will be flagged as such, but everything else + passes the authorization check with a value of `NOT_AUTHENTICATED`. + +* `AUTH_PUBLIC`: Allow all access, but attempt to authenticate the user. All + three authentication methods are specified, with a minimum level of `NONE` + and a user policy of `PUBLIC`. If the user can be authenticated by any + means, the identity is passed to the request. But if not, the request still + passes the authorization check, with a value of `NOT_AUTHENTICATED`. + +* `AUTH_PUBLIC_LOGGED_IN`: Allow access only by authenticated users. The + `API` and `LEGACY` authentication methods are supported, but not `INTERNAL`, + because that does not identify a user. The minimum level is `USER`, with a + user policy of `PUBLIC`. Only requests with a user authenticated via either + the legacy, cookie-based method or an API method (e.g. OAuth 2) are + authorized to run the action. + +* `AUTH_INTERNAL_OR_ADMIN`: Allow access only by admin users or internal + requests. This is appropriate for actions that should only be accessed by + someone trusted (as opposed to anyone with a Google login). This currently + allows only the `INTERNAL` and `API` methods, meaning that an admin user + cannot authenticate themselves via the legacy authentication mechanism, + which is used only for the registrar console. The minimum level is `APP`, + because we don't require a user for internal requests, but the user policy + is `ADMIN`, meaning that if there *is* a user, it needs to be an admin. + +* `AUTH_INTERNAL_ONLY`: Only internal requests are allowed. This is appropriate + for actions which are only executed by cron jobs, and therefore have no + authenticated user. The method is `INTERNAL`, the minimum level is `APP`, + and the user policy is `IGNORED`. + +### Action setting golden files + +To make sure that the authentication and authorization settings are correct for +all actions, a unit test uses reflection to compare all defined actions for a +specific service to a golden file containing the correct settings. These files +are: + +* `frontend_routing.txt` for the default (frontend) service +* `backend_routing.txt` for the backend service +* `tools_routing.txt` for the tools service + +Each of these files consists of lines listing a path, the class that handles +that path, the allowable HTTP methods (meaning GET and POST, as opposed to the +authentication methods described above), the value of the `automaticallyPrintOk` +attribute (not relevant for purposes of this document), and the three +authentication and authorization settings described above. Whenever actions are +added, or their attributes are modified, the golden files need to be updated. + +The golden files also serve as a convenient place to check out how things are +set up. For instance, the tools actions are, for the most part, accessible to +admins and internal requests only. The backend actions are mostly accessible +only to internal requests. And the frontend actions are a grab-bag; some are +open to the public, some to any user, some only to admins, etc. + +### Example + +The `EppTlsAction` class handles EPP commands which arrive from the proxy via +HTTP. Only admin users and internal requests should be allowed to execute this +action, to avoid anyone on the Internet sending us random EPP commands. Further, +the HTTP method needs to be `POST`, so that the EPP command is contained in the +body rather than the URL itself (which could be logged). Therefore, the class +definition looks like: + +```java +@Action( + path = "/_dr/epp", + method = Method.POST, + auth = Auth.AUTH_INTERNAL_OR_ADMIN +) +public class EppTlsAction implements Runnable { +... +``` + +and the corresponding line in frontend_routing.txt (including the header line) +is: + +```shell +PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY +/_dr/epp EppTlsAction POST n INTERNAL,API APP ADMIN +``` + +## Implementation + +The code implementing the authentication and authorization framework is +contained in the `google.registry.request.auth` package. The main method is +`authorize()`, in `RequestAuthenticator`. This method takes the auth settings +and an HTTP request, and tries to authenticate and authorize the request using +any of the specified methods, returning the result of its attempts. Note that +failed authorization (in which case `authorize()` returns `Optional.absent()`) +is different from the case where nothing can be authenticated, but the action +does not require any; in that case, `authorize()` succeeds, returning the +special result AuthResult.NOT_AUTHENTICATED. + +There are separate classes (described below) for the mechanism which handles +each authentication method. The list of allowable API authentication mechanisms +(by default, just OAuth 2) is configured in `AuthModule`. + +The ultimate caller of `authorize()` is +`google.registry.request.RequestHandler`, which is responsible for routing +incoming HTTP requests to the appropriate action. After determining the +appropriate action, and making sure that the incoming HTTP method is appropriate +for the action, it calls `authorize()`, and rejects the request if authorization +fails. + +### `LegacyAuthenticationMechanism` + +Legacy authentication is straightforward, because the App Engine `UserService` +API does all the work. Because the protocol might be vulnerable to an XSRF +attack, the authentication mechanism issues and checks XSRF tokens as part +of the process if the HTTP method is not GET or HEAD. + +### `OAuthAuthenticationMechanism` + +OAuth 2 authentication is performed using the App Engine `OAuthService` API. +There are three Nomulus configuration values involved: + +* `availableOauthScopes` is the set of OAuth scopes passed to the service to + be checked for their presence. + +* `requiredOauthScopes` is the set of OAuth scopes which must be present. This + should be a subset of the available scopes. All scopes in this set must be + present for authentication to succeed. + +* `allowedOauthClientIds` is the set of allowable OAuth client IDs. Any client + ID in this set is sufficient for successful authentication. + +The code looks for an `Authorization` HTTP header of the form "BEARER XXXX...", +containing the access token. If it finds one, it calls `OAuthService` to +validate the token, check that the scopes and client ID match, and retrieve the +flag indicating whether the user is an admin. + +### `AppEngineInternalAuthenticationMechanism` + +Detection of internal requests is a little hacky. App Engine uses a special HTTP +header, `X-AppEngine-QueueName`, to indicate the queue from which the request +originates. If this header is present, internal authentication succeeds. App +Engine normally strips this header from external requests, so only internal +requests will be authenticated. + +App Engine has a special carve-out for admin users, who are allowed to specify +headers which do not get stripped. So an admin user can use a command-line +utility like `curl` to craft a request which appears to Nomulus to be an +internal request. This has proven to be useful, facilitating the testing of +actions which otherwise could only be run via a dummy cron job. + +However, it only works if App Engine can authenticate the user as an admin via +the `UserService` API. OAuth won't work, because authentication is performed by +the Nomulus code, and the headers will already have been stripped by App Engine +before the request is executed. Only the legacy, cookie-based method will work. + +Be aware that App Engine defines an "admin user" as anyone with access to the +App Engine project, even those with read-only access. + +## Other topics + +### OAuth 2 not supported for the registry console + +Currently, OAuth 2 is only supported for requests which specify the +`Authorization` HTTP header. The OAuth code reads this header and passes it to +the Google OAuth server (no other authentication servers are currently +supported) to verify the user's identity. This works fine for the `nomulus` +command-line tool. + +It doesn't work for browser-based interactions such as the registrar console. +For that, we will (we think) need to redirect the user to the authentication +server, and upon receiving the user back, fish out the code and convert it to a +token which we store in a cookie. None of this is particularly hard, but for the +moment it seems easier to stick with the legacy App Engine UserService API. Of +course, contributions from the open-source community are welcome. :) + +### Authorization via `web.xml` + +Before the modern authentication and authorization framework described in this +document was put in place, Nomulus used to be protected by directives in the +`web.xml` file which allowed only logged-in users to access most endpoints. This +had the advantage of being very easy to implement, but it came with some +drawbacks, the primary one being lack of support for OAuth 2. App Engine's +standard login detection works fine when using a browser, but does not handle +cases where the request is coming from a standalone program such as the +`nomulus` command-line tool. By moving away from the `web.xml` approach, we +gained more flexibility to support an array of authentication and authorization +schemes, including custom ones developed by the Nomulus community, at the +expense of having to perform the authentication and authorization ourselves in +the code.