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 |
Authentication manager for YubiKey one-time-passwords. |
2257 |
27 Feb 14 |
nicklas |
The 'login' string is expected to be a YubiKey one-time password, but |
2257 |
27 Feb 14 |
nicklas |
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 |
To prevent YubiKey users to login using regular login, the first check is |
2257 |
27 Feb 14 |
nicklas |
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 |
If no matching user is found and the login is a valid YubiKey password, |
2257 |
27 Feb 14 |
nicklas |
a second user lookup is perfomed using the public ID part of the password. |
2257 |
27 Feb 14 |
nicklas |
If a user is found the password is sent to YubiCloud for verification. |
2257 |
27 Feb 14 |
nicklas |
If the verification is successful the final step is to use the regular |
2257 |
27 Feb 14 |
nicklas |
internal verification to check the password. |
2257 |
27 Feb 14 |
nicklas |
32 |
|
2257 |
27 Feb 14 |
nicklas |
@author nicklas |
2257 |
27 Feb 14 |
nicklas |
@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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// "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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// A valid YubiKey, but it not registered to any user... Hmmm... |
3994 |
13 Jun 16 |
nicklas |
// 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 |
// Check the password for each of the users in the list |
3994 |
13 Jun 16 |
nicklas |
// Normally, a single user is found, but theoretically it is |
3994 |
13 Jun 16 |
nicklas |
// possible to assign the same YubiKey to multiple accounts if |
3994 |
13 Jun 16 |
nicklas |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
// 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 |
If the authenticated user has a YubiKey configured we do not allow other |
5156 |
30 Nov 18 |
nicklas |
authentication methods, except those listed in the 'allow-other-authentication' |
5156 |
30 Nov 18 |
nicklas |
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 |
} |