1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-08-07 08:36:28 +02:00

359 Commits

Author SHA1 Message Date
Marco
095b8ccc70 Document 'changePasswordForUserById' from class 'Administration' 2018-03-21 03:24:06 +01:00
Marco
550a6d0355 Add tests for 'changePasswordForUserById' from class 'Administration' 2018-03-21 03:22:29 +01:00
Marco
c494e0fa13 Throw 'UnknownIdException' in 'updatePasswordInternal' when no matches 2018-03-21 03:20:11 +01:00
Marco
d7d9899167 Use 'changePasswordForUserById' for 'changePasswordForUserByUsername' 2018-03-21 02:55:31 +01:00
Marco
05165a44a6 Implement method 'changePasswordForUserById' in class 'Administration' 2018-03-21 02:54:50 +01:00
Marco
c3f2097750 Document 'changePasswordForUserByUsername' from 'Administration' 2018-03-21 02:35:09 +01:00
Marco
395a065fd4 Add tests for 'changePasswordForUserByUsername' from 'Administration' 2018-03-21 02:28:55 +01:00
Marco
627c592891 Let 'Administration' constructor be part of public API 2018-03-20 16:13:56 +01:00
Marco
2a6d1c4f7d Delete 'remember me' directives in 'changePasswordForUserByUsername' 2018-03-20 16:11:56 +01:00
Marco
a63e5ec053 Move essence of 'deleteRememberDirectiveForUserById' to 'UserManager' 2018-03-20 16:09:25 +01:00
Marco
4115340927 Improve language 2018-03-20 16:04:29 +01:00
Marco
09dac6a5f5 Rename method 'deleteRememberDirective' in class 'Auth'
Use more expressive name 'deleteRememberDirectiveForUserById'
2018-03-20 15:57:37 +01:00
Marco
3a7a860c6d Validate password in 'changePasswordForUserByUsername' for consistency 2018-03-20 15:54:19 +01:00
maxsenft
131aea3ded Implement method 'changePasswordForUserByUsername' in 'Administration' 2018-03-20 15:50:44 +01:00
maxsenft
e14f3d1925 Rename method 'updatePassword' to 'updatePasswordInternal' 2018-03-20 15:45:25 +01:00
maxsenft
1d54ff2f6b Move 'updatePassword' method from class 'Auth' to class 'UserManager' 2018-03-20 15:41:57 +01:00
maxsenft
ec6afdad48 Accept 'PdoDsn' and 'PDO' as well in 'Administration' constructor 2018-03-20 15:38:35 +01:00
Marco
58e69fdd0e Do not pass 'null' to 'count' which triggers a warning since PHP 7.2 2018-03-15 23:32:15 +01:00
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
Marco
4c084150c4 Update migration guide 2017-02-26 13:49:20 +01:00
Marco
dd51d2c07d Add tests for 'getStatus' and related methods in 'Auth' class 2017-02-26 13:45:26 +01:00
Marco
93477e4e7e Add shorthands for 'getStatus' in 'Auth' class 2017-02-26 13:45:00 +01:00
Marco
d59ac83d13 Refactor array definition in 'authenticateUserInternal' 2017-02-26 13:13:37 +01:00
Marco
9a0036b8a8 Add 'Status' class with constants 2017-02-26 13:05:37 +01:00
Marco
a05d277a2c Read status from 'users' table and provide read access in session 2017-02-26 13:03:52 +01:00
Marco
0839beefcb Improve readability of README 2017-02-26 11:54:41 +01:00
Marco
bf5db38361 Link to 'Migration' from README 2017-02-25 19:13:56 +01:00
Marco
d9be7a4c22 Add table of contents to 'Migration' 2017-02-25 18:59:15 +01:00
Marco
e9bae4a346 Reverse chronological order in 'Migration.md' 2017-02-25 18:49:27 +01:00
Marco
2317423550 Explain that constructor of 'Administration' is for internal use only 2017-02-25 18:40:49 +01:00
Marco
d9dccf8100 Add 'status' column to 'users' table 2017-02-25 18:13:55 +01:00
Marco
26ca48c3b9 Improve language 2017-02-25 18:07:02 +01:00
Marco
9ec74b3b2d Deprecate boolean format for parameter 'rememberDuration' with login 2017-02-25 18:06:07 +01:00
Marco
9c60acec0d Improve code style 2017-02-25 17:58:29 +01:00
Marco
94eeb9dbe0 Fix anchors in README 2017-02-25 17:35:38 +01:00
Marco
4dca8439d1 Add tests for method 'deleteUserById' and related methods 2017-02-25 17:34:36 +01:00
Marco
81bdd79906 Add method 'deleteUserById' and similar methods for email and username 2017-02-25 17:32:35 +01:00
Marco
63144d4dc0 Add private method 'deleteUsersByColumnValue' to 'Administration' 2017-02-25 17:14:24 +01:00
Marco
f06af42f87 Move method 'getUserDataByUsername' from 'Auth' to 'UserManager' 2017-02-25 16:18:51 +01:00
Marco
6c6f34935c Add tests for method 'createUser' in class 'Administration' 2017-02-25 15:45:58 +01:00
Marco
293c231003 Do not offer email verification when creating users as admin 2017-02-25 15:44:37 +01:00
Marco
05d72a849b Improve language 2017-02-21 10:02:49 +01:00
Marco
cf41c9a105 Add methods 'createUser' and 'createUserWithUniqueUsername' 2017-02-21 10:02:03 +01:00
Marco
da4bb583bf Add component for administrative tasks as class 'Administration' 2017-02-21 09:43:30 +01:00
Marco
d99979f270 Move method 'createUserInternal' from class 'Auth' to 'UserManager' 2017-02-21 09:28:42 +01:00
Marco
22872d55bd Import class 'IntegrityConstraintViolationException' in 'UserManager' 2017-02-21 09:27:54 +01:00
Marco
ff6d78942a Move method 'createConfirmationRequest' from 'Auth' to 'UserManager' 2017-02-21 09:26:10 +01:00
Marco
d27005df10 Import class 'Error' in 'UserManager' 2017-02-21 09:22:40 +01:00
Marco
ad2aa84e4a Move method 'validatePassword' from class 'Auth' to 'UserManager' 2017-02-21 09:19:09 +01:00
Marco
f7d50d53ea Move method 'validateEmailAddress' from class 'Auth' to 'UserManager' 2017-02-21 09:17:08 +01:00
Marco
e916c3d07e Move method 'createRandomString' from class 'Auth' to 'UserManager' 2017-02-21 09:13:39 +01:00
Marco
fdeff8a792 Emphasize that class 'UserManager' is for internal use only 2017-02-21 09:03:55 +01:00
Marco
43fa612d67 Move method 'throttle' and its constants from 'Auth' to 'UserManager' 2017-02-21 08:55:10 +01:00
Marco
0b0258f29a Manually require file 'Exceptions.php' in parent class as well 2017-02-21 08:45:27 +01:00
Marco
9252bee030 Let parent class 'UserManager' manage database connection for 'Auth' 2017-02-21 08:40:30 +01:00
Marco
6a15679238 Make class 'Base64' final 2017-02-21 08:28:14 +01:00
Marco
8ab08f41e1 Let autoloader fetch class 'Base64' instead of including it manually 2017-02-21 08:27:19 +01:00
Marco
83464c0be7 Improve description of 'Auth' class 2017-02-21 08:07:38 +01:00
Marco
b5c853388c Make class 'Auth' final 2017-02-21 08:01:41 +01:00
Marco
5585623e08 Let class 'Auth' extend abstract class 'UserManager' 2017-02-21 08:00:26 +01:00
Marco
a7d640154c Add abstract class 'UserManager' 2017-02-21 07:59:49 +01:00
Marco
8acd3a9779 Add tests for method 'loginWithUsername' in class 'Auth' 2017-02-20 21:46:36 +01:00
Marco
374f27176b Add tests for method 'registerWithUniqueUsername' in class 'Auth' 2017-02-20 21:42:48 +01:00
Marco
3cb2284870 Add public method 'loginWithUsername' to class 'Auth' 2017-02-20 21:36:45 +01:00
Marco
690485ba6d Add support for sign in via username to 'authenticateUserInternal' 2017-02-20 21:32:45 +01:00
Marco
495a87d499 No need to check for uniqueness of username if none has been provided 2017-02-20 20:52:02 +01:00
Marco
784030139b Treat empty string or whitespace-only string as non-existent username 2017-02-20 20:48:03 +01:00
Marco
fb6f3d31b8 Add private method 'getUserDataByUsername' to class 'Auth' 2017-02-20 19:57:23 +01:00
Marco
370ecc4933 Add class 'AmbiguousUsernameException' 2017-02-16 09:48:55 +01:00
Marco
da2d282648 Add class 'UnknownUsernameException' 2017-02-16 09:48:04 +01:00
Marco
4aaf85e3cf Add class 'EmailOrUsernameRequiredError' 2017-02-16 09:47:26 +01:00
Marco
f2561a1932 Re-use 'getUserDataByEmailAddress' in 'authenticateUserInternal' 2017-02-16 08:56:44 +01:00
Marco
8cc54473e3 Improve language of parameter name 2017-02-16 08:30:24 +01:00
Marco
f26f2209cd Store email address in session data as found in the database 2017-02-16 08:25:14 +01:00
Marco
188086f2e4 Do not validate password earlier than necessary 2017-02-16 08:18:48 +01:00
Marco
c6213a6081 Change order of parameters in 'authenticateUserInternal' 2017-02-16 08:15:48 +01:00
Marco
c55250c572 Refactor body of 'login' method into new 'authenticateUserInternal' 2017-02-15 18:29:15 +01:00
Marco
dac2850aba Add method 'registerWithUniqueUsername' 2017-02-15 17:19:16 +01:00
Marco
4268e3fcd5 Add support for unique username constraint in 'createUserInternal' 2017-02-15 17:11:56 +01:00
Marco
d579179494 Add class 'DuplicateUsernameException' 2017-02-15 17:06:08 +01:00
Marco
bd02e08f83 Refactor body of 'register' method into new 'createUserInternal' 2017-02-15 16:41:38 +01:00
Marco
d4fe11b844 Improve language 2017-02-15 16:39:17 +01:00
Marco
09fabd4c91 Improve notes in 'General advice' section of README 2017-01-31 23:49:36 +01:00
Marco
4dcf491ad9 Fix language in README 2017-01-30 19:33:04 +01:00
Marco
4f5ff151ef Improve installation instructions by excluding steps for manual setup 2017-01-30 19:31:45 +01:00
Marco
f5027c09e9 Add table of contents to 'Usage' section in README 2017-01-30 19:10:28 +01:00
Marco
6db82d1f65 Add guide on how to store and access additional user information 2017-01-30 19:04:11 +01:00
Marco
f944067aff Set 'ignore_user_abort' to 'true' in method 'register' 2017-01-30 18:48:23 +01:00
Marco
a640e8a5ad Define link to external documentation more precisely 2017-01-30 18:39:21 +01:00
Marco
2aee8a662e Move 'Reading and writing session data' below 'Utilities' in README 2017-01-30 18:37:47 +01:00
Marco
36ef710480 Group several sections in README under 'Accessing user information' 2017-01-30 18:24:44 +01:00
Marco
9187840767 Improve language in README 2017-01-30 18:23:30 +01:00
Marco
6bfa298836 Prevent usage of password reset if email has not been verified yet 2016-12-12 20:58:37 +01:00
Marco
6be456a27a Change 'getUserIdByEmailAddress' to 'getUserDataByEmailAddress' 2016-12-12 20:38:49 +01:00
Marco
78a16d8f50 Improve language 2016-12-04 17:27:58 +01:00
Marco
e669f6f017 Move documentation on 'remember me' to its own section 2016-12-04 17:24:21 +01:00
Marco
5aafd0b009 Improve language 2016-12-04 17:23:09 +01:00
Marco
d53a484c2e Improve language 2016-12-04 17:13:33 +01:00
Marco
07732dcaa9 Change 'remember me' for login from binary choice to custom interval 2016-12-04 17:05:57 +01:00
Marco
f486ab6763 Forget remembered sessions when passwords are reset or changed 2016-12-04 16:54:34 +01:00
Marco
5e331924f6 Increase entropy in tokens for remember directives 2016-12-04 16:52:18 +01:00
Marco
ac95be3714 Use dummy password (instead of no password at all) in examples 2016-12-04 16:49:09 +01:00
Marco
e6c8ae056c Ignore warnings for 'zend.assertions' that cannot be set 2016-12-04 16:46:05 +01:00
Marco
5bac29065d Improve documentation 2016-12-04 16:44:50 +01:00
Marco
36b590eb81 Update dependencies 2016-12-01 13:48:48 +01:00
Marco
5c6a71d921 Update migration guide 2016-09-15 23:52:24 +02:00
Marco
d94243f19d Update examples of how to provide a database connection 2016-09-15 23:51:29 +02:00
Marco
2a2d93f534 Improve exemplary database credentials 2016-09-15 23:45:35 +02:00
Marco
989c7940e5 Rewrite all SQL operations to use 'delight-im/db' instead of raw PDO 2016-09-15 23:43:40 +02:00
Marco
51a5735295 Require 'delight-im/db' as dependency 2016-09-14 16:54:54 +02:00
Marco
e5e465782b Update dependencies 2016-09-14 16:52:01 +02:00
Marco
83caa3e785 Improve list of requirements in README 2016-09-14 16:50:42 +02:00
Marco
f2a1aedf7a Change minimum required PHP version from 5.5.0 to 5.6.0 2016-09-14 16:49:13 +02:00
Marco
5c87e877db Import class 'Delight\Cookie\Session' 2016-09-14 16:42:52 +02:00
Marco
70842b4320 Import class 'Delight\Cookie\Cookie' 2016-09-14 16:42:00 +02:00
Marco
d527a82bfa Update documentation to include guide on password reset 2016-08-20 22:02:18 +02:00
Marco
31ae135740 Add method 'canResetPassword' 2016-08-20 22:00:41 +02:00
Marco
c5e3bd191d Postpone validation of new password in 'Auth#resetPassword' 2016-08-20 21:48:53 +02:00
Marco
53e1a5c1fc Add method 'resetPassword' 2016-08-20 21:09:56 +02:00
Marco
f3ca69010f Add method 'forgotPassword' 2016-08-20 21:09:34 +02:00
Marco
da8d22c599 Create internal method 'Auth#createPasswordResetRequest' 2016-08-20 21:00:49 +02:00
Marco
c993657f20 Improve PHPDoc 2016-08-20 20:57:48 +02:00
Marco
cce172442d Rename constant 2016-08-20 20:57:00 +02:00
Marco
aef2672942 Refactor validation of passwords 2016-08-20 20:55:50 +02:00
Marco
e0b69ee33c Update database schema 2016-08-20 20:51:38 +02:00
Marco
40a5518ba7 Rename parameters 2016-08-20 20:42:54 +02:00
Marco
2441ea2dc1 Improve PHPDoc 2016-08-20 20:39:29 +02:00
Marco
07f60d6610 Improve PHPDoc 2016-08-20 18:24:14 +02:00
Marco
35cc941f20 Add internal method 'Auth#getOpenPasswordResetRequests' 2016-08-20 18:07:18 +02:00
Marco
f4b464a6f8 Add internal method 'Auth#getUserIdByEmailAddress' 2016-08-20 18:06:36 +02:00
Marco
bfa5b5e6b1 Refactor announcement of exceeded request limit to the client 2016-08-20 18:04:01 +02:00
Marco
9d2d764ced Refactor validation of email addresses 2016-08-20 17:05:47 +02:00
Marco
f45e0f1cb4 Explain 'remember me' feature more clearly 2016-07-25 12:06:14 +02:00
15 changed files with 4771 additions and 738 deletions

View File

@@ -1,9 +1,21 @@
-- 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)
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
CREATE TABLE IF NOT EXISTS `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`username` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` tinyint(2) unsigned NOT NULL DEFAULT '0',
`verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
`resettable` tinyint(1) unsigned NOT NULL DEFAULT '1',
`roles_mask` int(10) unsigned NOT NULL DEFAULT '0',
`registered` int(10) unsigned NOT NULL,
`last_login` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
@@ -12,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` (
@@ -35,20 +49,23 @@ CREATE TABLE IF NOT EXISTS `users_remembered` (
CREATE TABLE IF NOT EXISTS `users_resets` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user` int(10) unsigned NOT NULL,
`selector` varchar(24) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`selector` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
KEY `user` (`user`)
KEY `user_expires` (`user`,`expires`)
) 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 */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

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,5 +1,159 @@
# 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:
```sql
ALTER TABLE `users` ADD COLUMN `status` TINYINT(2) UNSIGNED NOT NULL DEFAULT 0 AFTER `username`;
```
* The two classes `Auth` and `Base64` are now `final`, i.e. they can't be extended anymore, which has never been a good idea, anyway. If you still need to wrap your own methods around these classes, consider [object composition instead of class inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance).
## From `v3.x.x` to `v4.x.x`
* PHP 5.6.0 or higher is now required.
## From `v2.x.x` to `v3.x.x`
* The license has been changed from the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) to the [MIT License](https://opensource.org/licenses/MIT).
## From `v1.x.x` to `v2.x.x`
* The MySQL schema has been changed from charset `utf8` to charset `utf8mb4` and from collation `utf8_general_ci` to collation `utf8mb4_unicode_ci`. Use the statements below to update the database schema:
@@ -34,7 +188,3 @@
REPAIR TABLE users_throttling;
OPTIMIZE TABLE users_throttling;
```
## From `v2.x.x` to `v3.x.x`
* The license has been changed from the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) to the [MIT License](https://opensource.org/licenses/MIT).

1117
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,11 @@
"name": "delight-im/auth",
"description": "Authentication for PHP. Simple, lightweight and secure.",
"require": {
"php": ">=5.5.0",
"php": ">=5.6.0",
"ext-openssl": "*",
"delight-im/cookie": "^2.0"
"delight-im/base64": "^1.0",
"delight-im/cookie": "^3.1",
"delight-im/db": "^1.2"
},
"type": "library",
"keywords": [ "auth", "authentication", "login", "security" ],

105
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": "22e56875c7a1386807d5cf6ae01f50fa",
"content-hash": "b914ccd7ac15e1519d7a04b55dbe725e",
"content-hash": "54d541ae3c5ba25b0cc06688d2b65467",
"packages": [
{
"name": "delight-im/cookie",
"version": "v2.0.0",
"name": "delight-im/base64",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-Cookie.git",
"reference": "a746f4096885b6715a640a2122b1c21324624f8f"
"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/a746f4096885b6715a640a2122b1c21324624f8f",
"reference": "a746f4096885b6715a640a2122b1c21324624f8f",
"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,7 +86,48 @@
"samesite",
"xss"
],
"time": "2016-07-21 15:20:20"
"time": "2017-10-18T19:48:59+00:00"
},
{
"name": "delight-im/db",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-DB.git",
"reference": "df99ef7c2e86c7ce206647ffe8ba74447c075b57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-DB/zipball/df99ef7c2e86c7ce206647ffe8ba74447c075b57",
"reference": "df99ef7c2e86c7ce206647ffe8ba74447c075b57",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=5.6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Db\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Safe and convenient SQL database access in a driver-agnostic way",
"homepage": "https://github.com/delight-im/PHP-DB",
"keywords": [
"database",
"mysql",
"pdo",
"pgsql",
"postgresql",
"sql",
"sqlite"
],
"time": "2017-03-18T20:51:59+00:00"
},
{
"name": "delight-im/http",
@@ -82,7 +163,7 @@
"http",
"https"
],
"time": "2016-07-21 15:05:01"
"time": "2016-07-21T15:05:01+00:00"
}
],
"packages-dev": [],
@@ -92,7 +173,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.5.0",
"php": ">=5.6.0",
"ext-openssl": "*"
},
"platform-dev": []

571
src/Administration.php Normal file
View File

@@ -0,0 +1,571 @@
<?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;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
require_once __DIR__ . '/Exceptions.php';
/** Component that can be used for administrative tasks by privileged and authorized users */
final class Administration extends UserManager {
/**
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
* @param string|null $dbTablePrefix (optional) the prefix for the names of all database tables used by this component
*/
public function __construct($databaseConnection, $dbTablePrefix = null) {
parent::__construct($databaseConnection, $dbTablePrefix);
}
/**
* Creates a new user
*
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address was invalid
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function createUser($email, $password, $username = null) {
return $this->createUserInternal(false, $email, $password, $username, null);
}
/**
* Creates a new user while ensuring that the username is unique
*
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address was invalid
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws DuplicateUsernameException if the specified username wasn't unique
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function createUserWithUniqueUsername($email, $password, $username = null) {
return $this->createUserInternal(true, $email, $password, $username, null);
}
/**
* Deletes the user with the specified ID
*
* This action cannot be undone
*
* @param int $id the ID of the user to delete
* @throws UnknownIdException if no user with the specified ID has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function deleteUserById($id) {
$numberOfDeletedUsers = $this->deleteUsersByColumnValue('id', (int) $id);
if ($numberOfDeletedUsers === 0) {
throw new UnknownIdException();
}
}
/**
* Deletes the user with the specified email address
*
* This action cannot be undone
*
* @param string $email the email address of the user to delete
* @throws InvalidEmailException if no user with the specified email address has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function deleteUserByEmail($email) {
$email = self::validateEmailAddress($email);
$numberOfDeletedUsers = $this->deleteUsersByColumnValue('email', $email);
if ($numberOfDeletedUsers === 0) {
throw new InvalidEmailException();
}
}
/**
* Deletes the user with the specified username
*
* This action cannot be undone
*
* @param string $username the username of the user to delete
* @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 AuthError if an internal problem occurred (do *not* catch)
*/
public function deleteUserByUsername($username) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->deleteUsersByColumnValue('id', (int) $userData['id']);
}
/**
* 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();
}
}
/**
* Changes the password for the user with the given ID
*
* @param int $userId the ID of the user whose password to change
* @param string $newPassword the new password to set
* @throws UnknownIdException if no user with the specified ID has been found
* @throws InvalidPasswordException if the desired new password has been invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePasswordForUserById($userId, $newPassword) {
$userId = (int) $userId;
$newPassword = self::validatePassword($newPassword);
$this->updatePasswordInternal(
$userId,
$newPassword
);
$this->deleteRememberDirectiveForUserById($userId);
}
/**
* Changes the password for the user with the given username
*
* @param string $username the username of the user whose password to change
* @param string $newPassword the new password to set
* @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 InvalidPasswordException if the desired new password has been invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePasswordForUserByUsername($username, $newPassword) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->changePasswordForUserById(
(int) $userData['id'],
$newPassword
);
}
/**
* Deletes all existing users 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
* @return int the number of deleted users
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function deleteUsersByColumnValue($columnName, $columnValue) {
try {
return $this->db->delete(
$this->dbTablePrefix . 'users',
[
$columnName => $columnValue
]
);
}
catch (Error $e) {
throw new DatabaseError();
}
}
/**
* 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 = ($users !== null) ? \count($users) : 0;
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;
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

@@ -10,8 +10,12 @@ namespace Delight\Auth;
class AuthException extends \Exception {}
class UnknownIdException extends AuthException {}
class InvalidEmailException extends AuthException {}
class UnknownUsernameException extends AuthException {}
class InvalidPasswordException extends AuthException {}
class EmailNotVerifiedException extends AuthException {}
@@ -26,10 +30,24 @@ class TokenExpiredException extends AuthException {}
class TooManyRequestsException extends AuthException {}
class DuplicateUsernameException extends AuthException {}
class AmbiguousUsernameException extends AuthException {}
class AttemptCancelledException extends AuthException {}
class ResetDisabledException extends AuthException {}
class ConfirmationRequestNotFound extends AuthException {}
class AuthError extends \Exception {}
class DatabaseError extends AuthError {}
class DatabaseDriverError extends DatabaseError {}
class MissingCallbackError extends AuthError {}
class HeadersAlreadySentError extends AuthError {}
class EmailOrUsernameRequiredError extends AuthError {}

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() {}
}

20
src/Status.php Normal file
View File

@@ -0,0 +1,20 @@
<?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 Status {
const NORMAL = 0;
const ARCHIVED = 1;
const BANNED = 2;
const LOCKED = 3;
const PENDING_REVIEW = 4;
const SUSPENDED = 5;
}

384
src/UserManager.php Normal file
View File

@@ -0,0 +1,384 @@
<?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;
use Delight\Base64\Base64;
use Delight\Cookie\Session;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
use Delight\Db\Throwable\IntegrityConstraintViolationException;
require_once __DIR__ . '/Exceptions.php';
/**
* Abstract base class for components implementing user management
*
* @internal
*/
abstract class UserManager {
/** @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
*
* With the default parameter, the output should contain at least as much randomness as a UUID
*
* @param int $maxLength the maximum length of the output string (integer multiple of 4)
* @return string the new random string
*/
public static function createRandomString($maxLength = 24) {
// calculate how many bytes of randomness we need for the specified string length
$bytes = \floor((int) $maxLength / 4) * 3;
// get random data
$data = \openssl_random_pseudo_bytes($bytes);
// return the Base64-encoded result
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, $dbTablePrefix = null) {
if ($databaseConnection instanceof PdoDatabase) {
$this->db = $databaseConnection;
}
elseif ($databaseConnection instanceof PdoDsn) {
$this->db = PdoDatabase::fromDsn($databaseConnection);
}
elseif ($databaseConnection instanceof \PDO) {
$this->db = PdoDatabase::fromPdo($databaseConnection, true);
}
else {
$this->db = null;
throw new \InvalidArgumentException('The database connection must be an instance of either `PdoDatabase`, `PdoDsn` or `PDO`');
}
$this->dbTablePrefix = (string) $dbTablePrefix;
}
/**
* Creates a new user
*
* If you want the user's account to be activated by default, pass `null` as the callback
*
* If you want to make the user verify their email address first, pass an anonymous function as the callback
*
* The callback function must have the following signature:
*
* `function ($selector, $token)`
*
* Both pieces of information must be sent to the user, usually embedded in a link
*
* When the user wants to verify their email address as a next step, both pieces will be required again
*
* @param bool $requireUniqueUsername whether it must be ensured that the username is unique
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @param callable|null $callback (optional) the function that sends the confirmation email to the user
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address has been invalid
* @throws InvalidPasswordException if the password has been invalid
* @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) {
\ignore_user_abort(true);
$email = self::validateEmailAddress($email);
$password = self::validatePassword($password);
$username = isset($username) ? \trim($username) : null;
// if the supplied username is the empty string or has consisted of whitespace only
if ($username === '') {
// this actually means that there is no username
$username = null;
}
// if the uniqueness of the username is to be ensured
if ($requireUniqueUsername) {
// if a username has actually been provided
if ($username !== null) {
// count the number of users who do already have that specified username
$occurrencesOfUsername = $this->db->selectValue(
'SELECT COUNT(*) FROM ' . $this->dbTablePrefix . 'users WHERE username = ?',
[ $username ]
);
// if any user with that username does already exist
if ($occurrencesOfUsername > 0) {
// cancel the operation and report the violation of this requirement
throw new DuplicateUsernameException();
}
}
}
$password = \password_hash($password, \PASSWORD_DEFAULT);
$verified = \is_callable($callback) ? 0 : 1;
try {
$this->db->insert(
$this->dbTablePrefix . 'users',
[
'email' => $email,
'password' => $password,
'username' => $username,
'verified' => $verified,
'registered' => \time()
]
);
}
// if we have a duplicate entry
catch (IntegrityConstraintViolationException $e) {
throw new UserAlreadyExistsException();
}
catch (Error $e) {
throw new DatabaseError();
}
$newUserId = (int) $this->db->getLastInsertId();
if ($verified === 0) {
$this->createConfirmationRequest($newUserId, $email, $callback);
}
return $newUserId;
}
/**
* Updates the given user's password by setting it to the new specified password
*
* @param int $userId the ID of the user whose password should be updated
* @param string $newPassword the new password
* @throws UnknownIdException if no user with the specified ID has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function updatePasswordInternal($userId, $newPassword) {
$newPassword = \password_hash($newPassword, \PASSWORD_DEFAULT);
try {
$affected = $this->db->update(
$this->dbTablePrefix . 'users',
[ 'password' => $newPassword ],
[ 'id' => $userId ]
);
if ($affected === 0) {
throw new UnknownIdException();
}
}
catch (Error $e) {
throw new DatabaseError();
}
}
/**
* 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)
*
* You must never pass untrusted input to the parameter that takes the column list
*
* @param string $username the username to look for
* @param array $requestedColumns the columns to request from the user's record
* @return array the user data (if an account was found unambiguously)
* @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 AuthError if an internal problem occurred (do *not* catch)
*/
protected function getUserDataByUsername($username, array $requestedColumns) {
try {
$projection = \implode(', ', $requestedColumns);
$users = $this->db->select(
'SELECT ' . $projection . ' FROM ' . $this->dbTablePrefix . 'users WHERE username = ? LIMIT 2 OFFSET 0',
[ $username ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
if (empty($users)) {
throw new UnknownUsernameException();
}
else {
if (\count($users) === 1) {
return $users[0];
}
else {
throw new AmbiguousUsernameException();
}
}
}
/**
* Validates an email address
*
* @param string $email the email address to validate
* @return string the sanitized email address
* @throws InvalidEmailException if the email address has been invalid
*/
protected static function validateEmailAddress($email) {
if (empty($email)) {
throw new InvalidEmailException();
}
$email = \trim($email);
if (!\filter_var($email, \FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
return $email;
}
/**
* Validates a password
*
* @param string $password the password to validate
* @return string the sanitized password
* @throws InvalidPasswordException if the password has been invalid
*/
protected static function validatePassword($password) {
if (empty($password)) {
throw new InvalidPasswordException();
}
$password = \trim($password);
if (\strlen($password) < 1) {
throw new InvalidPasswordException();
}
return $password;
}
/**
* Creates a request for email confirmation
*
* The callback function must have the following signature:
*
* `function ($selector, $token)`
*
* Both pieces of information must be sent to the user, usually embedded in a link
*
* 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)
*/
protected function createConfirmationRequest($userId, $email, callable $callback) {
$selector = self::createRandomString(16);
$token = self::createRandomString(16);
$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
$expires = \time() + 60 * 60 * 24;
try {
$this->db->insert(
$this->dbTablePrefix . 'users_confirmations',
[
'user_id' => (int) $userId,
'email' => $email,
'selector' => $selector,
'token' => $tokenHashed,
'expires' => $expires
]
);
}
catch (Error $e) {
throw new DatabaseError();
}
if (\is_callable($callback)) {
$callback($selector, $token);
}
else {
throw new MissingCallbackError();
}
}
/**
* Clears an existing directive that keeps the user logged in ("remember me")
*
* @param int $userId the ID of the user who shouldn't be kept signed in anymore
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function deleteRememberDirectiveForUserById($userId) {
try {
$this->db->delete(
$this->dbTablePrefix . 'users_remembered',
[ 'user' => $userId ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
}
}

File diff suppressed because it is too large Load Diff