View Javadoc

    1   // ========================================================================
    2   // Copyright 2002-2005 Mort Bay Consulting Pty. Ltd.
    3   // ------------------------------------------------------------------------
    4   // Licensed under the Apache License, Version 2.0 (the "License");
    5   // you may not use this file except in compliance with the License.
    6   // You may obtain a copy of the License at
    7   // http://www.apache.org/licenses/LICENSE-2.0
    8   // Unless required by applicable law or agreed to in writing, software
    9   // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  // ========================================================================
    14
    15  package org.mortbay.jetty.security;
    16
    17  import java.io.IOException;
    18  import java.security.MessageDigest;
    19  import java.security.Principal;
    20
    21  import javax.servlet.http.HttpServletResponse;
    22
    23  import org.mortbay.jetty.HttpHeaders;
    24  import org.mortbay.jetty.Request;
    25  import org.mortbay.jetty.Response;
    26  import org.mortbay.log.Log;
    27  import org.mortbay.util.QuotedStringTokenizer;
    28  import org.mortbay.util.StringUtil;
    29  import org.mortbay.util.TypeUtil;
    30
    31  /* ------------------------------------------------------------ */
    32  /** DIGEST authentication.
    33   *
    34   * @author Greg Wilkins (gregw)
    35   */
    36  public class DigestAuthenticator implements Authenticator
    37  {
    38      protected long maxNonceAge=0;
    39      protected long nonceSecret=this.hashCode() ^ System.currentTimeMillis();
    40      protected boolean useStale=false;
    41
    42
    43      /* ------------------------------------------------------------ */
    44      /**
    45       * @return UserPrinciple if authenticated or null if not. If
    46       * Authentication fails, then the authenticator may have committed
    47       * the response as an auth challenge or redirect.
    48       * @exception IOException
    49       */
    50      public Principal authenticate(UserRealm realm,
    51                                             String pathInContext,
    52                                             Request request,
    53                                             Response response)
    54          throws IOException
    55      {
    56          // Get the user if we can
    57          boolean stale=false;
    58          Principal user=null;
    59          String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
    60
    61          if (credentials!=null )
    62          {
    63              if(Log.isDebugEnabled())Log.debug("Credentials: "+credentials);
    64              QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials,
    65                                                                          "=, ",
    66                                                                          true,
    67                                                                          false);
    68              Digest digest=new Digest(request.getMethod());
    69              String last=null;
    70              String name=null;
    71
    72            loop:
    73              while (tokenizer.hasMoreTokens())
    74              {
    75                  String tok = tokenizer.nextToken();
    76                  char c=(tok.length()==1)?tok.charAt(0):'\0';
    77
    78                  switch (c)
    79                  {
    80                    case '=':
    81                        name=last;
    82                        last=tok;
    83                        break;
    84                    case ',':
    85                        name=null;
    86                    case ' ':
    87                        break;
    88
    89                    default:
    90                        last=tok;
    91                        if (name!=null)
    92                        {
    93                            if ("username".equalsIgnoreCase(name))
    94                                digest.username=tok;
    95                            else if ("realm".equalsIgnoreCase(name))
    96                                digest.realm=tok;
    97                            else if ("nonce".equalsIgnoreCase(name))
    98                                digest.nonce=tok;
    99                            else if ("nc".equalsIgnoreCase(name))
    100                               digest.nc=tok;
    101                           else if ("cnonce".equalsIgnoreCase(name))
    102                               digest.cnonce=tok;
    103                           else if ("qop".equalsIgnoreCase(name))
    104                               digest.qop=tok;
    105                           else if ("uri".equalsIgnoreCase(name))
    106                               digest.uri=tok;
    107                           else if ("response".equalsIgnoreCase(name))
    108                               digest.response=tok;
    109                           name=null;
    110                       }
    111                 }
    112             }
    113
    114             int n=checkNonce(digest.nonce,request);
    115             if (n>0)
    116                 user = realm.authenticate(digest.username,digest,request);
    117             else if (n==0)
    118                 stale = true;
    119
    120             if (user==null)
    121                 Log.warn("AUTH FAILURE: user "+StringUtil.printable(digest.username));
    122             else
    123             {
    124                 request.setAuthType(Constraint.__DIGEST_AUTH);
    125                 request.setUserPrincipal(user);
    126             }
    127         }
    128
    129         // Challenge if we have no user
    130         if (user==null && response!=null)
    131             sendChallenge(realm,request,response,stale);
    132
    133         return user;
    134     }
    135
    136     /* ------------------------------------------------------------ */
    137     public String getAuthMethod()
    138     {
    139         return Constraint.__DIGEST_AUTH;
    140     }
    141
    142     /* ------------------------------------------------------------ */
    143     public void sendChallenge(UserRealm realm,
    144                               Request request,
    145                               Response response,
    146                               boolean stale)
    147         throws IOException
    148     {
    149         String domain=request.getContextPath();
    150         if (domain==null)
    151             domain="/";
    152         response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
    153 			    "Digest realm=\""+realm.getName()+
    154 			    "\", domain=\""+domain +
    155 			    "\", nonce=\""+newNonce(request)+
    156 			    "\", algorithm=MD5, qop=\"auth\"" + (useStale?(" stale="+stale):"")
    157                           );
    158         response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    159     }
    160
    161     /* ------------------------------------------------------------ */
    162     public String newNonce(Request request)
    163     {
    164         long ts=request.getTimeStamp();
    165         long sk=nonceSecret;
    166
    167         byte[] nounce = new byte[24];
    168         for (int i=0;i<8;i++)
    169         {
    170             nounce[i]=(byte)(ts&0xff);
    171             ts=ts>>8;
    172             nounce[8+i]=(byte)(sk&0xff);
    173             sk=sk>>8;
    174         }
    175
    176         byte[] hash=null;
    177         try
    178         {
    179             MessageDigest md = MessageDigest.getInstance("MD5");
    180             md.reset();
    181             md.update(nounce,0,16);
    182             hash = md.digest();
    183         }
    184         catch(Exception e)
    185         {
    186             Log.warn(e);
    187         }
    188
    189         for (int i=0;i<hash.length;i++)
    190         {
    191             nounce[8+i]=hash[i];
    192             if (i==23)
    193                 break;
    194         }
    195
    196         return new String(B64Code.encode(nounce));
    197     }
    198
    199     /**
    200      * @param nonce
    201      * @param request
    202      * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
    203      */
    204     /* ------------------------------------------------------------ */
    205     public int checkNonce(String nonce, Request request)
    206     {
    207         try
    208         {
    209             byte[] n = B64Code.decode(nonce.toCharArray());
    210             if (n.length!=24)
    211                 return -1;
    212
    213             long ts=0;
    214             long sk=nonceSecret;
    215             byte[] n2 = new byte[16];
    216             System.arraycopy(n, 0, n2, 0, 8);
    217             for (int i=0;i<8;i++)
    218             {
    219                 n2[8+i]=(byte)(sk&0xff);
    220                 sk=sk>>8;
    221                 ts=(ts<<8)+(0xff&(long)n[7-i]);
    222             }
    223
    224             long age=request.getTimeStamp()-ts;
    225             if (Log.isDebugEnabled()) Log.debug("age="+age);
    226
    227             byte[] hash=null;
    228             try
    229             {
    230                 MessageDigest md = MessageDigest.getInstance("MD5");
    231                 md.reset();
    232                 md.update(n2,0,16);
    233                 hash = md.digest();
    234             }
    235             catch(Exception e)
    236             {
    237                 Log.warn(e);
    238             }
    239
    240             for (int i=0;i<16;i++)
    241                 if (n[i+8]!=hash[i])
    242                     return -1;
    243
    244             if(maxNonceAge>0 && (age<0 || age>maxNonceAge))
    245                 return 0; // stale
    246
    247             return 1;
    248         }
    249         catch(Exception e)
    250         {
    251             Log.ignore(e);
    252         }
    253         return -1;
    254     }
    255
    256     /* ------------------------------------------------------------ */
    257     /* ------------------------------------------------------------ */
    258     /* ------------------------------------------------------------ */
    259     private static class Digest extends Credential
    260     {
    261         String method=null;
    262         String username = null;
    263         String realm = null;
    264         String nonce = null;
    265         String nc = null;
    266         String cnonce = null;
    267         String qop = null;
    268         String uri = null;
    269         String response=null;
    270
    271         /* ------------------------------------------------------------ */
    272         Digest(String m)
    273         {
    274             method=m;
    275         }
    276
    277         /* ------------------------------------------------------------ */
    278         public boolean check(Object credentials)
    279         {
    280             String password=(credentials instanceof String)
    281                 ?(String)credentials
    282                 :credentials.toString();
    283
    284             try{
    285                 MessageDigest md = MessageDigest.getInstance("MD5");
    286                 byte[] ha1;
    287                 if(credentials instanceof Credential.MD5)
    288                 {
    289                     // Credentials are already a MD5 digest - assume it's in
    290                     // form user:realm:password (we have no way to know since
    291                     // it's a digest, alright?)
    292                     ha1 = ((Credential.MD5)credentials).getDigest();
    293                 }
    294                 else
    295                 {
    296                     // calc A1 digest
    297                     md.update(username.getBytes(StringUtil.__ISO_8859_1));
    298                     md.update((byte)':');
    299                     md.update(realm.getBytes(StringUtil.__ISO_8859_1));
    300                     md.update((byte)':');
    301                     md.update(password.getBytes(StringUtil.__ISO_8859_1));
    302                     ha1=md.digest();
    303                 }
    304                 // calc A2 digest
    305                 md.reset();
    306                 md.update(method.getBytes(StringUtil.__ISO_8859_1));
    307                 md.update((byte)':');
    308                 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
    309                 byte[] ha2=md.digest();
    310
    311
    312
    313
    314
    315                 // calc digest
    316                 // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <">
    317                 // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
    318
    319
    320
    321                 md.update(TypeUtil.toString(ha1,16).getBytes(StringUtil.__ISO_8859_1));
    322                 md.update((byte)':');
    323                 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
    324                 md.update((byte)':');
    325                 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
    326                 md.update((byte)':');
    327                 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
    328                 md.update((byte)':');
    329                 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
    330                 md.update((byte)':');
    331                 md.update(TypeUtil.toString(ha2,16).getBytes(StringUtil.__ISO_8859_1));
    332                 byte[] digest=md.digest();
    333
    334                 // check digest
    335                 return (TypeUtil.toString(digest,16).equalsIgnoreCase(response));
    336             }
    337             catch (Exception e)
    338             {Log.warn(e);}
    339
    340             return false;
    341         }
    342
    343         public String toString()
    344         {
    345             return username+","+response;
    346         }
    347
    348     }
    349     /**
    350      * @return Returns the maxNonceAge.
    351      */
    352     public long getMaxNonceAge()
    353     {
    354         return maxNonceAge;
    355     }
    356     /**
    357      * @param maxNonceAge The maxNonceAge to set.
    358      */
    359     public void setMaxNonceAge(long maxNonceAge)
    360     {
    361         this.maxNonceAge = maxNonceAge;
    362     }
    363     /**
    364      * @return Returns the nonceSecret.
    365      */
    366     public long getNonceSecret()
    367     {
    368         return nonceSecret;
    369     }
    370     /**
    371      * @param nonceSecret The nonceSecret to set.
    372      */
    373     public void setNonceSecret(long nonceSecret)
    374     {
    375         this.nonceSecret = nonceSecret;
    376     }
    377
    378     public void setUseStale(boolean us)
    379     {
    380 	this.useStale=us;
    381     }
    382
    383     public boolean getUseStale()
    384     {
    385 	return useStale;
    386     }
    387 }
    388