feat(module,auth): auth based on jwt
This commit is contained in:
84
pkg/module/auth/module.go
Normal file
84
pkg/module/auth/module.go
Normal file
@ -0,0 +1,84 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
AnonymousSubject = "anonymous"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
server *app.Server
|
||||
keyFunc jwt.Keyfunc
|
||||
}
|
||||
|
||||
func (m *Module) Name() string {
|
||||
return "auth"
|
||||
}
|
||||
|
||||
func (m *Module) Export(export *goja.Object) {
|
||||
if err := export.Set("getSubject", m.getSubject); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'getSubject' function"))
|
||||
}
|
||||
|
||||
if err := export.Set("ANONYMOUS", AnonymousSubject); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'ANONYMOUS_USER' property"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) getSubject(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
ctx := util.AssertContext(call.Argument(0), rt)
|
||||
|
||||
req, ok := ctx.Value(module.ContextKeyOriginRequest).(*http.Request)
|
||||
if !ok {
|
||||
panic(errors.New("could not find http request in context"))
|
||||
}
|
||||
|
||||
rawToken := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
|
||||
if rawToken == "" {
|
||||
rawToken = req.URL.Query().Get("token")
|
||||
}
|
||||
|
||||
if rawToken == "" {
|
||||
return rt.ToValue(AnonymousSubject)
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(rawToken, m.keyFunc)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
panic(errors.Errorf("invalid jwt token: '%v'", token.Raw))
|
||||
}
|
||||
|
||||
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
panic(errors.Errorf("unexpected claims type '%T'", token.Claims))
|
||||
}
|
||||
|
||||
subject, exists := mapClaims["sub"]
|
||||
if !exists {
|
||||
return rt.ToValue(AnonymousSubject)
|
||||
}
|
||||
|
||||
return rt.ToValue(subject)
|
||||
}
|
||||
|
||||
func ModuleFactory(keyFunc jwt.Keyfunc) app.ServerModuleFactory {
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
return &Module{
|
||||
server: server,
|
||||
keyFunc: keyFunc,
|
||||
}
|
||||
}
|
||||
}
|
121
pkg/module/auth/module_test.go
Normal file
121
pkg/module/auth/module_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestAuthModule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
keyFunc, secret := getKeyFunc()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(keyFunc),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/auth.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := server.Load("testdata/auth.js", string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
req, err := http.NewRequest("GET", "/foo", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "jdoe",
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
})
|
||||
|
||||
rawToken, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+rawToken)
|
||||
|
||||
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req)
|
||||
|
||||
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthAnonymousModule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger.SetLevel(slog.LevelDebug)
|
||||
|
||||
keyFunc, _ := getKeyFunc()
|
||||
|
||||
server := app.NewServer(
|
||||
module.ConsoleModuleFactory(),
|
||||
ModuleFactory(keyFunc),
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := server.Load("testdata/auth_anonymous.js", string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer server.Stop()
|
||||
|
||||
req, err := http.NewRequest("GET", "/foo", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req)
|
||||
|
||||
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getKeyFunc() (jwt.Keyfunc, []byte) {
|
||||
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
|
||||
}
|
||||
|
||||
return keyFunc, secret
|
||||
}
|
9
pkg/module/auth/testdata/auth.js
vendored
Normal file
9
pkg/module/auth/testdata/auth.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
function testAuth(ctx) {
|
||||
var subject = auth.getSubject(ctx);
|
||||
|
||||
if (subject !== "jdoe") {
|
||||
throw new Error("subject: expected 'jdoe', got '"+subject+"'");
|
||||
}
|
||||
}
|
9
pkg/module/auth/testdata/auth_anonymous.js
vendored
Normal file
9
pkg/module/auth/testdata/auth_anonymous.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
function testAuth(ctx) {
|
||||
var subject = auth.getSubject(ctx);
|
||||
|
||||
if (subject !== auth.ANONYMOUS) {
|
||||
throw new Error("subject: expected '"+auth.ANONYMOUS+"', got '"+subject+"'");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user