001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: * Free SoftwareFoundation, Inc.
023: * 59 Temple Place, Suite 330
024: * Boston, MA 02111-1307 USA
025: *
026: * @author Scott Ferguson
027: */
028:
029: package com.caucho.server.security;
030:
031: import com.caucho.util.Base64;
032: import com.caucho.util.CharBuffer;
033: import com.caucho.util.CharCursor;
034: import com.caucho.util.RandomUtil;
035: import com.caucho.util.StringCharCursor;
036: import com.caucho.xml.XmlChar;
037:
038: import javax.servlet.ServletContext;
039: import javax.servlet.ServletException;
040: import javax.servlet.http.HttpServletRequest;
041: import javax.servlet.http.HttpServletResponse;
042: import java.io.IOException;
043: import java.security.Principal;
044: import java.util.logging.Level;
045:
046: /**
047: * Implements the "digest" auth-method. Basic uses the
048: * HTTP authentication with WWW-Authenticate and SC_UNAUTHORIZE.
049: */
050: public class DigestLogin extends AbstractLogin {
051: protected String _realm;
052:
053: /**
054: * Sets the login realm.
055: */
056: public void setRealmName(String realm) {
057: _realm = realm;
058: }
059:
060: /**
061: * Gets the realm.
062: */
063: public String getRealmName() {
064: return _realm;
065: }
066:
067: /**
068: * Returns the authentication type.
069: */
070: public String getAuthType() {
071: return "Digest";
072: }
073:
074: /**
075: * Logs a user in with a user name and a password. Basic authentication
076: * extracts the user and password from the authorization header. If
077: * the user/password is missing, authenticate will send a basic challenge.
078: *
079: * @param request servlet request
080: * @param response servlet response, in case any cookie need sending.
081: * @param application servlet application
082: *
083: * @return the logged in principal on success, null on failure.
084: */
085: public Principal authenticate(HttpServletRequest request,
086: HttpServletResponse response, ServletContext application)
087: throws ServletException, IOException {
088: Principal user;
089:
090: // If the user is already logged-in, return the user
091: user = getAuthenticator().getUserPrincipal(request, response,
092: application);
093: if (user != null)
094: return user;
095:
096: user = getDigestPrincipal(request, response, application);
097:
098: if (user != null)
099: return user;
100:
101: sendDigestChallenge(response, application);
102:
103: return null;
104: }
105:
106: /**
107: * Returns the current user with the user name and password.
108: *
109: * @param request servlet request
110: * @param response servlet response, in case any cookie need sending.
111: * @param application servlet application
112: *
113: * @return the logged in principal on success, null on failure.
114: */
115: public Principal getUserPrincipal(HttpServletRequest request,
116: HttpServletResponse response, ServletContext application)
117: throws ServletException {
118: ServletAuthenticator auth = getAuthenticator();
119:
120: Principal user = auth.getUserPrincipal(request, response,
121: application);
122:
123: if (user != null)
124: return user;
125:
126: return getDigestPrincipal(request, response, application);
127: }
128:
129: /**
130: * Sends a challenge for basic authentication.
131: */
132: protected void sendDigestChallenge(HttpServletResponse res,
133: ServletContext application) throws ServletException,
134: IOException {
135: String realm = getRealmName();
136: if (realm == null)
137: realm = "resin";
138:
139: CharBuffer cb = CharBuffer.allocate();
140: Base64.encode(cb, getRandomLong(application));
141: String nonce = cb.toString();
142: cb.clear();
143: cb.append("Digest ");
144: cb.append("realm=\"");
145: cb.append(realm);
146: cb.append("\", qop=\"auth\", ");
147: cb.append("nonce=\"");
148: cb.append(nonce);
149: cb.append("\"");
150:
151: res.setHeader("WWW-Authenticate", cb.close());
152:
153: res.sendError(res.SC_UNAUTHORIZED);
154: }
155:
156: protected long getRandomLong(ServletContext application) {
157: return RandomUtil.getRandomLong();
158: }
159:
160: /**
161: * Returns the principal from a basic authentication
162: *
163: * @param auth the authenticator for this application.
164: */
165: protected Principal getDigestPrincipal(HttpServletRequest request,
166: HttpServletResponse response, ServletContext application)
167: throws ServletException {
168: String value = request.getHeader("authorization");
169:
170: if (value == null)
171: return null;
172:
173: String username = null;
174: String realm = null;
175: String uri = null;
176: String nonce = null;
177: String cnonce = null;
178: String nc = null;
179: String qop = null;
180: String digest = null;
181:
182: CharCursor cursor = new StringCharCursor(value);
183:
184: String key = scanKey(cursor);
185:
186: if (!"Digest".equalsIgnoreCase(key))
187: return null;
188:
189: while ((key = scanKey(cursor)) != null) {
190: value = scanValue(cursor);
191:
192: if (key.equals("username"))
193: username = value;
194: else if (key.equals("realm"))
195: realm = value;
196: else if (key.equals("uri"))
197: uri = value;
198: else if (key.equals("nonce"))
199: nonce = value;
200: else if (key.equals("response"))
201: digest = value;
202: else if (key.equals("cnonce"))
203: cnonce = value;
204: else if (key.equals("nc"))
205: nc = value;
206: else if (key.equals("qop"))
207: qop = value;
208: }
209:
210: byte[] clientDigest = decodeDigest(digest);
211:
212: if (clientDigest == null || username == null || uri == null
213: || nonce == null)
214: return null;
215:
216: ServletAuthenticator auth = getAuthenticator();
217: Principal principal = auth.loginDigest(request, response,
218: application, username, realm, nonce, uri, qop, nc,
219: cnonce, clientDigest);
220:
221: if (log.isLoggable(Level.FINE))
222: log.fine("digest: " + username + " -> " + principal);
223:
224: return principal;
225: }
226:
227: protected byte[] decodeDigest(String digest) {
228: if (digest == null)
229: return null;
230:
231: int len = (digest.length() + 1) / 2;
232: byte[] clientDigest = new byte[len];
233:
234: for (int i = 0; i + 1 < digest.length(); i += 2) {
235: int ch1 = digest.charAt(i);
236: int ch2 = digest.charAt(i + 1);
237:
238: int b = 0;
239: if (ch1 >= '0' && ch1 <= '9')
240: b += ch1 - '0';
241: else if (ch1 >= 'a' && ch1 <= 'f')
242: b += ch1 - 'a' + 10;
243:
244: b *= 16;
245:
246: if (ch2 >= '0' && ch2 <= '9')
247: b += ch2 - '0';
248: else if (ch2 >= 'a' && ch2 <= 'f')
249: b += ch2 - 'a' + 10;
250:
251: clientDigest[i / 2] = (byte) b;
252: }
253:
254: return clientDigest;
255: }
256:
257: protected String scanKey(CharCursor cursor) throws ServletException {
258: int ch;
259: while (XmlChar.isWhitespace((ch = cursor.current()))
260: || ch == ',') {
261: cursor.next();
262: }
263:
264: ch = cursor.current();
265: if (ch == cursor.DONE)
266: return null;
267:
268: if (!XmlChar.isNameStart(ch))
269: throw new ServletException("bad key: " + (char) ch + " "
270: + cursor);
271:
272: CharBuffer cb = CharBuffer.allocate();
273: while (XmlChar.isNameChar(ch = cursor.read())) {
274: cb.append((char) ch);
275: }
276: if (ch != cursor.DONE)
277: cursor.previous();
278:
279: return cb.close();
280: }
281:
282: protected String scanValue(CharCursor cursor)
283: throws ServletException {
284: int ch;
285: skipWhitespace(cursor);
286:
287: ch = cursor.read();
288: if (ch != '=')
289: throw new ServletException("expected '='");
290:
291: skipWhitespace(cursor);
292:
293: CharBuffer cb = CharBuffer.allocate();
294:
295: ch = cursor.read();
296: if (ch == '"')
297: while ((ch = cursor.read()) != cursor.DONE && ch != '"')
298: cb.append((char) ch);
299: else {
300: for (; ch != cursor.DONE && ch != ','
301: && !XmlChar.isWhitespace(ch); ch = cursor.read())
302: cb.append((char) ch);
303:
304: if (ch != cursor.DONE)
305: cursor.previous();
306: }
307:
308: return cb.close();
309: }
310:
311: protected void skipWhitespace(CharCursor cursor) {
312: while (XmlChar.isWhitespace(cursor.current())) {
313: cursor.next();
314: }
315: }
316: }
|