com.sun.enterprise.security.auth.WebAndEjbToJaasBridge Maven / Gradle / Ivy
Show all versions of payara-micro Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// Portions Copyright [2019-2021] [Payara Foundation and/or its affiliates]
package com.sun.enterprise.security.auth;
import static com.sun.enterprise.security.SecurityLoggerInfo.auditAtnRefusedError;
import static com.sun.enterprise.security.SecurityLoggerInfo.certLoginBadRealmError;
import static com.sun.enterprise.security.SecurityLoggerInfo.invalidOperationForRealmError;
import static com.sun.enterprise.security.SecurityLoggerInfo.noSuchUserInRealmError;
import static com.sun.enterprise.security.SecurityLoggerInfo.unknownCredentialError;
import static com.sun.enterprise.security.auth.login.LoginContextDriver.auditAuthenticate;
import static com.sun.enterprise.security.auth.login.LoginContextDriver.getJaasContext;
import static com.sun.enterprise.security.auth.login.LoginContextDriver.getValidRealm;
import static com.sun.enterprise.security.auth.login.LoginContextDriver.throwLoginException;
import static com.sun.enterprise.security.auth.login.LoginContextDriver.tryJaasLogin;
import static com.sun.enterprise.security.common.AppservAccessController.privileged;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINER;
import static java.util.logging.Level.FINEST;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import java.security.Principal;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginContext;
import javax.security.auth.x500.X500Principal;
import org.glassfish.security.common.Group;
import org.glassfish.security.common.PrincipalImpl;
import com.sun.enterprise.common.iiop.security.AnonCredential;
import com.sun.enterprise.common.iiop.security.GSSUPName;
import com.sun.enterprise.security.SecurityContext;
import com.sun.enterprise.security.SecurityLoggerInfo;
import com.sun.enterprise.security.auth.login.DigestCredentials;
import com.sun.enterprise.security.auth.login.FileLoginModule;
import com.sun.enterprise.security.auth.login.common.LoginException;
import com.sun.enterprise.security.auth.login.common.PasswordCredential;
import com.sun.enterprise.security.auth.login.common.ServerLoginCallbackHandler;
import com.sun.enterprise.security.auth.login.common.X509CertificateCredential;
import com.sun.enterprise.security.auth.realm.InvalidOperationException;
import com.sun.enterprise.security.auth.realm.NoSuchRealmException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;
import com.sun.enterprise.security.auth.realm.Realm;
import com.sun.enterprise.security.auth.realm.certificate.CertificateRealm;
import com.sun.enterprise.security.auth.realm.certificate.OID;
import com.sun.enterprise.security.auth.realm.file.FileRealm;
/**
* This class contains a collection of methods that are used by the Web and EJB containers
* to interact with the JAAS based LoginModules and set the current (per thread) security context.
* The WebContainer uses these for the native Servlet authentication, which is distinct from the newer
* JASPIC Servlet Container Profile authentication.
*
*
* Note that the JAAS system determines which LoginModule is ultimately being called, for instance the
* {@link FileLoginModule}.
* Actual LoginModules in Payara are each paired with a Payara Realm, for instance the {@link FileLoginModule}
* is paired with the {@link FileRealm}. The LoginModule typically does very little else than directly delegating
* to its peer Realm.
*
*
* Also note that with few exceptions neither the LoginModule nor the Realm set the current security context, but only
* validate credentials and, if valid, return zero or more roles. The methods in this class set the security context if
* the JAAS credential validation succeeds.
*
*
* All LoginModules used by Payara have the convention that* credentials are passed in via a {@link Subject} instance
* (instead of the usual {@link CallbackHandler}). The validation outcome is a boolean, but is being passed via an exception.
* No exception means success, while an exception means no success. If the LoginModule/Realm returned any roles they will
* put into the same Subject instance that was used to pass the credentials in.
*
* @author Harpreet Singh ([email protected])
* @author Jyri Virkki
* @author Arjan Tijms (refactoring)
*
*/
public final class WebAndEjbToJaasBridge {
private static final Logger LOGGER = SecurityLoggerInfo.getLogger();
private WebAndEjbToJaasBridge() {
// No instances of this class
}
/**
* This method is just a convenience wrapper for login(Subject, Class) method. It will
* construct a PasswordCredential class.
*
* @param username
* @param password
* @param realmName the name of the realm to login into, if realmName is null, we login into
* the default realm
*/
public static void login(String username, char[] password, String realmName) {
Subject subject = new Subject();
privileged(() -> subject.getPrivateCredentials().add(new PasswordCredential(username, password, getValidRealm(realmName))));
login(subject, PasswordCredential.class);
}
/**
* This method performs the login on the server side.
*
*
* This method is the main login method for Payara. It is called with a Subject and the type (class)
* of credential which should be checked. The Subject must contain a credential of the specified
* type or login will fail.
*
*
* While the implementation has been cleaned up, the login process still consists of a number of
* special cases which are treated separately at the realm level. In the future tighter JAAS
* integration could clean some of this up.
*
*
* The following credential types are recognized at this time:
*
*
* - PasswordCredential - This is the general case for all login methods which rely on the client
* providing a name and password. It can be used with any realms/JAAS login modules which expect
* such data (e.g. file realm, LDAP realm, UNIX realm)
*
- X509CertificateCredential - Special case for SSL client auth. Here authentication has already
* been done by the SSL subsystem so this login only creates a security context based on the
* certificate data.
*
- AnonCredential - Unauthenticated session, set anonymous security context.
*
- GSSUPName - Retrieve user and realm and set security context.
*
- X500Name - Retrieve user and realm and set security context.
*
*
* @param subject the subject of the client
* @param credentialClass the class of the credential packaged in the subject.
*
* @throws LoginException when login fails
*/
public static void login(Subject subject, Class> credentialClass) {
LOGGER.finest(() -> "Processing login with credentials of type: " + credentialClass);
if (credentialClass.equals(PasswordCredential.class)) {
doPasswordLogin(subject);
} else if (credentialClass.equals(X509CertificateCredential.class)) {
doX509CertificateLogin(subject);
} else if (credentialClass.equals(AnonCredential.class)) {
doAnonymousLogin();
} else if (credentialClass.equals(GSSUPName.class)) {
doGSSUPLogin(subject);
} else if (credentialClass.equals(X500Principal.class)) {
doX500Login(subject, null);
} else {
LOGGER.log(INFO, unknownCredentialError, credentialClass.toString());
throw new LoginException("Unknown credential type, cannot login.");
}
}
public static void doX500Login(Subject subject, String appModuleID) {
doX500Login(subject, CertificateRealm.AUTH_TYPE, appModuleID);
}
/**
* A special case login for X500Name credentials.This is invoked for
* certificate login because the containers extract the X.500 name from the
* X.509 certificate before calling into this class.
*
* @param subject
* @param realmName
* @param appModuleID
* @throws LoginException when login fails
*
*/
public static void doX500Login(Subject subject, String realmName, String appModuleID) {
LOGGER.finest(() -> String.format("doX500Login(subject=%s, realmName=%s, appModuleID=%s)",
subject, realmName, appModuleID));
String user = null;
try {
X500Principal x500principal = getPublicCredentials(subject, X500Principal.class);
if (x500principal == null) {
// Should never happen
return;
}
user = x500principal.getName(X500Principal.RFC2253, OID.getOIDMap());
// In the RI-inherited implementation this directly creates
// some credentials and sets the security context.
//
// This means that the certificate realm does not get an opportunity to
// process the request. While the realm will not do any authentication
// (already done by this point) it can choose to adjust the groups or principal
// name or other variables of the security context.
//
// Of course, bug 4646134 needs to be kept in mind at all times, even though time has
// forgotten what 4646134 was.
Realm realm = Realm.getInstance(realmName);
if (realm instanceof CertificateRealm) { // Should always be true
CertificateRealm certRealm = (CertificateRealm) realm;
String jaasCtx = certRealm.getJAASContext();
if (jaasCtx != null) {
// The subject has the certificate Credential.
new LoginContext(
jaasCtx, subject,
new ServerLoginCallbackHandler(user, null, appModuleID))
.login();
}
// The name that the cert realm decided to set as the caller principal name
user = certRealm.authenticate(subject, x500principal);
auditAuthenticate(user, realmName, true);
} else {
// Should never come here
LOGGER.warning(certLoginBadRealmError);
setSecurityContext(user, subject, realmName);
}
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE, "X.500 name login succeeded for : {0}", user);
}
} catch (LoginException le) {
auditAuthenticate(user, realmName, false);
throw le;
} catch (Exception ex) {
throw (LoginException) new LoginException(ex.toString()).initCause(ex);
}
}
/**
* Performs Digest authentication based on RFC 2617. It
*
* @param digestCred DigestCredentials
*/
public static void login(DigestCredentials digestCred) throws javax.security.auth.login.LoginException {
Subject subject = new Subject();
subject.getPrivateCredentials().add(digestCred);
try {
tryJaasLogin(getJaasContext(digestCred.getRealmName()), subject);
} catch (Exception e) {
LOGGER.log(INFO, auditAtnRefusedError, digestCred.getUserName());
LOGGER.log(FINEST, "doPasswordLogin fails", e);
auditAuthenticate(digestCred.getUserName(), digestCred.getRealmName(), false);
throwLoginException(e);
}
setSecurityContext(digestCred.getUserName(), subject, digestCred.getRealmName());
}
/**
* This method is used for logging in a run As principal. It creates a JAAS subject whose credential
* is to type GSSUPName. This is used primarily for runas
*
* @throws LoginException if login fails
*/
public static void loginPrincipal(String username, String realmName) {
if (realmName == null || realmName.length() == 0) {
// No realm provided, assuming default
realmName = Realm.getDefaultRealm();
}
Subject subject = new Subject();
PrincipalImpl callerPrincipal = new PrincipalImpl(username);
GSSUPName name = new GSSUPName(username, realmName);
privileged(() -> {
subject.getPrincipals().add(callerPrincipal);
subject.getPublicCredentials().add(name);
});
try {
Enumeration groupNames = Realm.getInstance(realmName).getGroupNames(username);
Set principalSet = subject.getPrincipals();
while (groupNames.hasMoreElements()) {
principalSet.add(new Group(groupNames.nextElement()));
}
} catch (InvalidOperationException ex) {
LOGGER.log(WARNING, invalidOperationForRealmError, new Object[] { username, realmName, ex.toString() });
} catch (NoSuchUserException ex) {
LOGGER.log(WARNING, noSuchUserInRealmError, new Object[] { username, realmName, ex.toString() });
} catch (NoSuchRealmException ex) {
throw (LoginException) new LoginException(ex.toString()).initCause(ex);
}
setSecurityContext(username, subject, realmName);
}
/**
* This method logs out the user by clearing the security context.
*
* @throws LoginException if logout fails
*/
public static void logout() {
unsetSecurityContext();
}
// ############################ Private methods ######################################
/**
* Log in subject with PasswordCredential. This is a generic login which applies to all login
* mechanisms which process PasswordCredential. In other words, any mechanism which receives an
* actual username, realm and password set from the client.
*
*
* The realm contained in the credential is checked, and a JAAS LoginContext is created using a
* context name obtained from the appropriate Realm instance. The applicable JAAS LoginModule is
* initialized (based on the JAAS login configuration) and login() is invoked on it.
*
*
* RI code makes several assumptions which are retained here:
*
*
* - The PasswordCredential is stored as a private credential of the subject.
*
- There is only one such credential present (actually, only the first one is relevant if more are present).
*
*
* @param subject Subject to be authenticated.
* @throws LoginException Thrown if the login fails.
*
*/
private static void doPasswordLogin(Subject subject) {
PasswordCredential passwordCredential = getPrivateCredentials(subject, PasswordCredential.class);
String user = passwordCredential.getUser();
String realm = passwordCredential.getRealm();
String jaasCtx = getJaasContext(realm);
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE, "Logging in user [{0}] into realm: {1} using JAAS module: {2}", new Object[]{user, realm, jaasCtx});
}
try {
tryJaasLogin(jaasCtx, subject);
} catch (Exception e) {
LOGGER.log(FINEST, "doPasswordLogin fails", e);
auditAuthenticate(user, realm, false);
throwLoginException(e);
}
auditAuthenticate(user, realm, true);
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE, "Password login succeeded for : {0}", user);
}
setSecurityContext(user, subject, realm);
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE, "Set security context as user: {0}", user);
}
}
/**
* A special case login for handling X509CertificateCredential. This does not get triggered based on
* current RI code. See X500Login.
*
* @throws LoginException Thrown if the login fails.
*
*/
private static void doX509CertificateLogin(Subject subject) {
LOGGER.log(FINE, "Processing X509 certificate login.");
String user = null;
String realm = CertificateRealm.AUTH_TYPE;
try {
user = getPublicCredentials(subject, X509CertificateCredential.class).getAlias();
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE, "Set security context as user: {0}", user);
}
setSecurityContext(user, subject, realm);
auditAuthenticate(user, realm, true);
} catch (LoginException le) {
auditAuthenticate(user, realm, false);
throw le;
}
}
/**
* A special case login for anonymous credentials (no login info).
*
* @throws LoginException Thrown if the login fails.
*
*/
private static void doAnonymousLogin() {
// Instance of anonymous credential login with guest
SecurityContext.setUnauthenticatedContext();
LOGGER.log(FINE, "Set anonymous security context.");
}
/**
* A special case login for GSSUPName credentials.
*
* @throws LoginException Thrown if the login fails.
*/
private static void doGSSUPLogin(Subject s) {
LOGGER.fine("Processing GSSUP login.");
String user = null;
String realm = Realm.getDefaultRealm();
try {
user = getPublicCredentials(s, GSSUPName.class).getUser();
setSecurityContext(user, s, realm);
auditAuthenticate(user, realm, true);
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE, "GSSUP login succeeded for : {0}", user);
}
} catch (LoginException le) {
auditAuthenticate(user, realm, false);
throw le;
}
}
/**
* Retrieve a public credential of the given type (java class) from the subject.
*
*
* This method retains the RI assumption that only the first credential of the given type is used.
*
* @throws LoginException Thrown if the login fails.
*/
private static T getPublicCredentials(Subject subject, Class cls) {
Set credset = subject.getPublicCredentials(cls);
Iterator iter = credset.iterator();
if (!iter.hasNext()) {
String credentialType = cls.toString();
LOGGER.log(FINER, () -> "Expected public credentials of type : " + credentialType + " but none found.");
throw new LoginException("Expected public credential of type: " + credentialType + " but none found.");
}
try {
return privileged(() -> iter.next());
} catch (Exception exception) {
// Should never come here
throwLoginException(exception, e -> "Failed to retrieve public credential: " + e.getMessage());
return null;
}
}
/**
* Retrieve a private credential of the given type (java class) from the subject.
*
*
* This method retains the RI assumption that only the first credential of the given type is used.
*
* @throws LoginException Thrown if the login fails.
*/
private static T getPrivateCredentials(Subject subject, Class cls) {
Iterator iter = privileged(() -> subject.getPrivateCredentials(cls)).iterator();
if (!iter.hasNext()) {
String credmsg = cls.toString();
if (LOGGER.isLoggable(FINER)) {
LOGGER.log(FINER, "Expected private credential of type: {0} but none found.", credmsg);
}
throw new LoginException("Expected private credential of type: " + credmsg + " but none found.");
}
// Retrieve only first credential of give type
try {
return privileged(() -> iter.next());
} catch (Exception e) {
// should never come here
if (e instanceof LoginException) {
throw (LoginException) e;
}
throw new LoginException("Failed to retrieve private credential: " + e.getMessage(), e);
}
}
/**
* This method sets the security context on the current Thread Local Storage
*
* @param String username is the user who authenticated
* @param Subject is the subject representation of the user
* @param Credentials the credentials that the server associated with it
*/
private static void setSecurityContext(String userName, Subject subject, String realm) {
SecurityContext.setCurrent(new SecurityContext(userName, subject, realm));
}
/**
* Set the current security context on the Thread Local Storage to null.
*
*/
private static void unsetSecurityContext() {
SecurityContext.setCurrent((SecurityContext) null);
}
}