/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.security.auth.spi.otp; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.acl.Group; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import javax.security.jacc.PolicyContext; import javax.security.jacc.PolicyContextException; import javax.servlet.http.HttpServletRequest; import org.jboss.security.PicketBoxLogger; import org.jboss.security.PicketBoxMessages; import org.jboss.security.SecurityConstants; import org.jboss.security.SimplePrincipal; import org.jboss.security.otp.TimeBasedOTP; import org.jboss.security.otp.TimeBasedOTPUtil; /** * <p> * Login Module that can be configured to validate a Time based OTP. * </p> * * <p> * Usage: * This login module needs to be configured along with one of the other JBoss login modules such * as {@code org.jboss.security.auth.spi.DatabaseServerLoginModule} or * {@code org.jboss.security.auth.spi.LdapLoginModule} * </p> * Example configuration: * <p> * <pre> * {@code * <application-policy name="otp"> <authentication> <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required"> <module-option name="usersProperties">props/jmx-console-users.properties</module-option> <module-option name="rolesProperties">props/jmx-console-roles.properties</module-option> </login-module> <login-module code="org.jboss.security.auth.spi.otp.JBossTimeBasedOTPLoginModule" /> </authentication> </application-policy> * } * </pre> * </p> * * <p> * Configurable Options: * </p> * <p> * <ul> * <li>algorithm: either "HmacSHA1", "HmacSHA256" or "HmacSHA512" [Default: "HmacSHA1"]</li> * <li>numOfDigits: Number of digits in the TOTP. Default is 6.</li> * <li>additionalRoles: any additional roles that you want to add into the authenticated subject (on success). For multiple roles, * separate with a comma</li> * </ul> * </p> * * <p> * This login module requires the presence of "otp-users.properties" on the class path with the format: * username=key * </p> * * <p> * An example of otp-users.properties is: * </p> * <p> * <pre> admin=35cae61d6d51a7b3af </pre> * </p> * * * @author Anil.Saldhana@redhat.com * @since Sep 21, 2010 */ public class JBossTimeBasedOTPLoginModule implements LoginModule { // see AbstractServerLoginModule private static final String PASSWORD_STACKING = "password-stacking"; private static final String USE_FIRST_PASSWORD = "useFirstPass"; private static final String NUM_OF_DIGITS_OPT = "numOfDigits"; private static final String ALGORITHM = "algorithm"; private static final String ADDITIONAL_ROLES = "additionalRoles"; private static final String[] ALL_VALID_OPTIONS = { PASSWORD_STACKING,USE_FIRST_PASSWORD,NUM_OF_DIGITS_OPT,ALGORITHM,ADDITIONAL_ROLES }; public static final String TOTP = "totp"; private Map<String,Object> lmSharedState = new HashMap<String,Object>(); private Map<String, Object> lmOptions = new HashMap<String,Object>(); private CallbackHandler callbackHandler; private boolean useFirstPass; //This is the number of digits in the totp private int NUMBER_OF_DIGITS = 6; private String additionalRoles = null; /** * Default algorithm is HMAC_SHA1 */ private String algorithm = TimeBasedOTP.HMAC_SHA1; //Default private Subject subject; public void initialize( Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options ) { /* TODO: this module should really extend AbstractServerLoginModule where the options check is integrated. * the code here has been intentionally kept identical */ HashSet<String> validOptions = new HashSet<String>(Arrays.asList(ALL_VALID_OPTIONS)); for (String key : options.keySet()) { if (!validOptions.contains(key)) PicketBoxLogger.LOGGER.warnInvalidModuleOption(key); } this.subject = subject; this.callbackHandler = callbackHandler; this.lmSharedState.putAll( sharedState ); this.lmOptions.putAll( options ); /* Check for password sharing options. Any non-null value for password_stacking sets useFirstPass as this module has no way to validate any shared password. */ String passwordStacking = (String) options.get(PASSWORD_STACKING); if( passwordStacking != null && passwordStacking.equalsIgnoreCase(USE_FIRST_PASSWORD) ) useFirstPass = true; //Option for number of digits String numDigitString = (String) options.get(NUM_OF_DIGITS_OPT); if( numDigitString != null && numDigitString.length() > 0 ) NUMBER_OF_DIGITS = Integer.parseInt( numDigitString ); //Algorithm String algorithmStr = (String) options.get(ALGORITHM); if( algorithmStr != null && !algorithmStr.isEmpty()) { if( algorithmStr.equalsIgnoreCase( TimeBasedOTP.HMAC_SHA256) ) algorithm = TimeBasedOTP.HMAC_SHA256; if( algorithmStr.equalsIgnoreCase( TimeBasedOTP.HMAC_SHA512 )) algorithm = TimeBasedOTP.HMAC_SHA512; } additionalRoles = (String) options.get(ADDITIONAL_ROLES); } /** * @see {@code LoginModule#login()} */ public boolean login() throws LoginException { String username; if(useFirstPass) { username = (String) lmSharedState.get("javax.security.auth.login.name"); } else { NameCallback nc = new NameCallback(PicketBoxMessages.MESSAGES.enterUsernameMessage(), "guest"); Callback[] callbacks = { nc }; try { callbackHandler.handle(callbacks); } catch ( Exception e ) { LoginException le = new LoginException(); le.initCause(e); throw le; } username = nc.getName(); } //Load the otp-users.properties file ClassLoader tcl = SecurityActions.getContextClassLoader(); InputStream is = null; Properties otp = new Properties(); try { is = tcl.getResourceAsStream( "otp-users.properties" ); otp.load(is); } catch (IOException e) { LoginException le = new LoginException(); le.initCause(e); throw le; } finally { safeClose(is); } String seed = otp.getProperty( username ); String submittedTOTP = this.getTimeBasedOTPFromRequest(); if( submittedTOTP == null || submittedTOTP.length() == 0 ) { throw new LoginException(); } try { boolean result = false; if( algorithm.equals( TimeBasedOTP.HMAC_SHA1 )) { result = TimeBasedOTPUtil.validate( submittedTOTP, seed.getBytes() , NUMBER_OF_DIGITS ); } else if( algorithm.equals( TimeBasedOTP.HMAC_SHA256 )) { result = TimeBasedOTPUtil.validate256( submittedTOTP, seed.getBytes() , NUMBER_OF_DIGITS ); } else if( algorithm.equals( TimeBasedOTP.HMAC_SHA512 )) { result = TimeBasedOTPUtil.validate512( submittedTOTP, seed.getBytes() , NUMBER_OF_DIGITS ); } if(!result) throw new LoginException(); //add in roles if needed Set<Group> groupPrincipals = subject.getPrincipals( Group.class ); if( groupPrincipals != null && groupPrincipals.size() > 0 ) { appendRoles( groupPrincipals.iterator().next() ); } return result; } catch (GeneralSecurityException e) { LoginException le = new LoginException(); le.initCause(e); throw le; } } /** * @see {@code LoginModule#commit()} */ public boolean commit() throws LoginException { return true; } /** * @see {@code LoginModule#abort()} */ public boolean abort() throws LoginException { return true; } /** * @see {@code LoginModule#logout()} */ public boolean logout() throws LoginException { return true; } private String getTimeBasedOTPFromRequest() { String totp = null; //This is JBoss AS specific mechanism String WEB_REQUEST_KEY = "javax.servlet.http.HttpServletRequest"; try { HttpServletRequest request = (HttpServletRequest) PolicyContext.getContext(WEB_REQUEST_KEY); totp = request.getParameter( TOTP ); } catch (PolicyContextException e) { PicketBoxLogger.LOGGER.debugErrorGettingRequestFromPolicyContext(e); } return totp; } private void appendRoles( Group group ) { if( ! group.getName().equals( SecurityConstants.ROLES_IDENTIFIER ) ) return; if(additionalRoles != null && !additionalRoles.isEmpty()) { StringTokenizer st = new StringTokenizer( additionalRoles , "," ); while(st.hasMoreTokens()) { group.addMember( new SimplePrincipal( st.nextToken().trim() ) ); } } } private void safeClose(InputStream fis) { try { if(fis != null) { fis.close(); } } catch(Exception ignored) {} } }