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

com.slack.api.bolt.middleware.builtin.MultiTeamsAuthorization Maven / Gradle / Ivy

The newest version!
package com.slack.api.bolt.middleware.builtin;

import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.context.Context;
import com.slack.api.bolt.middleware.Middleware;
import com.slack.api.bolt.middleware.MiddlewareChain;
import com.slack.api.bolt.model.Bot;
import com.slack.api.bolt.model.Installer;
import com.slack.api.bolt.request.Request;
import com.slack.api.bolt.request.RequestType;
import com.slack.api.bolt.response.Response;
import com.slack.api.bolt.service.InstallationService;
import com.slack.api.bolt.util.Responder;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.response.auth.AuthTestResponse;
import com.slack.api.model.block.LayoutBlock;
import com.slack.api.token_rotation.RefreshedToken;
import com.slack.api.token_rotation.TokenRotator;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static com.slack.api.bolt.middleware.MiddlewareOps.isNoAuthRequiredRequest;
import static com.slack.api.bolt.middleware.MiddlewareOps.isNoTokenRequiredRequest;
import static com.slack.api.bolt.response.ResponseTypes.ephemeral;

/**
 * Verifies if valid installations exist for requests.
 */
@Slf4j
public class MultiTeamsAuthorization implements Middleware {

    private final AppConfig config;
    private final InstallationService installationService;
    private final TokenRotator tokenRotator;

    @Data
    @AllArgsConstructor
    static class CachedAuthTestResponse {
        private AuthTestResponse response;
        private long cachedMillis;
    }

    // token -> auth.test response
    private final ConcurrentMap tokenToAuthTestCache = new ConcurrentHashMap<>();
    private final Optional tokenToAuthTestCacheCleaner;

    private boolean alwaysRequestUserTokenNeeded;

    public boolean isAlwaysRequestUserTokenNeeded() {
        return alwaysRequestUserTokenNeeded;
    }

    public void setAlwaysRequestUserTokenNeeded(boolean alwaysRequestUserTokenNeeded) {
        this.alwaysRequestUserTokenNeeded = alwaysRequestUserTokenNeeded;
    }

    public MultiTeamsAuthorization(AppConfig config, InstallationService installationService) {
        this.config = config;
        this.installationService = installationService;
        this.tokenRotator = new TokenRotator(
                config.getSlack().methods(),
                config.getTokenRotationExpirationMillis(),
                config.getClientId(),
                config.getClientSecret()
        );
        setAlwaysRequestUserTokenNeeded(config.isAlwaysRequestUserTokenNeeded());
        if (config.isAuthTestCacheEnabled()) {
            boolean permanentCacheEnabled = config.getAuthTestCacheExpirationMillis() < 0;
            if (permanentCacheEnabled) {
                this.tokenToAuthTestCacheCleaner = Optional.empty();
            } else {
                this.tokenToAuthTestCacheCleaner = Optional.of(buildTokenToAuthTestCacheCleaner(() -> {
                    long expirationMillis = System.currentTimeMillis() - config.getAuthTestCacheExpirationMillis();
                    for (Map.Entry each : tokenToAuthTestCache.entrySet()) {
                        if (each.getValue() == null || each.getValue().getCachedMillis() < expirationMillis) {
                            tokenToAuthTestCache.remove(each.getKey());
                        }
                    }
                }));
            }
        } else {
            this.tokenToAuthTestCacheCleaner = Optional.empty();
        }
    }

    private ScheduledExecutorService buildTokenToAuthTestCacheCleaner(Runnable task) {
        String threadGroupName = MultiTeamsAuthorization.class.getSimpleName();
        ScheduledExecutorService tokenToAuthTestCacheCleaner =
                this.config.getExecutorServiceProvider().createThreadScheduledExecutor(threadGroupName);
        tokenToAuthTestCacheCleaner.scheduleAtFixedRate(task, 120_000, 30_000, TimeUnit.MILLISECONDS);
        log.debug("The tokenToAuthTestCacheCleaner (daemon thread) started");
        return tokenToAuthTestCacheCleaner;
    }

    @Override
    protected void finalize() throws Throwable {
        if (this.tokenToAuthTestCacheCleaner.isPresent()) {
            this.tokenToAuthTestCacheCleaner.get().shutdown();
        }
        super.finalize();
    }

    @Override
    public Response apply(Request req, Response resp, MiddlewareChain chain) throws Exception {
        if (isNoAuthRequiredRequest(req.getRequestType())) {
            return chain.next(req);
        }
        if (isNoTokenRequiredRequest(req)) {
            // Nothing to do here
            // enterprise_id / team_id are already set by Request object constructor
            return chain.next(req);
        }

        Context context = req.getContext();

        String botToken = null;
        String userToken = null;

        Bot bot = installationService.findBot(context.getEnterpriseId(), context.getTeamId());
        Installer installer = null;
        if (bot != null) {
            if (bot.getBotRefreshToken() != null) {
                // A refresh token exists if token rotation is enabled
                Optional maybeRefreshed = this.tokenRotator.performTokenRotation(r -> r
                        .accessToken(bot.getBotAccessToken())
                        .refreshToken(bot.getBotRefreshToken())
                        .expiresAt(bot.getBotTokenExpiresAt())
                );
                if (maybeRefreshed.isPresent()) {
                    RefreshedToken newOne = maybeRefreshed.get();
                    bot.setBotAccessToken(newOne.getAccessToken());
                    bot.setBotRefreshToken(newOne.getRefreshToken());
                    bot.setBotTokenExpiresAt(newOne.getExpiresAt());
                    installationService.saveBot(bot);
                }
            }
            botToken = bot.getBotAccessToken();
        }

        if ((isAlwaysRequestUserTokenNeeded() || bot == null) && context.getRequestUserId() != null) {
            // There are two patterns here:
            // 1) No bot token was found for this request -- trying to find installer's token instead
            // 2) A bot was found but this app needs to check if there is a user token
            //    which is associated with the user_id in this incoming request
            installer = installationService.findInstaller(
                    context.getEnterpriseId(),
                    context.getTeamId(),
                    context.getRequestUserId()
            );
            if (installer != null) {
                boolean refreshed = false;
                if (installer.getInstallerUserRefreshToken() != null) {
                    // A refresh token exists if token rotation is enabled
                    final Installer _i = installer;
                    Optional maybeRefreshed = this.tokenRotator.performTokenRotation(r -> r
                            .accessToken(_i.getInstallerUserAccessToken())
                            .refreshToken(_i.getInstallerUserRefreshToken())
                            .expiresAt(_i.getInstallerUserTokenExpiresAt())
                    );
                    refreshed = refreshed || maybeRefreshed.isPresent();
                    if (maybeRefreshed.isPresent()) {
                        RefreshedToken newOne = maybeRefreshed.get();
                        installer.setInstallerUserAccessToken(newOne.getAccessToken());
                        installer.setInstallerUserRefreshToken(newOne.getRefreshToken());
                        installer.setInstallerUserTokenExpiresAt(newOne.getExpiresAt());
                    }
                }
                if (installer.getBotRefreshToken() != null) {
                    // A refresh token exists if token rotation is enabled
                    final Installer _i = installer;
                    Optional maybeRefreshed = this.tokenRotator.performTokenRotation(r -> r
                            .accessToken(_i.getBotAccessToken())
                            .refreshToken(_i.getBotRefreshToken())
                            .expiresAt(_i.getBotTokenExpiresAt())
                    );
                    refreshed = refreshed || maybeRefreshed.isPresent();
                    if (maybeRefreshed.isPresent()) {
                        RefreshedToken newOne = maybeRefreshed.get();
                        installer.setBotAccessToken(newOne.getAccessToken());
                        installer.setBotRefreshToken(newOne.getRefreshToken());
                        installer.setBotTokenExpiresAt(newOne.getExpiresAt());
                    }
                }
                if (refreshed) {
                    // Save the refresh results for following data accesses
                    installationService.saveInstallerAndBot(installer);
                }
                userToken = installer.getInstallerUserAccessToken();
            }
        }

        if (botToken == null && userToken == null) {
            // In this case, no valid bot/user token was found for enterprise_id/team_id/user_id given by a request.
            // Bolt tries to ask the user to install the app if there is a response_url in the request.
            String responseUrl = req.getResponseUrl();
            if (responseUrl != null) {
                Responder responder = new Responder(config.getSlack(), responseUrl);
                if (req.getRequestType() != null) {
                    List blocks = installationService.getInstallationGuideBlocks(
                            context.getEnterpriseId(), context.getTeamId(), context.getRequestUserId());
                    String text = blocks == null ? installationService.getInstallationGuideText(
                            context.getEnterpriseId(), context.getTeamId(), context.getRequestUserId()) : null;
                    if (req.getRequestType().equals(RequestType.Command)) {
                        if (blocks != null) {
                            responder.sendToCommand(body -> body.responseType(ephemeral).blocks(blocks));
                        } else {
                            responder.sendToCommand(body -> body.responseType(ephemeral).text(text));
                        }
                    } else {
                        if (blocks != null) {
                            responder.sendToAction(body -> body.responseType(ephemeral).blocks(blocks));
                        } else {
                            responder.sendToAction(body -> body.responseType(ephemeral).text(text));
                        }
                    }
                    // just for acknowledging this request
                    return Response.builder().statusCode(200).build();
                }
            } else {
                return buildError(401, null, null, null);
            }
        }

        try {
            String token = botToken != null ? botToken : userToken;
            AuthTestResponse authTestResponse = callAuthTest(token, config, context.client());
            if (authTestResponse.isOk()) {
                context.setAuthTestResponse(authTestResponse);
                context.setBotToken(botToken);
                Map> botHeaders = authTestResponse.getHttpResponseHeaders();
                List botScopesHeader = botHeaders != null ? botHeaders.get("x-oauth-scopes") : null;
                context.setBotScopes(botScopesHeader != null ? Arrays.asList(botScopesHeader.get(0).split(",")) : null);
                context.setRequestUserToken(userToken);
                if (userToken != null && token != userToken) {
                    AuthTestResponse userAuthTestResponse = callAuthTest(userToken, config, context.client());
                    Map> userHeaders = userAuthTestResponse.getHttpResponseHeaders();
                    List userScopesHeader = userHeaders != null ? userHeaders.get("x-oauth-scopes") : null;
                    context.setRequestUserScopes(userScopesHeader != null ? Arrays.asList(userScopesHeader.get(0).split(",")) : null);
                }
                if (!authTestResponse.isEnterpriseInstall()) {
                    context.setTeamId(authTestResponse.getTeamId());
                    // As the team_id here is the org's ID,
                    // Request#updateContext() does this for enterprise_install
                }
                context.setEnterpriseId(authTestResponse.getEnterpriseId());
                context.setEnterpriseInstall(authTestResponse.isEnterpriseInstall());
                if (bot != null) {
                    context.setBotId(bot.getBotId());
                    context.setBotUserId(authTestResponse.getUserId());
                }
                return chain.next(req);
            } else {
                return handleAuthTestError(authTestResponse.getError(), bot, installer, authTestResponse);
            }
        } catch (IOException e) {
            return buildError(503, null, e, null);
        } catch (SlackApiException e) {
            return buildError(503, null, null, e);
        }
    }

    protected AuthTestResponse callAuthTest(String token, AppConfig config, MethodsClient client) throws IOException, SlackApiException {
        if (config.isAuthTestCacheEnabled()) {
            CachedAuthTestResponse cachedResponse = tokenToAuthTestCache.get(token);
            if (cachedResponse != null) {
                boolean permanentCacheEnabled = config.getAuthTestCacheExpirationMillis() < 0;
                if (permanentCacheEnabled) {
                    return cachedResponse.getResponse();
                }
                long millisToExpire = cachedResponse.getCachedMillis() + config.getAuthTestCacheExpirationMillis();
                if (millisToExpire > System.currentTimeMillis()) {
                    return cachedResponse.getResponse();
                }
            }
            AuthTestResponse response = client.authTest(r -> r.token(token));
            CachedAuthTestResponse newCache = new CachedAuthTestResponse(response, System.currentTimeMillis());
            tokenToAuthTestCache.put(token, newCache);
            return response;
        } else {
            return client.authTest(r -> r.token(token));
        }
    }

    protected Response handleAuthTestError(
            String errorCode,
            Bot foundBot,
            Installer foundInstaller,
            AuthTestResponse authTestResponse) throws Exception {
        if (errorCode.equals("account_inactive")) {
            // this is not recoverable - going to remove the data not to repeat the same error
            if (foundBot != null) {
                installationService.deleteBot(foundBot);
            } else if (foundInstaller != null) {
                installationService.deleteInstaller(foundInstaller);
            }
        }
        return buildError(401, authTestResponse, null, null);
    }

    protected Response buildError(
            int statusCode,
            AuthTestResponse authTestResponse,
            IOException ioException,
            SlackApiException slackException) {

        log.info("auth.test result: {}, io error: {}, api error: {}", authTestResponse, ioException, slackException);

        return Response.builder()
                .statusCode(statusCode)
                .contentType(Response.CONTENT_TYPE_APPLICATION_JSON_UTF8)
                .body("{\"error\":\"a request for an unknown workspace detected\"}")
                .build();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy