View Javadoc
1   /*
2    * Copyright (c) 2002-2021, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.oauth2.web;
35  
36  import java.io.IOException;
37  import java.io.Serializable;
38  import java.io.UnsupportedEncodingException;
39  import java.math.BigInteger;
40  import java.net.URLEncoder;
41  import java.security.NoSuchAlgorithmException;
42  import java.security.SecureRandom;
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.Iterator;
46  import java.util.List;
47  
48  import javax.servlet.http.HttpServletRequest;
49  import javax.servlet.http.HttpServletResponse;
50  import javax.servlet.http.HttpSession;
51  
52  import org.apache.commons.lang3.StringUtils;
53  import org.apache.log4j.Logger;
54  
55  import fr.paris.lutece.plugins.oauth2.business.AuthClientConf;
56  import fr.paris.lutece.plugins.oauth2.business.AuthServerConf;
57  import fr.paris.lutece.plugins.oauth2.business.Token;
58  import fr.paris.lutece.plugins.oauth2.dataclient.DataClient;
59  import fr.paris.lutece.plugins.oauth2.jwt.JWTParser;
60  import fr.paris.lutece.plugins.oauth2.jwt.TokenValidationException;
61  import fr.paris.lutece.plugins.oauth2.service.DataClientService;
62  import fr.paris.lutece.plugins.oauth2.service.PkceUtil;
63  import fr.paris.lutece.plugins.oauth2.service.TokenService;
64  import fr.paris.lutece.portal.service.util.AppLogService;
65  import fr.paris.lutece.portal.service.util.AppPathService;
66  import fr.paris.lutece.portal.service.util.AppPropertiesService;
67  import fr.paris.lutece.util.httpaccess.HttpAccessException;
68  import fr.paris.lutece.util.url.UrlItem;
69  
70  /**
71   * CallbackHandler
72   */
73  public class CallbackHandler implements Serializable
74  {
75      private static final String PROPERTY_ERROR_PAGE = "oauth2.error.page";
76      private static final long serialVersionUID = 1L;
77      private static Logger _logger = Logger.getLogger( Constants.LOGGER_OAUTH2 );
78      private String _handlerName;
79      private AuthServerConf _authServerConf;
80      private AuthClientConf _authClientConf;
81      private JWTParser _jWTParser;
82      private boolean _bDefault;
83  
84      /**
85       * @return the authServerConf
86       */
87      public AuthServerConf getAuthServerConf( )
88      {
89          return _authServerConf;
90      }
91  
92      /**
93       * @param authServerConf
94       *            the authServerConf to set
95       */
96      public void setAuthServerConf( AuthServerConf authServerConf )
97      {
98          _authServerConf = authServerConf;
99      }
100 
101     /**
102      * @return the authClientConf
103      */
104     public AuthClientConf getAuthClientConf( )
105     {
106         return _authClientConf;
107     }
108 
109     /**
110      * @param authClientConf
111      *            the authClientConf to set
112      */
113     public void setAuthClientConf( AuthClientConf authClientConf )
114     {
115         _authClientConf = authClientConf;
116     }
117 
118     /**
119      * Handle the callback
120      * 
121      * @param request
122      *            The HTTP request
123      * @param response
124      *            The HTTP response
125      */
126     void handle( HttpServletRequest request, HttpServletResponse response )
127     {
128         String strError = request.getParameter( Constants.PARAMETER_ERROR );
129         String strCode = request.getParameter( Constants.PARAMETER_CODE );
130 
131         if ( strError != null )
132         {
133             handleError( request, response, strError );
134         }
135         else
136             if ( strCode != null )
137             {
138                 handleAuthorizationCodeResponse( request, response );
139             }
140             else
141             {
142                 handleAuthorizationRequest( request, response );
143             }
144     }
145 
146     /**
147      * Handle an error
148      *
149      * @param request
150      *            The HTTP request
151      * @param response
152      *            The HTTP response
153      * @param strError
154      *            The Error message
155      */
156     private void handleError( HttpServletRequest request, HttpServletResponse response, String strError )
157     {
158         DataClient dataClient = DataClientService.instance( ).getClient( request );
159 
160         if ( dataClient != null )
161         {
162             // handle error of the data client
163             dataClient.handleError( request, response, strError );
164         }
165         else
166         {
167             // default method if there is no dataclient
168             try
169             {
170                 UrlItem url = new UrlItem( AppPathService.getBaseUrl( request ) + AppPropertiesService.getProperty( PROPERTY_ERROR_PAGE ) );
171                 url.addParameter( Constants.PARAMETER_ERROR, strError );
172                 _logger.info( strError );
173                 response.sendRedirect( url.getUrl( ) );
174             }
175             catch( IOException ex )
176             {
177 
178                 _logger.error( "Error redirecting to the error page : " + ex.getMessage( ), ex );
179             }
180         }
181     }
182 
183     /**
184      * Handle an authorization request to obtain an authorization code
185      * 
186      * @param request
187      *            The HTTP request
188      * @param response
189      *            The HTTP response
190      */
191     private void handleAuthorizationRequest( HttpServletRequest request, HttpServletResponse response )
192     {
193         try
194         {
195             HttpSession session = request.getSession( true );
196 
197             DataClient dataClient = DataClientService.instance( ).getClient( request );
198 
199             UrlItem url = new UrlItem( _authServerConf.getAuthorizationEndpointUri( ) );
200             url.addParameter( Constants.PARAMETER_CLIENT_ID, _authClientConf.getClientId( ) );
201             url.addParameter( Constants.PARAMETER_RESPONSE_TYPE, Constants.RESPONSE_TYPE_CODE );
202             url.addParameter( Constants.PARAMETER_REDIRECT_URI, URLEncoder.encode( generateRedirectUrl( request, dataClient ), "UTF-8" ) );
203             url.addParameter( Constants.PARAMETER_SCOPE, dataClient.getScopes( ) );
204             url.addParameter( Constants.PARAMETER_STATE, createState( session ) );
205             url.addParameter( Constants.PARAMETER_NONCE, createNonce( session ) );
206             if(_authClientConf.isPkce())
207             {
208             	String strCodeVerifier=createCodeVerifier(session);
209             	String strCodeChallenge=createCodeChallenge(strCodeVerifier, session);
210             	url.addParameter(Constants.PARAMETER_CODE_CHALLENGE,strCodeChallenge);
211             	url.addParameter(Constants.PARAMETER_CODE_CHALLENGE_METHOD,"S256");
212             }
213             
214             
215             
216 
217             addComplementaryParameters( url, request );
218             addBackPromptUrl(url, request);
219             
220             String strAcrValues = dataClient.getAcrValues( );
221             if ( strAcrValues != null )
222             {
223                 url.addParameter( Constants.PARAMETER_ACR_VALUES, strAcrValues );
224             }
225 
226             String strUrl = url.getUrl( );
227             _logger.debug( "OAuth request : " + strUrl );
228             response.sendRedirect( strUrl );
229         }
230         catch( IOException ex )
231         {
232             String strError = "Error retrieving an authorization code : " + ex.getMessage( );
233             _logger.error( strError, ex );
234             handleError( request, response, Constants.ERROR_TYPE_RETRIEVING_AUTHORIZATION_CODE );
235         }
236     }
237 
238     /**
239      * Handle an request that contains an authorization code
240      *
241      * @param request
242      *            The HTTP request
243      * @param response
244      *            The HTTP response
245      */
246     private void handleAuthorizationCodeResponse( HttpServletRequest request, HttpServletResponse response )
247     {
248         String strCode = request.getParameter( Constants.PARAMETER_CODE );
249 
250         _logger.info( "OAuth Authorization code received : " + strCode );
251 
252         // Check valid state
253         if ( !checkState( request ) )
254         {
255             handleError( request, response, Constants.ERROR_TYPE_INVALID_STATE );
256 
257             return;
258         }
259 
260         try
261         {
262             HttpSession session = request.getSession( );
263             DataClient dataClient = DataClientService.instance( ).getClient( request );
264             String strRedirectUri = generateRedirectUrl( request, dataClient );
265             Token token = getToken( strRedirectUri, strCode, session );
266             dataClient.handleToken( token, request, response );
267         }
268         catch( IOException ex )
269         {
270             String strError = "Error retrieving token : " + ex.getMessage( );
271             _logger.error( strError, ex );
272             handleError( request, response, strError );
273         }
274         catch( HttpAccessException ex )
275         {
276             String strError = "Error retrieving token : " + ex.getMessage( );
277             _logger.error( strError, ex );
278             handleError( request, response, strError );
279         }
280         catch( TokenValidationException ex )
281         {
282             String strError = "Error retrieving token : " + ex.getMessage( );
283             _logger.error( strError, ex );
284             handleError( request, response, strError );
285         }
286     }
287 
288     /**
289      * Retieve a token using an authorization code
290      * 
291      * @param the
292      *            client redirect Uri
293      * @param strAuthorizationCode
294      *            The authorization code
295      * @param session
296      *            The HTTP session
297      * @return The token
298      * @throws IOException
299      *             if an error occurs
300      * @throws HttpAccessException
301      *             if an error occurs
302      * @throws TokenValidationException
303      *             If the token validation failed
304      */
305     private Token getToken( String strRedirectUri, String strAuthorizationCode, HttpSession session )
306             throws IOException, HttpAccessException, TokenValidationException
307     {
308 
309         return TokenService.getService( ).getToken( strRedirectUri, _authClientConf, _authServerConf, strAuthorizationCode, session, _jWTParser,
310                 getStoredNonce( session ),getStoredCodeVerifier(session) );
311 
312     }
313 
314     ////////////////////////////////////////////////////////////////////////////
315     // Check and trace utils
316 
317     /**
318      * Create a cryptographically random nonce and store it in the session
319      *
320      * @param session
321      *            The session
322      * @return The nonce
323      */
324     private String createNonce( HttpSession session )
325     {
326         String nonce = new BigInteger( 128, new SecureRandom( ) ).toString( 16 );
327         session.setAttribute( getNonceAttributeSessionName( ), nonce );
328 
329         return nonce;
330     }
331     
332     
333     /**
334      * Create a cryptographically random nonce and store it in the session
335      *
336      * @param session
337      *            The session
338      * @return The nonce
339      */
340     private String createCodeChallenge(String strCodeVerifier,HttpSession session )
341     {
342         
343     	String strChallenge=null;
344 		try {
345 			if(strCodeVerifier!=null)
346 			{
347 				strChallenge = PkceUtil.generateCodeChallenge(strCodeVerifier);
348 			}
349 		} catch (UnsupportedEncodingException|NoSuchAlgorithmException  e) {
350 			AppLogService.error(e);
351 		}
352     			
353         session.setAttribute( getCodeChallengeAttributeSessionName(), strChallenge );
354 
355         return strChallenge;
356     }
357     
358     /**
359      * Create a cryptographically random nonce and store it in the session
360      *
361      * @param session
362      *            The session
363      * @return The nonce
364      */
365     private String createCodeVerifier( HttpSession session )
366     {
367         String StrCodeVerifier=null;
368 		try {
369 			StrCodeVerifier = PkceUtil.generateCodeVerifier();
370 		} catch (UnsupportedEncodingException e) {
371 			AppLogService.error(e);
372 		}
373         session.setAttribute( getCodeVerifierAttributeSessionName(), StrCodeVerifier );
374 
375         return StrCodeVerifier;
376     }
377     
378     
379 
380     /**
381      * Get the store code verifier 
382      *
383      * @param session
384      *            The session
385      * @return The stored nonce
386      */
387     private String getStoredCodeVerifier( HttpSession session )
388     {
389         return getStoredSessionString( session, getCodeVerifierAttributeSessionName() );
390     }
391     
392     
393     /**
394      * Get the store code verifier 
395      *
396      * @param session
397      *            The session
398      * @return The stored nonce
399      */
400     private String getStoredCodeChallenge( HttpSession session )
401     {
402         return getStoredSessionString( session, getCodeChallengeAttributeSessionName() );
403     }
404     
405     
406     
407     
408     /**
409      * Get the nonce we stored in the session
410      *
411      * @param session
412      *            The session
413      * @return The stored nonce
414      */
415     private String getStoredNonce( HttpSession session )
416     {
417         return getStoredSessionString( session, getNonceAttributeSessionName( ) );
418     }
419     
420     
421 
422     /**
423      * 
424      * @return the attribute name who store nonced
425      */
426     private String getNonceAttributeSessionName( )
427     {
428 
429         return getHandlerName( ) == null ? Constants.NONCE_SESSION_VARIABLE : getHandlerName( ) + Constants.NONCE_SESSION_VARIABLE;
430     }
431     
432     /**
433      * 
434      * @return the attribute name who store the verifier code
435      */
436     private String getCodeVerifierAttributeSessionName( )
437     {
438 
439         return getHandlerName( ) == null ? Constants.CODE_VERIFIER_SESSION_VARIABLE : getHandlerName( ) + Constants.CODE_VERIFIER_SESSION_VARIABLE;
440     }
441     
442     
443     /**
444      * 
445      * @return the attribute name who store the challenge attribute
446      */
447     private String getCodeChallengeAttributeSessionName( )
448     {
449 
450         return getHandlerName( ) == null ? Constants.CODE_CHALLENGE_SESSION_VARIABLE : getHandlerName( ) + Constants.CODE_CHALLENGE_SESSION_VARIABLE;
451     }
452 
453     
454     
455 
456     /**
457      * check state returned by Oauth2 to the callback uri
458      * 
459      * @param request
460      *            The HTTP request
461      * @return True if the state is valid
462      */
463     private boolean checkState( HttpServletRequest request )
464     {
465         String strState = request.getParameter( Constants.PARAMETER_STATE );
466         HttpSession session = request.getSession( );
467         String strStored = getStoredState( session );
468         boolean bCheck = ( ( strState == null ) || strState.equals( strStored ) );
469 
470         if ( !bCheck )
471         {
472             _logger.debug( "Bad state returned by server : " + strState + " while expecting : " + strStored );
473         }
474 
475         return bCheck;
476     }
477 
478     /**
479      * Create a cryptographically random state and store it in the session
480      *
481      * @param session
482      *            The session
483      * @return The state
484      */
485     private String createState( HttpSession session )
486     {
487         String strState = new BigInteger( 128, new SecureRandom( ) ).toString( 16 );
488         session.setAttribute( getStateAttributeSessionName( ), strState );
489 
490         return strState;
491     }
492 
493     /**
494      * 
495      * @return the attribute name who store the sate
496      */
497     private String getStateAttributeSessionName( )
498     {
499 
500         return getHandlerName( ) == null ? Constants.STATE_SESSION_VARIABLE : getHandlerName( ) + Constants.STATE_SESSION_VARIABLE;
501     }
502 
503     /**
504      * Get the state we stored in the session
505      *
506      * @param session
507      *            The session
508      * @return The stored state
509      */
510     private String getStoredState( HttpSession session )
511     {
512         return getStoredSessionString( session, getStateAttributeSessionName( ) );
513     }
514 
515     /**
516      * Get the named stored session variable as a string. Return null if not found or not a string.
517      *
518      * @param session
519      *            The session
520      * @param strKey
521      *            The key
522      * @return The session string
523      */
524     private static String getStoredSessionString( HttpSession session, String strKey )
525     {
526         Object object = session.getAttribute( strKey );
527 
528         if ( ( object != null ) && object instanceof String )
529         {
530             return object.toString( );
531         }
532         else
533         {
534             return null;
535         }
536     }
537 
538     /**
539      * get the handler Name
540      * 
541      * @return the handler name
542      */
543     public String getHandlerName( )
544     {
545         return _handlerName;
546     }
547 
548     /**
549      * set the handler name
550      * 
551      * @param _handlerName
552      *            specify the handler Name
553      */
554     public void setHandlerName( String _handlerName )
555     {
556         this._handlerName = _handlerName;
557     }
558 
559     /**
560      * 
561      * @return JWTParser
562      */
563     public JWTParser getJWTParser( )
564     {
565         return _jWTParser;
566     }
567 
568     /**
569      * 
570      * @param jWTParser
571      *            set JwtParser
572      */
573     public void setJWTParser( JWTParser jWTParser )
574     {
575         this._jWTParser = jWTParser;
576     }
577 
578     /**
579      * 
580      * @return true if the handler is the default handler
581      */
582     public boolean isDefault( )
583     {
584         return _bDefault;
585     }
586 
587     /**
588      * 
589      * @param _bDefault
590      *            true if the handler is the default handler
591      */
592     public void setDefault( boolean _bDefault )
593     {
594         this._bDefault = _bDefault;
595     }
596 
597     private void addComplementaryParameters( UrlItem url, HttpServletRequest request )
598     {
599         String[] strComplementaryParams = request.getParameterValues( Constants.PARAMETER_COMPLEMENTARY_PARAMETER );
600         String strParamValue;
601         String strParamCode;
602        
603         if ( strComplementaryParams!=null && strComplementaryParams.length > 0  )
604         {
605         	for (int i = 0; i < strComplementaryParams.length; i++) {
606         	
607         		if(strComplementaryParams[i].contains( "=" )) 
608         		{
609 	        		 strParamCode = strComplementaryParams[i].split( "=" ) [0];
610 	                 strParamValue = strComplementaryParams[i].substring( strComplementaryParams[i].indexOf( "=" ) + 1, strComplementaryParams[i].length( ) );
611 	                 try
612 	                 {
613 	                     strParamValue = URLEncoder.encode( strParamValue, "UTF-8" );
614 	                 }
615 	                 catch( UnsupportedEncodingException e )
616 	                 {
617 	                     _logger.error( "error during urlEncode of param" + strParamValue, e );
618 	                 }
619 	                 url.addParameter( strParamCode, strParamValue );
620         		}
621         		
622         	}
623         	
624         }
625 
626     }
627     
628     
629     
630     private void addBackPromptUrl( UrlItem url, HttpServletRequest request )
631     {
632         String strBackPromptUrl= request.getParameter( Constants.PARAMETER_BACK_PROMPT_URL);
633   
634         if ( !StringUtils.isEmpty( strBackPromptUrl) )
635         {
636             try
637             {
638             	
639             	  url.addParameter( Constants.PARAMETER_BACK_PROMPT_URL, URLEncoder.encode( strBackPromptUrl, "UTF-8" ) );
640         
641             }
642             catch( UnsupportedEncodingException e )
643             {
644                 _logger.error( "error during urlEncode of param " +Constants.PARAMETER_BACK_PROMPT_URL  + strBackPromptUrl, e );
645             }
646           
647         }
648 
649     }
650     
651 
652     /**
653      * Generate Redirect Url
654      * 
655      * @param request
656      *            the httpServletRequest
657      * @param dataClient
658      *            the dataCleint
659      * @return the
660      * 
661      */
662     private String generateRedirectUrl( HttpServletRequest request, DataClient dataClient )
663     {
664         String stRedirectUrl = _authClientConf.getRedirectUri( );
665         UrlItem url = new UrlItem(stRedirectUrl);
666         
667         if ( stRedirectUrl == null )
668         {
669             stRedirectUrl = DataClientService.instance( ).getDataClientUrl( request, dataClient.getName( ), getHandlerName( ) );
670             url=new UrlItem(stRedirectUrl);
671         
672         }
673         else
674         {
675         	
676             // add dataclient and handler name parameter
677            url.addParameter(Constants.PARAMETER_DATA_CLIENT, dataClient.getName( ));
678            if ( getHandlerName( ) != null )
679             {
680         	   url.addParameter(Constants.PARAMETER_HANDLER_NAME,  getHandlerName( ));
681                 
682             }
683         }
684         
685         String strBackPromptUrl= request.getParameter( Constants.PARAMETER_BACK_PROMPT_URL);
686         
687         if ( !StringUtils.isEmpty( strBackPromptUrl) )
688         {
689         	
690 			try {
691 				
692 				  url.addParameter(Constants.PARAMETER_BACK_PROMPT_URL,URLEncoder.encode(strBackPromptUrl, "UTF-8"));
693 			
694 			} catch (UnsupportedEncodingException e) {
695 				_logger.error(
696 						"error during urlEncode of param " + Constants.PARAMETER_BACK_PROMPT_URL + strBackPromptUrl, e);
697 			}
698         	
699         }
700         
701         return url.getUrl();
702     }
703 
704 }