Skip to content

Commit

Permalink
feature: #279 Added back security handling
Browse files Browse the repository at this point in the history
* Made it so that the port could be set via `WEOS_PORT` environment variable
* Created middleware for managing security
* Setup security in route initializer when security is setup
* Added httpClient to the container
* Updated the go version v1.22 because one of the libraries needed go v1.21
  • Loading branch information
akeemphilbert committed Apr 2, 2024
1 parent 6bb8a36 commit 6052d5a
Show file tree
Hide file tree
Showing 16 changed files with 1,868 additions and 113 deletions.
29 changes: 25 additions & 4 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
module github.com/wepala/weos/v2

go 1.20
go 1.22

require (
github.com/aws/aws-sdk-go-v2/config v1.27.9
github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.4
github.com/casbin/casbin/v2 v2.86.0
github.com/casbin/gorm-adapter/v3 v3.23.0
github.com/coreos/go-oidc/v3 v3.10.0
github.com/getkin/kin-openapi v0.123.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/labstack/echo/v4 v4.11.4
github.com/labstack/gommon v0.4.2
github.com/segmentio/ksuid v1.0.4
go.uber.org/fx v1.21.0
go.uber.org/zap v1.26.0
golang.org/x/net v0.19.0
gorm.io/datatypes v1.2.0
gorm.io/driver/mysql v1.5.6
gorm.io/driver/postgres v1.5.7
gorm.io/driver/sqlite v1.5.5
Expand All @@ -32,11 +37,18 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 // indirect
github.com/aws/smithy-go v1.20.1 // indirect
github.com/casbin/govaluate v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.20.3 // indirect
github.com/glebarez/sqlite v1.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
Expand All @@ -52,14 +64,23 @@ require (
github.com/microsoft/go-mssqldb v1.6.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/dig v1.17.1 // indirect
go.uber.org/goleak v1.2.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/datatypes v1.2.0 // indirect
gorm.io/plugin/dbresolver v1.3.0 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.20.3 // indirect
)
79 changes: 74 additions & 5 deletions v2/go.sum

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion v2/rest/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"github.com/labstack/echo/v4"
"go.uber.org/fx"
"golang.org/x/net/context"
"os"
)

// registerHooks registers the hooks for the application
func registerHooks(lifecycle fx.Lifecycle, e *echo.Echo) {
lifecycle.Append(fx.Hook{
OnStart: func(context.Context) error {
go func() {
if err := e.Start(":8681"); err != nil {
if err := e.Start(":" + os.Getenv("WEOS_PORT")); err != nil {
e.Logger.Info("shutting down the server")
}
}()
Expand All @@ -29,10 +30,12 @@ var API = fx.Module("rest",
Config,
NewEcho,
NewZap,
NewClient,
NewGORM,
NewCommandDispatcher,
NewResourceRepository,
NewGORMProjection,
NewSecurityConfiguration,
),
fx.Invoke(RouteInitializer, registerHooks),
)
263 changes: 263 additions & 0 deletions v2/rest/auth.go
Original file line number Diff line number Diff line change
@@ -1 +1,264 @@
package rest

import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"github.com/casbin/casbin/v2"
casbinmodel "github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/getkin/kin-openapi/openapi3"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"go.uber.org/fx"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"gorm.io/gorm"
"net/http"
"strings"
"time"
)

// ValidationResult is the result of a security validation
type ValidationResult struct {
Valid bool
Token string
UserID string
Role string
AccountID string
ApplicationID string
}

//security interfaces

type Validator interface {
Validate(ctxt echo.Context) (*ValidationResult, error)
FromSchema(ctx context.Context, scheme *openapi3.SecurityScheme, httpClient *http.Client) (Validator, error)
}

type SecurityParams struct {
fx.In
Config *openapi3.T
HttpClient *http.Client
GORMDB *gorm.DB
}

type SecurityConfiguration struct {
fx.Out
SecuritySchemes map[string]Validator
AuthEnforcer *casbin.Enforcer
}

func NewSecurityConfiguration(p SecurityParams) (result *SecurityConfiguration, err error) {
result = &SecurityConfiguration{
SecuritySchemes: make(map[string]Validator),
}
for name, schema := range p.Config.Components.SecuritySchemes {
if schema.Value != nil {
switch schema.Value.Type {
case "openIdConnect":
ctxt := context.WithValue(context.Background(), oauth2.HTTPClient, p.HttpClient)
result.SecuritySchemes[name], err = new(OpenIDConnect).FromSchema(ctxt, schema.Value, p.HttpClient)
default:
err = fmt.Errorf("unsupported security scheme '%s'", name)
return nil, err
}
}
}

//setup casbin enforcer
adapter, err := gormadapter.NewAdapterByDB(p.GORMDB)
if err != nil {
return nil, err
}

//default REST permission model
text := `[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
`
m, _ := casbinmodel.NewModelFromString(text)
result.AuthEnforcer, err = casbin.NewEnforcer(m, adapter)
return result, err
}

// OpenIDConnect authorizer for OpenID
type OpenIDConnect struct {
connectURL string
skipExpiryCheck bool
clientID string
userIDClaim string
roleClaim string
accountClaim string
applicationClaim string
httpClient *http.Client
KeySet oidc.KeySet
}

func (o *OpenIDConnect) Validate(ctxt echo.Context) (result *ValidationResult, err error) {
//get the Jwk url from open id connect url and validate url
openIDConfig, err := GetOpenIDConfig(o.connectURL, o.httpClient)
if err != nil {
return result, err
} else {
if jwks_uri, ok := openIDConfig["jwks_uri"]; ok {
//create key set and verifier
if o.KeySet == nil {
o.KeySet = oidc.NewRemoteKeySet(ctxt.Request().Context(), jwks_uri.(string))
}
keySet := o.KeySet
var algs []string
if talgs, ok := openIDConfig["id_token_signing_alg_values_supported"]; ok {
for _, alg := range talgs.([]interface{}) {
algs = append(algs, alg.(string))
}

}
if talgs, ok := openIDConfig["request_object_signing_alg_values_supported"]; ok {
for _, alg := range talgs.([]interface{}) {
algs = append(algs, alg.(string))
}
}
tokenVerifier := oidc.NewVerifier(o.connectURL, keySet, &oidc.Config{
ClientID: o.clientID,
SupportedSigningAlgs: algs,
SkipClientIDCheck: o.clientID == "",
SkipExpiryCheck: o.skipExpiryCheck,
SkipIssuerCheck: true,
Now: time.Now,
})
authorizationHeader := ctxt.Request().Header.Get("Authorization")
tokenString := strings.Replace(authorizationHeader, "Bearer ", "", -1)
token, err := tokenVerifier.Verify(ctxt.Request().Context(), tokenString)
err = fmt.Errorf("invalid token '%s': %s. Headers '%s'", tokenString, err, ctxt.Request().Header)

var userID string
var role string
var accountID string
var applicationID string

if token != nil {
tclaims := make(map[string]interface{})
tclaims[o.userIDClaim] = token.Subject
tclaims[o.roleClaim] = ""
if o.accountClaim != "" {
tclaims[o.accountClaim] = ""
}
if o.applicationClaim != "" {
tclaims[o.applicationClaim] = ""
}
err = token.Claims(&tclaims)
if err == nil {
role = tclaims[o.roleClaim].(string)
userID = tclaims[o.userIDClaim].(string)
if o.accountClaim != "" {
accountID = tclaims[o.accountClaim].(string)
}
if o.applicationClaim != "" {
applicationID = tclaims[o.applicationClaim].(string)
}
}
}

return &ValidationResult{
Valid: token != nil && err == nil,
Token: tokenString,
UserID: userID,
Role: role,
AccountID: accountID,
ApplicationID: applicationID,
}, err
} else {
return result, fmt.Errorf("expected jwks_url to be set")
}
}
}

func (o *OpenIDConnect) FromSchema(ctxt context.Context, scheme *openapi3.SecurityScheme, httpClient *http.Client) (Validator, error) {
var err error
o.httpClient = httpClient
o.connectURL = scheme.OpenIdConnectUrl

if tinterface, ok := scheme.Extensions[SkipExpiryCheckExtension]; ok {
if expiryCheck, ok := tinterface.(json.RawMessage); ok {
err = json.Unmarshal(expiryCheck, &o.skipExpiryCheck)
}
}

if jwtMapRaw, ok := scheme.Extensions[JWTMapExtension]; ok {
if user, ok := jwtMapRaw.(map[string]interface{})["user"]; ok {
o.userIDClaim = user.(string)
}
if value, ok := jwtMapRaw.(map[string]interface{})["role"]; ok {
o.roleClaim = value.(string)
}
if value, ok := jwtMapRaw.(map[string]interface{})["account"]; ok {
o.accountClaim = value.(string)
}
if value, ok := jwtMapRaw.(map[string]interface{})["application"]; ok {
o.applicationClaim = value.(string)
}
} else {
o.userIDClaim = "sub"
}

openIDConfig, err := GetOpenIDConfig(o.connectURL, o.httpClient)
if err != nil {
return o, fmt.Errorf("invalid open id connect url: '%s'", o.connectURL)
}
if jwks_uri, ok := openIDConfig["jwks_uri"]; ok {
//create key set and verifier
o.KeySet = oidc.NewRemoteKeySet(ctxt, jwks_uri.(string))
}
return o, err
}

type OAuth2 struct {
connectURL string
Flows *openapi3.OAuthFlows
clientSecret string
}

func (o *OAuth2) Validate(ctxt echo.Context) (*ValidationResult, error) {
authorizationHeader := ctxt.Request().Header.Get("Authorization")
tokenString := strings.Replace(authorizationHeader, "Bearer ", "", -1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

//TODO figure out good way to load certificate here
cert := ``

block, _ := pem.Decode([]byte(cert))
if block == nil {
return nil, fmt.Errorf("unable to decode cert")
}

pub, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key '%s'", err)
}

return pub.PublicKey, nil
})
return &ValidationResult{
Valid: token.Valid,
}, err
}

func (o *OAuth2) FromSchema(ctxt context.Context, scheme *openapi3.SecurityScheme, client *http.Client) (Validator, error) {
var err error
o.Flows = scheme.Flows
return o, err
}
Loading

0 comments on commit 6052d5a

Please sign in to comment.