001: // ========================================================================
002: // Copyright 2002-2005 Mort Bay Consulting Pty. Ltd.
003: // ------------------------------------------------------------------------
004: // Licensed under the Apache License, Version 2.0 (the "License");
005: // you may not use this file except in compliance with the License.
006: // You may obtain a copy of the License at
007: // http://www.apache.org/licenses/LICENSE-2.0
008: // Unless required by applicable law or agreed to in writing, software
009: // distributed under the License is distributed on an "AS IS" BASIS,
010: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011: // See the License for the specific language governing permissions and
012: // limitations under the License.
013: // ========================================================================
014:
015: package org.mortbay.jetty.security;
016:
017: import java.io.IOException;
018: import java.security.MessageDigest;
019: import java.security.Principal;
020:
021: import javax.servlet.http.HttpServletResponse;
022:
023: import org.mortbay.jetty.HttpHeaders;
024: import org.mortbay.jetty.Request;
025: import org.mortbay.jetty.Response;
026: import org.mortbay.log.Log;
027: import org.mortbay.util.QuotedStringTokenizer;
028: import org.mortbay.util.StringUtil;
029: import org.mortbay.util.TypeUtil;
030:
031: /* ------------------------------------------------------------ */
032: /** DIGEST authentication.
033: *
034: * @author Greg Wilkins (gregw)
035: */
036: public class DigestAuthenticator implements Authenticator {
037: protected long maxNonceAge = 0;
038: protected long nonceSecret = this .hashCode()
039: ^ System.currentTimeMillis();
040: protected boolean useStale = false;
041:
042: /* ------------------------------------------------------------ */
043: /**
044: * @return UserPrinciple if authenticated or null if not. If
045: * Authentication fails, then the authenticator may have committed
046: * the response as an auth challenge or redirect.
047: * @exception IOException
048: */
049: public Principal authenticate(UserRealm realm,
050: String pathInContext, Request request, Response response)
051: throws IOException {
052: // Get the user if we can
053: boolean stale = false;
054: Principal user = null;
055: String credentials = request
056: .getHeader(HttpHeaders.AUTHORIZATION);
057:
058: if (credentials != null) {
059: if (Log.isDebugEnabled())
060: Log.debug("Credentials: " + credentials);
061: QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(
062: credentials, "=, ", true, false);
063: Digest digest = new Digest(request.getMethod());
064: String last = null;
065: String name = null;
066:
067: loop: while (tokenizer.hasMoreTokens()) {
068: String tok = tokenizer.nextToken();
069: char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
070:
071: switch (c) {
072: case '=':
073: name = last;
074: last = tok;
075: break;
076: case ',':
077: name = null;
078: case ' ':
079: break;
080:
081: default:
082: last = tok;
083: if (name != null) {
084: if ("username".equalsIgnoreCase(name))
085: digest.username = tok;
086: else if ("realm".equalsIgnoreCase(name))
087: digest.realm = tok;
088: else if ("nonce".equalsIgnoreCase(name))
089: digest.nonce = tok;
090: else if ("nc".equalsIgnoreCase(name))
091: digest.nc = tok;
092: else if ("cnonce".equalsIgnoreCase(name))
093: digest.cnonce = tok;
094: else if ("qop".equalsIgnoreCase(name))
095: digest.qop = tok;
096: else if ("uri".equalsIgnoreCase(name))
097: digest.uri = tok;
098: else if ("response".equalsIgnoreCase(name))
099: digest.response = tok;
100: break;
101: }
102: }
103: }
104:
105: int n = checkNonce(digest.nonce, request);
106: if (n > 0)
107: user = realm.authenticate(digest.username, digest,
108: request);
109: else if (n == 0)
110: stale = true;
111:
112: if (user == null)
113: Log.warn("AUTH FAILURE: user " + digest.username);
114: else {
115: request.setAuthType(Constraint.__DIGEST_AUTH);
116: request.setUserPrincipal(user);
117: }
118: }
119:
120: // Challenge if we have no user
121: if (user == null && response != null)
122: sendChallenge(realm, request, response, stale);
123:
124: return user;
125: }
126:
127: /* ------------------------------------------------------------ */
128: public String getAuthMethod() {
129: return Constraint.__DIGEST_AUTH;
130: }
131:
132: /* ------------------------------------------------------------ */
133: public void sendChallenge(UserRealm realm, Request request,
134: Response response, boolean stale) throws IOException {
135: String contextPath = request.getContextPath();
136: response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
137: "Digest realm=\"" + realm.getName() + "\", domain=\""
138: + contextPath + "\", nonce=\""
139: + newNonce(request)
140: + "\", algorithm=MD5, qop=\"auth\""
141: + (useStale ? (" stale=" + stale) : ""));
142:
143: response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
144: }
145:
146: /* ------------------------------------------------------------ */
147: public String newNonce(Request request) {
148: long ts = request.getTimeStamp();
149: long sk = nonceSecret;
150:
151: byte[] nounce = new byte[24];
152: for (int i = 0; i < 8; i++) {
153: nounce[i] = (byte) (ts & 0xff);
154: ts = ts >> 8;
155: nounce[8 + i] = (byte) (sk & 0xff);
156: sk = sk >> 8;
157: }
158:
159: byte[] hash = null;
160: try {
161: MessageDigest md = MessageDigest.getInstance("MD5");
162: md.reset();
163: md.update(nounce, 0, 16);
164: hash = md.digest();
165: } catch (Exception e) {
166: Log.warn(e);
167: }
168:
169: for (int i = 0; i < hash.length; i++) {
170: nounce[8 + i] = hash[i];
171: if (i == 23)
172: break;
173: }
174:
175: return new String(B64Code.encode(nounce));
176: }
177:
178: /**
179: * @param nonce
180: * @param request
181: * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
182: */
183: /* ------------------------------------------------------------ */
184: public int checkNonce(String nonce, Request request) {
185: try {
186: byte[] n = B64Code.decode(nonce.toCharArray());
187: if (n.length != 24)
188: return -1;
189:
190: long ts = 0;
191: long sk = nonceSecret;
192: byte[] n2 = new byte[16];
193: System.arraycopy(n, 0, n2, 0, 8);
194: for (int i = 0; i < 8; i++) {
195: n2[8 + i] = (byte) (sk & 0xff);
196: sk = sk >> 8;
197: ts = (ts << 8) + (0xff & (long) n[7 - i]);
198: }
199:
200: long age = request.getTimeStamp() - ts;
201: if (Log.isDebugEnabled())
202: Log.debug("age=" + age);
203:
204: byte[] hash = null;
205: try {
206: MessageDigest md = MessageDigest.getInstance("MD5");
207: md.reset();
208: md.update(n2, 0, 16);
209: hash = md.digest();
210: } catch (Exception e) {
211: Log.warn(e);
212: }
213:
214: for (int i = 0; i < 16; i++)
215: if (n[i + 8] != hash[i])
216: return -1;
217:
218: if (maxNonceAge > 0 && (age < 0 || age > maxNonceAge))
219: return 0; // stale
220:
221: return 1;
222: } catch (Exception e) {
223: Log.ignore(e);
224: }
225: return -1;
226: }
227:
228: /* ------------------------------------------------------------ */
229: /* ------------------------------------------------------------ */
230: /* ------------------------------------------------------------ */
231: private static class Digest extends Credential {
232: String method = null;
233: String username = null;
234: String realm = null;
235: String nonce = null;
236: String nc = null;
237: String cnonce = null;
238: String qop = null;
239: String uri = null;
240: String response = null;
241:
242: /* ------------------------------------------------------------ */
243: Digest(String m) {
244: method = m;
245: }
246:
247: /* ------------------------------------------------------------ */
248: public boolean check(Object credentials) {
249: String password = (credentials instanceof String) ? (String) credentials
250: : credentials.toString();
251:
252: try {
253: MessageDigest md = MessageDigest.getInstance("MD5");
254: byte[] ha1;
255: if (credentials instanceof Credential.MD5) {
256: // Credentials are already a MD5 digest - assume it's in
257: // form user:realm:password (we have no way to know since
258: // it's a digest, alright?)
259: ha1 = ((Credential.MD5) credentials).getDigest();
260: } else {
261: // calc A1 digest
262: md.update(username
263: .getBytes(StringUtil.__ISO_8859_1));
264: md.update((byte) ':');
265: md.update(realm.getBytes(StringUtil.__ISO_8859_1));
266: md.update((byte) ':');
267: md.update(password
268: .getBytes(StringUtil.__ISO_8859_1));
269: ha1 = md.digest();
270: }
271: // calc A2 digest
272: md.reset();
273: md.update(method.getBytes(StringUtil.__ISO_8859_1));
274: md.update((byte) ':');
275: md.update(uri.getBytes(StringUtil.__ISO_8859_1));
276: byte[] ha2 = md.digest();
277:
278: // calc digest
279: // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <">
280: // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
281:
282: md.update(TypeUtil.toString(ha1, 16).getBytes(
283: StringUtil.__ISO_8859_1));
284: md.update((byte) ':');
285: md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
286: md.update((byte) ':');
287: md.update(nc.getBytes(StringUtil.__ISO_8859_1));
288: md.update((byte) ':');
289: md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
290: md.update((byte) ':');
291: md.update(qop.getBytes(StringUtil.__ISO_8859_1));
292: md.update((byte) ':');
293: md.update(TypeUtil.toString(ha2, 16).getBytes(
294: StringUtil.__ISO_8859_1));
295: byte[] digest = md.digest();
296:
297: // check digest
298: return (TypeUtil.toString(digest, 16)
299: .equalsIgnoreCase(response));
300: } catch (Exception e) {
301: Log.warn(e);
302: }
303:
304: return false;
305: }
306:
307: public String toString() {
308: return username + "," + response;
309: }
310:
311: }
312:
313: /**
314: * @return Returns the maxNonceAge.
315: */
316: public long getMaxNonceAge() {
317: return maxNonceAge;
318: }
319:
320: /**
321: * @param maxNonceAge The maxNonceAge to set.
322: */
323: public void setMaxNonceAge(long maxNonceAge) {
324: this .maxNonceAge = maxNonceAge;
325: }
326:
327: /**
328: * @return Returns the nonceSecret.
329: */
330: public long getNonceSecret() {
331: return nonceSecret;
332: }
333:
334: /**
335: * @param nonceSecret The nonceSecret to set.
336: */
337: public void setNonceSecret(long nonceSecret) {
338: this .nonceSecret = nonceSecret;
339: }
340:
341: public void setUseStale(boolean us) {
342: this .useStale = us;
343: }
344:
345: public boolean getUseStale() {
346: return useStale;
347: }
348: }
|