mirror of
https://github.com/google/nomulus
synced 2026-02-03 03:22:26 +00:00
This follows up on Brian's work to transition not just to a new format
with an empty scope value, but instead to replace the existing format
entirely with a new one that:
1) includes a version number to support future format migrations
2) doesn't include a field for the scope at all, since scoping the
tokens adds no real security benefit and just makes verification
more difficult
3) replaces the raw SHA-256 hash with a SHA-256 HMAC instead, as a
best practice to avoid length-extension attacks [1], even though
in our particular case they would only be able to extend the
timestamp and would thus be relatively innocuous
The new format will be produced by calling generateToken(), and the
scope-accepting version is renamed to generateLegacyToken() in addition
to its existing deprecation, for maximum clarity.
I changed the validateToken() logic to stop accepting a scope entirely;
when validating a legacy-style token, we'll test it against the two
existing legacy scope values ("admin" and "console") and accept it if
it matches either one.
Note that this means the xsrfScope parameter in @Action is now wholly
obsolete; I'll remove it in a follow-up to avoid bringing extra files
into this CL.
After this CL hits production, the next one will replace all calls to
generateLegacyToken() with generateToken(). Once that CL is deployed,
the last step will be removing the legacy fallback in validateToken().
[1] See https://en.wikipedia.org/wiki/Length_extension_attack
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=148936805
194 lines
8.0 KiB
Java
194 lines
8.0 KiB
Java
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package google.registry.request;
|
|
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
import static com.google.common.base.Strings.nullToEmpty;
|
|
import static com.google.common.net.HttpHeaders.LOCATION;
|
|
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
|
import static google.registry.security.XsrfTokenManager.X_CSRF_TOKEN;
|
|
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
|
import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
|
|
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
|
|
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
|
|
|
import com.google.appengine.api.users.UserService;
|
|
import com.google.common.base.Optional;
|
|
import google.registry.request.auth.AuthResult;
|
|
import google.registry.request.auth.RequestAuthenticator;
|
|
import google.registry.security.XsrfTokenManager;
|
|
import google.registry.util.FormattingLogger;
|
|
import google.registry.util.TypeUtils.TypeInstantiator;
|
|
import java.io.IOException;
|
|
import javax.annotation.Nullable;
|
|
import javax.inject.Provider;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
|
/**
|
|
* Dagger-based request processor.
|
|
*
|
|
* <p>This class creates an HTTP request processor from a Dagger component. It routes requests from
|
|
* your servlet to an {@link Action @Action} annotated handler class.
|
|
*
|
|
* <h3>Component Definition</h3>
|
|
*
|
|
* <p>Action instances are supplied on a per-request basis by invoking the methods on {@code C}.
|
|
* For example:
|
|
* <pre>
|
|
* {@literal @Component}
|
|
* interface ServerComponent {
|
|
* HelloAction helloAction();
|
|
* }</pre>
|
|
*
|
|
* <p>The rules for component methods are as follows:
|
|
* <ol>
|
|
* <li>Methods whose raw return type does not implement {@code Runnable} will be ignored
|
|
* <li>Methods whose raw return type does not have an {@code @Action} annotation are ignored
|
|
* </ol>
|
|
*
|
|
* <h3>Security Features</h3>
|
|
*
|
|
* <p>XSRF protection is built into this class. It can be enabled or disabled on individual actions
|
|
* using {@link Action#xsrfProtection() xsrfProtection} setting.
|
|
*
|
|
* <p>This class also enforces the {@link Action#requireLogin() requireLogin} setting.
|
|
*
|
|
* @param <C> request component type
|
|
*/
|
|
public class RequestHandler<C> {
|
|
|
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
|
|
|
private final Router router;
|
|
private final Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider;
|
|
private final UserService userService;
|
|
private final RequestAuthenticator requestAuthenticator;
|
|
private final XsrfTokenManager xsrfTokenManager;
|
|
|
|
/**
|
|
* Constructor for subclasses to create a new request handler for a specific request component.
|
|
*
|
|
* <p>This operation will generate a routing map for the component's {@code @Action}-returning
|
|
* methods using reflection, which is moderately expensive, so a given servlet should construct a
|
|
* single {@code RequestHandler} and re-use it across requests.
|
|
*
|
|
* @param requestComponentBuilderProvider a Dagger {@code Provider} of builder instances that can
|
|
* be used to construct new instances of the request component (with the required
|
|
* request-derived modules provided by this class)
|
|
* @param userService an instance of the App Engine UserService API
|
|
* @param requestAuthenticator an instance of the {@link RequestAuthenticator} class
|
|
* @param xsrfTokenManager an instance of the {@link XsrfTokenManager} class
|
|
*/
|
|
protected RequestHandler(
|
|
Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider,
|
|
UserService userService,
|
|
RequestAuthenticator requestAuthenticator,
|
|
XsrfTokenManager xsrfTokenManager) {
|
|
this(null, requestComponentBuilderProvider, userService, requestAuthenticator,
|
|
xsrfTokenManager);
|
|
}
|
|
|
|
/** Creates a new RequestHandler with an explicit component class for test purposes. */
|
|
public static <C> RequestHandler<C> createForTest(
|
|
Class<C> component,
|
|
Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider,
|
|
UserService userService,
|
|
RequestAuthenticator requestAuthenticator,
|
|
XsrfTokenManager xsrfTokenManager) {
|
|
return new RequestHandler<>(
|
|
checkNotNull(component),
|
|
requestComponentBuilderProvider,
|
|
userService,
|
|
requestAuthenticator,
|
|
xsrfTokenManager);
|
|
}
|
|
|
|
private RequestHandler(
|
|
@Nullable Class<C> component,
|
|
Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider,
|
|
UserService userService,
|
|
RequestAuthenticator requestAuthenticator,
|
|
XsrfTokenManager xsrfTokenManager) {
|
|
// If the component class isn't explicitly provided, infer it from the class's own typing.
|
|
// This is safe only for use by subclasses of RequestHandler where the generic parameter is
|
|
// preserved at runtime, so only expose that option via the protected constructor.
|
|
this.router = Router.create(
|
|
component != null ? component : new TypeInstantiator<C>(getClass()){}.getExactType());
|
|
this.requestComponentBuilderProvider = checkNotNull(requestComponentBuilderProvider);
|
|
this.userService = checkNotNull(userService);
|
|
this.requestAuthenticator = checkNotNull(requestAuthenticator);
|
|
this.xsrfTokenManager = checkNotNull(xsrfTokenManager);
|
|
}
|
|
|
|
/** Runs the appropriate action for a servlet request. */
|
|
public void handleRequest(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
|
|
checkNotNull(req);
|
|
checkNotNull(rsp);
|
|
Action.Method method;
|
|
try {
|
|
method = Action.Method.valueOf(req.getMethod());
|
|
} catch (IllegalArgumentException e) {
|
|
logger.infofmt("Unsupported method: %s", req.getMethod());
|
|
rsp.sendError(SC_METHOD_NOT_ALLOWED);
|
|
return;
|
|
}
|
|
String path = req.getRequestURI();
|
|
Optional<Route> route = router.route(path);
|
|
if (!route.isPresent()) {
|
|
logger.infofmt("No action found for: %s", path);
|
|
rsp.sendError(SC_NOT_FOUND);
|
|
return;
|
|
}
|
|
if (!route.get().isMethodAllowed(method)) {
|
|
logger.infofmt("Method %s not allowed for: %s", method, path);
|
|
rsp.sendError(SC_METHOD_NOT_ALLOWED);
|
|
return;
|
|
}
|
|
if (route.get().action().requireLogin() && !userService.isUserLoggedIn()) {
|
|
logger.info("not logged in");
|
|
rsp.setStatus(SC_MOVED_TEMPORARILY);
|
|
rsp.setHeader(LOCATION, userService.createLoginURL(req.getRequestURI()));
|
|
return;
|
|
}
|
|
if (route.get().shouldXsrfProtect(method)
|
|
&& !xsrfTokenManager.validateToken(nullToEmpty(req.getHeader(X_CSRF_TOKEN)))) {
|
|
rsp.sendError(SC_FORBIDDEN, "Invalid " + X_CSRF_TOKEN);
|
|
return;
|
|
}
|
|
Optional<AuthResult> authResult =
|
|
requestAuthenticator.authorize(route.get().action().auth(), req);
|
|
if (!authResult.isPresent()) {
|
|
rsp.sendError(SC_FORBIDDEN);
|
|
return;
|
|
}
|
|
|
|
// Build a new request component using any modules we've constructed by this point.
|
|
C component = requestComponentBuilderProvider.get()
|
|
.requestModule(new RequestModule(req, rsp, authResult.get()))
|
|
.build();
|
|
// Apply the selected Route to the component to produce an Action instance, and run it.
|
|
try {
|
|
route.get().instantiator().apply(component).run();
|
|
if (route.get().action().automaticallyPrintOk()) {
|
|
rsp.setContentType(PLAIN_TEXT_UTF_8.toString());
|
|
rsp.getWriter().write("OK\n");
|
|
}
|
|
} catch (HttpException e) {
|
|
e.send(rsp);
|
|
}
|
|
}
|
|
}
|