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                 // BEGIN android-added
    215                 if ( !hop.getHost().equalsIgnoreCase(sipUri.getHost())
    216                         && !hop.equals(sipStack.getRouter(challengedRequest).getOutboundProxy()) )
    217                 // END android-added
    218                 sipUri.setMAddrParam(hop.getHost());
    219                 if ( hop.getPort() != -1 ) sipUri.setPort(hop.getPort());
    220             }
    221             ClientTransaction retryTran = transactionCreator
    222             .getNewClientTransaction(reoriginatedRequest);
    223 
    224             WWWAuthenticateHeader authHeader = null;
    225             SipURI requestUri = (SipURI) challengedTransaction.getRequest().getRequestURI();
    226             while (authHeaders.hasNext()) {
    227                 authHeader = (WWWAuthenticateHeader) authHeaders.next();
    228                 String realm = authHeader.getRealm();
    229                 AuthorizationHeader authorization = null;
    230                 String sipDomain;
    231                 if ( this.accountManager instanceof SecureAccountManager ) {
    232                     UserCredentialHash credHash =
    233                         ((SecureAccountManager)this.accountManager).getCredentialHash(challengedTransaction,realm);
    234                     URI uri = reoriginatedRequest.getRequestURI();
    235                     sipDomain = credHash.getSipDomain();
    236                     authorization = this.getAuthorization(reoriginatedRequest
    237                             .getMethod(), uri.toString(),
    238                             (reoriginatedRequest.getContent() == null) ? "" : new String(
    239                             reoriginatedRequest.getRawContent()), authHeader, credHash);
    240                 } else {
    241                     UserCredentials userCreds = ((AccountManager) this.accountManager).getCredentials(challengedTransaction, realm);
    242                     sipDomain = userCreds.getSipDomain();
    243                     if (userCreds == null)
    244                          throw new SipException(
    245                             "Cannot find user creds for the given user name and realm");
    246 
    247                     // we haven't yet authenticated this realm since we were
    248                     // started.
    249 
    250                        authorization = this.getAuthorization(reoriginatedRequest
    251                                 .getMethod(), reoriginatedRequest.getRequestURI().toString(),
    252                                 (reoriginatedRequest.getContent() == null) ? "" : new String(
    253                                 reoriginatedRequest.getRawContent()), authHeader, userCreds);
    254                 }
    255                 if (sipStack.isLoggingEnabled())
    256                 	sipStack.getStackLogger().logDebug(
    257                         "Created authorization header: " + authorization.toString());
    258 
    259                 if (cacheTime != 0)
    260                     cachedCredentials.cacheAuthorizationHeader(sipDomain,
    261                             authorization, cacheTime);
    262 
    263                 reoriginatedRequest.addHeader(authorization);
    264             }
    265 
    266             if (sipStack.isLoggingEnabled()) {
    267                 sipStack.getStackLogger().logDebug(
    268                         "Returning authorization transaction." + retryTran);
    269             }
    270             return retryTran;
    271         } catch (SipException ex) {
    272             throw ex;
    273         } catch (Exception ex) {
    274             sipStack.getStackLogger().logError("Unexpected exception ", ex);
    275             throw new SipException("Unexpected exception ", ex);
    276         }
    277     }
    278 
    279 
    280 
    281 
    282     /**
    283      * Generates an authorisation header in response to wwwAuthHeader.
    284      *
    285      * @param method method of the request being authenticated
    286      * @param uri digest-uri
    287      * @param requestBody the body of the request.
    288      * @param authHeader the challenge that we should respond to
    289      * @param userCredentials username and pass
    290      *
    291      * @return an authorisation header in response to authHeader.
    292      *
    293      * @throws OperationFailedException if auth header was malformated.
    294      */
    295     private AuthorizationHeader getAuthorization(String method, String uri, String requestBody,
    296             WWWAuthenticateHeader authHeader, UserCredentials userCredentials) {
    297         String response = null;
    298 
    299         // JvB: authHeader.getQop() is a quoted _list_ of qop values
    300         // (e.g. "auth,auth-int") Client is supposed to pick one
    301         String qopList = authHeader.getQop();
    302         String qop = (qopList != null) ? "auth" : null;
    303         String nc_value = "00000001";
    304         String cnonce = "xyz";
    305 
    306         response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(),
    307                 userCredentials.getUserName(), authHeader.getRealm(), userCredentials
    308                         .getPassword(), authHeader.getNonce(), nc_value, // JvB added
    309                 cnonce, // JvB added
    310                 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed
    311 
    312         AuthorizationHeader authorization = null;
    313         try {
    314             if (authHeader instanceof ProxyAuthenticateHeader) {
    315                 authorization = headerFactory.createProxyAuthorizationHeader(authHeader
    316                         .getScheme());
    317             } else {
    318                 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme());
    319             }
    320 
    321             authorization.setUsername(userCredentials.getUserName());
    322             authorization.setRealm(authHeader.getRealm());
    323             authorization.setNonce(authHeader.getNonce());
    324             authorization.setParameter("uri", uri);
    325             authorization.setResponse(response);
    326             if (authHeader.getAlgorithm() != null) {
    327                 authorization.setAlgorithm(authHeader.getAlgorithm());
    328             }
    329 
    330             if (authHeader.getOpaque() != null) {
    331                 authorization.setOpaque(authHeader.getOpaque());
    332             }
    333 
    334             // jvb added
    335             if (qop != null) {
    336                 authorization.setQop(qop);
    337                 authorization.setCNonce(cnonce);
    338                 authorization.setNonceCount(Integer.parseInt(nc_value));
    339             }
    340 
    341             authorization.setResponse(response);
    342 
    343         } catch (ParseException ex) {
    344             throw new RuntimeException("Failed to create an authorization header!");
    345         }
    346 
    347         return authorization;
    348     }
    349     /**
    350      * Generates an authorisation header in response to wwwAuthHeader.
    351      *
    352      * @param method method of the request being authenticated
    353      * @param uri digest-uri
    354      * @param requestBody the body of the request.
    355      * @param authHeader the challenge that we should respond to
    356      * @param userCredentials username and pass
    357      *
    358      * @return an authorisation header in response to authHeader.
    359      *
    360      * @throws OperationFailedException if auth header was malformated.
    361      */
    362     private AuthorizationHeader getAuthorization(String method, String uri, String requestBody,
    363             WWWAuthenticateHeader authHeader, UserCredentialHash userCredentials) {
    364         String response = null;
    365 
    366         // JvB: authHeader.getQop() is a quoted _list_ of qop values
    367         // (e.g. "auth,auth-int") Client is supposed to pick one
    368         String qopList = authHeader.getQop();
    369         String qop = (qopList != null) ? "auth" : null;
    370         String nc_value = "00000001";
    371         String cnonce = "xyz";
    372 
    373         response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(),
    374               userCredentials.getHashUserDomainPassword(), authHeader.getNonce(), nc_value, // JvB added
    375                 cnonce, // JvB added
    376                 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed
    377 
    378         AuthorizationHeader authorization = null;
    379         try {
    380             if (authHeader instanceof ProxyAuthenticateHeader) {
    381                 authorization = headerFactory.createProxyAuthorizationHeader(authHeader
    382                         .getScheme());
    383             } else {
    384                 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme());
    385             }
    386 
    387             authorization.setUsername(userCredentials.getUserName());
    388             authorization.setRealm(authHeader.getRealm());
    389             authorization.setNonce(authHeader.getNonce());
    390             authorization.setParameter("uri", uri);
    391             authorization.setResponse(response);
    392             if (authHeader.getAlgorithm() != null) {
    393                 authorization.setAlgorithm(authHeader.getAlgorithm());
    394             }
    395 
    396             if (authHeader.getOpaque() != null) {
    397                 authorization.setOpaque(authHeader.getOpaque());
    398             }
    399 
    400             // jvb added
    401             if (qop != null) {
    402                 authorization.setQop(qop);
    403                 authorization.setCNonce(cnonce);
    404                 authorization.setNonceCount(Integer.parseInt(nc_value));
    405             }
    406 
    407             authorization.setResponse(response);
    408 
    409         } catch (ParseException ex) {
    410             throw new RuntimeException("Failed to create an authorization header!");
    411         }
    412 
    413         return authorization;
    414     }
    415     /**
    416      * Removes all via headers from <tt>request</tt> and replaces them with a new one, equal to
    417      * the one that was top most.
    418      *
    419      * @param request the Request whose branchID we'd like to remove.
    420      *
    421      */
    422     private void removeBranchID(Request request) {
    423 
    424         ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
    425 
    426         viaHeader.removeParameter("branch");
    427 
    428     }
    429 
    430     /*
    431      * (non-Javadoc)
    432      *
    433      * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#attachAuthenticationHeaders(javax.sip.message.Request)
    434      */
    435     public void setAuthenticationHeaders(Request request) {
    436         SIPRequest sipRequest = (SIPRequest) request;
    437 
    438         String callId = sipRequest.getCallId().getCallId();
    439 
    440         request.removeHeader(AuthorizationHeader.NAME);
    441         Collection<AuthorizationHeader> authHeaders = this.cachedCredentials
    442                 .getCachedAuthorizationHeaders(callId);
    443         if (authHeaders == null) {
    444         	if (sipStack.isLoggingEnabled())
    445         		sipStack.getStackLogger().logDebug(
    446                     "Could not find authentication headers for " + callId);
    447             return;
    448         }
    449 
    450         for (AuthorizationHeader authHeader : authHeaders) {
    451             request.addHeader(authHeader);
    452         }
    453 
    454     }
    455 
    456     /*
    457      * (non-Javadoc)
    458      *
    459      * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#removeCachedAuthenticationHeaders(java.lang.String)
    460      */
    461     public void removeCachedAuthenticationHeaders(String callId) {
    462         if (callId == null)
    463             throw new NullPointerException("Null callId argument ");
    464         this.cachedCredentials.removeAuthenticationHeader(callId);
    465 
    466     }
    467 
    468 }
    469