feat: add basic local login handler in dev cli
This commit is contained in:
parent
fd12d2ba42
commit
1f4f795d43
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,4 +3,5 @@
|
||||
/.env
|
||||
/tools
|
||||
*.sqlite
|
||||
/.gitea-release
|
||||
/.gitea-release
|
||||
/.edge
|
50
cmd/cli/command/app/default-accounts.json
Normal file
50
cmd/cli/command/app/default-accounts.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"username": "superadmin",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superadmin",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "SuperAdmin",
|
||||
"sub": "superadmin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "admin",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "admin",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Admin",
|
||||
"sub": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "superuser",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superuser",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "SuperUser",
|
||||
"sub": "superuser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "user",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "User",
|
||||
"sub": "user"
|
||||
}
|
||||
}
|
||||
]
|
47
cmd/cli/command/app/hash-password.go
Normal file
47
cmd/cli/command/app/hash-password.go
Normal file
@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
|
||||
)
|
||||
|
||||
func HashPasswordCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "hash-password",
|
||||
Usage: "Hash the provided password with the specified algorithm",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "hash `PASSWORD`",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "algorithm",
|
||||
Usage: fmt.Sprintf("use `ALGORITHM` to hash password (available: %v)", passwd.Algorithms()),
|
||||
Aliases: []string{"a"},
|
||||
Value: string(argon2id.Algo),
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
algo := ctx.String("algorithm")
|
||||
password := ctx.String("password")
|
||||
|
||||
hash, err := passwd.Hash(passwd.Algo(algo), password)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fmt.Println(hash)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ func Root() *cli.Command {
|
||||
Subcommands: []*cli.Command{
|
||||
RunCommand(),
|
||||
PackageCommand(),
|
||||
HashPasswordCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
@ -12,6 +14,7 @@ import (
|
||||
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
@ -22,9 +25,15 @@ import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
_ "embed"
|
||||
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
|
||||
)
|
||||
|
||||
func RunCommand() *cli.Command {
|
||||
@ -57,22 +66,12 @@ func RunCommand() *cli.Command {
|
||||
&cli.StringFlag{
|
||||
Name: "storage-file",
|
||||
Usage: "use `FILE` for SQLite storage database",
|
||||
Value: "data.sqlite",
|
||||
Value: ".edge/%APPID%/data.sqlite",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-subject",
|
||||
Usage: "set the `SUBJECT` associated with the simulated connected user",
|
||||
Value: "jdoe",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-role",
|
||||
Usage: "set the `ROLE` associated with the simulated connected user",
|
||||
Value: "user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-preferred-username",
|
||||
Usage: "set the `PREFERRED_USERNAME` associated with the simulated connected user",
|
||||
Value: "Jane Doe",
|
||||
Name: "accounts-file",
|
||||
Usage: "use `FILE` as local accounts",
|
||||
Value: ".edge/%APPID%/accounts.json",
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
@ -82,12 +81,6 @@ func RunCommand() *cli.Command {
|
||||
logFormat := ctx.String("log-format")
|
||||
logLevel := ctx.Int("log-level")
|
||||
|
||||
storageFile := ctx.String("storage-file")
|
||||
|
||||
authSubject := ctx.String("auth-subject")
|
||||
authRole := ctx.String("auth-role")
|
||||
authPreferredUsername := ctx.String("auth-preferred-username")
|
||||
|
||||
logger.SetFormat(logger.Format(logFormat))
|
||||
logger.SetLevel(logger.Level(logLevel))
|
||||
|
||||
@ -105,12 +98,12 @@ func RunCommand() *cli.Command {
|
||||
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load manifest from app bundle")
|
||||
}
|
||||
|
||||
mux.Use(middleware.Logger)
|
||||
mux.Use(dummyAuthMiddleware(authSubject, authRole, authPreferredUsername))
|
||||
|
||||
bus := memory.NewBus()
|
||||
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
|
||||
|
||||
db, err := sqlite.Open(storageFile)
|
||||
if err != nil {
|
||||
@ -119,6 +112,7 @@ func RunCommand() *cli.Command {
|
||||
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
bus := memory.NewBus()
|
||||
|
||||
handler := appHTTP.NewHandler(
|
||||
appHTTP.WithBus(bus),
|
||||
@ -128,11 +122,33 @@ func RunCommand() *cli.Command {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
mux.Handle("/*", handler)
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
accountsFile := injectAppID(ctx.String("accounts-file"), manifest.ID)
|
||||
|
||||
accounts, err := loadLocalAccounts(accountsFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load local accounts")
|
||||
}
|
||||
|
||||
// Add auth handler
|
||||
key, err := dummyKey()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(accounts...),
|
||||
))
|
||||
|
||||
// Add app handler
|
||||
router.Handle("/*", handler)
|
||||
|
||||
logger.Info(cmdCtx, "listening", logger.F("address", address))
|
||||
|
||||
if err := http.ListenAndServe(address, mux); err != nil {
|
||||
if err := http.ListenAndServe(address, router); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -153,10 +169,18 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(dummyKeyFunc),
|
||||
auth.WithJWT(dummyKeySet),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
@ -170,59 +194,72 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
|
||||
|
||||
var dummySecret = []byte("not_so_secret")
|
||||
|
||||
func dummyKeyFunc(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||
func dummyKey() (jwk.Key, error) {
|
||||
key, err := jwk.FromRaw(dummySecret)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return dummySecret, nil
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func dummyAuthMiddleware(subject, role, username string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
unauthenticated := subject == "" && role == "" && username == ""
|
||||
func dummyKeySet() (jwk.Set, error) {
|
||||
key, err := dummyKey()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if unauthenticated {
|
||||
h.ServeHTTP(w, r)
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return
|
||||
set := jwk.NewSet()
|
||||
|
||||
if err := set.AddKey(key); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
func ensureDir(path string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func injectAppID(str string, appID app.ID) string {
|
||||
return strings.ReplaceAll(str, "%APPID%", string(appID))
|
||||
}
|
||||
|
||||
//go:embed default-accounts.json
|
||||
var defaultAccounts []byte
|
||||
|
||||
func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
|
||||
if err := ensureDir(path); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
if subject != "" {
|
||||
claims["sub"] = subject
|
||||
}
|
||||
|
||||
if role != "" {
|
||||
claims["role"] = role
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
claims["preferred_username"] = username
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
rawToken, err := token.SignedString(dummySecret)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not sign token", logger.E(errors.WithStack(err)))
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Add("Authorization", "Bearer "+rawToken)
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
data = defaultAccounts
|
||||
} else {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
var accounts []authHTTP.LocalAccount
|
||||
|
||||
if err := json.Unmarshal(data, &accounts); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
50
go.mod
50
go.mod
@ -2,13 +2,23 @@ module forge.cadoles.com/arcad/edge
|
||||
|
||||
go 1.19
|
||||
|
||||
require modernc.org/sqlite v1.20.4
|
||||
require (
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||
modernc.org/sqlite v1.20.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect
|
||||
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // indirect
|
||||
)
|
||||
|
||||
@ -20,40 +30,40 @@ require (
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32 // indirect
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097 // indirect
|
||||
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.8 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/igm/sockjs-go/v3 v3.0.2 // indirect
|
||||
github.com/igm/sockjs-go/v3 v3.0.2
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/orcaman/concurrent-map v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/urfave/cli/v2 v2.24.3 // indirect
|
||||
github.com/urfave/cli/v2 v2.24.3
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 // indirect
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9
|
||||
go.opencensus.io v0.22.5 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
|
42
go.sum
42
go.sum
@ -72,6 +72,9 @@ github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e/go.mod h1:J7Y8Yc
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
@ -108,6 +111,8 @@ github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3yg
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
|
||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
@ -204,6 +209,18 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
|
||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@ -243,12 +260,18 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.1.5-0.20160925220609-976c720a22c8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
||||
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
@ -280,6 +303,10 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -315,6 +342,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -347,10 +376,13 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -407,6 +439,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -415,11 +448,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -432,6 +469,8 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -484,6 +523,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -587,6 +628,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -11,9 +11,10 @@ describe('Auth Module', function() {
|
||||
it('should retrieve user informations', function() {
|
||||
return Edge.rpc("getUserInfo")
|
||||
.then(userInfo => {
|
||||
chai.assert.isNotNull(userInfo.subject);
|
||||
chai.assert.isNotNull(userInfo.role);
|
||||
chai.assert.isNotNull(userInfo.preferredUsername);
|
||||
console.log("getUserInfo result:", userInfo);
|
||||
chai.assert.property(userInfo, 'subject');
|
||||
chai.assert.property(userInfo, 'role');
|
||||
chai.assert.property(userInfo, 'preferredUsername');
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
**/*.go
|
||||
**/*.tmpl
|
||||
pkg/sdk/client/src/**/*.js
|
||||
pkg/sdk/client/src/**/*.ts
|
||||
misc/client-sdk-testsuite/src/**/*
|
||||
@ -8,5 +9,5 @@ modd.conf
|
||||
prep: cd misc/client-sdk-testsuite && make dist
|
||||
prep: make GOTEST_ARGS="-short" test
|
||||
prep: make build
|
||||
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist --storage-file ./sdk-testsuite.sqlite
|
||||
daemon: bin/cli app run -p misc/client-sdk-testsuite/dist
|
||||
}
|
||||
|
35
pkg/module/auth/http/jwt.go
Normal file
35
pkg/module/auth/http/jwt.go
Normal file
@ -0,0 +1,35 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func generateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
|
||||
token := jwt.New()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for key, value := range claims {
|
||||
if err := token.Set(key, value); err != nil {
|
||||
return nil, errors.Wrapf(err, "could not set claim '%s' with value '%v'", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if err := token.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawToken, err := jwt.Sign(token, jwt.WithKey(algo, key))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return rawToken, nil
|
||||
}
|
27
pkg/module/auth/http/local_account.go
Normal file
27
pkg/module/auth/http/local_account.go
Normal file
@ -0,0 +1,27 @@
|
||||
package http
|
||||
|
||||
import "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||
|
||||
type LocalAccount struct {
|
||||
Username string `json:"username"`
|
||||
Algo passwd.Algo `json:"algo"`
|
||||
Password string `json:"password"`
|
||||
Claims map[string]any `json:"claims"`
|
||||
}
|
||||
|
||||
func NewLocalAccount(username, password string, algo passwd.Algo, claims map[string]any) LocalAccount {
|
||||
return LocalAccount{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Algo: algo,
|
||||
Claims: claims,
|
||||
}
|
||||
}
|
||||
|
||||
func toAccountsMap(accounts []LocalAccount) map[string]LocalAccount {
|
||||
accountsMap := make(map[string]LocalAccount)
|
||||
for _, acc := range accounts {
|
||||
accountsMap[acc.Username] = acc
|
||||
}
|
||||
return accountsMap
|
||||
}
|
183
pkg/module/auth/http/local_handler.go
Normal file
183
pkg/module/auth/http/local_handler.go
Normal file
@ -0,0 +1,183 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
//go:embed templates/login.html.tmpl
|
||||
var rawLoginTemplate string
|
||||
var loginTemplate *template.Template
|
||||
|
||||
var (
|
||||
errNotFound = errors.New("not found")
|
||||
errInvalidPassword = errors.New("invalid password")
|
||||
)
|
||||
|
||||
func init() {
|
||||
loginTemplate = template.Must(template.New("").Parse(rawLoginTemplate))
|
||||
}
|
||||
|
||||
type LocalHandler struct {
|
||||
router chi.Router
|
||||
algo jwa.KeyAlgorithm
|
||||
key jwk.Key
|
||||
accounts map[string]LocalAccount
|
||||
}
|
||||
|
||||
func (h *LocalHandler) initRouter(prefix string) {
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Route(prefix, func(r chi.Router) {
|
||||
r.Get("/login", h.serveForm)
|
||||
r.Post("/login", h.handleForm)
|
||||
r.Get("/logout", h.handleLogout)
|
||||
})
|
||||
|
||||
h.router = router
|
||||
}
|
||||
|
||||
type loginTemplateData struct {
|
||||
URL string
|
||||
Username string
|
||||
Password string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (h *LocalHandler) serveForm(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
data := loginTemplateData{
|
||||
URL: r.URL.String(),
|
||||
Username: "",
|
||||
Password: "",
|
||||
Message: "",
|
||||
}
|
||||
|
||||
if err := loginTemplate.Execute(w, data); err != nil {
|
||||
logger.Error(ctx, "could not execute login page template", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
logger.Error(ctx, "could not parse form", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
username := r.Form.Get("username")
|
||||
password := r.Form.Get("password")
|
||||
|
||||
data := loginTemplateData{
|
||||
URL: r.URL.String(),
|
||||
Username: username,
|
||||
Password: password,
|
||||
Message: "",
|
||||
}
|
||||
|
||||
account, err := h.authenticate(username, password)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNotFound) || errors.Is(err, errInvalidPassword) {
|
||||
data.Message = "Invalid username or password."
|
||||
|
||||
if err := loginTemplate.Execute(w, data); err != nil {
|
||||
logger.Error(ctx, "could not execute login page template", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not authenticate account", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
token, err := generateSignedToken(h.algo, h.key, account.Claims)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cookie := http.Cookie{
|
||||
Name: auth.CookieName,
|
||||
Value: string(token),
|
||||
HttpOnly: false,
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *LocalHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: auth.CookieName,
|
||||
Value: "",
|
||||
HttpOnly: false,
|
||||
Expires: time.Unix(0, 0),
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *LocalHandler) authenticate(username, password string) (*LocalAccount, error) {
|
||||
account, exists := h.accounts[username]
|
||||
if !exists {
|
||||
return nil, errors.WithStack(errNotFound)
|
||||
}
|
||||
|
||||
matches, err := passwd.Match(account.Algo, password, account.Password)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return nil, errors.WithStack(errInvalidPassword)
|
||||
}
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func NewLocalHandler(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...LocalHandlerOptionFunc) *LocalHandler {
|
||||
opts := defaultLocalHandlerOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
handler := &LocalHandler{
|
||||
algo: algo,
|
||||
key: key,
|
||||
accounts: toAccountsMap(opts.Accounts),
|
||||
}
|
||||
|
||||
handler.initRouter(opts.RoutePrefix)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *LocalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
var _ http.Handler = &LocalHandler{}
|
27
pkg/module/auth/http/options.go
Normal file
27
pkg/module/auth/http/options.go
Normal file
@ -0,0 +1,27 @@
|
||||
package http
|
||||
|
||||
type LocalHandlerOptions struct {
|
||||
RoutePrefix string
|
||||
Accounts []LocalAccount
|
||||
}
|
||||
|
||||
type LocalHandlerOptionFunc func(*LocalHandlerOptions)
|
||||
|
||||
func defaultLocalHandlerOptions() *LocalHandlerOptions {
|
||||
return &LocalHandlerOptions{
|
||||
RoutePrefix: "",
|
||||
Accounts: make([]LocalAccount, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func WithAccounts(accounts ...LocalAccount) LocalHandlerOptionFunc {
|
||||
return func(opts *LocalHandlerOptions) {
|
||||
opts.Accounts = accounts
|
||||
}
|
||||
}
|
||||
|
||||
func WithRoutePrefix(prefix string) LocalHandlerOptionFunc {
|
||||
return func(opts *LocalHandlerOptions) {
|
||||
opts.RoutePrefix = prefix
|
||||
}
|
||||
}
|
136
pkg/module/auth/http/passwd/argon2id/hasher.go
Normal file
136
pkg/module/auth/http/passwd/argon2id/hasher.go
Normal file
@ -0,0 +1,136 @@
|
||||
package argon2id
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
const (
|
||||
Algo passwd.Algo = "argon2id"
|
||||
)
|
||||
|
||||
func init() {
|
||||
passwd.Register(Algo, &Hasher{})
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidHash = errors.New("invalid hash")
|
||||
ErrIncompatibleVersion = errors.New("incompatible version")
|
||||
)
|
||||
|
||||
type params struct {
|
||||
memory uint32
|
||||
iterations uint32
|
||||
parallelism uint8
|
||||
saltLength uint32
|
||||
keyLength uint32
|
||||
}
|
||||
|
||||
var defaultParams = params{
|
||||
memory: 64 * 1024,
|
||||
iterations: 3,
|
||||
parallelism: 2,
|
||||
saltLength: 16,
|
||||
keyLength: 32,
|
||||
}
|
||||
|
||||
type Hasher struct{}
|
||||
|
||||
// Hash implements passwd.Hasher
|
||||
func (*Hasher) Hash(plaintext string) (string, error) {
|
||||
salt, err := generateRandomBytes(defaultParams.saltLength)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
hash := argon2.IDKey([]byte(plaintext), salt, defaultParams.iterations, defaultParams.memory, defaultParams.parallelism, defaultParams.keyLength)
|
||||
|
||||
// Base64 encode the salt and hashed password.
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
|
||||
// Return a string using the standard encoded hash representation.
|
||||
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, defaultParams.memory, defaultParams.iterations, defaultParams.parallelism, b64Salt, b64Hash)
|
||||
|
||||
return encodedHash, nil
|
||||
}
|
||||
|
||||
// Match implements passwd.Hasher.
|
||||
func (*Hasher) Match(plaintext string, hash string) (bool, error) {
|
||||
matches, err := comparePasswordAndHash(plaintext, hash)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
var _ passwd.Hasher = &Hasher{}
|
||||
|
||||
func generateRandomBytes(n uint32) ([]byte, error) {
|
||||
buf := make([]byte, n)
|
||||
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func comparePasswordAndHash(password, encodedHash string) (match bool, err error) {
|
||||
p, salt, hash, err := decodeHash(encodedHash)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
|
||||
|
||||
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func decodeHash(encodedHash string) (p *params, salt, hash []byte, err error) {
|
||||
vals := strings.Split(encodedHash, "$")
|
||||
if len(vals) != 6 {
|
||||
return nil, nil, nil, ErrInvalidHash
|
||||
}
|
||||
|
||||
var version int
|
||||
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return nil, nil, nil, ErrIncompatibleVersion
|
||||
}
|
||||
|
||||
p = ¶ms{}
|
||||
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
p.saltLength = uint32(len(salt))
|
||||
|
||||
hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
p.keyLength = uint32(len(hash))
|
||||
|
||||
return p, salt, hash, nil
|
||||
}
|
8
pkg/module/auth/http/passwd/hasher.go
Normal file
8
pkg/module/auth/http/passwd/hasher.go
Normal file
@ -0,0 +1,8 @@
|
||||
package passwd
|
||||
|
||||
type Algo string
|
||||
|
||||
type Hasher interface {
|
||||
Hash(plaintext string) (string, error)
|
||||
Match(plaintext string, hash string) (bool, error)
|
||||
}
|
31
pkg/module/auth/http/passwd/plain/hasher.go
Normal file
31
pkg/module/auth/http/passwd/plain/hasher.go
Normal file
@ -0,0 +1,31 @@
|
||||
package plain
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||
)
|
||||
|
||||
const (
|
||||
Algo passwd.Algo = "plain"
|
||||
)
|
||||
|
||||
func init() {
|
||||
passwd.Register(Algo, &Hasher{})
|
||||
}
|
||||
|
||||
type Hasher struct{}
|
||||
|
||||
// Hash implements passwd.Hasher
|
||||
func (*Hasher) Hash(plaintext string) (string, error) {
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// Match implements passwd.Hasher.
|
||||
func (*Hasher) Match(plaintext string, hash string) (bool, error) {
|
||||
matches := subtle.ConstantTimeCompare([]byte(plaintext), []byte(hash)) == 1
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
var _ passwd.Hasher = &Hasher{}
|
87
pkg/module/auth/http/passwd/registry.go
Normal file
87
pkg/module/auth/http/passwd/registry.go
Normal file
@ -0,0 +1,87 @@
|
||||
package passwd
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrAlgoNotFound = errors.New("algo not found")
|
||||
|
||||
type Registry struct {
|
||||
hashers map[Algo]Hasher
|
||||
}
|
||||
|
||||
func (r *Registry) Register(algo Algo, hasher Hasher) {
|
||||
r.hashers[algo] = hasher
|
||||
}
|
||||
|
||||
func (r *Registry) Match(algo Algo, plaintext string, hash string) (bool, error) {
|
||||
hasher, exists := r.hashers[algo]
|
||||
if !exists {
|
||||
return false, errors.WithStack(ErrAlgoNotFound)
|
||||
}
|
||||
|
||||
matches, err := hasher.Match(plaintext, hash)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Hash(algo Algo, plaintext string) (string, error) {
|
||||
hasher, exists := r.hashers[algo]
|
||||
if !exists {
|
||||
return "", errors.WithStack(ErrAlgoNotFound)
|
||||
}
|
||||
|
||||
hash, err := hasher.Hash(plaintext)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Algorithms() []Algo {
|
||||
algorithms := make([]Algo, 0, len(r.hashers))
|
||||
|
||||
for algo := range r.hashers {
|
||||
algorithms = append(algorithms, algo)
|
||||
}
|
||||
|
||||
return algorithms
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
hashers: make(map[Algo]Hasher),
|
||||
}
|
||||
}
|
||||
|
||||
var defaultRegistry = NewRegistry()
|
||||
|
||||
func Match(algo Algo, plaintext string, hash string) (bool, error) {
|
||||
matches, err := defaultRegistry.Match(algo, plaintext, hash)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func Hash(algo Algo, plaintext string) (string, error) {
|
||||
hash, err := defaultRegistry.Hash(algo, plaintext)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func Algorithms() []Algo {
|
||||
return defaultRegistry.Algorithms()
|
||||
}
|
||||
|
||||
func Register(algo Algo, hasher Hasher) {
|
||||
defaultRegistry.Register(algo, hasher)
|
||||
}
|
105
pkg/module/auth/http/templates/login.html.tmpl
Normal file
105
pkg/module/auth/http/templates/login.html.tmpl
Normal file
@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Login</title>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.form-control > label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-control > input {
|
||||
width: 100%;
|
||||
line-height: 1.4em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 1.2em;
|
||||
padding: 0 5px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#submit {
|
||||
float: right;
|
||||
background-color: #5e77ff;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#submit:hover {
|
||||
background-color: hsl(231deg 100% 71%);
|
||||
}
|
||||
|
||||
#login {
|
||||
padding: 1.5em 1em;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 2px #cccccc1c;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
#message {
|
||||
margin-bottom: 10px;
|
||||
color: red;
|
||||
text-shadow: 1px 1px #fff0f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<p id="message">{{ .Message }}</p>
|
||||
<div id="login">
|
||||
<form method="post" action="{{ .URL }}">
|
||||
<div class="form-control">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" value="{{ .Username }}" required />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" value="{{ .Password }}" required />
|
||||
</div>
|
||||
<input id="submit" type="submit" value="Login" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -5,14 +5,22 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WithJWT(keyFunc jwt.Keyfunc) OptionFunc {
|
||||
const (
|
||||
CookieName string = "edge-auth"
|
||||
)
|
||||
|
||||
type GetKeySetFunc func() (jwk.Set, error)
|
||||
|
||||
func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
|
||||
return func(o *Option) {
|
||||
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||
claim, err := getClaim[string](r, claimName, keyFunc)
|
||||
claim, err := getClaim[string](r, claimName, getKeySet)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
@ -22,28 +30,59 @@ func WithJWT(keyFunc jwt.Keyfunc) OptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func getClaim[T any](r *http.Request, claimAttr string, keyFunc jwt.Keyfunc) (T, error) {
|
||||
rawToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
|
||||
authorization := r.Header.Get("Authorization")
|
||||
|
||||
// Retrieve token from Authorization header
|
||||
rawToken := strings.TrimPrefix(authorization, "Bearer ")
|
||||
|
||||
// Retrieve token from ?edge-auth=<value>
|
||||
if rawToken == "" {
|
||||
rawToken = r.URL.Query().Get("token")
|
||||
rawToken = r.URL.Query().Get(CookieName)
|
||||
}
|
||||
|
||||
if rawToken == "" {
|
||||
return *new(T), errors.WithStack(ErrUnauthenticated)
|
||||
cookie, err := r.Cookie(CookieName)
|
||||
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if cookie != nil {
|
||||
rawToken = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(rawToken, keyFunc)
|
||||
if rawToken == "" {
|
||||
return nil, errors.WithStack(ErrUnauthenticated)
|
||||
}
|
||||
|
||||
keySet, err := getKeySet()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := jwt.Parse([]byte(rawToken),
|
||||
jwt.WithKeySet(keySet, jws.WithRequireKid(false)),
|
||||
jwt.WithValidate(true),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func getClaim[T any](r *http.Request, claimAttr string, getKeySet GetKeySetFunc) (T, error) {
|
||||
token, err := FindToken(r, getKeySet)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return *new(T), errors.Errorf("invalid jwt token: '%v'", token.Raw)
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return *new(T), errors.Errorf("unexpected claims type '%T'", token.Claims)
|
||||
mapClaims, err := token.AsMap(ctx)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawClaim, exists := mapClaims[claimAttr]
|
||||
|
@ -2,7 +2,6 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
@ -12,7 +11,9 @@ import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
@ -22,12 +23,12 @@ func TestAuthModule(t *testing.T) {
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
keyFunc, secret := getKeyFunc()
|
||||
key := getDummyKey()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(
|
||||
WithJWT(keyFunc),
|
||||
WithJWT(getDummyKeySet(key)),
|
||||
),
|
||||
)
|
||||
|
||||
@ -51,17 +52,22 @@ func TestAuthModule(t *testing.T) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "jdoe",
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
})
|
||||
token := jwt.New()
|
||||
|
||||
rawToken, err := token.SignedString(secret)
|
||||
if err := token.Set(jwt.SubjectKey, "jdoe"); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, key))
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+rawToken)
|
||||
req.Header.Add("Authorization", "Bearer "+string(rawToken))
|
||||
|
||||
ctx := context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
||||
|
||||
@ -75,11 +81,11 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
keyFunc, _ := getKeyFunc()
|
||||
key := getDummyKey()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(WithJWT(keyFunc)),
|
||||
ModuleFactory(WithJWT(getDummyKeySet(key))),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
||||
@ -109,16 +115,29 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func getKeyFunc() (jwt.Keyfunc, []byte) {
|
||||
func getDummyKey() jwk.Key {
|
||||
secret := []byte("not_so_secret")
|
||||
|
||||
keyFunc := func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
key, err := jwk.FromRaw(secret)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return keyFunc, secret
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func getDummyKeySet(key jwk.Key) GetKeySetFunc {
|
||||
return func() (jwk.Set, error) {
|
||||
set := jwk.NewSet()
|
||||
|
||||
if err := set.AddKey(key); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
}
|
||||
|
18
pkg/sdk/client/dist/client.js
vendored
18
pkg/sdk/client/dist/client.js
vendored
@ -3862,6 +3862,8 @@ var Edge = (() => {
|
||||
|
||||
// pkg/sdk/client/src/client.ts
|
||||
var import_sockjs_client = __toESM(require_entry());
|
||||
var EventTypeMessage = "message";
|
||||
var EdgeAuth = "edge-auth";
|
||||
var Client = class extends EventTarget {
|
||||
constructor(autoReconnect = true) {
|
||||
super();
|
||||
@ -3880,11 +3882,14 @@ var Edge = (() => {
|
||||
this.rpc = this.rpc.bind(this);
|
||||
this.send = this.send.bind(this);
|
||||
this.upload = this.upload.bind(this);
|
||||
this.addEventListener("message", this._handleRPCResponse);
|
||||
this.addEventListener(EventTypeMessage, this._handleRPCResponse);
|
||||
}
|
||||
connect(token = "") {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `//${document.location.host}/edge/sock?token=${token}`;
|
||||
if (token == "") {
|
||||
token = this._getAuthCookieToken();
|
||||
}
|
||||
const url = `//${document.location.host}/edge/sock?${EdgeAuth}=${token}`;
|
||||
this._log("opening connection to", url);
|
||||
const conn = new import_sockjs_client.default(url);
|
||||
const onOpen = () => {
|
||||
@ -3917,6 +3922,13 @@ var Edge = (() => {
|
||||
disconnect() {
|
||||
this._cleanupConnection();
|
||||
}
|
||||
_getAuthCookieToken() {
|
||||
const cookie = document.cookie.split("; ").find((row) => row.startsWith(EdgeAuth));
|
||||
if (cookie) {
|
||||
return cookie.split("=")[1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
_onConnectionMessage(evt) {
|
||||
const rawMessage = JSON.parse(evt.data);
|
||||
const message = messageFrom(rawMessage);
|
||||
@ -4006,7 +4018,7 @@ var Edge = (() => {
|
||||
}
|
||||
}
|
||||
send(data) {
|
||||
const msg = new Message("message", data);
|
||||
const msg = new Message(TypeMessage, data);
|
||||
this._sendOrQueue(msg);
|
||||
}
|
||||
rpc(method, params) {
|
||||
|
4
pkg/sdk/client/dist/client.js.map
vendored
4
pkg/sdk/client/dist/client.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,9 +1,10 @@
|
||||
import { EventTarget } from "./event-target";
|
||||
import { messageFrom,Message, TypeMessage } from "./message";
|
||||
import { messageFrom, Message, TypeMessage } from "./message";
|
||||
import { RPCError } from "./rpc-error";
|
||||
import SockJS from 'sockjs-client';
|
||||
|
||||
const EventTypeMessage = "message";
|
||||
const EdgeAuth = "edge-auth"
|
||||
|
||||
export class Client extends EventTarget {
|
||||
|
||||
@ -35,12 +36,16 @@ export class Client extends EventTarget {
|
||||
this.send = this.send.bind(this);
|
||||
this.upload = this.upload.bind(this);
|
||||
|
||||
this.addEventListener("message", this._handleRPCResponse);
|
||||
this.addEventListener(EventTypeMessage, this._handleRPCResponse);
|
||||
}
|
||||
|
||||
connect(token = "") {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `//${document.location.host}/edge/sock?token=${token}`;
|
||||
if (token == "") {
|
||||
token = this._getAuthCookieToken()
|
||||
}
|
||||
|
||||
const url = `//${document.location.host}/edge/sock?${EdgeAuth}=${token}`;
|
||||
this._log("opening connection to", url);
|
||||
const conn: any = new SockJS(url);
|
||||
|
||||
@ -79,6 +84,17 @@ export class Client extends EventTarget {
|
||||
this._cleanupConnection();
|
||||
}
|
||||
|
||||
_getAuthCookieToken() {
|
||||
const cookie = document.cookie.split("; ")
|
||||
.find((row) => row.startsWith(EdgeAuth));
|
||||
|
||||
if (cookie) {
|
||||
return cookie.split("=")[1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
_onConnectionMessage(evt) {
|
||||
const rawMessage = JSON.parse(evt.data);
|
||||
const message = messageFrom(rawMessage);
|
||||
@ -179,7 +195,7 @@ export class Client extends EventTarget {
|
||||
}
|
||||
|
||||
send(data) {
|
||||
const msg = new Message("message", data);
|
||||
const msg = new Message(TypeMessage, data);
|
||||
this._sendOrQueue(msg);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user