extensions/net.sf.basedb.yubikey/trunk/src/net/sf/basedb/yubikey/YubiKeyAuthenticationManager.java

Code
Comments
Other
Rev Date Author Line
2252 25 Feb 14 nicklas 1 package net.sf.basedb.yubikey;
2252 25 Feb 14 nicklas 2
2252 25 Feb 14 nicklas 3
2258 28 Feb 14 nicklas 4 import java.util.List;
2258 28 Feb 14 nicklas 5
3991 10 Jun 16 nicklas 6 import com.yubico.client.v2.ResponseStatus;
3991 10 Jun 16 nicklas 7 import com.yubico.client.v2.VerificationResponse;
2252 25 Feb 14 nicklas 8 import com.yubico.client.v2.YubicoClient;
2252 25 Feb 14 nicklas 9
2252 25 Feb 14 nicklas 10 import net.sf.basedb.core.AuthenticationContext;
2258 28 Feb 14 nicklas 11 import net.sf.basedb.core.Type;
2255 26 Feb 14 nicklas 12 import net.sf.basedb.core.authentication.AuthenticatedUser;
2252 25 Feb 14 nicklas 13 import net.sf.basedb.core.authentication.AuthenticationManager;
2252 25 Feb 14 nicklas 14 import net.sf.basedb.core.authentication.LoginException;
2252 25 Feb 14 nicklas 15 import net.sf.basedb.core.authentication.LoginRequest;
2252 25 Feb 14 nicklas 16 import net.sf.basedb.core.data.UserData;
2258 28 Feb 14 nicklas 17 import net.sf.basedb.core.query.QueryParameter;
2252 25 Feb 14 nicklas 18
2257 27 Feb 14 nicklas 19 /**
2257 27 Feb 14 nicklas 20    Authentication manager for YubiKey one-time-passwords.
2257 27 Feb 14 nicklas 21    The 'login' string is expected to be a YubiKey one-time password, but
2257 27 Feb 14 nicklas 22    may also be a regular login for user accounts that has not been YubiKey-enabled.
2257 27 Feb 14 nicklas 23    
2257 27 Feb 14 nicklas 24    To prevent YubiKey users to login using regular login, the first check is
2257 27 Feb 14 nicklas 25    to see if the login matches an existing user and if that user is YubiKey-enabled.
2257 27 Feb 14 nicklas 26    
2257 27 Feb 14 nicklas 27    If no matching user is found and the login is a valid YubiKey password,
2257 27 Feb 14 nicklas 28    a second user lookup is perfomed using the public ID part of the password.
2257 27 Feb 14 nicklas 29    If a user is found the password is sent to YubiCloud for verification.
2257 27 Feb 14 nicklas 30    If the verification is successful the final step is to use the regular
2257 27 Feb 14 nicklas 31    internal verification to check the password.
2257 27 Feb 14 nicklas 32    
2257 27 Feb 14 nicklas 33   @author nicklas
2257 27 Feb 14 nicklas 34   @since 1.0
2257 27 Feb 14 nicklas 35 */
2252 25 Feb 14 nicklas 36 public class YubiKeyAuthenticationManager 
2252 25 Feb 14 nicklas 37   implements AuthenticationManager 
2252 25 Feb 14 nicklas 38 {
2252 25 Feb 14 nicklas 39
2252 25 Feb 14 nicklas 40   private final AuthenticationContext context;
2252 25 Feb 14 nicklas 41
2252 25 Feb 14 nicklas 42   public YubiKeyAuthenticationManager(AuthenticationContext context)
2252 25 Feb 14 nicklas 43   {
2252 25 Feb 14 nicklas 44     this.context = context;
2252 25 Feb 14 nicklas 45   }
2252 25 Feb 14 nicklas 46   
2252 25 Feb 14 nicklas 47   @Override
2255 26 Feb 14 nicklas 48   public AuthenticatedUser authenticate() 
2252 25 Feb 14 nicklas 49   {
3993 10 Jun 16 nicklas 50     String clientId = context.getSessionControl().getExternalClientId();
3993 10 Jun 16 nicklas 51     boolean yubiKeyIsRequired = YubiKey.isYubiKeyRequiredForClient(clientId);
3993 10 Jun 16 nicklas 52
2252 25 Feb 14 nicklas 53     LoginRequest request = context.getLoginRequest();
2252 25 Feb 14 nicklas 54     String login = request.getLogin();
3994 13 Jun 16 nicklas 55     String username = request.getAttribute("username");
5148 26 Nov 18 nicklas 56     String loginForm = request.getAttribute("login-form");
5150 28 Nov 18 nicklas 57     
5150 28 Nov 18 nicklas 58     boolean isYubiKeyForm = loginForm != null && loginForm.startsWith("net.sf.basedb.yubikey");
5150 28 Nov 18 nicklas 59     if (loginForm != null && !isYubiKeyForm)
5148 26 Nov 18 nicklas 60     {
5150 28 Nov 18 nicklas 61       // The login is made with a different login manager...
5150 28 Nov 18 nicklas 62       if (yubiKeyIsRequired)
5150 28 Nov 18 nicklas 63       {
5150 28 Nov 18 nicklas 64         // If this client require YubiKey we do not allow the login
5150 28 Nov 18 nicklas 65         throw new LoginException("YubiKey login is required for using this client (" + clientId + ").");
5150 28 Nov 18 nicklas 66       }
5150 28 Nov 18 nicklas 67
5150 28 Nov 18 nicklas 68       // Return null to let internal authentication handle it
5148 26 Nov 18 nicklas 69       return null;
5148 26 Nov 18 nicklas 70     }
2252 25 Feb 14 nicklas 71     
3133 10 Feb 15 nicklas 72     // Is the login similar to a YubiKey OTP string?
3133 10 Feb 15 nicklas 73     if (!YubicoClient.isValidOTPFormat(login)) 
2252 25 Feb 14 nicklas 74     {
5150 28 Nov 18 nicklas 75       if (isYubiKeyForm)
3994 13 Jun 16 nicklas 76       {
5150 28 Nov 18 nicklas 77         // The YubiKey login form was used but it was not a valid OTP
5150 28 Nov 18 nicklas 78         throw new LoginException("Invalid YubiKey one-time-password format.");
3994 13 Jun 16 nicklas 79       }
3994 13 Jun 16 nicklas 80       
3993 10 Jun 16 nicklas 81       if (yubiKeyIsRequired)
3993 10 Jun 16 nicklas 82       {
5150 28 Nov 18 nicklas 83         // "null" login form, if this client require YubiKey we do not allow the login
5150 28 Nov 18 nicklas 84         throw new LoginException("YubiKey login is required for using this client (" + clientId + ").");
3993 10 Jun 16 nicklas 85       }
3993 10 Jun 16 nicklas 86       
3133 10 Feb 15 nicklas 87       // Return null to let internal authentication handle it
2257 27 Feb 14 nicklas 88       return null;
2252 25 Feb 14 nicklas 89     }
3133 10 Feb 15 nicklas 90
3133 10 Feb 15 nicklas 91     // Verify the one-time-password using the YubiCloud service
3991 10 Jun 16 nicklas 92     VerificationResponse response = null;
3133 10 Feb 15 nicklas 93     try
2252 25 Feb 14 nicklas 94     {
3133 10 Feb 15 nicklas 95       YubicoClient client = YubiKey.getYubicoClient();
3133 10 Feb 15 nicklas 96       response = client.verify(login);
3133 10 Feb 15 nicklas 97     }
3133 10 Feb 15 nicklas 98     catch (Exception e)
3133 10 Feb 15 nicklas 99     {
3133 10 Feb 15 nicklas 100       throw new LoginException(e.getMessage(), e);
3133 10 Feb 15 nicklas 101     }
2258 28 Feb 14 nicklas 102       
3991 10 Jun 16 nicklas 103     ResponseStatus status = response.getStatus();  
3991 10 Jun 16 nicklas 104     if (!response.isOk())
3133 10 Feb 15 nicklas 105     {
3133 10 Feb 15 nicklas 106       String msg = "Invalid YubiKey";
3991 10 Jun 16 nicklas 107       if (status == ResponseStatus.BAD_OTP)
2258 28 Feb 14 nicklas 108       {
5150 28 Nov 18 nicklas 109         msg = "Invalid one-time-password format.";
2258 28 Feb 14 nicklas 110       }
3991 10 Jun 16 nicklas 111       else if (status == ResponseStatus.REPLAYED_OTP)
2252 25 Feb 14 nicklas 112       {
5150 28 Nov 18 nicklas 113         msg = "One-time-password has already been used.";
3133 10 Feb 15 nicklas 114       }
3133 10 Feb 15 nicklas 115       throw new LoginException(msg + " (" + status + ")");
3133 10 Feb 15 nicklas 116     }
3133 10 Feb 15 nicklas 117     
3994 13 Jun 16 nicklas 118     // The YubiKey is valid! Now we need to verify the password and/or username
3133 10 Feb 15 nicklas 119     String publicId = YubicoClient.getPublicId(login);
3133 10 Feb 15 nicklas 120
3994 13 Jun 16 nicklas 121     // There are two cases depending on if a username was given or not
3133 10 Feb 15 nicklas 122     UserData user = null;
3994 13 Jun 16 nicklas 123     if (username != null)
3133 10 Feb 15 nicklas 124     {
3994 13 Jun 16 nicklas 125       // Find user with given login
3994 13 Jun 16 nicklas 126       UserData u = context.getUserByLogin(username);
3994 13 Jun 16 nicklas 127       if (u == null)
3133 10 Feb 15 nicklas 128       {
5150 28 Nov 18 nicklas 129         throw new LoginException("Unknown username '" + username + "'.");
2252 25 Feb 14 nicklas 130       }
3994 13 Jun 16 nicklas 131       
3994 13 Jun 16 nicklas 132       // Verify that the YubiKey public ID belongs to this user
3994 13 Jun 16 nicklas 133       if (!publicId.equals(u.getExtended("yubiKeyId")))
3133 10 Feb 15 nicklas 134       {
5150 28 Nov 18 nicklas 135         throw new LoginException("This YubiKey doesn't belong to user '" + username + "'.");
3133 10 Feb 15 nicklas 136       }
3994 13 Jun 16 nicklas 137       
3994 13 Jun 16 nicklas 138       // Verify password
3994 13 Jun 16 nicklas 139       LoginRequest internal = new LoginRequest(u.getId(), request.getPassword());
3994 13 Jun 16 nicklas 140       context.verifyUserInternal(internal);
3994 13 Jun 16 nicklas 141       
3994 13 Jun 16 nicklas 142       // All ok!
3994 13 Jun 16 nicklas 143       user = u;
2252 25 Feb 14 nicklas 144     }
3994 13 Jun 16 nicklas 145     else
3994 13 Jun 16 nicklas 146     {
3994 13 Jun 16 nicklas 147       // Find user(s) in BASE with yubiKeyId=<public-id>
3994 13 Jun 16 nicklas 148       QueryParameter qp = new QueryParameter("ykId", false, Type.STRING, publicId);
3994 13 Jun 16 nicklas 149       List<UserData> users = context.findUsers("yubiKeyId=:ykId", qp);
3133 10 Feb 15 nicklas 150
3994 13 Jun 16 nicklas 151       if (users.size() == 0)
3994 13 Jun 16 nicklas 152       {
3994 13 Jun 16 nicklas 153         // A valid YubiKey, but it not registered to any user... Hmmm...
3994 13 Jun 16 nicklas 154         // Anyone can buy a YubiKey and try to login here... Block it!
5150 28 Nov 18 nicklas 155         throw new LoginException("Unknown YubiKey (ID=" + publicId + ").");
3994 13 Jun 16 nicklas 156       }
3994 13 Jun 16 nicklas 157
3994 13 Jun 16 nicklas 158       // Check the password for each of the users in the list
3994 13 Jun 16 nicklas 159       // Normally, a single user is found, but theoretically it is
3994 13 Jun 16 nicklas 160       // possible to assign the same YubiKey to multiple accounts if
3994 13 Jun 16 nicklas 161       // they have different password
3994 13 Jun 16 nicklas 162       RuntimeException loginFailed = null;
3994 13 Jun 16 nicklas 163       for (UserData u : users)
3994 13 Jun 16 nicklas 164       {
3994 13 Jun 16 nicklas 165         LoginRequest internal = new LoginRequest(u.getId(), request.getPassword());
3994 13 Jun 16 nicklas 166         try
3994 13 Jun 16 nicklas 167         {
3994 13 Jun 16 nicklas 168           context.verifyUserInternal(internal);
3994 13 Jun 16 nicklas 169           // If we get here, the password is ok and we clear the error and break out of the loop
3994 13 Jun 16 nicklas 170           user = u;
3994 13 Jun 16 nicklas 171           loginFailed = null;
3994 13 Jun 16 nicklas 172           break;
3994 13 Jun 16 nicklas 173         }
3994 13 Jun 16 nicklas 174         catch (RuntimeException ex)
3994 13 Jun 16 nicklas 175         {
3994 13 Jun 16 nicklas 176           // Save the first exception so we can re-throw it later if needed
3994 13 Jun 16 nicklas 177           if (loginFailed == null) loginFailed = ex;
3994 13 Jun 16 nicklas 178         }
3994 13 Jun 16 nicklas 179       }
3994 13 Jun 16 nicklas 180
3994 13 Jun 16 nicklas 181       // If no user with correct password was found we re-throw the first exception
3994 13 Jun 16 nicklas 182       if (loginFailed != null) 
3994 13 Jun 16 nicklas 183       {
3994 13 Jun 16 nicklas 184         throw loginFailed;
3994 13 Jun 16 nicklas 185       }
3133 10 Feb 15 nicklas 186     }
2252 25 Feb 14 nicklas 187     
3133 10 Feb 15 nicklas 188     // The 'user' should always be non-null if we get here, but just in case we missed something...
3133 10 Feb 15 nicklas 189     AuthenticatedUser auth = null;
3133 10 Feb 15 nicklas 190     if (user != null)
3133 10 Feb 15 nicklas 191     {
3133 10 Feb 15 nicklas 192       // YubiKey is valid and the password is valid -- the user is allowed to access BASE!
3133 10 Feb 15 nicklas 193       auth = new AuthenticatedUser(YubiKey.AUTHENTICATION_METHOD, user);
3133 10 Feb 15 nicklas 194     }
3133 10 Feb 15 nicklas 195     return auth;
2252 25 Feb 14 nicklas 196   }
2252 25 Feb 14 nicklas 197
5156 30 Nov 18 nicklas 198   /**
5156 30 Nov 18 nicklas 199     If the authenticated user has a YubiKey configured we do not allow other
5156 30 Nov 18 nicklas 200     authentication methods, except those listed in the 'allow-other-authentication'
5156 30 Nov 18 nicklas 201     setting in the 'yubikey.properties' file.
5156 30 Nov 18 nicklas 202   */
5156 30 Nov 18 nicklas 203   @Override
5156 30 Nov 18 nicklas 204   public void vetoAuthenticatedUser(UserData user, AuthenticatedUser auth) 
5156 30 Nov 18 nicklas 205   {
5156 30 Nov 18 nicklas 206     String yubiKeyId = (String)user.getExtended("yubiKeyId");
5156 30 Nov 18 nicklas 207     if (yubiKeyId != null && !YubiKey.isAuthenticationMethodAllowed(auth.getAuthenticationMethod()))
5156 30 Nov 18 nicklas 208     {
5156 30 Nov 18 nicklas 209       throw new LoginException("User '" + user.getLogin() + "' must login with YubiKey!");
5156 30 Nov 18 nicklas 210     }
5156 30 Nov 18 nicklas 211   }
5156 30 Nov 18 nicklas 212
2252 25 Feb 14 nicklas 213 }