Skip to content

Commit

Permalink
initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher Najewicz committed Jan 2, 2020
1 parent bef2a64 commit bbc9376
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
vendor/
41 changes: 41 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true


[[constraint]]
name = "github.com/stretchr/testify"
version = "1.4.0"

[prune]
go-tests = true
unused-packages = true
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
project:=$(shell basename $(shell pwd))
commit:=$(shell git rev-parse --short HEAD)
importpath:=github.com/chiefy/$(project)
ts:=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')

$(GOPATH)/bin/dep:
@curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

vendor: $(GOPATH)/bin/dep
@dep ensure

test: vendor
@go test ./...
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,44 @@
# go-slack-utils
Some wrappers for incoming webhooks, Block Kit UI and message authentication for Slack

## What this is?

This is a general purpose utility library for using Slack's Block Kit UI, with Go structs corresponding to the blocks used to create UI elements. Also included is middleware for validing Slack requests using HMAC-256 and the Slack secret signing key.


## What this is not?

A Slack API wrapper. There's plenty of those out there.


## Installation

```
go get -u github.com/chiefy/go-slack-utils
```

## Usage

### Middleware

```
func main() {
r := mux.NewRouter()
r.HandleFunc("/command", MySlashCommandHandler).Methods(http.MethodPost)
r.Use(middleware.ValidateTimestamp)
// It's up to you on how you configure injection of the slack signing secret
signingSecret := os.Getenv("SLACK_SIGNING_SECRET")
// Generate the validation middleware by injecting the secret
validateReq := middleware.ValidateSlackRequest(signingSecret)
r.Use(validateReq)
srv := &http.Server{
Handler: r,
Addr: "127.0.0.1:" + os.Getenv("PORT"),
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
```
90 changes: 90 additions & 0 deletions pkg/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package middleware

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"time"
)

const (
slackSigHeader = "X-Slack-Signature"
slackSigHeaderTimestamp = "X-Slack-Request-Timestamp"
// RequestTTL asserts incoming requests must have a timestamp within this duration from server's time
RequestTTL = "1m"
)

// ValidateTimestamp asserts that the incoming request's timestamp is within a reasonable timespan from current time
func ValidateTimestamp(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateTimestamp(timestampFromRequest(r)) {
log.Println("invalid timestamp")
http.Error(w, "Invalid Timestamp", http.StatusBadRequest)
} else {
next.ServeHTTP(w, r)
}
})
}

// timestampFromRequest gets the unix epoch timestamp on the http request
func timestampFromRequest(r *http.Request) int64 {
ts, err := strconv.ParseInt(r.Header.Get(slackSigHeaderTimestamp), 10, 64)
if err != nil {
log.Println("could not get timestamp", err)
ts = 0
}
return ts
}

// validateTimestamp asserts that the request's timestamp is within a reasonable timeframe compared with server's current time
func validateTimestamp(ts int64) bool {
abs := func(a time.Duration) time.Duration {
if a >= 0 {
return a
}
return -a
}
m, _ := time.ParseDuration(RequestTTL)
d := abs(time.Since(time.Unix(ts, 0)))
return d < m
}

// ValidateSlackRequest validates a request's signature is signed by the provided slack secret token
func ValidateSlackRequest(signingSecretToken string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
f := func(w http.ResponseWriter, r *http.Request) {
bodyData, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("bad request body %s", err)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
incomingSig := []byte(fmt.Sprintf("v0:%d:%s", timestampFromRequest(r), string(bodyData)))
slackSig, _ := hex.DecodeString(strings.TrimPrefix(r.Header.Get(slackSigHeader), "v0="))
if !validMAC(incomingSig, slackSig, []byte(signingSecretToken)) {
log.Println("HMAC error - signatures did not match")
http.Error(w, "Forbidden", http.StatusForbidden)
} else {
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyData))
next.ServeHTTP(w, r)
}
}
return http.HandlerFunc(f)
}
}

// validMAC reports whether messageMAC is a valid HMAC tag for message.
func validMAC(message, messageMAC, key []byte) bool {
mac := hmac.New(sha256.New, key)
mac.Write(message)
expectedMAC := mac.Sum(nil)
return hmac.Equal(messageMAC, expectedMAC)
}
98 changes: 98 additions & 0 deletions pkg/middleware/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package middleware

import (
"bytes"
"encoding/json"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
)

const (
applicationJSON = "application/json"
)

// GetTestHandler returns a http.HandlerFunc for testing http middleware
func GetTestHandler() http.HandlerFunc {
fn := func(rw http.ResponseWriter, req *http.Request) {
return
}
return http.HandlerFunc(fn)
}

func TestValidateTimestampHandler(t *testing.T) {
assert := assert.New(t)

tenMinutesAgo, _ := time.ParseDuration("-10m")
justOverMinute, _ := time.ParseDuration(RequestTTL)
justOverMinute = justOverMinute + (time.Second * 10)

tests := []struct {
description string
url string
expectedBody string
expectedCode int
timestamp int64
}{
{
description: "invalid timestamp header",
url: "/",
expectedBody: "Invalid Timestamp\n",
expectedCode: 400,
timestamp: 0,
},
{
description: "ten minutes ago",
url: "/",
expectedBody: "Invalid Timestamp\n",
expectedCode: 400,
timestamp: time.Now().Add(tenMinutesAgo).Unix(),
},
{
description: "three minutes in the future",
url: "/",
expectedBody: "Invalid Timestamp\n",
expectedCode: 400,
timestamp: time.Now().Add(justOverMinute).Unix(),
},
{
description: "valid timestamp",
url: "/",
expectedBody: "",
expectedCode: 200,
timestamp: time.Now().Unix(),
},
}

ts := httptest.NewServer(ValidateTimestamp(GetTestHandler()))
defer ts.Close()

for _, tc := range tests {
j, _ := json.Marshal("")

req, err := http.NewRequest("POST", ts.URL, bytes.NewBuffer(j))
assert.NoError(err)

req.Header.Set("Content-Type", applicationJSON)
if tc.timestamp != 0 {
req.Header.Set(slackSigHeaderTimestamp, strconv.Itoa(int(tc.timestamp)))
}

res, err := http.DefaultClient.Do(req)
assert.NoError(err)

if res != nil {
defer res.Body.Close()
}

b, err := ioutil.ReadAll(res.Body)
assert.NoError(err)

assert.Equal(tc.expectedCode, res.StatusCode, tc.description)
assert.Equal(tc.expectedBody, string(b), tc.description)
}
}

0 comments on commit bbc9376

Please sign in to comment.