001: /*
002: * JOSSO: Java Open Single Sign-On
003: *
004: * Copyright 2004-2008, Atricore, Inc.
005: *
006: * This is free software; you can redistribute it and/or modify it
007: * under the terms of the GNU Lesser General Public License as
008: * published by the Free Software Foundation; either version 2.1 of
009: * the License, or (at your option) any later version.
010: *
011: * This software is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * You should have received a copy of the GNU Lesser General Public
017: * License along with this software; if not, write to the Free
018: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
019: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
020: */
021: package org.josso.auth.scheme;
022:
023: import org.apache.commons.codec.binary.Base64;
024: import org.apache.commons.logging.Log;
025: import org.apache.commons.logging.LogFactory;
026: import org.josso.auth.Credential;
027: import org.josso.auth.SimplePrincipal;
028: import org.josso.auth.CredentialProvider;
029: import org.josso.auth.exceptions.SSOAuthenticationException;
030: import org.josso.auth.util.Crypt;
031:
032: import java.io.UnsupportedEncodingException;
033: import java.security.MessageDigest;
034: import java.security.NoSuchAlgorithmException;
035: import java.security.Principal;
036:
037: /**
038: * Basic authentication scheme, supporting username and password credentials.
039: * <p/>
040: * Configuration properties supported by this authenticator are :
041: * <ul>
042: * <li>hashAlgorithm: The message digest algorithm to be used when hashing passwords.
043: * If not specified, no hashing is used.
044: * This must be an algorithm supported by the java.security.MessageDigest class on your platform.
045: * For J2SE 1.4.2 you can check :
046: * <a href="http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB">Java Cryptography Architecture API Specification & Reference - Apendix B : Algorithms</a>
047: * </li>
048: * <li>hashEncoding: The econding used to store hashed passwords.
049: * Supported values are HEX, BASE64.</li>
050: * <li>ignorePasswordCase: If true, password case will be igonred. This property is ignored if a hashAlgorithm was specified.
051: * Default to false.</li>
052: * <li>ignoreUserCase: If ture, username case will be ignored.</li>
053: * <li>credential-store: The credential store configured for this authenticator.
054: * Check specific stores for specific configuraiton options</li>
055: * <li>credential-store-key-adapter: The credential store key adapter configured for this authenticator.
056: * Check specific stores for specific configuraiton options</li>
057: * </ul>
058: * </p>
059: * <p/>
060: * Sample authenticator configuration for basic authentication (username/password) :
061: * </p>
062: * <pre>
063: * <authentication-scheme>
064: * <p/>
065: * <class>org.josso.auth.scheme.UsernamePasswordAuthScheme</class>
066: * <hashAlgorithm>MD5</hashAlgorithm>
067: * <hashEncoding>HEX</hashEncoding>
068: * <ignorePasswordCase>false</ignorePasswordCase>
069: * <ignoreUserCase>false</ignoreUserCase>
070: * <p/>
071: * <!-- Configure the propper store here -->
072: * <credential-store>
073: * ...
074: * </credential-store>
075: * <p/>
076: * <credential-store-key-adapter>
077: * ...
078: * </credential-store-key-adapter>
079: * <p/>
080: * </authentication-scheme>
081: * <p/>
082: * </pre>
083: *
084: * @author <a href="mailto:sgonzalez@josso.org">Sebastian Gonzalez Oyuela</a>
085: * @version $Id: UsernamePasswordAuthScheme.java 508 2008-02-18 13:32:29Z sgonzalez $
086: * @see org.josso.auth.CredentialStore
087: * @see org.josso.gateway.identity.service.store.AbstractStore
088: * @see UsernamePasswordCredentialProvider
089: */
090:
091: public class UsernamePasswordAuthScheme extends
092: AbstractAuthenticationScheme {
093:
094: private static final Log logger = LogFactory
095: .getLog(UsernamePasswordAuthScheme.class);
096:
097: private String _hashAlgorithm;
098:
099: private String _hashEncoding;
100:
101: private String _hashCharset;
102:
103: private boolean _ignorePasswordCase;
104:
105: private boolean _ignoreUserCase;
106:
107: private String _name;
108:
109: // Some spetial configuration attributes,
110:
111: /**
112: * This attribute is only used when CRYPT hasing is configued. The default value is 2.
113: */
114: private int _saltLenght = 2;
115:
116: /**
117: * The username recieved as UsernameCredential instance, if any.
118: *
119: *
120: */
121: public Principal getPrincipal() {
122: return new SimplePrincipal(getUsername(_inputCredentials));
123: }
124:
125: /**
126: * The username recieved as UsernameCredential instance, if any.
127: */
128: public Principal getPrincipal(Credential[] credentials) {
129: return new SimplePrincipal(getUsername(credentials));
130: }
131:
132: /**
133: * Authenticates the user using recieved credentials to proof his identity.
134: *
135: * @return the Principal if credentials are valid, null otherwise.
136: */
137: public boolean authenticate() throws SSOAuthenticationException {
138:
139: setAuthenticated(false);
140:
141: String username = getUsername(_inputCredentials);
142: String password = getPassword(_inputCredentials);
143:
144: // Check if all credentials are present.
145: if (username == null || username.length() == 0
146: || password == null || password.length() == 0) {
147:
148: if (logger.isDebugEnabled()) {
149: logger
150: .debug("Username "
151: + (username == null
152: || username.length() == 0 ? " not"
153: : "")
154: + " provided. "
155: + "Password "
156: + (password == null
157: || password.length() == 0 ? " not"
158: : "") + " provided.");
159: }
160:
161: // We don't support empty values !
162: return false;
163: }
164:
165: String knownUsername = getUsername(getKnownCredentials());
166: String expectedPassword = getPassword(getKnownCredentials());
167:
168: // We might have to hash the password.
169: password = createPasswordHash(password);
170:
171: // Validate user identity ...
172: if (!validateUsername(username, knownUsername)
173: || !validatePassword(password, expectedPassword)) {
174: return false;
175: }
176:
177: if (logger.isDebugEnabled())
178: logger.debug("[authenticate()], Principal authenticated : "
179: + username);
180:
181: // We have successfully authenticated this user.
182: setAuthenticated(true);
183: return true;
184: }
185:
186: /**
187: * Only one password credential supported.
188: *
189: *
190: */
191: public Credential[] getPrivateCredentials() {
192:
193: Credential c = getPasswordCredential(_inputCredentials);
194: if (c == null)
195: return new Credential[0];
196:
197: Credential[] r = { c };
198: return r;
199:
200: }
201:
202: /**
203: * Only one username credential supported.
204: *
205: *
206: */
207: public Credential[] getPublicCredentials() {
208: Credential c = getUsernameCredential(_inputCredentials);
209: if (c == null)
210: return new Credential[0];
211:
212: Credential[] r = { c };
213: return r;
214: }
215:
216: // --------------------------------------------------------------------
217: // Protected utils
218: // --------------------------------------------------------------------
219:
220: /**
221: * This method validates the input password agaist the expected password.
222: *
223: * @param inputPassword
224: * @param expectedPassword
225: *
226: */
227: protected boolean validatePassword(String inputPassword,
228: String expectedPassword) {
229:
230: if (logger.isDebugEnabled())
231: logger.debug("Validating passwords [" + inputPassword + "/"
232: + expectedPassword + "]");
233:
234: // Validate input and expected passwords.
235: if (inputPassword == null && expectedPassword == null)
236: return false;
237:
238: if (_ignorePasswordCase && _hashAlgorithm == null)
239: return inputPassword.equalsIgnoreCase(expectedPassword);
240: else
241: return inputPassword.equals(expectedPassword);
242: }
243:
244: /**
245: * This method validates the input password agaist the expected password.
246: *
247: * @param inputUsername
248: * @param expectedUsername
249: *
250: */
251: protected boolean validateUsername(String inputUsername,
252: String expectedUsername) {
253:
254: if (logger.isDebugEnabled())
255: logger.debug("Validating usernames [" + inputUsername + "/"
256: + expectedUsername + "]");
257:
258: if (inputUsername == null && expectedUsername == null)
259: return false;
260:
261: if (_ignoreUserCase)
262: return inputUsername.equalsIgnoreCase(expectedUsername);
263: else
264: return inputUsername.equals(expectedUsername);
265: }
266:
267: /**
268: * This method allows password hashing.
269: * In order to work, you need to specify hashAlgorithm and hashEncoding properties.
270: * You can optionally set hashCharset property.
271: *
272: * @return the hashed password.
273: */
274: protected String createPasswordHash(String password)
275: throws SSOAuthenticationException {
276:
277: // If none of this properties are set, do nothing ...
278: if (getHashAlgorithm() == null && getHashEncoding() == null) {
279: // Nothing to do ...
280: return password;
281: }
282:
283: if (logger.isDebugEnabled())
284: logger.debug("Creating password hash for [" + password
285: + "] with algorithm/encoding ["
286: + getHashAlgorithm() + "/" + getHashEncoding()
287: + "]");
288:
289: // Check for spetial encryption mechanisms, not supported by the JDK
290: if ("CRYPT".equalsIgnoreCase(getHashAlgorithm())) {
291: // Get known password
292: String knownPassword = getPassword(getKnownCredentials());
293: String salt = knownPassword != null
294: && knownPassword.length() > 1 ? knownPassword
295: .substring(0, _saltLenght) : "";
296:
297: return Crypt.crypt(salt, password);
298:
299: }
300:
301: byte[] passBytes;
302: String passwordHash = null;
303:
304: // convert password to byte data
305: try {
306: if (_hashCharset == null)
307: passBytes = password.getBytes();
308: else
309: passBytes = password.getBytes(_hashCharset);
310: } catch (UnsupportedEncodingException e) {
311: logger.error("charset " + _hashCharset
312: + " not found. Using platform default.");
313: passBytes = password.getBytes();
314: }
315:
316: // calculate the hash and apply the encoding.
317: try {
318:
319: byte[] hash;
320: // Hash algorithm is optional
321: if (_hashAlgorithm != null)
322: hash = getDigest().digest(passBytes);
323: else
324: hash = passBytes;
325:
326: // At this point, hashEncoding is required.
327: if ("BASE64".equalsIgnoreCase(_hashEncoding)) {
328: passwordHash = encodeBase64(hash);
329:
330: } else if ("HEX".equalsIgnoreCase(_hashEncoding)) {
331: passwordHash = encodeBase16(hash);
332:
333: } else if (_hashEncoding == null) {
334: logger
335: .error("You must specify a hashEncoding when using hashAlgorithm");
336:
337: } else {
338: logger.error("Unsupported hash encoding format "
339: + _hashEncoding);
340:
341: }
342:
343: } catch (Exception e) {
344: logger.error("Password hash calculation failed : \n"
345: + e.getMessage() != null ? e.getMessage() : e
346: .toString(), e);
347: }
348:
349: return passwordHash;
350:
351: }
352:
353: /**
354: * Base64 encoding. Charset ISO-8859-1 is assumed.
355: */
356: protected String encodeBase64(byte[] bytes)
357: throws UnsupportedEncodingException {
358: byte[] enc = Base64.encodeBase64(bytes);
359: return new String(enc, "ISO-8859-1");
360: }
361:
362: /**
363: * Base16 encoding (HEX).
364: */
365: protected String encodeBase16(byte[] bytes) {
366: StringBuffer sb = new StringBuffer(bytes.length * 2);
367: for (int i = 0; i < bytes.length; i++) {
368: byte b = bytes[i];
369: // top 4 bits
370: char c = (char) ((b >> 4) & 0xf);
371: if (c > 9)
372: c = (char) ((c - 10) + 'a');
373: else
374: c = (char) (c + '0');
375: sb.append(c);
376: // bottom 4 bits
377: c = (char) (b & 0xf);
378: if (c > 9)
379: c = (char) ((c - 10) + 'a');
380: else
381: c = (char) (c + '0');
382: sb.append(c);
383: }
384: return sb.toString();
385: }
386:
387: /**
388: * Only invoke this if algorithm is set.
389: *
390: *
391: * @throws SSOAuthenticationException
392: */
393: protected MessageDigest getDigest()
394: throws SSOAuthenticationException {
395:
396: MessageDigest _digest = null;
397: if (_hashAlgorithm != null) {
398:
399: try {
400: _digest = MessageDigest.getInstance(_hashAlgorithm);
401: logger.debug("Using hash algorithm/encoding : "
402: + _hashAlgorithm + "/" + _hashEncoding);
403: } catch (NoSuchAlgorithmException e) {
404: logger.error("Algorithm not supported : "
405: + _hashAlgorithm, e);
406: throw new SSOAuthenticationException(e.getMessage(), e);
407: }
408: }
409:
410: return _digest;
411:
412: }
413:
414: /**
415: * Gets the username from the received credentials.
416: *
417: * @param credentials
418: *
419: */
420: protected String getUsername(Credential[] credentials) {
421: UsernameCredential c = getUsernameCredential(credentials);
422: if (c == null)
423: return null;
424:
425: return (String) c.getValue();
426: }
427:
428: /**
429: * Gets the password from the recevied credentials.
430: *
431: * @param credentials
432: *
433: */
434: protected String getPassword(Credential[] credentials) {
435: PasswordCredential p = getPasswordCredential(credentials);
436: if (p == null)
437: return null;
438: return (String) p.getValue();
439: }
440:
441: /**
442: * Gets the credential that represents a password.
443: *
444: * @param credentials
445: *
446: */
447: protected PasswordCredential getPasswordCredential(
448: Credential[] credentials) {
449: for (int i = 0; i < credentials.length; i++) {
450: if (credentials[i] instanceof PasswordCredential) {
451: return (PasswordCredential) credentials[i];
452: }
453: }
454: return null;
455: }
456:
457: /**
458: * Gets the credential that represents a Username.
459: *
460: *
461: */
462: protected UsernameCredential getUsernameCredential(
463: Credential[] credentials) {
464:
465: for (int i = 0; i < credentials.length; i++) {
466: if (credentials[i] instanceof UsernameCredential) {
467: return (UsernameCredential) credentials[i];
468: }
469: }
470: return null;
471: }
472:
473: protected CredentialProvider doMakeCredentialProvider() {
474: return new UsernamePasswordCredentialProvider();
475: }
476:
477: public String getHashAlgorithm() {
478: return _hashAlgorithm;
479: }
480:
481: public void setHashAlgorithm(String hashAlgorithm) {
482: if (hashAlgorithm != null && hashAlgorithm.equals(""))
483: hashAlgorithm = null;
484: _hashAlgorithm = hashAlgorithm;
485: }
486:
487: /**
488: * Getter for the encoding used for password hashing.
489: * Supported values : HEX, BASE64
490: */
491: public String getHashEncoding() {
492: return _hashEncoding;
493: }
494:
495: /**
496: * Setter for the encoding used for password hashing.
497: * Supported values : HEX, BASE64
498: */
499: public void setHashEncoding(String hashEnconding) {
500: if (hashEnconding != null && hashEnconding.equals(""))
501: hashEnconding = null;
502: _hashEncoding = hashEnconding;
503: }
504:
505: public String getHashCharset() {
506: return _hashCharset;
507: }
508:
509: public void setHashCharset(String hashCharset) {
510: _hashCharset = hashCharset;
511: }
512:
513: public void setSaltLenght(String saltLenght) {
514: setSaltLength(Integer.valueOf(saltLenght).intValue());
515: }
516:
517: /**
518: * Only used when CRYPT is configured, default value is 2.
519: */
520: public int getSaltLength() {
521: return _saltLenght;
522: }
523:
524: public void setSaltLength(int sl) {
525: _saltLenght = sl;
526: }
527:
528: /**
529: * Values : true , false,
530: */
531: public void setIgnorePasswordCase(String ignorePasswordCase) {
532: _ignorePasswordCase = Boolean.valueOf(ignorePasswordCase)
533: .booleanValue();
534: }
535:
536: /**
537: * Values : true , false,
538: */
539: public void setIgnoreUserCase(String ignoreUserCase) {
540: _ignoreUserCase = Boolean.valueOf(ignoreUserCase)
541: .booleanValue();
542: }
543:
544: public Object clone() {
545:
546: UsernamePasswordAuthScheme s = (UsernamePasswordAuthScheme) super
547: .clone();
548:
549: s.setHashAlgorithm(_hashAlgorithm);
550: s.setHashCharset(_hashCharset);
551: s.setHashEncoding(_hashEncoding);
552: s.setIgnorePasswordCase(_ignorePasswordCase + "");
553: s.setIgnoreUserCase(_ignoreUserCase + "");
554: s.setName(_name);
555:
556: return s;
557: }
558:
559: /**
560: * Sets Authentication Scheme name
561: */
562: public void setName(String name) {
563: _name = name;
564: }
565:
566: /**
567: * Obtains the Authentication Scheme name
568: */
569: public String getName() {
570: return _name;
571: }
572:
573: }
|