All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.apphosting.runtime.jetty.EE10AppEngineAuthentication Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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 com.google.apphosting.runtime.jetty;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.runtime.jetty.AppEngineAuthentication.AppEnginePrincipal;
import com.google.apphosting.runtime.jetty.AppEngineAuthentication.AppEngineUserIdentity;
import com.google.common.flogger.GoogleLogger;
import jakarta.servlet.http.HttpServletResponse;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.function.Function;
import javax.security.auth.Subject;
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;

/**
 * {@code AppEngineAuthentication} is a utility class that can configure a Jetty {@link
 * SecurityHandler} to integrate with the App Engine authentication model.
 *
 * 

Specifically, it registers a custom {@link Authenticator} instance that knows how to redirect * users to a login URL using the {@link UserService}, and a custom {@link UserIdentity} that is * aware of the custom roles provided by the App Engine. */ public class EE10AppEngineAuthentication { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); /** * URLs that begin with this prefix are reserved for internal use by App Engine. We assume that * any URL with this prefix may be part of an authentication flow (as in the Dev Appserver). */ private static final String AUTH_URL_PREFIX = "/_ah/"; private static final String AUTH_METHOD = "Google Login"; private static final String REALM_NAME = "Google App Engine"; // Keep in sync with com.google.apphosting.runtime.jetty.JettyServletEngineAdapter. private static final String SKIP_ADMIN_CHECK_ATTR = "com.google.apphosting.internal.SkipAdminCheck"; /** * Any authenticated user is a member of the {@code "*"} role, and any administrators are members * of the {@code "admin"} role. Any other roles will be logged and ignored. */ private static final String USER_ROLE = "*"; private static final String ADMIN_ROLE = "admin"; /** * Inject custom {@link LoginService} and {@link Authenticator} implementations into the specified * {@link ConstraintSecurityHandler}. */ public static ConstraintSecurityHandler newSecurityHandler() { ConstraintSecurityHandler handler = new ConstraintSecurityHandler() { @Override protected Constraint getConstraint(String pathInContext, Request request) { if (request.getAttribute(SKIP_ADMIN_CHECK_ATTR) != null) { logger.atFine().log("Returning DeferredAuthentication because of SkipAdminCheck."); // Warning: returning ALLOWED here will bypass security restrictions! return Constraint.ALLOWED; } return super.getConstraint(pathInContext, request); } }; AppEngineLoginService loginService = new AppEngineLoginService(); AppEngineAuthenticator authenticator = new AppEngineAuthenticator(); DefaultIdentityService identityService = new DefaultIdentityService(); // Set allowed roles. handler.setRoles(new HashSet<>(Arrays.asList(USER_ROLE, ADMIN_ROLE))); handler.setLoginService(loginService); handler.setAuthenticator(authenticator); handler.setIdentityService(identityService); authenticator.setConfiguration(handler); return handler; } /** * {@code AppEngineAuthenticator} is a custom {@link LoginAuthenticator} that knows how to redirect the * current request to a login URL in order to authenticate the user. */ private static class AppEngineAuthenticator extends LoginAuthenticator { /** * Checks if the request could go to the login page. * * @param uri The uri requested. * @return True if the uri starts with "/_ah/", false otherwise. */ private static boolean isLoginOrErrorPage(String uri) { return uri.startsWith(AUTH_URL_PREFIX); } @Override public String getAuthenticationType() { return AUTH_METHOD; } @Override public Constraint.Authorization getConstraintAuthentication( String pathInContext, Constraint.Authorization existing, Function getSession) { // Check this before checking if there is a user logged in, so // that we can log out properly. Specifically, watch out for // the case where the user logs in, but as a role that isn't // allowed to see /*. They should still be able to log out. if (isLoginOrErrorPage(pathInContext)) { logger.atFine().log( "Got %s, returning DeferredAuthentication to imply authentication is in progress.", pathInContext); return Constraint.Authorization.ALLOWED; } return super.getConstraintAuthentication(pathInContext, existing, getSession); } /** * Validate a response. Compare to: * j.c.g.apphosting.utils.jetty.AppEngineAuthentication.AppEngineAuthenticator.authenticate(). * *

If authentication is required but the request comes from an untrusted ip, 307s the request * back to the trusted appserver. Otherwise, it will auth the request and return a login url if * needed. * *

From org.eclipse.jetty.server.Authentication: * * @param req The request * @param res The response * @param cb The callback */ @Override public AuthenticationState validateRequest(Request req, Response res, Callback cb) { UserService userService = UserServiceFactory.getUserService(); // If the user is authenticated already, just create a // AppEnginePrincipal or AppEngineFederatedPrincipal for them. if (userService.isUserLoggedIn()) { UserIdentity user = _loginService.login(null, null, null, null); logger.atFine().log("authenticate() returning new principal for %s", user); if (user != null) { return new LoginAuthenticator.UserAuthenticationSucceeded(getAuthenticationType(), user); } } if (AuthenticationState.Deferred.isDeferred(res)) { return null; } try { logger.atFine().log( "Got %s but no one was logged in, redirecting.", req.getHttpURI().getPath()); String url = userService.createLoginURL(HttpURI.build(req.getHttpURI()).asString()); Response.sendRedirect(req, res, cb, url); // Tell Jetty that we've already committed a response here. return AuthenticationState.CHALLENGE; } catch (ApiProxy.ApiProxyException ex) { // If we couldn't get a login URL for some reason, return a 403 instead. logger.atSevere().withCause(ex).log("Could not get login URL:"); Response.writeError(req, res, cb, HttpServletResponse.SC_FORBIDDEN); return AuthenticationState.SEND_FAILURE; } } } /** * {@code AppEngineLoginService} is a custom Jetty {@link LoginService} that is aware of the two * special role names implemented by Google App Engine. Any authenticated user is a member of the * {@code "*"} role, and any administrators are members of the {@code "admin"} role. Any other * roles will be logged and ignored. */ private static class AppEngineLoginService implements LoginService { private IdentityService identityService; /** * @return Get the name of the login service (aka Realm name) */ @Override public String getName() { return REALM_NAME; } @Override public UserIdentity login( String s, Object o, Request request, Function function) { return loadUser(); } /** * Creates a new AppEngineUserIdentity based on information retrieved from the Users API. * * @return A AppEngineUserIdentity if a user is logged in, or null otherwise. */ private AppEngineUserIdentity loadUser() { UserService userService = UserServiceFactory.getUserService(); User engineUser = userService.getCurrentUser(); if (engineUser == null) { return null; } return new AppEngineUserIdentity(new AppEnginePrincipal(engineUser)); } @Override public IdentityService getIdentityService() { return identityService; } @Override public void logout(UserIdentity user) { // Jetty calls this on every request -- even if user is null! if (user != null) { logger.atFine().log("Ignoring logout call for: %s", user); } } @Override public void setIdentityService(IdentityService identityService) { this.identityService = identityService; } @Override public boolean validate(UserIdentity user) { logger.atInfo().log("validate(%s) throwing UnsupportedOperationException.", user); throw new UnsupportedOperationException(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy