diff --git a/.sqlx/query-fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1.json b/.sqlx/query-0572e003ba6f926f1dcceecb1e534a6ef98659ae7ddf3a68c059886f0b9c2bfa.json similarity index 85% rename from .sqlx/query-fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1.json rename to .sqlx/query-0572e003ba6f926f1dcceecb1e534a6ef98659ae7ddf3a68c059886f0b9c2bfa.json index 07b07f5449..52e7998636 100644 --- a/.sqlx/query-fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1.json +++ b/.sqlx/query-0572e003ba6f926f1dcceecb1e534a6ef98659ae7ddf3a68c059886f0b9c2bfa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active FROM \"user\"", + "query": "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active, openid_login FROM \"user\"", "describe": { "columns": [ { @@ -50,6 +50,11 @@ "ordinal": 6, "name": "is_active", "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "openid_login", + "type_info": "Bool" } ], "parameters": { @@ -62,8 +67,9 @@ false, false, true, + false, false ] }, - "hash": "fd950a860fe104136816e1605ed080d70a714e7a02f15e8a1d7b165814cf85d1" + "hash": "0572e003ba6f926f1dcceecb1e534a6ef98659ae7ddf3a68c059886f0b9c2bfa" } diff --git a/.sqlx/query-1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8.json b/.sqlx/query-230fe22c6c9907c9ebab1dab9ef5ca984604c70823b13390169a5da57086df1a.json similarity index 89% rename from .sqlx/query-1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8.json rename to .sqlx/query-230fe22c6c9907c9ebab1dab9ef5ca984604c70823b13390169a5da57086df1a.json index ee2196861b..3ffbde7ccd 100644 --- a/.sqlx/query-1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8.json +++ b/.sqlx/query-230fe22c6c9907c9ebab1dab9ef5ca984604c70823b13390169a5da57086df1a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" WHERE username = $1", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login FROM \"user\" WHERE email = $1", "describe": { "columns": [ { @@ -90,6 +90,11 @@ "ordinal": 14, "name": "is_active", "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_login", + "type_info": "Bool" } ], "parameters": { @@ -112,8 +117,9 @@ true, false, false, + false, false ] }, - "hash": "1b29a6b1d3741ede2d85271f0f0e07069048ba01f8c3c5988f044e788200f9f8" + "hash": "230fe22c6c9907c9ebab1dab9ef5ca984604c70823b13390169a5da57086df1a" } diff --git a/.sqlx/query-e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3.json b/.sqlx/query-246af063a89129453ca514f8f32f8a644fcfc0bbb3ebf17f1f2410e24c3a2316.json similarity index 83% rename from .sqlx/query-e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3.json rename to .sqlx/query-246af063a89129453ca514f8f32f8a644fcfc0bbb3ebf17f1f2410e24c3a2316.json index 98488e7959..2a93c87f47 100644 --- a/.sqlx/query-e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3.json +++ b/.sqlx/query-246af063a89129453ca514f8f32f8a644fcfc0bbb3ebf17f1f2410e24c3a2316.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\"\n INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id\n INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id\n WHERE \"group\".name = $1", + "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login FROM \"user\"\n INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id\n INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id\n WHERE \"group\".name = $1", "describe": { "columns": [ { @@ -90,6 +90,11 @@ "ordinal": 14, "name": "is_active", "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_login", + "type_info": "Bool" } ], "parameters": { @@ -112,8 +117,9 @@ true, false, false, + false, false ] }, - "hash": "e253723a52a1632fd190ad5404cc0a6dfaf336b5416dafb8d634b521d3dce8d3" + "hash": "246af063a89129453ca514f8f32f8a644fcfc0bbb3ebf17f1f2410e24c3a2316" } diff --git a/.sqlx/query-52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212.json b/.sqlx/query-2f420621d8767a0014924246904b28e05088fe14b53d1b54105aa0d8f50c5d9c.json similarity index 86% rename from .sqlx/query-52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212.json rename to .sqlx/query-2f420621d8767a0014924246904b28e05088fe14b53d1b54105aa0d8f50c5d9c.json index 72a1d6e0f5..a792078586 100644 --- a/.sqlx/query-52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212.json +++ b/.sqlx/query-2f420621d8767a0014924246904b28e05088fe14b53d1b54105aa0d8f50c5d9c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", + "query": "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", "describe": { "columns": [ { @@ -90,6 +90,11 @@ "ordinal": 14, "name": "is_active", "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_login", + "type_info": "Bool" } ], "parameters": { @@ -112,8 +117,9 @@ true, false, false, + false, false ] }, - "hash": "52c659862cef5cd45bbe7bdc58cf70c93c46740cc778825583e060cdee73a212" + "hash": "2f420621d8767a0014924246904b28e05088fe14b53d1b54105aa0d8f50c5d9c" } diff --git a/.sqlx/query-40464c39168b11d05247d01336343bdbf791077e0c081b16e3d1c2d5b3cc074b.json b/.sqlx/query-40464c39168b11d05247d01336343bdbf791077e0c081b16e3d1c2d5b3cc074b.json new file mode 100644 index 0000000000..6ae98c3f09 --- /dev/null +++ b/.sqlx/query-40464c39168b11d05247d01336343bdbf791077e0c081b16e3d1c2d5b3cc074b.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", name, base_url, client_id, client_secret FROM openidprovider", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "base_url", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "client_id", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "client_secret", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "40464c39168b11d05247d01336343bdbf791077e0c081b16e3d1c2d5b3cc074b" +} diff --git a/.sqlx/query-43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e.json b/.sqlx/query-42fbc74f13a7febfbf6bda66cd024e61bf3fdef17222de87443fa9428ea2e30b.json similarity index 85% rename from .sqlx/query-43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e.json rename to .sqlx/query-42fbc74f13a7febfbf6bda66cd024e61bf3fdef17222de87443fa9428ea2e30b.json index 0e0a82ef1b..82c8bb185c 100644 --- a/.sqlx/query-43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e.json +++ b/.sqlx/query-42fbc74f13a7febfbf6bda66cd024e61bf3fdef17222de87443fa9428ea2e30b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\"", + "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"openid_login\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\"", "describe": { "columns": [ { @@ -50,26 +50,31 @@ }, { "ordinal": 9, - "name": "totp_enabled", + "name": "openid_login", "type_info": "Bool" }, { "ordinal": 10, - "name": "email_mfa_enabled", + "name": "totp_enabled", "type_info": "Bool" }, { "ordinal": 11, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 12, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 12, + "ordinal": 13, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 13, + "ordinal": 14, "name": "mfa_method: _", "type_info": { "Custom": { @@ -87,7 +92,7 @@ } }, { - "ordinal": 14, + "ordinal": 15, "name": "recovery_codes: _", "type_info": "TextArray" } @@ -107,11 +112,12 @@ false, false, false, + false, true, true, false, false ] }, - "hash": "43ac4b3a375a4756d77b015c0bb871f6ccc4f5ecc7ec32f0b3a9f64398f3686e" + "hash": "42fbc74f13a7febfbf6bda66cd024e61bf3fdef17222de87443fa9428ea2e30b" } diff --git a/.sqlx/query-61f006defe9db54d750f92c978522eae2b8fa401b802a07196f5541fa96524eb.json b/.sqlx/query-61f006defe9db54d750f92c978522eae2b8fa401b802a07196f5541fa96524eb.json new file mode 100644 index 0000000000..700ded04cd --- /dev/null +++ b/.sqlx/query-61f006defe9db54d750f92c978522eae2b8fa401b802a07196f5541fa96524eb.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", name, base_url, client_id, client_secret FROM openidprovider WHERE name = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "base_url", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "client_id", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "client_secret", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "61f006defe9db54d750f92c978522eae2b8fa401b802a07196f5541fa96524eb" +} diff --git a/.sqlx/query-6ae26abc026e5fe59e8505b2563fca6caf4a42bb9ad01d6f44db41a572abce95.json b/.sqlx/query-6ae26abc026e5fe59e8505b2563fca6caf4a42bb9ad01d6f44db41a572abce95.json new file mode 100644 index 0000000000..cc029dcf7e --- /dev/null +++ b/.sqlx/query-6ae26abc026e5fe59e8505b2563fca6caf4a42bb9ad01d6f44db41a572abce95.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"openidprovider\" SET \"name\" = $2,\"base_url\" = $3,\"client_id\" = $4,\"client_secret\" = $5 WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "6ae26abc026e5fe59e8505b2563fca6caf4a42bb9ad01d6f44db41a572abce95" +} diff --git a/.sqlx/query-6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d.json b/.sqlx/query-6e3362d9973d75894d3bd289e7d10d8f76bf8c7b2e3e11de5590e67c2898fe28.json similarity index 89% rename from .sqlx/query-6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d.json rename to .sqlx/query-6e3362d9973d75894d3bd289e7d10d8f76bf8c7b2e3e11de5590e67c2898fe28.json index 256dd6ac1c..6672cb3680 100644 --- a/.sqlx/query-6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d.json +++ b/.sqlx/query-6e3362d9973d75894d3bd289e7d10d8f76bf8c7b2e3e11de5590e67c2898fe28.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" WHERE email = $1", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login FROM \"user\" WHERE username = $1", "describe": { "columns": [ { @@ -90,6 +90,11 @@ "ordinal": 14, "name": "is_active", "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_login", + "type_info": "Bool" } ], "parameters": { @@ -112,8 +117,9 @@ true, false, false, + false, false ] }, - "hash": "6fc45edc38cf3f590daec8f930a4117ae77dd226c42ee175544f0ba5eccb315d" + "hash": "6e3362d9973d75894d3bd289e7d10d8f76bf8c7b2e3e11de5590e67c2898fe28" } diff --git a/.sqlx/query-7cf5b6245bab3bdf58bce20a791db4217a168edb014ce3bd9fd092b21553cdc5.json b/.sqlx/query-7cf5b6245bab3bdf58bce20a791db4217a168edb014ce3bd9fd092b21553cdc5.json new file mode 100644 index 0000000000..1bb2d632ff --- /dev/null +++ b/.sqlx/query-7cf5b6245bab3bdf58bce20a791db4217a168edb014ce3bd9fd092b21553cdc5.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM \"openidprovider\" WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "7cf5b6245bab3bdf58bce20a791db4217a168edb014ce3bd9fd092b21553cdc5" +} diff --git a/.sqlx/query-87ac2de4f3fc7801f3b9dcf51992f107c53f264016865af4379196f5e72acd4c.json b/.sqlx/query-87ac2de4f3fc7801f3b9dcf51992f107c53f264016865af4379196f5e72acd4c.json new file mode 100644 index 0000000000..11ad8e07f6 --- /dev/null +++ b/.sqlx/query-87ac2de4f3fc7801f3b9dcf51992f107c53f264016865af4379196f5e72acd4c.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", \"name\",\"base_url\",\"client_id\",\"client_secret\" FROM \"openidprovider\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "base_url", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "client_id", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "client_secret", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "87ac2de4f3fc7801f3b9dcf51992f107c53f264016865af4379196f5e72acd4c" +} diff --git a/.sqlx/query-c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269.json b/.sqlx/query-935d9b9ad81c18d764b54ea4fc1d742916968a18854c2f6d9d8004742b9afd54.json similarity index 74% rename from .sqlx/query-c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269.json rename to .sqlx/query-935d9b9ad81c18d764b54ea4fc1d742916968a18854c2f6d9d8004742b9afd54.json index 7111120273..781eaeef91 100644 --- a/.sqlx/query-c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269.json +++ b/.sqlx/query-935d9b9ad81c18d764b54ea4fc1d742916968a18854c2f6d9d8004742b9afd54.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id", + "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"openid_login\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING id", "describe": { "columns": [ { @@ -21,6 +21,7 @@ "Bool", "Bool", "Bool", + "Bool", "Bytea", "Bytea", { @@ -44,5 +45,5 @@ false ] }, - "hash": "c9773715f70d267a2bc1e23b86d06c9dd358479e5791b672369d3c0496d51269" + "hash": "935d9b9ad81c18d764b54ea4fc1d742916968a18854c2f6d9d8004742b9afd54" } diff --git a/.sqlx/query-a0700f9701a61fc64af20165feb4627ef6f053bcb59d81e41dd1cf1bd199b543.json b/.sqlx/query-a0700f9701a61fc64af20165feb4627ef6f053bcb59d81e41dd1cf1bd199b543.json new file mode 100644 index 0000000000..94ec297433 --- /dev/null +++ b/.sqlx/query-a0700f9701a61fc64af20165feb4627ef6f053bcb59d81e41dd1cf1bd199b543.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE openidprovider SET name = $1, base_url = $2, client_id = $3, client_secret = $4 WHERE id = $5", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "a0700f9701a61fc64af20165feb4627ef6f053bcb59d81e41dd1cf1bd199b543" +} diff --git a/.sqlx/query-f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3.json b/.sqlx/query-a3d548764fe912cc9009618d41504205b64526e89acd2fefd74d221b3c3ea620.json similarity index 89% rename from .sqlx/query-f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3.json rename to .sqlx/query-a3d548764fe912cc9009618d41504205b64526e89acd2fefd74d221b3c3ea620.json index d4dfe1b6b1..6344d31c1c 100644 --- a/.sqlx/query-f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3.json +++ b/.sqlx/query-a3d548764fe912cc9009618d41504205b64526e89acd2fefd74d221b3c3ea620.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active FROM \"user\" WHERE id = ANY($1)", + "query": "SELECT id \"id?\", username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login FROM \"user\" WHERE id = ANY($1)", "describe": { "columns": [ { @@ -90,6 +90,11 @@ "ordinal": 14, "name": "is_active", "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_login", + "type_info": "Bool" } ], "parameters": { @@ -112,8 +117,9 @@ true, false, false, + false, false ] }, - "hash": "f5ed6899054f1915882741843733fc196d59c356d69c0d06e9782cb1bc73e6f3" + "hash": "a3d548764fe912cc9009618d41504205b64526e89acd2fefd74d221b3c3ea620" } diff --git a/.sqlx/query-a6fe220739875c6894e1a678d4c24de34b7c3ca8bd5e48ed9f313d20cbe8f8e4.json b/.sqlx/query-a6fe220739875c6894e1a678d4c24de34b7c3ca8bd5e48ed9f313d20cbe8f8e4.json new file mode 100644 index 0000000000..e32f0cf87a --- /dev/null +++ b/.sqlx/query-a6fe220739875c6894e1a678d4c24de34b7c3ca8bd5e48ed9f313d20cbe8f8e4.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"openidprovider\" (\"name\",\"base_url\",\"client_id\",\"client_secret\") VALUES ($1,$2,$3,$4) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "a6fe220739875c6894e1a678d4c24de34b7c3ca8bd5e48ed9f313d20cbe8f8e4" +} diff --git a/.sqlx/query-3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736.json b/.sqlx/query-a7194f2160706f9d661d0d270aaad92ba54c1a84d7083d47a684a3b1d5ad206f.json similarity index 70% rename from .sqlx/query-3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736.json rename to .sqlx/query-a7194f2160706f9d661d0d270aaad92ba54c1a84d7083d47a684a3b1d5ad206f.json index e4f3bf2de0..d4f8666455 100644 --- a/.sqlx/query-3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736.json +++ b/.sqlx/query-a7194f2160706f9d661d0d270aaad92ba54c1a84d7083d47a684a3b1d5ad206f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"totp_enabled\" = $10,\"email_mfa_enabled\" = $11,\"totp_secret\" = $12,\"email_mfa_secret\" = $13,\"mfa_method\" = $14,\"recovery_codes\" = $15 WHERE id = $1", + "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"openid_login\" = $10,\"totp_enabled\" = $11,\"email_mfa_enabled\" = $12,\"totp_secret\" = $13,\"email_mfa_secret\" = $14,\"mfa_method\" = $15,\"recovery_codes\" = $16 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -16,6 +16,7 @@ "Bool", "Bool", "Bool", + "Bool", "Bytea", "Bytea", { @@ -37,5 +38,5 @@ }, "nullable": [] }, - "hash": "3063672b53bf0bada26d6d1b0adbebfbeb42e6a4949a10b2f23ea4d968aeb736" + "hash": "a7194f2160706f9d661d0d270aaad92ba54c1a84d7083d47a684a3b1d5ad206f" } diff --git a/.sqlx/query-e1875cb32cf6f270541b27319cb472abd8f6ed0dccc77a2ba0908dd487975ef5.json b/.sqlx/query-a9d694c5efe301ef975e1b6fc0ca0acc741c16c3efdd7a92475a419e9e30db4c.json similarity index 94% rename from .sqlx/query-e1875cb32cf6f270541b27319cb472abd8f6ed0dccc77a2ba0908dd487975ef5.json rename to .sqlx/query-a9d694c5efe301ef975e1b6fc0ca0acc741c16c3efdd7a92475a419e9e30db4c.json index bd60bb9df9..83250222b3 100644 --- a/.sqlx/query-e1875cb32cf6f270541b27319cb472abd8f6ed0dccc77a2ba0908dd487975ef5.json +++ b/.sqlx/query-a9d694c5efe301ef975e1b6fc0ca0acc741c16c3efdd7a92475a419e9e30db4c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"openid_enabled\",\"wireguard_enabled\",\"webhooks_enabled\",\"worker_enabled\",\"challenge_template\",\"instance_name\",\"main_logo_url\",\"nav_logo_url\",\"smtp_server\",\"smtp_port\",\"smtp_encryption\" \"smtp_encryption: _\",\"smtp_user\",\"smtp_password\" \"smtp_password?: SecretString\",\"smtp_sender\",\"enrollment_vpn_step_optional\",\"enrollment_welcome_message\",\"enrollment_welcome_email\",\"enrollment_welcome_email_subject\",\"enrollment_use_welcome_message_as_email\",\"uuid\",\"ldap_url\",\"ldap_bind_username\",\"ldap_bind_password\" \"ldap_bind_password?: SecretString\",\"ldap_group_search_base\",\"ldap_user_search_base\",\"ldap_user_obj_class\",\"ldap_group_obj_class\",\"ldap_username_attr\",\"ldap_groupname_attr\",\"ldap_group_member_attr\",\"ldap_member_attr\" FROM \"settings\"", + "query": "SELECT id \"id?\", \"openid_enabled\",\"wireguard_enabled\",\"webhooks_enabled\",\"worker_enabled\",\"challenge_template\",\"instance_name\",\"main_logo_url\",\"nav_logo_url\",\"smtp_server\",\"smtp_port\",\"smtp_encryption\" \"smtp_encryption: _\",\"smtp_user\",\"smtp_password\" \"smtp_password?: SecretString\",\"smtp_sender\",\"enrollment_vpn_step_optional\",\"enrollment_welcome_message\",\"enrollment_welcome_email\",\"enrollment_welcome_email_subject\",\"enrollment_use_welcome_message_as_email\",\"uuid\",\"ldap_url\",\"ldap_bind_username\",\"ldap_bind_password\" \"ldap_bind_password?: SecretString\",\"ldap_group_search_base\",\"ldap_user_search_base\",\"ldap_user_obj_class\",\"ldap_group_obj_class\",\"ldap_username_attr\",\"ldap_groupname_attr\",\"ldap_group_member_attr\",\"ldap_member_attr\",\"openid_create_account\" FROM \"settings\"", "describe": { "columns": [ { @@ -173,6 +173,11 @@ "ordinal": 31, "name": "ldap_member_attr", "type_info": "Text" + }, + { + "ordinal": 32, + "name": "openid_create_account", + "type_info": "Bool" } ], "parameters": { @@ -210,8 +215,9 @@ true, true, true, - true + true, + false ] }, - "hash": "e1875cb32cf6f270541b27319cb472abd8f6ed0dccc77a2ba0908dd487975ef5" + "hash": "a9d694c5efe301ef975e1b6fc0ca0acc741c16c3efdd7a92475a419e9e30db4c" } diff --git a/.sqlx/query-b12208760e0fc61c766c7c2d037d4e20c54f7bb6f612156de08f5ba26bd0b7df.json b/.sqlx/query-b12208760e0fc61c766c7c2d037d4e20c54f7bb6f612156de08f5ba26bd0b7df.json new file mode 100644 index 0000000000..0d1ced94ee --- /dev/null +++ b/.sqlx/query-b12208760e0fc61c766c7c2d037d4e20c54f7bb6f612156de08f5ba26bd0b7df.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", \"name\",\"base_url\",\"client_id\",\"client_secret\" FROM \"openidprovider\" WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "base_url", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "client_id", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "client_secret", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "b12208760e0fc61c766c7c2d037d4e20c54f7bb6f612156de08f5ba26bd0b7df" +} diff --git a/.sqlx/query-8bd853a756c53e3f6bf0ae002c46709a7bb35324df203ce9918d67905d59cdc8.json b/.sqlx/query-e769bdd845a133b817d33627fd43862607a138b42416ccb2dabfb2ea953b482f.json similarity index 91% rename from .sqlx/query-8bd853a756c53e3f6bf0ae002c46709a7bb35324df203ce9918d67905d59cdc8.json rename to .sqlx/query-e769bdd845a133b817d33627fd43862607a138b42416ccb2dabfb2ea953b482f.json index 591494b00b..ff55394f7a 100644 --- a/.sqlx/query-8bd853a756c53e3f6bf0ae002c46709a7bb35324df203ce9918d67905d59cdc8.json +++ b/.sqlx/query-e769bdd845a133b817d33627fd43862607a138b42416ccb2dabfb2ea953b482f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET \"openid_enabled\" = $2,\"wireguard_enabled\" = $3,\"webhooks_enabled\" = $4,\"worker_enabled\" = $5,\"challenge_template\" = $6,\"instance_name\" = $7,\"main_logo_url\" = $8,\"nav_logo_url\" = $9,\"smtp_server\" = $10,\"smtp_port\" = $11,\"smtp_encryption\" = $12,\"smtp_user\" = $13,\"smtp_password\" = $14,\"smtp_sender\" = $15,\"enrollment_vpn_step_optional\" = $16,\"enrollment_welcome_message\" = $17,\"enrollment_welcome_email\" = $18,\"enrollment_welcome_email_subject\" = $19,\"enrollment_use_welcome_message_as_email\" = $20,\"uuid\" = $21,\"ldap_url\" = $22,\"ldap_bind_username\" = $23,\"ldap_bind_password\" = $24,\"ldap_group_search_base\" = $25,\"ldap_user_search_base\" = $26,\"ldap_user_obj_class\" = $27,\"ldap_group_obj_class\" = $28,\"ldap_username_attr\" = $29,\"ldap_groupname_attr\" = $30,\"ldap_group_member_attr\" = $31,\"ldap_member_attr\" = $32 WHERE id = $1", + "query": "UPDATE \"settings\" SET \"openid_enabled\" = $2,\"wireguard_enabled\" = $3,\"webhooks_enabled\" = $4,\"worker_enabled\" = $5,\"challenge_template\" = $6,\"instance_name\" = $7,\"main_logo_url\" = $8,\"nav_logo_url\" = $9,\"smtp_server\" = $10,\"smtp_port\" = $11,\"smtp_encryption\" = $12,\"smtp_user\" = $13,\"smtp_password\" = $14,\"smtp_sender\" = $15,\"enrollment_vpn_step_optional\" = $16,\"enrollment_welcome_message\" = $17,\"enrollment_welcome_email\" = $18,\"enrollment_welcome_email_subject\" = $19,\"enrollment_use_welcome_message_as_email\" = $20,\"uuid\" = $21,\"ldap_url\" = $22,\"ldap_bind_username\" = $23,\"ldap_bind_password\" = $24,\"ldap_group_search_base\" = $25,\"ldap_user_search_base\" = $26,\"ldap_user_obj_class\" = $27,\"ldap_group_obj_class\" = $28,\"ldap_username_attr\" = $29,\"ldap_groupname_attr\" = $30,\"ldap_group_member_attr\" = $31,\"ldap_member_attr\" = $32,\"openid_create_account\" = $33 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -47,10 +47,11 @@ "Text", "Text", "Text", - "Text" + "Text", + "Bool" ] }, "nullable": [] }, - "hash": "8bd853a756c53e3f6bf0ae002c46709a7bb35324df203ce9918d67905d59cdc8" + "hash": "e769bdd845a133b817d33627fd43862607a138b42416ccb2dabfb2ea953b482f" } diff --git a/.sqlx/query-e181b36a398dc70efd14bb7dc52138a17a5ec4c3210d5960149684e77ae88726.json b/.sqlx/query-fb71770a725becd8d8f53554ded17e189c90b658aa343fc682b05be24f410b85.json similarity index 93% rename from .sqlx/query-e181b36a398dc70efd14bb7dc52138a17a5ec4c3210d5960149684e77ae88726.json rename to .sqlx/query-fb71770a725becd8d8f53554ded17e189c90b658aa343fc682b05be24f410b85.json index 45a90b155d..5441c96029 100644 --- a/.sqlx/query-e181b36a398dc70efd14bb7dc52138a17a5ec4c3210d5960149684e77ae88726.json +++ b/.sqlx/query-fb71770a725becd8d8f53554ded17e189c90b658aa343fc682b05be24f410b85.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"openid_enabled\",\"wireguard_enabled\",\"webhooks_enabled\",\"worker_enabled\",\"challenge_template\",\"instance_name\",\"main_logo_url\",\"nav_logo_url\",\"smtp_server\",\"smtp_port\",\"smtp_encryption\" \"smtp_encryption: _\",\"smtp_user\",\"smtp_password\" \"smtp_password?: SecretString\",\"smtp_sender\",\"enrollment_vpn_step_optional\",\"enrollment_welcome_message\",\"enrollment_welcome_email\",\"enrollment_welcome_email_subject\",\"enrollment_use_welcome_message_as_email\",\"uuid\",\"ldap_url\",\"ldap_bind_username\",\"ldap_bind_password\" \"ldap_bind_password?: SecretString\",\"ldap_group_search_base\",\"ldap_user_search_base\",\"ldap_user_obj_class\",\"ldap_group_obj_class\",\"ldap_username_attr\",\"ldap_groupname_attr\",\"ldap_group_member_attr\",\"ldap_member_attr\" FROM \"settings\" WHERE id = $1", + "query": "SELECT id \"id?\", \"openid_enabled\",\"wireguard_enabled\",\"webhooks_enabled\",\"worker_enabled\",\"challenge_template\",\"instance_name\",\"main_logo_url\",\"nav_logo_url\",\"smtp_server\",\"smtp_port\",\"smtp_encryption\" \"smtp_encryption: _\",\"smtp_user\",\"smtp_password\" \"smtp_password?: SecretString\",\"smtp_sender\",\"enrollment_vpn_step_optional\",\"enrollment_welcome_message\",\"enrollment_welcome_email\",\"enrollment_welcome_email_subject\",\"enrollment_use_welcome_message_as_email\",\"uuid\",\"ldap_url\",\"ldap_bind_username\",\"ldap_bind_password\" \"ldap_bind_password?: SecretString\",\"ldap_group_search_base\",\"ldap_user_search_base\",\"ldap_user_obj_class\",\"ldap_group_obj_class\",\"ldap_username_attr\",\"ldap_groupname_attr\",\"ldap_group_member_attr\",\"ldap_member_attr\",\"openid_create_account\" FROM \"settings\" WHERE id = $1", "describe": { "columns": [ { @@ -173,6 +173,11 @@ "ordinal": 31, "name": "ldap_member_attr", "type_info": "Text" + }, + { + "ordinal": 32, + "name": "openid_create_account", + "type_info": "Bool" } ], "parameters": { @@ -212,8 +217,9 @@ true, true, true, - true + true, + false ] }, - "hash": "e181b36a398dc70efd14bb7dc52138a17a5ec4c3210d5960149684e77ae88726" + "hash": "fb71770a725becd8d8f53554ded17e189c90b658aa343fc682b05be24f410b85" } diff --git a/.sqlx/query-2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9.json b/.sqlx/query-fd4750032e0fcd91f0f8fad9aa946211c645f786602e89561f03a5a60661d191.json similarity index 84% rename from .sqlx/query-2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9.json rename to .sqlx/query-fd4750032e0fcd91f0f8fad9aa946211c645f786602e89561f03a5a60661d191.json index c3182f082b..f0dc610017 100644 --- a/.sqlx/query-2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9.json +++ b/.sqlx/query-fd4750032e0fcd91f0f8fad9aa946211c645f786602e89561f03a5a60661d191.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\" WHERE id = $1", + "query": "SELECT id \"id?\", \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"openid_login\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\" WHERE id = $1", "describe": { "columns": [ { @@ -50,26 +50,31 @@ }, { "ordinal": 9, - "name": "totp_enabled", + "name": "openid_login", "type_info": "Bool" }, { "ordinal": 10, - "name": "email_mfa_enabled", + "name": "totp_enabled", "type_info": "Bool" }, { "ordinal": 11, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 12, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 12, + "ordinal": 13, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 13, + "ordinal": 14, "name": "mfa_method: _", "type_info": { "Custom": { @@ -87,7 +92,7 @@ } }, { - "ordinal": 14, + "ordinal": 15, "name": "recovery_codes: _", "type_info": "TextArray" } @@ -109,11 +114,12 @@ false, false, false, + false, true, true, false, false ] }, - "hash": "2b9b3aa210fa00d43bece1acbd3ae490fea37010d98f3eff2ba15f50e8a6d7a9" + "hash": "fd4750032e0fcd91f0f8fad9aa946211c645f786602e89561f03a5a60661d191" } diff --git a/.sqlx/query-7ce8c80bce6f2ed44b7abaa53e65f6ff40beeeabdd4d88a393dd9986ab59934b.json b/.sqlx/query-fdb364b8984eff1c2ca14153eaf44fa0ccc6a7e26874afecc62b00de993a12aa.json similarity index 84% rename from .sqlx/query-7ce8c80bce6f2ed44b7abaa53e65f6ff40beeeabdd4d88a393dd9986ab59934b.json rename to .sqlx/query-fdb364b8984eff1c2ca14153eaf44fa0ccc6a7e26874afecc62b00de993a12aa.json index de92e46380..9b24a3350d 100644 --- a/.sqlx/query-7ce8c80bce6f2ed44b7abaa53e65f6ff40beeeabdd4d88a393dd9986ab59934b.json +++ b/.sqlx/query-fdb364b8984eff1c2ca14153eaf44fa0ccc6a7e26874afecc62b00de993a12aa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"settings\" (\"openid_enabled\",\"wireguard_enabled\",\"webhooks_enabled\",\"worker_enabled\",\"challenge_template\",\"instance_name\",\"main_logo_url\",\"nav_logo_url\",\"smtp_server\",\"smtp_port\",\"smtp_encryption\",\"smtp_user\",\"smtp_password\",\"smtp_sender\",\"enrollment_vpn_step_optional\",\"enrollment_welcome_message\",\"enrollment_welcome_email\",\"enrollment_welcome_email_subject\",\"enrollment_use_welcome_message_as_email\",\"uuid\",\"ldap_url\",\"ldap_bind_username\",\"ldap_bind_password\",\"ldap_group_search_base\",\"ldap_user_search_base\",\"ldap_user_obj_class\",\"ldap_group_obj_class\",\"ldap_username_attr\",\"ldap_groupname_attr\",\"ldap_group_member_attr\",\"ldap_member_attr\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31) RETURNING id", + "query": "INSERT INTO \"settings\" (\"openid_enabled\",\"wireguard_enabled\",\"webhooks_enabled\",\"worker_enabled\",\"challenge_template\",\"instance_name\",\"main_logo_url\",\"nav_logo_url\",\"smtp_server\",\"smtp_port\",\"smtp_encryption\",\"smtp_user\",\"smtp_password\",\"smtp_sender\",\"enrollment_vpn_step_optional\",\"enrollment_welcome_message\",\"enrollment_welcome_email\",\"enrollment_welcome_email_subject\",\"enrollment_use_welcome_message_as_email\",\"uuid\",\"ldap_url\",\"ldap_bind_username\",\"ldap_bind_password\",\"ldap_group_search_base\",\"ldap_user_search_base\",\"ldap_user_obj_class\",\"ldap_group_obj_class\",\"ldap_username_attr\",\"ldap_groupname_attr\",\"ldap_group_member_attr\",\"ldap_member_attr\",\"openid_create_account\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32) RETURNING id", "describe": { "columns": [ { @@ -52,12 +52,13 @@ "Text", "Text", "Text", - "Text" + "Text", + "Bool" ] }, "nullable": [ false ] }, - "hash": "7ce8c80bce6f2ed44b7abaa53e65f6ff40beeeabdd4d88a393dd9986ab59934b" + "hash": "fdb364b8984eff1c2ca14153eaf44fa0ccc6a7e26874afecc62b00de993a12aa" } diff --git a/Cargo.lock b/Cargo.lock index 104e74e4ce..5a644ce8b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2604,6 +2604,7 @@ dependencies = [ "getrandom", "http 0.2.12", "rand", + "reqwest 0.11.27", "serde", "serde_json", "serde_path_to_error", diff --git a/Cargo.toml b/Cargo.toml index 13e39f3a11..6dc403ef54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,9 @@ lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } md4 = "0.10" mime_guess = "2.0" model_derive = { path = "model-derive" } -openidconnect = { version = "3.5", default-features = false, optional = true } +openidconnect = { version = "3.5", default-features = false, optional = true, features = [ + "reqwest", +] } otpauth = "0.4" prost = "0.12" pulldown-cmark = "0.11" diff --git a/LICENSE b/LICENSE index 8ddd140946..9d2476c053 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,8 @@ Copyright 2023 teonite ventures sp. z o.o. (teonite) +NOTE: The following license applies to the entire repository except for all the contents in the "enterprise" directory. +For the license of the enterprise directory, go to enterprise/LICENSE. + 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 diff --git a/e2e/tests/vpn/wizard.spec.ts b/e2e/tests/vpn/wizard.spec.ts index 4e9c597e32..8afb9f29ad 100644 --- a/e2e/tests/vpn/wizard.spec.ts +++ b/e2e/tests/vpn/wizard.spec.ts @@ -37,6 +37,7 @@ test.describe('Setup VPN (wizard) ', () => { ...testUserTemplate, firstName: `test${id}`, username: `test${id}`, + mail: `test${id}@test.com` })); await loginBasic(page, defaultUserAdmin); await apiCreateUsersBulk(page, users); diff --git a/migrations/20240716114732_add_external_openid_login.down.sql b/migrations/20240716114732_add_external_openid_login.down.sql new file mode 100644 index 0000000000..92196a2601 --- /dev/null +++ b/migrations/20240716114732_add_external_openid_login.down.sql @@ -0,0 +1,4 @@ +DROP TABLE openidprovider; +ALTER TABLE "user" DROP CONSTRAINT "user_email_key"; +ALTER TABLE "user" DROP COLUMN "openid_login"; +ALTER TABLE settings DROP COLUMN openid_create_account; diff --git a/migrations/20240716114732_add_external_openid_login.up.sql b/migrations/20240716114732_add_external_openid_login.up.sql new file mode 100644 index 0000000000..169801721f --- /dev/null +++ b/migrations/20240716114732_add_external_openid_login.up.sql @@ -0,0 +1,19 @@ +-- External OpenID login +CREATE TABLE openidprovider ( + id bigserial PRIMARY KEY, + "name" text NOT NULL, + "base_url" text NOT NULL, + "client_id" text NOT NULL, + "client_secret" text NOT NULL, + "enabled" boolean NOT NULL DEFAULT FALSE, + CONSTRAINT openidprovider_name_unique UNIQUE ("name"), + CONSTRAINT openidprovider_client_id_unique UNIQUE ("client_id"), + CONSTRAINT openidprovider_client_secret_unique UNIQUE ("client_secret") +); + +ALTER TABLE "user" ADD COLUMN "openid_login" BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE settings ADD COLUMN openid_create_account BOOLEAN NOT NULL DEFAULT TRUE; + +-- Make emails unique +-- This migration may fail if there are duplicate emails in the database already +ALTER TABLE "user" ADD CONSTRAINT "user_email_key" UNIQUE (email); diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 6f2c907c70..15b495af56 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -61,7 +61,7 @@ impl Group { User, "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes, is_active \ + mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login \ FROM \"user\" \ JOIN group_user ON \"user\".id = group_user.user_id \ WHERE group_user.group_id = $1", diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 14229c8fee..73722c1026 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -99,7 +99,7 @@ impl UserInfo { mfa_method: user.mfa_method.clone(), authorized_apps, is_active: user.is_active, - enrolled: user.has_password(), + enrolled: user.is_enrolled(), }) } diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index ed8257ba57..8f0d343f06 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -62,6 +62,8 @@ pub struct Settings { pub ldap_groupname_attr: Option, pub ldap_group_member_attr: Option, pub ldap_member_attr: Option, + // Whether to create a new account when users try to log in with external OpenID + pub openid_create_account: bool, } impl Settings { diff --git a/src/db/models/user.rs b/src/db/models/user.rs index a56aedf224..773c87b3af 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -78,6 +78,8 @@ pub struct User { pub phone: Option, pub mfa_enabled: bool, pub is_active: bool, + // Whether the user has logged in using OIDC + pub openid_login: bool, // secret has been verified and TOTP can be used pub(crate) totp_enabled: bool, pub(crate) email_mfa_enabled: bool, @@ -124,6 +126,7 @@ impl User { mfa_method: MFAMethod::None, recovery_codes: Vec::new(), is_active: true, + openid_login: false, } } @@ -154,6 +157,13 @@ impl User { format!("{} {}", self.first_name, self.last_name) } + /// Check if user is enrolled. + /// We assume the user is enrolled if they have a password set + /// or they have logged in using an external OIDC. + pub fn is_enrolled(&self) -> bool { + self.password_hash.is_some() || self.openid_login + } + /// Generate new TOTP secret, save it, then return it as RFC 4648 base32-encoded string. pub async fn new_totp_secret<'e, E>(&mut self, executor: E) -> Result where @@ -455,7 +465,7 @@ impl User { ) -> Result, SqlxError> { let users = query!( "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, \ - mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active \ + mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active, openid_login \ FROM \"user\"" ) .fetch_all(pool) @@ -469,7 +479,7 @@ impl User { mfa_enabled: u.mfa_enabled, id: u.id, is_active: u.is_active, - enrolled: u.password_hash.is_some(), + enrolled: u.password_hash.is_some() || u.openid_login, }) .collect(); Ok(res) @@ -485,7 +495,7 @@ impl User { "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes, is_active \ + mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login \ FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id @@ -576,7 +586,7 @@ impl User { Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login \ FROM \"user\" WHERE username = $1", username ) @@ -592,7 +602,7 @@ impl User { Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login \ FROM \"user\" WHERE email = $1", email ) diff --git a/src/enterprise/LICENSE b/src/enterprise/LICENSE new file mode 100644 index 0000000000..79e35089e6 --- /dev/null +++ b/src/enterprise/LICENSE @@ -0,0 +1 @@ +For enterprise license and usage of this code and binary distribution, please contact us by email at: salesdefguard.net diff --git a/src/enterprise/db/mod.rs b/src/enterprise/db/mod.rs new file mode 100644 index 0000000000..c446ac8833 --- /dev/null +++ b/src/enterprise/db/mod.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/src/enterprise/db/models/mod.rs b/src/enterprise/db/models/mod.rs new file mode 100644 index 0000000000..972680e374 --- /dev/null +++ b/src/enterprise/db/models/mod.rs @@ -0,0 +1 @@ +pub mod openid_provider; diff --git a/src/enterprise/db/models/openid_provider.rs b/src/enterprise/db/models/openid_provider.rs new file mode 100644 index 0000000000..1d63214957 --- /dev/null +++ b/src/enterprise/db/models/openid_provider.rs @@ -0,0 +1,64 @@ +use model_derive::Model; +use sqlx::{query, query_as, Error as SqlxError}; + +use crate::db::DbPool; + +#[derive(Deserialize, Model, Serialize)] +pub struct OpenIdProvider { + pub id: Option, + pub name: String, + pub base_url: String, + pub client_id: String, + pub client_secret: String, +} + +impl OpenIdProvider { + #[must_use] + pub fn new>(name: S, base_url: S, client_id: S, client_secret: S) -> Self { + Self { + id: None, + name: name.into(), + base_url: base_url.into(), + client_id: client_id.into(), + client_secret: client_secret.into(), + } + } + + pub async fn find_by_name(pool: &DbPool, name: &str) -> Result, SqlxError> { + query_as!( + OpenIdProvider, + "SELECT id \"id?\", name, base_url, client_id, client_secret FROM openidprovider WHERE name = $1", + name + ) + .fetch_optional(pool) + .await + } + + pub async fn upsert(&mut self, pool: &DbPool) -> Result<(), SqlxError> { + if let Some(provider) = OpenIdProvider::get_current(pool).await? { + query!( + "UPDATE openidprovider SET name = $1, base_url = $2, client_id = $3, client_secret = $4 WHERE id = $5", + self.name, + self.base_url, + self.client_id, + self.client_secret, + provider.id + ) + .execute(pool) + .await?; + } else { + self.save(pool).await?; + } + + Ok(()) + } + + pub async fn get_current(pool: &DbPool) -> Result, SqlxError> { + query_as!( + OpenIdProvider, + "SELECT id \"id?\", name, base_url, client_id, client_secret FROM openidprovider" + ) + .fetch_optional(pool) + .await + } +} diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs new file mode 100644 index 0000000000..0ffbdd9e5f --- /dev/null +++ b/src/enterprise/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod openid_login; +pub mod openid_providers; diff --git a/src/enterprise/handlers/openid_login.rs b/src/enterprise/handlers/openid_login.rs new file mode 100644 index 0000000000..5a81583633 --- /dev/null +++ b/src/enterprise/handlers/openid_login.rs @@ -0,0 +1,414 @@ +use axum::http::StatusCode; + +use axum::Json; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use serde_json::json; + +use time::Duration; + +use axum::extract::State; + +use axum_client_ip::{InsecureClientIp, LeftmostXForwardedFor}; +use axum_extra::extract::{CookieJar, PrivateCookieJar}; +use axum_extra::headers::UserAgent; +use axum_extra::TypedHeader; +use openidconnect::core::{ + CoreClient, CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, CoreResponseType, +}; +use openidconnect::{ + core::CoreProviderMetadata, reqwest::async_http_client, ClientId, ClientSecret, IssuerUrl, + ProviderMetadata, RedirectUrl, +}; +use openidconnect::{AuthenticationFlow, CsrfToken, EmptyAdditionalClaims, IdToken, Nonce, Scope}; + +use crate::appstate::AppState; +use crate::db::{DbPool, MFAInfo, Session, SessionState, Settings, User, UserInfo}; +use crate::enterprise::db::models::openid_provider::OpenIdProvider; +use crate::error::WebError; +use crate::handlers::user::check_username; +use crate::handlers::{ApiResponse, AuthResponse, SESSION_COOKIE_NAME, SIGN_IN_COOKIE_NAME}; +use crate::headers::{check_new_device_login, get_user_agent_device, parse_user_agent}; +use crate::server_config; + +type ProvMeta = ProviderMetadata< + openidconnect::EmptyAdditionalProviderMetadata, + openidconnect::core::CoreAuthDisplay, + openidconnect::core::CoreClientAuthMethod, + openidconnect::core::CoreClaimName, + openidconnect::core::CoreClaimType, + openidconnect::core::CoreGrantType, + openidconnect::core::CoreJweContentEncryptionAlgorithm, + openidconnect::core::CoreJweKeyManagementAlgorithm, + openidconnect::core::CoreJwsSigningAlgorithm, + openidconnect::core::CoreJsonWebKeyType, + openidconnect::core::CoreJsonWebKeyUse, + openidconnect::core::CoreJsonWebKey, + openidconnect::core::CoreResponseMode, + openidconnect::core::CoreResponseType, + openidconnect::core::CoreSubjectIdentifierType, +>; + +async fn get_provider_metadata(url: &str) -> Result { + let issuer_url = IssuerUrl::new(url.to_string()).unwrap(); + + // Discover the provider metadata based on a known base issuer URL + // The url should be in the form of e.g. https://accounts.google.com + // The url shouldn't contain a .well-known part, it will be added automatically + let provider_metadata = + match CoreProviderMetadata::discover_async(issuer_url, async_http_client).await { + Ok(metadata) => metadata, + Err(_) => { + return Err(WebError::Authorization(format!( + "Failed to discover provider metadata, make sure the providers' url is correct: {}", + url + ))); + } + }; + + Ok(provider_metadata) +} + +async fn make_oidc_client(pool: &DbPool) -> Result { + let provider = match OpenIdProvider::get_current(pool).await? { + Some(provider) => provider, + None => { + return Err(WebError::ObjectNotFound( + "OpenID provider not set".to_string(), + )); + } + }; + + let provider_metadata = get_provider_metadata(&provider.base_url).await?; + let client_id = ClientId::new(provider.client_id); + let client_secret = ClientSecret::new(provider.client_secret); + let config = server_config(); + let url = format!("{}auth/callback", config.url); + let redirect_url = match RedirectUrl::new(url) { + Ok(url) => url, + Err(err) => { + error!("Failed to create redirect URL: {:?}", err); + return Err(WebError::Authorization( + "Failed to create redirect URL".to_string(), + )); + } + }; + + Ok( + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + .set_redirect_uri(redirect_url), + ) +} + +pub async fn get_auth_info( + private_cookies: PrivateCookieJar, + State(appstate): State, +) -> Result<(PrivateCookieJar, ApiResponse), WebError> { + let client = make_oidc_client(&appstate.pool).await?; + + // Generate the redirect URL and the values needed later for callback authenticity verification + let (authorize_url, csrf_state, nonce) = client + .authorize_url( + AuthenticationFlow::::Implicit(false), + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())) + .url(); + + let config = server_config(); + let nonce_cookie = Cookie::build(("nonce", nonce.secret().clone())) + .domain( + config + .cookie_domain + .clone() + .expect("Cookie domain not found"), + ) + .path("/api/v1/openid/callback") + .http_only(true) + .same_site(SameSite::Strict) + .secure(true) + .max_age(Duration::days(1)) + .build(); + let csrf_cookie = Cookie::build(("csrf", csrf_state.secret().clone())) + .domain( + config + .cookie_domain + .clone() + .expect("Cookie domain not found"), + ) + .path("/api/v1/openid/callback") + .http_only(true) + .same_site(SameSite::Strict) + .secure(true) + .max_age(Duration::days(1)) + .build(); + + let private_cookies = private_cookies.add(nonce_cookie).add(csrf_cookie); + + Ok(( + private_cookies, + ApiResponse { + json: json!( + { + "url": authorize_url, + } + ), + status: StatusCode::OK, + }, + )) +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct AuthenticationResponse { + id_token: IdToken< + EmptyAdditionalClaims, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + >, + state: CsrfToken, +} + +pub async fn auth_callback( + cookies: CookieJar, + private_cookies: PrivateCookieJar, + user_agent: Option>, + forwarded_for_ip: Option, + InsecureClientIp(insecure_ip): InsecureClientIp, + State(appstate): State, + Json(payload): Json, +) -> Result<(CookieJar, PrivateCookieJar, ApiResponse), WebError> { + debug!("Auth callback received, logging in user..."); + + // Get the nonce and csrf cookies, we need them to verify the callback + let mut private_cookies = private_cookies; + let cookie_nonce = private_cookies + .get("nonce") + .ok_or(WebError::Authorization( + "Nonce cookie not found".to_string(), + ))? + .value_trimmed() + .to_string(); + let cookie_csrf = private_cookies + .get("csrf") + .ok_or(WebError::BadRequest("CSRF cookie not found".to_string()))? + .value_trimmed() + .to_string(); + + // Verify the csrf token + if *payload.state.secret() != cookie_csrf { + return Err(WebError::Authorization("CSRF token mismatch".to_string())); + }; + + // Get the ID token and verify it against the nonce value received in the callback + let client = make_oidc_client(&appstate.pool).await?; + let nonce = Nonce::new(cookie_nonce); + let token_verifier = client.id_token_verifier(); + let id_token = payload.id_token; + + private_cookies = private_cookies + .remove(Cookie::from("nonce")) + .remove(Cookie::from("csrf")); + + // claims = user attributes + let token_claims = match id_token.claims(&token_verifier, &nonce) { + Ok(claims) => claims, + Err(error) => { + return Err(WebError::Authorization(format!( + "Failed to verify ID token, error: {:?}", + error + ))); + } + }; + + // Only email and username is required for user lookup and login + let email = token_claims.email().ok_or(WebError::BadRequest( + "Email not found in the information returned from provider.".to_string(), + ))?; + let username = email + .split('@') + .next() + .ok_or(WebError::BadRequest( + "Failed to extract username from email address".to_string(), + ))? + // + is not allowed in usernames, but fairly common in email addresses + // TODO: Make this more robust, maybe trim everything that's forbidden in usernames + .replace('+', "_"); + + check_username(&username)?; + + // Handle logging in or creating the user + let settings = Settings::get_settings(&appstate.pool).await?; + let user = match User::find_by_email(&appstate.pool, email).await { + Ok(Some(mut user)) => { + // Make sure the user is not disabled + if !user.is_active { + return Err(WebError::Authorization("User is disabled".to_string())); + } + + if !user.openid_login { + user.openid_login = true; + user.save(&appstate.pool).await?; + } + user + } + Ok(None) => { + // Check if the user should be created if they don't exist (default: true) + if settings.openid_create_account { + // Check if user with the same username already exists + if User::find_by_username(&appstate.pool, &username) + .await? + .is_some() + { + return Err(WebError::Authorization(format!( + "User with username {} already exists", + username + ))); + } + + // Extract all necessary information from the token needed to create an account + let given_name_error = + "Given name not found in the information returned from provider."; + let given_name = token_claims + .given_name() + .ok_or(WebError::BadRequest(given_name_error.to_string()))? + // 'None' gets you the default value from a localized claim. Otherwise you would need to pass a locale. + .get(None) + .ok_or(WebError::BadRequest(given_name_error.to_string()))?; + let family_name_error = + "Family name not found in the information returned from provider."; + let family_name = token_claims + .family_name() + .ok_or(WebError::BadRequest(family_name_error.to_string()))? + .get(None) + .ok_or(WebError::BadRequest(family_name_error.to_string()))?; + let phone = token_claims.phone_number(); + + let mut user = User::new( + username.to_string(), + None, + family_name.to_string(), + given_name.to_string(), + email.to_string(), + phone.map(|v| v.to_string()), + ); + user.openid_login = true; + user.save(&appstate.pool).await?; + user + } else { + return Err(WebError::Authorization( + "User not found. The user needs to be created first in order to login using OIDC." + .to_string(), + )); + } + } + Err(e) => { + return Err(WebError::Authorization(e.to_string())); + } + }; + + // Handle creating the session + let ip_address = forwarded_for_ip.map_or(insecure_ip, |v| v.0).to_string(); + let user_agent_string = match user_agent { + Some(value) => value.to_string(), + None => String::new(), + }; + let agent = parse_user_agent(&appstate.user_agent_parser, &user_agent_string); + let device_info = agent.clone().map(|v| get_user_agent_device(&v)); + Session::delete_expired(&appstate.pool).await?; + let session = Session::new( + user.id.unwrap(), + SessionState::PasswordVerified, + ip_address.clone(), + device_info, + ); + session.save(&appstate.pool).await?; + let max_age = Duration::seconds(server_config().auth_cookie_timeout.as_secs() as i64); + let config = server_config(); + let auth_cookie = Cookie::build((SESSION_COOKIE_NAME, session.id.clone())) + .domain( + config + .cookie_domain + .clone() + .expect("Cookie domain not found"), + ) + .path("/") + .http_only(true) + .secure(!config.cookie_insecure) + .same_site(SameSite::Lax) + .max_age(max_age); + let cookies = cookies.add(auth_cookie); + let login_event_type = "AUTHENTICATION".to_string(); + + info!("Authenticated user {username} with external OpenID provider. Veryfing MFA status..."); + if user.mfa_enabled { + if let Some(mfa_info) = MFAInfo::for_user(&appstate.pool, &user).await? { + check_new_device_login( + &appstate.pool, + &appstate.mail_tx, + &session, + &user, + ip_address, + login_event_type, + agent, + ) + .await?; + Ok(( + cookies, + private_cookies, + ApiResponse { + json: json!(mfa_info), + status: StatusCode::CREATED, + }, + )) + } else { + error!("Couldn't fetch MFA info for user {username} with MFA enabled"); + Err(WebError::DbError("MFA info read error".into())) + } + } else { + let user_info = UserInfo::from_user(&appstate.pool, &user).await?; + + check_new_device_login( + &appstate.pool, + &appstate.mail_tx, + &session, + &user, + ip_address, + login_event_type, + agent, + ) + .await?; + + if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { + debug!("Found openid session cookie."); + let redirect_url = openid_cookie.value().to_string(); + Ok(( + cookies, + private_cookies.remove(openid_cookie), + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: Some(redirect_url) + }), + status: StatusCode::OK, + }, + )) + } else { + debug!("No OpenID session found"); + Ok(( + cookies, + private_cookies, + ApiResponse { + json: json!(AuthResponse { + user: user_info, + url: None, + }), + status: StatusCode::OK, + }, + )) + } + } +} diff --git a/src/enterprise/handlers/openid_providers.rs b/src/enterprise/handlers/openid_providers.rs new file mode 100644 index 0000000000..6c94149d1c --- /dev/null +++ b/src/enterprise/handlers/openid_providers.rs @@ -0,0 +1,162 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; + +use crate::{ + appstate::AppState, + auth::{AdminRole, SessionInfo}, + enterprise::db::models::openid_provider::OpenIdProvider, + handlers::{ApiResponse, ApiResult}, +}; + +use serde_json::json; + +#[derive(Debug, Deserialize, Serialize)] +pub struct AddProviderData { + name: String, + base_url: String, + client_id: String, + client_secret: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DeleteProviderData { + name: String, +} + +impl AddProviderData { + pub fn new(name: &str, base_url: &str, client_id: &str, client_secret: &str) -> Self { + Self { + name: name.to_string(), + base_url: base_url.to_string(), + client_id: client_id.to_string(), + client_secret: client_secret.to_string(), + } + } +} + +pub async fn add_openid_provider( + _admin: AdminRole, + session: SessionInfo, + State(appstate): State, + Json(provider_data): Json, +) -> ApiResult { + let mut new_provider = OpenIdProvider::new( + provider_data.name, + provider_data.base_url, + provider_data.client_id, + provider_data.client_secret, + ); + debug!( + "User {} adding OpenID provider {}", + session.user.username, new_provider.name + ); + // Currently, we only support one OpenID provider at a time + new_provider.upsert(&appstate.pool).await?; + info!( + "User {} added OpenID client {}", + session.user.username, new_provider.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::CREATED, + }) +} + +pub async fn get_current_openid_provider( + _admin: AdminRole, + State(appstate): State, +) -> ApiResult { + match OpenIdProvider::get_current(&appstate.pool).await? { + Some(provider) => Ok(ApiResponse { + json: json!(provider), + status: StatusCode::OK, + }), + None => Ok(ApiResponse { + json: json!({}), + status: StatusCode::NOT_FOUND, + }), + } +} + +pub async fn delete_openid_provider( + _admin: AdminRole, + session: SessionInfo, + State(appstate): State, + Path(provider_data): Path, +) -> ApiResult { + debug!( + "User {} deleting OpenID provider {}", + session.user.username, provider_data.name + ); + let provider = OpenIdProvider::find_by_name(&appstate.pool, &provider_data.name).await?; + if let Some(provider) = provider { + provider.delete(&appstate.pool).await?; + info!( + "User {} deleted OpenID provider {}", + session.user.username, provider_data.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::OK, + }) + } else { + warn!( + "User {} failed to delete OpenID provider {}. Such provider does not exist.", + session.user.username, provider_data.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::NOT_FOUND, + }) + } +} + +pub async fn modify_openid_provider( + _admin: AdminRole, + session: SessionInfo, + State(appstate): State, + Json(provider_data): Json, +) -> ApiResult { + debug!( + "User {} modifying OpenID provider {}", + session.user.username, provider_data.name + ); + let provider = OpenIdProvider::find_by_name(&appstate.pool, &provider_data.name).await?; + if let Some(mut provider) = provider { + provider.base_url = provider_data.base_url; + provider.client_id = provider_data.client_id; + provider.client_secret = provider_data.client_secret; + provider.save(&appstate.pool).await?; + info!( + "User {} modified OpenID client {}", + session.user.username, provider.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::OK, + }) + } else { + warn!( + "User {} failed to modify OpenID client {}. Such client does not exist.", + session.user.username, provider_data.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::NOT_FOUND, + }) + } +} + +pub async fn list_openid_providers( + _admin: AdminRole, + State(appstate): State, +) -> ApiResult { + let providers = OpenIdProvider::all(&appstate.pool).await?; + Ok(ApiResponse { + json: json!(providers), + status: StatusCode::OK, + }) +} diff --git a/src/enterprise/mod.rs b/src/enterprise/mod.rs new file mode 100644 index 0000000000..212ebefc90 --- /dev/null +++ b/src/enterprise/mod.rs @@ -0,0 +1,2 @@ +pub mod db; +pub mod handlers; diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 48dc0d44a8..1e314f8f45 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -516,7 +516,7 @@ impl From for AdminInfo { impl InitialUserInfo { async fn from_user(pool: &DbPool, user: User) -> Result { - let is_enrolled = user.has_password(); + let enrolled = user.is_enrolled(); let devices = user.devices(pool).await?; let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); Ok(Self { @@ -527,7 +527,7 @@ impl InitialUserInfo { phone_number: user.phone, is_active: user.is_active, device_names, - enrolled: is_enrolled, + enrolled, }) } } diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index d5c076aac1..088ffdf453 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -57,7 +57,7 @@ pub async fn authenticate( // check if user can proceed with login check_username(&appstate.failed_logins, &username)?; - let user = match User::find_by_username(&appstate.pool, &username).await { + let user: User = match User::find_by_username(&appstate.pool, &username).await { Ok(Some(user)) => match user.verify_password(&data.password) { Ok(()) => { if user.is_active { @@ -74,14 +74,39 @@ pub async fn authenticate( } }, Ok(None) => { - // create user from LDAP - debug!("User not found in DB, authenticating user {username} with LDAP"); - if let Ok(user) = user_from_ldap(&appstate.pool, &username, &data.password).await { - user - } else { - info!("Failed to authenticate user {username} with LDAP"); - log_failed_login_attempt(&appstate.failed_logins, &username); - return Err(WebError::Authorization("user not found".into())); + match User::find_by_email(&appstate.pool, &username).await { + Ok(Some(user)) => match user.verify_password(&data.password) { + Ok(()) => { + if user.is_active { + user + } else { + info!("Failed to authenticate user {username}: user is disabled"); + return Err(WebError::Authorization("user not found".into())); + } + } + Err(err) => { + info!("Failed to authenticate user {username}: {err}"); + log_failed_login_attempt(&appstate.failed_logins, &username); + return Err(WebError::Authorization(err.to_string())); + } + }, + Ok(None) => { + // create user from LDAP + debug!("User not found in DB, authenticating user {username} with LDAP"); + if let Ok(user) = + user_from_ldap(&appstate.pool, &username, &data.password).await + { + user + } else { + info!("Failed to authenticate user {username} with LDAP"); + log_failed_login_attempt(&appstate.failed_logins, &username); + return Err(WebError::Authorization("user not found".into())); + } + } + Err(err) => { + error!("DB error when authenticating user {username}: {err}"); + return Err(WebError::DbError(err.to_string())); + } } } Err(err) => { diff --git a/src/handlers/group.rs b/src/handlers/group.rs index 81e4a7454f..c4a099026e 100644 --- a/src/handlers/group.rs +++ b/src/handlers/group.rs @@ -63,7 +63,7 @@ pub(crate) async fn bulk_assign_to_groups( User, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_login \ FROM \"user\" WHERE id = ANY($1)", &data.users ) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 83b2f9a141..0612666d9a 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -37,7 +37,7 @@ pub mod worker; pub(crate) mod yubikey; pub(crate) static SESSION_COOKIE_NAME: &str = "defguard_session"; -static SIGN_IN_COOKIE_NAME: &str = "defguard_sign_in"; +pub(crate) static SIGN_IN_COOKIE_NAME: &str = "defguard_sign_in"; #[derive(Default, ToSchema)] pub struct ApiResponse { diff --git a/src/handlers/user.rs b/src/handlers/user.rs index bd5268978f..e0d55f92bf 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -35,7 +35,7 @@ use crate::{ /// - starts with non-special character /// - special characters: . - _ /// - no whitespaces -fn check_username(username: &str) -> Result<(), WebError> { +pub fn check_username(username: &str) -> Result<(), WebError> { // check length let length = username.len(); if !(3..64).contains(&length) { @@ -269,6 +269,17 @@ pub async fn add_user( status: StatusCode::BAD_REQUEST, }); } + // check if email doesn't already exist + if User::find_by_email(&appstate.pool, &user_data.email) + .await? + .is_some() + { + debug!("User with email {} already exists", user_data.email); + return Ok(ApiResponse { + json: json!({}), + status: StatusCode::BAD_REQUEST, + }); + } let password = match &user_data.password { Some(password) => { // check password strength diff --git a/src/lib.rs b/src/lib.rs index 37f6a175dd..78a38b1123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,10 @@ use axum::{ }; use assets::{index, svg, web_asset}; +use enterprise::handlers::{ + openid_login::{auth_callback, get_auth_info}, + openid_providers::{add_openid_provider, delete_openid_provider, get_current_openid_provider}, +}; use handlers::ssh_authorized_keys::{ add_authentication_key, delete_authentication_key, fetch_authentication_keys, }; @@ -115,6 +119,7 @@ pub mod assets; pub mod auth; pub mod config; pub mod db; +pub mod enterprise; mod error; pub mod grpc; pub mod handlers; @@ -397,7 +402,13 @@ pub fn build_webapp( .route("/webhook/:id", delete(delete_webhook)) .route("/webhook/:id", post(change_enabled)) // ldap - .route("/ldap/test", get(test_ldap_settings)), + .route("/ldap/test", get(test_ldap_settings)) + // OIDC login + .route("/openid/provider", get(get_current_openid_provider)) + .route("/openid/provider", post(add_openid_provider)) + .route("/openid/provider/:name", delete(delete_openid_provider)) + .route("/openid/callback", post(auth_callback)) + .route("/openid/auth_info", get(get_auth_info)), ); #[cfg(feature = "openid")] diff --git a/tests/enrollment.rs b/tests/enrollment.rs index ec34283d53..3e2fe00ef8 100644 --- a/tests/enrollment.rs +++ b/tests/enrollment.rs @@ -57,7 +57,7 @@ async fn test_initialize_enrollment() { username: "adumbledore2".into(), last_name: "Dumbledore".into(), first_name: "Albus".into(), - email: "a.dumbledore@hogwart.edu.uk".into(), + email: "a.dumbledore2@hogwart.edu.uk".into(), phone: Some("1234".into()), password: None, }; diff --git a/tests/openid_login.rs b/tests/openid_login.rs new file mode 100644 index 0000000000..93c6936a01 --- /dev/null +++ b/tests/openid_login.rs @@ -0,0 +1,69 @@ +use defguard::{ + config::DefGuardConfig, db::DbPool, enterprise::handlers::openid_providers::AddProviderData, + handlers::Auth, +}; +use reqwest::{StatusCode, Url}; +use serde::Deserialize; + +mod common; +use self::common::{client::TestClient, make_base_client, make_test_client}; + +async fn make_client() -> TestClient { + let (client, _) = make_test_client().await; + client +} + +async fn make_client_v2(pool: DbPool, config: DefGuardConfig) -> TestClient { + let (client, _) = make_base_client(pool, config).await; + client +} + +#[tokio::test] +async fn test_openid_providers() { + let client = make_client().await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let provider_data = AddProviderData::new( + "test", + "https://accounts.google.com", + "client_id", + "client_secret", + ); + + let response = client + .post("/api/v1/openid/provider") + .json(&provider_data) + .send() + .await; + + assert_eq!(response.status(), StatusCode::CREATED); + + let response = client.get("/api/v1/openid/auth_info").send().await; + + assert_eq!(response.status(), StatusCode::OK); + + #[derive(Deserialize)] + struct UrlResponse { + url: String, + } + + let provider: UrlResponse = response.json::().await; + + let url = Url::parse(&provider.url).unwrap(); + + let client_id = url + .query_pairs() + .find(|(key, _)| key == "client_id") + .unwrap(); + assert_eq!(client_id.1, "client_id"); + + let nonce = url.query_pairs().find(|(key, _)| key == "nonce"); + assert!(nonce.is_some()); + let state = url.query_pairs().find(|(key, _)| key == "state"); + assert!(state.is_some()); + let redirect_uri = url.query_pairs().find(|(key, _)| key == "redirect_uri"); + assert!(redirect_uri.is_some()); +} diff --git a/tests/user.rs b/tests/user.rs index 24845cd0ff..bae25d3dee 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -434,12 +434,12 @@ async fn test_check_username() { let invalid_usernames = ["ADumble dore", ".1user"]; let valid_usernames = ["user1", "use2r3", "not_wrong"]; - for username in invalid_usernames { + for (i, username) in invalid_usernames.into_iter().enumerate() { let new_user = AddUserData { username: username.into(), last_name: "Dumbledore".into(), first_name: "Albus".into(), - email: "a.dumbledore@hogwart.edu.uk".into(), + email: format!("a.dumbledore{i}@hogwart.edu.uk"), phone: Some("1234".into()), password: Some("Alohomora!12".into()), }; @@ -447,12 +447,12 @@ async fn test_check_username() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } - for username in valid_usernames { + for (i, username) in valid_usernames.into_iter().enumerate() { let new_user = AddUserData { username: username.into(), last_name: "Dumbledore".into(), first_name: "Albus".into(), - email: "a.dumbledore@hogwart.edu.uk".into(), + email: format!("a.dumbledore{i}@hogwart.edu.uk"), phone: Some("1234".into()), password: Some("Alohomora!12".into()), }; @@ -763,3 +763,36 @@ async fn test_disable() { .await; assert_eq!(response.status(), StatusCode::OK); } + +#[tokio::test] +async fn test_unique_email() { + let client = make_client().await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create user + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: Some("Password1234543$!".into()), + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // create user with same email + let new_user = AddUserData { + username: "adumbledore2".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: Some("Password1234543$!".into()), + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); +} diff --git a/web/src/components/App/App.tsx b/web/src/components/App/App.tsx index 6e94143410..56fa6e5eef 100644 --- a/web/src/components/App/App.tsx +++ b/web/src/components/App/App.tsx @@ -1,7 +1,7 @@ import 'react-loading-skeleton/dist/skeleton.css'; import './App.scss'; -import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'; +import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-dom'; import { AddDevicePage } from '../../pages/addDevice/AddDevicePage'; import { OpenidAllowPage } from '../../pages/allow/OpenidAllowPage'; diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index fac618863b..a5ed451099 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -889,6 +889,7 @@ const en: BaseTranslation = { smtp: 'SMTP', global: 'Global settings', ldap: 'LDAP', + openid: 'OpenID', }, messages: { editSuccess: 'Settings updated', @@ -920,6 +921,41 @@ const en: BaseTranslation = { }, }, }, + openIdSettings: { + general: { + title: 'External OpenID Settings', + helper: 'Here you can change general OpenID behavior in your Defguard instance.', + createAccount: { + label: 'Automatically create user account when logging in for the first time through external OpenID.', + helper: 'If this option is enabled, Defguard automatically creates new accounts for users who log in for the first time using an external OpenID provider. Otherwise, the user account must first be created by an administrator.', + }, + }, + form: { + title: 'External OpenID Client Settings', + helper: 'Here you can configure the OpenID client settings with values provided by your external OpenID provider.', + custom: "Custom", + documentation: 'Documentation', + delete: 'Delete provider', + labels: { + provider: { + label: 'Provider', + helper: 'Select your OpenID provider. You can use custom provider and fill in the base URL by yourself.', + }, + client_id: { + label: 'Client ID', + helper: 'Client ID provided by your OpenID provider.', + }, + client_secret: { + label: 'Client Secret', + helper: 'Client Secret provided by your OpenID provider.', + }, + base_url: { + label: 'Base URL', + helper: 'Base URL of your OpenID provider, e.g. https://accounts.google.com. Make sure to check our documentation for more information and examples.', + }, + }, + }, + }, modulesVisibility: { header: 'Modules Visibility', helper: `

@@ -1450,6 +1486,10 @@ const en: BaseTranslation = { }, loginPage: { pageTitle: 'Enter your credentials', + callback: { + return: 'Go back to login', + error: 'An error occurred during external OpenID login', + }, mfa: { title: 'Two-factor authentication', controls: { diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index edfee8010f..27fd686ad4 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2186,6 +2186,10 @@ type RootTranslation = { * L​D​A​P */ ldap: string + /** + * O​p​e​n​I​D + */ + openid: string } messages: { /** @@ -2271,6 +2275,92 @@ type RootTranslation = { } } } + openIdSettings: { + general: { + /** + * E​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​S​e​t​t​i​n​g​s + */ + title: string + /** + * H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​h​a​n​g​e​ ​g​e​n​e​r​a​l​ ​O​p​e​n​I​D​ ​b​e​h​a​v​i​o​r​ ​i​n​ ​y​o​u​r​ ​D​e​f​g​u​a​r​d​ ​i​n​s​t​a​n​c​e​. + */ + helper: string + createAccount: { + /** + * A​u​t​o​m​a​t​i​c​a​l​l​y​ ​c​r​e​a​t​e​ ​u​s​e​r​ ​a​c​c​o​u​n​t​ ​w​h​e​n​ ​l​o​g​g​i​n​g​ ​i​n​ ​f​o​r​ ​t​h​e​ ​f​i​r​s​t​ ​t​i​m​e​ ​t​h​r​o​u​g​h​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​. + */ + label: string + /** + * I​f​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​D​e​f​g​u​a​r​d​ ​a​u​t​o​m​a​t​i​c​a​l​l​y​ ​c​r​e​a​t​e​s​ ​n​e​w​ ​a​c​c​o​u​n​t​s​ ​f​o​r​ ​u​s​e​r​s​ ​w​h​o​ ​l​o​g​ ​i​n​ ​f​o​r​ ​t​h​e​ ​f​i​r​s​t​ ​t​i​m​e​ ​u​s​i​n​g​ ​a​n​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​.​ ​O​t​h​e​r​w​i​s​e​,​ ​t​h​e​ ​u​s​e​r​ ​a​c​c​o​u​n​t​ ​m​u​s​t​ ​f​i​r​s​t​ ​b​e​ ​c​r​e​a​t​e​d​ ​b​y​ ​a​n​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​. + */ + helper: string + } + } + form: { + /** + * E​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​C​l​i​e​n​t​ ​S​e​t​t​i​n​g​s + */ + title: string + /** + * H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​o​n​f​i​g​u​r​e​ ​t​h​e​ ​O​p​e​n​I​D​ ​c​l​i​e​n​t​ ​s​e​t​t​i​n​g​s​ ​w​i​t​h​ ​v​a​l​u​e​s​ ​p​r​o​v​i​d​e​d​ ​b​y​ ​y​o​u​r​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​. + */ + helper: string + /** + * C​u​s​t​o​m + */ + custom: string + /** + * D​o​c​u​m​e​n​t​a​t​i​o​n + */ + documentation: string + /** + * D​e​l​e​t​e​ ​p​r​o​v​i​d​e​r + */ + 'delete': string + labels: { + provider: { + /** + * P​r​o​v​i​d​e​r + */ + label: string + /** + * S​e​l​e​c​t​ ​y​o​u​r​ ​O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​.​ ​Y​o​u​ ​c​a​n​ ​u​s​e​ ​c​u​s​t​o​m​ ​p​r​o​v​i​d​e​r​ ​a​n​d​ ​f​i​l​l​ ​i​n​ ​t​h​e​ ​b​a​s​e​ ​U​R​L​ ​b​y​ ​y​o​u​r​s​e​l​f​. + */ + helper: string + } + client_id: { + /** + * C​l​i​e​n​t​ ​I​D + */ + label: string + /** + * C​l​i​e​n​t​ ​I​D​ ​p​r​o​v​i​d​e​d​ ​b​y​ ​y​o​u​r​ ​O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​. + */ + helper: string + } + client_secret: { + /** + * C​l​i​e​n​t​ ​S​e​c​r​e​t + */ + label: string + /** + * C​l​i​e​n​t​ ​S​e​c​r​e​t​ ​p​r​o​v​i​d​e​d​ ​b​y​ ​y​o​u​r​ ​O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​. + */ + helper: string + } + base_url: { + /** + * B​a​s​e​ ​U​R​L + */ + label: string + /** + * B​a​s​e​ ​U​R​L​ ​o​f​ ​y​o​u​r​ ​O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​,​ ​e​.​g​.​ ​h​t​t​p​s​:​/​/​a​c​c​o​u​n​t​s​.​g​o​o​g​l​e​.​c​o​m​.​ ​M​a​k​e​ ​s​u​r​e​ ​t​o​ ​c​h​e​c​k​ ​o​u​r​ ​d​o​c​u​m​e​n​t​a​t​i​o​n​ ​f​o​r​ ​m​o​r​e​ ​i​n​f​o​r​m​a​t​i​o​n​ ​a​n​d​ ​e​x​a​m​p​l​e​s​. + */ + helper: string + } + } + } + } modulesVisibility: { /** * M​o​d​u​l​e​s​ ​V​i​s​i​b​i​l​i​t​y @@ -3450,6 +3540,16 @@ type RootTranslation = { * E​n​t​e​r​ ​y​o​u​r​ ​c​r​e​d​e​n​t​i​a​l​s */ pageTitle: string + callback: { + /** + * G​o​ ​b​a​c​k​ ​t​o​ ​l​o​g​i​n + */ + 'return': string + /** + * A​n​ ​e​r​r​o​r​ ​o​c​c​u​r​r​e​d​ ​d​u​r​i​n​g​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​l​o​g​i​n + */ + error: string + } mfa: { /** * T​w​o​-​f​a​c​t​o​r​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n @@ -6074,6 +6174,10 @@ export type TranslationFunctions = { * LDAP */ ldap: () => LocalizedString + /** + * OpenID + */ + openid: () => LocalizedString } messages: { /** @@ -6159,6 +6263,92 @@ export type TranslationFunctions = { } } } + openIdSettings: { + general: { + /** + * External OpenID Settings + */ + title: () => LocalizedString + /** + * Here you can change general OpenID behavior in your Defguard instance. + */ + helper: () => LocalizedString + createAccount: { + /** + * Automatically create user account when logging in for the first time through external OpenID. + */ + label: () => LocalizedString + /** + * If this option is enabled, Defguard automatically creates new accounts for users who log in for the first time using an external OpenID provider. Otherwise, the user account must first be created by an administrator. + */ + helper: () => LocalizedString + } + } + form: { + /** + * External OpenID Client Settings + */ + title: () => LocalizedString + /** + * Here you can configure the OpenID client settings with values provided by your external OpenID provider. + */ + helper: () => LocalizedString + /** + * Custom + */ + custom: () => LocalizedString + /** + * Documentation + */ + documentation: () => LocalizedString + /** + * Delete provider + */ + 'delete': () => LocalizedString + labels: { + provider: { + /** + * Provider + */ + label: () => LocalizedString + /** + * Select your OpenID provider. You can use custom provider and fill in the base URL by yourself. + */ + helper: () => LocalizedString + } + client_id: { + /** + * Client ID + */ + label: () => LocalizedString + /** + * Client ID provided by your OpenID provider. + */ + helper: () => LocalizedString + } + client_secret: { + /** + * Client Secret + */ + label: () => LocalizedString + /** + * Client Secret provided by your OpenID provider. + */ + helper: () => LocalizedString + } + base_url: { + /** + * Base URL + */ + label: () => LocalizedString + /** + * Base URL of your OpenID provider, e.g. https://accounts.google.com. Make sure to check our documentation for more information and examples. + */ + helper: () => LocalizedString + } + } + } + } modulesVisibility: { /** * Modules Visibility @@ -7327,6 +7517,16 @@ export type TranslationFunctions = { * Enter your credentials */ pageTitle: () => LocalizedString + callback: { + /** + * Go back to login + */ + 'return': () => LocalizedString + /** + * An error occurred during external OpenID login + */ + error: () => LocalizedString + } mfa: { /** * Two-factor authentication diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 8006c3719c..e557a292c7 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -875,6 +875,7 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe smtp: 'SMTP', global: 'Globalne', ldap: 'LDAP', + openid: 'OpenID', }, messages: { editSuccess: 'Ustawienia zaktualizowane.', @@ -906,6 +907,41 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe submit: 'Test', }, }, + openIdSettings: { + general: { + title: 'Ustawienia zewnętrznego OpenID', + helper: 'Możesz tu zmienić ogólną mechanikę działania zewnętrznego OpenID w twojej instancji Defguarda.', + createAccount: { + label: 'Automatycznie twórz konta w momencie logowania przez zewnętrznego dostawcę OpenID', + helper: 'Jeśli ta opcja jest włączona, Defguard automatycznie tworzy nowe konta dla użytkowników, którzy logują się po raz pierwszy za pomocą zewnętrznego dostawcy OpenID. W innym przypadku konto użytkownika musi zostać najpierw utworzone przez administratora.', + }, + }, + form: { + title: 'Ustawienia klienta zewnętrznego OpenID', + helper: 'Tutaj możesz skonfigurować ustawienia klienta OpenID z wartościami dostarczonymi przez zewnętrznego dostawcę OpenID.', + custom: "Niestandardowy", + documentation: 'Dokumentacja', + delete: 'Usuń dostawcę', + labels: { + provider: { + label: 'Dostawca', + helper: 'Wybierz swojego dostawcę OpenID. Możesz użyć dostawcy niestandardowego i samodzielnie wypełnić pole URL bazowego.', + }, + client_id: { + label: 'ID klienta', + helper: 'ID klienta dostarczone przez dostawcę OpenID.', + }, + client_secret: { + label: 'Sekret klienta', + helper: 'Sekret klienta dostarczony przez dostawcę OpenID.', + }, + base_url: { + label: 'URL bazowy', + helper: 'Podstawowy adres URL twojego dostawcy OpenID, np. https://accounts.google.com. Sprawdź naszą dokumentację, aby uzyskać więcej informacji i zobaczyć przykłady.', + }, + }, + }, + }, modulesVisibility: { header: 'Widoczność modułów', helper: `

@@ -1436,6 +1472,10 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe }, loginPage: { pageTitle: 'Wprowadź swoje dane logowania', + callback: { + return: 'Powrót do logowania', + error: 'Wystąpił błąd podczas logowania przez zewnętrznego dostawcę OpenID', + }, mfa: { title: 'Autoryzacja dwuetapowa.', controls: { diff --git a/web/src/pages/auth/AuthPage.tsx b/web/src/pages/auth/AuthPage.tsx index ea2c14633c..ff734aed3a 100644 --- a/web/src/pages/auth/AuthPage.tsx +++ b/web/src/pages/auth/AuthPage.tsx @@ -16,6 +16,7 @@ import { RedirectPage } from '../redirect/RedirectPage'; import { Login } from './Login/Login'; import { MFARoute } from './MFARoute/MFARoute'; import { useMFAStore } from './shared/hooks/useMFAStore'; +import { OpenIDCallback } from './Callback/Callback'; export const AuthPage = () => { const { @@ -152,6 +153,7 @@ export const AuthPage = () => { } /> } /> } /> + } /> } /> diff --git a/web/src/pages/auth/Callback/Callback.tsx b/web/src/pages/auth/Callback/Callback.tsx new file mode 100644 index 0000000000..92256abc29 --- /dev/null +++ b/web/src/pages/auth/Callback/Callback.tsx @@ -0,0 +1,82 @@ +import './style.scss'; + +import { useMutation } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +import { useEffect, useState } from 'react'; +import useApi from '../../../shared/hooks/useApi'; +import { MutationKeys } from '../../../shared/mutations'; +import { CallbackData } from '../../../shared/types'; +import { useAuthStore } from '../../../shared/hooks/store/useAuthStore'; +import { LoaderSpinner } from '../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; +import { useToaster } from '../../../shared/hooks/useToaster'; +import { useI18nContext } from '../../../i18n/i18n-react'; +import { Button } from '../../../shared/defguard-ui/components/Layout/Button/Button'; +import { useNavigate } from 'react-router'; + +export const OpenIDCallback = () => { + const { + auth: { + openid: { callback }, + }, + } = useApi(); + const loginSubject = useAuthStore((state) => state.loginSubject); + const toaster = useToaster(); + const { LL } = useI18nContext(); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const callbackMutation = useMutation((data: CallbackData) => callback(data), { + mutationKey: [MutationKeys.OPENID_CALLBACK], + onSuccess: (data) => loginSubject.next(data), + onError: (error: AxiosError) => { + toaster.error(LL.messages.error()); + console.error(error); + }, + retry: false, + }); + + useEffect(() => { + if (window.location.hash && window.location.hash.length > 0) { + const hashFragment = window.location.hash.substring(1); + const params = new URLSearchParams(hashFragment); + + // check if error occured + const error = params.get('error'); + + if (error) { + setError(error); + toaster.error(LL.messages.error()); + return; + } + + const id_token = params.get('id_token'); + const state = params.get('state'); + + if (id_token && state) { + const data: CallbackData = { + id_token, + state, + }; + callbackMutation.mutate(data); + } + } + }, []); + + // TODO: Perhaphs make it a bit more user friendly + return error ? ( +

+

+ {LL.loginPage.callback.error()}: {error} +

+
+ ) : ( + + ); +}; diff --git a/web/src/pages/auth/Callback/style.scss b/web/src/pages/auth/Callback/style.scss new file mode 100644 index 0000000000..763c165186 --- /dev/null +++ b/web/src/pages/auth/Callback/style.scss @@ -0,0 +1,5 @@ +.error-info { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/web/src/pages/auth/Login/Login.tsx b/web/src/pages/auth/Login/Login.tsx index 4a9123c336..6d6a46a152 100644 --- a/web/src/pages/auth/Login/Login.tsx +++ b/web/src/pages/auth/Login/Login.tsx @@ -1,7 +1,7 @@ import './style.scss'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { useMemo } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; @@ -14,12 +14,15 @@ import { ButtonSize, ButtonStyleVariant, } from '../../../shared/defguard-ui/components/Layout/Button/types'; +import { LoaderSpinner } from '../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; import { useAuthStore } from '../../../shared/hooks/store/useAuthStore'; import useApi from '../../../shared/hooks/useApi'; import { MutationKeys } from '../../../shared/mutations'; import { patternSafeUsernameCharacters } from '../../../shared/patterns'; +import { QueryKeys } from '../../../shared/queries'; import { LoginData } from '../../../shared/types'; import { trimObjectStrings } from '../../../shared/utils/trimObjectStrings'; +import { OpenIdLoginButton } from './components/OidcButtons'; type Inputs = { username: string; @@ -28,6 +31,20 @@ type Inputs = { export const Login = () => { const { LL } = useI18nContext(); + const { + auth: { + login, + openid: { getOpenIdInfo: getOpenidInfo }, + }, + } = useApi(); + + const { data: openIdInfo, isLoading: openIdLoading } = useQuery({ + queryKey: [QueryKeys.FETCH_OPENID_INFO], + queryFn: getOpenidInfo, + refetchOnMount: true, + refetchOnWindowFocus: false, + retry: false, + }); const zodSchema = useMemo( () => @@ -46,10 +63,6 @@ export const Login = () => { [LL.form.error], ); - const { - auth: { login }, - } = useApi(); - const { handleSubmit, control, setError } = useForm({ resolver: zodResolver(zodSchema), mode: 'all', @@ -87,32 +100,39 @@ export const Login = () => { return (
-

{LL.loginPage.pageTitle()}

-
- - -
); }; diff --git a/web/src/pages/auth/Login/components/OidcButtons.tsx b/web/src/pages/auth/Login/components/OidcButtons.tsx new file mode 100644 index 0000000000..a073a238ec --- /dev/null +++ b/web/src/pages/auth/Login/components/OidcButtons.tsx @@ -0,0 +1,184 @@ +import './style.scss'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../shared/defguard-ui/components/Layout/Button/types'; + +export const OpenIdLoginButton = ({ url }: { url: string }) => { + const { hostname } = new URL(url); + + if (hostname === 'accounts.google.com') { + return ; + } else if (hostname === 'login.microsoftonline.com') { + return ; + } else { + return ; + } +}; + +const GoogleButton = ({ url }: { url: string }) => { + return ( + + ); +}; + +const CustomButton = ({ url }: { url: string }) => { + return ( + + ); +}; diff --git a/web/src/pages/auth/Login/components/style.scss b/web/src/pages/auth/Login/components/style.scss new file mode 100644 index 0000000000..dd798a7518 --- /dev/null +++ b/web/src/pages/auth/Login/components/style.scss @@ -0,0 +1,110 @@ +.gsi-material-button { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + -webkit-appearance: none; + background-color: WHITE; + background-image: none; + border: 1px solid #747775; + -webkit-border-radius: 4px; + border-radius: 4px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: #1f1f1f; + cursor: pointer; + font-family: 'Roboto', arial, sans-serif; + font-size: 14px; + height: 40px; + letter-spacing: 0.25px; + outline: none; + overflow: hidden; + padding: 0 12px; + position: relative; + text-align: center; + -webkit-transition: background-color .218s, border-color .218s, box-shadow .218s; + transition: background-color .218s, border-color .218s, box-shadow .218s; + vertical-align: middle; + white-space: nowrap; + width: auto; + max-width: 400px; + min-width: min-content; +} + +.gsi-material-button .gsi-material-button-icon { + height: 20px; + margin-right: 12px; + min-width: 20px; + width: 20px; +} + +.gsi-material-button .gsi-material-button-content-wrapper { + -webkit-align-items: center; + align-items: center; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + flex-wrap: nowrap; + height: 100%; + justify-content: space-between; + position: relative; + width: 100%; +} + +.gsi-material-button .gsi-material-button-contents { + -webkit-flex-grow: 1; + flex-grow: 1; + font-family: 'Roboto', arial, sans-serif; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +.gsi-material-button .gsi-material-button-state { + -webkit-transition: opacity .218s; + transition: opacity .218s; + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; +} + +.gsi-material-button:disabled { + cursor: default; + background-color: #ffffff61; + border-color: #1f1f1f1f; +} + +.gsi-material-button:disabled .gsi-material-button-contents { + opacity: 38%; +} + +.gsi-material-button:disabled .gsi-material-button-icon { + opacity: 38%; +} + +.gsi-material-button:not(:disabled):active .gsi-material-button-state, +.gsi-material-button:not(:disabled):focus .gsi-material-button-state { + background-color: #303030; + opacity: 12%; +} + +.gsi-material-button:not(:disabled):hover { + -webkit-box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15); + box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15); +} + +.gsi-material-button:not(:disabled):hover .gsi-material-button-state { + background-color: #303030; + opacity: 8%; +} + +.ms-button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; +} \ No newline at end of file diff --git a/web/src/pages/auth/Login/style.scss b/web/src/pages/auth/Login/style.scss index 46b746ad0e..a35ae9200c 100644 --- a/web/src/pages/auth/Login/style.scss +++ b/web/src/pages/auth/Login/style.scss @@ -36,6 +36,10 @@ margin-bottom: 5px; } + & > .btn { + margin-bottom: 10px; + } + & > * { width: 100%; } diff --git a/web/src/pages/settings/SettingsPage.tsx b/web/src/pages/settings/SettingsPage.tsx index bf6b9dac9f..8c2b8e1afb 100644 --- a/web/src/pages/settings/SettingsPage.tsx +++ b/web/src/pages/settings/SettingsPage.tsx @@ -14,6 +14,7 @@ import useApi from '../../shared/hooks/useApi'; import { QueryKeys } from '../../shared/queries'; import { GlobalSettings } from './components/GlobalSettings/GlobalSettings'; import { LdapSettings } from './components/LdapSettings/LdapSettings'; +import { OpenIdSettings } from './components/OpenIdSettings/OpenIdSettings'; import { SmtpSettings } from './components/SmtpSettings/SmtpSettings'; import { useSettingsPage } from './hooks/useSettingsPage'; @@ -21,6 +22,7 @@ const tabsContent: ReactNode[] = [ , , , + , ]; export const SettingsPage = () => { @@ -65,6 +67,12 @@ export const SettingsPage = () => { active: activeCard === 2, onClick: () => setActiveCard(2), }, + { + key: 3, + content: LL.settingsPage.tabs.openid(), + active: activeCard === 3, + onClick: () => setActiveCard(3), + }, ], [LL.settingsPage.tabs, activeCard], ); diff --git a/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx new file mode 100644 index 0000000000..b4a1b4d0d1 --- /dev/null +++ b/web/src/pages/settings/components/OpenIdSettings/OpenIdSettings.tsx @@ -0,0 +1,17 @@ +import './style.scss'; + +import { OpenIdGeneralSettings } from './components/OpenIdGeneralSettings'; +import { OpenIdSettingsForm } from './components/OpenIdSettingsForm'; + +export const OpenIdSettings = () => { + return ( + <> +
+ +
+
+ +
+ + ); +}; diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx new file mode 100644 index 0000000000..e768e9f80c --- /dev/null +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx @@ -0,0 +1,63 @@ +import './style.scss'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import parse from 'html-react-parser'; +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; +import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; +import useApi from '../../../../../shared/hooks/useApi'; +import { useToaster } from '../../../../../shared/hooks/useToaster'; +import { MutationKeys } from '../../../../../shared/mutations'; +import { QueryKeys } from '../../../../../shared/queries'; +import { useSettingsPage } from '../../../hooks/useSettingsPage'; + +export const OpenIdGeneralSettings = () => { + const { LL } = useI18nContext(); + const localLL = LL.settingsPage.openIdSettings; + const toaster = useToaster(); + const { + settings: { patchSettings }, + } = useApi(); + + const settings = useSettingsPage((state) => state.settings); + + const queryClient = useQueryClient(); + + const { mutate, isLoading } = useMutation([MutationKeys.EDIT_SETTINGS], patchSettings, { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.FETCH_ESSENTIAL_SETTINGS]); + queryClient.invalidateQueries([QueryKeys.FETCH_SETTINGS]); + toaster.success(LL.settingsPage.messages.editSuccess()); + }, + onError: () => { + toaster.error(LL.messages.error()); + }, + }); + + if (!settings) return null; + + return ( +
+
+

{localLL.general.title()}

+ {parse(localLL.general.helper())} +
+
+
+
+ + mutate({ openid_create_account: !settings.openid_create_account }) + } + /> + {localLL.general.createAccount.helper()} +
+
+
+
+ ); +}; diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx new file mode 100644 index 0000000000..336d9c0fad --- /dev/null +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx @@ -0,0 +1,250 @@ +import './style.scss'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import parse from 'html-react-parser'; +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import IconCheckmarkWhite from '../../../../../shared/components/svg/IconCheckmarkWhite'; +import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; +import { Button } from '../../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../../shared/defguard-ui/components/Layout/Button/types'; +import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; +import { Select } from '../../../../../shared/defguard-ui/components/Layout/Select/Select'; +import { + SelectOption, + SelectSelectedValue, + SelectSizeVariant, +} from '../../../../../shared/defguard-ui/components/Layout/Select/types'; +import useApi from '../../../../../shared/hooks/useApi'; +import { useToaster } from '../../../../../shared/hooks/useToaster'; +import { QueryKeys } from '../../../../../shared/queries'; +import { OpenIdProvider } from '../../../../../shared/types'; + +type FormFields = OpenIdProvider; + +export const OpenIdSettingsForm = () => { + const { LL } = useI18nContext(); + const localLL = LL.settingsPage.openIdSettings; + const [currentProvider, setCurrentProvider] = useState(null); + const queryClient = useQueryClient(); + + const { + settings: { fetchOpenIdProviders, addOpenIdProvider, deleteOpenIdProvider }, + } = useApi(); + + const { isLoading } = useQuery({ + queryFn: fetchOpenIdProviders, + queryKey: [QueryKeys.FETCH_OPENID_PROVIDERS], + refetchOnMount: true, + refetchOnWindowFocus: false, + onSuccess: (provider) => { + setCurrentProvider(provider); + }, + retry: false, + }); + + const toaster = useToaster(); + + const { mutate } = useMutation({ + mutationFn: addOpenIdProvider, + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.FETCH_OPENID_PROVIDERS]); + toaster.success(LL.settingsPage.messages.editSuccess()); + }, + onError: (error) => { + toaster.error(LL.messages.error()); + console.error(error); + }, + }); + + const { mutate: deleteProvider } = useMutation({ + mutationFn: deleteOpenIdProvider, + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.FETCH_OPENID_PROVIDERS]); + toaster.success(LL.settingsPage.messages.editSuccess()); + }, + onError: (error) => { + toaster.error(LL.messages.error()); + console.error(error); + }, + }); + + const schema = useMemo( + () => + z.object({ + name: z.string().min(1, LL.form.error.required()), + base_url: z + .string() + .url(LL.form.error.invalid()) + .min(1, LL.form.error.required()), + client_id: z.string().min(1, LL.form.error.required()), + client_secret: z.string().min(1, LL.form.error.required()), + }), + [LL.form.error], + ); + + const defaultValues = useMemo( + (): FormFields => ({ + id: currentProvider?.id ?? 0, + name: currentProvider?.name ?? '', + base_url: currentProvider?.base_url ?? '', + client_id: currentProvider?.client_id ?? '', + client_secret: currentProvider?.client_secret ?? '', + }), + [currentProvider], + ); + + const { handleSubmit, reset, control } = useForm({ + resolver: zodResolver(schema), + defaultValues, + mode: 'all', + }); + + // Make sure the form data is fresh + useEffect(() => { + reset(defaultValues); + }, [defaultValues, reset]); + + const handleValidSubmit: SubmitHandler = (data) => { + mutate(data); + }; + + const handleDeleteProvider = useCallback(() => { + if (currentProvider) { + deleteProvider(currentProvider.name); + setCurrentProvider(null); + } + }, [currentProvider, deleteProvider]); + + const options: SelectOption[] = useMemo( + () => [ + { + value: 'Google', + label: 'Google', + key: 1, + }, + { + value: 'Microsoft', + label: 'Microsoft', + key: 2, + }, + { + value: 'Custom', + label: localLL.form.custom(), + key: 3, + }, + ], + [], + ); + + const renderSelected = useCallback( + (selected: string): SelectSelectedValue => { + const option = options.find((o) => o.value === selected); + + if (!option) throw Error("Selected value doesn't exist"); + + return { + key: option.key, + displayValue: option.label, + }; + }, + [options], + ); + + const getProviderUrl = useCallback(({ name }: { name: string }): string | null => { + switch (name) { + case 'Google': + return 'https://accounts.google.com'; + case 'Microsoft': + return `https://login.microsoftonline.com//v2.0`; + default: + return null; + } + }, []); + + const handleChange = useCallback( + (val: string) => { + setCurrentProvider({ + id: currentProvider?.id ?? 0, + name: val, + base_url: getProviderUrl({ name: val }) ?? '', + client_id: currentProvider?.client_id ?? '', + client_secret: currentProvider?.client_secret ?? '', + }); + }, + [currentProvider, getProviderUrl], + ); + + return ( +
+
+

{localLL.form.title()}

+ {parse(localLL.form.helper())} +
+
+
+
+