2018-08-10 00:05:32 +00:00
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 (
2019-02-05 20:18:17 +00:00
"context"
2018-08-10 00:05:32 +00:00
"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"
2019-02-09 19:08:30 +00:00
"github.com/mediocregopher/mediocre-go-lib/mctx"
2018-08-10 00:05:32 +00:00
"github.com/mediocregopher/mediocre-go-lib/mhttp"
"github.com/mediocregopher/mediocre-go-lib/mlog"
"github.com/mediocregopher/mediocre-go-lib/mrand"
2019-01-25 03:05:17 +00:00
"github.com/mediocregopher/mediocre-go-lib/mrun"
2018-08-10 00:05:32 +00:00
"github.com/mediocregopher/mediocre-go-lib/mtime"
"github.com/pquerna/otp/totp"
)
func main ( ) {
2019-02-09 19:08:30 +00:00
ctx := m . ServiceContext ( )
ctx , cookieName := mcfg . WithString ( ctx , "cookie-name" , "_totp_proxy" , "String to use as the name for cookies" )
ctx , cookieTimeout := mcfg . WithDuration ( ctx , "cookie-timeout" , mtime . Duration { 1 * time . Hour } , "Timeout for cookies" )
2018-08-10 00:05:32 +00:00
var userSecrets map [ string ] string
2019-02-09 19:08:30 +00:00
ctx = mcfg . WithRequiredJSON ( ctx , "users" , & userSecrets , "JSON object which maps usernames to their TOTP secret strings" )
2018-08-10 00:05:32 +00:00
var secret mcrypto . Secret
2019-02-09 19:08:30 +00:00
ctx , secretStr := mcfg . WithString ( ctx , "secret" , "" , "String used to sign authentication tokens. If one isn't given a new one will be generated on each startup, invalidating all previous tokens." )
ctx = mrun . WithStartHook ( ctx , func ( context . Context ) error {
2018-08-10 00:05:32 +00:00
if * secretStr == "" {
* secretStr = mrand . Hex ( 32 )
}
2019-02-09 19:08:30 +00:00
mlog . Info ( "generating secret" , ctx )
2018-08-10 00:05:32 +00:00
secret = mcrypto . NewSecret ( [ ] byte ( * secretStr ) )
return nil
} )
proxyHandler := new ( struct { http . Handler } )
2019-02-09 19:08:30 +00:00
ctx , proxyURL := mcfg . WithRequiredString ( ctx , "dst-url" , "URL to proxy requests to. Only the scheme and host should be set." )
ctx = mrun . WithStartHook ( ctx , func ( context . Context ) error {
2018-08-10 00:05:32 +00:00
u , err := url . Parse ( * proxyURL )
if err != nil {
return err
}
proxyHandler . Handler = mhttp . ReverseProxy ( u )
return nil
} )
authHandler := http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2019-01-30 21:06:24 +00:00
// TODO mlog.FromHTTP?
2019-02-05 20:18:17 +00:00
ctx := r . Context ( )
2018-08-10 00:05:32 +00:00
unauthorized := func ( ) {
w . Header ( ) . Add ( "WWW-Authenticate" , "Basic" )
w . WriteHeader ( http . StatusUnauthorized )
}
authorized := func ( ) {
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 {
2019-02-09 19:08:30 +00:00
mlog . Debug ( "authenticating with cookie" ,
mctx . Annotate ( ctx , "cookie" , cookie . String ( ) ) )
2018-08-10 00:05:32 +00:00
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 != "" {
2019-02-09 19:08:30 +00:00
mlog . Debug ( "authenticating with user" ,
mctx . Annotate ( ctx , "user" , user ) )
2018-08-10 00:05:32 +00:00
if userSecret , ok := userSecrets [ user ] ; ok {
if totp . Validate ( pass , userSecret ) {
authorized ( )
return
}
}
}
unauthorized ( )
} )
2019-02-09 19:08:30 +00:00
ctx , _ = mhttp . WithListeningServer ( ctx , authHandler )
2019-02-24 22:23:38 +00:00
m . StartWaitStop ( ctx )
2018-08-10 00:05:32 +00:00
}