2
* Licensed to the Apache Software Foundation (ASF) under one or more
3
* contributor license agreements. See the NOTICE file distributed with
4
* this work for additional information regarding copyright ownership.
5
* The ASF licenses this file to You under the Apache License, Version 2.0
6
* (the "License"); you may not use this file except in compliance with
7
* the License. You may obtain a copy of the License at
9
* http://www.apache.org/licenses/LICENSE-2.0
11
* Unless required by applicable law or agreed to in writing, software
12
* distributed under the License is distributed on an "AS IS" BASIS,
13
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
* See the License for the specific language governing permissions and
15
* limitations under the License.
19
package org.apache.catalina.authenticator;
22
import java.io.IOException;
23
import java.security.MessageDigest;
24
import java.security.NoSuchAlgorithmException;
25
import java.security.Principal;
26
import java.util.StringTokenizer;
28
import javax.servlet.http.HttpServletResponse;
31
import org.apache.catalina.Realm;
32
import org.apache.catalina.connector.Request;
33
import org.apache.catalina.connector.Response;
34
import org.apache.catalina.deploy.LoginConfig;
35
import org.apache.catalina.util.MD5Encoder;
36
import org.apache.juli.logging.Log;
37
import org.apache.juli.logging.LogFactory;
42
* An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST
43
* Authentication (see RFC 2069).
45
* @author Craig R. McClanahan
46
* @author Remy Maucherat
47
* @version $Revision: 467222 $ $Date: 2006-10-24 05:17:11 +0200 (Di, 24. Okt 2006) $
50
public class DigestAuthenticator
51
extends AuthenticatorBase {
52
private static Log log = LogFactory.getLog(DigestAuthenticator.class);
55
// -------------------------------------------------------------- Constants
58
* The MD5 helper object for this class.
60
protected static final MD5Encoder md5Encoder = new MD5Encoder();
64
* Descriptive information about this implementation.
66
protected static final String info =
67
"org.apache.catalina.authenticator.DigestAuthenticator/1.0";
70
// ----------------------------------------------------------- Constructors
73
public DigestAuthenticator() {
76
if (md5Helper == null)
77
md5Helper = MessageDigest.getInstance("MD5");
78
} catch (NoSuchAlgorithmException e) {
80
throw new IllegalStateException();
85
// ----------------------------------------------------- Instance Variables
89
* MD5 message digest provider.
91
protected static MessageDigest md5Helper;
97
protected String key = "Catalina";
100
// ------------------------------------------------------------- Properties
104
* Return descriptive information about this Valve implementation.
106
public String getInfo() {
113
// --------------------------------------------------------- Public Methods
117
* Authenticate the user making this request, based on the specified
118
* login configuration. Return <code>true</code> if any specified
119
* constraint has been satisfied, or <code>false</code> if we have
120
* created a response challenge already.
122
* @param request Request we are processing
123
* @param response Response we are creating
124
* @param config Login configuration describing how authentication
125
* should be performed
127
* @exception IOException if an input/output error occurs
129
public boolean authenticate(Request request,
134
// Have we already authenticated someone?
135
Principal principal = request.getUserPrincipal();
136
//String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
137
if (principal != null) {
138
if (log.isDebugEnabled())
139
log.debug("Already authenticated '" + principal.getName() + "'");
140
// Associate the session with any existing SSO session in order
141
// to get coordinated session invalidation at logout
142
String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
144
associate(ssoId, request.getSessionInternal(true));
148
// NOTE: We don't try to reauthenticate using any existing SSO session,
149
// because that will only work if the original authentication was
150
// BASIC or FORM, which are less secure than the DIGEST auth-type
151
// specified for this webapp
153
// Uncomment below to allow previous FORM or BASIC authentications
154
// to authenticate users for this webapp
155
// TODO make this a configurable attribute (in SingleSignOn??)
157
// Is there an SSO session against which we can try to reauthenticate?
159
if (log.isDebugEnabled())
160
log.debug("SSO Id " + ssoId + " set; attempting " +
162
// Try to reauthenticate using data cached by SSO. If this fails,
163
// either the original SSO logon was of DIGEST or SSL (which
164
// we can't reauthenticate ourselves because there is no
165
// cached username and password), or the realm denied
166
// the user's reauthentication for some reason.
167
// In either case we have to prompt the user for a logon
168
if (reauthenticateFromSSO(ssoId, request))
173
// Validate any credentials already included with this request
174
String authorization = request.getHeader("authorization");
175
if (authorization != null) {
176
principal = findPrincipal(request, authorization, context.getRealm());
177
if (principal != null) {
178
String username = parseUsername(authorization);
179
register(request, response, principal,
180
Constants.DIGEST_METHOD,
186
// Send an "unauthorized" response and an appropriate challenge
188
// Next, generate a nOnce token (that is a token which is supposed
190
String nOnce = generateNOnce(request);
192
setAuthenticateHeader(request, response, config, nOnce);
193
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
194
// hres.flushBuffer();
200
// ------------------------------------------------------ Protected Methods
204
* Parse the specified authorization credentials, and return the
205
* associated Principal that these credentials authenticate (if any)
206
* from the specified Realm. If there is no such Principal, return
209
* @param request HTTP servlet request
210
* @param authorization Authorization credentials from this request
211
* @param realm Realm used to authenticate Principals
213
protected static Principal findPrincipal(Request request,
214
String authorization,
217
//System.out.println("Authorization token : " + authorization);
218
// Validate the authorization credentials format
219
if (authorization == null)
221
if (!authorization.startsWith("Digest "))
223
authorization = authorization.substring(7).trim();
225
// Bugzilla 37132: http://issues.apache.org/bugzilla/show_bug.cgi?id=37132
226
String[] tokens = authorization.split(",(?=(?:[^\"]*\"[^\"]*\")+$)");
228
String userName = null;
229
String realmName = null;
232
String cnonce = null;
235
String response = null;
236
String method = request.getMethod();
238
for (int i = 0; i < tokens.length; i++) {
239
String currentToken = tokens[i];
240
if (currentToken.length() == 0)
243
int equalSign = currentToken.indexOf('=');
246
String currentTokenName =
247
currentToken.substring(0, equalSign).trim();
248
String currentTokenValue =
249
currentToken.substring(equalSign + 1).trim();
250
if ("username".equals(currentTokenName))
251
userName = removeQuotes(currentTokenValue);
252
if ("realm".equals(currentTokenName))
253
realmName = removeQuotes(currentTokenValue, true);
254
if ("nonce".equals(currentTokenName))
255
nOnce = removeQuotes(currentTokenValue);
256
if ("nc".equals(currentTokenName))
257
nc = removeQuotes(currentTokenValue);
258
if ("cnonce".equals(currentTokenName))
259
cnonce = removeQuotes(currentTokenValue);
260
if ("qop".equals(currentTokenName))
261
qop = removeQuotes(currentTokenValue);
262
if ("uri".equals(currentTokenName))
263
uri = removeQuotes(currentTokenValue);
264
if ("response".equals(currentTokenName))
265
response = removeQuotes(currentTokenValue);
268
if ( (userName == null) || (realmName == null) || (nOnce == null)
269
|| (uri == null) || (response == null) )
272
// Second MD5 digest used to calculate the digest :
273
// MD5(Method + ":" + uri)
274
String a2 = method + ":" + uri;
275
//System.out.println("A2:" + a2);
277
byte[] buffer = null;
278
synchronized (md5Helper) {
279
buffer = md5Helper.digest(a2.getBytes());
281
String md5a2 = md5Encoder.encode(buffer);
283
return (realm.authenticate(userName, response, nOnce, nc, cnonce, qop,
290
* Parse the username from the specified authorization string. If none
291
* can be identified, return <code>null</code>
293
* @param authorization Authorization string to be parsed
295
protected String parseUsername(String authorization) {
297
//System.out.println("Authorization token : " + authorization);
298
// Validate the authorization credentials format
299
if (authorization == null)
301
if (!authorization.startsWith("Digest "))
303
authorization = authorization.substring(7).trim();
305
StringTokenizer commaTokenizer =
306
new StringTokenizer(authorization, ",");
308
while (commaTokenizer.hasMoreTokens()) {
309
String currentToken = commaTokenizer.nextToken();
310
int equalSign = currentToken.indexOf('=');
313
String currentTokenName =
314
currentToken.substring(0, equalSign).trim();
315
String currentTokenValue =
316
currentToken.substring(equalSign + 1).trim();
317
if ("username".equals(currentTokenName))
318
return (removeQuotes(currentTokenValue));
327
* Removes the quotes on a string. RFC2617 states quotes are optional for
328
* all parameters except realm.
330
protected static String removeQuotes(String quotedString,
331
boolean quotesRequired) {
332
//support both quoted and non-quoted
333
if (quotedString.length() > 0 && quotedString.charAt(0) != '"' &&
336
} else if (quotedString.length() > 2) {
337
return quotedString.substring(1, quotedString.length() - 1);
344
* Removes the quotes on a string.
346
protected static String removeQuotes(String quotedString) {
347
return removeQuotes(quotedString, false);
351
* Generate a unique token. The token is generated according to the
352
* following pattern. NOnceToken = Base64 ( MD5 ( client-IP ":"
353
* time-stamp ":" private-key ) ).
355
* @param request HTTP Servlet request
357
protected String generateNOnce(Request request) {
359
long currentTime = System.currentTimeMillis();
361
String nOnceValue = request.getRemoteAddr() + ":" +
362
currentTime + ":" + key;
364
byte[] buffer = null;
365
synchronized (md5Helper) {
366
buffer = md5Helper.digest(nOnceValue.getBytes());
368
nOnceValue = md5Encoder.encode(buffer);
375
* Generates the WWW-Authenticate header.
377
* The header MUST follow this template :
379
* WWW-Authenticate = "WWW-Authenticate" ":" "Digest"
382
* digest-challenge = 1#( realm | [ domain ] | nOnce |
383
* [ digest-opaque ] |[ stale ] | [ algorithm ] )
385
* realm = "realm" "=" realm-value
386
* realm-value = quoted-string
387
* domain = "domain" "=" <"> 1#URI <">
388
* nonce = "nonce" "=" nonce-value
389
* nonce-value = quoted-string
390
* opaque = "opaque" "=" quoted-string
391
* stale = "stale" "=" ( "true" | "false" )
392
* algorithm = "algorithm" "=" ( "MD5" | token )
395
* @param request HTTP Servlet request
396
* @param response HTTP Servlet response
397
* @param config Login configuration describing how authentication
398
* should be performed
399
* @param nOnce nonce token
401
protected void setAuthenticateHeader(Request request,
406
// Get the realm name
407
String realmName = config.getRealmName();
408
if (realmName == null)
409
realmName = request.getServerName() + ":"
410
+ request.getServerPort();
412
byte[] buffer = null;
413
synchronized (md5Helper) {
414
buffer = md5Helper.digest(nOnce.getBytes());
417
String authenticateHeader = "Digest realm=\"" + realmName + "\", "
418
+ "qop=\"auth\", nonce=\"" + nOnce + "\", " + "opaque=\""
419
+ md5Encoder.encode(buffer) + "\"";
420
response.setHeader("WWW-Authenticate", authenticateHeader);