Skip to content

Commit

Permalink
feat(security): add http basic auth (fix #6) (#7)
Browse files Browse the repository at this point in the history
feat(security): add http basic auth
  • Loading branch information
_eternal_flame authored and ncarlier committed Sep 4, 2018
1 parent ac754d9 commit 513e6d7
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 11 deletions.
8 changes: 8 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,12 @@ APP_HTTP_NOTIFIER_USER=api:key-xxxxxxxxxxxxxxxxxxxxxxxxxx
# Defaults: localhost:25
APP_SMTP_NOTIFIER_HOST=localhost:25

# Authentication Method
# Defaults: none
# Values:
# - "none" : No authentication (defaults).
# - "basic": HTTP Basic authentication.
AUTH=none

# Authentication Parameter
AUTH_PARAM=username:password
38 changes: 28 additions & 10 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
package main

import (
"bytes"
"flag"
"os"
"strconv"

"github.com/ncarlier/webhookd/pkg/auth"
)

// Config contain global configuration
type Config struct {
ListenAddr *string
NbWorkers *int
Debug *bool
Timeout *int
ScriptDir *string
ListenAddr *string
NbWorkers *int
Debug *bool
Timeout *int
ScriptDir *string
Authentication *string
AuthenticationParam *string
}

var config = &Config{
ListenAddr: flag.String("listen", getEnv("LISTEN_ADDR", ":8080"), "HTTP service address (e.g.address, ':8080')"),
NbWorkers: flag.Int("nb-workers", getIntEnv("NB_WORKERS", 2), "The number of workers to start"),
Debug: flag.Bool("debug", getBoolEnv("DEBUG", false), "Output debug logs"),
Timeout: flag.Int("timeout", getIntEnv("HOOK_TIMEOUT", 10), "Hook maximum delay before timeout (in second)"),
ScriptDir: flag.String("scripts", getEnv("SCRIPTS_DIR", "scripts"), "Scripts directory"),
ListenAddr: flag.String("listen", getEnv("LISTEN_ADDR", ":8080"), "HTTP service address (e.g.address, ':8080')"),
NbWorkers: flag.Int("nb-workers", getIntEnv("NB_WORKERS", 2), "The number of workers to start"),
Debug: flag.Bool("debug", getBoolEnv("DEBUG", false), "Output debug logs"),
Timeout: flag.Int("timeout", getIntEnv("HOOK_TIMEOUT", 10), "Hook maximum delay before timeout (in second)"),
ScriptDir: flag.String("scripts", getEnv("SCRIPTS_DIR", "scripts"), "Scripts directory"),
Authentication: flag.String("auth", getEnv("AUTH", "none"), ""),
AuthenticationParam: flag.String("auth-param", getEnv("AUTH_PARAM", ""), func() string {
authdocwriter := bytes.NewBufferString("Authentication method. Available methods: ")

for key, method := range auth.AvailableMethods {
authdocwriter.WriteRune('\n')
authdocwriter.WriteString(key)
authdocwriter.WriteRune(':')
authdocwriter.WriteString(method.Usage())
}
return authdocwriter.String()
}()),
}

func init() {
flag.StringVar(config.ListenAddr, "l", *config.ListenAddr, "HTTP service (e.g address: ':8080')")
flag.BoolVar(config.Debug, "d", *config.Debug, "Output debug logs")

}

func getEnv(key, fallback string) string {
Expand Down
19 changes: 18 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/ncarlier/webhookd/pkg/api"
"github.com/ncarlier/webhookd/pkg/auth"
"github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/worker"
)
Expand All @@ -34,13 +35,29 @@ func main() {
return
}

var authmethod auth.Method
name := *config.Authentication
if _, ok := auth.AvailableMethods[name]; ok {
authmethod = auth.AvailableMethods[name]
if err := authmethod.ParseParam(*config.AuthenticationParam); err != nil {
fmt.Println("Authentication parameter is not valid:", err.Error())
fmt.Println(authmethod.Usage())
os.Exit(2)
}
} else {
fmt.Println("Authentication name is not valid:", name)
os.Exit(2)
}

level := "info"
if *config.Debug {
level = "debug"
}
logger.Init(level)

logger.Debug.Println("Starting webhookd server...")
logger.Debug.Println("Using Authentication:", name)
authmethod.Init(*config.Debug)

router := http.NewServeMux()
router.Handle("/", api.Index(*config.Timeout, *config.ScriptDir))
Expand All @@ -52,7 +69,7 @@ func main() {

server := &http.Server{
Addr: *config.ListenAddr,
Handler: tracing(nextRequestID)(logging(logger.Debug)(router)),
Handler: authmethod.Middleware()(tracing(nextRequestID)(logging(logger.Debug)(router))),
ErrorLog: logger.Error,
}

Expand Down
27 changes: 27 additions & 0 deletions pkg/auth/authmethod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package auth

import "net/http"

// Method an interface describing an authentication method
type Method interface {
// Called after ParseParam method.
// auth.Method should initialize itself here and get ready to receive requests.
// Logger has been initialized so it is safe to call logger methods here.
Init(debug bool)
// Return Method Usage Info
Usage() string
// Parse the parameter passed through the -authparam flag
// Logger is not initialized at this state so do NOT call logger methods
// If the parameter is unacceptable, return an error and main should exit
ParseParam(string) error
// Return a middleware to handle connections.
Middleware() func(http.Handler) http.Handler
}

var (
// AvailableMethods Returns a map of available auth methods
AvailableMethods = map[string]Method{
"none": new(noAuth),
"basic": new(basicAuth),
}
)
59 changes: 59 additions & 0 deletions pkg/auth/basic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package auth

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/ncarlier/webhookd/pkg/logger"
)

type basicAuth struct {
username string
password string
authheader string
}

func (c *basicAuth) Init(_ bool) {}

func (c *basicAuth) Usage() string {
return "HTTP Basic Auth. Usage: -auth basic -authparam <username>:<password>[:<realm>] (example: -auth basic -auth-param foo:bar)"
}

func (c *basicAuth) ParseParam(param string) error {
res := strings.Split(param, ":")
realm := "Authentication required."
switch len(res) {
case 3:
realm = res[2]
fallthrough
case 2:
c.username, c.password = res[0], res[1]
c.authheader = fmt.Sprintf("Basic realm=\"%s\"", realm)
return nil
}
return errors.New("Invalid Auth param")

}

// BasicAuth HTTP Basic Auth implementation
func (c *basicAuth) Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if username, password, ok := r.BasicAuth(); ok && username == c.username && password == c.password {
logger.Info.Printf("HTTP Basic Auth: %s PASSED\n", username)
next.ServeHTTP(w, r)
} else if !ok {
logger.Debug.Println("HTTP Basic Auth: Auth header not present.")
w.Header().Add("WWW-Authenticate", c.authheader)
w.WriteHeader(401)
w.Write([]byte("Authentication required."))
} else {
logger.Warning.Printf("HTTP Basic Auth: Invalid credentials for username %s\n", username)
w.WriteHeader(403)
w.Write([]byte("Forbidden."))
}
})
}
}
25 changes: 25 additions & 0 deletions pkg/auth/none.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package auth

import (
"net/http"
)

type noAuth struct {
}

func (c *noAuth) Usage() string {
return "No Auth. Usage: -auth none"
}

func (c *noAuth) Init(_ bool) {}

func (c *noAuth) ParseParam(_ string) error {
return nil
}

// NoAuth A Nop Auth middleware
func (c *noAuth) Middleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return h
}
}

0 comments on commit 513e6d7

Please sign in to comment.