diff --git a/caddyfile_adapt_test.go b/caddyfile_adapt_test.go index f424922f..7306be59 100644 --- a/caddyfile_adapt_test.go +++ b/caddyfile_adapt_test.go @@ -118,6 +118,10 @@ func TestCaddyfileAdaptAuthenticationToJSON(t *testing.T) { name: "authenticate plugin config with registration", inputFileNamePrefix: "testcase_authenticate_with_registration", }, + { + name: "authenticate plugin config with auth challenges", + inputFileNamePrefix: "testcase_authenticate_with_auth_challenges", + }, { name: "security app config with authentication portal with static secrets manager plugin", inputFileNamePrefix: "testcase_security_with_secrets", diff --git a/caddyfile_identity_store.go b/caddyfile_identity_store.go index 3119f7ec..a1550874 100644 --- a/caddyfile_identity_store.go +++ b/caddyfile_identity_store.go @@ -154,6 +154,7 @@ func parseCaddyfileIdentityStore(d *caddyfile.Dispenser, cfg *authcrunch.Config, username := args[0] userMap["username"] = username apiKeyList := []map[string]interface{}{} + authChallengeRules := []string{} for userNesting := d.Nesting(); d.NextBlock(userNesting); { userPropName := d.Val() userPropValue := d.RemainingArgs() @@ -186,6 +187,11 @@ func parseCaddyfileIdentityStore(d *caddyfile.Dispenser, cfg *authcrunch.Config, return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, userPropName+" must contain one or more value") } userMap[userPropName] = userPropValue + case "auth": + if len(userPropValue) < 2 || userPropValue[0] != "challenges" { + return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, userPropName+" must be followed by challenges") + } + authChallengeRules = append(authChallengeRules, strings.Join(userPropValue[1:], " ")) case "api": if len(userPropValue) != 3 { return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, userPropName+" key must contain two values") @@ -204,6 +210,9 @@ func parseCaddyfileIdentityStore(d *caddyfile.Dispenser, cfg *authcrunch.Config, if len(apiKeyList) > 0 { userMap["api_keys"] = apiKeyList } + if len(authChallengeRules) > 0 { + userMap["auth_challenge_rules"] = authChallengeRules + } userMaps = append(userMaps, userMap) case "groups": // LDAP only. diff --git a/caddyfile_identity_store_test.go b/caddyfile_identity_store_test.go index ddd75004..ba1659f7 100644 --- a/caddyfile_identity_store_test.go +++ b/caddyfile_identity_store_test.go @@ -210,6 +210,92 @@ func TestParseCaddyfileIdentityStore(t *testing.T) { } }`, }, + { + name: "test local identity store with auth challenge rules", + d: caddyfile.NewTestDispenser(` + security { + local identity store localdb { + realm local + path /tmp/localdb + user jsmith { + name John Smith + email jsmith@localdomain.local + password "My@Password123" + roles authp/user + auth challenges u2f + auth challenges password totp if u2f not available + auth challenges password if u2f and totp not available + } + } + authentication portal myportal { + enable identity store localdb + } + }`), + want: `{ + "config": { + "authentication_portals": [ + { + "name": "myportal", + "ui": {}, + "cookie_config": { + "session_id_cookie_name": "AUTHP_SESSION_ID", + "referer_cookie_name": "AUTHP_REDIRECT_URL", + "sandbox_id_cookie_name": "AUTHP_SANDBOX_ID", + "identity_token_cookie_name": "AUTHP_ID_TOKEN", + "access_token_cookie_name": "AUTHP_ACCESS_TOKEN", + "refresh_token_cookie_name": "AUTHP_REFRESH_TOKEN", + "cookie_name_prefix": "AUTHP" + }, + "crypto_key_store_config": { + "auto_generate_algo": "ES512", + "auto_generate_tag": "default" + }, + "identity_stores": [ + "localdb" + ], + "portal_admin_roles": { + "authp/admin": true + }, + "portal_user_roles": { + "authp/user": true + }, + "portal_guest_roles": { + "authp/guest": true + }, + "api": { + "profile_enabled": true + }, + "token_validator_options": {}, + "token_grantor_options": {} + } + ], + "identity_stores": [ + { + "name": "localdb", + "kind": "local", + "params": { + "path": "/tmp/localdb", + "realm": "local", + "users": [ + { + "username": "jsmith", + "name": "John Smith", + "email_address": "jsmith@localdomain.local", + "password": "My@Password123", + "roles": ["authp/user"], + "auth_challenge_rules": [ + "u2f", + "password totp if u2f not available", + "password if u2f and totp not available" + ] + } + ] + } + } + ] + } + }`, + }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { diff --git a/caddyfile_resolve_test.go b/caddyfile_resolve_test.go index cd8ccc3a..7956741e 100644 --- a/caddyfile_resolve_test.go +++ b/caddyfile_resolve_test.go @@ -144,6 +144,10 @@ func TestResolveRuntimeAppConfig(t *testing.T) { name: "authenticate plugin config with ui", inputFileNamePrefix: "testcase_authenticate_with_ui", }, + { + name: "authenticate plugin config with auth challenges", + inputFileNamePrefix: "testcase_authenticate_with_auth_challenges", + }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { diff --git a/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.Caddyfile b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.Caddyfile new file mode 100644 index 00000000..7e0aa8ac --- /dev/null +++ b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.Caddyfile @@ -0,0 +1,26 @@ +{ + security { + local identity store localdb { + realm local + path assets/config/users.json + user jsmith { + name John Smith + email jsmith@localdomain.local + password "My@Password123" + roles authp/user + auth challenges u2f + auth challenges password totp if u2f not available + auth challenges password if u2f and totp not available + } + } + + authentication portal myportal { + crypto key sign-verify {env.JWT_SHARED_KEY} + enable identity store localdb + } + } +} + +:443 { + authenticate with myportal +} diff --git a/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.env b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.env new file mode 100644 index 00000000..3dbf37a1 --- /dev/null +++ b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.env @@ -0,0 +1 @@ +JWT_SHARED_KEY=testkey123 diff --git a/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.json b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.json new file mode 100644 index 00000000..7b216aa9 --- /dev/null +++ b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges.json @@ -0,0 +1,96 @@ +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "handle": [ + { + "handler": "authenticator", + "portal_name": "myportal", + "route_matcher": "*" + } + ] + } + ] + } + } + }, + "security": { + "config": { + "authentication_portals": [ + { + "name": "myportal", + "ui": {}, + "cookie_config": { + "session_id_cookie_name": "AUTHP_SESSION_ID", + "referer_cookie_name": "AUTHP_REDIRECT_URL", + "sandbox_id_cookie_name": "AUTHP_SANDBOX_ID", + "identity_token_cookie_name": "AUTHP_ID_TOKEN", + "access_token_cookie_name": "AUTHP_ACCESS_TOKEN", + "refresh_token_cookie_name": "AUTHP_REFRESH_TOKEN", + "cookie_name_prefix": "AUTHP" + }, + "identity_stores": [ + "localdb" + ], + "token_validator_options": {}, + "raw_crypto_key_store_config": [ + "crypto key sign-verify {env.JWT_SHARED_KEY}" + ], + "crypto_key_store_config": { + "raw_key_configs": [ + "crypto key sign-verify {env.JWT_SHARED_KEY}" + ], + "auto_generate_tag": "default", + "auto_generate_algo": "ES512" + }, + "token_grantor_options": {}, + "portal_admin_roles": { + "authp/admin": true + }, + "portal_user_roles": { + "authp/user": true + }, + "portal_guest_roles": { + "authp/guest": true + }, + "api": { + "profile_enabled": true + } + } + ], + "identity_stores": [ + { + "name": "localdb", + "kind": "local", + "params": { + "path": "assets/config/users.json", + "realm": "local", + "users": [ + { + "auth_challenge_rules": [ + "u2f", + "password totp if u2f not available", + "password if u2f and totp not available" + ], + "email_address": "jsmith@localdomain.local", + "name": "John Smith", + "password": "My@Password123", + "roles": [ + "authp/user" + ], + "username": "jsmith" + } + ] + } + } + ] + } + } + } +} diff --git a/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges_resolved.json b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges_resolved.json new file mode 100644 index 00000000..a30935dd --- /dev/null +++ b/testdata/caddyfile_adapt/testcase_authenticate_with_auth_challenges_resolved.json @@ -0,0 +1,70 @@ +{ + "authentication_portals": [ + { + "name": "myportal", + "ui": {}, + "cookie_config": { + "session_id_cookie_name": "AUTHP_SESSION_ID", + "referer_cookie_name": "AUTHP_REDIRECT_URL", + "sandbox_id_cookie_name": "AUTHP_SANDBOX_ID", + "identity_token_cookie_name": "AUTHP_ID_TOKEN", + "access_token_cookie_name": "AUTHP_ACCESS_TOKEN", + "refresh_token_cookie_name": "AUTHP_REFRESH_TOKEN", + "cookie_name_prefix": "AUTHP" + }, + "identity_stores": [ + "localdb" + ], + "token_validator_options": {}, + "raw_crypto_key_store_config": [ + "crypto key sign-verify testkey123" + ], + "crypto_key_store_config": { + "raw_key_configs": [ + "crypto key sign-verify testkey123" + ], + "auto_generate_tag": "default", + "auto_generate_algo": "ES512" + }, + "token_grantor_options": {}, + "portal_admin_roles": { + "authp/admin": true + }, + "portal_user_roles": { + "authp/user": true + }, + "portal_guest_roles": { + "authp/guest": true + }, + "api": { + "profile_enabled": true + } + } + ], + "identity_stores": [ + { + "name": "localdb", + "kind": "local", + "params": { + "path": "assets/config/users.json", + "realm": "local", + "users": [ + { + "auth_challenge_rules": [ + "u2f", + "password totp if u2f not available", + "password if u2f and totp not available" + ], + "email_address": "jsmith@localdomain.local", + "name": "John Smith", + "password": "My@Password123", + "roles": [ + "authp/user" + ], + "username": "jsmith" + } + ] + } + } + ] +}