diff --git a/README.md b/README.md index 6eb7a87..55c673c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # OpenID Connect Authenticator for Tomcat -This is an authenticator implementation for [Apache Tomcat](http://tomcat.apache.org/) 9.0, 8.5 and 8.0 that allows web-applications to use [OpenID Connect](http://openid.net/connect/) to log users in. +This is an authenticator implementation for [Apache Tomcat](http://tomcat.apache.org/) 9.0 and 8.5 that allows web-applications to use [OpenID Connect](http://openid.net/connect/) to log users in. -References to Tomcat documenation in this manual link to Tomcat version 9.0. Corresponding pages for Tomcat 8.5 and 8.0 can be easily found on the Apache Tomcat website. +References to Tomcat documenation in this manual link to Tomcat version 9.0. Corresponding pages for Tomcat 8.5 can be easily found on the Apache Tomcat website. A complete sample web-application is available at https://github.com/boylesoftware/tomcat-oidcauth-sample. @@ -41,7 +41,7 @@ https://boylesoftware.com/maven/repo-os/org/bsworks/catalina/authenticator/oidc/ The JAR need to be added to the Tomcat's classpath, for example, by placing it in `$CATALINA_BASE/lib` directory (see Tomcat's [Class Loader How-To](https://tomcat.apache.org/tomcat-9.0-doc/class-loader-howto.html) for more info). -Tomcat versions 9.0 and 8.5 have slightly different interface for the authenticators compared to version 8.0. That is why there are binaries of the authenticator for each version of Tomcat. Make sure that you use one built for the correct Tomcat version. +There are separate binaries of the authenticator for Tomcat version 8.5 and 9.0. Make sure that you use one that's built for your version of Tomcat. ## Configuration @@ -52,10 +52,10 @@ The authenticator is added to Tomcat configuration as a [Valve](https://tomcat.a providers="..." /> ``` -For Tomcat 8.0 it will look like the following: +For Tomcat 8.5 it will look like the following: ```xml - ``` diff --git a/build.gradle b/build.gradle index a94f858..7f9cce0 100644 --- a/build.gradle +++ b/build.gradle @@ -11,19 +11,13 @@ sourceCompatibility = '1.8' targetCompatibility = '1.8' group = 'org.bsworks.catalina.authenticator.oidc' -version = '2.2.5' +version = '2.3.0' task jar(type: Jar, overwrite: true) {} jar.enabled = false javadoc.enabled = false -sourceSets.remove(sourceSets.main) sourceSets { - mainTomcat80 { - java { - srcDirs = ['src/common/java', 'src/tomcat80/java'] - } - } mainTomcat85 { java { srcDirs = ['src/common/java', 'src/tomcat85/java'] @@ -37,7 +31,7 @@ sourceSets { } task compileJava(type: JavaCompile, overwrite: true) { - dependsOn compileMainTomcat80Java, compileMainTomcat85Java, compileMainTomcat90Java + dependsOn compileMainTomcat85Java, compileMainTomcat90Java } ext.sharedManifest = manifest { @@ -48,13 +42,6 @@ ext.sharedManifest = manifest { ) } -task mainTomcat80Jar(type: Jar) { - classifier = 'tomcat80' - from sourceSets.mainTomcat80.output - manifest = project.manifest { - from sharedManifest - } -} task mainTomcat85Jar(type: Jar) { classifier 'tomcat85' from sourceSets.mainTomcat85.output @@ -75,26 +62,20 @@ repositories { } dependencies { - mainTomcat80Implementation( - 'org.apache.tomcat:tomcat-catalina:8.0.+', - 'org.apache.tomcat:tomcat-juli:8.0.+', - 'org.apache.tomcat:tomcat-util:8.0.+' - ) mainTomcat85Implementation( - 'org.apache.tomcat:tomcat-catalina:8.5.+', - 'org.apache.tomcat:tomcat-juli:8.5.+', - 'org.apache.tomcat:tomcat-util:8.5.+' + 'org.apache.tomcat:tomcat-catalina:[8.5.50,8.6.0)', + 'org.apache.tomcat:tomcat-juli:[8.5.50,8.6.0)', + 'org.apache.tomcat:tomcat-util:[8.5.50,8.6.0)' ) mainTomcat90Implementation( - 'org.apache.tomcat:tomcat-catalina:9.0.+', - 'org.apache.tomcat:tomcat-juli:9.0.+', - 'org.apache.tomcat:tomcat-util:9.0.+' + 'org.apache.tomcat:tomcat-catalina:[9.0.30,9.1.0)', + 'org.apache.tomcat:tomcat-juli:[9.0.30,9.1.0)', + 'org.apache.tomcat:tomcat-util:[9.0.30,9.1.0)' ) } artifacts { archives( - mainTomcat80Jar, mainTomcat85Jar, mainTomcat90Jar ) @@ -135,14 +116,13 @@ publishing { url = 'https://github.com/boylesoftware/tomcat-oidcauth/issues' } } - artifact mainTomcat80Jar artifact mainTomcat85Jar artifact mainTomcat90Jar } } repositories { maven { - name = 'Boyle Software Open Source Maven Repository' + name = 'local' url = "file://${buildDir}/repo" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c..1b16c34 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/common/java/org/bsworks/catalina/authenticator/oidc/BaseOpenIDConnectAuthenticator.java b/src/common/java/org/bsworks/catalina/authenticator/oidc/BaseOpenIDConnectAuthenticator.java index 161d15c..2967d79 100644 --- a/src/common/java/org/bsworks/catalina/authenticator/oidc/BaseOpenIDConnectAuthenticator.java +++ b/src/common/java/org/bsworks/catalina/authenticator/oidc/BaseOpenIDConnectAuthenticator.java @@ -429,6 +429,44 @@ public String toString() { } + /** + * Authenticated user descriptor. + */ + private static final class AuthedUser { + + /** + * Principal. + */ + final Principal principal; + + /** + * Username. + */ + final String username; + + /** + * Password, if any. + */ + final String password; + + + /** + * Create new authenticated user descriptor. + * + * @param principal Principal. + * @param username Username. + * @param password Password, or {@code null} if not applicable. + */ + AuthedUser(final Principal principal, + final String username, final String password) { + + this.principal = principal; + this.username = username; + this.password = password; + } + } + + /** * Name of request attribute made available to the login page that maps * configured OP issuer IDs to the corresponding authorization endpoint @@ -722,6 +760,9 @@ public void setHttpReadTimeout(final int httpReadTimeout) { protected synchronized void startInternal() throws LifecycleException { + // verify Tomcat version + this.ensureTomcatVersion(); + // verify that providers are configured if (this.providers == null) throw new LifecycleException("OpenIDConnectAuthenticator requires" @@ -779,6 +820,15 @@ protected synchronized void startInternal() super.startInternal(); } + /** + * Verify that the authenticator is running under a compatible version of + * Tomcat. + * + * @throws LifecycleException If the Tomcat version is incompatible. + */ + protected abstract void ensureTomcatVersion() + throws LifecycleException; + /** * Parse deprecated OP configuration syntax. * @@ -814,10 +864,6 @@ protected boolean performAuthentication(final Request request, final boolean debug = this.log.isDebugEnabled(); - // check if already authenticated - if (this.checkForCachedAuthentication(request, response, true)) - return true; - // try to reauthenticate if caching principal is disabled if (!this.cache && this.reauthenticateNoCache(request, response)) return true; @@ -826,6 +872,10 @@ protected boolean performAuthentication(final Request request, if (this.matchRequest(request)) return this.processResubmit(request, response); + // check if already authenticated + if (this.checkForCachedAuthentication(request, response, true)) + return true; + // the request is not authenticated: // determine if authentication submission @@ -854,35 +904,33 @@ protected boolean performAuthentication(final Request request, if (session == null) { // log using container log (why container?) - if (this.containerLog.isDebugEnabled()) - this.containerLog.debug( - "user took so long to log on the session expired"); + this.log.debug("user took so long to log on the session expired"); - // redirect to the configured landing page, if any - if (!this.redirectToLandingPage(request, response)) - response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, - sm.getString("authenticator.sessionExpired")); + // process expired session + this.processExpiredSession(request, response); // done, authentication failure return false; } + if (debug) + this.log.debug("existing session id " + session.getId()); - // the authenticated principal - Principal principal = null; + // the authenticated user + AuthedUser authedUser = null; // check if OIDC authentication response or form submission if ((request.getParameter("code") != null) || (request.getParameter("error") != null)) { - principal = this.processAuthResponse(session, + authedUser = this.processAuthResponse(session, request); } else if (!this.noForm) { // form submission - principal = this.processAuthFormSubmission(session, + authedUser = this.processAuthFormSubmission(session, request.getParameter(Constants.FORM_USERNAME), request.getParameter(Constants.FORM_PASSWORD)); } // check if authentication failure - if (principal == null) { + if (authedUser == null) { this.forwardToErrorPage(request, response, this.context.getLoginConfig()); return false; @@ -890,14 +938,33 @@ protected boolean performAuthentication(final Request request, // successful authentication if (debug) - this.log.debug("authentication of \"" + principal.getName() - + "\" was successful"); + this.log.debug("authentication of \"" + + authedUser.principal.getName() + "\" was successful"); + + // change session id (to prevent a session fixation attack) + if (this.getChangeSessionIdOnAuthentication()) { + final String expectedSessionId = (String) session.getNote( + Constants.SESSION_ID_NOTE); + if ((expectedSessionId == null) || !expectedSessionId.equals( + request.getRequestedSessionId())) { + if (debug) + this.log.debug("unable to change session id" + + ", expiring the session: expected session id is " + + expectedSessionId + ", requested session id is " + + request.getRequestedSessionId()); + session.expire(); + this.processExpiredSession(request, response); + return false; + } + } // save the authenticated principal in our session - session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal); + this.register(request, response, authedUser.principal, + HttpServletRequest.FORM_AUTH, + authedUser.username, authedUser.password); // get the original unauthenticated request URI - final String origRequestURI = savedRequestURL(session); + final String origRequestURI = this.savedRequestURL(session); if (debug) this.log.debug("redirecting to original URI: " + origRequestURI); @@ -928,7 +995,7 @@ protected boolean performAuthentication(final Request request, * @param request The request. * @param response The response. * - * @return {@code true} if was successfully reauthenticated and not further + * @return {@code true} if was successfully reauthenticated and no further * authentication action is required. If authentication logic should * proceed, returns {@code false}. */ @@ -979,8 +1046,12 @@ protected boolean reauthenticateNoCache(final Request request, return false; } - // set principal on the session - session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal); + // successfully reauthenticated, register the principal + if (debug) + this.log.debug("successfully reauthenticated username \"" + + username + "\""); + this.register(request, response, principal, + HttpServletRequest.FORM_AUTH, username, password); // check if resubmit after successful authentication if (this.matchRequest(request)) { @@ -990,13 +1061,6 @@ protected boolean reauthenticateNoCache(final Request request, return false; } - // successfully reauthenticated, register the principal - if (debug) - this.log.debug("successfully reauthenticated username \"" - + username + "\""); - this.register(request, response, principal, - HttpServletRequest.FORM_AUTH, username, password); - // no further authentication action required return true; } @@ -1025,14 +1089,6 @@ protected boolean processResubmit(final Request request, this.log.debug("restore request from session " + session.getIdInternal()); - // get authenticated principal and register it on the request - final Principal principal = - (Principal) session.getNote(Constants.FORM_PRINCIPAL_NOTE); - this.register(request, response, principal, - HttpServletRequest.FORM_AUTH, - (String) session.getNote(Constants.SESS_USERNAME_NOTE), - (String) session.getNote(Constants.SESS_PASSWORD_NOTE)); - // if principal is cached, remove authentication info from the session if (this.cache) { session.removeNote(Constants.SESS_USERNAME_NOTE); @@ -1198,9 +1254,9 @@ protected void addLoginConfiguration(final Request request) * @param username Submitted username. * @param password Submitted password. * - * @return The authenticated principal, or {@code null} if login failure. + * @return The authenticated user, or {@code null} if login failure. */ - protected Principal processAuthFormSubmission(final Session session, + protected AuthedUser processAuthFormSubmission(final Session session, final String username, final String password) { final boolean debug = this.log.isDebugEnabled(); @@ -1217,12 +1273,8 @@ protected Principal processAuthFormSubmission(final Session session, return null; } - // save authentication info in the session - session.setNote(Constants.SESS_USERNAME_NOTE, username); - session.setNote(Constants.SESS_PASSWORD_NOTE, password); - - // return the principal - return principal; + // return the user descriptor + return new AuthedUser(principal, username, password); } /** @@ -1231,12 +1283,12 @@ protected Principal processAuthFormSubmission(final Session session, * @param session The session. * @param request The request representing the authentication response. * - * @return The authenticated principal, or {@code null} if could not + * @return The authenticated user, or {@code null} if could not * authenticate. * * @throws IOException If an I/O error happens communicating with the OP. */ - protected Principal processAuthResponse(final Session session, + protected AuthedUser processAuthResponse(final Session session, final Request request) throws IOException { @@ -1442,8 +1494,8 @@ protected Principal processAuthResponse(final Session session, // save authorization in the session for the application session.getSession().setAttribute(AUTHORIZATION_ATT, authorization); - // return the principal - return principal; + // return the user descriptor + return new AuthedUser(principal, username, null); } /** @@ -1606,6 +1658,27 @@ protected TokenEndpointResponse callTokenEndpoint(final OPDescriptor opDesc, return response; } + /** + * Process the case when the session expired while waiting for the user + * login input. If landing page is configured, tries to redirect to it. + * Otherwise, sends back an request timeout error response. + * + * @param request The request. + * @param response The response. + * + * @throws IOException If an I/O error happens communicating with the + * client. + */ + protected void processExpiredSession(final Request request, + final HttpServletResponse response) + throws IOException { + + // redirect to the configured landing page, if any + if (!this.redirectToLandingPage(request, response)) + response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, + sm.getString("authenticator.sessionExpired")); + } + /** * Redirect to the configured landing page, if any. * @@ -1666,4 +1739,19 @@ protected String getBaseURL(final Request request) { return baseURLBuf.toString(); } + + @Override + public void logout(final Request request) { + + final Session session = request.getSessionInternal(false); + if (session != null) { + session.removeNote(SESS_STATE_NOTE); + session.removeNote(Constants.SESS_USERNAME_NOTE); + session.removeNote(SESS_OIDC_AUTH_NOTE); + session.removeNote(Constants.FORM_REQUEST_NOTE); + session.getSession().removeAttribute(AUTHORIZATION_ATT); + } + + super.logout(request); + } } diff --git a/src/tomcat80/java/org/bsworks/catalina/authenticator/oidc/tomcat80/OpenIDConnectAuthenticator.java b/src/tomcat80/java/org/bsworks/catalina/authenticator/oidc/tomcat80/OpenIDConnectAuthenticator.java deleted file mode 100644 index 98b2414..0000000 --- a/src/tomcat80/java/org/bsworks/catalina/authenticator/oidc/tomcat80/OpenIDConnectAuthenticator.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.bsworks.catalina.authenticator.oidc.tomcat80; - -import java.io.IOException; - -import javax.servlet.http.HttpServletResponse; - -import org.apache.catalina.connector.Request; -import org.bsworks.catalina.authenticator.oidc.BaseOpenIDConnectAuthenticator; - - -/** - * OpenID Connect authenticator implementation for Tomcat 8.0. - * - * @author Lev Himmelfarb - */ -public class OpenIDConnectAuthenticator - extends BaseOpenIDConnectAuthenticator { - - /* (non-Javadoc) - * @see org.apache.catalina.authenticator.FormAuthenticator#authenticate(org.apache.catalina.connector.Request, javax.servlet.http.HttpServletResponse) - */ - @Override - public boolean authenticate(final Request request, - final HttpServletResponse response) - throws IOException { - - return this.performAuthentication(request, response); - } -} diff --git a/src/tomcat85/java/org/bsworks/catalina/authenticator/oidc/tomcat85/OpenIDConnectAuthenticator.java b/src/tomcat85/java/org/bsworks/catalina/authenticator/oidc/tomcat85/OpenIDConnectAuthenticator.java index 315ff0f..c33370d 100644 --- a/src/tomcat85/java/org/bsworks/catalina/authenticator/oidc/tomcat85/OpenIDConnectAuthenticator.java +++ b/src/tomcat85/java/org/bsworks/catalina/authenticator/oidc/tomcat85/OpenIDConnectAuthenticator.java @@ -1,10 +1,13 @@ package org.bsworks.catalina.authenticator.oidc.tomcat85; import java.io.IOException; +import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; +import org.apache.catalina.LifecycleException; import org.apache.catalina.connector.Request; +import org.apache.catalina.util.ServerInfo; import org.bsworks.catalina.authenticator.oidc.BaseOpenIDConnectAuthenticator; @@ -16,9 +19,20 @@ public class OpenIDConnectAuthenticator extends BaseOpenIDConnectAuthenticator { - /* (non-Javadoc) - * @see org.apache.catalina.authenticator.FormAuthenticator#doAuthenticate(org.apache.catalina.connector.Request, javax.servlet.http.HttpServletResponse) - */ + @Override + protected void ensureTomcatVersion() + throws LifecycleException { + + final Integer[] versionParts = Stream.of(ServerInfo.getServerNumber().split("\\.")) + .map(v -> Integer.parseInt(v)) + .toArray(Integer[]::new); + if ((versionParts[0].intValue() != 8) + || (versionParts[1].intValue() != 5) + || (versionParts[2].intValue() < 50)) + throw new LifecycleException("OpenIDConnectAuthenticator requires" + + " Apache Tomcat 8.5 version 8.5.50 or higher."); + } + @Override protected boolean doAuthenticate(final Request request, final HttpServletResponse response) diff --git a/src/tomcat90/java/org/bsworks/catalina/authenticator/oidc/tomcat90/OpenIDConnectAuthenticator.java b/src/tomcat90/java/org/bsworks/catalina/authenticator/oidc/tomcat90/OpenIDConnectAuthenticator.java index b2db9c2..cff89bf 100644 --- a/src/tomcat90/java/org/bsworks/catalina/authenticator/oidc/tomcat90/OpenIDConnectAuthenticator.java +++ b/src/tomcat90/java/org/bsworks/catalina/authenticator/oidc/tomcat90/OpenIDConnectAuthenticator.java @@ -1,10 +1,13 @@ package org.bsworks.catalina.authenticator.oidc.tomcat90; import java.io.IOException; +import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; +import org.apache.catalina.LifecycleException; import org.apache.catalina.connector.Request; +import org.apache.catalina.util.ServerInfo; import org.bsworks.catalina.authenticator.oidc.BaseOpenIDConnectAuthenticator; @@ -16,9 +19,20 @@ public class OpenIDConnectAuthenticator extends BaseOpenIDConnectAuthenticator { - /* (non-Javadoc) - * @see org.apache.catalina.authenticator.FormAuthenticator#doAuthenticate(org.apache.catalina.connector.Request, javax.servlet.http.HttpServletResponse) - */ + @Override + protected void ensureTomcatVersion() + throws LifecycleException { + + final Integer[] versionParts = Stream.of(ServerInfo.getServerNumber().split("\\.")) + .map(v -> Integer.parseInt(v)) + .toArray(Integer[]::new); + if ((versionParts[0].intValue() != 9) + || (versionParts[1].intValue() != 0) + || (versionParts[2].intValue() < 30)) + throw new LifecycleException("OpenIDConnectAuthenticator requires" + + " Apache Tomcat 9.0 version 9.0.30 or higher."); + } + @Override protected boolean doAuthenticate(final Request request, final HttpServletResponse response)