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.plugins.core.impl.authentication;
017
018import java.util.Collections;
019import java.util.List;
020import java.util.Map;
021import java.util.StringTokenizer;
022
023import org.apache.avalon.framework.context.Context;
024import org.apache.avalon.framework.context.ContextException;
025import org.apache.avalon.framework.context.Contextualizable;
026import org.apache.cocoon.components.ContextHelper;
027import org.apache.cocoon.environment.Redirector;
028import org.apache.cocoon.environment.Request;
029import org.apache.commons.codec.binary.Base64;
030import org.apache.commons.lang3.StringUtils;
031
032import org.ametys.core.authentication.AbstractCredentialProvider;
033import org.ametys.core.authentication.AuthenticateAction;
034import org.ametys.core.authentication.BlockingCredentialProvider;
035import org.ametys.core.user.UserIdentity;
036import org.ametys.core.user.directory.UserDirectory;
037import org.ametys.core.user.directory.UserDirectory.CredentialsResult;
038import org.ametys.core.user.population.UserPopulation;
039import org.ametys.runtime.authentication.AuthorizationRequiredException;
040
041/**
042 * Basic http authentication.
043 */
044public class BasicCredentialProvider extends AbstractCredentialProvider implements BlockingCredentialProvider, Contextualizable
045{
046    /** Name of the parameter holding the authentication realm */
047    protected static final String __PARAM_REALM = "runtime.authentication.basic.realm";
048    
049    private static final String BASIC_AUTHENTICATION_KEY = "BASIC ";
050
051    /** The realm */
052    protected String _realm;
053    
054    private Context _context;
055
056    @Override
057    public void contextualize(Context context) throws ContextException
058    {
059        _context = context;
060    }
061    
062    @Override
063    public void init(String id, String cpModelId, Map<String, Object> paramValues, String label) throws Exception
064    {
065        super.init(id, cpModelId, paramValues, label);
066        _realm = (String) paramValues.get(__PARAM_REALM);
067    }
068    
069    @Override
070    public boolean blockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception
071    {
072        // this manager is always valid
073        return true;
074    }
075    
076    @Override
077    public boolean blockingGrantAnonymousRequest()
078    {
079        // this implementation does not have any particular request
080        // to take into account
081        return false;
082    }
083
084    @Override
085    public UserIdentity blockingGetUserIdentity(Redirector redirector) throws Exception
086    {
087        Request request = ContextHelper.getRequest(_context);
088        
089        // Check authentication header
090        String auth = request.getHeader("Authorization");
091        if (auth == null)
092        {
093            // no auth
094            throw new AuthorizationRequiredException(_realm);
095        }
096
097        if (!auth.toUpperCase().startsWith(BASIC_AUTHENTICATION_KEY))
098        {
099            // we only do BASIC
100            return null;
101        }
102
103        // Get encoded user and password, comes after "BASIC "
104        String userpassEncoded = auth.substring(BASIC_AUTHENTICATION_KEY.length());
105
106        // Decode it, using any base 64 decoder
107        String userpassDecoded = new String(Base64.decodeBase64(userpassEncoded.getBytes("UTF-8")), "UTF-8");
108
109        // Login and password are separated with a :
110        StringTokenizer stk = new StringTokenizer(userpassDecoded, ":");
111        String login = stk.hasMoreTokens() ? stk.nextToken() : "";
112        String password = stk.hasMoreTokens() ? stk.nextToken() : "";
113
114        // Let's check password
115        List<UserPopulation> userPopulations = _getPopulations(request);
116        for (UserPopulation userPopulation : userPopulations)
117        {
118            for (UserDirectory userDirectory : userPopulation.getUserDirectories())
119            {
120                if (userDirectory.getUser(login) != null)
121                {
122                    if (userDirectory.checkCredentials(login, password) != CredentialsResult.NOT_AUTHENTICATED)
123                    {
124                        return new UserIdentity(login, userPopulation.getId());
125                    }
126                    else
127                    {
128                        throw new AuthorizationRequiredException(_realm);
129                    }
130                }
131            }
132        }
133        
134        throw new AuthorizationRequiredException(_realm);
135    }
136    
137    private List<UserPopulation> _getPopulations(Request request)
138    {
139        // With forms, the population should always be known!
140        @SuppressWarnings("unchecked")
141        List<UserPopulation> userPopulations = (List<UserPopulation>) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST);
142
143        // In this list a population was maybe chosen?
144        final String chosenUserPopulationId = (String) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_USER_POPULATION_ID);
145        if (StringUtils.isNotBlank(chosenUserPopulationId))
146        {
147            UserPopulation chosenUserPopulation = userPopulations.stream().filter(userPopulation -> StringUtils.equals(userPopulation.getId(), chosenUserPopulationId)).findFirst().get();
148            return Collections.singletonList(chosenUserPopulation);
149        }
150        
151        // If the list has one element only...
152        return userPopulations;
153    }
154
155    @Override
156    public void blockingUserNotAllowed(Redirector redirector)
157    {
158        // empty method, nothing more to do
159    }
160
161    @Override
162    public void blockingUserAllowed(UserIdentity userIdentity, Redirector redirector)
163    {
164        // empty method, nothing more to do
165    }
166
167    @Override
168    public boolean requiresNewWindow()
169    {
170        return false;
171    }
172}