Home | History | Annotate | Download | only in clientauthutils
      1 package gov.nist.javax.sip.clientauthutils;
      2 
      3 /*
      4  *
      5  * This code has been contributed with permission from:
      6  *
      7  * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client but has been significantly changed.
      8  * It is donated to the JAIN-SIP project as it is common code that many sip clients
      9  * need to perform class and others will consitute a set of utility functions
     10  * that will implement common operations that ease the life of the developer.
     11  *
     12  * Acknowledgements:
     13  * ----------------
     14  *
     15  * Fredrik Wickstrom reported that dialog cseq counters are not incremented
     16  * when resending requests. He later uncovered additional problems and
     17  * proposed a way to fix them (his proposition was taken into account).
     18  */
     19 
     20 import gov.nist.javax.sip.SipStackImpl;
     21 import gov.nist.javax.sip.address.SipUri;
     22 import gov.nist.javax.sip.message.SIPRequest;
     23 import gov.nist.javax.sip.stack.SIPClientTransaction;
     24 import gov.nist.javax.sip.stack.SIPTransactionStack;
     25 
     26 import java.text.ParseException;
     27 import java.util.Collection;
     28 import java.util.Iterator;
     29 import java.util.ListIterator;
     30 import java.util.Timer;
     31 
     32 import javax.sip.ClientTransaction;
     33 import javax.sip.DialogState;
     34 import javax.sip.InvalidArgumentException;
     35 import javax.sip.SipException;
     36 import javax.sip.SipProvider;
     37 import javax.sip.address.Hop;
     38 import javax.sip.address.SipURI;
     39 import javax.sip.address.URI;
     40 import javax.sip.header.AuthorizationHeader;
     41 import javax.sip.header.CSeqHeader;
     42 import javax.sip.header.Header;
     43 import javax.sip.header.HeaderFactory;
     44 import javax.sip.header.ProxyAuthenticateHeader;
     45 import javax.sip.header.ProxyAuthorizationHeader;
     46 import javax.sip.header.ViaHeader;
     47 import javax.sip.header.WWWAuthenticateHeader;
     48 import javax.sip.message.Request;
     49 import javax.sip.message.Response;
     50 
     51 /**
     52  * The class handles authentication challenges, caches user credentials and takes care (through
     53  * the SecurityAuthority interface) about retrieving passwords.
     54  *
     55  *
     56  * @author Emil Ivov
     57  * @author Jeroen van Bemmel
     58  * @author M. Ranganathan
     59  *
     60  * @since 2.0
     61  */
     62 
     63 public class AuthenticationHelperImpl implements AuthenticationHelper {
     64 
     65     /**
     66      * Credentials cached so far.
     67      */
     68     private CredentialsCache cachedCredentials;
     69 
     70     /**
     71      * The account manager for the system. Stores user credentials.
     72      */
     73     private Object accountManager = null;
     74 
     75     /*
     76      * Header factory for this security manager.
     77      */
     78     private HeaderFactory headerFactory;
     79 
     80     private SipStackImpl sipStack;
     81 
     82     Timer timer;
     83 
     84     /**
     85      * Default constructor for the security manager. There is one Account manager. There is one
     86      * SipSecurity manager for every user name,
     87      *
     88      * @param sipStack -- our stack.
     89      * @param accountManager -- an implementation of the AccountManager interface.
     90      * @param headerFactory -- header factory.
     91      */
     92     public AuthenticationHelperImpl(SipStackImpl sipStack, AccountManager accountManager,
     93             HeaderFactory headerFactory) {
     94         this.accountManager = accountManager;
     95         this.headerFactory = headerFactory;
     96         this.sipStack = sipStack;
     97 
     98         this.cachedCredentials = new CredentialsCache(((SIPTransactionStack) sipStack).getTimer());
     99     }
    100 
    101     /**
    102      * Default constructor for the security manager. There is one Account manager. There is one
    103      * SipSecurity manager for every user name,
    104      *
    105      * @param sipStack -- our stack.
    106      * @param accountManager -- an implementation of the AccountManager interface.
    107      * @param headerFactory -- header factory.
    108      */
    109     public AuthenticationHelperImpl(SipStackImpl sipStack, SecureAccountManager accountManager,
    110             HeaderFactory headerFactory) {
    111         this.accountManager = accountManager;
    112         this.headerFactory = headerFactory;
    113         this.sipStack = sipStack;
    114 
    115         this.cachedCredentials = new CredentialsCache(((SIPTransactionStack) sipStack).getTimer());
    116     }
    117 
    118 
    119     /*
    120      * (non-Javadoc)
    121      *
    122      * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#handleChallenge(javax.sip.message.Response,
    123      *      javax.sip.ClientTransaction, javax.sip.SipProvider)
    124      */
    125     public ClientTransaction handleChallenge(Response challenge,
    126             ClientTransaction challengedTransaction, SipProvider transactionCreator, int cacheTime)
    127             throws SipException, NullPointerException {
    128         try {
    129             if (sipStack.isLoggingEnabled()) {
    130                 sipStack.getStackLogger().logDebug("handleChallenge: " + challenge);
    131             }
    132 
    133             SIPRequest challengedRequest = ((SIPRequest) challengedTransaction.getRequest());
    134 
    135             Request reoriginatedRequest = null;
    136             /*
    137              * If the challenged request is part of a Dialog and the
    138              * Dialog is confirmed the re-originated request should be
    139              * generated as an in-Dialog request.
    140              */
    141             if (  challengedRequest.getToTag() != null  ||
    142                     challengedTransaction.getDialog() == null ||
    143                     challengedTransaction.getDialog().getState() != DialogState.CONFIRMED)  {
    144                 reoriginatedRequest = (Request) challengedRequest.clone();
    145             } else {
    146                 /*
    147                  * Re-originate the request by consulting the dialog. In particular
    148                  * the route set could change between the original request and the
    149                  * in-dialog challenge.
    150                  */
    151                 reoriginatedRequest =
    152                     challengedTransaction.getDialog().createRequest(challengedRequest.getMethod());
    153                 Iterator<String> headerNames = challengedRequest.getHeaderNames();
    154                 while (headerNames.hasNext()) {
    155                     String headerName = headerNames.next();
    156                     if ( reoriginatedRequest.getHeader(headerName) != null) {
    157                         ListIterator<Header> iterator = reoriginatedRequest.getHeaders(headerName);
    158                         while (iterator.hasNext()) {
    159                             reoriginatedRequest.addHeader(iterator.next());
    160                         }
    161                     }
    162                 }
    163             }
    164 
    165 
    166 
    167             // remove the branch id so that we could use the request in a new
    168             // transaction
    169             removeBranchID(reoriginatedRequest);
    170 
    171             if (challenge == null || reoriginatedRequest == null) {
    172                 throw new NullPointerException("A null argument was passed to handle challenge.");
    173             }
    174 
    175             ListIterator authHeaders = null;
    176 
    177             if (challenge.getStatusCode() == Response.UNAUTHORIZED) {
    178                 authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME);
    179             } else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED) {
    180                 authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME);
    181             } else {
    182                 throw new IllegalArgumentException("Unexpected status code ");
    183             }
    184 
    185             if (authHeaders == null) {
    186                 throw new IllegalArgumentException(
    187                         "Could not find WWWAuthenticate or ProxyAuthenticate headers");
    188             }
    189 
    190             // Remove all authorization headers from the request (we'll re-add them
    191             // from cache)
    192             reoriginatedRequest.removeHeader(AuthorizationHeader.NAME);
    193             reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME);
    194 
    195             // rfc 3261 says that the cseq header should be augmented for the new
    196             // request. do it here so that the new dialog (created together with
    197             // the new client transaction) takes it into account.
    198             // Bug report - Fredrik Wickstrom
    199             CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest.getHeader((CSeqHeader.NAME));
    200             try {
    201                 cSeq.setSeqNumber(cSeq.getSeqNumber() + 1l);
    202             } catch (InvalidArgumentException ex) {
    203                 throw new SipException("Invalid CSeq -- could not increment : "
    204                         + cSeq.getSeqNumber());
    205             }
    206 
    207 
    208             /* Resolve this to the next hop based on the previous lookup. If we are not using
    209              * lose routing (RFC2543) then just attach hop as a maddr param.
    210              */
    211             if ( challengedRequest.getRouteHeaders() == null ) {
    212                 Hop hop   = ((SIPClientTransaction) challengedTransaction).getNextHop();
    213                 SipURI sipUri = (SipURI) reoriginatedRequest.getRequestURI();
    214                 sipUri.setMAddrParam(hop.getHost());
    215                 if ( hop.getPort() != -1 ) sipUri.setPort(hop.getPort());
    216             }
    217             ClientTransaction retryTran = transactionCreator
    218             .getNewClientTransaction(reoriginatedRequest);
    219 
    220             WWWAuthenticateHeader authHeader = null;
    221             SipURI requestUri = (SipURI) challengedTransaction.getRequest().getRequestURI();
    222             while (authHeaders.hasNext()) {
    223                 authHeader = (WWWAuthenticateHeader) authHeaders.next();
    224                 String realm = authHeader.getRealm();
    225                 AuthorizationHeader authorization = null;
    226                 String sipDomain;
    227                 if ( this.accountManager instanceof SecureAccountManager ) {
    228                     UserCredentialHash credHash =
    229                         ((SecureAccountManager)this.accountManager).getCredentialHash(challengedTransaction,realm);
    230                     URI uri = reoriginatedRequest.getRequestURI();
    231                     sipDomain = credHash.getSipDomain();
    232                     authorization = this.getAuthorization(reoriginatedRequest
    233                             .getMethod(), uri.toString(),
    234                             (reoriginatedRequest.getContent() == null) ? "" : new String(
    235                             reoriginatedRequest.getRawContent()), authHeader, credHash);
    236                 } else {
    237                     UserCredentials userCreds = ((AccountManager) this.accountManager).getCredentials(challengedTransaction, realm);
    238                     sipDomain = userCreds.getSipDomain();
    239                     if (userCreds == null)
    240                          throw new SipException(
    241                             "Cannot find user creds for the given user name and realm");
    242 
    243                     // we haven't yet authenticated this realm since we were
    244                     // started.
    245 
    246                        authorization = this.getAuthorization(reoriginatedRequest
    247                                 .getMethod(), reoriginatedRequest.getRequestURI().toString(),
    248                                 (reoriginatedRequest.getContent() == null) ? "" : new String(
    249                                 reoriginatedRequest.getRawContent()), authHeader, userCreds);
    250                 }
    251                 if (sipStack.isLoggingEnabled())
    252                 	sipStack.getStackLogger().logDebug(
    253                         "Created authorization header: " + authorization.toString());
    254 
    255                 if (cacheTime != 0)
    256                     cachedCredentials.cacheAuthorizationHeader(sipDomain,
    257                             authorization, cacheTime);
    258 
    259                 reoriginatedRequest.addHeader(authorization);
    260             }
    261 
    262             if (sipStack.isLoggingEnabled()) {
    263                 sipStack.getStackLogger().logDebug(
    264                         "Returning authorization transaction." + retryTran);
    265             }
    266             return retryTran;
    267         } catch (SipException ex) {
    268             throw ex;
    269         } catch (Exception ex) {
    270             sipStack.getStackLogger().logError("Unexpected exception ", ex);
    271             throw new SipException("Unexpected exception ", ex);
    272         }
    273     }
    274 
    275 
    276 
    277 
    278     /**
    279      * Generates an authorisation header in response to wwwAuthHeader.
    280      *
    281      * @param method method of the request being authenticated
    282      * @param uri digest-uri
    283      * @param requestBody the body of the request.
    284      * @param authHeader the challenge that we should respond to
    285      * @param userCredentials username and pass
    286      *
    287      * @return an authorisation header in response to authHeader.
    288      *
    289      * @throws OperationFailedException if auth header was malformated.
    290      */
    291     private AuthorizationHeader getAuthorization(String method, String uri, String requestBody,
    292             WWWAuthenticateHeader authHeader, UserCredentials userCredentials) {
    293         String response = null;
    294 
    295         // JvB: authHeader.getQop() is a quoted _list_ of qop values
    296         // (e.g. "auth,auth-int") Client is supposed to pick one
    297         String qopList = authHeader.getQop();
    298         String qop = (qopList != null) ? "auth" : null;
    299         String nc_value = "00000001";
    300         String cnonce = "xyz";
    301 
    302         response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(),
    303                 userCredentials.getUserName(), authHeader.getRealm(), userCredentials
    304                         .getPassword(), authHeader.getNonce(), nc_value, // JvB added
    305                 cnonce, // JvB added
    306                 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed
    307 
    308         AuthorizationHeader authorization = null;
    309         try {
    310             if (authHeader instanceof ProxyAuthenticateHeader) {
    311                 authorization = headerFactory.createProxyAuthorizationHeader(authHeader
    312                         .getScheme());
    313             } else {
    314                 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme());
    315             }
    316 
    317             authorization.setUsername(userCredentials.getUserName());
    318             authorization.setRealm(authHeader.getRealm());
    319             authorization.setNonce(authHeader.getNonce());
    320             authorization.setParameter("uri", uri);
    321             authorization.setResponse(response);
    322             if (authHeader.getAlgorithm() != null) {
    323                 authorization.setAlgorithm(authHeader.getAlgorithm());
    324             }
    325 
    326             if (authHeader.getOpaque() != null) {
    327                 authorization.setOpaque(authHeader.getOpaque());
    328             }
    329 
    330             // jvb added
    331             if (qop != null) {
    332                 authorization.setQop(qop);
    333                 authorization.setCNonce(cnonce);
    334                 authorization.setNonceCount(Integer.parseInt(nc_value));
    335             }
    336 
    337             authorization.setResponse(response);
    338 
    339         } catch (ParseException ex) {
    340             throw new RuntimeException("Failed to create an authorization header!");
    341         }
    342 
    343         return authorization;
    344     }
    345     /**
    346      * Generates an authorisation header in response to wwwAuthHeader.
    347      *
    348      * @param method method of the request being authenticated
    349      * @param uri digest-uri
    350      * @param requestBody the body of the request.
    351      * @param authHeader the challenge that we should respond to
    352      * @param userCredentials username and pass
    353      *
    354      * @return an authorisation header in response to authHeader.
    355      *
    356      * @throws OperationFailedException if auth header was malformated.
    357      */
    358     private AuthorizationHeader getAuthorization(String method, String uri, String requestBody,
    359             WWWAuthenticateHeader authHeader, UserCredentialHash userCredentials) {
    360         String response = null;
    361 
    362         // JvB: authHeader.getQop() is a quoted _list_ of qop values
    363         // (e.g. "auth,auth-int") Client is supposed to pick one
    364         String qopList = authHeader.getQop();
    365         String qop = (qopList != null) ? "auth" : null;
    366         String nc_value = "00000001";
    367         String cnonce = "xyz";
    368 
    369         response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(),
    370               userCredentials.getHashUserDomainPassword(), authHeader.getNonce(), nc_value, // JvB added
    371                 cnonce, // JvB added
    372                 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed
    373 
    374         AuthorizationHeader authorization = null;
    375         try {
    376             if (authHeader instanceof ProxyAuthenticateHeader) {
    377                 authorization = headerFactory.createProxyAuthorizationHeader(authHeader
    378                         .getScheme());
    379             } else {
    380                 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme());
    381             }
    382 
    383             authorization.setUsername(userCredentials.getUserName());
    384             authorization.setRealm(authHeader.getRealm());
    385             authorization.setNonce(authHeader.getNonce());
    386             authorization.setParameter("uri", uri);
    387             authorization.setResponse(response);
    388             if (authHeader.getAlgorithm() != null) {
    389                 authorization.setAlgorithm(authHeader.getAlgorithm());
    390             }
    391 
    392             if (authHeader.getOpaque() != null) {
    393                 authorization.setOpaque(authHeader.getOpaque());
    394             }
    395 
    396             // jvb added
    397             if (qop != null) {
    398                 authorization.setQop(qop);
    399                 authorization.setCNonce(cnonce);
    400                 authorization.setNonceCount(Integer.parseInt(nc_value));
    401             }
    402 
    403             authorization.setResponse(response);
    404 
    405         } catch (ParseException ex) {
    406             throw new RuntimeException("Failed to create an authorization header!");
    407         }
    408 
    409         return authorization;
    410     }
    411     /**
    412      * Removes all via headers from <tt>request</tt> and replaces them with a new one, equal to
    413      * the one that was top most.
    414      *
    415      * @param request the Request whose branchID we'd like to remove.
    416      *
    417      */
    418     private void removeBranchID(Request request) {
    419 
    420         ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
    421 
    422         viaHeader.removeParameter("branch");
    423 
    424     }
    425 
    426     /*
    427      * (non-Javadoc)
    428      *
    429      * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#attachAuthenticationHeaders(javax.sip.message.Request)
    430      */
    431     public void setAuthenticationHeaders(Request request) {
    432         SIPRequest sipRequest = (SIPRequest) request;
    433 
    434         String callId = sipRequest.getCallId().getCallId();
    435 
    436         request.removeHeader(AuthorizationHeader.NAME);
    437         Collection<AuthorizationHeader> authHeaders = this.cachedCredentials
    438                 .getCachedAuthorizationHeaders(callId);
    439         if (authHeaders == null) {
    440         	if (sipStack.isLoggingEnabled())
    441         		sipStack.getStackLogger().logDebug(
    442                     "Could not find authentication headers for " + callId);
    443             return;
    444         }
    445 
    446         for (AuthorizationHeader authHeader : authHeaders) {
    447             request.addHeader(authHeader);
    448         }
    449 
    450     }
    451 
    452     /*
    453      * (non-Javadoc)
    454      *
    455      * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#removeCachedAuthenticationHeaders(java.lang.String)
    456      */
    457     public void removeCachedAuthenticationHeaders(String callId) {
    458         if (callId == null)
    459             throw new NullPointerException("Null callId argument ");
    460         this.cachedCredentials.removeAuthenticationHeader(callId);
    461 
    462     }
    463 
    464 }
    465