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"
2019-06-17 22:18:50 +00:00
"github.com/mediocregopher/mediocre-go-lib/merr"
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-06-17 22:18:50 +00:00
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" ) )
2018-08-10 00:05:32 +00:00
var userSecrets map [ string ] string
2019-06-17 22:18:50 +00:00
mcfg . JSON ( cmp , "users" , & userSecrets ,
mcfg . ParamRequired ( ) ,
mcfg . ParamUsage ( "JSON object which maps usernames to their TOTP secret strings" ) )
2018-08-10 00:05:32 +00:00
var secret mcrypto . Secret
2019-06-17 22:18:50 +00:00
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 {
2018-08-10 00:05:32 +00:00
if * secretStr == "" {
* secretStr = mrand . Hex ( 32 )
}
2019-06-17 22:18:50 +00:00
mlog . From ( cmp ) . Info ( "generating secret" )
2018-08-10 00:05:32 +00:00
secret = mcrypto . NewSecret ( [ ] byte ( * secretStr ) )
return nil
} )
proxyHandler := new ( struct { http . Handler } )
2019-06-17 22:18:50 +00:00
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 {
2018-08-10 00:05:32 +00:00
u , err := url . Parse ( * proxyURL )
if err != nil {
2019-06-17 22:18:50 +00:00
return merr . Wrap ( err , cmp . Context ( ) )
2018-08-10 00:05:32 +00:00
}
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 ( ) {
2019-06-17 22:18:50 +00:00
mlog . From ( cmp ) . Debug ( "connection is unauthorized" )
2018-08-10 00:05:32 +00:00
w . Header ( ) . Add ( "WWW-Authenticate" , "Basic" )
w . WriteHeader ( http . StatusUnauthorized )
}
authorized := func ( ) {
2019-06-17 22:18:50 +00:00
mlog . From ( cmp ) . Debug ( "connection is authorized, rewriting cookies" )
2018-08-10 00:05:32 +00:00
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-06-17 22:18:50 +00:00
mlog . From ( cmp ) . Debug ( "authenticating with cookie" ,
2019-02-09 19:08:30 +00:00
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-06-17 22:18:50 +00:00
mlog . From ( cmp ) . Debug ( "authenticating with user" ,
2019-02-09 19:08:30 +00:00
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-06-17 22:18:50 +00:00
mhttp . InstListeningServer ( cmp , authHandler )
m . Exec ( cmp )
2018-08-10 00:05:32 +00:00
}