1
0
mirror of https://github.com/greenpau/caddy-security.git synced 2025-04-18 08:04:02 +03:00

upgrade to github.com/greenpau/go-authcrunc v1.0.20

More info:
* split backends to identity stores and providers
* oauth: use first available key to validate token when kid not found (greenpau/caddy-security#77)
This commit is contained in:
Paul Greenberg 2022-03-30 18:39:01 -04:00
parent e3ef04c4b4
commit 44a1711ec0
29 changed files with 1356 additions and 787 deletions

View File

@ -81,10 +81,10 @@ module github.com/greenpau/caddy-security
go 1.16
require (
github.com/greenpau/go-authcrunch v1.0.19
github.com/greenpau/go-authcrunch v1.0.20
)
replace github.com/greenpau/go-authcrunch v1.0.19 => /home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
replace github.com/greenpau/go-authcrunch v1.0.20 => /home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
```
Then, modify `Makefile` such that that replacement passes to `xcaddy` builder:
@ -93,7 +93,7 @@ Then, modify `Makefile` such that that replacement passes to `xcaddy` builder:
@mkdir -p ../xcaddy-$(PLUGIN_NAME) && cd ../xcaddy-$(PLUGIN_NAME) && \
xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/caddy \
--with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR) \
--with github.com/greenpau/go-authcrunch@v1.0.19=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
--with github.com/greenpau/go-authcrunch@v1.0.20=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
```
Once all the necessary packages are installed, you should be ready to compile

View File

@ -17,7 +17,7 @@ all: info
xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/caddy \
--with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR) \
--with github.com/greenpau/caddy-trace@v1.1.8
@#--with github.com/greenpau/go-authcrunch@v1.0.19=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
@#--with github.com/greenpau/go-authcrunch@v1.0.20=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
@#bin/caddy run -config assets/config/Caddyfile
@for f in `find ./assets -type f -name 'Caddyfile'`; do bin/caddy fmt -overwrite $$f; done
@ -68,8 +68,10 @@ qtest: covdir
@echo "DEBUG: perform quick tests ..."
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestApp ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAppConfig ./*.go
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileIdentity ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileCredentials ./*.go
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileMessaging ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileMessaging ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileIdentit* ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAuthentication ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAuthorization ./*.go
@#go test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfile ./*.go

118
app.go
View File

@ -37,17 +37,15 @@ func init() {
// App implements security manager.
type App struct {
Name string `json:"-"`
Config *authcrunch.Config `json:"config,omitempty"`
logger *zap.Logger
portals []*authn.Portal
gatekeepers []*authz.Gatekeeper
Name string `json:"-"`
Config *authcrunch.Config `json:"config,omitempty"`
server *authcrunch.Server
logger *zap.Logger
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
// ID: caddy.ModuleID("security"),
ID: "security",
New: func() caddy.Module { return new(App) },
}
@ -63,60 +61,15 @@ func (app *App) Provision(ctx caddy.Context) error {
zap.String("app", app.Name),
)
for _, cfg := range app.Config.Portals {
portal, err := authn.NewPortal(cfg, app.logger)
if err != nil {
app.logger.Error(
"failed initializing auth portal",
zap.String("app", app.Name),
zap.String("portal_name", cfg.Name),
zap.Error(err),
)
return err
}
if app.Config.Credentials != nil {
portal.SetCredentials(app.Config.Credentials)
}
if app.Config.Messaging != nil {
portal.SetMessaging(app.Config.Messaging)
}
if err := portal.Register(); err != nil {
app.logger.Error(
"failed registering auth portal",
zap.String("app", app.Name),
zap.String("portal_name", cfg.Name),
zap.Error(err),
)
return err
}
app.portals = append(app.portals, portal)
}
for _, cfg := range app.Config.Policies {
gatekeeper, err := authz.NewGatekeeper(cfg, app.logger)
if err != nil {
app.logger.Error(
"failed initializing gatekeeper",
zap.String("app", app.Name),
zap.String("gatekeeper_name", cfg.Name),
zap.Error(err),
)
return err
}
if err := gatekeeper.Register(); err != nil {
app.logger.Error(
"failed registering gatekeeper",
zap.String("app", app.Name),
zap.String("gatekeeper_name", cfg.Name),
zap.Error(err),
)
return err
}
app.gatekeepers = append(app.gatekeepers, gatekeeper)
server, err := authcrunch.NewServer(app.Config, app.logger)
if err != nil {
app.logger.Error(
"failed provisioning app server instance",
zap.String("app", app.Name),
zap.Error(err),
)
}
app.server = server
app.logger.Info(
"provisioned app instance",
@ -127,57 +80,26 @@ func (app *App) Provision(ctx caddy.Context) error {
// Start starts the App.
func (app App) Start() error {
app.logger.Debug(
"starting app instance",
zap.String("app", app.Name),
)
/*
if msgs := app.manager.Start(); msgs != nil {
for _, msg := range msgs {
app.logger.Error(
"failed managing git repo",
zap.String("app", app.Name),
zap.String("repo", msg.Repository),
zap.Error(msg.Error),
)
}
return fmt.Errorf("git repo manager failed to start")
}
*/
app.logger.Debug(
"started app instance",
zap.String("app", app.Name),
)
return nil
}
// Stop stops the App.
func (app App) Stop() error {
app.logger.Debug(
"stopping app instance",
zap.String("app", app.Name),
)
/*
if msgs := app.manager.Stop(); msgs != nil {
for _, msg := range msgs {
app.logger.Error(
"failed stoppint git repo manager",
zap.String("app", app.Name),
zap.String("repo", msg.Repository),
zap.Error(msg.Error),
)
}
return fmt.Errorf("git repo manager failed to stop properly")
}
*/
app.logger.Debug(
"stopped app instance",
zap.String("app", app.Name),
)
return nil
}
func (app *App) getPortal(s string) (*authn.Portal, error) {
return app.server.GetPortalByName(s)
}
func (app *App) getGatekeeper(s string) (*authz.Gatekeeper, error) {
return app.server.GetGatekeeperByName(s)
}

View File

@ -18,10 +18,15 @@
bcc greenpau@localhost
}
local identity store localdb {
realm local
path assets/config/users.json
}
authentication portal myportal {
crypto default token lifetime 3600
crypto key sign-verify 01ee2688-36e4-47f9-8c06-d18483702520
backend local assets/config/users.json local
enable identity store localdb
ui {
links {
"My Website" "/app" icon "las la-star"

View File

@ -36,6 +36,8 @@ func init() {
//
// security {
// credentials ...
// identity store <name>
// [saml|oauth] identity provider <name>
// authentication ...
// authorization ...
// }
@ -60,6 +62,10 @@ func parseCaddyfile(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
if err := parseCaddyfileMessaging(d, repl, app.Config); err != nil {
return nil, err
}
case "local", "ldap", "oauth", "saml":
if err := parseCaddyfileIdentity(d, repl, app.Config, tld); err != nil {
return nil, err
}
case "authentication":
if err := parseCaddyfileAuthentication(d, repl, app.Config); err != nil {
return nil, err

View File

@ -15,6 +15,7 @@
package security
import (
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/caddy-security/pkg/util"
@ -38,41 +39,6 @@ const (
//
// authentication portal <name> {
//
// backend local <file/path/to/user/db> <realm/name>
// backend local {
// method <local>
// file <file_path>
// realm <name>
// }
//
// backend oauth2_generic {
// method oauth2
// realm generic
// provider generic
// base_auth_url <base_url>
// metadata_url <metadata_url>
// client_id <client_id>
// client_secret <client_secret>
// scopes openid email profile
// disable metadata_discovery
// authorization_url <authorization_url>
// disable key_verification
// }
//
// backend gitlab {
// method oauth2
// realm gitlab
// provider gitlab
// domain_name <domain>
// client_id <client_id>
// client_secret <client_secret>
// user_group_filters <regex_pattern>
// }
//
// backend google <client_id> <client_secret>
// backend github <client_id> <client_secret>
// backend facebook <client_id> <client_secret>
//
// crypto key sign-verify <shared_secret>
//
// ui {
@ -106,11 +72,14 @@ const (
//
// enable source ip tracking
// enable admin api
// enable identity store <name>
// enable identity provider <name>
// }
//
func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config) error {
// rootDirective is config key prefix.
var rootDirective string
backendHelpURL := "https://github.com/greenpau/caddy-security/issues/83"
args := util.FindReplaceAll(repl, d.RemainingArgs())
if len(args) != 2 {
return d.ArgErr()
@ -140,14 +109,8 @@ func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer,
if err := parseCaddyfileAuthPortalCookie(d, repl, p, rootDirective, v); err != nil {
return err
}
case "backend":
if err := parseCaddyfileAuthPortalBackendShortcuts(d, repl, p, rootDirective, v); err != nil {
return err
}
case "backends":
if err := parseCaddyfileAuthPortalBackends(d, repl, p, rootDirective); err != nil {
return err
}
case "backend", "backends":
return fmt.Errorf("The backend directive is no longer supported. Please see %s for details", backendHelpURL)
case "ui":
if err := parseCaddyfileAuthPortalUI(d, repl, p, rootDirective); err != nil {
return err

View File

@ -1,89 +0,0 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/caddy-security/pkg/util"
"github.com/greenpau/go-authcrunch/pkg/authn"
"github.com/greenpau/go-authcrunch/pkg/authn/backends"
"github.com/greenpau/go-authcrunch/pkg/errors"
)
func parseCaddyfileAuthPortalBackendShortcuts(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, ckp string, args []string) error {
v := util.FindReplaceAll(repl, args)
if len(v) == 0 {
return errors.ErrConfigDirectiveShort.WithArgs(ckp, v)
}
if v[len(v)-1] == "disabled" {
return nil
}
m := make(map[string]interface{})
switch v[0] {
case "local":
if len(v) != 3 {
return errors.ErrMalformedDirective.WithArgs(ckp, v)
}
m["name"] = fmt.Sprintf("local_backend_%d", len(portal.BackendConfigs))
m["method"] = "local"
m["path"] = v[1]
m["realm"] = v[2]
case "google":
if len(v) != 3 {
return errors.ErrMalformedDirective.WithArgs(ckp, v)
}
m["name"] = fmt.Sprintf("google_backend_%d", len(portal.BackendConfigs))
m["method"] = "oauth2"
m["realm"] = "google"
m["provider"] = "google"
m["client_id"] = v[1]
m["client_secret"] = v[2]
m["scopes"] = []string{"openid", "email", "profile"}
case "github":
if len(v) != 3 {
return errors.ErrMalformedDirective.WithArgs(ckp, v)
}
m["name"] = fmt.Sprintf("github_backend_%d", len(portal.BackendConfigs))
m["method"] = "oauth2"
m["realm"] = "github"
m["provider"] = "github"
m["client_id"] = v[1]
m["client_secret"] = v[2]
m["scopes"] = []string{"read:user"}
case "facebook":
if len(v) != 3 {
return errors.ErrMalformedDirective.WithArgs(ckp, v)
}
m["name"] = fmt.Sprintf("facebook_backend_%d", len(portal.BackendConfigs))
m["method"] = "oauth2"
m["realm"] = "facebook"
m["provider"] = "facebook"
m["client_id"] = v[1]
m["client_secret"] = v[2]
m["scopes"] = []string{"email"}
default:
return errors.ErrConfigDirectiveValueUnsupported.WithArgs(ckp, v)
}
backendConfig, err := backends.NewConfig(m)
if err != nil {
return errors.ErrConfigDirectiveFail.WithArgs(ckp, v, err)
}
portal.BackendConfigs = append(portal.BackendConfigs, *backendConfig)
return nil
}

View File

@ -1,214 +0,0 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/caddy-security/pkg/util"
"github.com/greenpau/go-authcrunch/pkg/authn"
"github.com/greenpau/go-authcrunch/pkg/authn/backends"
"strconv"
"strings"
)
func parseCaddyfileAuthPortalBackends(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string) error {
for nesting := h.Nesting(); h.NextBlock(nesting); {
backendName := h.Val()
cfg := make(map[string]interface{})
cfg["name"] = backendName
backendDisabled := false
var backendAuthMethod string
for subNesting := h.Nesting(); h.NextBlock(subNesting); {
backendArg := h.Val()
switch backendArg {
case "method", "type":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
backendAuthMethod = h.Val()
cfg["method"] = backendAuthMethod
case "trusted_authority":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
var trustedAuthorities []string
if v, exists := cfg["trusted_authorities"]; exists {
trustedAuthorities = v.([]string)
}
trustedAuthorities = append(trustedAuthorities, h.Val())
cfg["trusted_authorities"] = trustedAuthorities
case "disabled":
backendDisabled = true
break
case "username":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg["bind_username"] = util.FindReplace(repl, h.Val())
case "password":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg["bind_password"] = util.FindReplace(repl, h.Val())
case "search_base_dn", "search_group_filter", "path", "realm":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg[backendArg] = util.FindReplace(repl, h.Val())
case "search_filter", "search_user_filter":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg["search_user_filter"] = util.FindReplace(repl, h.Val())
case "required_token_fields":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg[backendArg] = util.FindReplaceAll(repl, h.RemainingArgs())
case "attributes":
attrMap := make(map[string]interface{})
for attrNesting := h.Nesting(); h.NextBlock(attrNesting); {
attrName := h.Val()
if !h.NextArg() {
return backendPropErr(h, backendName, backendArg, attrName, "has no value")
}
attrMap[attrName] = h.Val()
}
cfg[backendArg] = attrMap
case "servers":
serverMaps := []map[string]interface{}{}
for serverNesting := h.Nesting(); h.NextBlock(serverNesting); {
serverMap := make(map[string]interface{})
serverMap["address"] = h.Val()
serverProps := h.RemainingArgs()
if len(serverProps) > 0 {
for _, serverProp := range serverProps {
switch serverProp {
case "ignore_cert_errors", "posix_groups":
serverMap[serverProp] = true
default:
return backendPropErr(h, backendName, backendArg, serverProp, "is unsupported")
}
}
}
serverMaps = append(serverMaps, serverMap)
}
cfg[backendArg] = serverMaps
case "groups":
groupMaps := []map[string]interface{}{}
for groupNesting := h.Nesting(); h.NextBlock(groupNesting); {
groupMap := make(map[string]interface{})
groupDN := h.Val()
groupMap["dn"] = groupDN
groupRoles := h.RemainingArgs()
if len(groupRoles) == 0 {
return backendPropErr(h, backendName, backendArg, groupDN, "has no roles")
}
groupMap["roles"] = groupRoles
groupMaps = append(groupMaps, groupMap)
}
cfg[backendArg] = groupMaps
case "provider":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg[backendArg] = h.Val()
case "idp_metadata_location", "idp_sign_cert_location", "tenant_id", "idp_login_url",
"application_id", "application_name", "entity_id", "domain_name",
"client_id", "client_secret", "server_id", "base_auth_url", "metadata_url",
"identity_token_name", "authorization_url", "token_url":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
cfg[backendArg] = util.FindReplace(repl, h.Val())
case "acs_url":
if !h.NextArg() {
return backendValueErr(h, backendName, backendArg)
}
var acsURLs []string
if v, exists := cfg["acs_urls"]; exists {
acsURLs = v.([]string)
}
acsURLs = append(acsURLs, h.Val())
cfg["acs_urls"] = acsURLs
case "scopes", "user_group_filters", "user_org_filters", "response_type":
if _, exists := cfg[backendArg]; exists {
values := cfg[backendArg].([]string)
values = append(values, h.RemainingArgs()...)
cfg[backendArg] = values
} else {
cfg[backendArg] = h.RemainingArgs()
}
case "delay_start", "retry_attempts", "retry_interval":
backendVal := strings.Join(h.RemainingArgs(), "|")
i, err := strconv.Atoi(backendVal)
if err != nil {
return backendValueConversionErr(h, backendName, backendArg, backendVal, err)
}
cfg[backendArg] = i
case "disable":
backendVal := strings.Join(h.RemainingArgs(), "_")
switch backendVal {
case "metadata_discovery":
case "key_verification":
case "pass_grant_type":
case "response_type":
case "scope":
case "nonce":
default:
return backendPropErr(h, backendName, backendArg, backendVal, "is unsupported")
}
cfg[backendVal+"_disabled"] = true
case "enable":
backendVal := strings.Join(h.RemainingArgs(), "_")
switch backendVal {
case "accept_header":
case "js_callback":
default:
return backendPropErr(h, backendName, backendArg, backendVal, "is unsupported")
}
cfg[backendVal+"_enabled"] = true
default:
return backendUnsupportedValueErr(h, backendName, backendArg)
}
}
if !backendDisabled {
backendConfig, err := backends.NewConfig(cfg)
if err != nil {
return h.Errf("auth backend %s directive failed: %v", rootDirective, err.Error())
}
portal.BackendConfigs = append(portal.BackendConfigs, *backendConfig)
}
}
return nil
}
func backendValueErr(h *caddyfile.Dispenser, backendName, backendArg string) error {
return h.Errf("auth backend %s subdirective %s has no value", backendName, backendArg)
}
func backendUnsupportedValueErr(h *caddyfile.Dispenser, backendName, backendArg string) error {
return h.Errf("auth backend %s subdirective %s is unsupported", backendName, backendArg)
}
func backendPropErr(h *caddyfile.Dispenser, backendName, backendArg, attrName, attrErr string) error {
return h.Errf("auth backend %q subdirective %q key %q %s", backendName, backendArg, attrName, attrErr)
}
func backendValueConversionErr(h *caddyfile.Dispenser, backendName, k, v string, err error) error {
return h.Errf("auth backend %s subdirective %s value %q error: %v", backendName, k, v, err)
}

View File

@ -26,14 +26,30 @@ func parseCaddyfileAuthPortalMisc(h *caddyfile.Dispenser, repl *caddy.Replacer,
v = strings.TrimSpace(v)
switch k {
case "enable":
switch v {
case "source ip tracking":
switch {
case v == "source ip tracking":
portal.TokenGrantorOptions.EnableSourceAddress = true
case "admin api":
case v == "admin api":
if portal.API == nil {
portal.API = &authn.APIConfig{}
portal.API.Enabled = true
}
case strings.HasPrefix(v, "identity provider"):
if len(args) < 3 {
return h.Errf("malformed directive for %s: %s", rootDirective, v)
}
for _, providerName := range args[2:] {
portal.IdentityProviders = append(portal.IdentityProviders, providerName)
}
return nil
case strings.HasPrefix(v, "identity store"):
if len(args) < 3 {
return h.Errf("malformed directive for %s: %s", rootDirective, v)
}
for _, storeName := range args[2:] {
portal.IdentityStores = append(portal.IdentityStores, storeName)
}
return nil
default:
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
}

View File

@ -36,10 +36,10 @@ func TestParseCaddyfileAuthentication(t *testing.T) {
name: "test valid authentication portal config",
d: caddyfile.NewTestDispenser(`
security {
authentication portal myportal {
crypto default token lifetime 3600
crypto key sign-verify 01ee2688-36e4-47f9-8c06-d18483702520
backend local assets/config/users.json local
cookie domain contoso.com
ui {
links {
@ -61,255 +61,205 @@ func TestParseCaddyfileAuthentication(t *testing.T) {
}
enable source ip tracking
validate source address
enable identity provider contoso.com example.com
enable identity provider azure okta
}
backends {
ldap_backend {
method ldap
realm contoso.com
servers {
ldaps://ldaps.contoso.com ignore_cert_errors
}
attributes {
name givenName
surname sn
username sAMAccountName
member_of memberOf
email mail
}
username "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM"
password "P@ssW0rd123"
search_base_dn "DC=CONTOSO,DC=COM"
search_filter "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))"
groups {
"CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" admin
"CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" editor
"CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" viewer
}
ldap identity store contoso.com {
realm contoso.com
servers {
ldaps://ldaps.contoso.com ignore_cert_errors
}
attributes {
name givenName
surname sn
username sAMAccountName
member_of memberOf
email mail
}
ldap_backend2 {
method ldap
realm example.com
servers {
ldap://ldap.forumsys.com posix_groups
}
attributes {
name cn
surname foo
username uid
member_of uniqueMember
email mail
}
username "cn=read-only-admin,dc=example,dc=com"
password "password"
search_base_dn "DC=EXAMPLE,DC=COM"
search_filter "(&(|(uid=%s)(mail=%s))(objectClass=inetOrgPerson))"
groups {
"ou=mathematicians,dc=example,dc=com" authp/admin
"ou=scientists,dc=example,dc=com" authp/user
}
}
azure_saml_backend {
method saml
provider azure
realm azure
idp_metadata_location assets/conf/saml/azure/idp/azure_ad_app_metadata.xml
idp_sign_cert_location assets/conf/saml/azure/idp/azure_ad_app_signing_cert.pem
tenant_id "1b9e886b-8ff2-4378-b6c8-6771259a5f51"
application_id "623cae7c-e6b2-43c5-853c-2059c9b2cb58"
application_name "My Gatekeeper"
entity_id "urn:caddy:mygatekeeper"
acs_url https://mygatekeeper/saml
acs_url https://mygatekeeper.local/saml
acs_url https://192.168.10.10:3443/saml
acs_url https://localhost:3443/saml
}
okta_oauth2_backend {
method oauth2
realm okta
provider okta
domain_name dev-680653.okta.com
client_id 0oa121qw81PJW0Tj34x7
client_secret b3aJC5E59hU18YKC7Yca3994F4qFhWiAo_ZojanF
server_id default
scopes openid email profile groups
username "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM"
password "P@ssW0rd123"
search_base_dn "DC=CONTOSO,DC=COM"
search_filter "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))"
groups {
"CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" admin
"CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" editor
"CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" viewer
}
}
}
ldap identity store example.com {
realm example.com
servers {
ldap://ldap.forumsys.com posix_groups
}
attributes {
name cn
surname foo
username uid
member_of uniqueMember
email mail
}
username "cn=read-only-admin,dc=example,dc=com"
password "password"
search_base_dn "DC=EXAMPLE,DC=COM"
search_filter "(&(|(uid=%s)(mail=%s))(objectClass=inetOrgPerson))"
groups {
"ou=mathematicians,dc=example,dc=com" authp/admin
"ou=scientists,dc=example,dc=com" authp/user
}
}
saml identity provider azure {
method saml
provider azure
realm azure
idp_metadata_location assets/conf/saml/azure/idp/azure_ad_app_metadata.xml
idp_sign_cert_location assets/conf/saml/azure/idp/azure_ad_app_signing_cert.pem
tenant_id "1b9e886b-8ff2-4378-b6c8-6771259a5f51"
application_id "623cae7c-e6b2-43c5-853c-2059c9b2cb58"
application_name "My Gatekeeper"
entity_id "urn:caddy:mygatekeeper"
acs_url https://mygatekeeper/saml
acs_url https://mygatekeeper.local/saml
acs_url https://192.168.10.10:3443/saml
acs_url https://localhost:3443/saml
}
oauth identity provider okta {
realm okta
provider okta
domain_name dev-680653.okta.com
client_id 0oa121qw81PJW0Tj34x7
client_secret b3aJC5E59hU18YKC7Yca3994F4qFhWiAo_ZojanF
server_id default
scopes openid email profile groups
}
}`),
want: `{
"config": {
"auth_portal_configs": [
{
"name": "myportal",
"ui": {
"private_links": [
{"link": "/app", "title": "My Website", "icon_name": "las la-star", "icon_enabled": true},
{"link": "/auth/whoami", "title": "My Identity", "icon_name": "las la-user", "icon_enabled": true}
]
},
"user_registration_config": {
"code": "NY2020",
"dropbox": "assets/config/registrations.json",
"require_accept_terms": true,
"require_domain_mx": true,
"title": "User Registration"
"config": {
"authentication_portals": [
{
"name": "myportal",
"ui": {
"private_links": [
{
"link": "/app",
"title": "My Website",
"icon_name": "las la-star",
"icon_enabled": true
},
{
"link": "/auth/whoami",
"title": "My Identity",
"icon_name": "las la-user",
"icon_enabled": true
}
]
},
"user_transformer_configs": [
{
"matchers": ["exact match origin local"],
"actions": [
"action add role authp/user",
"ui link \"Portal Settings\" /auth/settings icon \"las la-cog\""
]
}
],
"cookie_config": {
"user_registration_config": {
"title": "User Registration",
"code": "NY2020",
"dropbox": "assets/config/registrations.json",
"require_accept_terms": true,
"require_domain_mx": true
},
"user_transformer_configs": [
{
"matchers": [
"exact match origin local"
],
"actions": [
"action add role authp/user",
"ui link \"Portal Settings\" /auth/settings icon \"las la-cog\""
]
}
],
"cookie_config": {
"domains": {
"contoso.com": {
"seq": 1,
"contoso.com": {
"seq": 1,
"domain": "contoso.com"
}
}
},
"backend_configs": [
{
"local": {
"name": "local_backend_0",
"method": "local",
"realm": "local",
"path": "assets/config/users.json"
}
},
{
"ldap": {
"name": "ldap_backend",
"method": "ldap",
"realm": "contoso.com",
"bind_password": "P@ssW0rd123",
"bind_username": "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM",
"search_base_dn": "DC=CONTOSO,DC=COM",
"search_user_filter": "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))",
"servers": [
{
"address": "ldaps://ldaps.contoso.com",
"ignore_cert_errors": true
}
],
"attributes": {
"email": "mail",
"member_of": "memberOf",
"name": "givenName",
"username": "sAMAccountName",
"surname": "sn"
},
"groups": [
{
"dn": "CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": ["admin"]
},
{
"dn": "CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": ["editor"]
},
{
"dn": "CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": ["viewer"]
}
]
}
},
{
"ldap": {
"name": "ldap_backend2",
"method": "ldap",
"realm": "example.com",
"servers": [
{
"address": "ldap://ldap.forumsys.com",
"posix_groups": true
}
],
"attributes": {
"name": "cn",
"surname": "foo",
"username": "uid",
"member_of": "uniqueMember",
"email": "mail"
},
"bind_password": "password",
"bind_username": "cn=read-only-admin,dc=example,dc=com",
"search_base_dn": "DC=EXAMPLE,DC=COM",
"search_user_filter": "(&(|(uid=%s)(mail=%s))(objectClass=inetOrgPerson))",
"groups": [
{
"dn": "ou=mathematicians,dc=example,dc=com",
"roles": ["authp/admin"]
},
{
"dn": "ou=scientists,dc=example,dc=com",
"roles": ["authp/user"]
}
]
}
},
{
"saml": {
"name": "azure_saml_backend",
"method": "saml",
"realm": "azure",
"provider": "azure",
"idp_metadata_location": "assets/conf/saml/azure/idp/azure_ad_app_metadata.xml",
"idp_sign_cert_location": "assets/conf/saml/azure/idp/azure_ad_app_signing_cert.pem",
"tenant_id": "1b9e886b-8ff2-4378-b6c8-6771259a5f51",
"application_id": "623cae7c-e6b2-43c5-853c-2059c9b2cb58",
"application_name": "My Gatekeeper",
"entity_id": "urn:caddy:mygatekeeper",
"acs_urls": [
"https://mygatekeeper/saml",
"https://mygatekeeper.local/saml",
"https://192.168.10.10:3443/saml",
"https://localhost:3443/saml"
]
}
},
{
"oauth2": {
"name": "okta_oauth2_backend",
"method": "oauth2",
"realm": "okta",
"provider": "okta",
"domain_name": "dev-680653.okta.com",
"client_id": "0oa121qw81PJW0Tj34x7",
"client_secret": "b3aJC5E59hU18YKC7Yca3994F4qFhWiAo_ZojanF",
"server_id": "default",
"scopes": ["openid", "email", "profile", "groups"]
}
}
],
"token_validator_options": {
},
"identity_providers": [
"contoso.com",
"example.com",
"azure",
"okta"
],
"token_validator_options": {
"validate_source_address": true
},
"crypto_key_configs": [
{
"id": "0",
"usage": "sign-verify",
"token_name": "access_token",
"source": "config",
"algorithm": "hmac",
"token_lifetime": 3600,
"token_secret": "01ee2688-36e4-47f9-8c06-d18483702520"
}
],
"crypto_key_store_config": {
"token_lifetime": 3600
},
"token_grantor_options": {
"crypto_key_configs": [
{
"id": "0",
"usage": "sign-verify",
"token_name": "access_token",
"source": "config",
"algorithm": "hmac",
"token_lifetime": 3600,
"token_secret": "01ee2688-36e4-47f9-8c06-d18483702520"
}
],
"crypto_key_store_config": {
"token_lifetime": 3600
},
"token_grantor_options": {
"enable_source_address": true
}
}
]
}
}`,
}
],
"identity_stores": [
{
"name": "contoso.com",
"kind": "ldap",
"params": {
"attributes": {
"email": "mail",
"member_of": "memberOf",
"name": "givenName",
"surname": "sn",
"username": "sAMAccountName"
},
"bind_password": "P@ssW0rd123",
"bind_username": "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM",
"groups": [
{
"dn": "CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": [
"admin"
]
},
{
"dn": "CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": [
"editor"
]
},
{
"dn": "CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": [
"viewer"
]
}
],
"realm": "contoso.com",
"search_base_dn": "DC=CONTOSO,DC=COM",
"search_user_filter": "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))",
"servers": [
{
"address": "ldaps://ldaps.contoso.com",
"ignore_cert_errors": true
}
]
}
}
]
}
}`,
},
{
name: "test malformed authentication portal definition",
@ -386,12 +336,12 @@ func TestParseCaddyfileAuthentication(t *testing.T) {
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
// t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
got := unpack(t, string(app.(httpcaddyfile.App).Value))
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {
t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
t.Errorf("parseCaddyfileAuthentication() mismatch (-want +got):\n%s", diff)
}
})

View File

@ -44,7 +44,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"auth_url_path": "/auth",
"auth_redirect_query_param": "redirect_url",
@ -98,7 +98,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"name": "mypolicy",
"auth_url_path": "/auth",
@ -182,7 +182,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"name": "mypolicy2",
"auth_url_path": "/auth",
@ -250,7 +250,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"name": "mypolicy",
"auth_url_path": "/auth",
@ -298,7 +298,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"name": "mypolicy",
"auth_url_path": "/auth",
@ -325,17 +325,26 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
security {
authorization policy mypolicy {
enable login hint
allow roles authp/admin authp/user
}
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"name": "mypolicy",
"auth_url_path": "/auth",
"auth_redirect_query_param": "redirect_url",
"auth_redirect_status_code": 302,
"login_hint_validators": ["email", "phone", "alphanumeric"]
"login_hint_validators": ["email", "phone", "alphanumeric"],
"access_list_rules": [
{
"conditions": [
"match roles authp/admin authp/user"
],
"action": "allow log debug"
}
]
}
]
}
@ -347,17 +356,26 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
security {
authorization policy mypolicy {
enable login hint with email phone
allow roles authp/admin authp/user
}
}`),
want: `{
"config": {
"authz_policy_configs": [
"authorization_policies": [
{
"name": "mypolicy",
"auth_url_path": "/auth",
"auth_redirect_query_param": "redirect_url",
"auth_redirect_status_code": 302,
"login_hint_validators": ["email", "phone"]
"login_hint_validators": ["email", "phone"],
"access_list_rules": [
{
"conditions": [
"match roles authp/admin authp/user"
],
"action": "allow log debug"
}
]
}
]
}

View File

@ -40,19 +40,24 @@ func TestParseCaddyfileCredentials(t *testing.T) {
username foo
password bar
}
local identity store localdb {
realm local
path /tmp/localdb
}
authentication portal myportal {
enable identity store localdb
}
}`),
want: `{
"config": {
"credentials": {
"generic": [
{
"name": "smtp.contoso.com",
"username": "foo",
"password": "bar"
}
]
"generic": [
{
"name": "smtp.contoso.com",
"username": "foo",
"password": "bar"
}
}
]
}`,
},
{
@ -64,20 +69,25 @@ func TestParseCaddyfileCredentials(t *testing.T) {
password bar
domain contoso.com
}
local identity store localdb {
realm local
path /tmp/localdb
}
authentication portal myportal {
enable identity store localdb
}
}`),
want: `{
"config": {
"credentials": {
"generic": [
{
"name": "smtp.contoso.com",
"username": "foo",
"password": "bar",
"domain": "contoso.com"
}
]
}
}
"generic": [
{
"name": "smtp.contoso.com",
"username": "foo",
"password": "bar",
"domain": "contoso.com"
}
]
}`,
},
{
@ -150,7 +160,9 @@ func TestParseCaddyfileCredentials(t *testing.T) {
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
got := unpack(t, string(app.(httpcaddyfile.App).Value))
fullCfg := unpack(t, string(app.(httpcaddyfile.App).Value))
cfg := fullCfg["config"].(map[string]interface{})
got := cfg["credentials"].(map[string]interface{})
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {

53
caddyfile_identity.go Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/caddy-security/pkg/util"
"github.com/greenpau/go-authcrunch"
"github.com/greenpau/go-authcrunch/pkg/errors"
)
const (
identPrefix = "security.identity"
)
func parseCaddyfileIdentity(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config, kind string) error {
args := util.FindReplaceAll(repl, d.RemainingArgs())
if len(args) < 3 {
return d.ArgErr()
}
switch {
case ((kind == "local" || kind == "ldap") && (args[0] == "identity")):
if args[1] != "store" {
return d.ArgErr()
}
if err := parseCaddyfileIdentityStore(d, repl, cfg, kind, args[2], args[3:]); err != nil {
return err
}
case ((kind == "oauth" || kind == "saml") && (args[0] == "identity")):
if args[1] != "provider" {
return d.ArgErr()
}
if err := parseCaddyfileIdentityProvider(d, repl, cfg, kind, args[2], args[3:]); err != nil {
return err
}
default:
return errors.ErrMalformedDirective.WithArgs(identPrefix, args)
}
return nil
}

View File

@ -0,0 +1,182 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/caddy-security/pkg/util"
"github.com/greenpau/go-authcrunch"
"github.com/greenpau/go-authcrunch/pkg/errors"
"strconv"
"strings"
)
// parseCaddyfileIdentityProvider parses identity provider configuration.
//
// Syntax:
//
// oauth identity provider <name> {
// realm <name>
// driver <name>
// base_auth_url <base_url>
// metadata_url <metadata_url>
// client_id <client_id>
// client_secret <client_secret>
// scopes openid email profile
// disable metadata_discovery
// authorization_url <authorization_url>
// disable key_verification
// }
//
// oauth identity provider <name> {
// realm gitlab
// driver gitlab
// domain_name <domain>
// client_id <client_id>
// client_secret <client_secret>
// user_group_filters <regex_pattern>
// }
//
// saml identity provider <name> {
// realm <name>
// driver <name>
// }
//
func parseCaddyfileIdentityProvider(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config, kind, name string, shortcuts []string) error {
m := make(map[string]interface{})
if len(shortcuts) > 0 {
switch kind {
case "oauth":
switch name {
case "github", "google", "facebook":
if len(shortcuts) != 2 {
return d.Errf("invalid %q shortcut: %v", name, shortcuts)
}
m["realm"] = name
m["driver"] = name
m["client_id"] = shortcuts[0]
m["client_secret"] = shortcuts[1]
default:
return d.Errf("unsupported %q shortcut: %v", name, shortcuts)
}
default:
return d.Errf("unsupported %q shortcut for %q provider type: %v", name, kind, shortcuts)
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
k := d.Val()
args := util.FindReplaceAll(repl, d.RemainingArgs())
rd := mkcp("security."+kind+".identity.provider["+name+"]", k)
switch k {
case "disabled":
return nil
case "realm", "driver", "tenant_id",
// OAuth
"domain_name", "client_id", "client_secret", "server_id", "base_auth_url",
"metadata_url", "identity_token_name", "authorization_url", "token_url",
// SAML
"idp_metadata_location", "idp_sign_cert_location", "idp_login_url",
"application_id", "application_name", "entity_id":
if len(args) != 1 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
}
m[k] = args[0]
case "acs_url":
// SAML only.
if len(args) != 1 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
}
var acsURLs []string
if v, exists := m["acs_urls"]; exists {
acsURLs = v.([]string)
}
acsURLs = append(acsURLs, args[0])
m["acs_urls"] = acsURLs
case "scopes", "user_group_filters", "user_org_filters", "response_type":
// OAuth only.
if v, exists := m[k]; exists {
values := v.([]string)
values = append(values, args...)
m[k] = values
} else {
m[k] = args
}
case "delay_start", "retry_attempts", "retry_interval":
// OAuth only.
if len(args) != 1 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
}
i, err := strconv.Atoi(args[0])
if err != nil {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, err)
}
m[k] = i
case "disable":
// OAuth only.
v := strings.Join(args, "_")
switch v {
case "metadata_discovery", "key_verification", "pass_grant_type",
"response_type", "scope", "nonce":
m[v+"_disabled"] = true
case "tls_verification":
m["tls_insecure_skip_verify"] = true
default:
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "unsupported value")
}
case "enable":
// OAuth only.
v := strings.Join(args, "_")
switch v {
case "accept_header":
case "js_callback":
default:
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "unsupported value")
}
m[v+"_enabled"] = true
case "required_token_fields":
// OAuth only.
if len(args) < 1 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain one or more values")
}
m[k] = args
case "jwks":
if len(args) != 3 {
return errors.ErrMalformedDirective.WithArgs(rd, args)
}
if args[0] != "key" {
return errors.ErrMalformedDirective.WithArgs(rd, args)
}
if v, exists := m["jwks_keys"]; exists {
data := v.(map[string]interface{})
data[args[1]] = args[2]
m["jwks_keys"] = data
} else {
m["jwks_keys"] = map[string]interface{}{
args[1]: args[2],
}
}
default:
return errors.ErrMalformedDirective.WithArgs(rd, args)
}
}
if err := cfg.AddIdentityProvider(name, kind, m); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,123 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/google/go-cmp/cmp"
"testing"
)
func TestParseCaddyfileIdentityProvider(t *testing.T) {
testcases := []struct {
name string
d *caddyfile.Dispenser
want string
shouldErr bool
err error
}{
{
name: "test generic oauth identity provider",
d: caddyfile.NewTestDispenser(`
security {
oauth identity provider authp {
realm authp
driver generic
client_id foo
client_secret bar
base_auth_url https://localhost/oauth
response_type code
required_token_fields access_token
authorization_url https://localhost/oauth/authorize
token_url https://localhost/oauth/access_token
jwks key 87329db33bf testdata/oauth/87329db33bf_pub.pem
disable key verification
disable tls verification
}
authentication portal myportal {
enable identity provider authp
}
}`),
want: `{
"config": {
"authentication_portals": [
{
"name": "myportal",
"ui": {},
"user_registration_config": {},
"cookie_config": {},
"identity_providers": [
"authp"
],
"token_validator_options": {},
"token_grantor_options": {}
}
],
"identity_providers": [
{
"name": "authp",
"kind": "oauth",
"params": {
"authorization_url": "https://localhost/oauth/authorize",
"base_auth_url": "https://localhost/oauth",
"client_id": "foo",
"client_secret": "bar",
"driver": "generic",
"jwks_keys": {
"87329db33bf": "testdata/oauth/87329db33bf_pub.pem"
},
"key_verification_disabled": true,
"realm": "authp",
"required_token_fields": [
"access_token"
],
"response_type": [
"code"
],
"tls_insecure_skip_verify": true,
"token_url": "https://localhost/oauth/access_token"
}
}
]
}
}`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
app, err := parseCaddyfile(tc.d, nil)
if err != nil {
if !tc.shouldErr {
t.Fatalf("expected success, got: %v", err)
}
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
}
return
}
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
got := unpack(t, string(app.(httpcaddyfile.App).Value))
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {
t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
t.Errorf("parseCaddyfileIdentityProvider() mismatch (-want +got):\n%s", diff)
}
})
}
}

147
caddyfile_identity_store.go Normal file
View File

@ -0,0 +1,147 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/caddy-security/pkg/util"
"github.com/greenpau/go-authcrunch"
"github.com/greenpau/go-authcrunch/pkg/errors"
// "github.com/greenpau/go-authcrunch/pkg/authn"
// "github.com/greenpau/go-authcrunch/pkg/authn/backends"
// "strconv"
// "strings"
)
// parseCaddyfileIdentityStore parses identity store configuration.
//
// Syntax:
//
// identity store <name> {
// type <local>
// file <file_path>
// realm <name>
// }
//
func parseCaddyfileIdentityStore(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config, kind, name string, shortcuts []string) error {
m := make(map[string]interface{})
if len(shortcuts) > 0 {
switch kind {
case "local":
if len(shortcuts) != 1 {
return d.Errf("invalid %q shortcut: %v", name, shortcuts)
}
m["realm"] = "local"
m["path"] = shortcuts[0]
default:
return d.Errf("unsupported %q shortcut for %q store type: %v", name, kind, shortcuts)
}
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
k := d.Val()
args := util.FindReplaceAll(repl, d.RemainingArgs())
rd := mkcp("security.identity.store["+name+"]", k)
switch k {
case "disabled":
return nil
case "realm",
// Local.
"path",
// LDAP
"search_base_dn", "search_group_filter", "search_user_filter",
"search_filter", "username", "password":
if len(args) != 1 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
}
switch k {
case "search_filter":
m["search_user_filter"] = args[0]
case "username":
m["bind_username"] = args[0]
case "password":
m["bind_password"] = args[0]
default:
m[k] = args[0]
}
case "trusted_authority":
// LDAP only.
if len(args) != 1 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single path")
}
var values []string
if v, exists := m["trusted_authorities"]; exists {
values = v.([]string)
}
values = append(values, args[0])
m["trusted_authorities"] = values
case "attributes":
// LDAP only.
attrMap := make(map[string]interface{})
for attrNesting := d.Nesting(); d.NextBlock(attrNesting); {
attrName := d.Val()
if !d.NextArg() {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "no attribute values found")
}
attrMap[attrName] = d.Val()
}
m["attributes"] = attrMap
case "servers":
// LDAP only.
serverMaps := []map[string]interface{}{}
for serverNesting := d.Nesting(); d.NextBlock(serverNesting); {
serverMap := make(map[string]interface{})
serverMap["address"] = d.Val()
serverProps := d.RemainingArgs()
if len(serverProps) > 0 {
for _, serverProp := range serverProps {
switch serverProp {
case "ignore_cert_errors", "posix_groups":
serverMap[serverProp] = true
default:
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "unsupported prop "+serverProp)
}
}
}
serverMaps = append(serverMaps, serverMap)
}
m[k] = serverMaps
case "groups":
// LDAP only.
groupMaps := []map[string]interface{}{}
for groupNesting := d.Nesting(); d.NextBlock(groupNesting); {
groupMap := make(map[string]interface{})
groupDN := d.Val()
groupMap["dn"] = groupDN
groupRoles := d.RemainingArgs()
if len(groupRoles) == 0 {
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "no roles found")
}
groupMap["roles"] = groupRoles
groupMaps = append(groupMaps, groupMap)
}
m[k] = groupMaps
default:
return errors.ErrMalformedDirective.WithArgs(rd, args)
}
}
if err := cfg.AddIdentityStore(name, kind, m); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,191 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/google/go-cmp/cmp"
"testing"
)
func TestParseCaddyfileIdentityStore(t *testing.T) {
testcases := []struct {
name string
d *caddyfile.Dispenser
want string
shouldErr bool
err error
}{
{
name: "test authdb identity store",
d: caddyfile.NewTestDispenser(`
security {
local identity store localdb {
realm local
path /tmp/localdb
}
authentication portal myportal {
enable identity store localdb
}
}`),
want: `{
"config": {
"authentication_portals": [
{
"name": "myportal",
"ui": {},
"user_registration_config": {},
"cookie_config": {},
"identity_stores": [
"localdb"
],
"token_validator_options": {},
"token_grantor_options": {}
}
],
"identity_stores": [
{
"name": "localdb",
"kind": "local",
"params": {
"path": "/tmp/localdb",
"realm": "local"
}
}
]
}
}`,
},
{
name: "test msad ldap identity store",
d: caddyfile.NewTestDispenser(`
security {
ldap identity store contoso.com {
realm contoso.com
servers {
ldaps://ldaps.contoso.com ignore_cert_errors
}
attributes {
name givenName
surname sn
username sAMAccountName
member_of memberOf
email mail
}
username "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM"
password "P@ssW0rd123"
search_base_dn "DC=CONTOSO,DC=COM"
search_filter "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))"
groups {
"CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" admin
"CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" editor
"CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" viewer
}
}
authentication portal myportal {
enable identity store contoso.com
}
}`),
want: `{
"config": {
"authentication_portals": [
{
"name": "myportal",
"ui": {},
"user_registration_config": {},
"cookie_config": {},
"identity_stores": [
"contoso.com"
],
"token_validator_options": {},
"token_grantor_options": {}
}
],
"identity_stores": [
{
"name": "contoso.com",
"kind": "ldap",
"params": {
"attributes": {
"email": "mail",
"member_of": "memberOf",
"name": "givenName",
"surname": "sn",
"username": "sAMAccountName"
},
"bind_password": "P@ssW0rd123",
"bind_username": "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM",
"groups": [
{
"dn": "CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": [
"admin"
]
},
{
"dn": "CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": [
"editor"
]
},
{
"dn": "CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
"roles": [
"viewer"
]
}
],
"realm": "contoso.com",
"search_base_dn": "DC=CONTOSO,DC=COM",
"search_user_filter": "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))",
"servers": [
{
"address": "ldaps://ldaps.contoso.com",
"ignore_cert_errors": true
}
]
}
}
]
}
}`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
app, err := parseCaddyfile(tc.d, nil)
if err != nil {
if !tc.shouldErr {
t.Fatalf("expected success, got: %v", err)
}
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
}
return
}
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
got := unpack(t, string(app.(httpcaddyfile.App).Value))
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {
t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
t.Errorf("parseCaddyfileIdentityStore() mismatch (-want +got):\n%s", diff)
}
})
}
}

158
caddyfile_identity_test.go Normal file
View File

@ -0,0 +1,158 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/google/go-cmp/cmp"
"testing"
)
func TestParseCaddyfileIdentity(t *testing.T) {
testcases := []struct {
name string
d *caddyfile.Dispenser
want string
shouldErr bool
err error
}{
{
name: "test authdb identity store and google oauth identity provider",
d: caddyfile.NewTestDispenser(`
security {
#local identity store localdb {
# realm local
# path /tmp/localdb
#}
local identity store localdb /tmp/localdb
oauth identity provider github foo bar
oauth identity provider authp {
realm authp
driver generic
client_id foo
client_secret bar
base_auth_url https://localhost/oauth
response_type code
required_token_fields access_token
authorization_url https://localhost/oauth/authorize
token_url https://localhost/oauth/access_token
jwks key 87329db33bf testdata/oauth/87329db33bf_pub.pem
disable key verification
disable tls verification
}
authentication portal myportal {
enable identity store localdb
enable identity provider authp github
}
}`),
want: `{
"config": {
"authentication_portals": [
{
"name": "myportal",
"ui": {},
"user_registration_config": {},
"cookie_config": {},
"identity_stores": [
"localdb"
],
"identity_providers": [
"authp",
"github"
],
"token_validator_options": {},
"token_grantor_options": {}
}
],
"identity_stores": [
{
"name": "localdb",
"kind": "local",
"params": {
"path": "/tmp/localdb",
"realm": "local"
}
}
],
"identity_providers": [
{
"name": "github",
"kind": "oauth",
"params": {
"client_id": "foo",
"client_secret": "bar",
"driver": "github",
"realm": "github"
}
},
{
"name": "authp",
"kind": "oauth",
"params": {
"authorization_url": "https://localhost/oauth/authorize",
"base_auth_url": "https://localhost/oauth",
"client_id": "foo",
"client_secret": "bar",
"driver": "generic",
"jwks_keys": {
"87329db33bf": "testdata/oauth/87329db33bf_pub.pem"
},
"key_verification_disabled": true,
"realm": "authp",
"required_token_fields": [
"access_token"
],
"response_type": [
"code"
],
"tls_insecure_skip_verify": true,
"token_url": "https://localhost/oauth/access_token"
}
}
]
}
}`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
app, err := parseCaddyfile(tc.d, nil)
if err != nil {
if !tc.shouldErr {
t.Fatalf("expected success, got: %v", err)
}
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
}
return
}
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
got := unpack(t, string(app.(httpcaddyfile.App).Value))
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {
t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
t.Errorf("parseCaddyfileIdentity() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@ -51,9 +51,18 @@ func TestParseCaddyfileMessaging(t *testing.T) {
template registration_ready path/to/registration_ready.tmpl
template registration_verdict path/to/registration_verdict.tmpl
template mfa_otp path/to/mfa_otp.tmpl
}
local identity store localdb {
realm local
path /tmp/localdb
}
authentication portal myportal {
enable identity store localdb
}
}`),
want: `{
"config": {
"credentials": {
"generic": [
{
@ -82,7 +91,6 @@ func TestParseCaddyfileMessaging(t *testing.T) {
}
]
}
}
}`,
},
{
@ -123,7 +131,15 @@ func TestParseCaddyfileMessaging(t *testing.T) {
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
got := unpack(t, string(app.(httpcaddyfile.App).Value))
fullCfg := unpack(t, string(app.(httpcaddyfile.App).Value))
cfg := fullCfg["config"].(map[string]interface{})
got := make(map[string]interface{})
for _, k := range []string{"credentials", "messaging"} {
got[k] = cfg[k].(map[string]interface{})
}
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {

View File

@ -20,7 +20,6 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/greenpau/go-authcrunch/pkg/authn"
"github.com/greenpau/caddy-security/pkg/util"
)
@ -29,20 +28,21 @@ func init() {
}
func getRouteFromParseAuthnPluginCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
a, err := parseAuthnPluginCaddyfile(h)
m, err := parseAuthnPluginCaddyfile(h)
if err != nil {
return nil, err
}
pathMatcher := caddy.ModuleMap{
"path": h.JSON(caddyhttp.MatchPath{a.Path}),
"path": h.JSON(caddyhttp.MatchPath{m["path"]}),
}
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(
&AuthnMiddleware{
Authenticator: a,
RouteMatcher: m["path"],
PortalName: m["portal_name"],
},
"handler",
authnPluginName,
@ -68,11 +68,12 @@ func getRouteFromParseAuthnPluginCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyf
// authenticate /* with myportal
// authenticate /auth* with myportal
//
func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, error) {
func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (map[string]string, error) {
var i int
repl := caddy.NewReplacer()
args := util.FindReplaceAll(repl, h.RemainingArgs())
a := &authn.Authenticator{}
m := make(map[string]string)
if args[0] != "authenticate" {
return nil, h.Errf("directive should start with authenticate: %s", args)
}
@ -80,12 +81,12 @@ func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, er
switch len(args) {
case 3:
i = 1
a.Path = "*"
a.PortalName = args[2]
m["path"] = "*"
m["portal_name"] = args[2]
case 4:
i = 2
a.Path = args[1]
a.PortalName = args[3]
m["path"] = args[1]
m["portal_name"] = args[3]
default:
return nil, h.Errf("malformed directive: %s", args)
}
@ -96,5 +97,5 @@ func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, er
if args[i] != "with" {
return nil, h.Errf("directive must contain %q keyword: %s", "with", args)
}
return a, nil
return m, nil
}

View File

@ -20,7 +20,6 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth"
"github.com/greenpau/go-authcrunch/pkg/authz"
"github.com/greenpau/caddy-security/pkg/util"
)
@ -29,14 +28,20 @@ func init() {
}
func getMiddlewareFromParseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
a, err := parseAuthzPluginCaddyfile(h)
m, err := parseAuthzPluginCaddyfile(h)
if err != nil {
return nil, err
}
return caddyauth.Authentication{
ProvidersRaw: caddy.ModuleMap{
authzPluginName: caddyconfig.JSON(AuthzMiddleware{Authorizer: a}, nil),
authzPluginName: caddyconfig.JSON(
AuthzMiddleware{
RouteMatcher: m["path"],
GatekeeperName: m["gatekeeper_name"],
},
nil,
),
},
}, nil
}
@ -54,24 +59,25 @@ func getMiddlewareFromParseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (caddyht
// authorize /* with mypolicy
// authorize /app* with mypolicy
//
func parseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (*authz.Authorizer, error) {
func parseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (map[string]string, error) {
var i int
repl := caddy.NewReplacer()
args := util.FindReplaceAll(repl, h.RemainingArgs())
a := &authz.Authorizer{}
if args[0] != "authorize" {
return nil, h.Errf("directive should start with authorize: %s", args)
}
m := make(map[string]string)
switch len(args) {
case 3:
i = 1
a.Path = "*"
a.GatekeeperName = args[2]
m["path"] = "*"
m["gatekeeper_name"] = args[2]
case 4:
i = 2
a.Path = args[1]
a.GatekeeperName = args[3]
m["path"] = args[1]
m["gatekeeper_name"] = args[3]
default:
return nil, h.Errf("malformed directive: %s", args)
}
@ -82,5 +88,5 @@ func parseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (*authz.Authorizer, error
if args[i] != "with" {
return nil, h.Errf("directive must contain %q keyword: %s", "with", args)
}
return a, nil
return m, nil
}

View File

@ -41,9 +41,18 @@ func TestParseCaddyfileAppConfig(t *testing.T) {
username foo
password bar
}
local identity store localdb {
realm local
path /tmp/localdb
}
authentication portal myportal {
enable identity store localdb
}
}`),
want: `{
"config": {
"credentials": {
"generic": [
{
@ -53,7 +62,6 @@ func TestParseCaddyfileAppConfig(t *testing.T) {
}
]
}
}
}`,
},
}
@ -73,7 +81,14 @@ func TestParseCaddyfileAppConfig(t *testing.T) {
t.Fatalf("unexpected success, want: %v", tc.err)
}
got := unpack(t, string(app.(httpcaddyfile.App).Value))
fullCfg := unpack(t, string(app.(httpcaddyfile.App).Value))
cfg := fullCfg["config"].(map[string]interface{})
got := make(map[string]interface{})
for _, k := range []string{"credentials"} {
got[k] = cfg[k].(map[string]interface{})
}
want := unpack(t, tc.want)
if diff := cmp.Diff(want, got); diff != "" {

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.16
require (
github.com/caddyserver/caddy/v2 v2.4.6
github.com/google/go-cmp v0.5.7
github.com/greenpau/go-authcrunch v1.0.19
github.com/greenpau/go-authcrunch v1.0.20
github.com/satori/go.uuid v1.2.0
go.uber.org/zap v1.20.0
)

4
go.sum
View File

@ -476,8 +476,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/greenpau/go-authcrunch v1.0.19 h1:O5qmYJpmQrEmGzOkgU3voZoSrd4OVr7AszpWGX0M6VA=
github.com/greenpau/go-authcrunch v1.0.19/go.mod h1:wiUiQW5IPGxX8jAZMnqs/nhMF0DFHQAqEYsb8nU3KEE=
github.com/greenpau/go-authcrunch v1.0.20 h1:78rPPqo/56CgBmtsdykn3VhVH2ujEfD1dYHFYY37Pd0=
github.com/greenpau/go-authcrunch v1.0.20/go.mod h1:d54vnpcLS68I0YzGX+d2Svv4tKWmAYs5F9vl5ali6uA=
github.com/greenpau/versioned v1.0.27 h1:aFJ16tzsUkbc6WT7DRia60S0VrgWzBNuul3h0RXFKxM=
github.com/greenpau/versioned v1.0.27/go.mod h1:rtFCvaWWNbMH4CJnje/xicgmrM63j++rUh5juSu0k/A=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=

View File

@ -22,9 +22,9 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/greenpau/caddy-security/pkg/util"
"github.com/greenpau/go-authcrunch/pkg/authn"
"github.com/greenpau/go-authcrunch/pkg/requests"
"github.com/greenpau/caddy-security/pkg/util"
)
const (
@ -38,7 +38,9 @@ func init() {
// AuthnMiddleware implements Form-Based, Basic, Local, LDAP,
// OpenID Connect, OAuth 2.0, SAML Authentication.
type AuthnMiddleware struct {
Authenticator *authn.Authenticator `json:"authenticator,omitempty"`
RouteMatcher string `json:"route_matcher,omitempty" xml:"route_matcher,omitempty" yaml:"route_matcher,omitempty"`
PortalName string `json:"portal_name,omitempty" xml:"portal_name,omitempty" yaml:"portal_name,omitempty"`
portal *authn.Portal
}
// CaddyModule returns the Caddy module information.
@ -56,48 +58,54 @@ func (m *AuthnMiddleware) Provision(ctx caddy.Context) error {
return err
}
secApp := appModule.(*App)
if secApp == nil {
app := appModule.(*App)
if app == nil {
return fmt.Errorf("security app is nil")
}
if secApp.Config == nil {
if app.Config == nil {
return fmt.Errorf("security app config is nil")
}
var foundRef bool
for _, cfg := range secApp.Config.Portals {
if cfg.Name == m.Authenticator.PortalName {
foundRef = true
break
}
}
if !foundRef {
return fmt.Errorf("security app has no %q authentication portal", m.Authenticator.PortalName)
portal, err := app.getPortal(m.PortalName)
if err != nil {
return fmt.Errorf("security app erred with %q authentication portal: %v", m.PortalName, err)
}
m.portal = portal
return m.Authenticator.Provision(ctx.Logger(m))
return nil
}
// UnmarshalCaddyfile unmarshals a caddyfile.
func (m *AuthnMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
a, err := parseAuthnPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
cfg, err := parseAuthnPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
if err != nil {
return err
}
m.Authenticator = a
m.RouteMatcher = cfg["path"]
m.PortalName = cfg["portal_name"]
return nil
}
// Validate implements caddy.Validator.
func (m *AuthnMiddleware) Validate() error {
return m.Authenticator.Validate()
if m.RouteMatcher == "" {
return fmt.Errorf("empty route matcher")
}
if m.PortalName == "" {
return fmt.Errorf("empty portal name")
}
if m.portal == nil {
return fmt.Errorf("portal is nil")
}
return nil
}
// ServeHTTP serves authentication portal.
func (m *AuthnMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
rr := requests.NewRequest()
rr.ID = util.GetRequestID(r)
return m.Authenticator.ServeHTTP(r.Context(), w, r, rr)
return m.portal.ServeHTTP(r.Context(), w, r, rr)
}
// Interface guards

View File

@ -41,7 +41,9 @@ func init() {
// AuthzMiddleware authorizes access to endpoints based on
// the presense and content of JWT token.
type AuthzMiddleware struct {
Authorizer *authz.Authorizer `json:"authorizer,omitempty"`
RouteMatcher string `json:"route_matcher,omitempty" xml:"route_matcher,omitempty" yaml:"route_matcher,omitempty"`
GatekeeperName string `json:"gatekeeper_name,omitempty" xml:"gatekeeper_name,omitempty" yaml:"gatekeeper_name,omitempty"`
gatekeeper *authz.Gatekeeper
}
// CaddyModule returns the Caddy module information.
@ -59,41 +61,46 @@ func (m *AuthzMiddleware) Provision(ctx caddy.Context) error {
return err
}
secApp := appModule.(*App)
if secApp == nil {
app := appModule.(*App)
if app == nil {
return fmt.Errorf("security app is nil")
}
if secApp.Config == nil {
if app.Config == nil {
return fmt.Errorf("security app config is nil")
}
var foundRef bool
for _, cfg := range secApp.Config.Policies {
if cfg.Name == m.Authorizer.GatekeeperName {
foundRef = true
break
}
}
if !foundRef {
return fmt.Errorf("security app has no %q authorization policy", m.Authorizer.GatekeeperName)
gatekeeper, err := app.getGatekeeper(m.GatekeeperName)
if err != nil {
return fmt.Errorf("security app erred with %q authorization policy: %v", m.GatekeeperName, err)
}
m.gatekeeper = gatekeeper
return m.Authorizer.Provision(ctx.Logger(m))
return nil
}
// UnmarshalCaddyfile unmarshals caddyfile.
func (m *AuthzMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
a, err := parseAuthzPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
cfg, err := parseAuthzPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
if err != nil {
return err
}
m.Authorizer = a
m.RouteMatcher = cfg["path"]
m.GatekeeperName = cfg["gatekeeper_name"]
return nil
}
// Validate implements caddy.Validator.
func (m *AuthzMiddleware) Validate() error {
return m.Authorizer.Validate()
if m.RouteMatcher == "" {
return fmt.Errorf("empty route matcher")
}
if m.GatekeeperName == "" {
return fmt.Errorf("empty gatekeeper name")
}
if m.gatekeeper == nil {
return fmt.Errorf("gatekeeper is nil")
}
return nil
}
// Authenticate authorizes access based on the presense and content of
@ -101,7 +108,7 @@ func (m *AuthzMiddleware) Validate() error {
func (m AuthzMiddleware) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) {
ar := requests.NewAuthorizationRequest()
ar.ID = util.GetRequestID(r)
if err := m.Authorizer.Authenticate(w, r, ar); err != nil {
if err := m.gatekeeper.Authenticate(w, r, ar); err != nil {
return caddyauth.User{}, false, errors.ErrAuthorizationFailed.WithArgs(
getAuthorizationDetails(r, ar), err,
)

51
testdata/oauth/87329db33bf_pri.pem vendored Normal file
View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

14
testdata/oauth/87329db33bf_pub.pem vendored Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt04Rq3MJETW1WwsaNlr1
mmth9f03a/dyHlopCMS0rf96ISAiQT1Sq8JUJQHZRWpl67ox9wWa+jxiwoe8G2rC
ZtlXBzot/HRc4spbjL2NWBX5RtD+PGDUekzgGPME4lusG9PhPpclgKoucEy2U7wP
ldLIf+fAy3Jis72J9BEHRcTyhnbV+qQUN3WpnZYrtIXYbu0hyBB0JnMP3k8Zs1jW
LsCtATsGEimSp1xAkAH5ek7l65Y1vzn/bSCa4gVxpdnTqeJaWpTg1lgtxG69RD7W
8ZA2K4U94Aisjuen2+liaQ+pGVL0dMAoODedq32nV4v4uUfiM7XLkb1ceBK1FkOm
/d5346y9Dk0zX8Xcf385qRX83r0BzHMUsgMmNDOuHXAMGhR6rrD7M0kTCw+5TWbm
GicMT896614du5P2HNpkeY8NwgxQwWG7G0KYA8jkCb4InqBKxbKXYqv8sWwvtlDa
67pUzlvAUhOL3gICxEjEJsdbaA1c51dsXaK+OuSr8irQtq12RCJ2/GXkcGWtt5Fx
O9BFXfxnYcYRofdvbYzA2lgz5sgPDB98ZAro5D9y4A/vtcDcJir9ud1lLSnxMZZ+
8ZMYCVHGHiFuqum6uUNJDTpuv18Bp6WCd/v6E9v5is6wNTe/C7/syyFiDj+vW1fF
HmKw4GKAL03ogHD3r0Cm88ECAwEAAQ==
-----END PUBLIC KEY-----

6
testdata/oauth/README.md vendored Normal file
View File

@ -0,0 +1,6 @@
Generate private-public RSA key pair:
```
openssl genrsa -out testdata/oauth/87329db33bf_pri.pem 4096
openssl rsa -in testdata/oauth/87329db33bf_pri.pem -pubout -out testdata/oauth/87329db33bf_pub.pem
```