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