001/*
002 *  Copyright 2023 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.workspaces.extrausermgt.authentication.oauth;
017
018import java.net.URI;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.avalon.framework.thread.ThreadSafe;
030import org.apache.cocoon.acting.AbstractAction;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Redirector;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.environment.Session;
035import org.apache.cocoon.environment.SourceResolver;
036
037import org.ametys.plugins.extrausermgt.oauth.DefaultOauthProvider;
038import org.ametys.plugins.extrausermgt.oauth.OAuthProvider;
039import org.ametys.plugins.extrausermgt.oauth.OauthProviderExtensionPoint;
040import org.ametys.runtime.authentication.AccessDeniedException;
041
042import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
043import com.nimbusds.oauth2.sdk.AuthorizationResponse;
044import com.nimbusds.oauth2.sdk.ErrorObject;
045import com.nimbusds.oauth2.sdk.id.State;
046
047/**
048 * Action intended to manage result of an authorize request.
049 * 
050 * This action will check the response, retrieve the authorization code
051 * use it to request an access token and finally store it in session before
052 * redirecting to the original request that redirected to the authorize request
053 */
054public class OAuthCallbackAction extends AbstractAction implements ThreadSafe, Serviceable
055{
056    private OauthProviderExtensionPoint _oauthEP;
057
058    public void service(ServiceManager manager) throws ServiceException
059    {
060        _oauthEP = (OauthProviderExtensionPoint) manager.lookup(OauthProviderExtensionPoint.ROLE);
061    }
062    
063    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
064    {
065        Request request = ObjectModelHelper.getRequest(objectModel);
066        Map<String, List<String>> params = new HashMap<>();
067        for (Iterator paramIterator = request.getParameterNames().asIterator(); paramIterator.hasNext();)
068        {
069            String paramName = (String) paramIterator.next();
070            params.put(paramName, Arrays.asList(request.getParameterValues(paramName)));
071        }
072        
073        AuthorizationResponse response = AuthorizationResponse.parse(URI.create(request.getRequestURI()), params);
074        
075        Session session = request.getSession();
076        if (!response.indicatesSuccess())
077        {
078            ErrorObject errorObject = response.toErrorResponse().getErrorObject();
079            throw new AccessDeniedException("Oauth authorization request failed with http status '" + errorObject.getHTTPStatusCode()
080            + "', code '" + errorObject.getCode()
081            + "' and description '" + errorObject.getDescription() + "'.");
082        }
083        
084        // check that response match the state saved for this request
085        checkResponseIntegrity(response, session);
086        
087        // use the state to retrieve the provider linked to this response
088        OAuthProvider provider = _oauthEP.getProviderForState(response.getState());
089        
090        // get the access token and store it
091        AuthorizationCodeGrant grant = new AuthorizationCodeGrant(response.toSuccessResponse().getAuthorizationCode(), URI.create(request.getRequestURI()));
092        provider.requestAccessToken(grant);
093        
094        // we are done with the authorization process.
095        // we can now redirect to the original request requiring a protected resource
096        String originalRequest = (String) session.getAttribute(DefaultOauthProvider.OAUTH_REDIRECT_URI_SESSION_ATTRIBUTE);
097        if (!redirector.hasRedirected())
098        {
099            redirector.redirect(false, originalRequest);
100        }
101        
102        return EMPTY_MAP;
103    }
104
105    /**
106     * Check the response integrity
107     * @param response the authorize request response
108     * @param session the current session
109     */
110    protected void checkResponseIntegrity(AuthorizationResponse response, Session session)
111    {
112        State state = (State) session.getAttribute(DefaultOauthProvider.OAUTH_STATE_SESSION_ATTRIBUTE);
113        if (state == null || !state.equals(response.getState()))
114        {
115            throw new AccessDeniedException("Failed to retrieve the authorization code. Oauth state mismatch.");
116        }
117        else
118        {
119            // the session state was successfully used to unsure that the response is linked to the current session.
120            // we can now remove it as it fulfilled its purpose
121            session.removeAttribute(DefaultOauthProvider.OAUTH_STATE_SESSION_ATTRIBUTE);
122        }
123    }
124}