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

com.netflix.spinnaker.echo.slack.SlackAppService.groovy Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 Netflix, Inc.
 *
 * 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.netflix.spinnaker.echo.slack

import com.netflix.spinnaker.echo.config.SlackAppProperties
import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException
import groovy.transform.Canonical
import groovy.util.logging.Slf4j
import org.springframework.http.HttpHeaders
import org.springframework.http.RequestEntity
import org.apache.commons.codec.binary.Hex

import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.security.InvalidKeyException
import java.time.Duration
import java.time.Instant

@Slf4j
@Canonical
class SlackAppService extends SlackService {

  SlackAppService(SlackClient client, SlackAppProperties config) {
    super(client, config)
  }

  // Reference: https://api.slack.com/docs/verifying-requests-from-slack
  // FIXME (lfp): this algorithm works as I've validated it against the sample data provided in the Slack documentation,
  //  but it doesn't work with our requests and signing secret for some reason. I've reached out to Slack support but
  //  have not received any definitive answers yet.
  void verifySignature(RequestEntity request, boolean preventReplays = true) {
    HttpHeaders headers = request.getHeaders()
    String body = request.getBody()
    String timestamp = headers['X-Slack-Request-Timestamp'].first()
    String signature = headers['X-Slack-Signature'].first()

    if (preventReplays &&
      (Instant.ofEpochSecond(Long.valueOf(timestamp)) + Duration.ofMinutes(5)).isBefore(Instant.now())) {
      // The request timestamp is more than five minutes from local time. It could be a replay attack.
      throw new InvalidRequestException("Slack request timestamp is older than 5 minutes. Replay attack?")
    }

    String calculatedSignature = calculateSignature(timestamp, body)

    if (calculatedSignature != signature) {
      throw new InvalidRequestException("Invalid Slack signature header.")
    }
  }

  String calculateSignature(String timestamp, String body, String version = "v0") {
    try {
      // For some reason, Spring URL-decodes asterisks in the body (but not other URL-encoded characters :-P)
      body = body.replaceAll(/\*/, "%2A")
      String signatureBaseString = "$version:$timestamp:$body"
      Mac mac = Mac.getInstance("HmacSHA256")
      SecretKeySpec secretKeySpec = new SecretKeySpec(config.signingSecret.getBytes(), "HmacSHA256")
      mac.init(secretKeySpec)
      byte[] digest = mac.doFinal(signatureBaseString.getBytes())
      return "$version=${Hex.encodeHex(digest).toString()}"
    } catch (InvalidKeyException e) {
      throw new InvalidRequestException("Invalid key exception verifying Slack request signature.")
    }
  }

  void verifyToken(String receivedToken) {
    if (receivedToken != config.verificationToken) {
      throw new InvalidRequestException("Token received from Slack does not match verification token.")
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy