View Javadoc
1   /*
2    * Copyright (c) 2002-2015, Mairie de 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.franceconnect.web;
35  
36  import fr.paris.lutece.plugins.franceconnect.oidc.AuthClientConf;
37  import fr.paris.lutece.plugins.franceconnect.oidc.AuthServerConf;
38  import fr.paris.lutece.plugins.franceconnect.oidc.Token;
39  import fr.paris.lutece.plugins.franceconnect.oidc.dataclient.DataClient;
40  import fr.paris.lutece.plugins.franceconnect.oidc.jwt.TokenValidationException;
41  import fr.paris.lutece.plugins.franceconnect.service.DataClientService;
42  import fr.paris.lutece.plugins.franceconnect.service.TokenService;
43  import fr.paris.lutece.portal.service.util.AppPathService;
44  import fr.paris.lutece.portal.service.util.AppPropertiesService;
45  import fr.paris.lutece.util.httpaccess.HttpAccess;
46  import fr.paris.lutece.util.httpaccess.HttpAccessException;
47  import fr.paris.lutece.util.url.UrlItem;
48  
49  import org.apache.log4j.Logger;
50  
51  import java.io.IOException;
52  
53  import java.math.BigInteger;
54  
55  import java.net.URLEncoder;
56  
57  import java.security.SecureRandom;
58  
59  import java.util.Map;
60  import java.util.concurrent.ConcurrentHashMap;
61  
62  import javax.servlet.http.HttpServletRequest;
63  import javax.servlet.http.HttpServletResponse;
64  import javax.servlet.http.HttpSession;
65  
66  
67  /**
68   * CallbackHandler
69   */
70  public class CallbackHandler
71  {
72      private static final String PROPERTY_ERROR_PAGE = "franceconnect.error.page";
73      private static final long serialVersionUID = 1L;
74      private static Logger _logger = Logger.getLogger( Constants.LOGGER_FRANCECONNECT );
75      private AuthServerConf _authServerConf;
76      private AuthClientConf _authClientConf;
77  
78      /**
79       * @return the authServerConf
80       */
81      public AuthServerConf getAuthServerConf(  )
82      {
83          return _authServerConf;
84      }
85  
86      /**
87       * @param authServerConf the authServerConf to set
88       */
89      public void setAuthServerConf( AuthServerConf authServerConf )
90      {
91          _authServerConf = authServerConf;
92      }
93  
94      /**
95       * @return the authClientConf
96       */
97      public AuthClientConf getAuthClientConf(  )
98      {
99          return _authClientConf;
100     }
101 
102     /**
103      * @param authClientConf the authClientConf to set
104      */
105     public void setAuthClientConf( AuthClientConf authClientConf )
106     {
107         _authClientConf = authClientConf;
108     }
109 
110     /**
111      * Handle the callback
112      * @param request The HTTP request
113      * @param response The HTTP response
114      */
115     void handle( HttpServletRequest request, HttpServletResponse response )
116     {
117         String strError = request.getParameter( Constants.PARAMETER_ERROR );
118         String strCode = request.getParameter( Constants.PARAMETER_CODE );
119 
120         if ( strError != null )
121         {
122             handleError( request, response, strError );
123         }
124         else if ( strCode != null )
125         {
126             handleAuthorizationCodeResponse( request, response );
127         }
128         else
129         {
130             handleAuthorizationRequest( request, response );
131         }
132     }
133 
134     /**
135      * Handle an error
136      *
137      * @param request The HTTP request
138      * @param response The HTTP response
139      * @param strError The Error message
140      */
141     private void handleError( HttpServletRequest request, HttpServletResponse response, String strError )
142     {
143         try
144         {
145             UrlItem url = new UrlItem( AppPathService.getBaseUrl( request ) +
146                     AppPropertiesService.getProperty( PROPERTY_ERROR_PAGE ) );
147             url.addParameter( Constants.PARAMETER_ERROR, strError );
148             _logger.info( strError );
149             response.sendRedirect( url.getUrl(  ) );
150         }
151         catch ( IOException ex )
152         {
153             _logger.error( "Error redirecting to the error page : " + ex.getMessage(  ), ex );
154         }
155     }
156 
157     /**
158      * Handle an authorization request to obtain an authorization code
159      * @param request The HTTP request
160      * @param response The HTTP response
161      */
162     private void handleAuthorizationRequest( HttpServletRequest request, HttpServletResponse response )
163     {
164         try
165         {
166             HttpSession session = request.getSession( true );
167 
168             String strDataClientName = request.getParameter( Constants.PARAMETER_DATA_CLIENT );
169             DataClient dataClient = DataClientService.instance(  ).getClient( strDataClientName );
170             session.setAttribute( Constants.SESSION_ATTRIBUTE_DATACLIENT, dataClient );
171 
172             UrlItem url = new UrlItem( _authServerConf.getAuthorizationEndpointUri(  ) );
173             url.addParameter( Constants.PARAMETER_CLIENT_ID, _authClientConf.getClientId(  ) );
174             url.addParameter( Constants.PARAMETER_RESPONSE_TYPE, Constants.RESPONSE_TYPE_CODE );
175             url.addParameter( Constants.PARAMETER_REDIRECT_URI,
176                 URLEncoder.encode( _authClientConf.getRedirectUri(  ), "UTF-8" ) );
177             url.addParameter( Constants.PARAMETER_SCOPE, dataClient.getScopes(  ) );
178             url.addParameter( Constants.PARAMETER_STATE, createState( session ) );
179             url.addParameter( Constants.PARAMETER_NONCE, createNonce( session ) );
180             
181             String strAcrValues = dataClient.getAcrValues();
182             if( strAcrValues != null )
183             {
184                 url.addParameter( Constants.PARAMETER_ACR_VALUES, strAcrValues );
185             }
186 
187             String strUrl = url.getUrl(  );
188             _logger.debug( "OAuth request : " + strUrl );
189             response.sendRedirect( strUrl );
190         }
191         catch ( IOException ex )
192         {
193             String strError = "Error retrieving an authorization code : " + ex.getMessage(  );
194             _logger.error( strError, ex );
195             handleError( request, response, strError );
196         }
197     }
198 
199     /**
200      * Handle an request that contains an authorization code
201      *
202      * @param request The HTTP request
203      * @param response The HTTP response
204      */
205     private void handleAuthorizationCodeResponse( HttpServletRequest request, HttpServletResponse response )
206     {
207         String strCode = request.getParameter( Constants.PARAMETER_CODE );
208         _logger.info( "OAuth Authorization code received : " + strCode );
209 
210         // Check valid state
211         if ( !checkState( request ) )
212         {
213             handleError( request, response, "Invalid state returned by FranceConnect !" );
214 
215             return;
216         }
217 
218         try
219         {
220             HttpSession session = request.getSession(  );
221             Token token = getToken( strCode, session );
222             DataClient dataClient = (DataClient) session.getAttribute( Constants.SESSION_ATTRIBUTE_DATACLIENT );
223             dataClient.handleToken( token , request , response );
224         }
225         catch ( IOException ex )
226         {
227             String strError = "Error retrieving token : " + ex.getMessage(  );
228             _logger.error( strError, ex );
229             handleError( request, response, strError );
230         }
231         catch ( HttpAccessException ex )
232         {
233             String strError = "Error retrieving token : " + ex.getMessage(  );
234             _logger.error( strError, ex );
235             handleError( request, response, strError );
236         }
237         catch ( TokenValidationException ex )
238         {
239             String strError = "Error retrieving token : " + ex.getMessage(  );
240             _logger.error( strError, ex );
241             handleError( request, response, strError );
242         }
243     }
244 
245     /**
246      * Retieve a token using an authorization code
247      * @param strAuthorizationCode The authorization code
248      * @param session The HTTP session
249      * @return The token
250      * @throws IOException if an error occurs
251      * @throws HttpAccessException if an error occurs
252      * @throws TokenValidationException If the token validation failed
253      */
254     private Token getToken( String strAuthorizationCode, HttpSession session )
255         throws IOException, HttpAccessException, TokenValidationException
256     {
257         String strRedirectUri = _authClientConf.getRedirectUri(  );
258         Map<String, String> mapParameters = new ConcurrentHashMap<String, String>(  );
259         mapParameters.put( Constants.PARAMETER_GRANT_TYPE, Constants.GRANT_TYPE_CODE );
260         mapParameters.put( Constants.PARAMETER_CODE, strAuthorizationCode );
261         mapParameters.put( Constants.PARAMETER_CLIENT_ID, _authClientConf.getClientId(  ) );
262         mapParameters.put( Constants.PARAMETER_CLIENT_SECRET, _authClientConf.getClientSecret(  ) );
263 
264         if ( strRedirectUri != null )
265         {
266             mapParameters.put( Constants.PARAMETER_REDIRECT_URI, strRedirectUri );
267         }
268 
269         HttpAccess httpAccess = new HttpAccess(  );
270         String strUrl = _authServerConf.getTokenEndpointUri(  );
271 
272         _logger.debug( "Posted URL : " + strUrl + "\nParameters :\n" + traceMap( mapParameters ) );
273 
274         String strResponse = httpAccess.doPost( strUrl, mapParameters );
275         _logger.debug( "FranceConnect response : " + strResponse );
276 
277         return TokenService.parse( strResponse, _authClientConf, _authServerConf, getStoredNonce( session ) );
278     }
279 
280     ////////////////////////////////////////////////////////////////////////////
281     // Check and trace utils
282 
283     /**
284      * Create a cryptographically random nonce and store it in the session
285      *
286      * @param session The session
287      * @return The nonce
288      */
289     private static String createNonce( HttpSession session )
290     {
291         String nonce = new BigInteger( 50, new SecureRandom(  ) ).toString( 16 );
292         session.setAttribute( Constants.NONCE_SESSION_VARIABLE, nonce );
293 
294         return nonce;
295     }
296 
297     /**
298      * Get the nonce we stored in the session
299      *
300      * @param session The session
301      * @return The stored nonce
302      */
303     private static String getStoredNonce( HttpSession session )
304     {
305         return getStoredSessionString( session, Constants.NONCE_SESSION_VARIABLE );
306     }
307 
308     /**
309      * check state returned by FranceConnect to the callback uri
310      * @param request The HTTP request
311      * @return True if the state is valid
312      */
313     private boolean checkState( HttpServletRequest request )
314     {
315         String strState = request.getParameter( Constants.PARAMETER_STATE );
316         HttpSession session = request.getSession(  );
317         String strStored = getStoredState( session );
318         boolean bCheck = ( ( strState == null ) || strState.equals( strStored ) );
319 
320         if ( !bCheck )
321         {
322             _logger.debug( "Bad state returned by server : " + strState + " while expecting : " + strStored );
323         }
324 
325         return bCheck;
326     }
327 
328     /**
329      * Create a cryptographically random state and store it in the session
330      *
331      * @param session The session
332      * @return The state
333      */
334     private static String createState( HttpSession session )
335     {
336         String strState = new BigInteger( 50, new SecureRandom(  ) ).toString( 16 );
337         session.setAttribute( Constants.STATE_SESSION_VARIABLE, strState );
338 
339         return strState;
340     }
341 
342     /**
343      * Get the state we stored in the session
344      *
345      * @param session The session
346      * @return The stored state
347      */
348     private static String getStoredState( HttpSession session )
349     {
350         return getStoredSessionString( session, Constants.STATE_SESSION_VARIABLE );
351     }
352 
353     /**
354      * Get the named stored session variable as a string. Return null if not
355      * found or not a string.
356      *
357      * @param session The session
358      * @param strKey The key
359      * @return The session string
360      */
361     private static String getStoredSessionString( HttpSession session, String strKey )
362     {
363         Object object = session.getAttribute( strKey );
364 
365         if ( ( object != null ) && object instanceof String )
366         {
367             return object.toString(  );
368         }
369         else
370         {
371             return null;
372         }
373     }
374 
375     /**
376      * Utils to trace map content
377      * @param map The map
378      * @return The content
379      */
380     private String traceMap( Map<String, String> map )
381     {
382         StringBuilder sbTrace = new StringBuilder(  );
383 
384         for ( Map.Entry entry : map.entrySet(  ) )
385         {
386             sbTrace.append( entry.getKey(  ) ).append( ":[" ).append( entry.getValue(  ) ).append( "]\n" );
387         }
388 
389         return sbTrace.toString(  );
390     }
391 }