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