123 lines
3.7 KiB
Go
123 lines
3.7 KiB
Go
package main
|
|
|
|
/*
|
|
totp-proxy is a reverse proxy which implements basic time-based one-time
|
|
password (totp) authentication for any website.
|
|
|
|
It takes in a JSON object which maps usernames to totp secrets (generated at
|
|
a site like https://freeotp.github.io/qrcode.html), as well as a url to
|
|
proxy requests to. Users are prompted with a basic-auth prompt, and if they
|
|
succeed their totp challenge a cookie is set and requests are proxied to the
|
|
destination.
|
|
*/
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/mediocregopher/mediocre-go-lib/m"
|
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
|
"github.com/mediocregopher/mediocre-go-lib/mcrypto"
|
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
|
"github.com/mediocregopher/mediocre-go-lib/mhttp"
|
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
|
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
|
"github.com/mediocregopher/mediocre-go-lib/mtime"
|
|
"github.com/pquerna/otp/totp"
|
|
)
|
|
|
|
func main() {
|
|
cmp := m.RootServiceComponent()
|
|
cookieName := mcfg.String(cmp, "cookie-name",
|
|
mcfg.ParamDefault("_totp_proxy"),
|
|
mcfg.ParamUsage("String to use as the name for cookies"))
|
|
cookieTimeout := mcfg.Duration(cmp, "cookie-timeout",
|
|
mcfg.ParamDefault(mtime.Duration{1 * time.Hour}),
|
|
mcfg.ParamUsage("Timeout for cookies"))
|
|
|
|
var userSecrets map[string]string
|
|
mcfg.JSON(cmp, "users", &userSecrets,
|
|
mcfg.ParamRequired(),
|
|
mcfg.ParamUsage("JSON object which maps usernames to their TOTP secret strings"))
|
|
|
|
var secret mcrypto.Secret
|
|
secretStr := mcfg.String(cmp, "secret",
|
|
mcfg.ParamUsage("String used to sign authentication tokens. If one isn't given a new one will be generated on each startup, invalidating all previous tokens."))
|
|
mrun.InitHook(cmp, func(context.Context) error {
|
|
if *secretStr == "" {
|
|
*secretStr = mrand.Hex(32)
|
|
}
|
|
mlog.From(cmp).Info("generating secret")
|
|
secret = mcrypto.NewSecret([]byte(*secretStr))
|
|
return nil
|
|
})
|
|
|
|
proxyHandler := new(struct{ http.Handler })
|
|
proxyURL := mcfg.String(cmp, "dst-url",
|
|
mcfg.ParamRequired(),
|
|
mcfg.ParamUsage("URL to proxy requests to. Only the scheme and host should be set."))
|
|
mrun.InitHook(cmp, func(context.Context) error {
|
|
u, err := url.Parse(*proxyURL)
|
|
if err != nil {
|
|
return merr.Wrap(err, cmp.Context())
|
|
}
|
|
proxyHandler.Handler = mhttp.ReverseProxy(u)
|
|
return nil
|
|
})
|
|
|
|
authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// TODO mlog.FromHTTP?
|
|
ctx := r.Context()
|
|
|
|
unauthorized := func() {
|
|
mlog.From(cmp).Debug("connection is unauthorized")
|
|
w.Header().Add("WWW-Authenticate", "Basic")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}
|
|
|
|
authorized := func() {
|
|
mlog.From(cmp).Debug("connection is authorized, rewriting cookies")
|
|
sig := mcrypto.SignString(secret, "")
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: *cookieName,
|
|
Value: sig.String(),
|
|
MaxAge: int((*cookieTimeout).Seconds()),
|
|
})
|
|
proxyHandler.ServeHTTP(w, r)
|
|
}
|
|
|
|
if cookie, _ := r.Cookie(*cookieName); cookie != nil {
|
|
mlog.From(cmp).Debug("authenticating with cookie",
|
|
mctx.Annotate(ctx, "cookie", cookie.String()))
|
|
var sig mcrypto.Signature
|
|
if err := sig.UnmarshalText([]byte(cookie.Value)); err == nil {
|
|
err := mcrypto.VerifyString(secret, sig, "")
|
|
if err == nil && time.Since(sig.Time()) < (*cookieTimeout).Duration {
|
|
authorized()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if user, pass, ok := r.BasicAuth(); ok && pass != "" {
|
|
mlog.From(cmp).Debug("authenticating with user",
|
|
mctx.Annotate(ctx, "user", user))
|
|
if userSecret, ok := userSecrets[user]; ok {
|
|
if totp.Validate(pass, userSecret) {
|
|
authorized()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
unauthorized()
|
|
})
|
|
|
|
mhttp.InstListeningServer(cmp, authHandler)
|
|
m.Exec(cmp)
|
|
}
|