diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 00000000..773c4524 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,71 @@ +package auth + +import ( + "context" + nerrors "errors" + "net/http" + "os" + "sync" + + "github.com/coreos/go-oidc/v3/oidc" + + "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/users" +) + +// MethodJWTAuth is used to identify JWTAuth auth. +const MethodJWTAuth settings.AuthMethod = "jwt-header" + +// JWTAuth is a JWTAuth implementation of an auther. +type JWTAuth struct { + CertsURL string `json:"certsurl"` + Aud string `json:"aud"` + Iss string `json:"iss"` + UsernameClaim string `json:"usernameClaim"` + Header string `json:"header"` + remoteKeySet *oidc.RemoteKeySet + init sync.Once +} + +// Auth authenticates the user via a JWT token in an HTTP header. +func (a *JWTAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) { + a.init.Do(func() { + a.remoteKeySet = oidc.NewRemoteKeySet(context.Background(), a.CertsURL) + }) + + accessJWT := r.Header.Get(a.Header) + if accessJWT == "" { + return nil, os.ErrPermission + } + + // The Application Audience (AUD) tag for your application + config := &oidc.Config{ + ClientID: a.Aud, + } + + verifier := oidc.NewVerifier(a.Iss, a.remoteKeySet, config) + + token, err := verifier.Verify(r.Context(), accessJWT) + if err != nil { + return nil, os.ErrPermission + } + + payload := map[string]any{} + err = token.Claims(&payload) + if err != nil { + return nil, os.ErrPermission + } + + user, err := usr.Get(srv.Root, payload[a.UsernameClaim]) + if nerrors.Is(err, errors.ErrNotExist) { + return nil, os.ErrPermission + } + + return user, err +} + +// LoginPage tells that proxy auth doesn't require a login page. +func (a *JWTAuth) LoginPage() bool { + return false +} diff --git a/cmd/config.go b/cmd/config.go index ed3cc772..d2fd8ca0 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -36,6 +36,11 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") flags.String("auth.header", "", "HTTP header for auth.method=proxy") flags.String("auth.command", "", "command for auth.method=hook") + flags.String("auth.jwt-header.header", "", "HTTP header for auth.method=jwt-header") + flags.String("auth.jwt-header.aud", "", "The Application Audience (AUD) tag for JWT validation for auth.method=jwt-header") + flags.String("auth.jwt-header.iss", "", "The Issuer (AUD) for JWT validation for auth.method=jwt-header") + flags.String("auth.jwt-header.certsurl", "", "The URL to download certs from for JWT validation for auth.method=jwt-header") + flags.String("auth.jwt-header.usernameClaim", "", "The claim which will contain the username for auth.method=jwt-header") flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China") flags.String("recaptcha.key", "", "ReCaptcha site key") @@ -49,7 +54,110 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") } -//nolint:gocyclo +func initProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) auth.Auther { + header := mustGetString(flags, "auth.header") + + if header == "" { + header = defaultAuther["header"].(string) + } + + if header == "" { + checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'")) + } + + return &auth.ProxyAuth{Header: header} +} + +func initJWTAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) auth.Auther { + header := mustGetString(flags, "auth.jwt-header.header") + aud := mustGetString(flags, "auth.jwt-header.aud") + iss := mustGetString(flags, "auth.jwt-header.iss") + certsurl := mustGetString(flags, "auth.jwt-header.certsurl") + usernameClaim := mustGetString(flags, "auth.jwt-header.usernameClaim") + + if header == "" { + header = defaultAuther["header"].(string) + } + if aud == "" { + aud = defaultAuther["aud"].(string) + } + if iss == "" { + iss = defaultAuther["iss"].(string) + } + if certsurl == "" { + certsurl = defaultAuther["certsurl"].(string) + } + if usernameClaim == "" { + usernameClaim = defaultAuther["usernameClaim"].(string) + } + + if header == "" { + checkErr(nerrors.New("you must set the flag 'auth.jwt-header.header' for method 'jwt-header'")) + } + if aud == "" { + checkErr(nerrors.New("you must set the flag 'auth.jwt-header.aud' for method 'jwt-header'")) + } + if iss == "" { + checkErr(nerrors.New("you must set the flag 'auth.jwt-header.iss' for method 'jwt-header'")) + } + if certsurl == "" { + checkErr(nerrors.New("you must set the flag 'auth.jwt-header.certsurl' for method 'jwt-header'")) + } + if usernameClaim == "" { + checkErr(nerrors.New("you must set the flag 'auth.jwt-header.usernameClaim' for method 'jwt-header'")) + } + + return &auth.JWTAuth{ + Header: header, + Aud: aud, + Iss: iss, + CertsURL: certsurl, + UsernameClaim: usernameClaim, + } +} + +func initJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) auth.Auther { + jsonAuth := &auth.JSONAuth{} + host := mustGetString(flags, "recaptcha.host") + key := mustGetString(flags, "recaptcha.key") + secret := mustGetString(flags, "recaptcha.secret") + + if key == "" { + if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok { + key = kmap["key"].(string) + } + } + + if secret == "" { + if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok { + secret = smap["secret"].(string) + } + } + + if key != "" && secret != "" { + jsonAuth.ReCaptcha = &auth.ReCaptcha{ + Host: host, + Key: key, + Secret: secret, + } + } + return jsonAuth +} + +func initHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) auth.Auther { + command := mustGetString(flags, "auth.command") + + if command == "" { + command = defaultAuther["command"].(string) + } + + if command == "" { + checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'")) + } + + return &auth.HookAuth{Command: command} +} + func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) { method := settings.AuthMethod(mustGetString(flags, "auth.method")) @@ -71,67 +179,18 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings. } var auther auth.Auther - if method == auth.MethodProxyAuth { - header := mustGetString(flags, "auth.header") - - if header == "" { - header = defaultAuther["header"].(string) - } - - if header == "" { - checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'")) - } - - auther = &auth.ProxyAuth{Header: header} - } - - if method == auth.MethodNoAuth { + switch method { + case auth.MethodProxyAuth: + auther = initProxyAuth(flags, defaultAuther) + case auth.MethodJWTAuth: + auther = initJWTAuth(flags, defaultAuther) + case auth.MethodNoAuth: auther = &auth.NoAuth{} - } - - if method == auth.MethodJSONAuth { - jsonAuth := &auth.JSONAuth{} - host := mustGetString(flags, "recaptcha.host") - key := mustGetString(flags, "recaptcha.key") - secret := mustGetString(flags, "recaptcha.secret") - - if key == "" { - if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok { - key = kmap["key"].(string) - } - } - - if secret == "" { - if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok { - secret = smap["secret"].(string) - } - } - - if key != "" && secret != "" { - jsonAuth.ReCaptcha = &auth.ReCaptcha{ - Host: host, - Key: key, - Secret: secret, - } - } - auther = jsonAuth - } - - if method == auth.MethodHookAuth { - command := mustGetString(flags, "auth.command") - - if command == "" { - command = defaultAuther["command"].(string) - } - - if command == "" { - checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'")) - } - - auther = &auth.HookAuth{Command: command} - } - - if auther == nil { + case auth.MethodJSONAuth: + auther = initJSONAuth(flags, defaultAuther) + case auth.MethodHookAuth: + auther = initHookAuth(flags, defaultAuther) + default: panic(errors.ErrInvalidAuthMethod) } diff --git a/go.mod b/go.mod index 03560f19..7643bdd0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/asdine/storm/v3 v3.2.1 + github.com/coreos/go-oidc/v3 v3.9.0 github.com/disintegration/imaging v1.6.2 github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 @@ -38,6 +39,7 @@ require ( github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-errors/errors v1.1.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -60,7 +62,9 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sys v0.15.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 34ba7f31..fb9ff71f 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= +github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -93,6 +95,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= @@ -126,6 +130,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -249,6 +254,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -287,6 +293,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= @@ -378,6 +385,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -444,6 +453,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -529,8 +539,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/storage/bolt/auth.go b/storage/bolt/auth.go index cf15a8fe..de14c918 100644 --- a/storage/bolt/auth.go +++ b/storage/bolt/auth.go @@ -22,6 +22,8 @@ func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) { auther = &auth.ProxyAuth{} case auth.MethodHookAuth: auther = &auth.HookAuth{} + case auth.MethodJWTAuth: + auther = &auth.JWTAuth{} case auth.MethodNoAuth: auther = &auth.NoAuth{} default: