All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.predic8.membrane.core.interceptor.oauth2client.OAuth2Resource2Interceptor Maven / Gradle / Ivy

There is a newer version: 5.7.3
Show newest version
/* Copyright 2013 predic8 GmbH, www.predic8.com

   Licensed 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 com.predic8.membrane.core.interceptor.oauth2client;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.predic8.membrane.annot.MCAttribute;
import com.predic8.membrane.annot.MCChildElement;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.annot.Required;
import com.predic8.membrane.core.Router;
import com.predic8.membrane.core.exchange.AbstractExchange;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.exchange.snapshots.AbstractExchangeSnapshot;
import com.predic8.membrane.core.http.Header;
import com.predic8.membrane.core.http.Response;
import com.predic8.membrane.core.interceptor.AbstractInterceptorWithSession;
import com.predic8.membrane.core.interceptor.Outcome;
import com.predic8.membrane.core.interceptor.oauth2.OAuth2AnswerParameters;
import com.predic8.membrane.core.interceptor.oauth2.OAuth2Statistics;
import com.predic8.membrane.core.interceptor.oauth2.ParamNames;
import com.predic8.membrane.core.interceptor.oauth2.authorizationservice.AuthorizationService;
import com.predic8.membrane.core.interceptor.oauth2client.rf.*;
import com.predic8.membrane.core.interceptor.oauth2client.rf.token.AccessTokenRefresher;
import com.predic8.membrane.core.interceptor.oauth2client.rf.token.AccessTokenRevalidator;
import com.predic8.membrane.core.interceptor.session.Session;
import com.predic8.membrane.core.util.URIFactory;
import com.predic8.membrane.core.util.URLParamUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

import static com.predic8.membrane.core.exchange.Exchange.OAUTH2;
import static com.predic8.membrane.core.http.Header.*;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.interceptor.oauth2client.rf.StateManager.generateNewState;
import static com.predic8.membrane.core.interceptor.oauth2client.rf.OAuthUtils.isOAuth2RedirectRequest;
import static com.predic8.membrane.core.interceptor.oauth2client.temp.OAuth2Constants.*;
import static com.predic8.membrane.core.interceptor.session.SessionManager.*;

/**
 * @description Allows only authorized HTTP requests to pass through. Unauthorized requests get a redirect to the
 * authorization server as response.
 * @topic 6. Security
 */
@MCElement(name = "oauth2Resource2")
public class OAuth2Resource2Interceptor extends AbstractInterceptorWithSession {

    private static final Logger log = LoggerFactory.getLogger(OAuth2Resource2Interceptor.class.getName());
    public static final String ERROR_STATUS = "oauth2-error-status";
    public static final String EXPECTED_AUDIENCE = "oauth2-expected-audience";
    public static final String WANTED_SCOPE = "oauth2-wanted-scope";

    private AuthorizationService auth;
    private OAuth2Statistics statistics;
    private URIFactory uriFactory;
    private OriginalExchangeStore originalExchangeStore;
    private String callbackPath = "oauth2callback";

    private final AccessTokenRevalidator accessTokenRevalidator = new AccessTokenRevalidator();
    private final AccessTokenRefresher accessTokenRefresher = new AccessTokenRefresher();
    private PublicUrlManager publicUrlManager = new PublicUrlManager();
    private final SessionAuthorizer sessionAuthorizer = new SessionAuthorizer();
    private final OAuth2CallbackRequestHandler oAuth2CallbackRequestHandler = new OAuth2CallbackRequestHandler();
    private final TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();
    private String customHeaderUserPropertyPrefix;
    private String logoutUrl;
    private String afterLogoutUrl = "/";
    private String afterErrorUrl = null;
    private List loginParameters = new ArrayList<>();
    private boolean appendAccessTokenToRequest;
    private boolean onlyRefreshToken = false;

    @Override
    public void init() throws Exception {
        super.init();

        if (originalExchangeStore == null) {
            originalExchangeStore = new CookieOriginialExchangeStore();
        }
    }

    @Override
    public void init(Router router) throws Exception {
        name = "OAuth 2 Client";
        setFlow(Flow.Set.REQUEST_RESPONSE);

        super.init(router);

        auth.init(router);
        statistics = new OAuth2Statistics();
        uriFactory = router.getUriFactory();

        publicUrlManager.init(auth, callbackPath);
        accessTokenRevalidator.init(auth, statistics);
        accessTokenRefresher.init(auth, onlyRefreshToken);
        sessionAuthorizer.init(auth, router, statistics);
        oAuth2CallbackRequestHandler.init(uriFactory, auth, originalExchangeStore, accessTokenRevalidator,
                sessionAuthorizer, publicUrlManager, callbackPath, onlyRefreshToken);
        tokenAuthenticator.init(sessionAuthorizer, statistics, accessTokenRevalidator, auth);
    }

    @Override
    protected Outcome handleResponseInternal(Exchange exc) {
        return Outcome.CONTINUE;
    }

    @Override
    public final Outcome handleRequestInternal(Exchange exc) throws Exception {

        Session session = getSessionManager().getSession(exc);

        if (isLogoutRequest(exc)) {
            exc.setResponse(Response.redirect(afterLogoutUrl, false).build());

            logOutSession(exc);

            return RETURN;
        }

        if (isFaviconRequest(exc)) {
            exc.setResponse(Response.badRequest().build());
            return RETURN;
        }

        OAuthUtils.simplifyMultipleOAuth2Answers(session);

        if (isOAuth2RedirectRequest(exc)) {
            handleOriginalRequest(exc);
        }

        String wantedScope = (String) exc.getProperty(WANTED_SCOPE);
        if (tokenAuthenticator.userInfoIsNullAndShouldRedirect(session, exc, wantedScope)) {
            return respondWithRedirect(exc);
        }

        accessTokenRevalidator.revalidateIfNeeded(session, wantedScope);

        if (session.hasOAuth2Answer(wantedScope)) {
            exc.setProperty(Exchange.OAUTH2, session.getOAuth2AnswerParameters(wantedScope));
        }

        accessTokenRefresher.refreshIfNeeded(session, exc);

        if (session.isVerified()) {
            applyBackendAuthorization(exc, session);
            statistics.successfulRequest();
            appendAccessTokenToRequest(exc);
            return Outcome.CONTINUE;
        }

        try {
            if (handleRequest(exc, session)) {
                if (exc.getResponse() == null && exc.getRequest() != null && session.isVerified() && session.hasOAuth2Answer()) {
                    exc.setProperty(Exchange.OAUTH2, session.getOAuth2AnswerParameters(wantedScope));
                    appendAccessTokenToRequest(exc);
                    return Outcome.CONTINUE;
                }

                if (exc.getResponse().getStatusCode() >= 400) {
                    session.clear();
                }

                return RETURN;
            }

            log.debug("session present, but not verified, redirecting.");
            return respondWithRedirect(exc);
        } catch (OAuth2Exception e) {
            if (afterErrorUrl != null) {
                FormPostGenerator fpg = new FormPostGenerator(afterErrorUrl).withParameter("error", e.getError());
                if (e.getErrorDescription() != null)
                    fpg.withParameter("error_description", e.getErrorDescription());
                exc.setResponse(fpg.build());
            } else {
                exc.setResponse(e.getResponse());
            }
            return RETURN;
        }
    }

    private void handleOriginalRequest(Exchange exc) throws Exception {
        Map params = URLParamUtil.getParams(uriFactory, exc, URLParamUtil.DuplicateKeyOrInvalidFormStrategy.ERROR);
        String oa2redirect = params.get(OA2REDIRECT);

        Session session = getSessionManager().getSession(exc);

        AbstractExchange originalExchange = new ObjectMapper().readValue(
                        session.get(OAuthUtils.oa2redictKeyNameInSession(oa2redirect)).toString(),
                        AbstractExchangeSnapshot.class)
                .toAbstractExchange();
        session.remove(OAuthUtils.oa2redictKeyNameInSession(oa2redirect));

        doOriginalRequest(exc, originalExchange);
    }

    private boolean isLogoutRequest(Exchange exc) {
        return logoutUrl != null && exc.getRequestURI().startsWith(logoutUrl);
    }

    public void logOutSession(Exchange exc) {
        Session session = getSessionManager().getSession(exc);

        session.clear();
        getSessionManager().removeSession(exc);
        exc.getProperties().remove(SESSION);
        exc.getProperties().remove(SESSION_COOKIE_ORIGINAL);
    }

    private boolean isFaviconRequest(Exchange exc) {
        return exc.getRequestURI().startsWith("/favicon.ico");
    }

    private void applyBackendAuthorization(Exchange exc, Session s) {
        if (customHeaderUserPropertyPrefix == null)
            return;
        Header h = exc.getRequest().getHeader();
        for (Map.Entry e : s.get().entrySet()) {
            if (e.getKey().startsWith(customHeaderUserPropertyPrefix)) {
                String headerName = e.getKey().substring(customHeaderUserPropertyPrefix.length());
                h.removeFields(headerName);
                h.add(headerName, e.getValue().toString());
            }
        }
    }

    public Outcome respondWithRedirect(Exchange exc) throws Exception {
        Integer errorStatus = (Integer) exc.getProperty(ERROR_STATUS);
        if (errorStatus != null) {
            exc.setResponse(Response.statusCode(errorStatus).build());
            return RETURN;
        }

        String state = generateNewState();

        Map lps = loginParameters.stream()
                .collect(HashMap::new, (m, lp) -> m.put(lp.getName(), lp.getValue()), HashMap::putAll);
        Optional.ofNullable((List) exc.getProperty("loginParameters")).orElse(List.of())
                .forEach(lp -> lps.put(lp.getName(), lp.getValue()));

        var combinedLoginParameters = lps.entrySet().stream()
                .filter(e -> {
                    String key = e.getKey();
                    return !"client_id".equals(key) && !"response_type".equals(key) && !"scope".equals(key)
                            && !"redirect_uri".equals(key) && !"response_mode".equals(key) && !"state".equals(key)
                            && !"claims".equals(key);
                })
                .map(e ->
            new LoginParameter(e.getKey(), e.getValue())
        ).toList();

        exc.setResponse(Response.redirect(auth.getLoginURL(state, publicUrlManager.getPublicURL(exc) + callbackPath, exc.getRequestURI()) + LoginParameter.copyLoginParameters(exc, combinedLoginParameters), false).build());

        readBodyFromStreamIntoMemory(exc);

        Session session = getSessionManager().getSession(exc);

        originalExchangeStore.store(exc, session, state, exc);

        if (session.get().containsKey(ParamNames.STATE))
            state = session.get(ParamNames.STATE) + SESSION_VALUE_SEPARATOR + state;
        session.put(ParamNames.STATE, state);

        return RETURN;
    }

    private void readBodyFromStreamIntoMemory(Exchange exc) {
        exc.getRequest().getBodyAsStringDecoded();
    }

    private boolean handleRequest(Exchange exc, Session session) throws Exception {
        String path = uriFactory.create(exc.getDestinations().get(0)).getPath();

        if (path == null) {
            return false;
        }

        if (path.endsWith("/" + callbackPath)) {
            return oAuth2CallbackRequestHandler.handleRequest(exc, session);
        }

        return false;
    }

    private void doOriginalRequest(Exchange exc, AbstractExchange originalRequest) {
        originalRequest.getRequest().getHeader().add("Cookie", exc.getRequest().getHeader().getFirstValue("Cookie"));
        exc.setRequest(originalRequest.getRequest());

        exc.getDestinations().clear();
        String xForwardedProto = originalRequest.getRequest().getHeader().getFirstValue(X_FORWARDED_PROTO);
        String xForwardedHost = originalRequest.getRequest().getHeader().getFirstValue(X_FORWARDED_HOST);
        String originalRequestUri = originalRequest.getOriginalRequestUri();
        exc.getDestinations().add(xForwardedProto + "://" + xForwardedHost + originalRequestUri);

        exc.setOriginalRequestUri(originalRequestUri);
        exc.setOriginalHostHeader(xForwardedHost);
    }

    private void appendAccessTokenToRequest(Exchange exc) {
        if (!appendAccessTokenToRequest)
            return;
        if (exc.getProperty(OAUTH2) == null)
            return;
        OAuth2AnswerParameters params = (OAuth2AnswerParameters) exc.getProperty(OAUTH2);
        if (params.getAccessToken() == null)
            return;
        exc.getRequest().getHeader().setValue(AUTHORIZATION, "Bearer " + params.getAccessToken());
    }

    @Override
    public String getShortDescription() {
        return "Client of the oauth2 authentication process.\n" + statistics.toString();
    }

    public OriginalExchangeStore getOriginalExchangeStore() {
        return originalExchangeStore;
    }

    @MCChildElement(order = 20, allowForeign = true)
    public void setOriginalExchangeStore(OriginalExchangeStore originalExchangeStore) {
        this.originalExchangeStore = originalExchangeStore;
    }

    public boolean isSkipUserInfo() {
        return sessionAuthorizer.isSkipUserInfo();
    }

    @MCAttribute
    public void setSkipUserInfo(boolean skipUserInfo) {
        sessionAuthorizer.setSkipUserInfo(skipUserInfo);
    }

    @MCChildElement(order = 5)
    public void setPublicUrlManager(PublicUrlManager publicUrlManager) {
        this.publicUrlManager = publicUrlManager;
    }

    public PublicUrlManager getPublicUrlManager() {
        return publicUrlManager;
    }

    public AuthorizationService getAuthService() {
        return auth;
    }

    @Required
    @MCChildElement(order = 10)
    public void setAuthService(AuthorizationService auth) {
        this.auth = auth;
    }

    public int getRevalidateTokenAfter() {
        return accessTokenRevalidator.getRevalidateTokenAfter();
    }

    /**
     * @description time in seconds until a oauth2 access token is revalidatet with authorization server. This is disabled for values < 0
     * @default -1
     */
    @MCAttribute
    public void setRevalidateTokenAfter(int revalidateTokenAfter) {
        accessTokenRevalidator.setRevalidateTokenAfter(revalidateTokenAfter);
    }

    public String getCallbackPath() {
        return callbackPath;
    }

    /**
     * @description the path used for the OAuth2 callback. ensure that it does not collide with any path used by the application
     * @default oauth2callback
     */
    @MCAttribute
    public void setCallbackPath(String callbackPath) {
        this.callbackPath = callbackPath;
    }

    public String getCustomHeaderUserPropertyPrefix() {
        return customHeaderUserPropertyPrefix;
    }

    /**
     * @description A user property prefix (e.g. "header"), which can be used to make the interceptor emit custom per-user headers.
     * For example, if you have a user property "headerX: Y" on a user U, and the user U logs in, all requests belonging to this
     * user will have an additional HTTP header "X: Y". If null, this feature is disabled.
     * @default null
     */
    @MCAttribute
    public void setCustomHeaderUserPropertyPrefix(String customHeaderUserPropertyPrefix) {
        this.customHeaderUserPropertyPrefix = customHeaderUserPropertyPrefix;
    }

    public String getLogoutUrl() {
        return logoutUrl;
    }

    @MCAttribute
    public void setLogoutUrl(String logoutUrl) {
        this.logoutUrl = logoutUrl;
    }

    public String getAfterLogoutUrl() {
        return afterLogoutUrl;
    }

    @MCAttribute
    public void setAfterLogoutUrl(String afterLogoutUrl) {
        this.afterLogoutUrl = afterLogoutUrl;
    }

    public List getLoginParameters() {
        return loginParameters;
    }

    @MCChildElement(order = 25)
    public void setLoginParameters(List loginParameters) {
        this.loginParameters = loginParameters;
    }

    public boolean isAppendAccessTokenToRequest() {
        return appendAccessTokenToRequest;
    }

    @MCAttribute
    public void setAppendAccessTokenToRequest(boolean appendAccessTokenToRequest) {
        this.appendAccessTokenToRequest = appendAccessTokenToRequest;
    }

    public String getAfterErrorUrl() {
        return afterErrorUrl;
    }

    @MCAttribute
    public void setAfterErrorUrl(String afterErrorUrl) {
        this.afterErrorUrl = afterErrorUrl;
    }

    public boolean isOnlyRefreshToken() {
        return onlyRefreshToken;
    }

    @MCAttribute
    public void setOnlyRefreshToken(boolean onlyRefreshToken) {
        this.onlyRefreshToken = onlyRefreshToken;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy