1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-08-06 16:16:29 +02:00

227 Commits

Author SHA1 Message Date
Marco
e7e174b05d Only configure and start session if not already started 2018-03-12 22:29:56 +01:00
Marco
8f35cc9965 Optimize spacing in PostgreSQL schema 2018-03-12 18:44:32 +01:00
Marco
142ccc362f Shorten line of text in README for better overview 2018-03-12 02:18:44 +01:00
Marco
bce31f9cfc Link to MariaDB schema separately from MySQL in README 2018-03-12 02:15:35 +01:00
Marco
3ddc7af1b4 Document support for PostgreSQL 2018-03-12 02:11:54 +01:00
Marco
62d9e44aa4 Add check constraints for unsigned integers in PostgreSQL schema 2018-03-12 01:51:33 +01:00
Marco
1121685cef Improve database schema for PostgreSQL 2018-03-12 01:51:15 +01:00
Tiberiu Chibici
2f9bab4779 Add database schema for PostgreSQL 2018-03-12 00:32:53 +01:00
Marco
89e99d727d Document resynchronization of session data with authoritative database 2018-03-10 20:54:24 +01:00
Marco
21341d3c18 Regularly resynchronize session data with authoritative source in DB 2018-03-10 20:53:13 +01:00
Marco
a1ae66374b Improve documentation on password reset by dividing it into steps 2018-03-10 17:47:03 +01:00
Marco
477164e8ec Rename identifiers in comments to prevent highlighting in IDE 2018-03-10 17:46:05 +01:00
Marco
9478a43e9b Re-implement method 'canResetPassword' using 'canResetPasswordOrThrow' 2018-03-10 04:13:14 +01:00
Marco
1ba8e1ff21 Document method 'canResetPasswordOrThrow' from class 'Auth' 2018-03-10 04:10:22 +01:00
Marco
1657102f75 Add tests for method 'canResetPasswordOrThrow' from class 'Auth' 2018-03-10 04:06:45 +01:00
Marco
d246248ab5 Implement method 'canResetPasswordOrThrow' in class 'Auth' 2018-03-10 03:54:42 +01:00
Marco
94531f24d3 Improve language 2018-03-10 03:50:12 +01:00
Marco
2f29830ed9 Improve documentation to use more suitable data source for token 2018-03-10 03:47:55 +01:00
Marco
42a8c1616c Document method 'getRolesForUserById' from class 'Administration' 2018-03-10 03:10:17 +01:00
Marco
a2be4c61ee Add tests for method 'getRolesForUserById' from class 'Administration' 2018-03-10 03:05:41 +01:00
Marco
d9f9198b45 Implement method 'getRolesForUserById' in class 'Administration' 2018-03-10 03:03:57 +01:00
Marco
13b58abebc Document method 'getRoles' from class 'Auth' 2018-03-10 03:01:23 +01:00
Marco
b0bf7647ce Add tests for method 'getRoles' from class 'Auth' 2018-03-10 02:56:32 +01:00
Marco
012577227a Implement method 'getRoles' in class 'Auth' 2018-03-10 02:54:57 +01:00
Marco
d834623954 Document methods 'getMap', 'getNames' and 'getValues' of class 'Role' 2018-03-10 02:51:27 +01:00
Marco
d3594898cc Make use of new method 'getMap' from class 'Role' in 'tests' 2018-03-10 02:03:25 +01:00
Marco
7d44158c32 Implement methods 'getMap', 'getNames' and 'getValues' in class 'Role' 2018-03-10 01:58:54 +01:00
Marco
04edd9f88f Simplify migration guide using that method names are case-insensitive 2018-03-09 15:22:55 +01:00
Marco
cd2ac47912 Simplify general notes for any update or upgrade in migration guide 2018-01-25 00:01:50 +01:00
Marco
7bcf201972 Improve documentation on default value for IP address in README 2017-11-08 21:34:50 +01:00
Marco
09247e7203 Provide possibility to disable throttling during development 2017-11-08 21:34:05 +01:00
Marco
ab1c54fae2 Optimize order of throttling in 'changeEmail' method from class 'Auth' 2017-11-08 20:40:37 +01:00
Marco
23acb66cc7 Reduce permitted frequency of requests to change one's email address 2017-11-08 20:38:16 +01:00
Marco
a7a9d45302 Drop constant 'CONFIRMATION_REQUESTS_TTL_IN_SECONDS' in 'UserManager' 2017-11-08 20:30:09 +01:00
Marco
ba4dc29ca5 Optimize order of throttling in 'resendConfirmationForColumnValue' 2017-11-08 20:23:34 +01:00
Marco
0a97f67515 Enforce limits for resending confirmations solely via throttling 2017-11-08 20:21:35 +01:00
Marco
7a94c6acef Improve documentation in 'confirmEmail' method from 'Auth' class 2017-11-08 19:23:22 +01:00
Marco
dbbbf1b193 Remove superfluous comment in 'UserManager' 2017-11-08 19:18:14 +01:00
Marco
9637dfa60d Improve language 2017-11-05 02:37:48 +01:00
Marco
aec738a9db Document methods for impersonating users in class 'Administration' 2017-11-03 15:48:21 +01:00
Marco
382ee5bf93 Add tests for methods to impersonate users in class 'Administration' 2017-11-03 15:44:39 +01:00
Marco
47d1e303aa Implement methods for impersonating users in class 'Administration' 2017-11-03 15:21:45 +01:00
Marco
67443c122a Move core logic of 'onLoginSuccessful' from 'Auth' to 'UserManager' 2017-11-03 08:50:59 +01:00
Marco
24056e89a4 Move constants holding names of session fields to 'UserManager' 2017-11-03 08:49:10 +01:00
Marco
c06bc7da1a Improve documentation for method 'onLoginSuccessful' in class 'Auth' 2017-11-03 08:38:17 +01:00
Marco
aedd2125fc Document constants holding names of session fields 2017-11-03 08:36:03 +01:00
Marco
425cf9b6f6 Write to session fields directly instead of using accessor methods 2017-11-03 08:33:41 +01:00
Marco
739fa7d574 Fix internal links in migration guide that should point to README 2017-10-21 23:15:15 +02:00
Marco
302feb5da2 Document 'secure' cookie attribute and how to change it in README 2017-10-21 22:32:01 +02:00
Marco
2ded232d8e Document 'httponly' cookie attribute and how to change it in README 2017-10-21 22:30:41 +02:00
Marco
70a905afd7 Document 'path' cookie attribute and how to change it in README 2017-10-21 22:29:19 +02:00
Marco
84f3ad10a9 Document 'domain' cookie attribute and how to change it in README 2017-10-21 22:26:25 +02:00
Marco
81091df66b Drop constructor arguments 'useHttps' and 'allowCookiesScriptAccess' 2017-10-20 23:07:36 +02:00
Marco
8926e7e708 Improve general upgrade guide in migration notes 2017-10-20 22:46:01 +02:00
Marco
eec450677f Do not duplicate and overwrite parts of cookie configuration anymore
Previously, PHP's configuration directives 'session.cookie_httponly'
and 'session.cookie_secure' were always overwritten with duplicated
and separately tracked variants of each directive
2017-10-20 22:30:16 +02:00
Marco
f1360dceba Improve code style 2017-10-20 08:53:02 +02:00
Marco
2cf7b27ba3 Support empty path scope for cookies to restrict to current directory 2017-10-20 08:47:56 +02:00
Marco
ecd8015acf Explain changes to domain scope of cookies in migration guide 2017-10-20 08:01:00 +02:00
Marco
1eedfd0e02 Simplify code based on assumptions about new 'Delight\Cookie' behavior 2017-10-20 01:12:04 +02:00
Marco
757579523c Use constants from 'Delight\Cookie\Cookie' class for cookie prefixes 2017-10-19 22:33:18 +02:00
Marco
d695328a5a Update dependencies 2017-10-19 22:29:50 +02:00
Marco
71506eaa05 Rename two methods for logout to highlight the better default version 2017-10-19 20:25:11 +02:00
Marco
ce8dbbc436 Delete 'remember me' cookies from previous major versions as well 2017-10-19 20:19:19 +02:00
Marco
d181219e40 Add documentation about cookies and their usage to README 2017-10-19 20:11:28 +02:00
Marco
891cef2511 Do not make repeated attempts to use invalid 'remember me' cookies 2017-10-19 03:00:28 +02:00
Marco
f70613b2b8 Ignore defined but empty selectors and tokens from 'remember me' 2017-10-19 02:55:49 +02:00
Marco
59816d1a40 Re-use 'remember me' cookie from previous major versions if available 2017-10-19 02:50:24 +02:00
Marco
1284f64f04 Fix documentation for method 'setRememberCookie' in class 'Auth' 2017-10-19 02:27:42 +02:00
Marco
8165e8917b Change name of 'remember me' cookie to be dependent on session name 2017-10-19 01:44:19 +02:00
Marco
a4b68167a1 Prepare migration guide for next major release 2017-10-19 00:47:47 +02:00
Marco
fc2fb4bb44 Move 'Refresh' button from bottom to top in 'tests' 2017-10-19 00:36:08 +02:00
Marco
b2a3fde696 Add tests for method 'createRememberCookieName' from class 'Auth' 2017-10-18 23:08:01 +02:00
Marco
36880b87c9 Implement method 'createRememberCookieName' in class 'Auth' 2017-10-18 23:03:41 +02:00
Marco
4a66965994 Add tests for method 'createCookieName' from class 'Auth' 2017-10-18 23:01:15 +02:00
Marco
e7b590dc80 Implement method 'createCookieName' in class 'Auth' 2017-10-18 22:52:00 +02:00
Marco
33d2384c93 Add list of available cookie prefixes as constant in class 'Auth' 2017-10-18 22:48:14 +02:00
Marco
1169856217 Improve code style 2017-10-18 22:47:24 +02:00
Marco
fa75811679 Display current session name in 'tests' 2017-10-18 22:30:06 +02:00
Marco
fa8fa4887e Improve documentation in class 'Auth' 2017-10-18 21:59:25 +02:00
Marco
8fecb86f15 Improve code style 2017-10-12 02:42:40 +02:00
Pavel Levin
04c466b309 Drop superfluous check using 'isset' 2017-10-12 02:32:13 +02:00
Marco
61041cc6fd Invalidate outstanding password reset tokens on email address change 2017-09-27 21:58:28 +02:00
Marco
2ca835ac75 Improve language 2017-09-26 22:54:42 +02:00
Marco
1e23e6de13 Improve code style 2017-09-26 22:44:37 +02:00
Marco
50220d463b Simplify documentation for 'changePasswordWithoutOldPassword' 2017-09-26 22:35:16 +02:00
Marco
f0bdd7b63e Improve language 2017-09-26 22:28:00 +02:00
Marco
0473d59c39 Show that users are to reconfirm their password for reset control 2017-09-26 22:24:46 +02:00
Marco
f8f44a0286 Show that users are to reconfirm their password for email changes 2017-09-26 22:23:28 +02:00
Marco
ea91d8c92e Explain that users should be notified about email address changes 2017-09-26 22:20:07 +02:00
Marco
7983bebd83 Explain that users should be informed via email about password changes 2017-09-26 22:17:21 +02:00
Marco
ddc5b50459 Improve list of methods that support 'remember me' feature in README 2017-09-23 00:17:31 +02:00
Marco
0b67f3d1e2 Document new method 'logOutButKeepSession' from class 'Auth' 2017-09-18 16:10:47 +02:00
Marco
16bcfa85ef Add tests for new method 'logOutButKeepSession' in class 'Auth' 2017-09-18 16:10:19 +02:00
Marco
404739634d Compose 'logout' using new 'logOutButKeepSession' and 'destroySession' 2017-09-18 16:08:32 +02:00
Marco
82a24fbbca Implement method 'destroySession' in class 'Auth' 2017-09-18 16:07:05 +02:00
Marco
1a195adf39 Implement method 'logOutButKeepSession' in class 'Auth' 2017-09-18 16:01:47 +02:00
Marco
5e4d4fd072 Improve language 2017-09-18 15:53:59 +02:00
Marco
6162092618 Catch undefined IP address in rare use cases such as CLI usage 2017-09-18 14:54:39 +02:00
Marco
f142dd91dc Emphasize primary line of project description in README 2017-08-25 07:57:47 +02:00
Marco
05567acc7c Remove exception from tests that cannot be thrown with specified call 2017-08-19 00:47:42 +02:00
Marco
3d8c583823 Remove exception from PHPDoc that cannot reasonably appear in practice 2017-08-19 00:46:38 +02:00
Marco
546a57cbf9 Document 'throttle' method for throttling or rate limiting in README 2017-08-19 00:45:27 +02:00
Marco
52ba03248d Make 'throttle' method for throttling or rate limiting a public method 2017-08-19 00:42:53 +02:00
Marco
c5ed53898e Explain changes to interface of internal throttling in migration guide 2017-08-19 00:40:42 +02:00
Marco
a66312bbcf Re-implement internal throttling or rate limiting from scratch 2017-08-19 00:22:21 +02:00
Marco
c1bb10f58d Fix language in migration guide 2017-08-07 23:30:35 +02:00
Marco
4fd37f079b Describe required changes to SQLite schema in migration guide 2017-08-07 23:20:34 +02:00
Marco
8ff3776e75 Completely rewrite SQLite schema for table 'users_throttling' 2017-08-07 23:19:51 +02:00
Marco
b24979ae26 Describe required changes to MySQL schema in migration guide 2017-08-07 23:18:53 +02:00
Marco
30b2f30aec Completely rewrite MySQL schema for table 'users_throttling' 2017-08-07 23:17:12 +02:00
Marco
b3d37ada86 Document methods for re-sending confirmation requests in class 'Auth' 2017-08-07 21:27:20 +02:00
Marco
27adc9fa91 Add tests for re-sending confirmation requests with class 'Auth' 2017-08-07 21:09:31 +02:00
Marco
c9a4e28c7b Implement methods for re-sending confirmation requests in class 'Auth' 2017-08-07 21:08:06 +02:00
Marco
f83ac969d4 Add class 'ConfirmationRequestNotFound' 2017-08-07 19:36:13 +02:00
Marco
0bbf9d32b1 Describe required changes to SQLite schema in migration guide 2017-08-07 19:30:44 +02:00
Marco
381e05f102 Update SQLite schema to index on 'user_id' in 'users_confirmations' 2017-08-07 19:28:49 +02:00
Marco
2839743c46 Describe required changes to MySQL schema in migration guide 2017-08-07 19:26:25 +02:00
Marco
d86d7ffd25 Update MySQL schema to index on 'user_id' in 'users_confirmations' 2017-08-07 19:23:58 +02:00
Marco
e3873f2d15 Use alternative 'LIMIT' syntax with wider compatibility in SQL query 2017-08-07 18:52:36 +02:00
Marco
b7a47fc707 Extract TTL in seconds of (email) confirmation requests into constant 2017-08-07 18:51:21 +02:00
Marco
91f50a80bb Document method 'changePasswordWithoutOldPassword' from class 'Auth' 2017-08-04 00:45:41 +02:00
Marco
7272fbb9a8 Add tests for method 'changePasswordWithoutOldPassword' from 'Auth' 2017-08-04 00:43:17 +02:00
Marco
62c5fab1ad Re-implement 'changePassword' method using two existing methods
Make use of 'reconfirmPassword' and 'changePasswordWithoutOldPassword'
2017-08-04 00:35:50 +02:00
Marco
1800525b51 Implement new method 'changePasswordWithoutOldPassword' in 'Auth' 2017-08-04 00:31:35 +02:00
Marco
e4f8673eab Remove documentation on half-baked support for multi-factor auth 2017-08-03 22:02:09 +02:00
Marco
59cd626bd0 Document method 'changeEmail' from class 'Auth' 2017-07-30 21:09:57 +02:00
Marco
3809b9d5d5 Add tests for method 'changeEmail' from class 'Auth' 2017-07-30 21:01:41 +02:00
Marco
3329c6a985 Let signed-in users perform email confirmation as well in 'tests' 2017-07-30 20:58:40 +02:00
Marco
7b98993bf8 Extract form for email verification into separate method in 'tests' 2017-07-30 20:57:13 +02:00
Marco
d5ae78a418 Hint at related methods for email confirmation where required 2017-07-30 20:53:18 +02:00
Marco
e925a73ef8 Implement method 'changeEmail' in class 'Auth' 2017-07-30 20:51:58 +02:00
Marco
39f9b00b45 Reflect changed email address in same session immediately 2017-07-30 20:24:19 +02:00
Marco
ea67c66bd1 Explain new exception of email confirmation methods in migration guide 2017-07-30 20:17:41 +02:00
Marco
7b4c4bf0e1 Document new exception for 'confirmEmail' and 'confirmEmailAndSignIn' 2017-07-30 20:15:58 +02:00
Marco
f13302b014 Update tests for 'confirmEmail' and its wrapper to catch new exception 2017-07-30 20:13:28 +02:00
Marco
af5ce5a0b4 Allow 'confirmEmail' to be used additionally to change email addresses 2017-07-30 20:04:08 +02:00
Marco
15f73567b6 Update accounts by ID instead of email after confirming email address 2017-07-30 19:59:09 +02:00
Marco
90c621aeb0 Store affected user ID when creating new email confirmation requests 2017-07-30 19:46:45 +02:00
Marco
28979925d7 Let 'Auth' access 'createConfirmationRequest' from 'UserManager' 2017-07-30 19:41:27 +02:00
Marco
b2e6f68a22 Describe required changes to SQLite schema in migration guide 2017-07-30 19:38:20 +02:00
Marco
d14d929bc3 Update SQLite schema to include 'user_id' in 'users_confirmations' 2017-07-30 19:36:29 +02:00
Marco
f962008fc4 Describe required changes to MySQL schema in migration guide 2017-07-30 19:35:52 +02:00
Marco
ec8e9eab4e Update MySQL schema to include 'user_id' in 'users_confirmations' 2017-07-30 19:34:30 +02:00
Marco
65b4f812c0 Document two methods that let users enable or disable password resets 2017-07-30 17:02:59 +02:00
Marco
b8e04e3c6a Add tests for methods that let users enable or disable password resets 2017-07-30 16:45:54 +02:00
Marco
5c92d026c9 Pass 'Auth' instance to 'showAuthenticatedUserForm' in 'tests' 2017-07-30 16:37:34 +02:00
Marco
2247c2781c Allow for users to enable or disable password resets on their own 2017-07-30 16:34:29 +02:00
Marco
72b2468aa3 Explain new exception from password reset methods in migration guide 2017-07-30 16:22:34 +02:00
Marco
7cc27b814e Add tests for new exception from 'forgotPassword' and 'resetPassword' 2017-07-30 16:21:21 +02:00
Marco
dbc463c95e Document new exception for 'forgotPassword' and 'resetPassword' 2017-07-30 16:17:04 +02:00
Marco
4b6afc7c48 Fail with exception in 'resetPassword' if password reset is disabled 2017-07-30 16:12:57 +02:00
Marco
a3a28af2aa Fail with exception in 'forgotPassword' if password reset is disabled 2017-07-30 16:12:10 +02:00
Marco
c842fa9792 Add class 'ResetDisabledException' 2017-07-30 15:48:19 +02:00
Marco
a599771bd5 Describe required changes to SQLite schema in migration guide 2017-07-30 14:42:21 +02:00
Marco
e73f29eec0 Update SQLite schema to include 'resettable' column in 'users' table 2017-07-30 14:41:53 +02:00
Marco
c118116a52 Describe required changes to MySQL schema in migration guide 2017-07-30 14:41:28 +02:00
Marco
0e969ccd8d Update MySQL schema to include 'resettable' column in 'users' table 2017-07-30 14:40:11 +02:00
Marco
aae0bfb5ab Document method 'confirmEmailAndSignIn' from class 'Auth' 2017-07-30 14:21:33 +02:00
Marco
fb982cee6a Add tests for method 'confirmEmailAndSignIn' from class 'Auth' 2017-07-30 14:20:31 +02:00
Marco
838c6edf66 Implement method 'confirmEmailAndSignIn' in class 'Auth' 2017-07-30 14:19:07 +02:00
Marco
ad5784364d Return confirmed email address from 'confirmEmail' in class 'Auth' 2017-07-30 14:16:52 +02:00
Marco
d8f21a35fc Add documentation for method 'reconfirmPassword' from class 'Auth' 2017-07-30 01:17:16 +02:00
Marco
79ecb85bb6 Add tests for method 'reconfirmPassword' from class 'Auth' 2017-07-30 00:57:38 +02:00
Marco
f56e7e6871 Implement method 'reconfirmPassword' in class 'Auth' 2017-07-30 00:54:06 +02:00
Marco
83f2ab0a9c Document optional prefix for the names of all database tables 2017-07-30 00:11:10 +02:00
Marco
5274dd5f8e Support optional prefix for the names of all database tables 2017-07-30 00:04:48 +02:00
Marco
b93d9616d0 Fix URL fragment for internal link in README 2017-07-29 23:31:46 +02:00
Marco
0af55ad77c Document features related to roles in 'Administration' interface 2017-07-29 23:21:57 +02:00
Marco
7b6287a7dc Document features related to roles in 'Auth' interface 2017-07-29 23:18:38 +02:00
Marco
cf7493d87e Fix response on exception in tests 2017-07-29 22:59:09 +02:00
Marco
f68d29000e Remove tests for 'onBeforeSuccess' callback of login methods 2017-07-29 20:43:31 +02:00
Marco
cd3469c137 Add tests for checking roles for users via 'Administration' class 2017-07-29 20:28:18 +02:00
Marco
bc44a08b1b Allow for roles to be checked for users via 'Administration' class 2017-07-29 20:24:24 +02:00
Marco
8ff4242f8f Add tests for taking roles away from users via 'Administration' class 2017-07-29 19:09:04 +02:00
Marco
1a4041ea60 Allow for roles to be taken away from users via 'Administration' class 2017-07-29 19:06:13 +02:00
Marco
b7e6ca6dee Add tests for assigning roles to users via 'Administration' class 2017-07-29 19:02:55 +02:00
Marco
f2074e1537 Allow for roles to be assigned to users via 'Administration' class 2017-07-29 18:55:15 +02:00
Marco
9c63c30cd9 Add method in 'tests' that creates list of roles for HTML 'select' 2017-07-29 18:52:18 +02:00
Marco
8a1140a485 Add private methods to 'Administration' for modifying users' roles 2017-07-29 18:47:32 +02:00
Marco
23b172055b Add tests for read access to user's roles via 'Auth' interface 2017-07-29 18:21:27 +02:00
Marco
c25b74d405 Provide read access to user's roles via 'Auth' interface 2017-07-29 18:19:00 +02:00
Marco
2278b86fba Read user's roles from database and maintain value in session data 2017-07-29 18:15:17 +02:00
Marco
4eca6bb151 Merge notes about 'Base64' class in migration guide 2017-07-29 18:09:04 +02:00
Marco
db4c99e729 Include general guide for any update in migration notes 2017-07-29 18:02:59 +02:00
Marco
d6bc8c6492 Describe required changes to SQLite schema in migration guide 2017-07-29 18:01:51 +02:00
Marco
b577322939 Update SQLite schema to include 'roles_mask' column in 'users' table 2017-07-29 18:01:04 +02:00
Marco
6cf955ed52 Describe required changes to MySQL schema in migration guide 2017-07-29 17:59:14 +02:00
Marco
8c2c32f9dc Update MySQL schema to include 'roles_mask' column in 'users' table 2017-07-29 17:50:43 +02:00
Marco
2d7ad74c44 Explain in migration guide that the database schema will have changed 2017-07-26 16:19:28 +02:00
Marco
a91cde706d Improve formatting 2017-07-26 16:18:50 +02:00
Marco
8feda0ae58 Update dependencies 2017-07-26 16:16:20 +02:00
Marco
78b7fb4169 Add warning to 'tests' explaining that files are not to be re-used 2017-07-24 23:28:26 +02:00
Marco
499fbb6542 Explain why login attempts may (confusingly) be cancelled in 'tests' 2017-07-24 23:26:30 +02:00
Marco
50b9c48f8d Improve note on 'Base64' class in migration guide 2017-07-24 22:10:10 +02:00
Marco
fcbace0aec Add another note regarding 'Base64' class to migration guide 2017-07-24 22:00:22 +02:00
Marco
c2ab825354 Extract class 'Base64' into external library 2017-07-24 21:56:35 +02:00
Marco
b1ac859fd2 Update dependencies 2017-07-24 21:21:44 +02:00
Marco
0d9be76f8b Add note regarding 'Base64' class to migration guide 2017-07-23 23:50:22 +02:00
Marco
64d15263ae Prepare migration guide for next major version 2017-07-23 23:49:29 +02:00
Marco
854bc2b62b Swap positions of hyphen and underscore characters in URL-safe Base64
This ensures compatibility with RFC 4648 and the example from the
appendix of RFC 7515, aside from the padding character that is used.
2017-07-23 23:18:28 +02:00
Marco
01a52b76bc Switch characters in URL-safe Base64 to use tilde (~) for padding
The tilde character is less familiar to most users and harder to type
on most keyboards (compared to the hyphen and underscore characters).
2017-07-23 22:56:28 +02:00
Marco
ad88c1c6ab Use tilde character (~) instead of dot (.) for URL-safe Base64 coding
The dot character is excluded from auto-linking in most email clients
and is ambiguous in all other contexts when occurring at the end of a
URL. The tilde character, being the only unreserved character for use
in URLs that remains, as per RFC 3986, is thus a good alternative.
2017-07-23 22:16:13 +02:00
Marco
449e1c69ee Remove obsolete 'pre-check' and 'post-check' for 'Cache-Control' 2017-07-21 06:20:30 +02:00
Marco
63734fc5ee Add 'Role' class with constants for individual roles or groups 2017-07-10 20:59:45 +02:00
Marco
6e3728a918 Include help link for Composer in README 2017-07-08 23:32:48 +02:00
Marco
0909291cf1 Support multi-factor authentication via 'onBeforeSuccess' callback 2017-07-02 23:12:36 +02:00
Marco
6aa3f58059 Add 'AttemptCancelledException' 2017-07-02 22:17:43 +02:00
Marco
6156b1c135 Explain how to achieve interoperability with other session-based libs 2017-07-02 21:13:23 +02:00
Marco
829d5614ed Explain how to allow framing or embedding on third-party sites 2017-06-22 22:06:20 +02:00
Marco
47afa1c411 Remove enforcement of hard dependency on 'mysqlnd' in code 2017-06-20 02:19:46 +02:00
Marco
26cb41e992 Document support of SQLite 2017-06-12 20:35:07 +02:00
Marco
ee485f99ab Ensure compatibility with SQLite which does not cast to native types 2017-06-12 20:29:58 +02:00
Marco
8fc0b98493 Remove superfluous blank line 2017-06-12 20:28:47 +02:00
prometeusweb
45553afaea Add database schema for SQLite 2017-06-12 20:26:14 +02:00
Marco
7834455e16 Add 'What about password hashing?' to FAQ in README 2017-04-24 21:06:06 +02:00
Marco
e49adf0150 Move 'Custom password requirements' to FAQ in README 2017-04-24 20:58:18 +02:00
Marco
0fb653d6e0 Add section 'Custom password requirements' to README 2017-03-24 17:07:26 +01:00
Marco
dc233d9d46 Remove 'Features' section in README 2017-03-24 16:49:37 +01:00
Marco
7c842f903e Add 'MySQL Native Driver' as full name of 'mysqlnd' driver in README 2017-03-18 22:35:44 +01:00
Marco
0e2279ecda Document what is required to make library work with other databases 2017-03-18 22:31:30 +01:00
Marco
79db94f500 Add 'mysqlnd' driver for PDO as platform dependency in README 2017-03-18 22:30:19 +01:00
Marco
f38d7bd62c Add PDO as platform dependency in README 2017-03-18 22:25:23 +01:00
Marco
04a2e8ef4e Throw error if 'libmysqlclient' driver is used instead of 'mysqlnd' 2017-03-18 22:21:23 +01:00
Marco
59505479a5 Add class 'WrongMysqlDatabaseDriverError' 2017-03-18 22:13:28 +01:00
Marco
fdcfd6f78c Add class 'DatabaseDriverError' 2017-03-18 22:12:49 +01:00
Marco
20606bc507 Update dependencies 2017-03-18 22:11:05 +01:00
Marco
89a7af17fe Add documentation on how to retrieve status information 2017-02-26 14:15:26 +01:00
14 changed files with 3160 additions and 687 deletions

View File

@@ -14,6 +14,8 @@ CREATE TABLE IF NOT EXISTS `users` (
`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,
PRIMARY KEY (`id`),
@@ -22,13 +24,15 @@ CREATE TABLE IF NOT EXISTS `users` (
CREATE TABLE IF NOT EXISTS `users_confirmations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) 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,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
KEY `email_expires` (`email`,`expires`)
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` (
@@ -54,13 +58,12 @@ CREATE TABLE IF NOT EXISTS `users_resets` (
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_throttling` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`action_type` enum('login','register','confirm_email') COLLATE utf8mb4_unicode_ci NOT NULL,
`selector` varchar(44) CHARACTER SET latin1 COLLATE latin1_general_cs DEFAULT NULL,
`time_bucket` int(10) unsigned NOT NULL,
`attempts` mediumint(8) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `action_type_selector_time_bucket` (`action_type`,`selector`,`time_bucket`)
`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,
PRIMARY KEY (`bucket`),
KEY `expires_at` (`expires_at`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

57
Database/PostgreSQL.sql Normal file
View File

@@ -0,0 +1,57 @@
-- 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)
BEGIN;
CREATE TABLE IF NOT EXISTS "users" (
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
"email" VARCHAR(249) UNIQUE NOT NULL,
"password" VARCHAR(255) NOT NULL,
"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),
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
"last_login" INTEGER DEFAULT NULL CHECK ("last_login" >= 0)
);
CREATE TABLE IF NOT EXISTS "users_confirmations" (
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
"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,
"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 TABLE IF NOT EXISTS "users_remembered" (
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(24) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "user" ON "users_remembered" ("user");
CREATE TABLE IF NOT EXISTS "users_resets" (
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(20) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "user_expires" ON "users_resets" ("user", "expires");
CREATE TABLE IF NOT EXISTS "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY,
"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");
COMMIT;

59
Database/SQLite.sql Normal file
View File

@@ -0,0 +1,59 @@
-- 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)
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",
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
"last_login" INTEGER CHECK ("last_login" >= 0) DEFAULT NULL,
CONSTRAINT "email" UNIQUE ("email")
);
CREATE TABLE "users_confirmations" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
"email" VARCHAR(249) NOT NULL,
"selector" VARCHAR(16) NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
CONSTRAINT "selector" 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 TABLE "users_remembered" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(24) NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
CONSTRAINT "selector" UNIQUE ("selector")
);
CREATE INDEX "users_remembered.user" ON "users_remembered" ("user");
CREATE TABLE "users_resets" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(20) NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
CONSTRAINT "selector" UNIQUE ("selector")
);
CREATE INDEX "users_resets.user_expires" ON "users_resets" ("user", "expires");
CREATE TABLE "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY NOT NULL,
"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");

View File

@@ -1,10 +1,141 @@
# Migration
* [General](#general)
* [From `v6.x.x` to `v7.x.x`](#from-v6xx-to-v7xx)
* [From `v5.x.x` to `v6.x.x`](#from-v5xx-to-v6xx)
* [From `v4.x.x` to `v5.x.x`](#from-v4xx-to-v5xx)
* [From `v3.x.x` to `v4.x.x`](#from-v3xx-to-v4xx)
* [From `v2.x.x` to `v3.x.x`](#from-v2xx-to-v3xx)
* [From `v1.x.x` to `v2.x.x`](#from-v1xx-to-v2xx)
## General
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 `v6.x.x` to `v7.x.x`
* The method `logOutButKeepSession` from class `Auth` is now simply called `logOut`. Therefore, the former method `logout` is now called `logOutAndDestroySession`.
* The second argument of the `Auth` constructor, which was named `$useHttps`, has been removed. If you previously had it set to `true`, make sure to set the value of the `session.cookie_secure` directive to `1` now. You may do so either directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. Otherwise, make sure that directive is set to `0`.
* The third argument of the `Auth` constructor, which was named `$allowCookiesScriptAccess`, has been removed. If you previously had it set to `true`, make sure to set the value of the `session.cookie_httponly` directive to `0` now. You may do so either directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. Otherwise, make sure that directive is set to `1`.
* Only if *both* of the following two conditions are met:
* The directive `session.cookie_domain` is set to an empty value. It may have been set directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. You can check the value of that directive by executing the following statement somewhere in your application:
```php
\var_dump(\ini_get('session.cookie_domain'));
```
* Your application is accessed via a registered or registrable *domain name*, either by yourself during development and testing or by your visitors and users in production. That means your application is *not*, or *not only*, accessed via `localhost` or via an IP address.
Then the domain scope for the [two cookies](README.md#cookies) used by this library has changed. You can handle this change in one of two different ways:
* Restore the old behavior by placing the following statement as early as possible in your application, and before you create the `Auth` instance:
```php
\ini_set('session.cookie_domain', \preg_replace('/^www\./', '', $_SERVER['HTTP_HOST']));
```
You may also evaluate the complete second parameter and put its value directly into your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`).
* Use the new domain scope for your application. To do so, you only need to [rename the cookies](README.md#renaming-the-librarys-cookies) used by this library in order to prevent conflicts with old cookies that have been created previously. Renaming the cookies is critically important here. We recommend a versioned name such as `session_v1` for the session cookie.
* Only if *both* of the following two conditions are met:
* The directive `session.cookie_domain` is set to a value that starts with the `www` subdomain. It may have been set directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. You can check the value of that directive by executing the following statement somewhere in your application:
```php
\var_dump(\ini_get('session.cookie_domain'));
```
* Your application is accessed via a registered or registrable *domain name*, either by yourself during development and testing or by your visitors and users in production. That means your application is *not*, or *not only*, accessed via `localhost` or via an IP address.
Then the domain scope for [one of the cookies](README.md#cookies) used by this library has changed. To make your application work correctly with the new scope, [rename the cookies](README.md#renaming-the-librarys-cookies) used by this library in order to prevent conflicts with old cookies that have been created previously. Renaming the cookies is critically important here. We recommend a versioned name such as `session_v1` for the session cookie.
* If the directive `session.cookie_path` is set to an empty value, then the path scope for [one of the cookies](README.md#cookies) used by this library has changed. To make your application work correctly with the new scope, [rename the cookies](README.md#renaming-the-librarys-cookies) used by this library in order to prevent conflicts with old cookies that have been created previously. Renaming the cookies is critically important here. We recommend a versioned name such as `session_v1` for the session cookie.
The directive may have been set directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. You can check the value of that directive by executing the following statement somewhere in your application:
```php
\var_dump(\ini_get('session.cookie_path'));
```
## From `v5.x.x` to `v6.x.x`
* The database schema has changed.
* The MySQL database schema has changed. Use the statements below to update your database:
```sql
ALTER TABLE users
ADD COLUMN roles_mask INT(10) UNSIGNED NOT NULL DEFAULT 0 AFTER verified,
ADD COLUMN resettable TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 AFTER verified;
ALTER TABLE users_confirmations
ADD COLUMN user_id INT(10) UNSIGNED NULL DEFAULT NULL AFTER id;
UPDATE users_confirmations SET user_id = (
SELECT id FROM users WHERE email = users_confirmations.email
) WHERE user_id IS NULL;
ALTER TABLE users_confirmations
CHANGE COLUMN user_id user_id INT(10) UNSIGNED NOT NULL;
ALTER TABLE users_confirmations
ADD INDEX user_id (user_id ASC);
DROP TABLE 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,
PRIMARY KEY (bucket),
KEY expires_at (expires_at)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
* The SQLite database schema has changed. Use the statements below to update your database:
```sql
ALTER TABLE users
ADD COLUMN "roles_mask" INTEGER NOT NULL CHECK ("roles_mask" >= 0) DEFAULT "0",
ADD COLUMN "resettable" INTEGER NOT NULL CHECK ("resettable" >= 0) DEFAULT "1";
ALTER TABLE users_confirmations
ADD COLUMN "user_id" INTEGER CHECK ("user_id" >= 0);
UPDATE users_confirmations SET user_id = (
SELECT id FROM users WHERE email = users_confirmations.email
) WHERE user_id IS NULL;
CREATE INDEX "users_confirmations.user_id" ON "users_confirmations" ("user_id");
DROP TABLE users_throttling;
CREATE TABLE "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY NOT NULL,
"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");
```
* The method `setThrottlingOptions` has been removed.
* The method `changePassword` may now throw an additional `\Delight\Auth\TooManyRequestsException` if too many attempts have been made without the correct old password.
* The two methods `confirmEmail` and `confirmEmailAndSignIn` may now throw an additional `\Delight\Auth\UserAlreadyExistsException` if an attempt has been made to change the email address to an address that has become occupied in the meantime.
* The two methods `forgotPassword` and `resetPassword` may now throw an additional `\Delight\Auth\ResetDisabledException` if the user has disabled password resets for their account.
* The `Base64` class is now an external module and has been moved from the namespace `Delight\Auth` to the namespace `Delight\Base64`. The interface and the return values are not compatible with those from previous versions anymore.
## From `v4.x.x` to `v5.x.x`
* The MySQL database schema has changed. Use the statement below to update your database:

841
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@
"require": {
"php": ">=5.6.0",
"ext-openssl": "*",
"delight-im/cookie": "^2.1",
"delight-im/db": "^1.0"
"delight-im/base64": "^1.0",
"delight-im/cookie": "^3.1",
"delight-im/db": "^1.2"
},
"type": "library",
"keywords": [ "auth", "authentication", "login", "security" ],

72
composer.lock generated
View File

@@ -4,26 +4,66 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "bd80e3e52b8bd8a4a0c74c7cf9f5bf5e",
"content-hash": "3f836c43e0ff2293051f2ccb739d23cf",
"content-hash": "54d541ae3c5ba25b0cc06688d2b65467",
"packages": [
{
"name": "delight-im/cookie",
"version": "v2.1.0",
"name": "delight-im/base64",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-Cookie.git",
"reference": "3e41e0d44959b59de98722b5b1b1fb83f9f528f3"
"url": "https://github.com/delight-im/PHP-Base64.git",
"reference": "687b2a49f663e162030a8d27b32838bbe7f91c78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-Cookie/zipball/3e41e0d44959b59de98722b5b1b1fb83f9f528f3",
"reference": "3e41e0d44959b59de98722b5b1b1fb83f9f528f3",
"url": "https://api.github.com/repos/delight-im/PHP-Base64/zipball/687b2a49f663e162030a8d27b32838bbe7f91c78",
"reference": "687b2a49f663e162030a8d27b32838bbe7f91c78",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Base64\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Simple and convenient Base64 encoding and decoding for PHP",
"homepage": "https://github.com/delight-im/PHP-Base64",
"keywords": [
"URL-safe",
"base-64",
"base64",
"decode",
"decoding",
"encode",
"encoding",
"url"
],
"time": "2017-07-24T18:59:51+00:00"
},
{
"name": "delight-im/cookie",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-Cookie.git",
"reference": "76ef2a21817cf7a034f85fc3f4d4bfc60f873947"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-Cookie/zipball/76ef2a21817cf7a034f85fc3f4d4bfc60f873947",
"reference": "76ef2a21817cf7a034f85fc3f4d4bfc60f873947",
"shasum": ""
},
"require": {
"delight-im/http": "^2.0",
"php": ">=5.3.0"
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
@@ -46,20 +86,20 @@
"samesite",
"xss"
],
"time": "2016-11-23 20:09:42"
"time": "2017-10-18T19:48:59+00:00"
},
{
"name": "delight-im/db",
"version": "v1.0.2",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-DB.git",
"reference": "c8d1eba6583007471d55bf7d88eb3c9d87ea849d"
"reference": "df99ef7c2e86c7ce206647ffe8ba74447c075b57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-DB/zipball/c8d1eba6583007471d55bf7d88eb3c9d87ea849d",
"reference": "c8d1eba6583007471d55bf7d88eb3c9d87ea849d",
"url": "https://api.github.com/repos/delight-im/PHP-DB/zipball/df99ef7c2e86c7ce206647ffe8ba74447c075b57",
"reference": "df99ef7c2e86c7ce206647ffe8ba74447c075b57",
"shasum": ""
},
"require": {
@@ -87,7 +127,7 @@
"sql",
"sqlite"
],
"time": "2016-12-01 12:40:36"
"time": "2017-03-18T20:51:59+00:00"
},
{
"name": "delight-im/http",
@@ -123,7 +163,7 @@
"http",
"https"
],
"time": "2016-07-21 15:05:01"
"time": "2016-07-21T15:05:01+00:00"
}
],
"packages-dev": [],

View File

@@ -20,9 +20,10 @@ final class Administration extends UserManager {
* @internal
*
* @param PdoDatabase $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
*/
public function __construct(PdoDatabase $databaseConnection) {
parent::__construct($databaseConnection);
public function __construct(PdoDatabase $databaseConnection, $dbTablePrefix = null) {
parent::__construct($databaseConnection, $dbTablePrefix);
}
/**
@@ -106,15 +107,267 @@ final class Administration extends UserManager {
*/
public function deleteUserByUsername($username) {
$userData = $this->getUserDataByUsername(
trim($username),
\trim($username),
[ 'id' ]
);
$this->deleteUsersByColumnValue('id', (int) $userData['id']);
}
protected function throttle($actionType, $customSelector = null) {
// do nothing
/**
* Assigns the specified role to the user with the given ID
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param int $userId the ID of the user to assign the role to
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function addRoleForUserById($userId, $role) {
$userFound = $this->addRoleForUserByColumnValue(
'id',
(int) $userId,
$role
);
if ($userFound === false) {
throw new UnknownIdException();
}
}
/**
* Assigns the specified role to the user with the given email address
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $userEmail the email address of the user to assign the role to
* @param int $role the role as one of the constants from the {@see Role} class
* @throws InvalidEmailException if no user with the specified email address has been found
*
* @see Role
*/
public function addRoleForUserByEmail($userEmail, $role) {
$userEmail = self::validateEmailAddress($userEmail);
$userFound = $this->addRoleForUserByColumnValue(
'email',
$userEmail,
$role
);
if ($userFound === false) {
throw new InvalidEmailException();
}
}
/**
* Assigns the specified role to the user with the given username
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $username the username of the user to assign the role to
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
*
* @see Role
*/
public function addRoleForUserByUsername($username, $role) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->addRoleForUserByColumnValue(
'id',
(int) $userData['id'],
$role
);
}
/**
* Takes away the specified role from the user with the given ID
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param int $userId the ID of the user to take the role away from
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function removeRoleForUserById($userId, $role) {
$userFound = $this->removeRoleForUserByColumnValue(
'id',
(int) $userId,
$role
);
if ($userFound === false) {
throw new UnknownIdException();
}
}
/**
* Takes away the specified role from the user with the given email address
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $userEmail the email address of the user to take the role away from
* @param int $role the role as one of the constants from the {@see Role} class
* @throws InvalidEmailException if no user with the specified email address has been found
*
* @see Role
*/
public function removeRoleForUserByEmail($userEmail, $role) {
$userEmail = self::validateEmailAddress($userEmail);
$userFound = $this->removeRoleForUserByColumnValue(
'email',
$userEmail,
$role
);
if ($userFound === false) {
throw new InvalidEmailException();
}
}
/**
* Takes away the specified role from the user with the given username
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $username the username of the user to take the role away from
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
*
* @see Role
*/
public function removeRoleForUserByUsername($username, $role) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->removeRoleForUserByColumnValue(
'id',
(int) $userData['id'],
$role
);
}
/**
* Returns whether the user with the given ID has the specified role
*
* @param int $userId the ID of the user to check the roles for
* @param int $role the role as one of the constants from the {@see Role} class
* @return bool
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function doesUserHaveRole($userId, $role) {
$userId = (int) $userId;
$role = (int) $role;
$rolesBitmask = $this->db->selectValue(
'SELECT roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE id = ?',
[ $userId ]
);
if ($rolesBitmask === null) {
throw new UnknownIdException();
}
return ($rolesBitmask & $role) === $role;
}
/**
* Returns the roles of the user with the given ID, mapping the numerical values to their descriptive names
*
* @param int $userId the ID of the user to return the roles for
* @return array
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function getRolesForUserById($userId) {
$userId = (int) $userId;
$rolesBitmask = $this->db->selectValue(
'SELECT roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE id = ?',
[ $userId ]
);
if ($rolesBitmask === null) {
throw new UnknownIdException();
}
return \array_filter(
Role::getMap(),
function ($each) use ($rolesBitmask) {
return ($rolesBitmask & $each) === $each;
},
\ARRAY_FILTER_USE_KEY
);
}
/**
* Signs in as the user with the specified ID
*
* @param int $id the ID of the user to sign in as
* @throws UnknownIdException if no user with the specified ID has been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserById($id) {
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('id', (int) $id);
if ($numberOfMatchedUsers === 0) {
throw new UnknownIdException();
}
}
/**
* Signs in as the user with the specified email address
*
* @param string $email the email address of the user to sign in as
* @throws InvalidEmailException if no user with the specified email address has been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserByEmail($email) {
$email = self::validateEmailAddress($email);
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('email', $email);
if ($numberOfMatchedUsers === 0) {
throw new InvalidEmailException();
}
}
/**
* Signs in as the user with the specified display name
*
* @param string $username the display name of the user to sign in as
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserByUsername($username) {
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('username', \trim($username));
if ($numberOfMatchedUsers === 0) {
throw new UnknownUsernameException();
}
elseif ($numberOfMatchedUsers > 1) {
throw new AmbiguousUsernameException();
}
}
/**
@@ -130,7 +383,7 @@ final class Administration extends UserManager {
private function deleteUsersByColumnValue($columnName, $columnValue) {
try {
return $this->db->delete(
'users',
$this->dbTablePrefix . 'users',
[
$columnName => $columnValue
]
@@ -141,4 +394,136 @@ final class Administration extends UserManager {
}
}
/**
* Modifies the roles for the user where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @param callable $modification the modification to apply to the existing bitmask of roles
* @return bool whether any user with the given column constraints has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see Role
*/
private function modifyRolesForUserByColumnValue($columnName, $columnValue, callable $modification) {
try {
$userData = $this->db->selectRow(
'SELECT id, roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE ' . $columnName . ' = ?',
[ $columnValue ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
if ($userData === null) {
return false;
}
$newRolesBitmask = $modification($userData['roles_mask']);
try {
$this->db->exec(
'UPDATE ' . $this->dbTablePrefix . 'users SET roles_mask = ? WHERE id = ?',
[
$newRolesBitmask,
(int) $userData['id']
]
);
return true;
}
catch (Error $e) {
throw new DatabaseError();
}
}
/**
* Assigns the specified role to the user where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @param int $role the role as one of the constants from the {@see Role} class
* @return bool whether any user with the given column constraints has been found
*
* @see Role
*/
private function addRoleForUserByColumnValue($columnName, $columnValue, $role) {
$role = (int) $role;
return $this->modifyRolesForUserByColumnValue(
$columnName,
$columnValue,
function ($oldRolesBitmask) use ($role) {
return $oldRolesBitmask | $role;
}
);
}
/**
* Takes away the specified role from the user where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @param int $role the role as one of the constants from the {@see Role} class
* @return bool whether any user with the given column constraints has been found
*
* @see Role
*/
private function removeRoleForUserByColumnValue($columnName, $columnValue, $role) {
$role = (int) $role;
return $this->modifyRolesForUserByColumnValue(
$columnName,
$columnValue,
function ($oldRolesBitmask) use ($role) {
return $oldRolesBitmask & ~$role;
}
);
}
/**
* Signs in as the user for which the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @return int the number of matched users (where only a value of one means that the login may have been successful)
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
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',
[ $columnValue ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
$numberOfMatchingUsers = \count($users);
if ($numberOfMatchingUsers === 1) {
$user = $users[0];
if ((int) $user['verified'] === 1) {
$this->onLoginSuccessful($user['id'], $user['email'], $user['username'], $user['status'], $user['roles_mask'], false);
}
else {
throw new EmailNotVerifiedException();
}
}
return $numberOfMatchingUsers;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +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;
final class Base64 {
const SPECIAL_CHARS_ORIGINAL = '+/=';
const SPECIAL_CHARS_SAFE = '._-';
public static function encode($data, $safeChars = false) {
$result = base64_encode($data);
if ($safeChars) {
$result = strtr($result, self::SPECIAL_CHARS_ORIGINAL, self::SPECIAL_CHARS_SAFE);
}
return $result;
}
public static function decode($data) {
$data = strtr($data, self::SPECIAL_CHARS_SAFE, self::SPECIAL_CHARS_ORIGINAL);
$result = base64_decode($data, true);
return $result;
}
}

View File

@@ -34,10 +34,18 @@ 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 {}

79
src/Role.php Normal file
View File

@@ -0,0 +1,79 @@
<?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 Role {
const ADMIN = 1;
const AUTHOR = 2;
const COLLABORATOR = 4;
const CONSULTANT = 8;
const CONSUMER = 16;
const CONTRIBUTOR = 32;
const COORDINATOR = 64;
const CREATOR = 128;
const DEVELOPER = 256;
const DIRECTOR = 512;
const EDITOR = 1024;
const EMPLOYEE = 2048;
const MAINTAINER = 4096;
const MANAGER = 8192;
const MODERATOR = 16384;
const PUBLISHER = 32768;
const REVIEWER = 65536;
const SUBSCRIBER = 131072;
const SUPER_ADMIN = 262144;
const SUPER_EDITOR = 524288;
const SUPER_MODERATOR = 1048576;
const TRANSLATOR = 2097152;
// const XYZ = 4194304;
// const XYZ = 8388608;
// const XYZ = 16777216;
// const XYZ = 33554432;
// const XYZ = 67108864;
// const XYZ = 134217728;
// const XYZ = 268435456;
// const XYZ = 536870912;
/**
* Returns an array mapping the numerical role values to their descriptive names
*
* @return array
*/
public static function getMap() {
$reflectionClass = new \ReflectionClass(static::class);
return \array_flip($reflectionClass->getConstants());
}
/**
* Returns the descriptive role names
*
* @return string[]
*/
public static function getNames() {
$reflectionClass = new \ReflectionClass(static::class);
return \array_keys($reflectionClass->getConstants());
}
/**
* Returns the numerical role values
*
* @return int[]
*/
public static function getValues() {
$reflectionClass = new \ReflectionClass(static::class);
return \array_values($reflectionClass->getConstants());
}
private function __construct() {}
}

View File

@@ -8,6 +8,8 @@
namespace Delight\Auth;
use Delight\Base64\Base64;
use Delight\Cookie\Session;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
@@ -22,12 +24,27 @@ require_once __DIR__ . '/Exceptions.php';
*/
abstract class UserManager {
const THROTTLE_ACTION_LOGIN = 'login';
const THROTTLE_ACTION_REGISTER = 'register';
const THROTTLE_ACTION_CONSUME_TOKEN = 'confirm_email';
/** @var string session field for whether the client is currently signed in */
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
/** @var string session field for the ID of the user who is currently signed in (if any) */
const SESSION_FIELD_USER_ID = 'auth_user_id';
/** @var string session field for the email address of the user who is currently signed in (if any) */
const SESSION_FIELD_EMAIL = 'auth_email';
/** @var string session field for the display name (if any) of the user who is currently signed in (if any) */
const SESSION_FIELD_USERNAME = 'auth_username';
/** @var string session field for the status of the user who is currently signed in (if any) as one of the constants from the {@see Status} class */
const SESSION_FIELD_STATUS = 'auth_status';
/** @var string session field for the roles of the user who is currently signed in (if any) as a bitmask using constants from the {@see Role} class */
const SESSION_FIELD_ROLES = 'auth_roles';
/** @var string session field for whether the user who is currently signed in (if any) has been remembered (instead of them having authenticated actively) */
const SESSION_FIELD_REMEMBERED = 'auth_remembered';
/** @var string session field for the UNIX timestamp in seconds of the session data's last resynchronization with its authoritative source in the database */
const SESSION_FIELD_LAST_RESYNC = 'auth_last_resync';
/** @var PdoDatabase the database connection to operate on */
protected $db;
/** @var string the prefix for the names of all database tables used by this component */
protected $dbTablePrefix;
/**
* Creates a random string with the given maximum length
@@ -39,19 +56,20 @@ abstract class UserManager {
*/
public static function createRandomString($maxLength = 24) {
// calculate how many bytes of randomness we need for the specified string length
$bytes = floor(intval($maxLength) / 4) * 3;
$bytes = \floor((int) $maxLength / 4) * 3;
// get random data
$data = openssl_random_pseudo_bytes($bytes);
$data = \openssl_random_pseudo_bytes($bytes);
// return the Base64-encoded result
return Base64::encode($data, true);
return Base64::encodeUrlSafe($data);
}
/**
* @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
*/
protected function __construct($databaseConnection) {
protected function __construct($databaseConnection, $dbTablePrefix = null) {
if ($databaseConnection instanceof PdoDatabase) {
$this->db = $databaseConnection;
}
@@ -66,6 +84,8 @@ abstract class UserManager {
throw new \InvalidArgumentException('The database connection must be an instance of either `PdoDatabase`, `PdoDsn` or `PDO`');
}
$this->dbTablePrefix = (string) $dbTablePrefix;
}
/**
@@ -94,16 +114,17 @@ abstract class UserManager {
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws DuplicateUsernameException if it was specified that the username must be unique while it was *not*
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see confirmEmail
* @see confirmEmailAndSignIn
*/
protected function createUserInternal($requireUniqueUsername, $email, $password, $username = null, callable $callback = null) {
$this->throttle(self::THROTTLE_ACTION_REGISTER);
ignore_user_abort(true);
\ignore_user_abort(true);
$email = self::validateEmailAddress($email);
$password = self::validatePassword($password);
$username = isset($username) ? trim($username) : null;
$username = isset($username) ? \trim($username) : null;
// if the supplied username is the empty string or has consisted of whitespace only
if ($username === '') {
@@ -117,7 +138,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 users WHERE username = ?',
'SELECT COUNT(*) FROM ' . $this->dbTablePrefix . 'users WHERE username = ?',
[ $username ]
);
@@ -129,18 +150,18 @@ abstract class UserManager {
}
}
$password = password_hash($password, PASSWORD_DEFAULT);
$verified = isset($callback) && is_callable($callback) ? 0 : 1;
$password = \password_hash($password, \PASSWORD_DEFAULT);
$verified = \is_callable($callback) ? 0 : 1;
try {
$this->db->insert(
'users',
$this->dbTablePrefix . 'users',
[
'email' => $email,
'password' => $password,
'username' => $username,
'verified' => $verified,
'registered' => time()
'registered' => \time()
]
);
}
@@ -155,12 +176,40 @@ abstract class UserManager {
$newUserId = (int) $this->db->getLastInsertId();
if ($verified === 0) {
$this->createConfirmationRequest($email, $callback);
$this->createConfirmationRequest($newUserId, $email, $callback);
}
return $newUserId;
}
/**
* Called when a user has successfully logged in
*
* This may happen via the standard login, via the "remember me" feature, or due to impersonation by administrators
*
* @param int $userId the ID of the user
* @param string $email the email address of the user
* @param string $username the display name (if any) of the user
* @param int $status the status of the user as one of the constants from the {@see Status} class
* @param int $roles the roles of the user as a bitmask using constants from the {@see Role} class
* @param bool $remembered whether the user has been remembered (instead of them having authenticated actively)
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function onLoginSuccessful($userId, $email, $username, $status, $roles, $remembered) {
// re-generate the session ID to prevent session fixation attacks (requests a cookie to be written on the client)
Session::regenerate(true);
// save the user data in the session variables maintained by this library
$_SESSION[self::SESSION_FIELD_LOGGED_IN] = true;
$_SESSION[self::SESSION_FIELD_USER_ID] = (int) $userId;
$_SESSION[self::SESSION_FIELD_EMAIL] = $email;
$_SESSION[self::SESSION_FIELD_USERNAME] = $username;
$_SESSION[self::SESSION_FIELD_STATUS] = (int) $status;
$_SESSION[self::SESSION_FIELD_ROLES] = (int) $roles;
$_SESSION[self::SESSION_FIELD_REMEMBERED] = $remembered;
$_SESSION[self::SESSION_FIELD_LAST_RESYNC] = \time();
}
/**
* Returns the requested user data for the account with the specified username (if any)
*
@@ -175,10 +224,10 @@ abstract class UserManager {
*/
protected function getUserDataByUsername($username, array $requestedColumns) {
try {
$projection = implode(', ', $requestedColumns);
$projection = \implode(', ', $requestedColumns);
$users = $this->db->select(
'SELECT ' . $projection . ' FROM users WHERE username = ? LIMIT 0, 2',
'SELECT ' . $projection . ' FROM ' . $this->dbTablePrefix . 'users WHERE username = ? LIMIT 2 OFFSET 0',
[ $username ]
);
}
@@ -190,7 +239,7 @@ abstract class UserManager {
throw new UnknownUsernameException();
}
else {
if (count($users) === 1) {
if (\count($users) === 1) {
return $users[0];
}
else {
@@ -211,9 +260,9 @@ abstract class UserManager {
throw new InvalidEmailException();
}
$email = trim($email);
$email = \trim($email);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
if (!\filter_var($email, \FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
@@ -232,25 +281,15 @@ abstract class UserManager {
throw new InvalidPasswordException();
}
$password = trim($password);
$password = \trim($password);
if (strlen($password) < 1) {
if (\strlen($password) < 1) {
throw new InvalidPasswordException();
}
return $password;
}
/**
* Throttles the specified action for the user to protect against too many requests
*
* @param string $actionType one of the constants from this class starting with `THROTTLE_ACTION_`
* @param mixed|null $customSelector a custom selector to use for throttling (if any), otherwise the IP address will be used
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
abstract protected function throttle($actionType, $customSelector = null);
/**
* Creates a request for email confirmation
*
@@ -262,22 +301,22 @@ abstract class UserManager {
*
* When the user wants to verify their email address as a next step, both pieces will be required again
*
* @param int $userId the user's ID
* @param string $email the email address to verify
* @param callable $callback the function that sends the confirmation email to the user
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function createConfirmationRequest($email, callable $callback) {
protected function createConfirmationRequest($userId, $email, callable $callback) {
$selector = self::createRandomString(16);
$token = self::createRandomString(16);
$tokenHashed = password_hash($token, PASSWORD_DEFAULT);
// the request shall be valid for one day
$expires = time() + 60 * 60 * 24;
$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
$expires = \time() + 60 * 60 * 24;
try {
$this->db->insert(
'users_confirmations',
$this->dbTablePrefix . 'users_confirmations',
[
'user_id' => (int) $userId,
'email' => $email,
'selector' => $selector,
'token' => $tokenHashed,
@@ -289,7 +328,7 @@ abstract class UserManager {
throw new DatabaseError();
}
if (isset($callback) && is_callable($callback)) {
if (\is_callable($callback)) {
$callback($selector, $token);
}
else {

View File

@@ -6,32 +6,45 @@
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
/*
* WARNING:
*
* Do *not* use these files from the `tests` directory as the foundation
* for the usage of this library in your own code. Instead, please follow
* the `README.md` file in the root directory of this project.
*/
// enable error reporting
error_reporting(E_ALL);
ini_set('display_errors', 'stdout');
\error_reporting(\E_ALL);
\ini_set('display_errors', 'stdout');
// enable assertions
ini_set('assert.active', 1);
@ini_set('zend.assertions', 1);
ini_set('assert.exception', 1);
\ini_set('assert.active', 1);
@\ini_set('zend.assertions', 1);
\ini_set('assert.exception', 1);
header('Content-type: text/html; charset=utf-8');
\header('Content-type: text/html; charset=utf-8');
require __DIR__.'/../vendor/autoload.php';
$db = new PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8mb4', 'root', 'monkey');
$db = new \PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8mb4', 'root', 'monkey');
// or
// $db = new \PDO('pgsql:dbname=php_auth;host=127.0.0.1;port=5432', 'postgres', 'monkey');
// or
// $db = new \PDO('sqlite:../Databases/php_auth.sqlite');
$auth = new \Delight\Auth\Auth($db);
$result = processRequestData($auth);
$result = \processRequestData($auth);
showDebugData($auth, $result);
\showGeneralForm();
\showDebugData($auth, $result);
if ($auth->check()) {
showAuthenticatedUserForm();
\showAuthenticatedUserForm($auth);
}
else {
showGuestUserForm();
\showGuestUserForm();
}
function processRequestData(\Delight\Auth\Auth $auth) {
@@ -73,7 +86,7 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'wrong password';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
return 'email address not verified';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
@@ -88,11 +101,11 @@ function processRequestData(\Delight\Auth\Auth $auth) {
echo "\n";
echo ' > Selector';
echo "\t\t\t\t";
echo htmlspecialchars($selector);
echo \htmlspecialchars($selector);
echo "\n";
echo ' > Token';
echo "\t\t\t\t";
echo htmlspecialchars($token);
echo \htmlspecialchars($token);
echo '</pre>';
};
}
@@ -129,7 +142,20 @@ function processRequestData(\Delight\Auth\Auth $auth) {
}
else if ($_POST['action'] === 'confirmEmail') {
try {
$auth->confirmEmail($_POST['selector'], $_POST['token']);
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;
}
$auth->confirmEmailAndSignIn($_POST['selector'], $_POST['token'], $rememberDuration);
}
else {
$auth->confirmEmail($_POST['selector'], $_POST['token']);
}
return 'ok';
}
@@ -139,6 +165,59 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\TokenExpiredException $e) {
return 'token expired';
}
catch (\Delight\Auth\UserAlreadyExistsException $e) {
return 'email address already exists';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'resendConfirmationForEmail') {
try {
$auth->resendConfirmationForEmail($_POST['email'], function ($selector, $token) {
echo '<pre>';
echo 'Email confirmation';
echo "\n";
echo ' > Selector';
echo "\t\t\t\t";
echo \htmlspecialchars($selector);
echo "\n";
echo ' > Token';
echo "\t\t\t\t";
echo \htmlspecialchars($token);
echo '</pre>';
});
return 'ok';
}
catch (\Delight\Auth\ConfirmationRequestNotFound $e) {
return 'no request found';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'resendConfirmationForUserId') {
try {
$auth->resendConfirmationForUserId($_POST['userId'], function ($selector, $token) {
echo '<pre>';
echo 'Email confirmation';
echo "\n";
echo ' > Selector';
echo "\t\t\t\t";
echo \htmlspecialchars($selector);
echo "\n";
echo ' > Token';
echo "\t\t\t\t";
echo \htmlspecialchars($token);
echo '</pre>';
});
return 'ok';
}
catch (\Delight\Auth\ConfirmationRequestNotFound $e) {
return 'no request found';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
@@ -151,11 +230,11 @@ function processRequestData(\Delight\Auth\Auth $auth) {
echo "\n";
echo ' > Selector';
echo "\t\t\t\t";
echo htmlspecialchars($selector);
echo \htmlspecialchars($selector);
echo "\n";
echo ' > Token';
echo "\t\t\t\t";
echo htmlspecialchars($token);
echo \htmlspecialchars($token);
echo '</pre>';
});
@@ -165,7 +244,10 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'invalid email address';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
return 'email address not verified';
}
catch (\Delight\Auth\ResetDisabledException $e) {
return 'password reset is disabled';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
@@ -183,6 +265,9 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\TokenExpiredException $e) {
return 'token expired';
}
catch (\Delight\Auth\ResetDisabledException $e) {
return 'password reset is disabled';
}
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password';
}
@@ -190,6 +275,36 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'canResetPassword') {
try {
$auth->canResetPasswordOrThrow($_POST['selector'], $_POST['token']);
return 'yes';
}
catch (\Delight\Auth\InvalidSelectorTokenPairException $e) {
return 'invalid token';
}
catch (\Delight\Auth\TokenExpiredException $e) {
return 'token expired';
}
catch (\Delight\Auth\ResetDisabledException $e) {
return 'password reset is disabled';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'reconfirmPassword') {
try {
return $auth->reconfirmPassword($_POST['password']) ? 'correct' : 'wrong';
}
catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'changePassword') {
try {
$auth->changePassword($_POST['oldPassword'], $_POST['newPassword']);
@@ -202,9 +317,74 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password(s)';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'logout') {
$auth->logout();
else if ($_POST['action'] === 'changePasswordWithoutOldPassword') {
try {
$auth->changePasswordWithoutOldPassword($_POST['newPassword']);
return 'ok';
}
catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password';
}
}
else if ($_POST['action'] === 'changeEmail') {
try {
$auth->changeEmail($_POST['newEmail'], function ($selector, $token) {
echo '<pre>';
echo 'Email confirmation';
echo "\n";
echo ' > Selector';
echo "\t\t\t\t";
echo \htmlspecialchars($selector);
echo "\n";
echo ' > Token';
echo "\t\t\t\t";
echo \htmlspecialchars($token);
echo '</pre>';
});
return 'ok';
}
catch (\Delight\Auth\InvalidEmailException $e) {
return 'invalid email address';
}
catch (\Delight\Auth\UserAlreadyExistsException $e) {
return 'email address already exists';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'account not verified';
}
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);
return 'ok';
}
catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
}
else if ($_POST['action'] === 'logOut') {
$auth->logOut();
return 'ok';
}
else if ($_POST['action'] === 'logOutAndDestroySession') {
$auth->logOutAndDestroySession();
return 'ok';
}
@@ -263,13 +443,179 @@ function processRequestData(\Delight\Auth\Auth $auth) {
}
}
else {
return 'either ID, email or username required';
return 'either ID, email address or username required';
}
return 'ok';
}
else if ($_POST['action'] === 'admin.addRole') {
if (isset($_POST['role'])) {
if (isset($_POST['id'])) {
try {
$auth->admin()->addRoleForUserById($_POST['id'], $_POST['role']);
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
}
elseif (isset($_POST['email'])) {
try {
$auth->admin()->addRoleForUserByEmail($_POST['email'], $_POST['role']);
}
catch (\Delight\Auth\InvalidEmailException $e) {
return 'unknown email address';
}
}
elseif (isset($_POST['username'])) {
try {
$auth->admin()->addRoleForUserByUsername($_POST['username'], $_POST['role']);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
return 'unknown username';
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
return 'ambiguous username';
}
}
else {
return 'either ID, email address or username required';
}
}
else {
return 'role required';
}
return 'ok';
}
else if ($_POST['action'] === 'admin.removeRole') {
if (isset($_POST['role'])) {
if (isset($_POST['id'])) {
try {
$auth->admin()->removeRoleForUserById($_POST['id'], $_POST['role']);
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
}
elseif (isset($_POST['email'])) {
try {
$auth->admin()->removeRoleForUserByEmail($_POST['email'], $_POST['role']);
}
catch (\Delight\Auth\InvalidEmailException $e) {
return 'unknown email address';
}
}
elseif (isset($_POST['username'])) {
try {
$auth->admin()->removeRoleForUserByUsername($_POST['username'], $_POST['role']);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
return 'unknown username';
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
return 'ambiguous username';
}
}
else {
return 'either ID, email address or username required';
}
}
else {
return 'role required';
}
return 'ok';
}
else if ($_POST['action'] === 'admin.hasRole') {
if (isset($_POST['id'])) {
if (isset($_POST['role'])) {
try {
return $auth->admin()->doesUserHaveRole($_POST['id'], $_POST['role']) ? 'yes' : 'no';
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
}
else {
return 'role required';
}
}
else {
return 'ID required';
}
}
else if ($_POST['action'] === 'admin.getRoles') {
if (isset($_POST['id'])) {
try {
return $auth->admin()->getRolesForUserById($_POST['id']);
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
}
else {
return 'ID required';
}
}
else if ($_POST['action'] === 'admin.logInAsUserById') {
if (isset($_POST['id'])) {
try {
$auth->admin()->logInAsUserById($_POST['id']);
return 'ok';
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email address not verified';
}
}
else {
return 'ID required';
}
}
else if ($_POST['action'] === 'admin.logInAsUserByEmail') {
if (isset($_POST['email'])) {
try {
$auth->admin()->logInAsUserByEmail($_POST['email']);
return 'ok';
}
catch (\Delight\Auth\InvalidEmailException $e) {
return 'unknown email address';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email address not verified';
}
}
else {
return 'Email address required';
}
}
else if ($_POST['action'] === 'admin.logInAsUserByUsername') {
if (isset($_POST['username'])) {
try {
$auth->admin()->logInAsUserByUsername($_POST['username']);
return 'ok';
}
catch (\Delight\Auth\UnknownUsernameException $e) {
return 'unknown username';
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
return 'ambiguous username';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email address not verified';
}
}
else {
return 'Username required';
}
}
else {
throw new Exception('Unexpected action: '.$_POST['action']);
throw new Exception('Unexpected action: ' . $_POST['action']);
}
}
}
@@ -280,44 +626,68 @@ function processRequestData(\Delight\Auth\Auth $auth) {
function showDebugData(\Delight\Auth\Auth $auth, $result) {
echo '<pre>';
echo 'Last operation'."\t\t\t\t";
var_dump($result);
echo 'Session ID'."\t\t\t\t";
var_dump(session_id());
echo 'Last operation' . "\t\t\t\t";
\var_dump($result);
echo 'Session ID' . "\t\t\t\t";
\var_dump(\session_id());
echo "\n";
echo '$auth->isLoggedIn()'."\t\t\t";
var_dump($auth->isLoggedIn());
echo '$auth->check()'."\t\t\t\t";
var_dump($auth->check());
echo '$auth->isLoggedIn()' . "\t\t\t";
\var_dump($auth->isLoggedIn());
echo '$auth->check()' . "\t\t\t\t";
\var_dump($auth->check());
echo "\n";
echo '$auth->getUserId()'."\t\t\t";
var_dump($auth->getUserId());
echo '$auth->id()'."\t\t\t\t";
var_dump($auth->id());
echo '$auth->getUserId()' . "\t\t\t";
\var_dump($auth->getUserId());
echo '$auth->id()' . "\t\t\t\t";
\var_dump($auth->id());
echo "\n";
echo '$auth->getEmail()'."\t\t\t";
var_dump($auth->getEmail());
echo '$auth->getUsername()'."\t\t\t";
var_dump($auth->getUsername());
echo '$auth->getEmail()' . "\t\t\t";
\var_dump($auth->getEmail());
echo '$auth->getUsername()' . "\t\t\t";
\var_dump($auth->getUsername());
echo '$auth->getStatus()'."\t\t\t";
echo convertStatusToText($auth);
echo '$auth->getStatus()' . "\t\t\t";
echo \convertStatusToText($auth);
echo ' / ';
var_dump($auth->getStatus());
\var_dump($auth->getStatus());
echo '$auth->isRemembered()'."\t\t\t";
var_dump($auth->isRemembered());
echo '$auth->getIpAddress()'."\t\t\t";
var_dump($auth->getIpAddress());
echo "\n";
echo 'Auth::createRandomString()'."\t\t";
var_dump(\Delight\Auth\Auth::createRandomString());
echo 'Auth::createUuid()'."\t\t\t";
var_dump(\Delight\Auth\Auth::createUuid());
echo 'Roles (super moderator)' . "\t\t\t";
\var_dump($auth->hasRole(\Delight\Auth\Role::SUPER_MODERATOR));
echo 'Roles (developer *or* manager)' . "\t\t";
\var_dump($auth->hasAnyRole(\Delight\Auth\Role::DEVELOPER, \Delight\Auth\Role::MANAGER));
echo 'Roles (developer *and* manager)' . "\t\t";
\var_dump($auth->hasAllRoles(\Delight\Auth\Role::DEVELOPER, \Delight\Auth\Role::MANAGER));
echo 'Roles' . "\t\t\t\t\t";
echo \json_encode($auth->getRoles()) . "\n";
echo "\n";
echo '$auth->isRemembered()' . "\t\t\t";
\var_dump($auth->isRemembered());
echo '$auth->getIpAddress()' . "\t\t\t";
\var_dump($auth->getIpAddress());
echo "\n";
echo 'Session name' . "\t\t\t\t";
\var_dump(\session_name());
echo 'Auth::createRememberCookieName()' . "\t";
\var_dump(\Delight\Auth\Auth::createRememberCookieName());
echo "\n";
echo 'Auth::createCookieName(\'session\')' . "\t";
\var_dump(\Delight\Auth\Auth::createCookieName('session'));
echo 'Auth::createRandomString()' . "\t\t";
\var_dump(\Delight\Auth\Auth::createRandomString());
echo 'Auth::createUuid()' . "\t\t\t";
\var_dump(\Delight\Auth\Auth::createUuid());
echo '</pre>';
}
@@ -358,8 +728,12 @@ function showGeneralForm() {
echo '</form>';
}
function showAuthenticatedUserForm() {
showGeneralForm();
function showAuthenticatedUserForm(\Delight\Auth\Auth $auth) {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="reconfirmPassword" />';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<button type="submit">Reconfirm password</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="changePassword" />';
@@ -369,19 +743,45 @@ function showAuthenticatedUserForm() {
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="logout" />';
echo '<button type="submit">Logout</button>';
echo '<input type="hidden" name="action" value="changePasswordWithoutOldPassword" />';
echo '<input type="text" name="newPassword" placeholder="New password" /> ';
echo '<button type="submit">Change password without old password</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="changeEmail" />';
echo '<input type="text" name="newEmail" placeholder="New email address" /> ';
echo '<button type="submit">Change email address</button>';
echo '</form>';
\showConfirmEmailForm();
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="setPasswordResetEnabled" />';
echo '<select name="enabled" size="1">';
echo '<option value="0"' . ($auth->isPasswordResetEnabled() ? '' : ' selected="selected"') . '>Disabled</option>';
echo '<option value="1"' . ($auth->isPasswordResetEnabled() ? ' selected="selected"' : '') . '>Enabled</option>';
echo '</select> ';
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="logOut" />';
echo '<button type="submit">Log out</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="logOutAndDestroySession" />';
echo '<button type="submit">Log out and destroy session</button>';
echo '</form>';
}
function showGuestUserForm() {
showGeneralForm();
echo '<h1>Public</h1>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="login" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<select name="remember" size="1">';
echo '<option value="0">Remember (keep logged in)? — No</option>';
@@ -403,7 +803,7 @@ function showGuestUserForm() {
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" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<input type="text" name="username" placeholder="Username (optional)" /> ';
echo '<select name="require_verification" size="1">';
@@ -417,16 +817,11 @@ function showGuestUserForm() {
echo '<button type="submit">Register</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="confirmEmail" />';
echo '<input type="text" name="selector" placeholder="Selector" /> ';
echo '<input type="text" name="token" placeholder="Token" /> ';
echo '<button type="submit">Confirm email</button>';
echo '</form>';
\showConfirmEmailForm();
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="forgotPassword" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Forgot password</button>';
echo '</form>';
@@ -438,11 +833,18 @@ function showGuestUserForm() {
echo '<button type="submit">Reset password</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="canResetPassword" />';
echo '<input type="text" name="selector" placeholder="Selector" /> ';
echo '<input type="text" name="token" placeholder="Token" /> ';
echo '<button type="submit">Can reset password?</button>';
echo '</form>';
echo '<h1>Administration</h1>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.createUser" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<input type="text" name="username" placeholder="Username (optional)" /> ';
echo '<select name="require_unique_username" size="1">';
@@ -460,7 +862,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.deleteUser" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Delete user by email</button>';
echo '</form>';
@@ -469,4 +871,113 @@ function showGuestUserForm() {
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<button type="submit">Delete user by username</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.addRole" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Add role for user by ID</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.addRole" />';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Add role for user by email</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.addRole" />';
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Add role for user by username</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.removeRole" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Remove role for user by ID</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.removeRole" />';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Remove role for user by email</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.removeRole" />';
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Remove role for user by username</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.hasRole" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Does user have role?</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.getRoles" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<button type="submit">Get user\'s roles</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.logInAsUserById" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<button type="submit">Log in as user by ID</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.logInAsUserByEmail" />';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Log in as user by email address</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.logInAsUserByUsername" />';
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<button type="submit">Log in as user by username</button>';
echo '</form>';
}
function showConfirmEmailForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="confirmEmail" />';
echo '<input type="text" name="selector" placeholder="Selector" /> ';
echo '<input type="text" name="token" placeholder="Token" /> ';
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">Confirm email</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="resendConfirmationForEmail" />';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Re-send confirmation</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="resendConfirmationForUserId" />';
echo '<input type="text" name="userId" placeholder="User ID" /> ';
echo '<button type="submit">Re-send confirmation</button>';
echo '</form>';
}
function createRolesOptions() {
$out = '';
foreach (\Delight\Auth\Role::getMap() as $roleValue => $roleName) {
$out .= '<option value="' . $roleValue . '">' . $roleName . '</option>';
}
return $out;
}