/
SASLAuthentication.java
874 lines (814 loc) · 39.7 KB
/
SASLAuthentication.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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.net;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.security.KeyStoreException;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.IncomingServerSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalIncomingServerSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SASLAuthentication is responsible for returning the available SASL mechanisms to use and for
* actually performing the SASL authentication.<p>
*
* The list of available SASL mechanisms is determined by:
* <ol>
* <li>The type of {@link org.jivesoftware.openfire.user.UserProvider} being used since
* some SASL mechanisms require the server to be able to retrieve user passwords</li>
* <li>Whether anonymous logins are enabled or not.</li>
* <li>Whether shared secret authentication is enabled or not.</li>
* <li>Whether the underlying connection has been secured or not.</li>
* </ol>
*
* @author Hao Chen
* @author Gaston Dombiak
*/
public class SASLAuthentication {
private static final Logger Log = LoggerFactory.getLogger(SASLAuthentication.class);
// http://stackoverflow.com/questions/8571501/how-to-check-whether-the-string-is-base64-encoded-or-not
// plus an extra regex alternative to catch a single equals sign ('=', see RFC 6120 6.4.2)
private static final Pattern BASE64_ENCODED = Pattern.compile("^(=|([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$");
/**
* The utf-8 charset for decoding and encoding Jabber packet streams.
*/
protected static String CHARSET = "UTF-8";
private static final String SASL_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"";
private static Map<String, ElementType> typeMap = new TreeMap<String, ElementType>();
private static Set<String> mechanisms = null;
static {
initMechanisms();
}
public enum ElementType {
ABORT("abort"), AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");
private String name = null;
@Override
public String toString() {
return name;
}
private ElementType(String name) {
this.name = name;
typeMap.put(this.name, this);
}
public static ElementType valueof(String name) {
if (name == null) {
return UNDEF;
}
ElementType e = typeMap.get(name);
return e != null ? e : UNDEF;
}
}
private enum Failure {
ABORTED("aborted"),
ACCOUNT_DISABLED("account-disabled"),
CREDENTIALS_EXPIRED("credentials-expired"),
ENCRYPTION_REQUIRED("encryption-required"),
INCORRECT_ENCODING("incorrect-encoding"),
INVALID_AUTHZID("invalid-authzid"),
INVALID_MECHANISM("invalid-mechanism"),
MALFORMED_REQUEST("malformed-request"),
MECHANISM_TOO_WEAK("mechanism-too-weak"),
NOT_AUTHORIZED("not-authorized"),
TEMPORARY_AUTH_FAILURE("temporary-auth-failure");
private String name = null;
private Failure(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
public enum Status {
/**
* Entity needs to respond last challenge. Session is still negotiating
* SASL authentication.
*/
needResponse,
/**
* SASL negotiation has failed. The entity may retry a few times before the connection
* is closed.
*/
failed,
/**
* SASL negotiation has been successful.
*/
authenticated;
}
/**
* Returns a string with the valid SASL mechanisms available for the specified session. If
* the session's connection is not secured then only include the SASL mechanisms that don't
* require TLS.
*
* @param session The current session
*
* @return a string with the valid SASL mechanisms available for the specified session.
*/
public static String getSASLMechanisms(LocalSession session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return "";
}
Element mechs = getSASLMechanismsElement(session);
return mechs.asXML();
}
public static Element getSASLMechanismsElement(Session session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return null;
}
Element mechs = DocumentHelper.createElement(new QName("mechanisms",
new Namespace("", "urn:ietf:params:xml:ns:xmpp-sasl")));
if (session instanceof LocalIncomingServerSession) {
// Server connections don't follow the same rules as clients
if (session.isSecure()) {
boolean haveTrustedCertificate = false;
try {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
X509Certificate trusted = CertificateManager.getEndEntityCertificate(svr.getConnection().getPeerCertificates(), SSLConfig.getKeyStore(), SSLConfig.gets2sTrustStore());
haveTrustedCertificate = trusted != null;
if (trusted != null && svr.getDefaultIdentity() != null) {
haveTrustedCertificate = verifyCertificate(trusted, svr.getDefaultIdentity());
}
} catch (IOException ex) {
Log.warn("Exception occurred while trying to determine whether remote certificate is trusted. Treating as untrusted.", ex);
}
if (haveTrustedCertificate) {
// Offer SASL EXTERNAL only if TLS has already been negotiated and the peer has a trusted cert.
Element mechanism = mechs.addElement("mechanism");
mechanism.setText("EXTERNAL");
}
}
}
else {
for (String mech : getSupportedMechanisms()) {
Element mechanism = mechs.addElement("mechanism");
mechanism.setText(mech);
}
}
return mechs;
}
/**
* Handles the SASL authentication packet. The entity may be sending an initial
* authentication request or a response to a challenge made by the server. The returned
* value indicates whether the authentication has finished either successfully or not or
* if the entity is expected to send a response to a challenge.
*
* @param session the session that is authenticating with the server.
* @param doc the stanza sent by the authenticating entity.
* @return value that indicates whether the authentication has finished either successfully
* or not or if the entity is expected to send a response to a challenge.
* @throws UnsupportedEncodingException If UTF-8 charset is not supported.
*/
public static Status handle(LocalSession session, Element doc) throws UnsupportedEncodingException {
Status status;
String mechanism;
if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
ElementType type = ElementType.valueof(doc.getName());
switch (type) {
case ABORT:
authenticationFailed(session, Failure.ABORTED);
status = Status.failed;
break;
case AUTH:
mechanism = doc.attributeValue("mechanism");
// http://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism
// The initiating entity did not specify a mechanism
if (mechanism == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
break;
}
// Store the requested SASL mechanism by the client
session.setSessionData("SaslMechanism", mechanism);
//Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
if (mechanism.equalsIgnoreCase("ANONYMOUS") &&
mechanisms.contains("ANONYMOUS")) {
status = doAnonymousAuthentication(session);
}
else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
// The selected SASL mechanism requires the server to send a challenge
// to the client
try {
Map<String, String> props = new TreeMap<String, String>();
props.put(Sasl.QOP, "auth");
if (mechanism.equals("GSSAPI")) {
props.put(Sasl.SERVER_AUTH, "TRUE");
}
SaslServer ss = Sasl.createSaslServer(mechanism, "xmpp",
JiveGlobals.getProperty("xmpp.fqdn", session.getServerName()), props,
new XMPPCallbackHandler());
if (ss == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
return Status.failed;
}
// evaluateResponse doesn't like null parameter
byte[] token = new byte[0];
String value = doc.getTextTrim();
if (value.length() > 0) {
if (!BASE64_ENCODED.matcher(value).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
// If auth request includes a value then validate it
token = StringUtils.decodeBase64(value);
if (token == null) {
token = new byte[0];
}
}
if (mechanism.equals("DIGEST-MD5")) {
// RFC2831 (DIGEST-MD5) says the client MAY provide an initial response on subsequent
// authentication. Java SASL does not (currently) support this and thows an exception
// if we try. This violates the RFC, so we just strip any initial token.
token = new byte[0];
}
byte[] challenge = ss.evaluateResponse(token);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
session.setSessionData("SaslServer", ss);
}
catch (SaslException e) {
Log.info("User Login Failed. " + e.getMessage());
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn("Client wants to do a MECH we don't support: '" +
mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
case RESPONSE:
// Store the requested SASL mechanism by the client
mechanism = (String) session.getSessionData("SaslMechanism");
if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanism.equalsIgnoreCase("JIVE-SHAREDSECRET")) {
status = doSharedSecretAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
SaslServer ss = (SaslServer) session.getSessionData("SaslServer");
if (ss != null) {
boolean ssComplete = ss.isComplete();
String response = doc.getTextTrim();
if (!BASE64_ENCODED.matcher(response).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
try {
if (ssComplete) {
authenticationSuccessful(session, ss.getAuthorizationID(),
null);
status = Status.authenticated;
}
else {
byte[] data = StringUtils.decodeBase64(response);
if (data == null) {
data = new byte[0];
}
byte[] challenge = ss.evaluateResponse(data);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
}
}
catch (SaslException e) {
Log.debug("SASLAuthentication: SaslException", e);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.error("SaslServer is null, should be valid object instead.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn(
"Client responded to a MECH we don't support: '" + mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
default:
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
// Ignore
break;
}
}
else {
Log.debug("SASLAuthentication: Unknown namespace sent in auth element: " + doc.asXML());
authenticationFailed(session, Failure.MALFORMED_REQUEST);
status = Status.failed;
}
// Check if SASL authentication has finished so we can clean up temp information
if (status == Status.failed || status == Status.authenticated) {
// Remove the SaslServer from the Session
session.removeSessionData("SaslServer");
// Remove the requested SASL mechanism by the client
session.removeSessionData("SaslMechanism");
}
return status;
}
/**
* Returns true if shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @return true if shared secret authentication is enabled.
*/
public static boolean isSharedSecretAllowed() {
return JiveGlobals.getBooleanProperty("xmpp.auth.sharedSecretEnabled");
}
/**
* Sets whether shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @param sharedSecretAllowed true if shared secret authentication should be enabled.
*/
public static void setSharedSecretAllowed(boolean sharedSecretAllowed) {
JiveGlobals.setProperty("xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false");
}
/**
* Returns the shared secret value, or <tt>null</tt> if shared secret authentication is
* disabled. If this is the first time the shared secret value has been requested (and
* shared secret auth is enabled), the key will be randomly generated and stored in the
* property <tt>xmpp.auth.sharedSecret</tt>.
*
* @return the shared secret value.
*/
public static String getSharedSecret() {
if (!isSharedSecretAllowed()) {
return null;
}
String sharedSecret = JiveGlobals.getProperty("xmpp.auth.sharedSecret");
if (sharedSecret == null) {
sharedSecret = StringUtils.randomString(8);
JiveGlobals.setProperty("xmpp.auth.sharedSecret", sharedSecret);
}
return sharedSecret;
}
/**
* Returns true if the supplied digest matches the shared secret value. The digest
* must be an MD5 hash of the secret key, encoded as hex. This value is supplied
* by clients attempting shared secret authentication.
*
* @param digest the MD5 hash of the secret key, encoded as hex.
* @return true if authentication succeeds.
*/
public static boolean authenticateSharedSecret(String digest) {
if (!isSharedSecretAllowed()) {
return false;
}
String sharedSecert = getSharedSecret();
return StringUtils.hash(sharedSecert).equals(digest);
}
private static Status doAnonymousAuthentication(LocalSession session) {
if (XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
// Verify that client can connect from his IP address
boolean forbidAccess = false;
try {
String hostAddress = session.getConnection().getHostAddress();
if (!LocalClientSession.getAllowedAnonymIPs().isEmpty() &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(hostAddress)) {
byte[] address = session.getConnection().getAddress();
String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." +
(address[2] & 0xff) +
".*";
String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
String range3 = (address[0] & 0xff) + ".*.*.*";
if (!LocalClientSession.getAllowedAnonymIPs().containsKey(range1) &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(range2) &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(range3)) {
forbidAccess = true;
}
}
} catch (UnknownHostException e) {
forbidAccess = true;
}
if (forbidAccess) {
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Just accept the authentication :)
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
else {
// anonymous login is disabled so close the connection
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
}
private static Status doExternalAuthentication(LocalSession session, Element doc)
throws UnsupportedEncodingException {
// At this point the connection has already been secured using TLS
if (session instanceof IncomingServerSession) {
String hostname = doc.getTextTrim();
if (hostname == null || hostname.length() == 0) {
// No hostname was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
hostname = new String(StringUtils.decodeBase64(hostname), CHARSET);
if (hostname.length() == 0) {
hostname = null;
}
try {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
String defHostname = svr.getDefaultIdentity();
if (hostname == null) {
hostname = defHostname;
} else if (!hostname.equals(defHostname)) {
// Mismatch; really odd.
Log.info("SASLAuthentication rejected from='{}' and authzid='{}'", hostname, defHostname);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
} catch(Exception e) {
// Erm. Nothing?
}
if (hostname == null) {
Log.info("No authzid supplied for anonymous session.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Check if certificate validation is disabled for s2s
// Flag that indicates if certificates of the remote server should be validated.
// Disabling certificate validation is not recommended for production environments.
boolean verify =
JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true);
if (!verify) {
authenticationSuccessful(session, hostname, null);
return Status.authenticated;
} else if(verifyCertificates(session.getConnection().getPeerCertificates(), hostname)) {
authenticationSuccessful(session, hostname, null);
LocalIncomingServerSession s = (LocalIncomingServerSession)session;
if (s != null) {
s.tlsAuth();
}
return Status.authenticated;
}
}
else if (session instanceof LocalClientSession) {
// Client EXTERNALL login
Log.debug("SASLAuthentication: EXTERNAL authentication via SSL certs for c2s connection");
// This may be null, we will deal with that later
String username = new String(StringUtils.decodeBase64(doc.getTextTrim()), CHARSET);
String principal = "";
ArrayList<String> principals = new ArrayList<String>();
Connection connection = session.getConnection();
if (connection.getPeerCertificates().length < 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but no certificates found.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
X509Certificate trusted;
try {
trusted = CertificateManager.getEndEntityCertificate(connection.getPeerCertificates(), SSLConfig.getKeyStore(), SSLConfig.gets2sTrustStore());
} catch (IOException e) {
trusted = null;
}
if (trusted == null) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but EE cert untrusted.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
principals.addAll(CertificateManager.getPeerIdentities((X509Certificate)trusted));
if(principals.size() == 1) {
principal = principals.get(0);
} else if(principals.size() > 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication: more than one principal found, using first.");
principal = principals.get(0);
} else {
Log.debug("SASLAuthentication: EXTERNAL authentication: No principals found.");
}
if (username == null || username.length() == 0) {
// No username was provided, according to XEP-0178 we need to:
// * attempt to get it from the cert first
// * have the server assign one
// There shouldn't be more than a few principals in here. One ideally
// We set principal to the first one in the list to have a sane default
// If this list is empty, then the cert had no identity at all, which
// will cause an authorization failure
for(String princ : principals) {
String u = AuthorizationManager.map(princ);
if(!u.equals(princ)) {
username = u;
principal = princ;
break;
}
}
if (username == null || username.length() == 0) {
// Still no username. Punt.
username = principal;
}
Log.debug("SASLAuthentication: no username requested, using "+username);
}
//Its possible that either/both username and principal are null here
//The providers should not allow a null authorization
if (AuthorizationManager.authorize(username,principal)) {
Log.debug("SASLAuthentication: "+principal+" authorized to "+username);
authenticationSuccessful(session, username, null);
return Status.authenticated;
}
} else {
Log.debug("SASLAuthentication: unknown session type. Cannot perform EXTERNAL authentication");
}
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) {
for (String identity : CertificateManager.getPeerIdentities(trustedCert)) {
// Verify that either the identity is the same as the hostname, or for wildcarded
// identities that the hostname ends with .domainspecified or -is- domainspecified.
if ((identity.startsWith("*.")
&& (hostname.endsWith(identity.replace("*.", "."))
|| hostname.equals(identity.replace("*.", ""))))
|| hostname.equals(identity)) {
return true;
}
}
return false;
}
public static boolean verifyCertificates(Certificate[] chain, String hostname) {
try {
X509Certificate trusted = CertificateManager.getEndEntityCertificate(chain, SSLConfig.getKeyStore(), SSLConfig.gets2sTrustStore());
if (trusted != null) {
return verifyCertificate(trusted, hostname);
}
} catch(IOException e) {
Log.warn("Keystore issue while verifying certificate chain: {}", e.getMessage());
}
return false;
}
private static Status doSharedSecretAuthentication(LocalSession session, Element doc)
throws UnsupportedEncodingException
{
String secretDigest;
String response = doc.getTextTrim();
if (response == null || response.length() == 0) {
// No info was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
// Parse data and obtain username & password
String data = new String(StringUtils.decodeBase64(response), CHARSET);
StringTokenizer tokens = new StringTokenizer(data, "\0");
tokens.nextToken();
secretDigest = tokens.nextToken();
if (authenticateSharedSecret(secretDigest)) {
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
// Otherwise, authentication failed.
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
private static void sendChallenge(Session session, byte[] challenge) {
StringBuilder reply = new StringBuilder(250);
if (challenge == null) {
challenge = new byte[0];
}
String challenge_b64 = StringUtils.encodeBase64(challenge).trim();
if ("".equals(challenge_b64)) {
challenge_b64 = "="; // Must be padded if null
}
reply.append(
"<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
reply.append(challenge_b64);
reply.append("</challenge>");
session.deliverRawText(reply.toString());
}
private static void authenticationSuccessful(LocalSession session, String username,
byte[] successData) {
if (username != null && LockOutManager.getInstance().isAccountDisabled(username)) {
// Interception! This person is locked out, fail instead!
LockOutManager.getInstance().recordFailedLogin(username);
authenticationFailed(session, Failure.ACCOUNT_DISABLED);
return;
}
StringBuilder reply = new StringBuilder(80);
reply.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"");
if (successData != null) {
String successData_b64 = StringUtils.encodeBase64(successData).trim();
reply.append(">").append(successData_b64).append("</success>");
}
else {
reply.append("/>");
}
session.deliverRawText(reply.toString());
// We only support SASL for c2s
if (session instanceof ClientSession) {
((LocalClientSession) session).setAuthToken(new AuthToken(username));
}
else if (session instanceof IncomingServerSession) {
String hostname = username;
// Add the validated domain as a valid domain. The remote server can
// now send packets from this address
((LocalIncomingServerSession) session).addValidatedDomain(hostname);
Log.info("Inbound Server {} authenticated (via TLS)", username);
}
}
private static void authenticationFailed(LocalSession session, Failure failure) {
StringBuilder reply = new StringBuilder(80);
reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><");
reply.append(failure.toString());
reply.append("/></failure>");
session.deliverRawText(reply.toString());
// Give a number of retries before closing the connection
Integer retries = (Integer) session.getSessionData("authRetries");
if (retries == null) {
retries = 1;
}
else {
retries = retries + 1;
}
session.setSessionData("authRetries", retries);
if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) {
// Close the connection
session.close();
}
}
/**
* Adds a new SASL mechanism to the list of supported SASL mechanisms by the server. The
* new mechanism will be offered to clients and connection managers as stream features.<p>
*
* Note: this method simply registers the SASL mechanism to be advertised as a supported
* mechanism by Openfire. Actual SASL handling is done by Java itself, so you must add
* the provider to Java.
*
* @param mechanism the new SASL mechanism.
*/
public static void addSupportedMechanism(String mechanism) {
mechanisms.add(mechanism);
}
/**
* Removes a SASL mechanism from the list of supported SASL mechanisms by the server.
*
* @param mechanism the SASL mechanism to remove.
*/
public static void removeSupportedMechanism(String mechanism) {
mechanisms.remove(mechanism);
}
/**
* Returns the list of supported SASL mechanisms by the server. Note that Java may have
* support for more mechanisms but some of them may not be returned since a special setup
* is required that might be missing. Use {@link #addSupportedMechanism(String)} to add
* new SASL mechanisms.
*
* @return the list of supported SASL mechanisms by the server.
*/
public static Set<String> getSupportedMechanisms() {
Set<String> answer = new HashSet<String>(mechanisms);
// Clean up not-available mechanisms
for (Iterator<String> it=answer.iterator(); it.hasNext();) {
String mech = it.next();
if (mech.equals("CRAM-MD5") || mech.equals("DIGEST-MD5")) {
// Check if the user provider in use supports passwords retrieval. Accessing
// to the users passwords will be required by the CallbackHandler
if (!AuthFactory.supportsPasswordRetrieval()) {
it.remove();
}
}
else if (mech.equals("ANONYMOUS")) {
// Check anonymous is supported
if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
it.remove();
}
}
else if (mech.equals("JIVE-SHAREDSECRET")) {
// Check shared secret is supported
if (!isSharedSecretAllowed()) {
it.remove();
}
}
}
return answer;
}
private static void initMechanisms() {
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty("sasl.mechs");
JiveGlobals.migrateProperty("sasl.gssapi.debug");
JiveGlobals.migrateProperty("sasl.gssapi.config");
JiveGlobals.migrateProperty("sasl.gssapi.useSubjectCredsOnly");
mechanisms = new HashSet<String>();
String available = JiveGlobals.getProperty("sasl.mechs");
if (available == null) {
mechanisms.add("ANONYMOUS");
mechanisms.add("PLAIN");
mechanisms.add("DIGEST-MD5");
mechanisms.add("CRAM-MD5");
mechanisms.add("JIVE-SHAREDSECRET");
}
else {
StringTokenizer st = new StringTokenizer(available, " ,\t\n\r\f");
while (st.hasMoreTokens()) {
String mech = st.nextToken().toUpperCase();
// Check that the mech is a supported mechansim. Maybe we shouldnt check this and allow any?
if (mech.equals("ANONYMOUS") ||
mech.equals("PLAIN") ||
mech.equals("DIGEST-MD5") ||
mech.equals("CRAM-MD5") ||
mech.equals("GSSAPI") ||
mech.equals("EXTERNAL") ||
mech.equals("JIVE-SHAREDSECRET"))
{
Log.debug("SASLAuthentication: Added " + mech + " to mech list");
mechanisms.add(mech);
}
}
if (mechanisms.contains("GSSAPI")) {
if (JiveGlobals.getProperty("sasl.gssapi.config") != null) {
System.setProperty("java.security.krb5.debug",
JiveGlobals.getProperty("sasl.gssapi.debug", "false"));
System.setProperty("java.security.auth.login.config",
JiveGlobals.getProperty("sasl.gssapi.config"));
System.setProperty("javax.security.auth.useSubjectCredsOnly",
JiveGlobals.getProperty("sasl.gssapi.useSubjectCredsOnly", "false"));
}
else {
//Not configured, remove the option.
Log.debug("SASLAuthentication: Removed GSSAPI from mech list");
mechanisms.remove("GSSAPI");
}
}
}
//Add our providers to the Security class
Security.addProvider(new org.jivesoftware.openfire.sasl.SaslProvider());
}
}