Skip to main content

슬랙에서는 어떻게 인증처리를 할까?

아래는 Slack Slash Command 기능을 활용한 슬랙 봇의 인증 부분에 관한 설명이에요. 인하우스 용도로 사용되는 슬랙 봇은 조직에 속한 구성원들만 사용할 수 있기에 별도의 확인 절차를 거치지 않고 인증처리를 할 수 있기에 편리하게 사용할 수 있어요.

그럼 본론으로 돌아가서 슬랙에서는 인증처리를 어떻게 수행할까요?

먼저 코드를 보면 인증 부분에 대한 로직은 아래와 같아요.

type SlackVerifier interface {
VerifySignature(c *gin.Context, SlackSigningSecretByte []byte) bool
checkTimestamp(request time.Time, standard time.Time) bool
checkMac(clientSlackSigningSecret string, sig string, SlackSigningSecretByte []byte) bool
}

type SlackRequestVerifier struct{}

var _ SlackVerifier = (*SlackRequestVerifier)(nil)

// Authenticate is a function to authenticate the request from Slack
func Authenticate(c *gin.Context) bool {
SlackRequestVerifier := SlackRequestVerifier{}
if !SlackRequestVerifier.VerifySignature(c, []byte(configs.SLACK_SIGNING_SECRET)) {
logrus.WithFields(logrus.Fields{
"client_ip": c.ClientIP(),
}).Warn("failed to verify signature: Authenticate")
return false
}
return true
}

// VerifySignature is a function to verify the request from Slack
func (srv *SlackRequestVerifier) VerifySignature(c *gin.Context, SlackSigningSecretByte []byte) bool {

// Extract the request body
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = io.ReadAll(c.Request.Body)
} else {
logrus.Error("Request body is nil: VerifySignature")
return false
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
bString := string(bodyBytes)

// Extract the timestamp and signature
ts := c.GetHeader("X-Slack-Request-Timestamp")
sig := c.GetHeader("X-Slack-Signature")

tsInt64, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
logrus.WithFields(logrus.Fields{
"tsInt64": tsInt64,
}).WithError(err).Warn("failed to parse timestamp: VerifySignature")
return false
}

requestTime := time.Unix(tsInt64, 0)
currentTime := time.Now()
if !srv.checkTimestamp(requestTime, currentTime) {
logrus.Warn("failed to check timestamp: VerifySignature")
return false
}

version := "v0"
calculatedSigningSecret := version + ":" + ts + ":" + bString
return srv.checkMac(calculatedSigningSecret, sig, SlackSigningSecretByte)
}

// checkTimestamp is a function to check between the request time and the current time with a 5-minute difference
// if the difference is greater than 5 minutes, return false
func (srv *SlackRequestVerifier) checkTimestamp(request time.Time, standard time.Time) bool {
timeDiff := standard.Sub(request)
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
return false
}
return true
}

// checkMac is a function to check the signature
func (srv *SlackRequestVerifier) checkMac(
calculatedSigningSecret string, /* Calculated Signing Secret (version + timestamp + body) */
sig string, /* HTTP Header (X-Slack-Signature) */
SlackSigningSecretByte []byte, /* Slack Signing Secret */
) bool {

// Create a new HMAC by passing the SHA-256 hash algorithm and the Slack Signing Secret
mac := hmac.New(sha256.New, SlackSigningSecretByte)

// Write the calculated signing secret to the HMAC
if _, err := mac.Write([]byte(calculatedSigningSecret)); err != nil {
logrus.WithError(err).Warn("failed to write mac: checkMac")
return false
}

// Calculate the MAC
originalMac := "v0=" + hex.EncodeToString(mac.Sum(nil))

// Compare the calculated MAC with the original MAC
return hmac.Equal([]byte(originalMac), []byte(sig))
}

개요

  • Slack은 Slash Command를 사용하여 API 서버에 요청을 보낼 때 사용자의 요청이 인증된 사용자가 수행한 액션인지 Signature를 계산하여 확인한다.
  • Slack을 통해 API 서버에 전달하는 요청의 HTTP 헤더에는 X-Slack-Signature이 포함되며 대소문자를 구분하지 않는다. 서명 계산 방식은 먼저 요청 본문을 SHA-256 함수로 해싱하고 이를 HMAC Signing secret (SLACK_SIGNING_SECRET)과 결합한다.
  • 이러한 계산 방식으로 요청마다 씨크릿 정보를 포함하지 않고서 앱의 보안을 유지할 수 있다.

Pattern

  • slack-endpoint-server가 Slack으로부터 요청을 받는다.
  • slack-endpoint-server에서 요청을 기반으로 서명을 계산합니다.
  • 계산한 서명이 요청의 서명과 일치하는지 확인합니다.

자세히

  1. Slack Signing Secret을 관리자로부터 받습니다.
  2. Request에서 타임스탬프 헤더를 추출합니다.
  3. 서버에서 요청을 확인한 현재 시간과 Client에서 요청한 시간(Request 타임스탬프 헤더)을 서로 비교하고 만약 5분 이상 차이가 난다면 인증 불가 처리를 합니다.
  4. 콜론(:)을 구분 기호로 사용하여 버전 번호, 타임스탬프, 요청 본문을 합쳐 하나의 문자열로 생성합니다.
  5. 1에서 관리자로부터 받은 Slack Signing Secret을 사용하여 하나로 합친 문자열을 해시하고 해시의 16진수 다이제스트를 가져옵니다.
  6. 해시의 16진수 다이제스트와 Request의 헤더 X-Slack-Signature과 비교합니다.
  7. 서로 같으면 인증을 통과시키고 그렇지 않으면 인증을 통과시키지 않습니다.