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("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 {

View File

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