001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.site;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.nio.charset.StandardCharsets;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.Enumeration;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Set;
029import java.util.regex.Pattern;
030
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
033import org.apache.avalon.framework.parameters.Parameters;
034import org.apache.cocoon.environment.ObjectModelHelper;
035import org.apache.cocoon.environment.Redirector;
036import org.apache.cocoon.environment.Request;
037import org.apache.cocoon.environment.Session;
038import org.apache.cocoon.environment.http.HttpCookie;
039import org.apache.commons.lang3.StringUtils;
040import org.apache.http.NameValuePair;
041import org.apache.http.client.entity.UrlEncodedFormEntity;
042import org.apache.http.client.methods.CloseableHttpResponse;
043import org.apache.http.client.methods.HttpGet;
044import org.apache.http.client.methods.HttpPost;
045import org.apache.http.impl.client.CloseableHttpClient;
046import org.apache.http.message.BasicNameValuePair;
047
048import org.ametys.core.authentication.AuthenticateAction;
049import org.ametys.core.authentication.CredentialProvider;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.plugins.core.user.UserDAO;
052import org.ametys.plugins.site.Site;
053import org.ametys.runtime.config.Config;
054import org.ametys.runtime.workspace.WorkspaceMatcher;
055
056/**
057 * The authenticate action for front side 
058 */
059public class FrontAuthenticateAction extends AuthenticateAction
060{
061    /** url requires for authentication */
062    protected Collection<Pattern> _acceptedSiteUrlPatterns = Arrays.asList(new Pattern[]{Pattern.compile("^plugins/site/authenticate/[0-9]+$")});
063    
064    @Override
065    protected boolean _acceptedUrl(Request request)
066    {
067        // URL without server context and leading slash.
068        String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL);
069        for (Pattern pattern : _acceptedSiteUrlPatterns)
070        {
071            if (pattern.matcher(url).matches())
072            {
073                // Anonymous request
074                request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
075
076                return true;
077            }
078        }
079        
080        return false;
081    }
082
083    @Override
084    protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
085    {
086        setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode);
087    }
088    
089    /**
090     * Save user identity in request
091     * @param request The request
092     * @param userIdentity The useridentity to save
093     * @param credentialProvider The credential provider used to connect
094     * @param blockingMode The mode used for the credential provider
095     */
096    public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
097    {
098        Site site = (Site) request.getAttribute("site");
099        String siteName = site.getName();
100        
101        Session session = renewSession(request);
102        _resetConnectingStateToSession(request);
103        session.setAttribute(SESSION_USERIDENTITY + "-" + siteName, userIdentity);
104        session.setAttribute(SESSION_CREDENTIALPROVIDER + "-" + siteName, credentialProvider);
105        session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE + "-" + siteName, blockingMode);
106    }
107    
108    @Override
109    protected UserIdentity _getUserIdentityFromSession(Request request)
110    {
111        UserIdentity userIdentityFromSession = getUserIdentityFromSession(request);
112        if (userIdentityFromSession != null)
113        {
114            return userIdentityFromSession;
115        }
116
117        Session session = request.getSession(false);
118        if (session != null)
119        {
120            Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, _getContexts(request, null));
121
122            // check if connected in the site application, not only on this site
123            userIdentityFromSession = super._getUserIdentityFromSession(request);
124            if (userIdentityFromSession != null && availableUserPopulationsIds.contains(userIdentityFromSession.getPopulationId()))
125            {
126                _setUserIdentityInSession(request, userIdentityFromSession, new UserDAO.ImpersonateCredentialProvider(), true);
127                return userIdentityFromSession;
128            }
129             
130            Enumeration<String> attributeNames = session.getAttributeNames();
131            while (attributeNames.hasMoreElements())
132            {
133                String attributeName = attributeNames.nextElement();
134                if (attributeName.startsWith(SESSION_USERIDENTITY + "-"))
135                {
136                    UserIdentity userIdentity = (UserIdentity) session.getAttribute(attributeName);
137                    
138                    if (availableUserPopulationsIds.contains(userIdentity.getPopulationId()))
139                    {
140                        _setUserIdentityInSession(request, userIdentity, new UserDAO.ImpersonateCredentialProvider(), true);
141                        return userIdentity;
142                    }
143                }
144            }
145        }
146
147        return null;
148    }
149    
150    @Override
151    protected void _handleWeakPassord(Request request, Redirector redirector, UserIdentity userIdentity) throws Exception
152    {
153        Site site = (Site) request.getAttribute("site");
154        
155        String defaultLanguage = "en";
156        Locale local = request.getLocale();
157        
158        String prefLanguage = local != null ? local.getLanguage() : defaultLanguage;
159
160        String weakPasswordUrl = site.getWeakPasswordUrl(prefLanguage);
161        if (weakPasswordUrl == null)
162        {
163            // try to get weak password from local language
164            weakPasswordUrl = site.getWeakPasswordUrl(prefLanguage);
165        }
166        
167        if (weakPasswordUrl == null)
168        {
169            // try to get weak password from default language
170            weakPasswordUrl = site.getWeakPasswordUrl(defaultLanguage);
171        }
172        
173        if (weakPasswordUrl == null)
174        {
175            for (String lang : site.getLanguages())
176            {
177                weakPasswordUrl = site.getWeakPasswordUrl(lang);
178                if (weakPasswordUrl != null)
179                {
180                    break;
181                }
182            }
183        }
184        
185        if (weakPasswordUrl != null)
186        {
187            // Force redirect to weak password url to force change password
188            getLogger().info("Password of user " + userIdentity + " does not meet the security requirements. Force to change password.");
189            redirector.redirect(false, weakPasswordUrl + "&userIdentity=" + UserIdentity.userIdentityToString(userIdentity));
190        }
191        else
192        {
193            // No weak password url is available for this site, user will be authenticated
194            super._handleWeakPassord(request, redirector, userIdentity);
195        }
196    }
197    
198    /**
199     * Get the user identity of the connected user from the session 
200     * @param request The request
201     * @return The connected useridentity or null
202     */
203    public static UserIdentity getUserIdentityFromSession(Request request)
204    {
205        Site site = (Site) request.getAttribute("site");
206        String siteName = site.getName();
207        
208        return getUserIdentityFromSession(request, siteName);
209    }
210    
211    /**
212     * Get the user identity of the connected user from the session 
213     * @param request The request
214     * @param siteName The current site name
215     * @return The connected useridentity or null
216     */
217    public static UserIdentity getUserIdentityFromSession(Request request, String siteName)
218    {
219        Session session = request.getSession(false);
220        if (session != null)
221        {
222            return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY + "-" + siteName);
223        }
224        return null;
225    }
226    
227    @Override
228    protected CredentialProvider _getCredentialProviderFromSession(Request request)
229    {
230        return getCredentialProviderFromSession(request);
231    }
232    
233    /**
234     * Get the credential provider used for the current connection
235     * @param request The request 
236     * @return The credential provider used or null
237     */
238    public static CredentialProvider getCredentialProviderFromSession(Request request)
239    {
240        Site site = (Site) request.getAttribute("site");
241        
242        if (site == null)
243        {
244            return null;
245        }
246        
247        String siteName = site.getName();
248        
249        return getCredentialProviderFromSession(request, siteName);
250    }
251    
252    /**
253     * Get the credential provider used for the current connection
254     * @param request The request 
255     * @param siteName The current site name
256     * @return The credential provider used or null
257     */
258    public static CredentialProvider getCredentialProviderFromSession(Request request, String siteName)
259    {
260        Session session = request.getSession(false);
261        if (session != null)
262        {
263            return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER + "-" + siteName);
264        }
265        return null;
266    }
267    
268    @Override
269    protected Boolean _getCredentialProviderModeFromSession(Request request)
270    {
271        return getCredentialProviderModeFromSession(request);
272    }
273    
274    /**
275     * Get the credential provider mode used for the current connection
276     * @param request The request 
277     * @return The credential provider mode used or null
278     */
279    public static Boolean getCredentialProviderModeFromSession(Request request)
280    {
281        Site site = (Site) request.getAttribute("site");
282        String siteName = site.getName();
283        
284        return getCredentialProviderModeFromSession(request, siteName);
285    }
286    
287    /**
288     * Get the credential provider mode used for the current connection
289     * @param request The request 
290     * @param siteName The current site name
291     * @return The credential provider mode used or null
292     */
293    public static Boolean getCredentialProviderModeFromSession(Request request, String siteName)
294    {
295        Session session = request.getSession(false);
296        if (session != null)
297        {
298            return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE + "-" + siteName);
299        }
300        return null;
301    }
302    
303    @Override
304    protected List<String> _getContexts(Request request, Parameters parameters)
305    {
306        Site site = (Site) request.getAttribute("site");
307        String siteName = site.getName();
308        return Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName);
309    }
310
311    @Override
312    protected String getLoginURL(Request request)
313    {
314        Site site = (Site) request.getAttribute("site");
315        String siteName = site.getName();
316        
317        return getLoginURLParameters(request, "cocoon://_generate/plugins/web/frontoffice-formbasedauthentication/login/login/" + siteName);
318    }
319    
320    @Override
321    protected String getLogoutURL(Request request)
322    {
323        Site site = (Site) request.getAttribute("site");
324        String siteName = site.getName();
325        return "cocoon://_generate/plugins/web/frontoffice-formbasedauthentication/login/logout/" + siteName;
326    }
327    
328    @Override
329    protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception
330    {
331        boolean logout = super._handleLogout(redirector, objectModel, source, parameters);
332        if (logout)
333        {
334            // Additionally we need to destroy potential server side session
335            Request request = ObjectModelHelper.getRequest(objectModel);
336            HttpCookie sessionId = (HttpCookie) request.getCookieMap().get(GeneratePageAction.__BACKOFFICE_JSESSION_ID);
337            if (sessionId != null)
338            {
339                try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient())
340                {
341                    String cmsURL = Config.getInstance().getValue("org.ametys.site.bo");
342                    HttpGet httpGet = new HttpGet(cmsURL + "/logout.html");
343                    httpGet.addHeader("Cookie", "JSESSIONID=" + sessionId.getValue());
344                    httpClient.execute(httpGet);
345                }
346            }
347        }
348        return logout;
349    }
350    
351    @Override
352    protected UserIdentity _validateToken(String token, String context)
353    {
354        // Get site names and URLs from the CMS
355        String cmsURL = Config.getInstance().getValue("org.ametys.site.bo");
356        
357        try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient())
358        {
359            HttpPost httpPost = new HttpPost(cmsURL + "/_validate_token.xml");
360            httpPost.addHeader("X-Ametys-FO", "true");
361            
362            List<NameValuePair> nvps = new ArrayList<>();
363            nvps.add(new BasicNameValuePair("token", token));
364            nvps.add(new BasicNameValuePair("tokenContext", context));
365            httpPost.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
366            
367            try (CloseableHttpResponse response = httpClient.execute(httpPost); ByteArrayOutputStream os = new ByteArrayOutputStream())
368            {
369                switch (response.getStatusLine().getStatusCode())
370                {
371                    case 200: 
372                        break;
373                    
374                    case 403: 
375                        throw new IllegalStateException("The CMS back-office refused the connection");
376                        
377                    case 500: 
378                    default:
379                        throw new IllegalStateException("The CMS back-office returned an error");
380                }
381                
382                response.getEntity().writeTo(os);
383                try (ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()))
384                {
385                    Configuration conf = new DefaultConfigurationBuilder().build(is);
386                    
387                    String login = conf.getChild("login").getValue(null);
388                    String populationId = conf.getChild("populationId").getValue(null);
389                    
390                    if (StringUtils.isNoneBlank(login, populationId))
391                    {
392                        return new UserIdentity(login, populationId);
393                    }
394                    else
395                    {
396                        return null;
397                    }
398                }
399            }
400        }
401        catch (Exception e)
402        {
403            throw new RuntimeException("Unable to synchronize site data", e);
404        }
405    }
406}