/
ClientCertificateLoginModule.java
279 lines (239 loc) · 10.5 KB
/
ClientCertificateLoginModule.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/*
* Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 1997, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.enterprise.security.auth.login;
import static java.util.logging.Level.FINE;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.Map;
import java.util.logging.Logger;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.ChoiceCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.glassfish.internal.api.Globals;
import org.glassfish.security.common.UserNameAndPassword;
import org.glassfish.security.common.UserPrincipal;
import com.sun.enterprise.security.SecurityLoggerInfo;
import com.sun.enterprise.security.auth.login.common.X509CertificateCredential;
import com.sun.enterprise.security.integration.AppClientSSL;
import com.sun.enterprise.security.ssl.SSLUtils;
import com.sun.enterprise.util.LocalStringManagerImpl;
/**
* <p>
* This LoginModule authenticates users with X509 certificates.
*
* <p>
* If testUser successfully authenticates itself, a <code>UserPrincipal</code> with the user's username is added to the
* Subject.
*
* <p>
* This LoginModule recognizes the debug option. If set to true in the login Configuration, debug messages will be
* output to the output stream, System.out.
*
* @author Harpreet Singh (harpreet.singh@sun.com)
*/
public class ClientCertificateLoginModule implements LoginModule {
private static final Logger _logger = SecurityLoggerInfo.getLogger();
private static LocalStringManagerImpl localStrings = new LocalStringManagerImpl(ClientCertificateLoginModule.class);
private static KeyStore keyStore;
// initial state
private Subject subject;
private CallbackHandler callbackHandler;
// configurable option
private boolean debug;
// the authentication status
private boolean succeeded;
private boolean commitSucceeded;
private String alias;
private X509Certificate certificate;
private UserPrincipal userPrincipal;
private AppClientSSL ssl;
private SSLUtils sslUtils;
public static void setKeyStore(KeyStore keyStore) {
ClientCertificateLoginModule.keyStore = keyStore;
}
/**
* Initialize this <code>LoginModule</code>.
*
* @param subject the <code>Subject</code> to be authenticated.
* @param callbackHandler a <code>CallbackHandler</code> for communicating with the end user (prompting for usernames
* and passwords, for example).
* @param sharedState shared <code>LoginModule</code> state.
* @param options options specified in the login <code>Configuration</code> for this particular
* <code>LoginModule</code>.
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
// Initialize any configured options
debug = "true".equalsIgnoreCase((String) options.get("debug"));
sslUtils = Globals.getDefaultHabitat().getService(SSLUtils.class);
}
/**
* Authenticate the user by prompting for a username and password.
*
* @return true in all cases since this <code>LoginModule</code> should not be ignored.
* @throws LoginException if this <code>LoginModule</code> is unable to perform the authentication.
*/
@Override
public boolean login() throws LoginException {
// Prompt for a username and password
if (callbackHandler == null) {
throw new LoginException("Error: no CallbackHandler available " + "to garner authentication information from the user");
}
try {
String[] as = new String[keyStore.size()];
String[] aliasString = new String[keyStore.size()];
Enumeration<String> aliases = keyStore.aliases();
for (int i = 0; i < keyStore.size(); i++) {
aliasString[i] = aliases.nextElement();
as[i] = ((X509Certificate) keyStore.getCertificate(aliasString[i])).getSubjectX500Principal().getName();
}
Callback[] callbacks = new Callback[1];
callbacks[0] = new ChoiceCallback(localStrings.getLocalString("login.certificate", "Choose from list of certificates: "), as, 0,
false);
callbackHandler.handle(callbacks);
int[] idx = ((ChoiceCallback) callbacks[0]).getSelectedIndexes();
if (idx == null) {
throw new LoginException("No certificate selected!");
}
if (idx[0] == -1) {
throw new LoginException("Incorrect keystore password");
}
// Print debugging information
if (debug) {
if (_logger.isLoggable(FINE)) {
_logger.log(FINE, "\t\t[ClientCertificateLoginModule] " + "user entered certificate: ");
for (int element : idx) {
_logger.log(FINE, aliasString[element]);
}
}
}
// The authenticate method previously picked out the wrong alias.
// Since we allow only 1 choice the first element in idx idx[0] should have the selected index.
this.alias = aliasString[idx[0]];
certificate = (X509Certificate) keyStore.getCertificate(alias);
// the authenticate should always return a true.
if (debug) {
if (_logger.isLoggable(FINE)) {
_logger.log(FINE, "\t\t[ClientCertificateLoginModule] authentication succeeded");
}
}
succeeded = true;
return true;
} catch (java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException("Error: " + uce.getCallback().toString() + " not available to garner authentication information " + "from the user");
} catch (Exception e) {
throw new LoginException(e.toString());
}
}
/**
* <p>
* This method is called if the LoginContext's overall authentication succeeded (the relevant REQUIRED, REQUISITE,
* SUFFICIENT and OPTIONAL LoginModules succeeded).
*
* <p>
* If this LoginModule's own authentication attempt succeeded (checked by retrieving the private state saved by the
* <code>login</code> method), then this method associates a <code>UserPrincipal</code> with the <code>Subject</code>
* located in the <code>LoginModule</code>. If this LoginModule's own authentication attempted failed, then this method
* removes any state that was originally saved.
*
* @throws LoginException if the commit fails.
* @return true if this LoginModule's own login and commit attempts succeeded, or false otherwise.
*/
@Override
public boolean commit() throws LoginException {
if (succeeded == false) {
return false;
}
userPrincipal = new UserNameAndPassword(alias);
if (!subject.getPrincipals().contains(userPrincipal)) {
subject.getPrincipals().add(userPrincipal);
}
if (debug) {
_logger.log(FINE, "\t\t[ClientCertificateLoginModule] added UserPrincipal to Subject");
}
ssl = new AppClientSSL();
ssl.setCertNickname(this.alias);
sslUtils.setAppclientSsl(ssl);
String realm = LoginContextDriver.CERT_REALMNAME;
X509Certificate[] certChain = new X509Certificate[1];
certChain[0] = certificate;
X509CertificateCredential pc = new X509CertificateCredential(certChain, alias, realm);
if (!subject.getPrivateCredentials().contains(pc)) {
subject.getPrivateCredentials().add(pc);
}
commitSucceeded = true;
return true;
}
/**
* <p>
* This method is called if the LoginContext's overall authentication failed. (the relevant REQUIRED, REQUISITE,
* SUFFICIENT and OPTIONAL LoginModules did not succeed).
* <p>
* If this LoginModule's own authentication attempt succeeded (checked by retrieving the private state saved by the
* <code>login</code> and <code>commit</code> methods), then this method cleans up any state that was originally saved.
*
* @throws LoginException if the abort fails.
* @return false if this LoginModule's own login and/or commit attempts failed, and true otherwise.
*/
@Override
public boolean abort() throws LoginException {
if (succeeded == false) {
return false;
}
if (succeeded == true && commitSucceeded == false) {
// login succeeded but overall authentication failed
succeeded = false;
alias = null;
userPrincipal = null;
} else {
// overall authentication succeeded and commit succeeded,
// but someone else's commit failed
logout();
}
return true;
}
/**
* Logout the user.
* <p>
* This method removes the <code>UserPrincipal</code> that was added by the <code>commit</code> method.
* <p>
*
* @throws LoginException if the logout fails.
* @return true in all cases since this <code>LoginModule</code> should not be ignored.
*/
@Override
public boolean logout() throws LoginException {
// unset the alias
ssl = null;
sslUtils.setAppclientSsl(ssl);
subject.getPrincipals().remove(userPrincipal);
succeeded = false;
commitSucceeded = false;
alias = null;
userPrincipal = null;
return true;
}
}