Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT Verification #280

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions configs/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package configs

import (
"net/url"
"time"

"github.com/pkg/errors"
"github.com/unbasical/kelon/internal/pkg/util"
)

type JwtAuthentication struct {
JwksStringURLs []string `yaml:"jwks_urls"`
JwksMaxWait time.Duration `yaml:"jwks_max_wait"`
JwksTTL time.Duration `yaml:"jwks_ttl"`
TargetAudience []string `yaml:"target_audience"`
TrustedIssuers []string `yaml:"trusted_issuers"`
AllowedAlgorithms []string `yaml:"allowed_algorithms"`
RequiredScopes []string `yaml:"required_scopes"`
ScopeStrategy string `yaml:"scope_strategy"`
TokenFrom string `yaml:"token_from"`
JwksURLs []*url.URL `yaml:"-"`
}

func (c *JwtAuthentication) UnmarshalYAML(unmarshal func(interface{}) error) error {
var partial struct {
JwksStringURLs []string `yaml:"jwks_urls"`
JwksMaxWait time.Duration `yaml:"jwks_max_wait"`
JwksTTL time.Duration `yaml:"jwks_ttl"`
TargetAudience []string `yaml:"target_audience"`
TrustedIssuers []string `yaml:"trusted_issuers"`
AllowedAlgorithms []string `yaml:"allowed_algorithms"`
RequiredScopes []string `yaml:"required_scopes"`
ScopeStrategy string `yaml:"scope_strategy"`
}

err := unmarshal(&partial)
if err != nil {
return err
}

c.JwksStringURLs = partial.JwksStringURLs
c.JwksMaxWait = partial.JwksMaxWait
c.JwksTTL = partial.JwksTTL
c.TargetAudience = partial.TargetAudience
c.TrustedIssuers = partial.TrustedIssuers
c.AllowedAlgorithms = partial.AllowedAlgorithms
c.RequiredScopes = partial.RequiredScopes
c.ScopeStrategy = partial.ScopeStrategy
c.JwksURLs = make([]*url.URL, 0, len(c.JwksStringURLs))

for _, strURL := range c.JwksStringURLs {
u, urlErr := url.Parse(strURL)
if urlErr != nil {
return errors.Wrapf(err, "unable to parse [%s] to url", strURL)
}

c.JwksURLs = append(c.JwksURLs, util.RelativeFileURLToAbsolute(u))
}

return nil
}
17 changes: 9 additions & 8 deletions configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ type AppConfig struct {

// ExternalConfig holds all externally configurable properties
type ExternalConfig struct {
APIMappings []*DatastoreAPIMapping `yaml:"apis"`
Datastores map[string]*Datastore `yaml:"datastores"`
DatastoreSchemas map[string]map[string]*EntitySchema `yaml:"entity_schemas"`
OPA interface{} `yaml:"opa"`
APIMappings []*DatastoreAPIMapping `yaml:"apis"`
Datastores map[string]*Datastore `yaml:"datastores"`
DatastoreSchemas map[string]map[string]*EntitySchema `yaml:"entity_schemas"`
JwtAuthenticators map[string]*JwtAuthentication `yaml:"jwt"`
OPA interface{} `yaml:"opa"`
}

func (ec *ExternalConfig) Defaults() {
Expand Down Expand Up @@ -93,14 +94,14 @@ func (l FileConfigLoader) Load() (*ExternalConfig, error) {

// Load configBy from file
var (
ioError error
datastoreBytes []byte
ioError error
fileBytes []byte
)
if datastoreBytes, ioError = os.ReadFile(l.FilePath); ioError != nil {
if fileBytes, ioError = os.ReadFile(l.FilePath); ioError != nil {
return nil, ioError
}

return ByteConfigLoader{
FileBytes: datastoreBytes,
FileBytes: fileBytes,
}.Load()
}
22 changes: 22 additions & 0 deletions configs/config_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package configs_test

import (
"net/url"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/unbasical/kelon/configs"
"github.com/unbasical/kelon/internal/pkg/util"
)

//nolint:gochecknoglobals,gocritic
Expand Down Expand Up @@ -76,6 +79,25 @@ var wantedExternalConfig = configs.ExternalConfig{
},
},
},
JwtAuthenticators: map[string]*configs.JwtAuthentication{
"example": {
JwksStringURLs: []string{
"file:///path/to/jwks.json",
"https://example.domain.com/.well-known/openid-configuration",
},
JwksMaxWait: time.Millisecond * 100,
JwksTTL: time.Minute * 30,
TargetAudience: []string{"aud-1", "aud-2"},
TrustedIssuers: []string{"iss-1"},
AllowedAlgorithms: []string{"HS256", "RS256"},
RequiredScopes: []string{"scope-1"},
ScopeStrategy: "exact",
JwksURLs: []*url.URL{
util.MustParseURL("file:///path/to/jwks.json"),
util.MustParseURL("https://example.domain.com/.well-known/openid-configuration"),
},
},
},
OPA: struct{}{},
}

Expand Down
20 changes: 20 additions & 0 deletions configs/testdata/valid.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,23 @@ entity_schemas:
- name: users
- name: user_followers
alias: followers

# JWT Authenticator config
jwt:
example:
jwks_urls:
- "file:///path/to/jwks.json"
- "https://example.domain.com/.well-known/openid-configuration"
jwks_max_wait: 100ms
jwks_ttl: 30m
target_audience:
- "aud-1"
- "aud-2"
trusted_issuers:
- "iss-1"
allowed_algorithms:
- "HS256"
- "RS256"
required_scopes:
- "scope-1"
scope_strategy: "exact"
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ services:
- ./examples/docker-compose/config/:/conf
- ./call-operands/:/call-operands
- ./examples/docker-compose/policies/:/policies
- ./examples/docker-compose/jwks/:/jwks
environment:
- DATASTORE_CONF=/conf/datastore.yml
- API_CONF=/conf/api.yml
Expand Down
19 changes: 19 additions & 0 deletions examples/docker-compose/config/kelon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,25 @@ entity_schemas:
- name: user
alias: users

# JWT Authenticator config
jwt:
example:
jwks_urls:
- "file:///jwks/jwks-hs.json"
jwks_max_wait: 100ms
jwks_ttl: 30m
target_audience:
- "aud-1"
- "aud-2"
trusted_issuers:
- "iss-1"
- "iss-2"
allowed_algorithms:
- "HS256"
required_scopes:
- "required"
scope_strategy: "exact"

opa:
labels:
app: Kelon
Expand Down
11 changes: 11 additions & 0 deletions examples/docker-compose/jwks/jwks-hs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"keys": [
{
"kty": "oct",
"kid": "64f48a05-7ea5-4aaf-80cf-f06b0033b477",
"k": "-2UH7JcKyPpR4HLOJCmRbsu1-x0gltAhfJaLeFbAaIc",
"alg": "HS256",
"use": "sig"
}
]
}
15 changes: 15 additions & 0 deletions examples/docker-compose/policies/jwt_example.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package applications.authn

# Deny all by default
default allow := false

# Path: GET /api/authn/apps/:app_id
# The first app is public
allow {
input.method == "GET"
input.path == ["api", "authn", "apps", "1"]
}

allow {
jwt_verify(input.token, ["example"])
}
90 changes: 90 additions & 0 deletions examples/docker-compose/policies/mixed_example.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package applications.mixed

# Here we mix multiple datastores (MongoDB and Postgres)
# NOTE: Only one datastore can be used in a allow/verify policy

verify {
input.path == ["api", "mixed", "apps", "1"]
}

# Verify using Postgres as Datastore
verify {
some user

data.pg.pg_users[user].name == input.user
user.password = input.password
}

# Deny all by default
allow := false

# Path: GET /api/pg/apps/:app_id
# Datastore: Postgres
# The first app is public
allow {
input.method == "GET"
input.path == ["api", "mixed", "apps", "1"]
}

# Path: GET /api/pg/apps/:app_id
# Datastore: Postgres
# Users with right 'OWNER' on app can access it always
allow {
some app_id, u, r
input.method == "GET"
input.path = ["api", "mixed", "apps", app_id]

# Join
data.pg.pg_users[u].id == data.pg.pg_app_rights[r].user_id

# Where
u.name == input.user
r.right == "OWNER"
r.app_id == app_id
}

# Path: GET /api/pg/apps/:app_id
# Datastore: Postgres
# All apps with 5 stars are public
allow {
some app, app_id
input.method == "GET"
input.path = ["api", "mixed", "apps", app_id]

data.pg.pg_apps[app].id == app_id
app.stars == 5
}

# Path: GET <any>
# Datastore: Mongo
# All users that are a friends of Kevin are allowed see everything
allow {
input.method == "GET"

# Query
data.mongo.users[user].name == input.user
old_or_kevin(user.age, user.friend)
}

# Path: GET /api/pg/apps/:app_id
# Datastore: MongoDB
# Test for count function
allow {
some app
input.method == "GET"
input.path = ["api", "mixed", "apps", "4"]

# Get all apps with 5 starts
data.mongo.apps[app].stars > 4

# If there is any one return true
count(app) > 0
}

old_or_kevin(age, friend) {
age == 42
}

old_or_kevin(age, friend) {
friend == "Kevin"
}
Loading