From ff7201c00c7b7f48e3d868421cc68d361defb84e Mon Sep 17 00:00:00 2001 From: Travis Johnson Date: Thu, 25 May 2023 22:30:02 -0400 Subject: [PATCH] refactor as JWT header auth --- auth/cloudflare.go | 63 ------------------------------------------ auth/jwt.go | 66 ++++++++++++++++++++++++++++++++++++++++++++ cmd/config.go | 32 +++++++++++++++------ storage/bolt/auth.go | 4 +-- 4 files changed, 91 insertions(+), 74 deletions(-) delete mode 100644 auth/cloudflare.go create mode 100644 auth/jwt.go diff --git a/auth/cloudflare.go b/auth/cloudflare.go deleted file mode 100644 index fe241bd4..00000000 --- a/auth/cloudflare.go +++ /dev/null @@ -1,63 +0,0 @@ -package auth - -import ( - "fmt" - "net/http" - "os" - - "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" -) - -// MethodCloudflareAuth is used to identify no auth. -const MethodCloudflareAuth settings.AuthMethod = "cloudflare-access" - -// CloudflareAuth is a proxy implementation of an auther. -type CloudflareAuth struct { - Team string `json:"team"` - Aud string `json:"aud"` -} - -type CloudflareTokenPayload struct { - Email string -} - -// Auth authenticates the user via an HTTP header. -func (a CloudflareAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) { - accessJWT := r.Header.Get("Cf-Access-Jwt-Assertion") - if accessJWT == "" { - return nil, os.ErrPermission - } - - // The Application Audience (AUD) tag for your application - config := &oidc.Config{ - ClientID: a.Aud, - } - - teamDomain := fmt.Sprintf("https://%s.cloudflareaccess.com", a.Team) - certsURL := fmt.Sprintf("%s/cdn-cgi/access/certs", teamDomain) - keySet := oidc.NewRemoteKeySet(r.Context(), certsURL) - verifier := oidc.NewVerifier(teamDomain, keySet, config) - - token, err := verifier.Verify(r.Context(), accessJWT) - if err != nil { - return nil, os.ErrPermission - } - - payload := new(CloudflareTokenPayload) - token.Claims(&payload) - - user, err := usr.Get(srv.Root, payload.Email) - if err == errors.ErrNotExist { - return nil, os.ErrPermission - } - - return user, err -} - -// LoginPage tells that proxy auth doesn't require a login page. -func (a CloudflareAuth) LoginPage() bool { - return false -} diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 00000000..ced9375a --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,66 @@ +package auth + +import ( + "context" + "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"` + Claim string `json:"claim"` + 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]string{} + token.Claims(&payload) + + user, err := usr.Get(srv.Root, payload[a.Claim]) + if 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 c9c97f05..de079695 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -34,10 +34,12 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("shell", "", "shell command to which other commands should be appended") flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") - flags.String("auth.header", "", "HTTP header for auth.method=proxy") + flags.String("auth.header", "", "HTTP header for auth.method=proxy and auth.method=jwt-header") flags.String("auth.command", "", "command for auth.method=hook") - flags.String("auth.team", "", "Cloudflare Access Team name for auth.method=cloudflare-access") - flags.String("auth.team", "", "The Application Audience (AUD) tag for your application for auth.method=cloudflare-access") + flags.String("auth.aud", "", "The Application Audience (AUD) tag for JWT validation auth.method=jwt-header") + flags.String("auth.iss", "", "The Issuer (AUD) for JWT validation auth.method=jwt-header") + flags.String("auth.certsurl", "", "The URL to download certs from for JWT validation auth.method=jwt-header") + flags.String("auth.claim", "", "The claim which will contain the username 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") @@ -86,18 +88,30 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings. auther = &auth.ProxyAuth{Header: header} } - if method == auth.MethodCloudflareAuth { - team := mustGetString(flags, "auth.team") + if method == auth.MethodJWTAuth { + header := mustGetString(flags, "auth.header") aud := mustGetString(flags, "auth.aud") + iss := mustGetString(flags, "auth.iss") + certsurl := mustGetString(flags, "auth.certsurl") + claim := mustGetString(flags, "auth.claim") - if team == "" { - checkErr(nerrors.New("you must set the flag 'auth.team' for method 'cloudflare-access'")) + if header == "" { + checkErr(nerrors.New("you must set the flag 'auth.header' for method 'jwt-header'")) } if aud == "" { - checkErr(nerrors.New("you must set the flag 'auth.aud' for method 'cloudflare-access'")) + checkErr(nerrors.New("you must set the flag 'auth.aud' for method 'jwt-header'")) + } + if iss == "" { + checkErr(nerrors.New("you must set the flag 'auth.iss' for method 'jwt-header'")) + } + if certsurl == "" { + checkErr(nerrors.New("you must set the flag 'auth.certsurl' for method 'jwt-header'")) + } + if claim == "" { + checkErr(nerrors.New("you must set the flag 'auth.claim' for method 'jwt-header'")) } - auther = &auth.CloudflareAuth{Team: team, Aud: aud} + auther = &auth.JWTAuth{} } if method == auth.MethodNoAuth { diff --git a/storage/bolt/auth.go b/storage/bolt/auth.go index 99b45726..de14c918 100644 --- a/storage/bolt/auth.go +++ b/storage/bolt/auth.go @@ -22,8 +22,8 @@ func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) { auther = &auth.ProxyAuth{} case auth.MethodHookAuth: auther = &auth.HookAuth{} - case auth.MethodCloudflareAuth: - auther = &auth.CloudflareAuth{} + case auth.MethodJWTAuth: + auther = &auth.JWTAuth{} case auth.MethodNoAuth: auther = &auth.NoAuth{} default: