net.shibboleth.idp.oidc.flow.BuildAuthorizationRequestContextAction.java Source code

Java tutorial

Introduction

Here is the source code for net.shibboleth.idp.oidc.flow.BuildAuthorizationRequestContextAction.java

Source

/*
 * Licensed to the University Corporation for Advanced Internet Development, 
 * Inc. (UCAID) under one or more contributor license agreements. See the 
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID licenses this file to You under the Apache 
 * License, Version 2.0 (the "License"); you may not use this file except in 
 * compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.shibboleth.idp.oidc.flow;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import net.shibboleth.idp.oidc.OIDCException;
import net.shibboleth.idp.oidc.util.OIDCUtils;
import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.utilities.java.support.collection.Pair;
import org.apache.http.client.utils.URIBuilder;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.service.ClientDetailsEntityService;
import org.mitre.openid.connect.request.ConnectRequestParameters;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Builds an oidc authZ context message from an incoming request.
 */
public class BuildAuthorizationRequestContextAction extends AbstractProfileAction {
    /**
     * The Log.
     */
    private final Logger log = LoggerFactory.getLogger(BuildAuthorizationRequestContextAction.class);

    /**
     * The Auth request factory.
     */
    @Autowired
    private OAuth2RequestFactory authRequestFactory;

    /**
     * The Client service.
     */
    @Autowired
    private ClientDetailsEntityService clientService;

    /**
     * The Redirect resolver.
     */
    @Autowired
    private RedirectResolver redirectResolver;

    /**
     * Instantiates a new authentication context action.
     */
    public BuildAuthorizationRequestContextAction() {
    }

    @Nonnull
    @Override
    protected Event doExecute(@Nonnull final RequestContext springRequestContext,
            @Nonnull final ProfileRequestContext profileRequestContext) {
        final HttpServletRequest request = OIDCUtils.getHttpServletRequest(springRequestContext);
        if (request == null) {
            throw new OIDCException("HttpServletRequest cannot be null");
        }

        final HttpServletResponse response = OIDCUtils.getHttpServletResponse(springRequestContext);
        if (response == null) {
            throw new OIDCException("HttpServletRequest cannot be null");
        }

        final AuthorizationRequest authorizationRequest = createAuthorizationRequest(request);
        if (Strings.isNullOrEmpty(authorizationRequest.getClientId())) {
            throw new OIDCException("No client id is specified in the authorization request");
        }

        final OIDCAuthorizationRequestContext authZContext = new OIDCAuthorizationRequestContext();
        authZContext.setAuthorizationRequest(authorizationRequest);

        if (authZContext.isImplicitResponseType() && Strings.isNullOrEmpty(authZContext.getNonce())) {
            log.error("{} is required since the requesting flow is implicit");
            throw new OIDCException("{} is required when handling implicit response type");
        }

        final ClientDetailsEntity client = loadClientObject(authZContext);
        ensureRedirectUriIsAuthorized(authorizationRequest, client);

        log.debug("Found client {}.", client.getClientId());

        processLoginHintParameterIfNeeded(request, authZContext);

        Pair<Events, ? extends Object> pairEvent = new Pair<>(Events.Success, null);
        final String prompt = (String) authorizationRequest.getExtensions().get(ConnectRequestParameters.PROMPT);
        if (prompt != null) {
            log.debug("Authorization request contains prompt {}", prompt);
            pairEvent = checkForPrompts(prompt, request, client, authZContext);
        }

        return produceFinalEvent(profileRequestContext, response, authZContext, pairEvent, springRequestContext,
                client);
    }

    /**
     * Ensure redirect uri is authorized.
     *
     * @param authorizationRequest the authorization request
     * @param client               the client
     */
    private static void ensureRedirectUriIsAuthorized(final AuthorizationRequest authorizationRequest,
            final ClientDetailsEntity client) {
        if (!Strings.isNullOrEmpty(authorizationRequest.getRedirectUri())) {
            boolean found = false;
            final Iterator<String> it = client.getRedirectUris().iterator();

            while (!found && it.hasNext()) {
                found = it.next().equals(authorizationRequest.getRedirectUri());
            }
            if (!found) {
                throw new OIDCException(
                        "Redirect uri in the authorization request " + authorizationRequest.getRedirectUri()
                                + " is not registered for client " + client.getClientId());
            }
        }
    }

    /**
     * Produce final event event.
     *
     * @param profileRequestContext the profile request context
     * @param response              the response
     * @param authorizationRequest  the authorization request
     * @param pairEvent             the pair event
     * @param springRequestContext  the spring request context
     * @param client   the client details entity
     * @return the event
     */
    private Event produceFinalEvent(final ProfileRequestContext profileRequestContext,
            final HttpServletResponse response, final OIDCAuthorizationRequestContext authorizationRequest,
            final Pair<Events, ? extends Object> pairEvent, final RequestContext springRequestContext,
            final ClientDetailsEntity client) {

        try {
            if (pairEvent.getFirst() == null) {
                log.error("Could not determine the final event based on authorization request");
                return Events.BadRequest.event(this);
            }

            switch (pairEvent.getFirst()) {
            case Failure:
                log.error("Failed to process authorization request. Sending back response error");
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
                break;
            case Redirect:
                if (pairEvent.getSecond() != null) {
                    log.debug("Authorization request indicated a redirect event to {}", pairEvent.getSecond());
                    final OIDCResponse oidcResponse = new OIDCResponse();
                    oidcResponse.setAuthorizationRequest(authorizationRequest.getAuthorizationRequest());
                    oidcResponse.setRedirectUri(pairEvent.getSecond().toString());
                    oidcResponse.setClient(client);
                    OIDCUtils.putOIDCResponseIntoScope(oidcResponse, springRequestContext.getFlowScope());
                } else {
                    throw new OIDCException("No redirect url could be found based on the request");
                }
                break;
            case Success:
                log.debug("Success. Proceeding with building the authorization context based on the request");
                profileRequestContext.addSubcontext(authorizationRequest, true);
                break;
            default:
                log.debug("Proceeding to final event");
            }
            final Event ev = pairEvent.getFirst().event(this);
            log.debug("Returning final event {}", ev.getId());
            return ev;
        } catch (final Exception e) {
            log.error(e.getMessage(), e);
            throw new OIDCException(e);
        }
    }

    /**
     * Load client object client details entity.
     *
     * @param authorizationRequest the authorization request
     * @return the client details entity
     */
    private ClientDetailsEntity loadClientObject(final OIDCAuthorizationRequestContext authorizationRequest) {
        log.debug("Loading client by id {}", authorizationRequest.getClientId());
        return clientService.loadClientByClientId(authorizationRequest.getClientId());
    }

    /**
     * Process login hint parameter if needed.
     *
     * @param request              the request
     * @param authorizationRequest the authorization request
     */
    private void processLoginHintParameterIfNeeded(final HttpServletRequest request,
            final OIDCAuthorizationRequestContext authorizationRequest) {
        final Object loginHint = authorizationRequest.getLoginHint();
        if (loginHint != null) {
            OIDCUtils.putSessionAttribute(request, ConnectRequestParameters.LOGIN_HINT, loginHint);
            log.debug("Saved login hint {} into session", loginHint);
        } else {
            OIDCUtils.removeSessionAttribute(request, ConnectRequestParameters.LOGIN_HINT);
            log.debug("Removed login hint attribute from session");
        }
    }

    /**
     * Create authorization request authorization request.
     *
     * @param request the request
     * @return the authorization request
     */
    private AuthorizationRequest createAuthorizationRequest(final HttpServletRequest request) {
        log.debug("Constructing authorization request");
        final Map<String, String> requestParameters = createRequestMap(request.getParameterMap());
        return authRequestFactory.createAuthorizationRequest(requestParameters);
    }

    /**
     * Check for prompts in the authorization request. Evaluates
     * if redirects to the client should execute
     * or authentication cleared forcing the user
     * to authenticate again.
     *
     * @param prompt      the prompt
     * @param request     the request
     * @param client      the client
     * @param authRequest the auth request
     * @return the event
     * @throws java.io.IOException the IO exception
     */
    private Pair<Events, ? extends Object> checkForPrompts(final String prompt, final HttpServletRequest request,
            final ClientDetailsEntity client, final OIDCAuthorizationRequestContext authRequest) {

        final List<String> prompts = Splitter.on(ConnectRequestParameters.PROMPT_SEPARATOR)
                .splitToList(Strings.nullToEmpty(prompt));
        if (prompts.contains(ConnectRequestParameters.PROMPT_NONE)) {
            return checkForNonePrompt(client, authRequest);
        }

        if (prompts.contains(ConnectRequestParameters.PROMPT_LOGIN)) {
            log.debug("Prompt contains {} which will require forced authN", ConnectRequestParameters.PROMPT_LOGIN);
            SecurityContextHolder.clearContext();
            authRequest.setForceAuthentication(true);
        } else {
            log.debug("Prompt {} is not supported", prompt);
        }
        return new Pair<>(Events.Success, null);
    }

    /**
     * Check for none prompt pair.
     *
     * @param client      the client
     * @param authRequest the auth request
     * @return the pair
     */
    private Pair<Events, ? extends Object> checkForNonePrompt(final ClientDetailsEntity client,
            final OIDCAuthorizationRequestContext authRequest) {
        log.debug("Prompt contains {}", ConnectRequestParameters.PROMPT_NONE);
        final Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        if (auth != null) {
            log.debug("Authentication context is found for {}. Already logged in; continue without prompt",
                    auth.getPrincipal());
            return new Pair(Events.Success, auth);
        }

        log.info("Client requested no prompt");
        if (client != null && authRequest.getRedirectUri() != null) {
            try {
                final String url = redirectResolver.resolveRedirect(authRequest.getRedirectUri(), client);
                log.debug("Initial redirect url resolved for client {} is {}", client.getClientName(), url);

                final URIBuilder uriBuilder = new URIBuilder(url);

                if (authRequest.isImplicitResponseType()) {
                    log.debug("Request is asking for implicit grant type. Encoding parameters as fragments");
                    final StringBuilder builder = new StringBuilder();
                    builder.append(ConnectRequestParameters.ERROR).append('=')
                            .append(ConnectRequestParameters.LOGIN_REQUIRED);

                    if (!Strings.isNullOrEmpty(authRequest.getState())) {
                        builder.append('&').append(ConnectRequestParameters.STATE).append('=')
                                .append(authRequest.getState());
                    }
                    uriBuilder.setFragment(builder.toString());
                } else {
                    log.debug("Request is asking for code grant type. Encoding parameters as url parameters");
                    uriBuilder.addParameter(ConnectRequestParameters.ERROR,
                            ConnectRequestParameters.LOGIN_REQUIRED);
                    if (!Strings.isNullOrEmpty(authRequest.getState())) {
                        uriBuilder.addParameter(ConnectRequestParameters.STATE, authRequest.getState());
                    }
                }
                log.debug("Resolved redirect url {}", uriBuilder.toString());
                return new Pair<>(Events.Redirect, uriBuilder.toString());

            } catch (final URISyntaxException e) {
                log.error("Can't build redirect URI for prompt=none, sending error instead", e);
            }
        } else {
            log.warn("Access denied. Either client is not found or no redirect uri is specified");

        }
        return new Pair(Events.Failure, null);
    }

    /**
     * Creates a map of request parameters. Uses the first parameter value
     * in case multi-valued parameters are found
     *
     * @param parameterMap the original request parameters map
     * @return newly built parameters map
     */
    private Map<String, String> createRequestMap(final Map<String, String[]> parameterMap) {
        final Map<String, String> requestMap = new HashMap<>();
        for (final Map.Entry<String, String[]> stringEntry : parameterMap.entrySet()) {
            final String[] val = stringEntry.getValue();
            if (val != null && val.length > 0) {
                log.debug("Added request parameter {} with value {}", stringEntry.getKey(), val[0]);
                requestMap.put(stringEntry.getKey(), val[0]);
            }
        }

        return requestMap;
    }
}