mirror of
https://github.com/delight-im/PHP-Auth.git
synced 2025-08-06 08:07:27 +02:00
Compare commits
133 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cc8c212acb | ||
|
ef996fd2ae | ||
|
245e10b390 | ||
|
288bc1d967 | ||
|
ed7fb0b2eb | ||
|
68beb69984 | ||
|
10cf5a3855 | ||
|
cdcc82040f | ||
|
2d2ff46121 | ||
|
1fc2a87232 | ||
|
f4514372f6 | ||
|
5249a75fcd | ||
|
0a4100b8c7 | ||
|
db97bbaed7 | ||
|
f1d2476fb9 | ||
|
e6c827cd79 | ||
|
5cc4745fc7 | ||
|
8875697eec | ||
|
7a20e96600 | ||
|
15e9761b6b | ||
|
293d57f243 | ||
|
e087c9af2f | ||
|
1cac1a5188 | ||
|
3625622670 | ||
|
6b7ef7c93c | ||
|
d73a1bf919 | ||
|
ff4e52d111 | ||
|
05854dad61 | ||
|
233640502c | ||
|
ea6cbf6089 | ||
|
e771398527 | ||
|
3defd87461 | ||
|
c0a289c352 | ||
|
5609c80af0 | ||
|
efae015004 | ||
|
fcdb946042 | ||
|
61e4367c31 | ||
|
60175e1889 | ||
|
df31a85e4a | ||
|
663268c712 | ||
|
bf64593ebf | ||
|
960dc7ffdc | ||
|
ff3038386c | ||
|
0e82d095cf | ||
|
ceac62c3f3 | ||
|
e5ccc81988 | ||
|
2a37898560 | ||
|
a25b57cd7b | ||
|
e5bc48eaa6 | ||
|
d2602121ab | ||
|
eba7cd2657 | ||
|
2ffe09c52e | ||
|
75c372198d | ||
|
4dc67aaa30 | ||
|
87c4ad0b92 | ||
|
aebaea128b | ||
|
0f71c335e6 | ||
|
1f231d0a94 | ||
|
e447e972af | ||
|
9464d754bd | ||
|
804141f1d4 | ||
|
8b870567e7 | ||
|
b0965525de | ||
|
ea7b1208ad | ||
|
0ff92ce870 | ||
|
c249c3b060 | ||
|
e266178f95 | ||
|
c21f59d4d5 | ||
|
68f5b23fc5 | ||
|
4d92ca24c2 | ||
|
8f249d0080 | ||
|
96b72f0be9 | ||
|
bc15776348 | ||
|
9cab58ecb4 | ||
|
561d6cd450 | ||
|
e919eec2a9 | ||
|
8b0f5f3407 | ||
|
3c7e17fca8 | ||
|
fc468397e2 | ||
|
76c756118b | ||
|
dc04d52249 | ||
|
29fbd7b480 | ||
|
b79246ff40 | ||
|
8256fd11e8 | ||
|
e5310aa699 | ||
|
bcfbc1d2f8 | ||
|
3d19df85fc | ||
|
db7480be38 | ||
|
67b4cba4d9 | ||
|
d58519d831 | ||
|
759a523a92 | ||
|
88fcc61562 | ||
|
ada9553919 | ||
|
f9700fcae6 | ||
|
892512f6e1 | ||
|
79cc249318 | ||
|
0d240e4322 | ||
|
7bce546def | ||
|
df16db9b2b | ||
|
fa655c4908 | ||
|
fd67044826 | ||
|
6333d25cf2 | ||
|
f5060b5a1d | ||
|
729c76668f | ||
|
cc6430a83e | ||
|
6f933ac560 | ||
|
157a7095b0 | ||
|
0f976a260b | ||
|
dcd893a12c | ||
|
0086419175 | ||
|
d49b35690c | ||
|
171519fdf3 | ||
|
14ce7b1e8f | ||
|
49c70eff41 | ||
|
2f772b00c8 | ||
|
5214da1f59 | ||
|
d8847fb197 | ||
|
1757ad3fd1 | ||
|
54f6c5320a | ||
|
4b3f2ab91c | ||
|
df990b5b75 | ||
|
7b2ac9b107 | ||
|
ad90c7d04a | ||
|
c0baa517fa | ||
|
3120e3a6a5 | ||
|
4cd6360fc7 | ||
|
382832457d | ||
|
f70923679f | ||
|
521e73662d | ||
|
2b3bf611e2 | ||
|
352260c759 | ||
|
cbf2b52f29 | ||
|
c685f22937 |
@@ -7,62 +7,101 @@
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
CREATE TABLE `users` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`username` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`status` tinyint(2) unsigned NOT NULL DEFAULT '0',
|
||||
`verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`resettable` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
`roles_mask` int(10) unsigned NOT NULL DEFAULT '0',
|
||||
`registered` int(10) unsigned NOT NULL,
|
||||
`last_login` int(10) unsigned DEFAULT NULL,
|
||||
`force_logout` mediumint(7) unsigned NOT NULL DEFAULT '0',
|
||||
`status` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`verified` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`resettable` tinyint unsigned NOT NULL DEFAULT '1',
|
||||
`roles_mask` int unsigned NOT NULL DEFAULT '0',
|
||||
`registered` int unsigned NOT NULL,
|
||||
`last_login` int unsigned DEFAULT NULL,
|
||||
`force_logout` mediumint unsigned NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `email` (`email`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_confirmations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
CREATE TABLE `users_2fa` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int unsigned NOT NULL,
|
||||
`mechanism` tinyint unsigned NOT NULL,
|
||||
`seed` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`created_at` int unsigned NOT NULL,
|
||||
`expires_at` int unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `user_id_mechanism` (`user_id`,`mechanism`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `users_audit_log` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int unsigned DEFAULT NULL,
|
||||
`event_at` int unsigned NOT NULL,
|
||||
`event_type` varchar(128) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
|
||||
`admin_id` int unsigned DEFAULT NULL,
|
||||
`ip_address` varchar(49) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL,
|
||||
`user_agent` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`details_json` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `event_at` (`event_at`),
|
||||
KEY `user_id_event_at` (`user_id`,`event_at`),
|
||||
KEY `user_id_event_type_event_at` (`user_id`,`event_type`,`event_at`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `users_confirmations` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int unsigned NOT NULL,
|
||||
`email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`selector` varchar(16) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`expires` int(10) unsigned NOT NULL,
|
||||
`expires` int unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `selector` (`selector`),
|
||||
KEY `email_expires` (`email`,`expires`),
|
||||
KEY `user_id` (`user_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_remembered` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user` int(10) unsigned NOT NULL,
|
||||
CREATE TABLE `users_otps` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int unsigned NOT NULL,
|
||||
`mechanism` tinyint unsigned NOT NULL,
|
||||
`single_factor` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`selector` varchar(24) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`expires` int(10) unsigned NOT NULL,
|
||||
`expires_at` int unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id_mechanism` (`user_id`,`mechanism`),
|
||||
KEY `selector_user_id` (`selector`,`user_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `users_remembered` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user` int unsigned NOT NULL,
|
||||
`selector` varchar(24) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`expires` int unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `selector` (`selector`),
|
||||
KEY `user` (`user`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_resets` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user` int(10) unsigned NOT NULL,
|
||||
CREATE TABLE `users_resets` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user` int unsigned NOT NULL,
|
||||
`selector` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`expires` int(10) unsigned NOT NULL,
|
||||
`expires` int unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `selector` (`selector`),
|
||||
KEY `user_expires` (`user`,`expires`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_throttling` (
|
||||
CREATE TABLE `users_throttling` (
|
||||
`bucket` varchar(44) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
|
||||
`tokens` float unsigned NOT NULL,
|
||||
`replenished_at` int(10) unsigned NOT NULL,
|
||||
`expires_at` int(10) unsigned NOT NULL,
|
||||
`tokens` float NOT NULL,
|
||||
`replenished_at` int unsigned NOT NULL,
|
||||
`expires_at` int unsigned NOT NULL,
|
||||
PRIMARY KEY (`bucket`),
|
||||
KEY `expires_at` (`expires_at`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
@@ -4,55 +4,91 @@
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
|
||||
CREATE TABLE "users" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"email" VARCHAR(249) UNIQUE NOT NULL,
|
||||
"password" VARCHAR(255) NOT NULL,
|
||||
"password" VARCHAR(255) NOT NULL COLLATE "C",
|
||||
"username" VARCHAR(100) DEFAULT NULL,
|
||||
"status" SMALLINT NOT NULL DEFAULT '0' CHECK ("status" >= 0),
|
||||
"verified" SMALLINT NOT NULL DEFAULT '0' CHECK ("verified" >= 0),
|
||||
"resettable" SMALLINT NOT NULL DEFAULT '1' CHECK ("resettable" >= 0),
|
||||
"roles_mask" INTEGER NOT NULL DEFAULT '0' CHECK ("roles_mask" >= 0),
|
||||
"status" SMALLINT NOT NULL DEFAULT 0 CHECK ("status" >= 0),
|
||||
"verified" SMALLINT NOT NULL DEFAULT 0 CHECK ("verified" >= 0 AND "verified" <= 1),
|
||||
"resettable" SMALLINT NOT NULL DEFAULT 1 CHECK ("resettable" >= 0 AND "resettable" <= 1),
|
||||
"roles_mask" INTEGER NOT NULL DEFAULT 0 CHECK ("roles_mask" >= 0),
|
||||
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
|
||||
"last_login" INTEGER DEFAULT NULL CHECK ("last_login" >= 0),
|
||||
"force_logout" INTEGER NOT NULL DEFAULT '0' CHECK ("force_logout" >= 0)
|
||||
"force_logout" INTEGER NOT NULL DEFAULT 0 CHECK ("force_logout" >= 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "users_confirmations" (
|
||||
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
|
||||
CREATE TABLE "users_2fa" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
|
||||
"mechanism" SMALLINT NOT NULL CHECK ("mechanism" >= 0),
|
||||
"seed" VARCHAR(255) DEFAULT NULL COLLATE "C",
|
||||
"created_at" INTEGER NOT NULL CHECK ("created_at" >= 0),
|
||||
"expires_at" INTEGER DEFAULT NULL CHECK ("expires_at" >= 0)
|
||||
);
|
||||
CREATE UNIQUE INDEX "users_2fa_user_id_mechanism_uq" ON "users_2fa" ("user_id", "mechanism");
|
||||
|
||||
CREATE TABLE "users_audit_log" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_id" INTEGER DEFAULT NULL CHECK ("user_id" >= 0),
|
||||
"event_at" INTEGER NOT NULL CHECK ("event_at" >= 0),
|
||||
"event_type" VARCHAR(128) NOT NULL COLLATE "C",
|
||||
"admin_id" INTEGER DEFAULT NULL CHECK ("admin_id" >= 0),
|
||||
"ip_address" INET DEFAULT NULL,
|
||||
"user_agent" TEXT DEFAULT NULL,
|
||||
"details_json" JSONB DEFAULT NULL
|
||||
);
|
||||
CREATE INDEX "users_audit_log_event_at_ix" ON "users_audit_log" ("event_at");
|
||||
CREATE INDEX "users_audit_log_user_id_event_at_ix" ON "users_audit_log" ("user_id", "event_at");
|
||||
CREATE INDEX "users_audit_log_user_id_event_type_event_at_ix" ON "users_audit_log" ("user_id", "event_type", "event_at");
|
||||
|
||||
CREATE TABLE "users_confirmations" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
|
||||
"email" VARCHAR(249) NOT NULL,
|
||||
"selector" VARCHAR(16) UNIQUE NOT NULL,
|
||||
"token" VARCHAR(255) NOT NULL,
|
||||
"selector" VARCHAR(16) UNIQUE NOT NULL COLLATE "C",
|
||||
"token" VARCHAR(255) NOT NULL COLLATE "C",
|
||||
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "email_expires" ON "users_confirmations" ("email", "expires");
|
||||
CREATE INDEX IF NOT EXISTS "user_id" ON "users_confirmations" ("user_id");
|
||||
CREATE INDEX "users_confirmations_email_expires_ix" ON "users_confirmations" ("email", "expires");
|
||||
CREATE INDEX "users_confirmations_user_id_ix" ON "users_confirmations" ("user_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "users_remembered" (
|
||||
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
|
||||
CREATE TABLE "users_otps" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
|
||||
"mechanism" SMALLINT NOT NULL CHECK ("mechanism" >= 0),
|
||||
"single_factor" SMALLINT NOT NULL DEFAULT 0 CHECK ("single_factor" >= 0 AND "single_factor" <= 1),
|
||||
"selector" VARCHAR(24) NOT NULL COLLATE "C",
|
||||
"token" VARCHAR(255) NOT NULL COLLATE "C",
|
||||
"expires_at" INTEGER DEFAULT NULL CHECK ("expires_at" >= 0)
|
||||
);
|
||||
CREATE INDEX "users_otps_user_id_mechanism_ix" ON "users_otps" ("user_id", "mechanism");
|
||||
CREATE INDEX "users_otps_selector_user_id_ix" ON "users_otps" ("selector", "user_id");
|
||||
|
||||
CREATE TABLE "users_remembered" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user" INTEGER NOT NULL CHECK ("user" >= 0),
|
||||
"selector" VARCHAR(24) UNIQUE NOT NULL,
|
||||
"token" VARCHAR(255) NOT NULL,
|
||||
"selector" VARCHAR(24) UNIQUE NOT NULL COLLATE "C",
|
||||
"token" VARCHAR(255) NOT NULL COLLATE "C",
|
||||
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "user" ON "users_remembered" ("user");
|
||||
CREATE INDEX "users_remembered_user_ix" ON "users_remembered" ("user");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "users_resets" (
|
||||
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
|
||||
CREATE TABLE "users_resets" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user" INTEGER NOT NULL CHECK ("user" >= 0),
|
||||
"selector" VARCHAR(20) UNIQUE NOT NULL,
|
||||
"token" VARCHAR(255) NOT NULL,
|
||||
"selector" VARCHAR(20) UNIQUE NOT NULL COLLATE "C",
|
||||
"token" VARCHAR(255) NOT NULL COLLATE "C",
|
||||
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "user_expires" ON "users_resets" ("user", "expires");
|
||||
CREATE INDEX "users_resets_user_expires_ix" ON "users_resets" ("user", "expires");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "users_throttling" (
|
||||
"bucket" VARCHAR(44) PRIMARY KEY,
|
||||
CREATE TABLE "users_throttling" (
|
||||
"bucket" VARCHAR(44) PRIMARY KEY COLLATE "C",
|
||||
"tokens" REAL NOT NULL CHECK ("tokens" >= 0),
|
||||
"replenished_at" INTEGER NOT NULL CHECK ("replenished_at" >= 0),
|
||||
"expires_at" INTEGER NOT NULL CHECK ("expires_at" >= 0)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "expires_at" ON "users_throttling" ("expires_at");
|
||||
CREATE INDEX "users_throttling_expires_at_ix" ON "users_throttling" ("expires_at");
|
||||
|
||||
COMMIT;
|
||||
|
@@ -5,56 +5,92 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE "users" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
|
||||
"email" VARCHAR(249) NOT NULL,
|
||||
"password" VARCHAR(255) NOT NULL,
|
||||
"username" VARCHAR(100) DEFAULT NULL,
|
||||
"status" INTEGER NOT NULL CHECK ("status" >= 0) DEFAULT "0",
|
||||
"verified" INTEGER NOT NULL CHECK ("verified" >= 0) DEFAULT "0",
|
||||
"resettable" INTEGER NOT NULL CHECK ("resettable" >= 0) DEFAULT "1",
|
||||
"roles_mask" INTEGER NOT NULL CHECK ("roles_mask" >= 0) DEFAULT "0",
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"email" TEXT NOT NULL COLLATE NOCASE CHECK (LENGTH("email") <= 249),
|
||||
"password" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("password") <= 255),
|
||||
"username" TEXT DEFAULT NULL COLLATE NOCASE CHECK (LENGTH("username") <= 100),
|
||||
"status" INTEGER NOT NULL CHECK ("status" >= 0) DEFAULT 0,
|
||||
"verified" INTEGER NOT NULL CHECK ("verified" >= 0 AND "verified" <= 1) DEFAULT 0,
|
||||
"resettable" INTEGER NOT NULL CHECK ("resettable" >= 0 AND "resettable" <= 1) DEFAULT 1,
|
||||
"roles_mask" INTEGER NOT NULL CHECK ("roles_mask" >= 0) DEFAULT 0,
|
||||
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
|
||||
"last_login" INTEGER CHECK ("last_login" >= 0) DEFAULT NULL,
|
||||
"force_logout" INTEGER NOT NULL CHECK ("force_logout" >= 0) DEFAULT "0",
|
||||
CONSTRAINT "email" UNIQUE ("email")
|
||||
"force_logout" INTEGER NOT NULL CHECK ("force_logout" >= 0) DEFAULT 0,
|
||||
CONSTRAINT "users_email_uq" UNIQUE ("email")
|
||||
);
|
||||
|
||||
CREATE TABLE "users_2fa" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
|
||||
"mechanism" INTEGER NOT NULL CHECK ("mechanism" >= 0),
|
||||
"seed" TEXT DEFAULT NULL COLLATE BINARY CHECK (LENGTH("seed") <= 255),
|
||||
"created_at" INTEGER NOT NULL CHECK ("created_at" >= 0),
|
||||
"expires_at" INTEGER CHECK ("expires_at" >= 0) DEFAULT NULL,
|
||||
CONSTRAINT "users_2fa_user_id_mechanism_uq" UNIQUE ("user_id", "mechanism")
|
||||
);
|
||||
|
||||
CREATE TABLE "users_audit_log" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user_id" INTEGER DEFAULT NULL CHECK ("user_id" >= 0),
|
||||
"event_at" INTEGER NOT NULL CHECK ("event_at" >= 0),
|
||||
"event_type" TEXT NOT NULL COLLATE NOCASE CHECK (LENGTH("event_type") <= 128),
|
||||
"admin_id" INTEGER DEFAULT NULL CHECK ("admin_id" >= 0),
|
||||
"ip_address" TEXT DEFAULT NULL COLLATE NOCASE CHECK (LENGTH("ip_address") <= 49),
|
||||
"user_agent" TEXT DEFAULT NULL,
|
||||
"details_json" TEXT DEFAULT NULL
|
||||
);
|
||||
CREATE INDEX "users_audit_log_event_at_ix" ON "users_audit_log" ("event_at");
|
||||
CREATE INDEX "users_audit_log_user_id_event_at_ix" ON "users_audit_log" ("user_id", "event_at");
|
||||
CREATE INDEX "users_audit_log_user_id_event_type_event_at_ix" ON "users_audit_log" ("user_id", "event_type", "event_at");
|
||||
|
||||
CREATE TABLE "users_confirmations" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
|
||||
"email" VARCHAR(249) NOT NULL,
|
||||
"selector" VARCHAR(16) NOT NULL,
|
||||
"token" VARCHAR(255) NOT NULL,
|
||||
"email" TEXT NOT NULL COLLATE NOCASE CHECK (LENGTH("email") <= 249),
|
||||
"selector" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("selector") <= 16),
|
||||
"token" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("token") <= 255),
|
||||
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
|
||||
CONSTRAINT "selector" UNIQUE ("selector")
|
||||
CONSTRAINT "users_confirmations_selector_uq" UNIQUE ("selector")
|
||||
);
|
||||
CREATE INDEX "users_confirmations.email_expires" ON "users_confirmations" ("email", "expires");
|
||||
CREATE INDEX "users_confirmations.user_id" ON "users_confirmations" ("user_id");
|
||||
CREATE INDEX "users_confirmations_email_expires_ix" ON "users_confirmations" ("email", "expires");
|
||||
CREATE INDEX "users_confirmations_user_id_ix" ON "users_confirmations" ("user_id");
|
||||
|
||||
CREATE TABLE "users_otps" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
|
||||
"mechanism" INTEGER NOT NULL CHECK ("mechanism" >= 0),
|
||||
"single_factor" INTEGER NOT NULL CHECK ("single_factor" >= 0 AND "single_factor" <= 1) DEFAULT 0,
|
||||
"selector" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("selector") <= 24),
|
||||
"token" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("token") <= 255),
|
||||
"expires_at" INTEGER CHECK ("expires_at" >= 0) DEFAULT NULL
|
||||
);
|
||||
CREATE INDEX "users_otps_user_id_mechanism_ix" ON "users_otps" ("user_id", "mechanism");
|
||||
CREATE INDEX "users_otps_selector_user_id_ix" ON "users_otps" ("selector", "user_id");
|
||||
|
||||
CREATE TABLE "users_remembered" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user" INTEGER NOT NULL CHECK ("user" >= 0),
|
||||
"selector" VARCHAR(24) NOT NULL,
|
||||
"token" VARCHAR(255) NOT NULL,
|
||||
"selector" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("selector") <= 24),
|
||||
"token" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("token") <= 255),
|
||||
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
|
||||
CONSTRAINT "selector" UNIQUE ("selector")
|
||||
CONSTRAINT "users_remembered_selector_uq" UNIQUE ("selector")
|
||||
);
|
||||
CREATE INDEX "users_remembered.user" ON "users_remembered" ("user");
|
||||
CREATE INDEX "users_remembered_user_ix" ON "users_remembered" ("user");
|
||||
|
||||
CREATE TABLE "users_resets" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user" INTEGER NOT NULL CHECK ("user" >= 0),
|
||||
"selector" VARCHAR(20) NOT NULL,
|
||||
"token" VARCHAR(255) NOT NULL,
|
||||
"selector" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("selector") <= 20),
|
||||
"token" TEXT NOT NULL COLLATE BINARY CHECK (LENGTH("token") <= 255),
|
||||
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
|
||||
CONSTRAINT "selector" UNIQUE ("selector")
|
||||
CONSTRAINT "users_resets_selector_uq" UNIQUE ("selector")
|
||||
);
|
||||
CREATE INDEX "users_resets.user_expires" ON "users_resets" ("user", "expires");
|
||||
CREATE INDEX "users_resets_user_expires_ix" ON "users_resets" ("user", "expires");
|
||||
|
||||
CREATE TABLE "users_throttling" (
|
||||
"bucket" VARCHAR(44) PRIMARY KEY NOT NULL,
|
||||
"bucket" TEXT PRIMARY KEY NOT NULL COLLATE BINARY CHECK (LENGTH("bucket") <= 44),
|
||||
"tokens" REAL NOT NULL CHECK ("tokens" >= 0),
|
||||
"replenished_at" INTEGER NOT NULL CHECK ("replenished_at" >= 0),
|
||||
"expires_at" INTEGER NOT NULL CHECK ("expires_at" >= 0)
|
||||
);
|
||||
CREATE INDEX "users_throttling.expires_at" ON "users_throttling" ("expires_at");
|
||||
CREATE INDEX "users_throttling_expires_at_ix" ON "users_throttling" ("expires_at");
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# Migration
|
||||
|
||||
* [General](#general)
|
||||
* [From `v8.x.x` to `v9.x.x`](#from-v8xx-to-v9xx)
|
||||
* [From `v7.x.x` to `v8.x.x`](#from-v7xx-to-v8xx)
|
||||
* [From `v6.x.x` to `v7.x.x`](#from-v6xx-to-v7xx)
|
||||
* [From `v5.x.x` to `v6.x.x`](#from-v5xx-to-v6xx)
|
||||
@@ -13,6 +14,10 @@
|
||||
|
||||
Update your version of this library using Composer and its `composer update` or `composer require` commands [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md#how-do-i-update-libraries-or-modules-within-my-application).
|
||||
|
||||
## From `v8.x.x` to `v9.x.x`
|
||||
|
||||
* The database schema has changed. Create the three new tables `users_2fa`, `users_otps` and `users_audit_log` in your [MySQL](Database/MySQL.sql), [PostgreSQL](Database/PostgreSQL.sql) or [SQLite](Database/SQLite.sql) schema to update your database.
|
||||
|
||||
## From `v7.x.x` to `v8.x.x`
|
||||
|
||||
* The database schema has changed.
|
||||
|
@@ -6,7 +6,8 @@
|
||||
"ext-openssl": "*",
|
||||
"delight-im/base64": "^1.0",
|
||||
"delight-im/cookie": "^3.1",
|
||||
"delight-im/db": "^1.2"
|
||||
"delight-im/db": "^1.5",
|
||||
"delight-im/otp": "^1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"keywords": [ "auth", "authentication", "login", "security" ],
|
||||
|
167
composer.lock
generated
167
composer.lock
generated
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "54d541ae3c5ba25b0cc06688d2b65467",
|
||||
"content-hash": "2467e7d9c74e16240dd81cd23d33a880",
|
||||
"packages": [
|
||||
{
|
||||
"name": "delight-im/base64",
|
||||
@@ -49,16 +49,16 @@
|
||||
},
|
||||
{
|
||||
"name": "delight-im/cookie",
|
||||
"version": "v3.1.0",
|
||||
"version": "v3.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/delight-im/PHP-Cookie.git",
|
||||
"reference": "76ef2a21817cf7a034f85fc3f4d4bfc60f873947"
|
||||
"reference": "67065d34272377d63bab0bd58f984f9b228c803f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-Cookie/zipball/76ef2a21817cf7a034f85fc3f4d4bfc60f873947",
|
||||
"reference": "76ef2a21817cf7a034f85fc3f4d4bfc60f873947",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-Cookie/zipball/67065d34272377d63bab0bd58f984f9b228c803f",
|
||||
"reference": "67065d34272377d63bab0bd58f984f9b228c803f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -86,20 +86,24 @@
|
||||
"samesite",
|
||||
"xss"
|
||||
],
|
||||
"time": "2017-10-18T19:48:59+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/delight-im/PHP-Cookie/issues",
|
||||
"source": "https://github.com/delight-im/PHP-Cookie/tree/v3.4.0"
|
||||
},
|
||||
"time": "2020-04-16T11:01:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "delight-im/db",
|
||||
"version": "v1.2.0",
|
||||
"version": "v1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/delight-im/PHP-DB.git",
|
||||
"reference": "df99ef7c2e86c7ce206647ffe8ba74447c075b57"
|
||||
"reference": "c613571382fa76359abc6de71d19738d7b7f1d13"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-DB/zipball/df99ef7c2e86c7ce206647ffe8ba74447c075b57",
|
||||
"reference": "df99ef7c2e86c7ce206647ffe8ba74447c075b57",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-DB/zipball/c613571382fa76359abc6de71d19738d7b7f1d13",
|
||||
"reference": "c613571382fa76359abc6de71d19738d7b7f1d13",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -127,20 +131,24 @@
|
||||
"sql",
|
||||
"sqlite"
|
||||
],
|
||||
"time": "2017-03-18T20:51:59+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/delight-im/PHP-DB/issues",
|
||||
"source": "https://github.com/delight-im/PHP-DB/tree/v1.5.0"
|
||||
},
|
||||
"time": "2025-05-26T16:39:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "delight-im/http",
|
||||
"version": "v2.0.0",
|
||||
"version": "v2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/delight-im/PHP-HTTP.git",
|
||||
"reference": "0a19a72a7eac8b1301aa972fb20cff494ac43e09"
|
||||
"reference": "a5c2c4eae1dd3207f797984e8f64f2d71ed889dd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-HTTP/zipball/0a19a72a7eac8b1301aa972fb20cff494ac43e09",
|
||||
"reference": "0a19a72a7eac8b1301aa972fb20cff494ac43e09",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-HTTP/zipball/a5c2c4eae1dd3207f797984e8f64f2d71ed889dd",
|
||||
"reference": "a5c2c4eae1dd3207f797984e8f64f2d71ed889dd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -163,7 +171,129 @@
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"time": "2016-07-21T15:05:01+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/delight-im/PHP-HTTP/issues",
|
||||
"source": "https://github.com/delight-im/PHP-HTTP/tree/v2.1.0"
|
||||
},
|
||||
"time": "2021-10-12T18:52:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "delight-im/otp",
|
||||
"version": "v1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/delight-im/PHP-OTP.git",
|
||||
"reference": "d012342f5ee3430394b568b46a00c412c24f4f4a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/delight-im/PHP-OTP/zipball/d012342f5ee3430394b568b46a00c412c24f4f4a",
|
||||
"reference": "d012342f5ee3430394b568b46a00c412c24f4f4a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"paragonie/constant_time_encoding": "~1.1.0",
|
||||
"php": ">=5.6.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Delight\\Otp\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "One-time password (OTP) implementation for two-factor authentication with TOTP in accordance with RFC 6238 and RFC 4226",
|
||||
"homepage": "https://github.com/delight-im/PHP-OTP",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"google-authenticator",
|
||||
"hotp",
|
||||
"otp",
|
||||
"rfc-4226",
|
||||
"rfc-6238",
|
||||
"rfc4226",
|
||||
"rfc6238",
|
||||
"tfa",
|
||||
"totp",
|
||||
"two-factor",
|
||||
"two-factor-authentication"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/delight-im/PHP-OTP/issues",
|
||||
"source": "https://github.com/delight-im/PHP-OTP/tree/v1.0.1"
|
||||
},
|
||||
"time": "2023-07-03T08:13:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/constant_time_encoding",
|
||||
"version": "v1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/constant_time_encoding.git",
|
||||
"reference": "317718fb438e60151f72b20404f040cb5ae1d494"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/317718fb438e60151f72b20404f040cb5ae1d494",
|
||||
"reference": "317718fb438e60151f72b20404f040cb5ae1d494",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3|^7|^8"
|
||||
},
|
||||
"require-dev": {
|
||||
"paragonie/random_compat": "^1.4|^2",
|
||||
"phpunit/phpunit": ">= 4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ParagonIE\\ConstantTime\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com",
|
||||
"homepage": "https://paragonie.com",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Steve 'Sc00bz' Thomas",
|
||||
"email": "steve@tobtu.com",
|
||||
"homepage": "https://www.tobtu.com",
|
||||
"role": "Original Developer"
|
||||
}
|
||||
],
|
||||
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
|
||||
"keywords": [
|
||||
"base16",
|
||||
"base32",
|
||||
"base32_decode",
|
||||
"base32_encode",
|
||||
"base64",
|
||||
"base64_decode",
|
||||
"base64_encode",
|
||||
"bin2hex",
|
||||
"encoding",
|
||||
"hex",
|
||||
"hex2bin",
|
||||
"rfc4648"
|
||||
],
|
||||
"support": {
|
||||
"email": "info@paragonie.com",
|
||||
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
|
||||
"source": "https://github.com/paragonie/constant_time_encoding"
|
||||
},
|
||||
"time": "2022-01-17T05:23:46+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
@@ -176,5 +306,6 @@
|
||||
"php": ">=5.6.0",
|
||||
"ext-openssl": "*"
|
||||
},
|
||||
"platform-dev": []
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
@@ -12,17 +12,16 @@ use Delight\Db\PdoDatabase;
|
||||
use Delight\Db\PdoDsn;
|
||||
use Delight\Db\Throwable\Error;
|
||||
|
||||
require_once __DIR__ . '/Exceptions.php';
|
||||
|
||||
/** Component that can be used for administrative tasks by privileged and authorized users */
|
||||
final class Administration extends UserManager {
|
||||
|
||||
/**
|
||||
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
|
||||
* @param string|null $dbTablePrefix (optional) the prefix for the names of all database tables used by this component
|
||||
* @param string|null $dbSchema (optional) the schema name for all database tables used by this component
|
||||
*/
|
||||
public function __construct($databaseConnection, $dbTablePrefix = null) {
|
||||
parent::__construct($databaseConnection, $dbTablePrefix);
|
||||
public function __construct($databaseConnection, $dbTablePrefix = null, $dbSchema = null) {
|
||||
parent::__construct($databaseConnection, $dbTablePrefix, $dbSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,7 +276,7 @@ final class Administration extends UserManager {
|
||||
$userId = (int) $userId;
|
||||
|
||||
$rolesBitmask = $this->db->selectValue(
|
||||
'SELECT roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE id = ?',
|
||||
'SELECT roles_mask FROM ' . $this->makeTableName('users') . ' WHERE id = ?',
|
||||
[ $userId ]
|
||||
);
|
||||
|
||||
@@ -303,7 +302,7 @@ final class Administration extends UserManager {
|
||||
$userId = (int) $userId;
|
||||
|
||||
$rolesBitmask = $this->db->selectValue(
|
||||
'SELECT roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE id = ?',
|
||||
'SELECT roles_mask FROM ' . $this->makeTableName('users') . ' WHERE id = ?',
|
||||
[ $userId ]
|
||||
);
|
||||
|
||||
@@ -385,7 +384,7 @@ final class Administration extends UserManager {
|
||||
*/
|
||||
public function changePasswordForUserById($userId, $newPassword) {
|
||||
$userId = (int) $userId;
|
||||
$newPassword = self::validatePassword($newPassword);
|
||||
$newPassword = self::validatePassword($newPassword, true);
|
||||
|
||||
$this->updatePasswordInternal(
|
||||
$userId,
|
||||
@@ -430,7 +429,7 @@ final class Administration extends UserManager {
|
||||
private function deleteUsersByColumnValue($columnName, $columnValue) {
|
||||
try {
|
||||
return $this->db->delete(
|
||||
$this->dbTablePrefix . 'users',
|
||||
$this->makeTableNameComponents('users'),
|
||||
[
|
||||
$columnName => $columnValue
|
||||
]
|
||||
@@ -457,7 +456,7 @@ final class Administration extends UserManager {
|
||||
private function modifyRolesForUserByColumnValue($columnName, $columnValue, callable $modification) {
|
||||
try {
|
||||
$userData = $this->db->selectRow(
|
||||
'SELECT id, roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE ' . $columnName . ' = ?',
|
||||
'SELECT id, roles_mask FROM ' . $this->makeTableName('users') . ' WHERE ' . $columnName . ' = ?',
|
||||
[ $columnValue ]
|
||||
);
|
||||
}
|
||||
@@ -473,7 +472,7 @@ final class Administration extends UserManager {
|
||||
|
||||
try {
|
||||
$this->db->exec(
|
||||
'UPDATE ' . $this->dbTablePrefix . 'users SET roles_mask = ? WHERE id = ?',
|
||||
'UPDATE ' . $this->makeTableName('users') . ' SET roles_mask = ? WHERE id = ?',
|
||||
[
|
||||
$newRolesBitmask,
|
||||
(int) $userData['id']
|
||||
@@ -549,7 +548,7 @@ final class Administration extends UserManager {
|
||||
private function logInAsUserByColumnValue($columnName, $columnValue) {
|
||||
try {
|
||||
$users = $this->db->select(
|
||||
'SELECT verified, id, email, username, status, roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE ' . $columnName . ' = ? LIMIT 2 OFFSET 0',
|
||||
'SELECT verified, id, email, username, status, roles_mask FROM ' . $this->makeTableName('users') . ' WHERE ' . $columnName . ' = ? LIMIT 2 OFFSET 0',
|
||||
[ $columnValue ]
|
||||
);
|
||||
}
|
||||
|
11
src/AmbiguousUsernameException.php
Normal file
11
src/AmbiguousUsernameException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class AmbiguousUsernameException extends AuthException {}
|
12
src/AttemptCancelledException.php
Normal file
12
src/AttemptCancelledException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** @deprecated */
|
||||
class AttemptCancelledException extends AuthException {}
|
1356
src/Auth.php
1356
src/Auth.php
File diff suppressed because it is too large
Load Diff
12
src/AuthError.php
Normal file
12
src/AuthError.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** Base class for all (unchecked) errors */
|
||||
class AuthError extends \Exception {}
|
12
src/AuthException.php
Normal file
12
src/AuthException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** Base class for all (checked) exceptions */
|
||||
class AuthException extends \Exception {}
|
11
src/ConfirmationRequestNotFound.php
Normal file
11
src/ConfirmationRequestNotFound.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class ConfirmationRequestNotFound extends AuthException {}
|
11
src/DatabaseError.php
Normal file
11
src/DatabaseError.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class DatabaseError extends AuthError {}
|
11
src/DuplicateUsernameException.php
Normal file
11
src/DuplicateUsernameException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class DuplicateUsernameException extends AuthException {}
|
59
src/EmailAddress.php
Normal file
59
src/EmailAddress.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
final class EmailAddress {
|
||||
|
||||
/**
|
||||
* Returns a masked version of the given email address that can be used for privacy reasons and data safety reasons
|
||||
*
|
||||
* @param string $emailAddress
|
||||
* @return string
|
||||
*/
|
||||
public static function mask($emailAddress) {
|
||||
if (empty($emailAddress)) {
|
||||
return $emailAddress;
|
||||
}
|
||||
|
||||
// split the email address into local part and domain part and then split the domain part into individual segments
|
||||
$emailAddress = \trim((string) $emailAddress);
|
||||
$partsSeparatedByAtSymbol = \explode('@', $emailAddress);
|
||||
$domainPart = \array_pop($partsSeparatedByAtSymbol);
|
||||
$localPart = \implode('@', $partsSeparatedByAtSymbol);
|
||||
$localPart = \str_replace('"', '', $localPart);
|
||||
$localPart = \str_replace("'", "", $localPart);
|
||||
$parts = \explode('.', $domainPart);
|
||||
\array_unshift($parts, $localPart);
|
||||
|
||||
// mask the individual parts of the address one by one
|
||||
for ($i = 0; $i < \count($parts); $i++) {
|
||||
$parts[$i] = \trim($parts[$i]);
|
||||
|
||||
if (\mb_strlen($parts[$i]) >= 5) {
|
||||
$parts[$i] = \mb_substr($parts[$i], 0, 1) . '***' . \mb_substr($parts[$i], -1);
|
||||
}
|
||||
elseif (\mb_strlen($parts[$i]) === 4) {
|
||||
$parts[$i] = \mb_substr($parts[$i], 0, 1) . '**' . \mb_substr($parts[$i], -1);
|
||||
}
|
||||
elseif (\mb_strlen($parts[$i]) === 3 && $i <= 1) {
|
||||
$parts[$i] = \mb_substr($parts[$i], 0, 1) . '*' . \mb_substr($parts[$i], -1);
|
||||
}
|
||||
elseif (\mb_strlen($parts[$i]) === 2 && $i <= 1) {
|
||||
$parts[$i] = \mb_substr($parts[$i], 0, 1) . '*';
|
||||
}
|
||||
elseif (\mb_strlen($parts[$i]) === 1 && $i <= 1) {
|
||||
$parts[$i] = '*';
|
||||
}
|
||||
}
|
||||
|
||||
// join the individual parts back together
|
||||
return \array_shift($parts) . '@' . \implode('.', $parts);
|
||||
}
|
||||
|
||||
}
|
11
src/EmailNotVerifiedException.php
Normal file
11
src/EmailNotVerifiedException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class EmailNotVerifiedException extends AuthException {}
|
11
src/EmailOrUsernameRequiredError.php
Normal file
11
src/EmailOrUsernameRequiredError.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class EmailOrUsernameRequiredError extends AuthError {}
|
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class AuthException extends \Exception {}
|
||||
|
||||
class UnknownIdException extends AuthException {}
|
||||
|
||||
class InvalidEmailException extends AuthException {}
|
||||
|
||||
class UnknownUsernameException extends AuthException {}
|
||||
|
||||
class InvalidPasswordException extends AuthException {}
|
||||
|
||||
class EmailNotVerifiedException extends AuthException {}
|
||||
|
||||
class UserAlreadyExistsException extends AuthException {}
|
||||
|
||||
class NotLoggedInException extends AuthException {}
|
||||
|
||||
class InvalidSelectorTokenPairException extends AuthException {}
|
||||
|
||||
class TokenExpiredException extends AuthException {}
|
||||
|
||||
class TooManyRequestsException extends AuthException {}
|
||||
|
||||
class DuplicateUsernameException extends AuthException {}
|
||||
|
||||
class AmbiguousUsernameException extends AuthException {}
|
||||
|
||||
class AttemptCancelledException extends AuthException {}
|
||||
|
||||
class ResetDisabledException extends AuthException {}
|
||||
|
||||
class ConfirmationRequestNotFound extends AuthException {}
|
||||
|
||||
class AuthError extends \Exception {}
|
||||
|
||||
class DatabaseError extends AuthError {}
|
||||
|
||||
class DatabaseDriverError extends DatabaseError {}
|
||||
|
||||
class MissingCallbackError extends AuthError {}
|
||||
|
||||
class HeadersAlreadySentError extends AuthError {}
|
||||
|
||||
class EmailOrUsernameRequiredError extends AuthError {}
|
11
src/HeadersAlreadySentError.php
Normal file
11
src/HeadersAlreadySentError.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class HeadersAlreadySentError extends AuthError {}
|
11
src/InvalidEmailException.php
Normal file
11
src/InvalidEmailException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class InvalidEmailException extends AuthException {}
|
12
src/InvalidOneTimePasswordException.php
Normal file
12
src/InvalidOneTimePasswordException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** Exception that is thrown when a one-time password (OTP) provided by the user is not valid */
|
||||
class InvalidOneTimePasswordException extends AuthException {}
|
11
src/InvalidPasswordException.php
Normal file
11
src/InvalidPasswordException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class InvalidPasswordException extends AuthException {}
|
11
src/InvalidPhoneNumberException.php
Normal file
11
src/InvalidPhoneNumberException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class InvalidPhoneNumberException extends AuthException {}
|
11
src/InvalidSelectorTokenPairException.php
Normal file
11
src/InvalidSelectorTokenPairException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class InvalidSelectorTokenPairException extends AuthException {}
|
11
src/InvalidStateError.php
Normal file
11
src/InvalidStateError.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class InvalidStateError extends AuthError {}
|
111
src/IpAddress.php
Normal file
111
src/IpAddress.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
final class IpAddress {
|
||||
|
||||
const IPV4_LENGTH_BITS = 32;
|
||||
const IPV4_LENGTH_BYTES = 4;
|
||||
const IPV6_LENGTH_BITS = 128;
|
||||
const IPV6_LENGTH_BYTES = 16;
|
||||
|
||||
/**
|
||||
* Returns a masked version of the given IP address (IPv4 or IPv6) that can be used for privacy reasons and data safety reasons
|
||||
*
|
||||
* For IPv4-mapped IPv6 addresses, only the embedded IPv4 portion is masked (like an IPv4 address) and returned as IPv6 again
|
||||
*
|
||||
* @param string $ip the IP address (IPv4 or IPv6), e.g. '192.0.2.128' or '2001:db8:be4d:fbe0:c0af:b298:1242:33e4'
|
||||
* @param int|null $maskBitsIpv4 (optional) the number of bits to zero out from the right in IPv4 addresses
|
||||
* @param int|null $maskBitsIpv6 (optional) the number of bits to zero out from the right in IPv6 addresses
|
||||
* @param bool|null $includePrefixLength (optional) whether to include the prefix length (e.g. '/24' at the end) or not
|
||||
* @return string|null
|
||||
*/
|
||||
public static function mask($ip, $maskBitsIpv4 = null, $maskBitsIpv6 = null, $includePrefixLength = null) {
|
||||
$maskBitsIpv4 = isset($maskBitsIpv4) ? \max(0, \min(self::IPV4_LENGTH_BITS, (int) $maskBitsIpv4)) : 8;
|
||||
$maskBitsIpv6 = isset($maskBitsIpv6) ? \max(0, \min(self::IPV6_LENGTH_BITS, (int) $maskBitsIpv6)) : 80;
|
||||
$packedIp = @\inet_pton($ip);
|
||||
|
||||
if ($packedIp === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ipLengthInBytes = \strlen($packedIp);
|
||||
|
||||
// for IPv4 addresses
|
||||
if ($ipLengthInBytes === self::IPV4_LENGTH_BYTES) {
|
||||
if ($maskBitsIpv4 === 0) {
|
||||
return $ip;
|
||||
}
|
||||
elseif ($maskBitsIpv4 === self::IPV4_LENGTH_BITS) {
|
||||
return '0.0.0.0';
|
||||
}
|
||||
|
||||
// unpack to a 32-bit unsigned integer in network byte order
|
||||
$ipInt32 = unpack('N', $packedIp)[1];
|
||||
|
||||
// create a bitmask (like 0xFFFFFF00 to mask 8 bits or 0xFFFF0000 to mask 16 bits) using a bitwise right shift and then left shift
|
||||
$mask = (0xFFFFFFFF >> $maskBitsIpv4) << $maskBitsIpv4;
|
||||
|
||||
$packedIp = \pack('N', $ipInt32 & $mask);
|
||||
|
||||
$prefixLength = self::IPV4_LENGTH_BITS - $maskBitsIpv4;
|
||||
}
|
||||
// for IPv6 addresses
|
||||
elseif ($ipLengthInBytes === self::IPV6_LENGTH_BYTES) {
|
||||
// if the IP address is an IPv4-mapped IPv6 address
|
||||
if (\substr($packedIp, 0, 12) === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
||||
// the last 4 bytes are the IPv4 address, so mask bits as per IPv4 option
|
||||
$maskBitsIpv6 = $maskBitsIpv4;
|
||||
}
|
||||
|
||||
if ($maskBitsIpv6 === 0) {
|
||||
return $ip;
|
||||
}
|
||||
elseif ($maskBitsIpv6 === self::IPV6_LENGTH_BITS) {
|
||||
return '::';
|
||||
}
|
||||
|
||||
$maskBytesIpv6 = (int) \ceil($maskBitsIpv6 / 8);
|
||||
$maskBitsInFirstByteIpv6 = $maskBitsIpv6 % 8;
|
||||
|
||||
// work byte by byte for IPv6 due to lack of 128-bit integers
|
||||
|
||||
for ($i = 0; $i < $maskBytesIpv6; $i++) {
|
||||
// start from the rightmost byte
|
||||
$byteIndex = $ipLengthInBytes - $i - 1;
|
||||
|
||||
// if we are at the first byte and it should only be masked partially (i.e. masking 1-7 bits there)
|
||||
if ($i === ($maskBytesIpv6 - 1) && $maskBitsInFirstByteIpv6 !== 0) {
|
||||
$firstByteMask = (0xFF >> $maskBitsInFirstByteIpv6) << $maskBitsInFirstByteIpv6;
|
||||
$packedIp[$byteIndex] = \chr(\ord($packedIp[$byteIndex]) & $firstByteMask);
|
||||
}
|
||||
// when masking a full first byte or any byte after the first byte
|
||||
else {
|
||||
$packedIp[$byteIndex] = "\x00";
|
||||
}
|
||||
}
|
||||
|
||||
$prefixLength = self::IPV6_LENGTH_BITS - $maskBitsIpv6;
|
||||
}
|
||||
// for addresses with invalid lengths in bytes
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ip = \inet_ntop($packedIp);
|
||||
|
||||
if ($includePrefixLength) {
|
||||
return $ip . '/' . $prefixLength;
|
||||
}
|
||||
else {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
11
src/MissingCallbackError.php
Normal file
11
src/MissingCallbackError.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class MissingCallbackError extends AuthError {}
|
11
src/NotLoggedInException.php
Normal file
11
src/NotLoggedInException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class NotLoggedInException extends AuthException {}
|
96
src/PasswordHash.php
Normal file
96
src/PasswordHash.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
final class PasswordHash {
|
||||
|
||||
const HASH_ALGORITHM_IDENTIFIER = \PASSWORD_DEFAULT;
|
||||
const PEPPER_HMAC_SHA_512_PREHASH = 'bec95beffb3afd078df7cbfd4c4617ba214ac4641a157c1ca64106e7544c9fb4cef6e99b0a8f0b63e96328c09943ce96b9b8899ff54fa7ea57b622675442dbbf';
|
||||
const PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH = '$pa01';
|
||||
const PREFIX_LENGTH = 5;
|
||||
|
||||
/**
|
||||
* Creates a computationally expensive hash from a password
|
||||
*
|
||||
* @param string $passwordText
|
||||
* @return string|bool
|
||||
*/
|
||||
public static function from($passwordText) {
|
||||
// if the bcrypt algorithm will be used for computationally expensive hashing
|
||||
if (self::HASH_ALGORITHM_IDENTIFIER === \PASSWORD_BCRYPT || self::HASH_ALGORITHM_IDENTIFIER === null) {
|
||||
// pre-hash the password to support passwords with more than 72 bytes (i.e. more than 18-72 characters) and passwords containing null bytes
|
||||
$passwordText = self::prehash($passwordText);
|
||||
// use 72 out of the ~88 bytes from the prehash in bcrypt later and denote this in a custom hash prefix
|
||||
$outputPrefix = self::PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH;
|
||||
}
|
||||
else {
|
||||
$outputPrefix = '';
|
||||
}
|
||||
|
||||
return $outputPrefix . \password_hash($passwordText, self::HASH_ALGORITHM_IDENTIFIER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether a password matches a computationally expensive hash
|
||||
*
|
||||
* @param string $passwordText
|
||||
* @param string $expectedHash
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify($passwordText, $expectedHash) {
|
||||
// if the expected hash has a custom prefix that indicates a prehash has been used
|
||||
if (\substr($expectedHash, 0, self::PREFIX_LENGTH) === self::PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH) {
|
||||
// pre-hash the password here as well to allow for a possible match
|
||||
$passwordText = self::prehash($passwordText);
|
||||
// and drop the custom prefix from the expected hash
|
||||
$expectedHash = \substr($expectedHash, self::PREFIX_LENGTH);
|
||||
}
|
||||
|
||||
return \password_verify($passwordText, $expectedHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a computationally expensive hash needs to be updated to match a desired algorithm and set of options
|
||||
*
|
||||
* @param string $existingHash
|
||||
* @return bool
|
||||
*/
|
||||
public static function needsRehash($existingHash) {
|
||||
// if the existing hash has a custom prefix indicating that a prehash has been used
|
||||
if (\substr($existingHash, 0, self::PREFIX_LENGTH) === self::PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH) {
|
||||
// drop that custom prefix from the existing hash
|
||||
$existingHash = \substr($existingHash, self::PREFIX_LENGTH);
|
||||
}
|
||||
/*// if the existing hash has no custom prefix denoting a prehash
|
||||
else {
|
||||
// if the existing hash used the bcrypt algorithm
|
||||
if (\preg_match('/^\$2[abxy]?\$/', $existingHash) === 1) {
|
||||
// the prehash needs to be applied
|
||||
return true;
|
||||
}
|
||||
}*/
|
||||
|
||||
return \password_needs_rehash($existingHash, self::HASH_ALGORITHM_IDENTIFIER);
|
||||
}
|
||||
|
||||
private static function prehash($passwordText) {
|
||||
$pepperBinary = \hex2bin(self::PEPPER_HMAC_SHA_512_PREHASH);
|
||||
|
||||
// do not just use SHA-512 but apply an HMAC with a (semi-public) pepper to avoid breach correlation or "password shucking"
|
||||
$hmacBinary = \hash_hmac('sha512', $passwordText, $pepperBinary, true);
|
||||
|
||||
if (empty($hmacBinary)) {
|
||||
throw new AuthError('Could not generate HMAC');
|
||||
}
|
||||
|
||||
// encode the prehash using Base64 to avoid passing null bytes to the main hash function later (which could truncate the input)
|
||||
return \base64_encode($hmacBinary);
|
||||
}
|
||||
|
||||
}
|
67
src/PhoneNumber.php
Normal file
67
src/PhoneNumber.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
final class PhoneNumber {
|
||||
|
||||
/**
|
||||
* Returns a masked version of the given phone number that can be used for privacy reasons and data safety reasons
|
||||
*
|
||||
* @param string $phoneNumber
|
||||
* @return string
|
||||
*/
|
||||
public static function mask($phoneNumber) {
|
||||
if (empty($phoneNumber)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$phoneNumber = \preg_replace('/[^0-9A-Za-z+]+/', '', $phoneNumber);
|
||||
|
||||
if (empty($phoneNumber)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hasLeadingPlus = \mb_substr($phoneNumber, 0, 1) === '+';
|
||||
|
||||
if ($hasLeadingPlus) {
|
||||
$phoneNumber = \mb_substr($phoneNumber, 1);
|
||||
}
|
||||
|
||||
$significantCharsLength = \mb_strlen($phoneNumber);
|
||||
|
||||
if ($significantCharsLength >= 7) {
|
||||
$phoneNumber = \mb_substr($phoneNumber, 0, 2) . '***' . \mb_substr($phoneNumber, -2);
|
||||
}
|
||||
elseif ($significantCharsLength === 6) {
|
||||
$phoneNumber = \mb_substr($phoneNumber, 0, 2) . '**' . \mb_substr($phoneNumber, -2);
|
||||
}
|
||||
elseif ($significantCharsLength === 5) {
|
||||
$phoneNumber = \mb_substr($phoneNumber, 0, 1) . '**' . \mb_substr($phoneNumber, -2);
|
||||
}
|
||||
elseif ($significantCharsLength === 4) {
|
||||
$phoneNumber = \mb_substr($phoneNumber, 0, 1) . '**' . \mb_substr($phoneNumber, -1);
|
||||
}
|
||||
elseif ($significantCharsLength === 3) {
|
||||
$phoneNumber = '**' . \mb_substr($phoneNumber, -1);
|
||||
}
|
||||
elseif ($significantCharsLength === 2) {
|
||||
$phoneNumber = '**';
|
||||
}
|
||||
else {
|
||||
$phoneNumber = '*';
|
||||
}
|
||||
|
||||
if ($hasLeadingPlus) {
|
||||
$phoneNumber = '+' . $phoneNumber;
|
||||
}
|
||||
|
||||
return $phoneNumber;
|
||||
}
|
||||
|
||||
}
|
11
src/ResetDisabledException.php
Normal file
11
src/ResetDisabledException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class ResetDisabledException extends AuthException {}
|
74
src/SecondFactorRequiredException.php
Normal file
74
src/SecondFactorRequiredException.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** Exception that is thrown when a first factor has been successfully provided for authentification but a second one is still required */
|
||||
class SecondFactorRequiredException extends AuthException {
|
||||
|
||||
protected $totp;
|
||||
protected $smsRecipient;
|
||||
protected $smsRecipientMasked;
|
||||
protected $smsOtpValue;
|
||||
protected $emailRecipient;
|
||||
protected $emailRecipientMasked;
|
||||
protected $emailOtpValue;
|
||||
|
||||
public function hasTotpOption() {
|
||||
return !empty($this->totp);
|
||||
}
|
||||
|
||||
public function hasSmsOption() {
|
||||
return !empty($this->smsRecipient) && !empty($this->smsOtpValue);
|
||||
}
|
||||
|
||||
public function getSmsRecipient() {
|
||||
return $this->smsRecipient;
|
||||
}
|
||||
|
||||
public function getSmsRecipientMasked() {
|
||||
return $this->smsRecipientMasked;
|
||||
}
|
||||
|
||||
public function getSmsOtpValue() {
|
||||
return $this->smsOtpValue;
|
||||
}
|
||||
|
||||
public function hasEmailOption() {
|
||||
return !empty($this->emailRecipient) && !empty($this->emailOtpValue);
|
||||
}
|
||||
|
||||
public function getEmailRecipient() {
|
||||
return $this->emailRecipient;
|
||||
}
|
||||
|
||||
public function getEmailRecipientMasked() {
|
||||
return $this->emailRecipientMasked;
|
||||
}
|
||||
|
||||
public function getEmailOtpValue() {
|
||||
return $this->emailOtpValue;
|
||||
}
|
||||
|
||||
public function addTotpOption() {
|
||||
$this->totp = true;
|
||||
}
|
||||
|
||||
public function addSmsOption($otpValue, $recipient, $recipientMasked = null) {
|
||||
$this->smsOtpValue = !empty($otpValue) ? (string) $otpValue : null;
|
||||
$this->smsRecipient = !empty($recipient) ? (string) $recipient : null;
|
||||
$this->smsRecipientMasked = !empty($recipientMasked) ? (string) $recipientMasked : null;
|
||||
}
|
||||
|
||||
public function addEmailOption($otpValue, $recipient, $recipientMasked = null) {
|
||||
$this->emailOtpValue = !empty($otpValue) ? (string) $otpValue : null;
|
||||
$this->emailRecipient = !empty($recipient) ? (string) $recipient : null;
|
||||
$this->emailRecipientMasked = !empty($recipientMasked) ? (string) $recipientMasked : null;
|
||||
}
|
||||
|
||||
}
|
11
src/TokenExpiredException.php
Normal file
11
src/TokenExpiredException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class TokenExpiredException extends AuthException {}
|
46
src/TokenHash.php
Normal file
46
src/TokenHash.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
final class TokenHash {
|
||||
|
||||
const HASH_ALGORITHM_IDENTIFIER = \PASSWORD_DEFAULT;
|
||||
|
||||
/**
|
||||
* Creates a computationally expensive hash from a token
|
||||
*
|
||||
* @param string $tokenText
|
||||
* @return string|bool
|
||||
*/
|
||||
public static function from($tokenText) {
|
||||
return \password_hash($tokenText, self::HASH_ALGORITHM_IDENTIFIER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether a token matches a computationally expensive hash
|
||||
*
|
||||
* @param string $tokenText
|
||||
* @param string $expectedHash
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify($tokenText, $expectedHash) {
|
||||
return \password_verify($tokenText, $expectedHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a computationally expensive hash needs to be updated to match a desired algorithm and set of options
|
||||
*
|
||||
* @param string $existingHash
|
||||
* @return bool
|
||||
*/
|
||||
public static function needsRehash($existingHash) {
|
||||
return \password_needs_rehash($existingHash, self::HASH_ALGORITHM_IDENTIFIER);
|
||||
}
|
||||
|
||||
}
|
11
src/TooManyRequestsException.php
Normal file
11
src/TooManyRequestsException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class TooManyRequestsException extends AuthException {}
|
12
src/TwoFactorMechanismAlreadyEnabledException.php
Normal file
12
src/TwoFactorMechanismAlreadyEnabledException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** Exception that is thrown when a given mechanism for two-factor authentification has already been enabled */
|
||||
class TwoFactorMechanismAlreadyEnabledException extends AuthException {}
|
12
src/TwoFactorMechanismNotInitializedException.php
Normal file
12
src/TwoFactorMechanismNotInitializedException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
/** Exception that is thrown when a given mechanism for two-factor authentification has not been initialized yet or the prior initialization is not valid anymore */
|
||||
class TwoFactorMechanismNotInitializedException extends AuthException {}
|
11
src/UnknownIdException.php
Normal file
11
src/UnknownIdException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class UnknownIdException extends AuthException {}
|
11
src/UnknownUsernameException.php
Normal file
11
src/UnknownUsernameException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class UnknownUsernameException extends AuthException {}
|
11
src/UserAlreadyExistsException.php
Normal file
11
src/UserAlreadyExistsException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
|
||||
* Copyright (c) delight.im (https://www.delight.im/)
|
||||
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
*/
|
||||
|
||||
namespace Delight\Auth;
|
||||
|
||||
class UserAlreadyExistsException extends AuthException {}
|
@@ -15,8 +15,6 @@ use Delight\Db\PdoDsn;
|
||||
use Delight\Db\Throwable\Error;
|
||||
use Delight\Db\Throwable\IntegrityConstraintViolationException;
|
||||
|
||||
require_once __DIR__ . '/Exceptions.php';
|
||||
|
||||
/**
|
||||
* Abstract base class for components implementing user management
|
||||
*
|
||||
@@ -42,9 +40,17 @@ abstract class UserManager {
|
||||
const SESSION_FIELD_LAST_RESYNC = 'auth_last_resync';
|
||||
/** @var string session field for the counter that keeps track of forced logouts that need to be performed in the current session */
|
||||
const SESSION_FIELD_FORCE_LOGOUT = 'auth_force_logout';
|
||||
/** @var string session field for the UNIX timestamp in seconds until which the first factor of authentication is considered to be completed and valid */
|
||||
const SESSION_FIELD_AWAITING_2FA_UNTIL = 'auth_awaiting_2fa_until';
|
||||
/** @var string session field for the ID of the user for whom the first factor of authentication has already been completed */
|
||||
const SESSION_FIELD_AWAITING_2FA_USER_ID = 'auth_awaiting_2fa_user_id';
|
||||
/** @var string session field for the desired "remember me" duration that the user originally requested when attempting to sign in */
|
||||
const SESSION_FIELD_AWAITING_2FA_REMEMBER_DURATION = 'auth_awaiting_2fa_remember_duration';
|
||||
|
||||
/** @var PdoDatabase the database connection to operate on */
|
||||
protected $db;
|
||||
/** @var string|null the schema name for all database tables used by this component */
|
||||
protected $dbSchema;
|
||||
/** @var string the prefix for the names of all database tables used by this component */
|
||||
protected $dbTablePrefix;
|
||||
|
||||
@@ -70,8 +76,9 @@ abstract class UserManager {
|
||||
/**
|
||||
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
|
||||
* @param string|null $dbTablePrefix (optional) the prefix for the names of all database tables used by this component
|
||||
* @param string|null $dbSchema (optional) the schema name for all database tables used by this component
|
||||
*/
|
||||
protected function __construct($databaseConnection, $dbTablePrefix = null) {
|
||||
protected function __construct($databaseConnection, $dbTablePrefix = null, $dbSchema = null) {
|
||||
if ($databaseConnection instanceof PdoDatabase) {
|
||||
$this->db = $databaseConnection;
|
||||
}
|
||||
@@ -87,6 +94,7 @@ abstract class UserManager {
|
||||
throw new \InvalidArgumentException('The database connection must be an instance of either `PdoDatabase`, `PdoDsn` or `PDO`');
|
||||
}
|
||||
|
||||
$this->dbSchema = $dbSchema !== null ? (string) $dbSchema : null;
|
||||
$this->dbTablePrefix = (string) $dbTablePrefix;
|
||||
}
|
||||
|
||||
@@ -124,7 +132,7 @@ abstract class UserManager {
|
||||
\ignore_user_abort(true);
|
||||
|
||||
$email = self::validateEmailAddress($email);
|
||||
$password = self::validatePassword($password);
|
||||
$password = self::validatePassword($password, true);
|
||||
|
||||
$username = isset($username) ? \trim($username) : null;
|
||||
|
||||
@@ -140,7 +148,7 @@ abstract class UserManager {
|
||||
if ($username !== null) {
|
||||
// count the number of users who do already have that specified username
|
||||
$occurrencesOfUsername = $this->db->selectValue(
|
||||
'SELECT COUNT(*) FROM ' . $this->dbTablePrefix . 'users WHERE username = ?',
|
||||
'SELECT COUNT(*) FROM ' . $this->makeTableName('users') . ' WHERE username = ?',
|
||||
[ $username ]
|
||||
);
|
||||
|
||||
@@ -152,12 +160,12 @@ abstract class UserManager {
|
||||
}
|
||||
}
|
||||
|
||||
$password = \password_hash($password, \PASSWORD_DEFAULT);
|
||||
$password = PasswordHash::from($password);
|
||||
$verified = \is_callable($callback) ? 0 : 1;
|
||||
|
||||
try {
|
||||
$this->db->insert(
|
||||
$this->dbTablePrefix . 'users',
|
||||
$this->makeTableNameComponents('users'),
|
||||
[
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
@@ -193,11 +201,11 @@ abstract class UserManager {
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
protected function updatePasswordInternal($userId, $newPassword) {
|
||||
$newPassword = \password_hash($newPassword, \PASSWORD_DEFAULT);
|
||||
$newPassword = PasswordHash::from($newPassword);
|
||||
|
||||
try {
|
||||
$affected = $this->db->update(
|
||||
$this->dbTablePrefix . 'users',
|
||||
$this->makeTableNameComponents('users'),
|
||||
[ 'password' => $newPassword ],
|
||||
[ 'id' => $userId ]
|
||||
);
|
||||
@@ -239,6 +247,9 @@ abstract class UserManager {
|
||||
$_SESSION[self::SESSION_FIELD_FORCE_LOGOUT] = (int) $forceLogout;
|
||||
$_SESSION[self::SESSION_FIELD_REMEMBERED] = $remembered;
|
||||
$_SESSION[self::SESSION_FIELD_LAST_RESYNC] = \time();
|
||||
$_SESSION[self::SESSION_FIELD_AWAITING_2FA_UNTIL] = null;
|
||||
$_SESSION[self::SESSION_FIELD_AWAITING_2FA_USER_ID] = null;
|
||||
$_SESSION[self::SESSION_FIELD_AWAITING_2FA_REMEMBER_DURATION] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +269,7 @@ abstract class UserManager {
|
||||
$projection = \implode(', ', $requestedColumns);
|
||||
|
||||
$users = $this->db->select(
|
||||
'SELECT ' . $projection . ' FROM ' . $this->dbTablePrefix . 'users WHERE username = ? LIMIT 2 OFFSET 0',
|
||||
'SELECT ' . $projection . ' FROM ' . $this->makeTableName('users') . ' WHERE username = ? LIMIT 2 OFFSET 0',
|
||||
[ $username ]
|
||||
);
|
||||
}
|
||||
@@ -304,20 +315,28 @@ abstract class UserManager {
|
||||
* Validates a password
|
||||
*
|
||||
* @param string $password the password to validate
|
||||
* @param bool|null $isNewPassword (optional) whether the password is a new password that the user wants to use
|
||||
* @return string the sanitized password
|
||||
* @throws InvalidPasswordException if the password has been invalid
|
||||
*/
|
||||
protected static function validatePassword($password) {
|
||||
protected static function validatePassword($password, $isNewPassword = null) {
|
||||
if (empty($password)) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
|
||||
$password = \trim($password);
|
||||
$isNewPassword = ($isNewPassword !== null) ? (bool) $isNewPassword : false;
|
||||
|
||||
if (\strlen($password) < 1) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
|
||||
if ($isNewPassword) {
|
||||
if (\strlen($password) > 2048) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
@@ -340,12 +359,12 @@ abstract class UserManager {
|
||||
protected function createConfirmationRequest($userId, $email, callable $callback) {
|
||||
$selector = self::createRandomString(16);
|
||||
$token = self::createRandomString(16);
|
||||
$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
|
||||
$tokenHashed = TokenHash::from($token);
|
||||
$expires = \time() + 60 * 60 * 24;
|
||||
|
||||
try {
|
||||
$this->db->insert(
|
||||
$this->dbTablePrefix . 'users_confirmations',
|
||||
$this->makeTableNameComponents('users_confirmations'),
|
||||
[
|
||||
'user_id' => (int) $userId,
|
||||
'email' => $email,
|
||||
@@ -385,7 +404,7 @@ abstract class UserManager {
|
||||
|
||||
try {
|
||||
$this->db->delete(
|
||||
$this->dbTablePrefix . 'users_remembered',
|
||||
$this->makeTableNameComponents('users_remembered'),
|
||||
$whereMappings
|
||||
);
|
||||
}
|
||||
@@ -403,9 +422,50 @@ abstract class UserManager {
|
||||
protected function forceLogoutForUserById($userId) {
|
||||
$this->deleteRememberDirectiveForUserById($userId);
|
||||
$this->db->exec(
|
||||
'UPDATE ' . $this->dbTablePrefix . 'users SET force_logout = force_logout + 1 WHERE id = ?',
|
||||
'UPDATE ' . $this->makeTableName('users') . ' SET force_logout = force_logout + 1 WHERE id = ?',
|
||||
[ $userId ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a (qualified) full table name from an optional qualifier, an optional prefix, and the table name itself
|
||||
*
|
||||
* The optional qualifier may be a database name or a schema name, for example
|
||||
*
|
||||
* @param string $name the name of the table
|
||||
* @return string[] the components of the (qualified) full name of the table
|
||||
*/
|
||||
protected function makeTableNameComponents($name) {
|
||||
$components = [];
|
||||
|
||||
if (!empty($this->dbSchema)) {
|
||||
$components[] = $this->dbSchema;
|
||||
}
|
||||
|
||||
if (!empty($name)) {
|
||||
if (!empty($this->dbTablePrefix)) {
|
||||
$components[] = $this->dbTablePrefix . $name;
|
||||
}
|
||||
else {
|
||||
$components[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a (qualified) full table name from an optional qualifier, an optional prefix, and the table name itself
|
||||
*
|
||||
* The optional qualifier may be a database name or a schema name, for example
|
||||
*
|
||||
* @param string $name the name of the table
|
||||
* @return string the (qualified) full name of the table
|
||||
*/
|
||||
protected function makeTableName($name) {
|
||||
$components = $this->makeTableNameComponents($name);
|
||||
|
||||
return \implode('.', $components);
|
||||
}
|
||||
|
||||
}
|
||||
|
366
tests/index.php
366
tests/index.php
@@ -35,6 +35,14 @@ $db = new \PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8mb4', 'root', '
|
||||
|
||||
$auth = new \Delight\Auth\Auth($db);
|
||||
|
||||
echo '<!DOCTYPE html>';
|
||||
echo '<html>';
|
||||
echo '<head>';
|
||||
echo '<meta charset="utf-8">';
|
||||
echo '<title></title>';
|
||||
echo '</head>';
|
||||
echo '<body>';
|
||||
|
||||
$result = \processRequestData($auth);
|
||||
|
||||
\showGeneralForm();
|
||||
@@ -88,6 +96,37 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
catch (\Delight\Auth\EmailNotVerifiedException $e) {
|
||||
return 'email address not verified';
|
||||
}
|
||||
catch (\Delight\Auth\SecondFactorRequiredException $e) {
|
||||
$secondFactorOptions = [];
|
||||
|
||||
if ($e->hasTotpOption()) {
|
||||
$secondFactorOptions[] = 'TOTP';
|
||||
}
|
||||
if ($e->hasSmsOption()) {
|
||||
$secondFactorOptions[] = 'SMS (' . $e->getSmsRecipient() . ' / ' . $e->getSmsRecipientMasked() . ') with ' . $e->getSmsOtpValue();
|
||||
}
|
||||
if ($e->hasEmailOption()) {
|
||||
$secondFactorOptions[] = 'email (' . $e->getEmailRecipient() . ' / ' . $e->getEmailRecipientMasked() . ') with ' . $e->getEmailOtpValue();
|
||||
}
|
||||
|
||||
return 'second factor required: ' . \implode(' / ', $secondFactorOptions);
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'provideOneTimePasswordAsSecondFactor') {
|
||||
try {
|
||||
$auth->provideOneTimePasswordAsSecondFactor($_POST['otpValue']);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\InvalidOneTimePasswordException $e) {
|
||||
return 'invalid OTP';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'first factor not completed';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
@@ -167,6 +206,21 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
catch (\Delight\Auth\UserAlreadyExistsException $e) {
|
||||
return 'email address already exists';
|
||||
}
|
||||
catch (\Delight\Auth\SecondFactorRequiredException $e) {
|
||||
$secondFactorOptions = [];
|
||||
|
||||
if ($e->hasTotpOption()) {
|
||||
$secondFactorOptions[] = 'TOTP';
|
||||
}
|
||||
if ($e->hasSmsOption()) {
|
||||
$secondFactorOptions[] = 'SMS (' . $e->getSmsRecipient() . ' / ' . $e->getSmsRecipientMasked() . ') with ' . $e->getSmsOtpValue();
|
||||
}
|
||||
if ($e->hasEmailOption()) {
|
||||
$secondFactorOptions[] = 'email (' . $e->getEmailRecipient() . ' / ' . $e->getEmailRecipientMasked() . ') with ' . $e->getEmailOtpValue();
|
||||
}
|
||||
|
||||
return 'second factor required: ' . \implode(' / ', $secondFactorOptions);
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
@@ -254,9 +308,21 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
}
|
||||
else if ($_POST['action'] === 'resetPassword') {
|
||||
try {
|
||||
$auth->resetPassword($_POST['selector'], $_POST['token'], $_POST['password']);
|
||||
if (isset($_POST['login']) && $_POST['login'] > 0) {
|
||||
if ($_POST['login'] == 2) {
|
||||
// keep logged in for one year
|
||||
$rememberDuration = (int) (60 * 60 * 24 * 365.25);
|
||||
}
|
||||
else {
|
||||
// do not keep logged in after session ends
|
||||
$rememberDuration = null;
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
return $auth->resetPasswordAndSignIn($_POST['selector'], $_POST['token'], $_POST['password'], $rememberDuration);
|
||||
}
|
||||
else {
|
||||
return $auth->resetPassword($_POST['selector'], $_POST['token'], $_POST['password']);
|
||||
}
|
||||
}
|
||||
catch (\Delight\Auth\InvalidSelectorTokenPairException $e) {
|
||||
return 'invalid token';
|
||||
@@ -270,6 +336,21 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
catch (\Delight\Auth\InvalidPasswordException $e) {
|
||||
return 'invalid password';
|
||||
}
|
||||
catch (\Delight\Auth\SecondFactorRequiredException $e) {
|
||||
$secondFactorOptions = [];
|
||||
|
||||
if ($e->hasTotpOption()) {
|
||||
$secondFactorOptions[] = 'TOTP';
|
||||
}
|
||||
if ($e->hasSmsOption()) {
|
||||
$secondFactorOptions[] = 'SMS (' . $e->getSmsRecipient() . ' / ' . $e->getSmsRecipientMasked() . ') with ' . $e->getSmsOtpValue();
|
||||
}
|
||||
if ($e->hasEmailOption()) {
|
||||
$secondFactorOptions[] = 'email (' . $e->getEmailRecipient() . ' / ' . $e->getEmailRecipientMasked() . ') with ' . $e->getEmailOtpValue();
|
||||
}
|
||||
|
||||
return 'second factor required: ' . \implode(' / ', $secondFactorOptions);
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
@@ -293,6 +374,175 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'prepareTwoFactorViaTotp') {
|
||||
try {
|
||||
$keyUriAndSecret = $auth->prepareTwoFactorViaTotp($_POST['serviceName']);
|
||||
|
||||
return \implode(' | ', $keyUriAndSecret);
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismAlreadyEnabledException $e) {
|
||||
return 'already enabled';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'prepareTwoFactorViaSms') {
|
||||
try {
|
||||
$phoneNumberAndOtpValue = $auth->prepareTwoFactorViaSms($_POST['phoneNumber']);
|
||||
|
||||
return $phoneNumberAndOtpValue[1] . ' -> ' . $phoneNumberAndOtpValue[0];
|
||||
}
|
||||
catch (\Delight\Auth\InvalidPhoneNumberException $e) {
|
||||
return 'invalid phone number';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismAlreadyEnabledException $e) {
|
||||
return 'already enabled';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'prepareTwoFactorViaEmail') {
|
||||
try {
|
||||
$emailAddressAndOtpValue = $auth->prepareTwoFactorViaEmail();
|
||||
|
||||
return $emailAddressAndOtpValue[1] . ' -> ' . $emailAddressAndOtpValue[0];
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismAlreadyEnabledException $e) {
|
||||
return 'already enabled';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'enableTwoFactorViaTotp') {
|
||||
try {
|
||||
$recoveryCodes = $auth->enableTwoFactorViaTotp($_POST['otpValue']);
|
||||
|
||||
return \implode(' | ', $recoveryCodes);
|
||||
}
|
||||
catch (\Delight\Auth\InvalidOneTimePasswordException $e) {
|
||||
return 'invalid OTP';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismNotInitializedException $e) {
|
||||
return 'not initialized';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismAlreadyEnabledException $e) {
|
||||
return 'already enabled';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'enableTwoFactorViaSms') {
|
||||
try {
|
||||
$recoveryCodes = $auth->enableTwoFactorViaSms($_POST['otpValue']);
|
||||
|
||||
return \implode(' | ', $recoveryCodes);
|
||||
}
|
||||
catch (\Delight\Auth\InvalidOneTimePasswordException $e) {
|
||||
return 'invalid OTP';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismNotInitializedException $e) {
|
||||
return 'not initialized';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismAlreadyEnabledException $e) {
|
||||
return 'already enabled';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'enableTwoFactorViaEmail') {
|
||||
try {
|
||||
$recoveryCodes = $auth->enableTwoFactorViaEmail($_POST['otpValue']);
|
||||
|
||||
return \implode(' | ', $recoveryCodes);
|
||||
}
|
||||
catch (\Delight\Auth\InvalidOneTimePasswordException $e) {
|
||||
return 'invalid OTP';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismNotInitializedException $e) {
|
||||
return 'not initialized';
|
||||
}
|
||||
catch (\Delight\Auth\TwoFactorMechanismAlreadyEnabledException $e) {
|
||||
return 'already enabled';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'disableTwoFactorViaTotp') {
|
||||
try {
|
||||
$auth->disableTwoFactorViaTotp();
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'disableTwoFactorViaSms') {
|
||||
try {
|
||||
$auth->disableTwoFactorViaSms();
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'disableTwoFactorViaEmail') {
|
||||
try {
|
||||
$auth->disableTwoFactorViaEmail();
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'disableTwoFactor') {
|
||||
try {
|
||||
$auth->disableTwoFactor();
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'reconfirmPassword') {
|
||||
try {
|
||||
return $auth->reconfirmPassword($_POST['password']) ? 'correct' : 'wrong';
|
||||
@@ -367,6 +617,22 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'changeUsername') {
|
||||
try {
|
||||
$auth->changeUsername($_POST['newUsername'], $_POST['requireUnique']);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\DuplicateUsernameException $e) {
|
||||
return 'username already exists';
|
||||
}
|
||||
catch (\Delight\Auth\NotLoggedInException $e) {
|
||||
return 'not logged in';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
}
|
||||
}
|
||||
else if ($_POST['action'] === 'setPasswordResetEnabled') {
|
||||
try {
|
||||
$auth->setPasswordResetEnabled($_POST['enabled'] == 1);
|
||||
@@ -730,7 +996,22 @@ function showDebugData(\Delight\Auth\Auth $auth, $result) {
|
||||
\var_dump($auth->isRemembered());
|
||||
echo '$auth->getIpAddress()' . "\t\t\t";
|
||||
\var_dump($auth->getIpAddress());
|
||||
echo "\n";
|
||||
echo '$auth->hasTwoFactor()' . "\t\t\t";
|
||||
\var_dump($auth->hasTwoFactor());
|
||||
echo '$auth->hasTwoFactorViaTotp()' . "\t\t";
|
||||
\var_dump($auth->hasTwoFactorViaTotp());
|
||||
echo '$auth->hasTwoFactorViaSms()' . "\t\t";
|
||||
\var_dump($auth->hasTwoFactorViaSms());
|
||||
echo '$auth->hasTwoFactorViaEmail()' . "\t\t";
|
||||
\var_dump($auth->hasTwoFactorViaEmail());
|
||||
echo 'Waiting for 2FA' . "\t\t\t\t";
|
||||
if ($auth->isWaitingForSecondFactor()) {
|
||||
echo 'User #' . ((int) $_SESSION[\Delight\Auth\Auth::SESSION_FIELD_AWAITING_2FA_USER_ID]) . ' (' . ($_SESSION[\Delight\Auth\Auth::SESSION_FIELD_AWAITING_2FA_UNTIL] - \time()) . ' seconds)';
|
||||
}
|
||||
else {
|
||||
echo 'No';
|
||||
}
|
||||
echo "\n\n";
|
||||
|
||||
echo 'Session name' . "\t\t\t\t";
|
||||
\var_dump(\session_name());
|
||||
@@ -810,6 +1091,16 @@ function showAuthenticatedUserForm(\Delight\Auth\Auth $auth) {
|
||||
echo '<button type="submit">Change email address</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="changeUsername" />';
|
||||
echo '<input type="text" name="newUsername" placeholder="New username" /> ';
|
||||
echo '<select name="requireUnique" size="1">';
|
||||
echo '<option value="0">Any</option>';
|
||||
echo '<option value="1">Unique</option>';
|
||||
echo '</select> ';
|
||||
echo '<button type="submit">Change username</button>';
|
||||
echo '</form>';
|
||||
|
||||
\showConfirmEmailForm();
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
@@ -821,6 +1112,61 @@ function showAuthenticatedUserForm(\Delight\Auth\Auth $auth) {
|
||||
echo '<button type="submit">Control password resets</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="prepareTwoFactorViaTotp" />';
|
||||
echo '<input type="text" name="serviceName" placeholder="Service name" value="' . \htmlspecialchars(!empty($_SERVER['SERVER_NAME']) ? (string) $_SERVER['SERVER_NAME'] : (!empty($_SERVER['SERVER_ADDR']) ? (string) $_SERVER['SERVER_ADDR'] : '')) . '" /> ';
|
||||
echo '<button type="submit">Prepare 2FA via TOTP</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="prepareTwoFactorViaSms" />';
|
||||
echo '<input type="text" name="phoneNumber" placeholder="Phone number" /> ';
|
||||
echo '<button type="submit">Prepare 2FA via SMS</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="prepareTwoFactorViaEmail" />';
|
||||
echo '<button type="submit">Prepare 2FA via email</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="enableTwoFactorViaTotp" />';
|
||||
echo '<input type="text" name="otpValue" placeholder="OTP value" /> ';
|
||||
echo '<button type="submit">Enable 2FA via TOTP</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="enableTwoFactorViaSms" />';
|
||||
echo '<input type="text" name="otpValue" placeholder="OTP value" /> ';
|
||||
echo '<button type="submit">Enable 2FA via SMS</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="enableTwoFactorViaEmail" />';
|
||||
echo '<input type="text" name="otpValue" placeholder="OTP value" /> ';
|
||||
echo '<button type="submit">Enable 2FA via email</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="disableTwoFactorViaTotp" />';
|
||||
echo '<button type="submit">Disable 2FA via TOTP</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="disableTwoFactorViaSms" />';
|
||||
echo '<button type="submit">Disable 2FA via SMS</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="disableTwoFactorViaEmail" />';
|
||||
echo '<button type="submit">Disable 2FA via email</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="disableTwoFactor" />';
|
||||
echo '<button type="submit">Disable 2FA</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="logOut" />';
|
||||
echo '<button type="submit">Log out</button>';
|
||||
@@ -864,6 +1210,12 @@ function showGuestUserForm() {
|
||||
echo '<button type="submit">Log in with username</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="provideOneTimePasswordAsSecondFactor" />';
|
||||
echo '<input type="text" name="otpValue" placeholder="OTP value" /> ';
|
||||
echo '<button type="submit">Provide OTP</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="register" />';
|
||||
echo '<input type="text" name="email" placeholder="Email address" /> ';
|
||||
@@ -893,6 +1245,11 @@ function showGuestUserForm() {
|
||||
echo '<input type="text" name="selector" placeholder="Selector" /> ';
|
||||
echo '<input type="text" name="token" placeholder="Token" /> ';
|
||||
echo '<input type="text" name="password" placeholder="New password" /> ';
|
||||
echo '<select name="login" size="1">';
|
||||
echo '<option value="0">Sign in automatically? — No</option>';
|
||||
echo '<option value="1">Sign in automatically? — Yes</option>';
|
||||
echo '<option value="2">Sign in automatically? — Yes (and remember)</option>';
|
||||
echo '</select> ';
|
||||
echo '<button type="submit">Reset password</button>';
|
||||
echo '</form>';
|
||||
|
||||
@@ -1067,3 +1424,6 @@ function createRolesOptions() {
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
echo '</body>';
|
||||
echo '</html>';
|
||||
|
Reference in New Issue
Block a user