refactor as JWT header auth

This commit is contained in:
Travis Johnson 2023-05-25 22:30:02 -04:00
parent 9097bcda6e
commit ff7201c00c
4 changed files with 91 additions and 74 deletions

View File

@ -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
}

66
auth/jwt.go Normal file
View File

@ -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
}

View File

@ -34,10 +34,12 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("shell", "", "shell command to which other commands should be appended") flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") 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.command", "", "command for auth.method=hook")
flags.String("auth.team", "", "Cloudflare Access Team name for auth.method=cloudflare-access") flags.String("auth.aud", "", "The Application Audience (AUD) tag for JWT validation auth.method=jwt-header")
flags.String("auth.team", "", "The Application Audience (AUD) tag for your application for auth.method=cloudflare-access") 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.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key") flags.String("recaptcha.key", "", "ReCaptcha site key")
@ -86,18 +88,30 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
auther = &auth.ProxyAuth{Header: header} auther = &auth.ProxyAuth{Header: header}
} }
if method == auth.MethodCloudflareAuth { if method == auth.MethodJWTAuth {
team := mustGetString(flags, "auth.team") header := mustGetString(flags, "auth.header")
aud := mustGetString(flags, "auth.aud") aud := mustGetString(flags, "auth.aud")
iss := mustGetString(flags, "auth.iss")
certsurl := mustGetString(flags, "auth.certsurl")
claim := mustGetString(flags, "auth.claim")
if team == "" { if header == "" {
checkErr(nerrors.New("you must set the flag 'auth.team' for method 'cloudflare-access'")) checkErr(nerrors.New("you must set the flag 'auth.header' for method 'jwt-header'"))
} }
if aud == "" { 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 { if method == auth.MethodNoAuth {

View File

@ -22,8 +22,8 @@ func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) {
auther = &auth.ProxyAuth{} auther = &auth.ProxyAuth{}
case auth.MethodHookAuth: case auth.MethodHookAuth:
auther = &auth.HookAuth{} auther = &auth.HookAuth{}
case auth.MethodCloudflareAuth: case auth.MethodJWTAuth:
auther = &auth.CloudflareAuth{} auther = &auth.JWTAuth{}
case auth.MethodNoAuth: case auth.MethodNoAuth:
auther = &auth.NoAuth{} auther = &auth.NoAuth{}
default: default: