/* * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.security.auth.login; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Map; import java.util.HashMap; import java.text.MessageFormat; import javax.security.auth.Subject; import javax.security.auth.AuthPermission; import javax.security.auth.callback.*; import javax.security.auth.spi.LoginModule; import java.security.AccessControlContext; import java.util.ServiceLoader; import sun.security.util.PendingException; import sun.security.util.ResourcesMgr; /** *

The {@code LoginContext} class describes the basic methods used * to authenticate Subjects and provides a way to develop an * application independent of the underlying authentication technology. * A {@code Configuration} specifies the authentication technology, or * {@code LoginModule}, to be used with a particular application. * Different LoginModules can be plugged in under an application * without requiring any modifications to the application itself. * *

In addition to supporting pluggable authentication, this class * also supports the notion of stacked authentication. * Applications may be configured to use more than one * LoginModule. For example, one could * configure both a Kerberos LoginModule and a smart card * LoginModule under an application. * *

A typical caller instantiates a LoginContext with * a name and a {@code CallbackHandler}. * LoginContext uses the name as the index into a * Configuration to determine which LoginModules should be used, * and which ones must succeed in order for the overall authentication to * succeed. The {@code CallbackHandler} is passed to the underlying * LoginModules so they may communicate and interact with users * (prompting for a username and password via a graphical user interface, * for example). * *

Once the caller has instantiated a LoginContext, * it invokes the {@code login} method to authenticate * a {@code Subject}. The {@code login} method invokes * the configured modules to perform their respective types of authentication * (username/password, smart card pin verification, etc.). * Note that the LoginModules will not attempt authentication retries nor * introduce delays if the authentication fails. * Such tasks belong to the LoginContext caller. * *

If the {@code login} method returns without * throwing an exception, then the overall authentication succeeded. * The caller can then retrieve * the newly authenticated Subject by invoking the * {@code getSubject} method. Principals and Credentials associated * with the Subject may be retrieved by invoking the Subject's * respective {@code getPrincipals}, {@code getPublicCredentials}, * and {@code getPrivateCredentials} methods. * *

To logout the Subject, the caller calls * the {@code logout} method. As with the {@code login} * method, this {@code logout} method invokes the {@code logout} * method for the configured modules. * *

A LoginContext should not be used to authenticate * more than one Subject. A separate LoginContext * should be used to authenticate each different Subject. * *

The following documentation applies to all LoginContext constructors: *

    * *
  1. {@code Subject} * * *
  2. {@code Configuration} * * *
  3. {@code CallbackHandler} * *
* * @see java.security.Security * @see javax.security.auth.AuthPermission * @see javax.security.auth.Subject * @see javax.security.auth.callback.CallbackHandler * @see javax.security.auth.login.Configuration * @see javax.security.auth.spi.LoginModule * @see java.security.Security security properties */ public class LoginContext { private static final String LOGIN_METHOD = "login"; private static final String COMMIT_METHOD = "commit"; private static final String ABORT_METHOD = "abort"; private static final String LOGOUT_METHOD = "logout"; private static final String OTHER = "other"; private static final String DEFAULT_HANDLER = "auth.login.defaultCallbackHandler"; private Subject subject = null; private boolean subjectProvided = false; private boolean loginSucceeded = false; private CallbackHandler callbackHandler; private Map state = new HashMap(); private Configuration config; private AccessControlContext creatorAcc = null; // customized config only private ModuleInfo[] moduleStack; private ClassLoader contextClassLoader = null; // state saved in the event a user-specified asynchronous exception // was specified and thrown private int moduleIndex = 0; private LoginException firstError = null; private LoginException firstRequiredError = null; private boolean success = false; private static final sun.security.util.Debug debug = sun.security.util.Debug.getInstance("logincontext", "\t[LoginContext]"); private void init(String name) throws LoginException { SecurityManager sm = System.getSecurityManager(); if (sm != null && creatorAcc == null) { sm.checkPermission(new AuthPermission ("createLoginContext." + name)); } if (name == null) throw new LoginException (ResourcesMgr.getString("Invalid.null.input.name")); // get the Configuration if (config == null) { config = java.security.AccessController.doPrivileged (new java.security.PrivilegedAction() { public Configuration run() { return Configuration.getConfiguration(); } }); } // get the LoginModules configured for this application AppConfigurationEntry[] entries = config.getAppConfigurationEntry(name); if (entries == null) { if (sm != null && creatorAcc == null) { sm.checkPermission(new AuthPermission ("createLoginContext." + OTHER)); } entries = config.getAppConfigurationEntry(OTHER); if (entries == null) { MessageFormat form = new MessageFormat(ResourcesMgr.getString ("No.LoginModules.configured.for.name")); Object[] source = {name}; throw new LoginException(form.format(source)); } } moduleStack = new ModuleInfo[entries.length]; for (int i = 0; i < entries.length; i++) { // clone returned array moduleStack[i] = new ModuleInfo (new AppConfigurationEntry (entries[i].getLoginModuleName(), entries[i].getControlFlag(), entries[i].getOptions()), null); } contextClassLoader = java.security.AccessController.doPrivileged (new java.security.PrivilegedAction() { public ClassLoader run() { ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader == null) { // Don't use bootstrap class loader directly to ensure // proper package access control! loader = ClassLoader.getSystemClassLoader(); } return loader; } }); } private void loadDefaultCallbackHandler() throws LoginException { // get the default handler class try { final ClassLoader finalLoader = contextClassLoader; this.callbackHandler = java.security.AccessController.doPrivileged( new java.security.PrivilegedExceptionAction() { public CallbackHandler run() throws Exception { String defaultHandler = java.security.Security.getProperty (DEFAULT_HANDLER); if (defaultHandler == null || defaultHandler.length() == 0) return null; Class c = Class.forName( defaultHandler, true, finalLoader).asSubclass(CallbackHandler.class); @SuppressWarnings("deprecation") CallbackHandler result = c.newInstance(); return result; } }); } catch (java.security.PrivilegedActionException pae) { throw new LoginException(pae.getException().toString()); } // secure it with the caller's ACC if (this.callbackHandler != null && creatorAcc == null) { this.callbackHandler = new SecureCallbackHandler (java.security.AccessController.getContext(), this.callbackHandler); } } /** * Instantiate a new {@code LoginContext} object with a name. * * @param name the name used as the index into the * {@code Configuration}. * * @exception LoginException if the caller-specified {@code name} * does not appear in the {@code Configuration} * and there is no {@code Configuration} entry * for "{@code other}", or if the * {@code auth.login.defaultCallbackHandler} * security property was set, but the implementation * class could not be loaded. * * @exception SecurityException if a SecurityManager is set and * the caller does not have * AuthPermission("createLoginContext.name"), * or if a configuration entry for {@code name} does not exist and * the caller does not additionally have * AuthPermission("createLoginContext.other") */ public LoginContext(String name) throws LoginException { init(name); loadDefaultCallbackHandler(); } /** * Instantiate a new {@code LoginContext} object with a name * and a {@code Subject} object. * * @param name the name used as the index into the * {@code Configuration}. * * @param subject the {@code Subject} to authenticate. * * @exception LoginException if the caller-specified {@code name} * does not appear in the {@code Configuration} * and there is no {@code Configuration} entry * for "other", if the caller-specified {@code subject} * is {@code null}, or if the * auth.login.defaultCallbackHandler * security property was set, but the implementation * class could not be loaded. * * @exception SecurityException if a SecurityManager is set and * the caller does not have * AuthPermission("createLoginContext.name"), * or if a configuration entry for name does not exist and * the caller does not additionally have * AuthPermission("createLoginContext.other") */ public LoginContext(String name, Subject subject) throws LoginException { init(name); if (subject == null) throw new LoginException (ResourcesMgr.getString("invalid.null.Subject.provided")); this.subject = subject; subjectProvided = true; loadDefaultCallbackHandler(); } /** * Instantiate a new {@code LoginContext} object with a name * and a {@code CallbackHandler} object. * * @param name the name used as the index into the * {@code Configuration}. * * @param callbackHandler the {@code CallbackHandler} object used by * LoginModules to communicate with the user. * * @exception LoginException if the caller-specified {@code name} * does not appear in the {@code Configuration} * and there is no {@code Configuration} entry * for "{@code other}", or if the caller-specified * {@code callbackHandler} is {@code null}. * * @exception SecurityException if a SecurityManager is set and * the caller does not have * AuthPermission("createLoginContext.name"), * or if a configuration entry for name does not exist and * the caller does not additionally have * AuthPermission("createLoginContext.other") */ public LoginContext(String name, CallbackHandler callbackHandler) throws LoginException { init(name); if (callbackHandler == null) throw new LoginException(ResourcesMgr.getString ("invalid.null.CallbackHandler.provided")); this.callbackHandler = new SecureCallbackHandler (java.security.AccessController.getContext(), callbackHandler); } /** * Instantiate a new {@code LoginContext} object with a name, * a {@code Subject} to be authenticated, and a * {@code CallbackHandler} object. * * @param name the name used as the index into the * {@code Configuration}. * * @param subject the {@code Subject} to authenticate. * * @param callbackHandler the {@code CallbackHandler} object used by * LoginModules to communicate with the user. * * @exception LoginException if the caller-specified {@code name} * does not appear in the {@code Configuration} * and there is no {@code Configuration} entry * for "other", or if the caller-specified * {@code subject} is {@code null}, * or if the caller-specified * {@code callbackHandler} is {@code null}. * * @exception SecurityException if a SecurityManager is set and * the caller does not have * AuthPermission("createLoginContext.name"), * or if a configuration entry for name does not exist and * the caller does not additionally have * AuthPermission("createLoginContext.other") */ public LoginContext(String name, Subject subject, CallbackHandler callbackHandler) throws LoginException { this(name, subject); if (callbackHandler == null) throw new LoginException(ResourcesMgr.getString ("invalid.null.CallbackHandler.provided")); this.callbackHandler = new SecureCallbackHandler (java.security.AccessController.getContext(), callbackHandler); } /** * Instantiate a new {@code LoginContext} object with a name, * a {@code Subject} to be authenticated, * a {@code CallbackHandler} object, and a login {@code Configuration}. * * @param name the name used as the index into the caller-specified * {@code Configuration}. * * @param subject the {@code Subject} to authenticate, * or {@code null}. * * @param callbackHandler the {@code CallbackHandler} object used by * LoginModules to communicate with the user, or {@code null}. * * @param config the {@code Configuration} that lists the * login modules to be called to perform the authentication, * or {@code null}. * * @exception LoginException if the caller-specified {@code name} * does not appear in the {@code Configuration} * and there is no {@code Configuration} entry * for "other". * * @exception SecurityException if a SecurityManager is set, * config is {@code null}, * and either the caller does not have * AuthPermission("createLoginContext.name"), * or if a configuration entry for name does not exist and * the caller does not additionally have * AuthPermission("createLoginContext.other") * * @since 1.5 */ public LoginContext(String name, Subject subject, CallbackHandler callbackHandler, Configuration config) throws LoginException { this.config = config; if (config != null) { creatorAcc = java.security.AccessController.getContext(); } init(name); if (subject != null) { this.subject = subject; subjectProvided = true; } if (callbackHandler == null) { loadDefaultCallbackHandler(); } else if (creatorAcc == null) { this.callbackHandler = new SecureCallbackHandler (java.security.AccessController.getContext(), callbackHandler); } else { this.callbackHandler = callbackHandler; } } /** * Perform the authentication. * *

This method invokes the {@code login} method for each * LoginModule configured for the name specified to the * {@code LoginContext} constructor, as determined by the login * {@code Configuration}. Each {@code LoginModule} * then performs its respective type of authentication * (username/password, smart card pin verification, etc.). * *

This method completes a 2-phase authentication process by * calling each configured LoginModule's {@code commit} method * if the overall authentication succeeded (the relevant REQUIRED, * REQUISITE, SUFFICIENT, and OPTIONAL LoginModules succeeded), * or by calling each configured LoginModule's {@code abort} method * if the overall authentication failed. If authentication succeeded, * each successful LoginModule's {@code commit} method associates * the relevant Principals and Credentials with the {@code Subject}. * If authentication failed, each LoginModule's {@code abort} method * removes/destroys any previously stored state. * *

If the {@code commit} phase of the authentication process * fails, then the overall authentication fails and this method * invokes the {@code abort} method for each configured * {@code LoginModule}. * *

If the {@code abort} phase * fails for any reason, then this method propagates the * original exception thrown either during the {@code login} phase * or the {@code commit} phase. In either case, the overall * authentication fails. * *

In the case where multiple LoginModules fail, * this method propagates the exception raised by the first * {@code LoginModule} which failed. * *

Note that if this method enters the {@code abort} phase * (either the {@code login} or {@code commit} phase failed), * this method invokes all LoginModules configured for the * application regardless of their respective {@code Configuration} * flag parameters. Essentially this means that {@code Requisite} * and {@code Sufficient} semantics are ignored during the * {@code abort} phase. This guarantees that proper cleanup * and state restoration can take place. * * @exception LoginException if the authentication fails. */ public void login() throws LoginException { loginSucceeded = false; if (subject == null) { subject = new Subject(); } try { // module invoked in doPrivileged invokePriv(LOGIN_METHOD); invokePriv(COMMIT_METHOD); loginSucceeded = true; } catch (LoginException le) { try { invokePriv(ABORT_METHOD); } catch (LoginException le2) { throw le; } throw le; } } /** * Logout the {@code Subject}. * *

This method invokes the {@code logout} method for each * {@code LoginModule} configured for this {@code LoginContext}. * Each {@code LoginModule} performs its respective logout procedure * which may include removing/destroying * {@code Principal} and {@code Credential} information * from the {@code Subject} and state cleanup. * *

Note that this method invokes all LoginModules configured for the * application regardless of their respective * {@code Configuration} flag parameters. Essentially this means * that {@code Requisite} and {@code Sufficient} semantics are * ignored for this method. This guarantees that proper cleanup * and state restoration can take place. * * @exception LoginException if the logout fails. */ public void logout() throws LoginException { if (subject == null) { throw new LoginException(ResourcesMgr.getString ("null.subject.logout.called.before.login")); } // module invoked in doPrivileged invokePriv(LOGOUT_METHOD); } /** * Return the authenticated Subject. * * @return the authenticated Subject. If the caller specified a * Subject to this LoginContext's constructor, * this method returns the caller-specified Subject. * If a Subject was not specified and authentication succeeds, * this method returns the Subject instantiated and used for * authentication by this LoginContext. * If a Subject was not specified, and authentication fails or * has not been attempted, this method returns null. */ public Subject getSubject() { if (!loginSucceeded && !subjectProvided) return null; return subject; } private void clearState() { moduleIndex = 0; firstError = null; firstRequiredError = null; success = false; } private void throwException(LoginException originalError, LoginException le) throws LoginException { // first clear state clearState(); // throw the exception LoginException error = (originalError != null) ? originalError : le; throw error; } /** * Invokes the login, commit, and logout methods * from a LoginModule inside a doPrivileged block restricted * by creatorAcc (may be null). * * This version is called if the caller did not instantiate * the LoginContext with a Configuration object. */ private void invokePriv(final String methodName) throws LoginException { try { java.security.AccessController.doPrivileged (new java.security.PrivilegedExceptionAction() { public Void run() throws LoginException { invoke(methodName); return null; } }, creatorAcc); } catch (java.security.PrivilegedActionException pae) { throw (LoginException)pae.getException(); } } private void invoke(String methodName) throws LoginException { // start at moduleIndex // - this can only be non-zero if methodName is LOGIN_METHOD for (int i = moduleIndex; i < moduleStack.length; i++, moduleIndex++) { try { if (moduleStack[i].module == null) { // locate and instantiate the LoginModule // String name = moduleStack[i].entry.getLoginModuleName(); ServiceLoader sc = AccessController.doPrivileged( (PrivilegedAction>) () -> ServiceLoader.load( LoginModule.class, contextClassLoader)); for (LoginModule m: sc) { if (m.getClass().getName().equals(name)) { moduleStack[i].module = m; if (debug != null) { debug.println(name + " loaded as a service"); } break; } } if (moduleStack[i].module == null) { try { @SuppressWarnings("deprecation") Object tmp = Class.forName(name, false, contextClassLoader).newInstance(); moduleStack[i].module = (LoginModule) tmp; if (debug != null) { debug.println(name + " loaded via reflection"); } } catch (ClassNotFoundException e) { throw new LoginException("No LoginModule found for " + name); } } // invoke the LoginModule initialize method moduleStack[i].module.initialize(subject, callbackHandler, state, moduleStack[i].entry.getOptions()); } // find the requested method in the LoginModule boolean status; switch (methodName) { case LOGIN_METHOD: status = moduleStack[i].module.login(); break; case COMMIT_METHOD: status = moduleStack[i].module.commit(); break; case LOGOUT_METHOD: status = moduleStack[i].module.logout(); break; case ABORT_METHOD: status = moduleStack[i].module.abort(); break; default: throw new AssertionError("Unknown method " + methodName); } if (status == true) { // if SUFFICIENT, return if no prior REQUIRED errors if (!methodName.equals(ABORT_METHOD) && !methodName.equals(LOGOUT_METHOD) && moduleStack[i].entry.getControlFlag() == AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT && firstRequiredError == null) { // clear state clearState(); if (debug != null) debug.println(methodName + " SUFFICIENT success"); return; } if (debug != null) debug.println(methodName + " success"); success = true; } else { if (debug != null) debug.println(methodName + " ignored"); } } catch (Exception ite) { // failure cases LoginException le; if (ite instanceof PendingException && methodName.equals(LOGIN_METHOD)) { // XXX // // if a module's LOGIN_METHOD threw a PendingException // then immediately throw it. // // when LoginContext is called again, // the module that threw the exception is invoked first // (the module list is not invoked from the start). // previously thrown exception state is still present. // // it is assumed that the module which threw // the exception can have its // LOGIN_METHOD invoked twice in a row // without any commit/abort in between. // // in all cases when LoginContext returns // (either via natural return or by throwing an exception) // we need to call clearState before returning. // the only time that is not true is in this case - // do not call throwException here. throw (PendingException)ite; } else if (ite instanceof LoginException) { le = (LoginException)ite; } else if (ite instanceof SecurityException) { // do not want privacy leak // (e.g., sensitive file path in exception msg) le = new LoginException("Security Exception"); le.initCause(new SecurityException()); if (debug != null) { debug.println ("original security exception with detail msg " + "replaced by new exception with empty detail msg"); debug.println("original security exception: " + ite.toString()); } } else { // capture an unexpected LoginModule exception java.io.StringWriter sw = new java.io.StringWriter(); ite.printStackTrace (new java.io.PrintWriter(sw)); sw.flush(); le = new LoginException(sw.toString()); } if (moduleStack[i].entry.getControlFlag() == AppConfigurationEntry.LoginModuleControlFlag.REQUISITE) { if (debug != null) debug.println(methodName + " REQUISITE failure"); // if REQUISITE, then immediately throw an exception if (methodName.equals(ABORT_METHOD) || methodName.equals(LOGOUT_METHOD)) { if (firstRequiredError == null) firstRequiredError = le; } else { throwException(firstRequiredError, le); } } else if (moduleStack[i].entry.getControlFlag() == AppConfigurationEntry.LoginModuleControlFlag.REQUIRED) { if (debug != null) debug.println(methodName + " REQUIRED failure"); // mark down that a REQUIRED module failed if (firstRequiredError == null) firstRequiredError = le; } else { if (debug != null) debug.println(methodName + " OPTIONAL failure"); // mark down that an OPTIONAL module failed if (firstError == null) firstError = le; } } } // we went thru all the LoginModules. if (firstRequiredError != null) { // a REQUIRED module failed -- return the error throwException(firstRequiredError, null); } else if (success == false && firstError != null) { // no module succeeded -- return the first error throwException(firstError, null); } else if (success == false) { // no module succeeded -- all modules were IGNORED throwException(new LoginException (ResourcesMgr.getString("Login.Failure.all.modules.ignored")), null); } else { // success clearState(); return; } } /** * Wrap the caller-specified CallbackHandler in our own * and invoke it within a privileged block, constrained by * the caller's AccessControlContext. */ private static class SecureCallbackHandler implements CallbackHandler { private final java.security.AccessControlContext acc; private final CallbackHandler ch; SecureCallbackHandler(java.security.AccessControlContext acc, CallbackHandler ch) { this.acc = acc; this.ch = ch; } public void handle(final Callback[] callbacks) throws java.io.IOException, UnsupportedCallbackException { try { java.security.AccessController.doPrivileged (new java.security.PrivilegedExceptionAction() { public Void run() throws java.io.IOException, UnsupportedCallbackException { ch.handle(callbacks); return null; } }, acc); } catch (java.security.PrivilegedActionException pae) { if (pae.getException() instanceof java.io.IOException) { throw (java.io.IOException)pae.getException(); } else { throw (UnsupportedCallbackException)pae.getException(); } } } } /** * LoginModule information - * incapsulates Configuration info and actual module instances */ private static class ModuleInfo { AppConfigurationEntry entry; LoginModule module; ModuleInfo(AppConfigurationEntry newEntry, LoginModule newModule) { this.entry = newEntry; this.module = newModule; } } }