You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tomcat.apache.org by Louis <la...@gmail.com> on 2007/12/21 16:02:08 UTC
TrustedPrincipalAuthorizor (extends AuthenticatorBase) Discussion
/ Comments
Hi, I'm new to the list though I've been developing on Tomcat and JBoss
for a few years now. Presented below are two classes I'd like feedback
on. Anything is appreciated but I'm hoping for feedback mostly on the
ideas behind the implementation more than particular bug fixes (that can
come if I'm on the right track).
Also, I've introduced this to the users list first in case I've gone
astray and there's a better way to do this elsewhere.
## Boring Backstory ## (This rambles for a bit. You can skip to the
juicy bit by scrolling down to # Implementation Details #)
My background is primarily struts/hibernate based development. In that
time I've had a few chances to write LoginModules (Similar to custom
Realm in Tomcat) for a couple of different needs.
I recently joined a team that was using the IIS isapi_redirector.dll for
tomcat to do transparent authentication. This is great for the end user.
However a problem the team was having is that they're still doing
programmatic security for everything. Why? The IIS connector connects to
Tomcat and embeds a Principal with the users Windows Domain / Username
in it. The team simply took this and then manually checked the users
permissions for each operation. Yes there's a time and a place for
programmatic security, but if you can let the container manage security
why not? (Note: This is all being done for a MS Windows Corporate
Intranet which is why the use of NTLM is accepted)
It was trivial to write a JBoss Login Module for their existing security
database. However, I came across a problem when getting the
authentication to happen: Tomcat wasn't authenticating the users. This
makes sense since I hadn't told Tomcat to do any authentication in the
WEB.XML file.
When trying to decide which Authentication I should use I reviewed the
source for the existing ones and realized that none of them would work.
The existing authenticators all check to see if a principal is already
in the request. If it is then the user has already been authenticated
and the authenticator never goes to the Realm to see what permissions /
roles the user has. Since the IIS connector plugs-in a Principal for the
user, this effectively short-circuited the ability of the container to
bind roles to the user.
My solution was to code my own TrustedPrincipalAuthorizor (extends
AuthenticatorBase). I called it an authorizor since it wasn't doing any
actual Authentication. This class checks for the existence of a
principal in the request. It then uses that principal as the key to tell
the security realm to lookup the users permissions and bind them into
the container for this request. Ergo the name. The last step of the
puzzle is to make 'authenticating' (ie: checking the password) optional
in your realm. I coded my LoginModule to have a flag checkPassword="false".
I know that's not the whole picture but it's the gist of it to get a
discussion going. What do people here think, am I following a logical
path or have I completely jumped the shark?
TrustedPrincipalAuthorizor.java
This allows me to force authentication to continue on to the realms in
order to populate the user's request with their roles. (IE: use the full
power of container managed security).
ContextLookupAuthenticatorBase.java
This came about because: 1. JBoss 3.2.5 doesn't support the <context>
element or a context.xml file from tomcat to be able to make deployment
changes to a program.
2. After studying the AuthenticatorBase I determined that it didn't
actually _need_ to be hosted inside of a context element. It can get the
context from the request, so it can determine dynamically which context
and realm apply to the request.
This was desireable since we use this function across all our intranet
applications and didn't want to have to declare it again for each app.
This is actually fairly redundant and I (at present) don't see why the
AuthenticatorBase must be implmented to be context dependant. (I can see
why it is, just not the architectural reason why it _must be_).
## Implementation Details ##
Here's the rough source for my TrustedPrincipalAuthorizor.
The java files can be downloaded from:
http://www.laj.ca/source/tomcat/TrustedPrincipalAuthorizor.java
http://www.laj.ca/source/tomcat/ContextLookupAuthenticatorBase.java
/*
* Copyright 2007 Louis A. J. Pierrson
* based on code
* Copyright 1999-2001,2004 The Apache Software Foundation.
*
* 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 ca.laj.catalina.authenticator;
import java.io.IOException;
import java.security.Principal;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.Realm;
import org.apache.catalina.authenticator.ContextLookupAuthenticatorBase;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* This class relies on a host container to supply the users name as a
Principal.
*
* The host container used during development was IIS with the Apache
* isapi_redirector2.dll for IIS. This dll redirects IIS requests from the
* specified URI's to Tomcat (or JBoss). With
* this, IIS includes the user's LAN domain/username from IIS' NTLM
authentication of
* the user. In this way, we now have an authenticated security
credential for
* the user. ie: We know who they are. What we don't know is what they
can do.
* That is for another zone.
* <p>
* Due to the need to force an 'authentication' to take place in order to be
* able to use the IIS user credential with the normal tomcat/JBoss (Java
* servlet container) security model, (ie: to lookup the user's roles)
* this custom authenticator provides a
* transparent challenge of the user. This allows us to rely on normal
* container based (declarative) security.
* <p>
* We can use this trusted credential to retreive the user's authorizations
* (via either Active Directory, company database, property file, KERBEROS,
* etc...)
*
* <p>
* PRE: This class must be used in conjunction with the
isapi_redirector2.dll
* or some other mechanism that results in the user's login getting embedded
* in the session in a <em>trusted</em> fashion. A base user principal
is set and the call
* request.getRemoteUser(); should not be returning null at this point.
* <p>
* To have this Authorizor recognized in web.xml by tomcat
* {@link org.apache.catalina.startup.ContextConfig}
* needs to be updated.
*
* <p>Installation:
* <code>
* <Host ...>
* <!--
* IISAuthorizer is embedded in a context. The context requires
a directory to exist
* under <server-instance>\iisauthorizer as well as having a
web-application using
* the context path <server-instance>\deploy\iisauthorizer.war
* The crossContext flag enables the operation of the
IISConnectorAuthorizer across other
* contexts.
* -->
* <Context path="iisauthorizer" crossContext="true">
* <Valve
className="ca.laj.catalina.authenticator.IISConnectorAuthorizer" />
* </Context>
* </Host>
* </code>
*
* @author Louis A. J. Pierrson
*/
public class TrustedPrincipalAuthorizor extends
ContextLookupAuthenticatorBase implements Lifecycle {
private static final Log log =
LogFactory.getLog(TrustedPrincipalAuthorizor.class);
// Authentication methods for login configuration
public static final String NTLM_METHOD = "NTLM";
public static final String IIS_NTLM_METHOD = "IIS_NTLM";
public String removeDomainFromPrincipal = "true";
/**
* @return the removeDomainFromPrincipal
*/
public String getRemoveDomainFromPrincipal() {
return removeDomainFromPrincipal;
}
/**
* @param removeDomainFromPrincipal the removeDomainFromPrincipal to set
*/
public void setRemoveDomainFromPrincipal(String
removeDomainFromPrincipal) {
if(removeDomainFromPrincipal == null) {
log.trace("removeDomainFromPrincipal set to default. (true)");
}
log.trace("Set removeDomainFromPrincipal to [" +
removeDomainFromPrincipal + "]");
this.removeDomainFromPrincipal = removeDomainFromPrincipal;
}
/**
* Authenticate the user making this request, based on the specified
* login configuration. Return <code>true</code> if any specified
* constraint has been satisfied, or <code>false</code> if we have
* created a response challenge already.
*
* @param request Request we are processing
* @param response Response we are creating
* @param config Login configuration describing how authentication
* should be performed
*
* @exception IOException if an input/output error occurs
* @see
ContextLookupAuthenticatorBase#authenticate(org.apache.catalina.HttpRequest,
org.apache.catalina.HttpResponse, LoginConfig)
*/
public boolean authenticate(org.apache.catalina.HttpRequest
catRequest, org.apache.catalina.HttpResponse catResponse, LoginConfig
loginConfig)
throws IOException
{
log.trace("called");
HttpServletRequest request =
(HttpServletRequest)catRequest.getRequest();
ServletResponse response = catResponse.getResponse();
HttpSession session = request.getSession();
// The following calls should all work
// Get the user principal set by the isapi_redirector2.dll
String remoteUser = request.getRemoteUser(); // should be not
null and a string like this: <domain>\\<username>
String authType = request.getAuthType(); // should be NTLM
String remoteAddr = request.getRemoteAddr();
String remoteHost = request.getRemoteHost();
Integer remotePort = new Integer(request.getRemotePort());
Principal userPrincipal = request.getUserPrincipal();
String authorization = request.getHeader("authorization");
// If the userPrincipal isn't set then there is an error in the
// configuration
//XXX DISCUSS: This could fail over to BASIC, FORM or some other
form of
// authentication
if(userPrincipal == null)
{
log.error("Could not authenticate the user as the expected " +
"user credential was not found.");
HttpServletResponse hres =
(HttpServletResponse) catResponse.getResponse();
hres.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}
// Process an IIS based NTLM login
if(!"NTLM".equals(authType)) {
log.info("Not NTLM authType");
return false;
}
// Pick the user name and password from the remoteUser & (possibly)
// NTLM challenge
String ntUsername = getNTUsername(remoteUser);
String ntDomain = getNTDomain(remoteUser);
String ntPassword = null; // Use NTLM to get the
password/credential data?
//FIXME
String parameterPassword = request.getParameter("password");
if(ntPassword == null && parameterPassword != null) {
log.warn("Using supplied password to spoof the realm.");
ntPassword = parameterPassword;
}
log.debug("Authorizing user [" +
ntDomain + ":" +
ntUsername +
"] using supplied NTLM credentials.");
// Instead of null, it would be good to be able to actually
// provide some mechanism of knowing how to trust this at the
// LoginModule layer (if indeed this goes to the login module
// layer)
Realm realm = getContext(catRequest).getRealm();
log.debug("Realm container: " + realm.getContainer().getName());
log.debug("Realm info: " + realm.getInfo());
String constraints = "";
SecurityConstraint[] sc =
realm.findSecurityConstraints(catRequest, getContext(catRequest));
for(int i = 0; i < sc.length; i++) {
constraints += sc[i] + ", ";
}
log.debug("Realm info: " + constraints);
String challengeUsername = ntUsername;
if("true".equals(removeDomainFromPrincipal)) {
userPrincipal.getName();
}
log.info("Challenging the realm with [" +
challengeUsername +
":" +
ntPassword +
"] using supplied NTLM credentials.");
Principal realmPrincipal =
realm.authenticate(challengeUsername,ntPassword);
if(realmPrincipal == null) {
log.info("User was not authenticated in the realm.");
HttpServletResponse hres =
(HttpServletResponse) catResponse.getResponse();
hres.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}
// Set the username/password on the session and set the
principal in request
// session.setNote(Constants.SESS_USERNAME_NOTE, ntUsername);
// session.setNote(Constants.SESS_PASSWORD_NOTE,
ntPassword);
log.info("User was authenticated. Registering in request.");
// Register the retrieved realm information
register(catRequest, catResponse, realmPrincipal, authType,
challengeUsername, ntPassword);
return true;
}
final static int NTDOMAIN = 1;
final static int NTUSER = 0;
public String getNTUsername(String ntRemoteUser) {
return splitNTRemoteUser(ntRemoteUser, NTUSER);
}
public String getNTDomain(String ntRemoteUser) {
return splitNTRemoteUser(ntRemoteUser, NTDOMAIN);
}
public String splitNTRemoteUser(String ntRemoteUser, int position) {
if(ntRemoteUser == null)
return null;
String[] parts = ntRemoteUser.split("\\\\");
// Reverse the order to give priority to the name
String[] tmp = new String[parts.length];
for(int i = 0; i < parts.length; i++)
tmp[i] = parts[parts.length-1 -i];
parts = tmp;
// If domain isn't supplied return null for the domain
if(position >= parts.length)
return null;
return parts[position];
}
}
/*
* Copyright 2007 Louis A. J. Pierrson
* from original code
* Copyright 1999-2001,2004 The Apache Software Foundation.
*
* 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 org.apache.catalina.authenticator;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.Authenticator;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.HttpRequest;
import org.apache.catalina.HttpResponse;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Logger;
import org.apache.catalina.Manager;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Realm;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.Session;
import org.apache.catalina.Valve;
import org.apache.catalina.ValveContext;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.util.DateTool;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.util.StringManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* JBoss 3.2.5 doesn't properly support the context elements and anything
* it doesn't explicitly support aren't supported at all.
* 3.2.6 accepts and reads the context.xml file.
* This is a work around needed to define the authenticator in
* a <context>
* <p>
* Specifically, this AuthenticatorBase dynamically checks which context it
* is being called by to determine the relevant context specific
information.
* (Like which SecurityRealm applies to the context)
* <p>
* This class completelely wraps the AuthenticatorBase in order to be
able to
* dynamically discover which Context(and thus Realm) a Request belongs
to. (This
* is why the original comments for the AuthenticatorBase are included.)
*
* @author Louis A. J. Pierrson
* @see AuthenticatorBase
*
*<p>
* Basic implementation of the <b>Valve</b> interface that enforces the
* <code><security-constraint></code> elements in the web application
* deployment descriptor. This functionality is implemented as a Valve
* so that it can be ommitted in environments that do not require these
* features. Individual implementations of each supported authentication
* method can subclass this base class as required.
* <p>
* <b>USAGE CONSTRAINT</b>: When this class is utilized, the Context to
* which it is attached (or a parent Container in a hierarchy) must have an
* associated Realm that can be used for authenticating users and
enumerating
* the roles to which they have been assigned.
* <p>
* <b>USAGE CONSTRAINT</b>: This Valve is only useful when processing HTTP
* requests. Requests of any other type will simply be passed through.
*
* @author Craig R. McClanahan
* @version $Revision: 1.19 $ $Date: 2004/04/26 21:54:15 $
*/
public abstract class ContextLookupAuthenticatorBase
extends AuthenticatorBase
implements Authenticator, Lifecycle {
private static Log log =
LogFactory.getLog(ContextLookupAuthenticatorBase.class);
// ----------------------------------------------------- Instance
Variables
/**
* The default message digest algorithm to use if we cannot use
* the requested one.
*/
protected static final String DEFAULT_ALGORITHM = "MD5";
/**
* The number of random bytes to include when generating a
* session identifier.
*/
protected static final int SESSION_ID_BYTES = 16;
/**
* The message digest algorithm to be used when generating session
* identifiers. This must be an algorithm supported by the
* <code>java.security.MessageDigest</code> class on your platform.
*/
protected String algorithm = DEFAULT_ALGORITHM;
/**
* Should we cache authenticated Principals if the request is part of
* an HTTP session?
*/
protected boolean cache = true;
/**
* The Context to which this Valve is attached.
*/
protected Context context = null;
/**
* The debugging detail level for this component.
*/
protected int debug = 0;
/**
* Return the MessageDigest implementation to be used when
* creating session identifiers.
*/
protected MessageDigest digest = null;
/**
* A String initialization parameter used to increase the entropy of
* the initialization of our random number generator.
*/
protected String entropy = null;
/**
* Descriptive information about this implementation.
*/
protected static final String info =
"org.apache.catalina.authenticator.AuthenticatorBase/1.0";
/**
* Flag to determine if we disable proxy caching, or leave the issue
* up to the webapp developer.
*/
protected boolean disableProxyCaching = true;
/**
* The lifecycle event support for this component.
*/
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
/**
* A random number generator to use when generating session identifiers.
*/
protected Random random = null;
/**
* The Java class name of the random number generator class to be used
* when generating session identifiers.
*/
protected String randomClass = "java.security.SecureRandom";
/**
* The string manager for this package.
*/
protected static final StringManager sm =
StringManager.getManager(Constants.Package);
/**
* The SingleSignOn implementation in our request processing chain,
* if there is one.
*/
protected SingleSignOn sso = null;
/**
* Has this component been started?
*/
protected boolean started = false;
/**
* "Expires" header always set to Date(1), so generate once only
*/
private static final String DATE_ONE =
(new SimpleDateFormat(DateTool.HTTP_RESPONSE_DATE_HEADER,
Locale.US)).format(new Date(1));
// -------------------------------------------------------------
Properties
/**
* Return the message digest algorithm for this Manager.
*/
public String getAlgorithm() {
return (this.algorithm);
}
/**
* Set the message digest algorithm for this Manager.
*
* @param algorithm The new message digest algorithm
*/
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
/**
* Return the cache authenticated Principals flag.
*/
public boolean getCache() {
return (this.cache);
}
/**
* Set the cache authenticated Principals flag.
*
* @param cache The new cache flag
*/
public void setCache(boolean cache) {
this.cache = cache;
}
/**
* Checks if the defined <code>container</code> is a Context. If not
* it will look up the context for the supplied request.
*
* @param request
* @return
*/
protected Context getContext(Request request) {
if(this.context instanceof Context)
return this.context;
else
return request.getContext();
}
/**
* Return the Container to which this Valve is attached.
*/
public Container getContainer() {
return (this.context);
}
/**
* Set the Container to which this Valve is attached.
*
* @param container The container to which we are attached
*/
public void setContainer(Container container) {
// Ignore behaviour of original AuthenticatorBase
//super.setContainer(container);
this.container = container;
if (!(container instanceof Context)) {
log.warn("Running using context lookup for each request.");
// throw new IllegalArgumentException
// (sm.getString("authenticator.notContext"));
} else {
this.context = (Context) container;
}
}
/**
* Return the debugging detail level for this component.
*/
public int getDebug() {
return (this.debug);
}
/**
* Set the debugging detail level for this component.
*
* @param debug The new debugging detail level
*/
public void setDebug(int debug) {
this.debug = debug;
}
/**
* Return the entropy increaser value, or compute a semi-useful value
* if this String has not yet been set.
*/
public String getEntropy() {
// Calculate a semi-useful value if this has not been set
if (this.entropy == null)
setEntropy(this.toString());
return (this.entropy);
}
/**
* Set the entropy increaser value.
*
* @param entropy The new entropy increaser value
*/
public void setEntropy(String entropy) {
this.entropy = entropy;
}
/**
* Return descriptive information about this Valve implementation.
*/
public String getInfo() {
return (info);
}
/**
* Return the random number generator class name.
*/
public String getRandomClass() {
return (this.randomClass);
}
/**
* Set the random number generator class name.
*
* @param randomClass The new random number generator class name
*/
public void setRandomClass(String randomClass) {
this.randomClass = randomClass;
}
/**
* Return the flag that states if we add headers to disable caching by
* proxies.
*/
public boolean getDisableProxyCaching() {
return disableProxyCaching;
}
/**
* Set the value of the flag that states if we add headers to disable
* caching by proxies.
* @param nocache <code>true</code> if we add headers to disable proxy
* caching, <code>false</code> if we leave the headers
alone.
*/
public void setDisableProxyCaching(boolean nocache) {
disableProxyCaching = nocache;
}
// --------------------------------------------------------- Public
Methods
/**
* Enforce the security restrictions in the web application deployment
* descriptor of our associated Context.
*
* @param request Request to be processed
* @param response Response to be processed
* @param context The valve context used to invoke the next valve
* in the current processing pipeline
*
* @exception IOException if an input/output error occurs
* @exception ServletException if thrown by a processing element
*/
public void invoke(Request request, Response response,
ValveContext context)
throws IOException, ServletException {
// If this is not an HTTP request, do nothing
if (!(request instanceof HttpRequest) ||
!(response instanceof HttpResponse)) {
context.invokeNext(request, response);
return;
}
if (!(request.getRequest() instanceof HttpServletRequest) ||
!(response.getResponse() instanceof HttpServletResponse)) {
context.invokeNext(request, response);
return;
}
HttpRequest hrequest = (HttpRequest) request;
HttpResponse hresponse = (HttpResponse) response;
if (log.isDebugEnabled())
log.debug("Security checking request " +
((HttpServletRequest) request.getRequest()).getMethod()
+ " " +
((HttpServletRequest)
request.getRequest()).getRequestURI());
LoginConfig config = getContext(request).getLoginConfig();
if(config == null || !"IIS-NTLM".equals(config.getAuthMethod())) {
context.invokeNext(request, response);
return;
}
// Have we got a cached authenticated Principal to record?
if (cache) {
Principal principal =
((HttpServletRequest)
request.getRequest()).getUserPrincipal();
if (principal == null) {
Session session = getSession(hrequest);
if (session != null) {
principal = session.getPrincipal();
if (principal != null) {
if (log.isDebugEnabled())
log.debug("We have cached auth type " +
session.getAuthType() +
" for principal " +
session.getPrincipal());
hrequest.setAuthType(session.getAuthType());
hrequest.setUserPrincipal(principal);
}
}
}
}
// Special handling for form-based logins to deal with the case
// where the login form (and therefore the "j_security_check" URI
// to which it submits) might be outside the secured area
String contextPath = getContext(request).getPath();
String requestURI = hrequest.getDecodedRequestURI();
if (requestURI.startsWith(contextPath) &&
requestURI.endsWith(Constants.FORM_ACTION)) {
if (!authenticate(hrequest, hresponse, config)) {
if (log.isDebugEnabled())
log.debug(" Failed authenticate() test ??" +
requestURI );
return;
}
}
Realm realm = getContext(request).getRealm();
// Is this request URI subject to a security constraint?
SecurityConstraint [] constraints
= realm.findSecurityConstraints(hrequest, getContext(request));
if ((constraints == null) /* &&
(!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) {
if (log.isDebugEnabled())
log.debug(" Not subject to any constraint");
context.invokeNext(request, response);
return;
}
// Make sure that constrained resources are not cached by web
proxies
// or browsers as caching can provide a security hole
HttpServletRequest hsrequest =
(HttpServletRequest)hrequest.getRequest();
if (disableProxyCaching &&
// FIXME: Disabled for Mozilla FORM support over SSL
// (improper caching issue)
//!hsrequest.isSecure() &&
!"POST".equalsIgnoreCase(hsrequest.getMethod())) {
HttpServletResponse sresponse =
(HttpServletResponse) response.getResponse();
sresponse.setHeader("Pragma", "No-cache");
sresponse.setHeader("Cache-Control", "no-cache");
sresponse.setHeader("Expires", DATE_ONE);
}
int i;
// Enforce any user data constraint for this security constraint
if (log.isDebugEnabled()) {
log.debug(" Calling hasUserDataPermission()");
}
if (!realm.hasUserDataPermission(hrequest, hresponse,
constraints)) {
if (log.isDebugEnabled()) {
log.debug(" Failed hasUserDataPermission() test");
}
/*
* ASSERT: Authenticator already set the appropriate
* HTTP status code, so we do not have to do anything special
*/
return;
}
for(i=0; i < constraints.length; i++) {
// Authenticate based upon the specified login configuration
if (constraints[i].getAuthConstraint()) {
if (log.isDebugEnabled()) {
log.debug(" Calling authenticate()");
}
if (!authenticate(hrequest, hresponse, config)) {
if (log.isDebugEnabled()) {
log.debug(" Failed authenticate() test");
}
/*
* ASSERT: Authenticator already set the appropriate
* HTTP status code, so we do not have to do anything
* special
*/
return;
} else {
break;
}
}
}
if (log.isDebugEnabled()) {
log.debug(" Calling accessControl()");
}
if (!realm.hasResourcePermission(hrequest, hresponse,
constraints,
getContext(request))) {
if (log.isDebugEnabled()) {
log.debug(" Failed accessControl() test");
}
/*
* ASSERT: AccessControl method has already set the
* appropriate HTTP status code, so we do not have to do
* anything special
*/
return;
}
// Any and all specified constraints have been satisfied
if (log.isDebugEnabled()) {
log.debug(" Successfully passed all security constraints");
}
context.invokeNext(request, response);
}
// ------------------------------------------------------ Protected
Methods
/**
* Associate the specified single sign on identifier with the
* specified Session.
*
* @param ssoId Single sign on identifier
* @param session Session to be associated
*/
protected void associate(String ssoId, Session session) {
if (sso == null)
return;
sso.associate(ssoId, session);
}
/**
* Authenticate the user making this request, based on the specified
* login configuration. Return <code>true</code> if any specified
* constraint has been satisfied, or <code>false</code> if we have
* created a response challenge already.
*
* @param request Request we are processing
* @param response Response we are creating
* @param config Login configuration describing how authentication
* should be performed
*
* @exception IOException if an input/output error occurs
*/
protected abstract boolean authenticate(HttpRequest request,
HttpResponse response,
LoginConfig config)
throws IOException;
/**
* Generate and return a new session identifier for the cookie that
* identifies an SSO principal.
*/
protected synchronized String generateSessionId() {
// Generate a byte array containing a session identifier
byte bytes[] = new byte[SESSION_ID_BYTES];
getRandom().nextBytes(bytes);
bytes = getDigest().digest(bytes);
// Render the result as a String of hexadecimal digits
StringBuffer result = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
byte b1 = (byte) ((bytes[i] & 0xf0) >> 4);
byte b2 = (byte) (bytes[i] & 0x0f);
if (b1 < 10)
result.append((char) ('0' + b1));
else
result.append((char) ('A' + (b1 - 10)));
if (b2 < 10)
result.append((char) ('0' + b2));
else
result.append((char) ('A' + (b2 - 10)));
}
return (result.toString());
}
/**
* Return the MessageDigest object to be used for calculating
* session identifiers. If none has been created yet, initialize
* one the first time this method is called.
*/
protected synchronized MessageDigest getDigest() {
if (this.digest == null) {
try {
this.digest = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
try {
this.digest =
MessageDigest.getInstance(DEFAULT_ALGORITHM);
} catch (NoSuchAlgorithmException f) {
this.digest = null;
}
}
}
return (this.digest);
}
/**
* Return the random number generator instance we should use for
* generating session identifiers. If there is no such generator
* currently defined, construct and seed a new one.
*/
protected synchronized Random getRandom() {
if (this.random == null) {
try {
Class clazz = Class.forName(randomClass);
this.random = (Random) clazz.newInstance();
long seed = System.currentTimeMillis();
char entropy[] = getEntropy().toCharArray();
for (int i = 0; i < entropy.length; i++) {
long update = ((byte) entropy[i]) << ((i % 8) * 8);
seed ^= update;
}
this.random.setSeed(seed);
} catch (Exception e) {
this.random = new java.util.Random();
}
}
return (this.random);
}
/**
* Return the internal Session that is associated with this HttpRequest,
* or <code>null</code> if there is no such Session.
*
* @param request The HttpRequest we are processing
*/
protected Session getSession(HttpRequest request) {
return (getSession(request, false));
}
/**
* Return the internal Session that is associated with this HttpRequest,
* possibly creating a new one if necessary, or <code>null</code> if
* there is no such session and we did not create one.
*
* @param request The HttpRequest we are processing
* @param create Should we create a session if needed?
*/
protected Session getSession(HttpRequest request, boolean create) {
HttpServletRequest hreq =
(HttpServletRequest) request.getRequest();
HttpSession hses = hreq.getSession(create);
if (hses == null)
return (null);
Manager manager = getContext(request).getManager();
if (manager == null)
return (null);
else {
try {
return (manager.findSession(hses.getId()));
} catch (IOException e) {
return (null);
}
}
}
/**
* Log a message on the Logger associated with our Container (if any).
*
* @param message Message to be logged
*/
protected void log(String message) {
Logger logger = context.getLogger();
if (logger != null)
logger.log("Authenticator[" + context.getPath() + "]: " +
message);
else
System.out.println("Authenticator[" + context.getPath() +
"]: " + message);
}
/**
* Log a message on the Logger associated with our Container (if any).
*
* @param message Message to be logged
* @param throwable Associated exception
*/
protected void log(String message, Throwable throwable) {
Logger logger = null;
if(context != null)
logger = context.getLogger();
if (logger != null)
logger.log("Authenticator[" + context.getPath() + "]: " +
message, throwable);
else {
System.out.println("Authenticator[" + context.getPath() +
"]: " + message);
throwable.printStackTrace(System.out);
}
}
/**
* Attempts reauthentication to the <code>Realm</code> using
* the credentials included in argument <code>entry</code>.
*
* @param ssoId identifier of SingleSignOn session with which the
* caller is associated
* @param request the request that needs to be authenticated
*/
protected boolean reauthenticateFromSSO
(String ssoId, HttpRequest request) {
if (sso == null || ssoId == null)
return false;
boolean reauthenticated = false;
Container parent = getContainer();
if (parent != null) {
Realm realm = parent.getRealm();
if (realm != null) {
reauthenticated = sso.reauthenticate(ssoId, realm, request);
}
}
if (reauthenticated) {
associate(ssoId, getSession(request, true));
if (log.isDebugEnabled()) {
HttpServletRequest hreq =
(HttpServletRequest) request.getRequest();
log.debug(" Reauthenticated cached principal '" +
hreq.getUserPrincipal().getName() +
"' with auth type '" + hreq.getAuthType() + "'");
}
}
return reauthenticated;
}
/**
* Register an authenticated Principal and authentication type in our
* request, in the current session (if there is one), and with our
* SingleSignOn valve, if there is one. Set the appropriate cookie
* to be returned.
*
* @param request The servlet request we are processing
* @param response The servlet response we are generating
* @param principal The authenticated Principal to be registered
* @param authType The authentication type to be registered
* @param username Username used to authenticate (if any)
* @param password Password used to authenticate (if any)
*/
protected void register(HttpRequest request, HttpResponse response,
Principal principal, String authType,
String username, String password) {
if (log.isDebugEnabled())
log.debug("Authenticated '" + principal.getName() + "' with
type '"
+ authType + "'");
// Cache the authentication information in our request
request.setAuthType(authType);
request.setUserPrincipal(principal);
Session session = getSession(request, false);
// Cache the authentication information in our session, if any
if (cache) {
if (session != null) {
session.setAuthType(authType);
session.setPrincipal(principal);
if (username != null)
session.setNote(Constants.SESS_USERNAME_NOTE, username);
else
session.removeNote(Constants.SESS_USERNAME_NOTE);
if (password != null)
session.setNote(Constants.SESS_PASSWORD_NOTE, password);
else
session.removeNote(Constants.SESS_PASSWORD_NOTE);
}
}
// Construct a cookie to be returned to the client
if (sso == null)
return;
// Only create a new SSO entry if the SSO did not already set a note
// for an existing entry (as it would do with subsequent requests
// for DIGEST and SSL authenticated contexts)
String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
if (ssoId == null) {
// Construct a cookie to be returned to the client
HttpServletResponse hres =
(HttpServletResponse) response.getResponse();
ssoId = generateSessionId();
Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE,
ssoId);
cookie.setMaxAge(-1);
cookie.setPath("/");
hres.addCookie(cookie);
// Register this principal with our SSO valve
sso.register(ssoId, principal, authType, username, password);
request.setNote(Constants.REQ_SSOID_NOTE, ssoId);
} else {
// Update the SSO session with the latest authentication data
sso.update(ssoId, principal, authType, username, password);
}
// Fix for Bug 10040
// Always associate a session with a new SSO reqistration.
// SSO entries are only removed from the SSO registry map when
// associated sessions are destroyed; if a new SSO entry is created
// above for this request and the user never revisits the
context, the
// SSO entry will never be cleared if we don't associate the session
if (session == null)
session = getSession(request, true);
sso.associate(ssoId, session);
}
// ------------------------------------------------------ Lifecycle
Methods
/**
* Add a lifecycle event listener to this component.
*
* @param listener The listener to add
*/
public void addLifecycleListener(LifecycleListener listener) {
lifecycle.addLifecycleListener(listener);
}
/**
* Get the lifecycle listeners associated with this lifecycle. If this
* Lifecycle has no listeners registered, a zero-length array is
returned.
*/
public LifecycleListener[] findLifecycleListeners() {
return lifecycle.findLifecycleListeners();
}
/**
* Remove a lifecycle event listener from this component.
*
* @param listener The listener to remove
*/
public void removeLifecycleListener(LifecycleListener listener) {
lifecycle.removeLifecycleListener(listener);
}
/**
* Prepare for the beginning of active use of the public methods of this
* component. This method should be called after
<code>configure()</code>,
* and before any of the public methods of the component are utilized.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
public void start() throws LifecycleException {
// Validate and update our current component state
if (started)
throw new LifecycleException
(sm.getString("authenticator.alreadyStarted"));
lifecycle.fireLifecycleEvent(START_EVENT, null);
if(context != null)
if ("org.apache.catalina.core.StandardContext".equals
(context.getClass().getName())) {
try {
// XXX What is this ???
Class paramTypes[] = new Class[0];
Object paramValues[] = new Object[0];
Method method =
container.getClass().getMethod("getDebug", paramTypes);
Integer result = (Integer) method.invoke(context,
paramValues);
setDebug(result.intValue());
} catch (Exception e) {
log.error("Exception getting debug value", e);
}
}
started = true;
// Look up the SingleSignOn implementation in our request processing
// path, if there is one
Container parent = container;
if(context != null)
parent = context.getParent();
while ((sso == null) && (parent != null)) {
if (!(parent instanceof Pipeline)) {
parent = parent.getParent();
continue;
}
Valve valves[] = ((Pipeline) parent).getValves();
for (int i = 0; i < valves.length; i++) {
if (valves[i] instanceof SingleSignOn) {
sso = (SingleSignOn) valves[i];
break;
}
}
if (sso == null)
parent = parent.getParent();
}
if (log.isDebugEnabled()) {
if (sso != null)
log.debug("Found SingleSignOn Valve at " + sso);
else
log.debug("No SingleSignOn Valve is present");
}
}
/**
* Gracefully terminate the active use of the public methods of this
* component. This method should be the last one called on a given
* instance of this component.
*
* @exception LifecycleException if this component detects a fatal error
* that needs to be reported
*/
public void stop() throws LifecycleException {
// Validate and update our current component state
if (!started)
throw new LifecycleException
(sm.getString("authenticator.notStarted"));
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
started = false;
sso = null;
}
}
---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org
Re: TrustedPrincipalAuthorizor (extends AuthenticatorBase) Discussion
/ Comments
Posted by Louis <la...@gmail.com>.
Christopher Schultz wrote:
> Louis wrote:
>
>> When trying to decide which Authentication I should use I reviewed the
>> source for the existing ones and realized that none of them would work.
>> The existing authenticators all check to see if a principal is already
>> in the request. If it is then the user has already been authenticated
>> and the authenticator never goes to the Realm to see what permissions /
>> roles the user has.
>>
>
> That's not how it's supposed to work; authentication and authorization
> are separate. Without looking at the source (don't have it handy right
> now), the flow ought to be:
>
> - - Determine authentication and authorization requirements
> based upon request URL
> - - Check request for principal
> |- if not found, check request for credentials
> | - if found, load principal into request
> | - if not found, display login challenge and exit
> - - Use found principal for authorization (check roles)
>
> Tomcat's built-in Principal and Request objects conspire to correctly
> wire HttpServletRequest.isUserInRole to something within the Principal.
> If you choose to gather your Principal information from somewhere else
> (NTLM), then you need to take care to play along with Tomcat's notion of
> where roles are stored.
>
> If Tomcat does not use the HttpServletRequest.isUserInRole method to
> perform its authorization, then you will need to pay attention to the
> way it /does/ check for the roles.
>
>
>> Since the IIS connector plugs-in a Principal for the
>> user, this effectively short-circuited the ability of the container to
>> bind roles to the user.
>>
>
> You should be able to wrap that Principal in some other object if necessary.
>
>
>> My solution was to code my own TrustedPrincipalAuthorizor (extends
>> AuthenticatorBase). I called it an authorizor since it wasn't doing any
>> actual Authentication.
>>
>
> Is it doing any authorization, or is it just attaching roles to a
> Principal and allowing Tomcat to perform the authorization?
>
>
>> This class checks for the existence of a
>> principal in the request. It then uses that principal as the key to tell
>> the security realm to lookup the users permissions and bind them into
>> the container for this request.
>>
>
> That seems a little time-consuming to do for every request. Since you're
> working with Tomcat's internals, why not permanently attach the role
> information to the Principal object one time?
>
>
>> Ergo the name. The last step of the
>> puzzle is to make 'authenticating' (ie: checking the password) optional
>> in your realm. I coded my LoginModule to have a flag checkPassword="false".
>>
>> I know that's not the whole picture but it's the gist of it to get a
>> discussion going. What do people here think, am I following a logical
>> path or have I completely jumped the shark?
>>
>
> Why not work with the code that the redirector uses to set the Principal
> in the first place? I think it would be a cleaner implementation to set
> the roles at the time the Principal object is created.
>
> - -chris
Thanks Chris for the detailed response.
I'll have to go back and review how the IIS isapi_redirector.dll is
hooked into Tomcat to see if changing the redirector would better suit
my needs.
The reason why I went with this style implementation: it was the first
way I've figured out of implementing my requirements and it worked. (ie:
not a good reason architecturally, it just worked). One argument in
favour of this style of implementation is that it can work with other
external redirector implementations that embed a Principal in the Request.
I'm not sure about your question re: Is it doing any authorization? My
understanding of JAAS is that the binding of Roles to the Principal was
the Authorization. Doing this does allows the security declarations in
the web.xml to work as expected. ie: The container (tomcat/jboss/etc.)
is doing the work of checking if the user is in the role.
Yes it is time consuming to do bind the roles on every request. I'm not
sure yet how the redirector binds the Principal to the Request (per
session or per request) and/or how Roles are normally bound to the
Principal.
I'll have to go back to the Tomcat source and trace what's happening in
the Realm source.
I don't have the experience or tools to be able to create an updated
redirector. I'm trying to make an implementation that isn't specific to
the IIS redirector and could be used with other types of redirectors.
Also, I'm trying to keep my implementation 'java' and I don't know what
the redirector .dll is written in.
I see where you're going with your suggestions. If the Principal from
the redirector is created once and the same one is used across requests
(in a session) then it should be trivial to mutate it (by binding the
roles in) instead of substituting it with a new one with the roles bound in.
Thanks for taking the time to write a detailed response. I'll get back
to the list with my changes when I've got them done.
Best Regards,
Louis
---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org
Re: TrustedPrincipalAuthorizor (extends AuthenticatorBase) Discussion
/ Comments
Posted by Christopher Schultz <ch...@christopherschultz.net>.
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Louis,
Louis wrote:
> When trying to decide which Authentication I should use I reviewed the
> source for the existing ones and realized that none of them would work.
> The existing authenticators all check to see if a principal is already
> in the request. If it is then the user has already been authenticated
> and the authenticator never goes to the Realm to see what permissions /
> roles the user has.
That's not how it's supposed to work; authentication and authorization
are separate. Without looking at the source (don't have it handy right
now), the flow ought to be:
- - Determine authentication and authorization requirements
based upon request URL
- - Check request for principal
|- if not found, check request for credentials
| - if found, load principal into request
| - if not found, display login challenge and exit
- - Use found principal for authorization (check roles)
Tomcat's built-in Principal and Request objects conspire to correctly
wire HttpServletRequest.isUserInRole to something within the Principal.
If you choose to gather your Principal information from somewhere else
(NTLM), then you need to take care to play along with Tomcat's notion of
where roles are stored.
If Tomcat does not use the HttpServletRequest.isUserInRole method to
perform its authorization, then you will need to pay attention to the
way it /does/ check for the roles.
> Since the IIS connector plugs-in a Principal for the
> user, this effectively short-circuited the ability of the container to
> bind roles to the user.
You should be able to wrap that Principal in some other object if necessary.
> My solution was to code my own TrustedPrincipalAuthorizor (extends
> AuthenticatorBase). I called it an authorizor since it wasn't doing any
> actual Authentication.
Is it doing any authorization, or is it just attaching roles to a
Principal and allowing Tomcat to perform the authorization?
> This class checks for the existence of a
> principal in the request. It then uses that principal as the key to tell
> the security realm to lookup the users permissions and bind them into
> the container for this request.
That seems a little time-consuming to do for every request. Since you're
working with Tomcat's internals, why not permanently attach the role
information to the Principal object one time?
> Ergo the name. The last step of the
> puzzle is to make 'authenticating' (ie: checking the password) optional
> in your realm. I coded my LoginModule to have a flag checkPassword="false".
>
> I know that's not the whole picture but it's the gist of it to get a
> discussion going. What do people here think, am I following a logical
> path or have I completely jumped the shark?
Why not work with the code that the redirector uses to set the Principal
in the first place? I think it would be a cleaner implementation to set
the roles at the time the Principal object is created.
- -chris
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.8 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org
iEYEARECAAYFAkdt67EACgkQ9CaO5/Lv0PCxWgCfTrpnpbtbocP5XFna6akX9hNM
UIIAoJwGgxGIL/wkVscAeh47g2rjk2RQ
=simi
-----END PGP SIGNATURE-----
---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org