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