Compare commits

...

297 Commits

Author SHA1 Message Date
Andrew Millington
0b0b43d433 Merge pull request #1035 from matt-allan/prevent-public-client-confidential-grant
Prevent public clients from using the client_credentials grant type
2019-07-25 19:20:11 +01:00
Andrew Millington
cd8742f630 Reword changelog 2019-07-25 19:14:08 +01:00
Andrew Millington
2097edd6eb Update changelog 2019-07-25 19:12:33 +01:00
Andrew Millington
705120c974 Add blank space to keep formatting consistent 2019-07-25 19:09:47 +01:00
Andrew Millington
8a78e00a2e Add blank line above throw 2019-07-25 19:04:44 +01:00
Matt Allan
3413c20590 Prevent public clients from using the client_credentials grant type
See https://tools.ietf.org/html/rfc6749#section-4.4.2
2019-07-22 18:21:29 -04:00
Andrew Millington
e1dc4d708c Update changelog for release of version 8 2019-07-13 19:58:26 +01:00
Andrew Millington
18dabd36e3 Remove branch 8.0.0 from travis checks 2019-07-13 19:56:44 +01:00
Andrew Millington
1a3107b4fc Merge pull request #1033 from thephpleague/8.0.0
8.0.0
2019-07-13 19:46:10 +01:00
Andrew Millington
1d9ca35fec Merge pull request #1032 from thephpleague/update-examples-for-version-8
Update Examples for Version 8
2019-07-13 19:39:38 +01:00
Andrew Millington
c7f998ee02 Add PR number for JTI PR to changelog 2019-07-13 18:03:24 +01:00
Andrew Millington
4b1c9ed503 Merge pull request #1031 from Sephster/remove-jti-from-header
Remove JTI Claim From JWT Header
2019-07-13 17:50:20 +01:00
Andrew Millington
dc3c74601a Update changelog 2019-07-13 17:52:35 +01:00
Andrew Millington
f5e910e6ec Remove jti replication from JWT Header 2019-07-13 17:51:56 +01:00
Andrew Millington
2b7923c593 Fix inheritdoc case 2019-07-13 17:49:26 +01:00
Andrew Millington
3f95c0d11e Update validateClient arguments list 2019-07-13 17:40:38 +01:00
Andrew Millington
4be97e6fd0 Update composer dependencies and remove mustValidateSecret 2019-07-13 17:37:45 +01:00
Andrew Millington
aba5353257 Add validateClient() function to ClientRepository 2019-07-13 17:31:09 +01:00
sephster
7f0879b8b4 Change header type 2019-07-02 22:52:13 +01:00
sephster
cb9aa25c89 Re-add removed changelog instances 2019-07-02 22:51:12 +01:00
sephster
a6a499f8fb Remove Simon Hamp from README 2019-07-02 22:20:37 +01:00
Andrew Millington
ccf36588ee Merge pull request #1024 from Sephster/update-dependencies
Update Dependencies
2019-07-02 22:15:29 +01:00
sephster
6b2a3db185 Removing php stan strict rules 2019-07-02 22:10:17 +01:00
sephster
1a6ebdf81c Fix order of imports 2019-07-02 19:24:19 +01:00
sephster
46c86ed5b1 Apply style fix 2019-07-02 19:21:13 +01:00
sephster
a92a274d15 Use reflection instead of extension in test 2019-07-02 19:09:47 +01:00
sephster
c4c354e2df Fix phpstan issues 2019-07-01 19:17:43 +01:00
sephster
7bc1ec643e Remove unused import 2019-06-27 13:24:58 +01:00
sephster
51b97f87c1 Fix issues setting attributes on requests 2019-06-27 13:15:37 +01:00
sephster
e3b23fa826 Update dependencies and fix PHPUnit tests 2019-06-27 12:54:22 +01:00
Andrew Millington
bac79a26a8 Merge pull request #1010 from iansltx/protect-client-entity-gets
Ensure unvalidated ClientEntity gets throw/emit if they return null
2019-06-23 13:54:14 -04:00
sephster
012808f094 Update changelog 2019-06-23 17:56:32 +01:00
sephster
0db54cf1e5 Reinstate use for ClientEntityInterface 2019-06-23 17:40:39 +01:00
sephster
c7d047f7f5 Remove extra line spaces 2019-06-23 17:35:24 +01:00
sephster
e1324b88b2 Merge remote-tracking branch 'upstream/8.0.0' into protect-client-entity-gets 2019-06-23 17:23:40 +01:00
Andrew Millington
c60e8e3581 Merge pull request #1011 from iansltx/readme-release-notes-73-cleanup
Update release notes, clean up readme, add PHP 7.3 test for v8
2019-06-18 17:41:52 -04:00
sephster
e0ee244506 Remove duplicate entries from changelog 2019-06-18 22:30:17 +01:00
Ian Littman
8b5841870f Add more detail/precision to 8.0.0 changelog around breaking changes 2019-05-19 21:01:46 -05:00
Ian Littman
048e45d8cd Add more recent 7.x releases to changelog 2019-05-19 21:01:46 -05:00
Ian Littman
bf75596989 Update security contact email to current maintainer 2019-05-19 21:01:41 -05:00
Ian Littman
c5cfc0a371 Remove dead Commercial Support link 2019-05-19 20:52:28 -05:00
Andrew Millington
5ab4323856 Merge pull request #1014 from Sephster/drop-php-70-support
Drop php 7.0 support
2019-05-14 21:42:12 +01:00
sephster
28709f300f Add pull request number to changelog 2019-05-14 21:30:30 +01:00
sephster
bd483d701b Remove support for PHP 7.0 2019-05-14 21:26:17 +01:00
Andrew Millington
3dc324af6e Merge pull request #1013 from Sephster/8.0.0
Add The Latest Changes from Master into 8.0.0 Branch
2019-05-14 21:09:46 +01:00
sephster
17923634bf Set private keys in tests 2019-05-14 20:56:54 +01:00
sephster
a1cf22a3a9 Remove duplicate setting of expirydatetime 2019-05-14 16:11:34 +01:00
sephster
86d1581cd9 Remove unused imports 2019-05-14 15:57:13 +01:00
sephster
521ed9a8cb Merge master into 8.0.0 branch 2019-05-14 15:46:01 +01:00
Andrew Millington
1bbcb57d63 Merge pull request #1009 from iansltx/skip-s256-if-not-installed
Skip SHA256 verifier if system doesn't support sha256
2019-05-14 14:55:39 +01:00
Andrew Millington
93d4b947d8 Merge pull request #1008 from iansltx/typehints-and-exts
Typehint ServerRequestInterface on OAuthServerException, explicitly require ext-json
2019-05-13 10:25:32 +01:00
Ian Littman
27d5c5ed8d Ensure unvalidated ClientEntity gets throw/emit if they return null
In many cases, we validate client info before pulling from client itself
from the repository, in which case it's safe to assume that you can grab
the client once validation passes. However on implicit/auth code grants
we don't have this guarantee due to non-confidential clients that just
reference the client ID. In those cases the client may supply a client
ID that doesn't exist, and we don't do a validation step before pulling
it from the repo.

The issue with that is that ClientRepository doesn't actually enforce
returning a ClientInterface via typehint, nor does it even suggest an
exception to throw if the client doesn't exist. So in most places we
do an instanceof check after the repository returns and throw/emit an
error event if the client doesn't exist.

This approach ends up being a bit error-prone; we missed one case where
we should've been doing this check: in the access token request on an
auth code grant. We don't do enough validation beforehand to assume that
the incoming request has an accurate client ID, so L96 could absolutely
be a method call on a non-object.

This commit centralizes the return-check-emit-throw logic so it's a
one-liner for wherever we need it, including the access token request
processor for auth code grants.
2019-05-11 14:35:59 -05:00
Ian Littman
4ecd3131c1 Skip SHA256 verifier if system doesn't support sha256 2019-05-11 14:23:56 -05:00
Ian Littman
3fdfbe11f6 Explicitly require ext-json
Makes phpstorm happier; take or leave
2019-05-11 13:37:22 -05:00
Ian Littman
42df2d9c47 Add typehints to OAuthServerException calls 2019-05-11 13:35:24 -05:00
Andrew Millington
2eb1cf79e5 Update changelog for version 7.4.0 2019-05-05 10:22:01 +01:00
Andrew Millington
382b6f5fbf Merge pull request #1000 from filecage/master
Optional Refresh Tokens
2019-05-05 09:48:53 +01:00
sephster
86869eafbb Add whitespace around control blocks 2019-05-05 09:03:13 +01:00
sephster
9236e842d9 Clarify changelog message 2019-05-05 08:58:34 +01:00
filecage
9bc7f6c8c5 removing simplified_null_return 2019-04-29 19:13:26 +02:00
David
1e9a468e66 Merge branch 'master' into master 2019-04-12 11:17:37 +02:00
Andrew Millington
c7f4998497 Update links 2019-03-29 18:19:35 +00:00
Andrew Millington
0a78236f17 Update changelog for version 7.3.3 2019-03-29 18:18:35 +00:00
Andrew Millington
a68f8001a4 Merge pull request #1006 from marc-mabe/fix-958-error_description
spec compliant 'error_description' but keep 'message' for BC
2019-03-29 16:28:33 +00:00
Marc Bennewitz
b88198a9a4 spec compliant 'error_description' but keep 'message' for BC 2019-03-29 16:00:26 +01:00
filecage
8cf39fd9cd applies style CI diff 2019-03-16 13:15:38 +01:00
filecage
6f6820f629 removes @var hints
the @var hints make PHP stan fail together with PHPUnit 6.3
2019-03-16 13:12:34 +01:00
filecage
0742d5150c explicit is better than implicit :) 2019-03-13 10:08:57 +01:00
filecage
64f0d89fad getNewRefreshToken() can also return NULL 2019-03-11 23:28:47 +01:00
filecage
ebf78132d7 refreshTokenRepository parameter can not be null, condition is obsolete 2019-03-11 23:28:20 +01:00
filecage
aa5bbe5f06 boyscout: style CI tweaks 2019-03-11 23:26:35 +01:00
filecage
66d4ce6de8 Update CHANGELOG.md 2019-03-08 18:21:55 +01:00
filecage
2ea76ca4fd Adds handling for null issued refresh token to Grant implementations 2019-03-08 18:19:16 +01:00
filecage
b2840474fd AbstractGrant no longer tries to issue a refresh token if the Repository returned null 2019-03-08 18:16:16 +01:00
Andrew Millington
0227f14b7b Merge pull request #988 from lordrhodos/feature/test-cleanup
Cleanup: remove unused local variable $scopeEntity from ImplicitGrantTest
2019-01-22 20:59:33 +00:00
Patrick Rodacker
fad42a88fd removes unused local variable $scopeEntity from ImplicitGrantTest 2019-01-20 22:11:22 +01:00
Andrew Millington
d7defafd83 Merge pull request #963 from marc-mabe/date-time-handling
BC-Break: cleanup DateTime handline for 8.0.0
2018-12-19 13:10:20 +00:00
sephster
16f37560d4 Merge latest version of 8 branch 2018-12-19 13:03:10 +00:00
sephster
5ed8e59ef3 Update changelog 2018-12-19 12:58:11 +00:00
sephster
c2cd12e0b8 Remove return types 2018-12-19 12:54:26 +00:00
Andrew Millington
8e9368cf44 Merge pull request #978 from Devristo/fix-http-basic-respond-to-access-token-request
Fixed respondToAccessTokenRequest using Http Basic Auth
2018-12-10 23:07:58 +00:00
sephster
894724c45b Remove invalid commenting 2018-12-10 23:01:45 +00:00
sephster
fd65bf9e54 Streamline tests 2018-12-10 22:51:58 +00:00
Andrew Millington
2a16dbeb7f Merge pull request #981 from Sephster/support-php-7.3
Add support for PHP 7.3
2018-12-06 23:53:55 +00:00
sephster
faa350792a Add support for PHP 7.3 2018-12-06 23:46:28 +00:00
Chris Tanaskoski
b6955a6c65 Fixed respondToAccessTokenRequest such that it accepts client_id through request body and Http Basic Auth 2018-11-30 10:19:06 +01:00
Chris Tanaskoski
ec8a663a81 Added test for respondToAccessTokenRequest using Http Basic Auth for client credentials 2018-11-29 09:28:36 +01:00
Andrew Millington
dc3181bbb0 Merge pull request #977 from spideyfusion/symfony-community-integration
Add Symfony community integration to README.md
2018-11-28 12:47:30 +00:00
Petar Obradović
1e3a7adb19 Add Symfony community integration to README.md 2018-11-28 12:24:16 +01:00
sephster
b71f382cd7 Update changelog 2018-11-21 21:42:43 +00:00
Andrew Millington
9783388523 Merge pull request #969 from ceeram/fix-bc-break
Fix bc breaking change
2018-11-21 21:38:37 +00:00
sephster
46493c461e Update changelog for 7.3.2 release 2018-11-21 21:29:55 +00:00
sephster
8b421818f2 Add blank line to better format 2018-11-21 21:26:54 +00:00
Marc Ypes
b09154af33 Add test to prove bc break 2018-11-16 13:29:47 +01:00
Marc Ypes
f1454cde36 Fix bc breaking change 2018-11-16 12:44:41 +01:00
Andrew Millington
f2cd3646ff Merge pull request #970 from Sephster/interface-revert
Revert Interface Change
2018-11-15 22:37:18 +00:00
sephster
7839a61170 Update changelog 2018-11-15 22:33:34 +00:00
sephster
443d7c485a Revert interface change so class can be extende 2018-11-15 22:22:08 +00:00
Andrew Millington
a61c6a318a Update changelog for 7.3.0 release 2018-11-13 20:17:20 +00:00
Andrew Millington
eea9c30e70 Merge pull request #967 from Sephster/password-grant-use-invalid-grant
Password Grant Should Issue an invalid_grant Error When Credentials are Incorrect
2018-11-13 18:28:09 +00:00
sephster
a936962716 Update changelog 2018-11-13 18:27:03 +00:00
sephster
685dc6edea Update test 2018-11-13 18:19:20 +00:00
sephster
2b4974b697 Change to use invalid_grant 2018-11-13 18:18:07 +00:00
sephster
94e75ba6f3 Fix bug 2018-11-13 12:56:06 +00:00
Andrew Millington
efa8ef6fce Merge pull request #965 from ceeram/add-previous
Include previous exception in catch and throw
2018-11-13 12:44:43 +00:00
sephster
7982275757 Fix docblock alignment 2018-11-13 12:34:16 +00:00
sephster
f6c1070ccc Add use Throwable 2018-11-13 12:32:52 +00:00
sephster
d64fb3f526 Merge master into this branch 2018-11-13 12:28:39 +00:00
Andrew Millington
95a9f4649d Merge pull request #966 from ceeram/fully_qualified_strict_types
Replace fqn with unqualified name
2018-11-13 11:58:31 +00:00
Marc Ypes
4bb5b747c1 Replace fqn with unqualified name 2018-11-13 01:33:11 +01:00
Marc Ypes
5868996961 Add test for previous exceptions 2018-11-13 00:25:22 +01:00
sephster
9542af627e Update changelog 2018-11-12 19:57:35 +00:00
Marc Ypes
3b983ad0b4 Include previous exception in catch and throw 2018-11-12 13:58:31 +01:00
sephster
34ec35019b Remove additional whitespace 2018-11-08 13:10:22 +00:00
Marc Bennewitz
16f9de86f2 cleanup DateTime handline
* DateTime -> DateTimeImmutable
* DateTime::format('U') -> DateTime::getTimestamp()
* (new DateTime())->getTimestamp() -> time()
2018-11-08 12:45:18 +01:00
Andrew Millington
ac818bd921 Minor formatting adjustment 2018-11-06 21:42:05 +00:00
Andrew Millington
73698e28d9 Update CHANGELOG.md 2018-11-06 21:38:31 +00:00
Andrew Millington
c87be9477c Merge pull request #960 from marc-mabe/stateless-server
Make AuthorizationServer stateless
2018-11-06 21:34:26 +00:00
Marc Bennewitz
d288a2ad8a Make AuthorizationServer stateless 2018-11-05 09:08:02 +01:00
Andrew Millington
a34f5dd7db Merge pull request #953 from Sephster/code-tidyup
Code Tidyup
2018-10-13 17:06:21 +01:00
sephster
c0efdf0dd0 Revert changes to throws and returns ordering 2018-10-13 16:54:31 +01:00
sephster
f96fca3b48 Minor code tidyup 2018-10-13 16:44:40 +01:00
sephster
20b355b025 Re-order docblock throws 2018-10-13 16:31:36 +01:00
sephster
793f65d3a3 Remove unused scope entity interface 2018-10-13 16:14:15 +01:00
sephster
322b55eddf Remove getScopes function and use validateScopes instead 2018-10-13 16:11:44 +01:00
sephster
50ab9dd8ac Remove unused import 2018-10-13 15:28:39 +01:00
sephster
b624124d5a Chaneg param types to satisfy PHPStan 2018-10-13 15:25:49 +01:00
sephster
dbf2b55bc5 Fix docblock alignment 2018-10-13 15:16:50 +01:00
sephster
b11d628e8b Change docblock type for 2018-10-13 14:49:29 +01:00
sephster
0515129c9c Fix coding standards issues 2018-10-13 14:37:36 +01:00
sephster
50566cdc87 Reduce complexity of respondToAccessTokenRequest 2018-10-13 14:34:35 +01:00
sephster
b4d88995de Add throws tag for DateInterval exception 2018-10-13 13:42:27 +01:00
sephster
398029be56 Add roave security advisories 2018-10-13 13:35:36 +01:00
sephster
30ed221481 Add Andy to authors list 2018-10-13 13:27:31 +01:00
sephster
939c0619d0 Shorten variable name 2018-10-13 13:22:27 +01:00
sephster
4042a31159 Update composer dependency versions 2018-10-13 13:17:45 +01:00
Andrew Millington
0bdd02cdb4 Merge pull request #952 from Sephster/add-scope-entity-trait
Add Scope Entity Trait
2018-10-13 13:14:33 +01:00
sephster
7bf7700645 Update changelog 2018-10-12 23:32:44 +01:00
sephster
d76025d613 Change example to use scope trait 2018-10-12 23:23:24 +01:00
sephster
d6792c1662 Add scope entity trait 2018-10-12 23:05:07 +01:00
Andrew Millington
9882f6716c Merge pull request #923 from christiaangoossens/fix_implicit_grant_scopes
ImplicitGrant finalizes scopes without user identifier
2018-09-23 18:31:53 +01:00
sephster
71c605117a Add missing word 2018-09-23 18:31:26 +01:00
sephster
6bc6ac09d2 Update changelog 2018-09-23 18:30:14 +01:00
Andrew Millington
fe421878e6 Merge pull request #938 from Sephster/force-pkce-for-public-clients
Force PKCE for public clients
2018-09-21 20:50:46 +01:00
sephster
0c2356a508 Fix file names for code challenge verifier tests 2018-09-21 20:43:04 +01:00
sephster
9645119ccb Fix missing comma 2018-09-21 20:35:04 +01:00
sephster
da2742bea7 Add details on client validation changes 2018-09-21 20:32:47 +01:00
sephster
efb5ce5e2a Update changelog 2018-09-21 20:29:27 +01:00
sephster
1ddc27e792 Add code challenge verifier tests 2018-09-21 20:12:05 +01:00
Andrew Millington
b7b7dda28c Merge pull request #945 from ezimuel/add-expressive-readme
Added Expressive in the community integration of README
2018-09-19 11:28:31 +01:00
Enrico Zimuel
ef864b5cba Added Expressive in the community integration of README 2018-09-19 12:02:06 +02:00
sephster
fcd6eb8a3c Fix variable name 2018-09-18 18:01:24 +01:00
sephster
133d9cc97a Fix missing 2018-09-18 17:51:11 +01:00
Andrew Millington
592dd2f433 Fix typo in function name 2018-09-17 20:10:26 +01:00
sephster
4a464dd336 Fix coding standard issue 2018-09-17 12:49:37 +01:00
sephster
970df8f34b Add code challenge verifiers 2018-09-17 12:48:32 +01:00
sephster
6a1645aebc Start to add code challenge verifier interfaces 2018-09-14 18:56:22 +01:00
sephster
e3e7abf41e Set default isConfidential to false for client entity 2018-09-03 13:09:52 +01:00
sephster
d831868d58 Fix getClientEntity parameters 2018-09-02 16:27:31 +01:00
sephster
36bf4ff8f2 Fix accidental paste of code 2018-09-02 16:19:47 +01:00
sephster
07ebe43b91 Change else if to elseif 2018-09-02 16:17:34 +01:00
sephster
5d3d9d95be Remove extra line 2018-09-02 15:46:59 +01:00
sephster
e85a8e31e8 Remove assignment as not needed 2018-09-02 14:58:02 +01:00
sephster
de899fbe0a Fix incorrect usage of isConfidential 2018-09-01 15:05:12 +01:00
sephster
3eabbafe5b Client says if it is confidential instead of repository 2018-09-01 14:53:27 +01:00
sephster
cfa9b8d3b4 Move grant check for client back to validate method 2018-09-01 14:38:31 +01:00
sephster
060a090479 Change tests to use validClient instead of getClientEntity 2018-09-01 14:26:22 +01:00
sephster
46c2f99b06 Change function name to be more explicit 2018-09-01 13:17:36 +01:00
Andrew Millington
27b956c149 Merge pull request #932 from JasonTheAdams/patch-1
Added docbloc to UniqueTokenIdentifierConstraintViolationException
2018-08-19 11:39:03 +01:00
Jason Adams
6949a007e5 Added docbloc to UniqueTokenIdentifierConstraintViolationException 2018-08-18 16:57:31 -07:00
sephster
74495cac49 Set proper confidential settings in existing tests 2018-08-16 12:59:10 +01:00
Andrew Millington
fb43801458 Change function name to setConfidential() 2018-08-15 21:40:41 +01:00
sephster
8ab27ede39 Add test to ensure public clients are asked to provide a code challenge 2018-08-13 22:54:12 +01:00
sephster
0105a20126 Reverted tests to remove isConfidential check 2018-08-13 22:00:34 +01:00
sephster
491852b521 Move code challenge check to auth code request 2018-08-13 21:47:53 +01:00
sephster
7f2fd7b22c Add set confidential to clients for tests 2018-08-13 21:21:59 +01:00
Andrew Millington
abef682031 Add setIsConfidential to client stub for tests 2018-08-12 20:34:58 +01:00
Andrew Millington
04807a1e2a Fix incorrect variable reference 2018-08-12 20:29:39 +01:00
Andrew Millington
d07b5a4a03 Add isConfidential function to client entity trait 2018-08-12 20:26:46 +01:00
Andrew Millington
838f206832 Tidy up comments 2018-08-12 20:09:55 +01:00
Andrew Millington
972808561d Add optional code challenge check for public clients 2018-08-12 20:06:34 +01:00
Andrew Millington
5ad00b0e33 Remove enableCodeExchangeProof function 2018-07-29 22:34:37 +01:00
Andrew Millington
f49cc65c13 Change to store code challenge and method whenever sent for PKCE 2018-07-29 19:56:30 +01:00
Christiaan Goossens
acf16e924a Actually use finalizedScopes in access token 2018-07-13 13:11:18 +02:00
Christiaan Goossens
a479b5762e Fix implicit grant scopes 2018-07-13 11:47:32 +02:00
Andrew Millington
dc2a048b95 Merge pull request #919 from Sephster/fix-909-v2
Fix 909
2018-06-24 13:55:33 +01:00
Andrew Millington
0c542637fe Merge branch '8.0.0' into fix-909-v2 2018-06-24 13:51:04 +01:00
Andrew Millington
0cdd535f7d Add changes to changelog 2018-06-24 13:48:52 +01:00
Andrew Millington
2fcee76d13 Remove unused stub function 2018-06-24 13:39:40 +01:00
Andrew Millington
574299d862 Fix tests 2018-06-24 13:38:55 +01:00
Andrew Millington
dad3b1e1c9 Remove unused test 2018-06-24 13:32:49 +01:00
Andrew Millington
7df0dfff9d Remove double function calls 2018-06-24 13:31:38 +01:00
Andrew Millington
ca5fe10934 Fix merge issues 2018-06-24 01:30:15 +01:00
Andrew Millington
369c7005a3 Merge master into version 8 branch 2018-06-24 01:10:02 +01:00
Andrew Millington
8184f771d4 Update for version 7.2.0 2018-06-23 17:57:59 +01:00
Andrew Millington
51b3b415b4 Update changelog for version 4.1.7 2018-06-23 17:46:19 +01:00
Andrew Millington
e3ad09d4a2 Update unreleased link in changelog 2018-06-23 17:35:51 +01:00
Andrew Millington
aeb1fe48d3 Add missing 4.1.6 release to changelog 2018-06-23 17:35:14 +01:00
Andrew Millington
f54980da25 Update changelog to add PR 917 2018-06-21 23:24:13 +01:00
Andrew Millington
105834af96 Merge pull request #917 from Erikvv/master
Allow 640 as key file permisions
2018-06-21 23:16:13 +01:00
Erik van Velzen
ffffc4bfeb Allow 640 as key file permisions 2018-06-21 17:02:01 +02:00
Andrew Millington
a77732e97c Merge pull request #912 from fizzka/extract-validate-uri
Extract validate uri
2018-06-15 14:42:01 +01:00
Ilya Bulah
614bba2c11 update changelog 2018-06-15 15:57:01 +03:00
Ilya Bulah
224763cda6 Fix docblock 2018-06-15 00:06:33 +03:00
Ilya Bulah
a31bc7d4cc Extract validateRedirectUri() 2018-06-14 23:50:58 +03:00
Ilya Bulah
0d20c755d4 Formatting 2018-06-14 23:50:58 +03:00
Ilya Bulah
e36ff17ad9 Fix psr2 2018-06-14 23:15:01 +03:00
Andrew Millington
a339d99135 Change sentence 2018-06-08 11:19:27 +01:00
Andrew Millington
a7a1065e38 Merge pull request #908 from fizzka/update-encryptionKey-comments
update encryptionKey comments
2018-06-05 10:34:56 +01:00
Andrew Millington
09bf988922 Add capital letter to start of class doc summary 2018-06-05 10:34:12 +01:00
Ilya Bulakh
a571e2262b Update CryptTrait.php 2018-06-04 16:32:02 +03:00
Andrew Millington
519543e925 Merge pull request #703 from iansltx/exception-has-redirect
Add hasRedirect() method for OAuthServerException

Fixes #694
2018-05-25 10:09:15 +01:00
Andrew Millington
3614f8bd7c Update changelog 2018-05-25 10:03:58 +01:00
Andrew Millington
e4a7fea834 Move OAuthServerExceptionTest to appropriate folder 2018-05-25 10:00:21 +01:00
Andrew Millington
68c9fbd83c Add a summary for hasRedirect function 2018-05-25 09:53:59 +01:00
Andrew Millington
466e1a639d Merge remote-tracking branch 'upstream/master' into exception-has-redirect 2018-05-25 09:49:14 +01:00
Andrew Millington
d7ab153500 Merge pull request #905 from Sephster/add-test-for-unsigned-token-exception
Add Exception Detection Test for Unsigned JWT
2018-05-25 09:42:19 +01:00
Andrew Millington
72ead2e3ce Fix unused use statement 2018-05-24 12:23:26 +01:00
Andrew Millington
ae4ab26aaf Add test for unsigned access token 2018-05-24 12:19:55 +01:00
Andrew Millington
ca6be0577c Merge pull request #874 from lookyman/access-token-jwt
Generalized access token format
2018-05-24 11:00:29 +01:00
Andrew Millington
ef75d13255 Update changelog 2018-05-23 16:44:00 +01:00
Andrew Millington
bd741e9203 Update travis to check 8.0.0 branch 2018-05-23 16:39:55 +01:00
Andrew Millington
aac64e49cf Fix style issue 2018-05-23 16:36:43 +01:00
Andrew Millington
61156ef8c7 Use __toString() for access token 2018-05-23 16:34:39 +01:00
Andrew Millington
2a7f671a95 Merge pull request #904 from dzibma/master
Fix uncaught exception produced by unsigned token
2018-05-22 18:13:17 +01:00
Andrew Millington
02609c37cc Update changelog 2018-05-22 18:10:19 +01:00
Martin Dzibela
9941a96feb Fix uncaught exception produced by unsigned token 2018-05-22 14:22:12 +02:00
Andrew Millington
4b0383b16c Updated changelog 2018-05-21 16:20:48 +01:00
Andrew Millington
4aeb92aa98 Merge remote-tracking branch 'upstream/8.0.0' into access-token-jwt 2018-05-21 16:18:24 +01:00
Andrew Millington
b182389395 Remove native type hints 2018-05-21 15:45:09 +01:00
Andrew Millington
92f598d1dc Merge pull request #899 from Sephster/fix-745
Only Add Authenticate Header if Present in Original Request - Tests
2018-05-21 15:44:13 +01:00
Andrew Millington
a40092ff54 Update changelog to fix merge conflict 2018-05-21 15:28:42 +01:00
Andrew Millington
2e47fa7fca Add PR reference 2018-05-21 15:01:37 +01:00
Andrew Millington
fc55621f20 Add link to 7.1.1 release in changelog 2018-05-21 15:00:06 +01:00
Andrew Millington
beec37d95f Modify changelog for 7.1.1 release 2018-05-21 14:58:56 +01:00
Andrew Millington
98812e6fab Update changelog 2018-05-21 11:21:44 +01:00
Andrew Millington
354aed7671 Merge in master 2018-05-21 10:53:24 +01:00
Andrew Millington
5a499bf03c Merge pull request #902 from Sephster/fix-745-without-tests
Only Add Authenticate Header if Present in Original Request. Fix #745
2018-05-17 13:34:30 +01:00
Andrew Millington
2e3ee60a2a Remove additional whitespace 2018-05-17 13:27:30 +01:00
Andrew Millington
0242d0c996 Remove spaces at end of line 2018-05-17 13:21:39 +01:00
Andrew Millington
3ea0cdc936 Set authScheme 2018-05-17 13:19:32 +01:00
Andrew Millington
19d782d223 Fix alignment 2018-05-17 13:13:30 +01:00
Andrew Millington
8a25e0a01b Update changelog 2018-05-17 13:12:32 +01:00
Andrew Millington
a3d4f583ed Fix #745 2018-05-17 13:06:03 +01:00
Andrew Millington
28276cb688 Add PSR-7 to the requirements in the readme
This fixes issue #640
2018-05-16 13:36:29 +01:00
Andrew Millington
b1b33207ab Fix namespacing for Exception test 2018-05-13 18:02:23 +01:00
Andrew Millington
793000f149 Fix ServerRequestInterface docblock type 2018-05-13 17:52:45 +01:00
Andrew Millington
f8c2e721a0 Remove return voids and fix docblock and use orders 2018-05-13 17:41:21 +01:00
Andrew Millington
cbce5f45ba Fix case for serverRequest variable and remove unused variable 2018-05-13 17:38:07 +01:00
Andrew Millington
c2dcdee266 Change order of use statements 2018-05-13 17:34:06 +01:00
Andrew Millington
33ce849617 Add tests for invalid client exception 2018-05-13 17:29:07 +01:00
Andrew Millington
ff5e9f57a5 Only add authenticate header if present in original request thephpleague/oauth2-server#745 2018-05-10 22:07:03 +01:00
Lukáš Unger
577065c270 Use native typehints 2018-05-08 11:35:06 +02:00
Lukáš Unger
a1da9beb92 Removed convertToJWT() method from AccessTokenEntityInterface 2018-05-07 20:37:20 +02:00
Andrew Millington
48ce5f36cf Change function name to be less technically specific 2018-05-07 20:37:20 +02:00
Lukáš Unger
fd72d79ad3 Generalized access token format 2018-05-07 20:37:20 +02:00
Andrew Millington
35c6f28aef Add drupal integration to the readme 2018-05-03 17:06:27 +01:00
Andrew Millington
bd47b58f81 Update changelog for version 7.1.0 2018-04-22 15:16:23 +01:00
Andrew Millington
e38ea4ab34 Update code of conduct link in the readme 2018-04-22 12:58:05 +01:00
Andrew Millington
8b40858509 Rename CONDUCT.md to CODE_OF_CONDUCT.md 2018-04-21 22:25:51 +01:00
Andrew Millington
aa5cc5eac7 Adding contact email address 2018-04-21 22:22:53 +01:00
Andrew Millington
38f26d5229 Update our code of conduct 2018-04-21 22:07:30 +01:00
Andrew Millington
a5a51fad95 Add PR 893 to the changelog 2018-04-21 22:05:08 +01:00
Andrew Millington
52d7952ba5 Merge pull request #893 from Sephster/fix-exception-hint
Change hint so it applies to both the auth and access token requests
2018-04-21 22:02:09 +01:00
Andrew Millington
2375b8c7f3 Merge pull request #856 from lookyman/phpstan-level-7
PHPStan level 7
2018-04-21 22:01:25 +01:00
Andrew Millington
f9a91a79c2 Put upgrade of PHPStan under changed category 2018-04-21 21:59:47 +01:00
Andrew Millington
242dd4dcfe Fix docblock 2018-04-21 21:51:25 +01:00
Andrew Millington
c94ec122aa Fix PR number for changelog 2018-04-21 21:41:48 +01:00
Andrew Millington
05f5d90034 Update changelog 2018-04-21 21:39:28 +01:00
Andrew Millington
491c23c1e9 Merge remote-tracking branch 'upstream/master' into phpstan-level-7 2018-04-21 21:37:24 +01:00
Andrew Millington
27323b5c9a Fix spacing issue 2018-04-21 21:31:48 +01:00
Andrew Millington
80bc291c51 Added null checks before calling set functions 2018-04-21 21:29:21 +01:00
Andrew Millington
8a619e5c1e Change hint so it applies to both the auth and access token requests 2018-04-21 18:07:38 +01:00
Andrew Millington
7e07033b10 Merge pull request #892 from Sephster/fix-issue-837
Revert fix for client ID exception
2018-04-21 17:49:42 +01:00
Andrew Millington
6991777ff3 Fix blank line spacing issue 2018-04-20 18:33:46 +01:00
Andrew Millington
9febc32e14 Add spacing around logical blocks 2018-04-20 18:27:47 +01:00
Andrew Millington
c8b44ff5c7 Revert fix for client ID exception 2018-04-20 18:22:07 +01:00
Andrew Millington
9fc288ce53 Merge pull request #876 from steverhoades/fix-example
Fix fatal error in examples caused by ClientRepositoryInterface change
2018-03-17 20:43:53 +00:00
Steve Rhoades
8f1bf88792 Fix fatal error caused by ClientRepositoryInterface change 2018-03-17 09:30:14 -07:00
Andrew Millington
cc19da50b4 Merge pull request #814 from SunMar/master
Allow CryptTrait to accept a \Defuse\Crypto\Key as encryption key #812
2018-02-28 21:12:39 +00:00
Andrew Millington
bec0de16bb Update Changelog 2018-02-28 21:00:30 +00:00
Andrew Millington
a56acc8dd0 Minor code tidy up 2018-02-28 20:33:19 +00:00
Andrew Millington
c9b07f386c Fix StyleCI issues and remove phpdoc order from StyleCI 2018-02-28 20:01:01 +00:00
Andrew Millington
00a7972f74 Merge remote-tracking branch 'upstream/master' 2018-02-28 19:45:41 +00:00
Andrew Millington
e3266cb50a Fix changelog categorisation 2018-02-26 20:08:02 +00:00
Andrew Millington
2fdd6ce494 Add change for access and refresh token emitters 2018-02-26 20:07:02 +00:00
Andrew Millington
6fd3024c48 Merge pull request #860 from Zaszczyk/new-events-to-emitter-#825
Add new event types: access_token_issued and refresh_token_issued.
2018-02-26 20:01:22 +00:00
Andrew Millington
62e06b7d3a Removing Yoda condition
Removed Yoda condition from code base
2018-02-26 19:51:03 +00:00
Simon Hamp
009c109716 TravisCI fix for PHPStan 2018-02-26 16:04:48 +00:00
Simon Hamp
6723aadfe8 Fix #837
Unifies how we fetch the client_id from the request and allows us to throw a more appropriate exception when the client_id parameter is missing.

Improves the test method for this validation by checking the culpable method in this particular case. The test was missing this by calling the wrong method.
2018-02-26 15:56:28 +00:00
Andrew Millington
e24964af07 Update changelog
Add removal of paragone/random_compat to changelog
2018-02-26 12:57:11 +00:00
Andrew Millington
99e42f6f25 Remove paragonie/random_compat
Removing paragonie/random_compat as no longer supporting PHP 5.x branches
2018-02-26 12:38:31 +00:00
Mateusz Błaszczyk
6700b113a8 Add new event types: access_token_issued and refresh_token_issued. 2018-02-23 17:48:51 +01:00
Andrew Millington
28e1418f64 Change to use correct release date for version 7 2018-02-18 20:29:37 +00:00
Lukáš Unger
143afc9561 PHPStan level 7 2018-02-18 21:20:48 +01:00
SunMar
292272d128 Allow CryptTrait to accept a \Defuse\Crypto\Key as encryption key #812 2018-01-04 15:14:03 +01:00
Ian Littman
d8ece093d5 Add hasRedirect() method for OAuthServerException
Resolves #694.
2017-02-04 14:50:46 -05:00
71 changed files with 2507 additions and 1295 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ phpunit.xml
examples/public.key
examples/private.key
build
*.orig

View File

@@ -4,6 +4,7 @@ enabled:
- binary_operator_spaces
- blank_line_before_return
- concat_with_spaces
- fully_qualified_strict_types
- function_typehint_space
- hash_to_slash_comment
- include
@@ -29,7 +30,6 @@ enabled:
- phpdoc_inline_tag
- phpdoc_no_access
- phpdoc_no_simplified_null_return
- phpdoc_order
- phpdoc_property
- phpdoc_scalar
- phpdoc_separation
@@ -41,7 +41,6 @@ enabled:
- print_to_echo
- short_array_syntax
- short_scalar_cast
- simplified_null_return
- single_quote
- spaces_cast
- standardize_not_equal

View File

@@ -12,16 +12,16 @@ env:
- DEPENDENCIES="--prefer-lowest --prefer-stable"
php:
- 7.0
- 7.1
- 7.2
- 7.3
install:
- composer update --no-interaction --prefer-dist $DEPENDENCIES
script:
- vendor/bin/phpunit --coverage-clover=coverage.clover
- vendor/bin/phpstan analyse -l 6 -c phpstan.neon src tests
- vendor/bin/phpstan analyse -l 7 -c phpstan.neon src tests
after_script:
- wget https://scrutinizer-ci.com/ocular.phar

View File

@@ -6,7 +6,104 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
## [7.0.0] - released 2018-02-17
### Fixed
- Clients are now explicitly prevented from using the Client Credentials grant unless they are confidential to conform
with the OAuth2 spec (PR #1035)
## [8.0.0] - released 2019-07-13
### Added
- Flag, `requireCodeChallengeForPublicClients`, used to reject public clients that do not provide a code challenge for the Auth Code Grant; use AuthCodeGrant::disableRequireCodeCallengeForPublicClients() to turn off this requirement (PR #938)
- Public clients can now use the Auth Code Grant (PR #938)
- `isConfidential` getter added to `ClientEntity` to identify type of client (PR #938)
- Function `validateClient()` added to validate clients which was previously performed by the `getClientEntity()` function (PR #938)
- Add a new function to the AbstractGrant class called `getClientEntityOrFail()`. This is a wrapper around the `getClientEntity()` function that ensures we emit and throw an exception if the repo doesn't return a client entity. (PR #1010)
### Changed
- Replace `convertToJWT()` interface with a more generic `__toString()` to improve extensibility; AccessTokenEntityInterface now requires `setPrivateKey(CryptKey $privateKey)` so `__toString()` has everything it needs to work (PR #874)
- The `invalidClient()` function accepts a PSR-7 compliant `$serverRequest` argument to avoid accessing the `$_SERVER` global variable and improve testing (PR #899)
- `issueAccessToken()` in the Abstract Grant no longer sets access token client, user ID or scopes. These values should already have been set when calling `getNewToken()` (PR #919)
- No longer need to enable PKCE with `enableCodeExchangeProof` flag. Any client sending a code challenge will initiate PKCE checks. (PR #938)
- Function `getClientEntity()` no longer performs client validation (PR #938)
- Password Grant now returns an invalid_grant error instead of invalid_credentials if a user cannot be validated (PR #967)
- Use `DateTimeImmutable()` instead of `DateTime()`, `time()` instead of `(new DateTime())->getTimeStamp()`, and `DateTime::getTimeStamp()` instead of `DateTime::format('U')` (PR #963)
### Removed
- `enableCodeExchangeProof` flag (PR #938)
- Support for PHP 7.0 (PR #1014)
- Remove JTI claim from JWT header (PR #1031)
## [7.4.0] - released 2019-05-05
### Changed
- RefreshTokenRepository can now return null, allowing refresh tokens to be optional. (PR #649)
## [7.3.3] - released 2019-03-29
### Added
- Added `error_description` to the error payload to improve standards compliance. The contents of this are copied from the existing `message` value. (PR #1006)
### Deprecated
- Error payload will not issue `message` value in the next major release (PR #1006)
## [7.3.2] - released 2018-11-21
### Fixed
- Revert setting keys on response type to be inside `getResponseType()` function instead of AuthorizationServer constructor (PR #969)
## [7.3.1] - released 2018-11-15
### Fixed
- Fix issue with previous release where interface had changed for the AuthorizationServer. Reverted to the previous interface while maintaining functionality changes (PR #970)
## [7.3.0] - released 2018-11-13
### Changed
- Moved the `finalizeScopes()` call from `validateAuthorizationRequest` method to the `completeAuthorizationRequest` method so it is called just before the access token is issued (PR #923)
### Added
- Added a ScopeTrait to provide an implementation for jsonSerialize (PR #952)
- Ability to nest exceptions (PR #965)
### Fixed
- Fix issue where AuthorizationServer is not stateless as ResponseType could store state of a previous request (PR #960)
## [7.2.0] - released 2018-06-23
### Changed
- Added new`validateRedirectUri` method AbstractGrant to remove three instances of code duplication (PR #912)
- Allow 640 as a crypt key file permission (PR #917)
### Added
- Function `hasRedirect()` added to `OAuthServerException` (PR #703)
### Fixed
- Catch and handle `BadMethodCallException` from the `verify()` method of the JWT token in the `validateAuthorization` method (PR #904)
## [4.1.7] - released 2018-06-23
### Fixed
- Ensure `empty()` function call only contains variable to be compatible with PHP 5.4 (PR #918)
## [7.1.1] - released 2018-05-21
### Fixed
- No longer set a WWW-Authenticate header for invalid clients if the client did not send an Authorization header in the original request (PR #902)
## [7.1.0] - released 2018-04-22
### Changed
- Changed hint for unsupportedGrantType exception so it no longer references the grant type parameter which isn't always expected (PR #893)
- Upgrade PHPStan checks to level 7 (PR #856)
### Added
- Added event emitters for issued access and refresh tokens (PR #860)
- Can now use Defuse\Crypto\Key for encryption/decryption of keys which is faster than the Cryto class (PR #812)
### Removed
- Remove paragone/random_compat from dependencies
## [7.0.0] - released 2018-02-18
### Added
- Use PHPStan for static analysis of code (PR #848)
@@ -79,6 +176,10 @@ To address feedback from the security release the following change has been made
- Fixed `finalizeScopes` call (Issue #650)
## [4.1.6] - 2016-09-13
- Less restrictive on Authorization header check (Issue #652)
## [5.1.1] - 2016-07-26
- Improved test suite (Issue #614)
@@ -371,7 +472,16 @@ Version 5 is a complete code rewrite.
- First major release
[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/7.0.0...HEAD
[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.0.0...HEAD
[8.0.0]: https://github.com/thephpleague/oauth2-server/compare/7.4.0...8.0.0
[7.4.0]: https://github.com/thephpleague/oauth2-server/compare/7.3.3...7.4.0
[7.3.3]: https://github.com/thephpleague/oauth2-server/compare/7.3.2...7.3.3
[7.3.2]: https://github.com/thephpleague/oauth2-server/compare/7.3.1...7.3.2
[7.3.1]: https://github.com/thephpleague/oauth2-server/compare/7.3.0...7.3.1
[7.3.0]: https://github.com/thephpleague/oauth2-server/compare/7.2.0...7.3.0
[7.2.0]: https://github.com/thephpleague/oauth2-server/compare/7.1.1...7.2.0
[7.1.1]: https://github.com/thephpleague/oauth2-server/compare/7.1.0...7.1.1
[7.1.0]: https://github.com/thephpleague/oauth2-server/compare/7.0.0...7.1.0
[7.0.0]: https://github.com/thephpleague/oauth2-server/compare/6.1.1...7.0.0
[6.1.1]: https://github.com/thephpleague/oauth2-server/compare/6.0.0...6.1.1
[6.1.0]: https://github.com/thephpleague/oauth2-server/compare/6.0.2...6.1.0
@@ -389,6 +499,8 @@ Version 5 is a complete code rewrite.
[5.0.0]: https://github.com/thephpleague/oauth2-server/compare/5.0.0-RC2...5.0.0
[5.0.0-RC2]: https://github.com/thephpleague/oauth2-server/compare/5.0.0-RC1...5.0.0-RC2
[5.0.0-RC1]: https://github.com/thephpleague/oauth2-server/compare/4.1.5...5.0.0-RC1
[4.1.7]: https://github.com/thephpleague/oauth2-server/compare/4.1.6...4.1.7
[4.1.6]: https://github.com/thephpleague/oauth2-server/compare/4.1.5...4.1.6
[4.1.5]: https://github.com/thephpleague/oauth2-server/compare/4.1.4...4.1.5
[4.1.4]: https://github.com/thephpleague/oauth2-server/compare/4.1.3...4.1.4
[4.1.3]: https://github.com/thephpleague/oauth2-server/compare/4.1.2...4.1.3

73
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,73 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at andrew@noexceptions.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org

View File

@@ -1,22 +0,0 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community in a direct capacity. Personal views, beliefs and values of individuals do not necessarily reflect those of the organisation or affiliated individuals and organisations.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)

View File

@@ -31,11 +31,13 @@ This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](ht
The following versions of PHP are supported:
* PHP 7.0
* PHP 7.1
* PHP 7.2
* PHP 7.3
The `openssl` extension is also required.
The `openssl` and `json` extensions are also required.
All HTTP messages passed to the server should be [PSR-7 compliant](https://www.php-fig.org/psr/psr-7/). This ensures interoperability with other packages and frameworks.
## Installation
@@ -54,7 +56,7 @@ The library uses [PHPUnit](https://phpunit.de/) for unit tests and [PHPStan](htt
```
vendor/bin/phpunit
vendor/bin/phpstan analyse -l 6 -c phpstan.neon src tests
vendor/bin/phpstan analyse -l 7 -c phpstan.neon src tests
```
## Continous Integration
@@ -63,8 +65,11 @@ We use [Travis CI](https://travis-ci.org/), [Scrutinizer](https://scrutinizer-ci
## Community Integrations
* [Drupal](https://www.drupal.org/project/simple_oauth)
* [Laravel Passport](https://github.com/laravel/passport)
* [OAuth 2 Server for CakePHP 3](https://github.com/uafrica/oauth-server)
* [OAuth 2 Server for Expressive](https://github.com/zendframework/zend-expressive-authentication-oauth2)
* [Trikoder OAuth 2 Bundle (Symfony)](https://github.com/trikoder/oauth2-bundle)
## Changelog
@@ -72,7 +77,7 @@ See the [project changelog](https://github.com/thephpleague/oauth2-server/blob/m
## Contributing
Contributions are always welcome. Please see [CONTRIBUTING.md](https://github.com/thephpleague/oauth2-server/blob/master/CONTRIBUTING.md) and [CONDUCT.md](https://github.com/thephpleague/oauth2-server/blob/master/CONDUCT.md) for details.
Contributions are always welcome. Please see [CONTRIBUTING.md](https://github.com/thephpleague/oauth2-server/blob/master/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](https://github.com/thephpleague/oauth2-server/blob/master/CODE_OF_CONDUCT.md) for details.
## Support
@@ -80,13 +85,9 @@ Bugs and feature request are tracked on [GitHub](https://github.com/thephpleague
If you have any questions about OAuth _please_ open a ticket here; please **don't** email the address below.
## Commercial Support
If you would like help implementing this library into your existing platform, or would be interested in OAuth advice or training for you and your team please get in touch with [Glynde Labs](https://glyndelabs.com).
## Security
If you discover any security related issues, please email `hello@alexbilbie.com` instead of using the issue tracker.
If you discover any security related issues, please email `andrew@noexceptions.io` instead of using the issue tracker.
## License
@@ -94,7 +95,7 @@ This package is released under the MIT License. See the bundled [LICENSE](https:
## Credits
This code is principally developed and maintained by [Andy Millington](https://twitter.com/Sephster) and [Simon Hamp](https://twitter.com/simonhamp).
This code is principally developed and maintained by [Andy Millington](https://twitter.com/Sephster).
Between 2012 and 2017 this library was developed and maintained by [Alex Bilbie](https://alexbilbie.com/).

View File

@@ -4,20 +4,20 @@
"homepage": "https://oauth2.thephpleague.com/",
"license": "MIT",
"require": {
"php": ">=7.0.0",
"php": ">=7.1.0",
"ext-openssl": "*",
"league/event": "^2.1",
"lcobucci/jwt": "^3.2.2",
"paragonie/random_compat": "^2.0",
"league/event": "^2.2",
"lcobucci/jwt": "^3.3.1",
"psr/http-message": "^1.0.1",
"defuse/php-encryption": "^2.1"
"defuse/php-encryption": "^2.2.1",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^6.3 || ^7.0",
"zendframework/zend-diactoros": "^1.3.2",
"phpstan/phpstan": "^0.9.2",
"phpstan/phpstan-phpunit": "^0.9.4",
"phpstan/phpstan-strict-rules": "^0.9.0"
"phpunit/phpunit": "^7.5.13 || ^8.2.3",
"zendframework/zend-diactoros": "^2.1.2",
"phpstan/phpstan": "^0.11.8",
"phpstan/phpstan-phpunit": "^0.11.2",
"roave/security-advisories": "dev-master"
},
"repositories": [
{
@@ -47,6 +47,12 @@
"email": "hello@alexbilbie.com",
"homepage": "http://www.alexbilbie.com",
"role": "Developer"
},
{
"name": "Andy Millington",
"email": "andrew@noexceptions.io",
"homepage": "https://www.noexceptions.io",
"role": "Developer"
}
],
"replace": {

View File

@@ -1,14 +1,13 @@
{
"require": {
"slim/slim": "3.0.*"
"slim/slim": "^3.0.0"
},
"require-dev": {
"league/event": "^2.1",
"lcobucci/jwt": "^3.1",
"paragonie/random_compat": "^2.0",
"league/event": "^2.2",
"lcobucci/jwt": "^3.3",
"psr/http-message": "^1.0",
"defuse/php-encryption": "^2.1",
"zendframework/zend-diactoros": "^1.0"
"defuse/php-encryption": "^2.2",
"zendframework/zend-diactoros": "^2.1.0"
},
"autoload": {
"psr-4": {

242
examples/composer.lock generated
View File

@@ -1,10 +1,10 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9813ed7c3b6dcf107f44df9392935b8f",
"content-hash": "a7f5c3fdcadb17399bbd97f15e1b11f1",
"packages": [
{
"name": "container-interop/container-interop",
@@ -39,21 +39,24 @@
},
{
"name": "nikic/fast-route",
"version": "v0.6.0",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "31fa86924556b80735f98b294a7ffdfb26789f22"
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/31fa86924556b80735f98b294a7ffdfb26789f22",
"reference": "31fa86924556b80735f98b294a7ffdfb26789f22",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"type": "library",
"autoload": {
"psr-4": {
@@ -78,29 +81,33 @@
"router",
"routing"
],
"time": "2015-06-18T19:15:47+00:00"
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "pimple/pimple",
"version": "v3.0.2",
"version": "v3.2.3",
"source": {
"type": "git",
"url": "https://github.com/silexphp/Pimple.git",
"reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a"
"reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a",
"reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a",
"url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32",
"reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": ">=5.3.0",
"psr/container": "^1.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^3.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
"dev-master": "3.2.x-dev"
}
},
"autoload": {
@@ -124,7 +131,7 @@
"container",
"dependency injection"
],
"time": "2015-09-11T15:10:35+00:00"
"time": "2018-01-21T07:42:36+00:00"
},
{
"name": "psr/container",
@@ -227,27 +234,32 @@
},
{
"name": "slim/slim",
"version": "3.0.0",
"version": "3.12.1",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e"
"reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/3b06f0f2d84dabbe81b6cea46ace46a3e883253e",
"reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/eaee12ef8d0750db62b8c548016d82fb33addb6b",
"reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b",
"shasum": ""
},
"require": {
"container-interop/container-interop": "^1.1",
"nikic/fast-route": "^0.6",
"container-interop/container-interop": "^1.2",
"nikic/fast-route": "^1.0",
"php": ">=5.5.0",
"pimple/pimple": "^3.0",
"psr/container": "^1.0",
"psr/http-message": "^1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
"phpunit/phpunit": "^4.0",
"squizlabs/php_codesniffer": "^2.5"
},
"type": "library",
"autoload": {
@@ -282,38 +294,38 @@
}
],
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
"homepage": "http://slimframework.com",
"homepage": "https://slimframework.com",
"keywords": [
"api",
"framework",
"micro",
"router"
],
"time": "2015-12-07T14:11:09+00:00"
"time": "2019-04-16T16:47:29+00:00"
}
],
"packages-dev": [
{
"name": "defuse/php-encryption",
"version": "v2.1.0",
"version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/defuse/php-encryption.git",
"reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689"
"reference": "0f407c43b953d571421e0020ba92082ed5fb7620"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/defuse/php-encryption/zipball/5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689",
"reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689",
"url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620",
"reference": "0f407c43b953d571421e0020ba92082ed5fb7620",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"paragonie/random_compat": "~2.0",
"paragonie/random_compat": ">= 2",
"php": ">=5.4.0"
},
"require-dev": {
"nikic/php-parser": "^2.0|^3.0",
"nikic/php-parser": "^2.0|^3.0|^4.0",
"phpunit/phpunit": "^4|^5"
},
"bin": [
@@ -354,37 +366,34 @@
"security",
"symmetric key cryptography"
],
"time": "2017-05-18T21:28:48+00:00"
"time": "2018-07-24T23:27:56+00:00"
},
{
"name": "lcobucci/jwt",
"version": "3.2.1",
"version": "3.3.1",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "ddce703826f9c5229781933b1a39069e38e6a0f3"
"reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/ddce703826f9c5229781933b1a39069e38e6a0f3",
"reference": "ddce703826f9c5229781933b1a39069e38e6a0f3",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
"reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-openssl": "*",
"php": ">=5.5"
"php": "^5.6 || ^7.0"
},
"require-dev": {
"mdanter/ecc": "~0.3.1",
"mikey179/vfsstream": "~1.5",
"phpmd/phpmd": "~2.2",
"phpunit/php-invoker": "~1.1",
"phpunit/phpunit": "~4.5",
"phpunit/phpunit": "^5.7 || ^7.3",
"squizlabs/php_codesniffer": "~2.3"
},
"suggest": {
"mdanter/ecc": "Required to use Elliptic Curves based algorithms."
},
"type": "library",
"extra": {
"branch-alias": {
@@ -412,20 +421,20 @@
"JWS",
"jwt"
],
"time": "2016-10-31T20:09:32+00:00"
"time": "2019-05-24T18:30:49+00:00"
},
{
"name": "league/event",
"version": "2.1.2",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/event.git",
"reference": "e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd"
"reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/event/zipball/e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd",
"reference": "e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd",
"url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119",
"reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119",
"shasum": ""
},
"require": {
@@ -433,7 +442,7 @@
},
"require-dev": {
"henrikbjorn/phpspec-code-coverage": "~1.0.1",
"phpspec/phpspec": "~2.0.0"
"phpspec/phpspec": "^2.2"
},
"type": "library",
"extra": {
@@ -462,37 +471,33 @@
"event",
"listener"
],
"time": "2015-05-21T12:24:47+00:00"
"time": "2018-11-26T11:52:41+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.10",
"version": "v9.99.99",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d"
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d",
"reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
"php": "^7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
@@ -507,10 +512,129 @@
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"time": "2017-03-13T16:27:32+00:00"
"time": "2018-07-02T15:55:56+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"time": "2019-04-30T12:38:16+00:00"
},
{
"name": "zendframework/zend-diactoros",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-diactoros.git",
"reference": "279723778c40164bcf984a2df12ff2c6ec5e61c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/279723778c40164bcf984a2df12ff2c6ec5e61c1",
"reference": "279723778c40164bcf984a2df12ff2c6ec5e61c1",
"shasum": ""
},
"require": {
"php": "^7.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-dom": "*",
"ext-libxml": "*",
"http-interop/http-factory-tests": "^0.5.0",
"php-http/psr7-integration-tests": "dev-master",
"phpunit/phpunit": "^7.0.2",
"zendframework/zend-coding-standard": "~1.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev",
"dev-develop": "2.2.x-dev",
"dev-release-1.8": "1.8.x-dev"
}
},
"autoload": {
"files": [
"src/functions/create_uploaded_file.php",
"src/functions/marshal_headers_from_sapi.php",
"src/functions/marshal_method_from_sapi.php",
"src/functions/marshal_protocol_version_from_sapi.php",
"src/functions/marshal_uri_from_sapi.php",
"src/functions/normalize_server.php",
"src/functions/normalize_uploaded_files.php",
"src/functions/parse_cookie_header.php"
],
"psr-4": {
"Zend\\Diactoros\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "PSR HTTP Message implementations",
"keywords": [
"http",
"psr",
"psr-7"
],
"time": "2019-07-10T16:13:25+00:00"
}
],
"aliases": [],

View File

@@ -11,13 +11,9 @@ namespace OAuth2ServerExamples\Entities;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\ScopeTrait;
class ScopeEntity implements ScopeEntityInterface
{
use EntityTrait;
public function jsonSerialize()
{
return $this->getIdentifier();
}
use EntityTrait, ScopeTrait;
}

View File

@@ -14,16 +14,33 @@ use OAuth2ServerExamples\Entities\ClientEntity;
class ClientRepository implements ClientRepositoryInterface
{
const CLIENT_NAME = 'My Awesome App';
const REDIRECT_URI = 'http://foo/bar';
/**
* {@inheritdoc}
*/
public function getClientEntity($clientIdentifier, $grantType, $clientSecret = null, $mustValidateSecret = true)
public function getClientEntity($clientIdentifier)
{
$client = new ClientEntity();
$client->setIdentifier($clientIdentifier);
$client->setName(self::CLIENT_NAME);
$client->setRedirectUri(self::REDIRECT_URI);
return $client;
}
/**
* {@inheritdoc}
*/
public function validateClient($clientIdentifier, $clientSecret, $grantType)
{
$clients = [
'myawesomeapp' => [
'secret' => password_hash('abc123', PASSWORD_BCRYPT),
'name' => 'My Awesome App',
'redirect_uri' => 'http://foo/bar',
'name' => self::CLIENT_NAME,
'redirect_uri' => self::REDIRECT_URI,
'is_confidential' => true,
],
];
@@ -34,18 +51,10 @@ class ClientRepository implements ClientRepositoryInterface
}
if (
$mustValidateSecret === true
&& $clients[$clientIdentifier]['is_confidential'] === true
$clients[$clientIdentifier]['is_confidential'] === true
&& password_verify($clientSecret, $clients[$clientIdentifier]['secret']) === false
) {
return;
}
$client = new ClientEntity();
$client->setIdentifier($clientIdentifier);
$client->setName($clients[$clientIdentifier]['name']);
$client->setRedirectUri($clients[$clientIdentifier]['redirect_uri']);
return $client;
}
}

View File

@@ -18,7 +18,7 @@ class RefreshTokenRepository implements RefreshTokenRepositoryInterface
/**
* {@inheritdoc}
*/
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntityInterface)
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
{
// Some logic to persist the refresh token in a database
}

View File

@@ -1,5 +1,8 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-phpunit/strictRules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
services:
-
class: LeagueTests\PHPStan\AbstractGrantExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

View File

@@ -9,6 +9,8 @@
namespace League\OAuth2\Server;
use DateInterval;
use Defuse\Crypto\Key;
use League\Event\EmitterAwareInterface;
use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
@@ -33,7 +35,7 @@ class AuthorizationServer implements EmitterAwareInterface
protected $enabledGrantTypes = [];
/**
* @var \DateInterval[]
* @var DateInterval[]
*/
protected $grantTypeAccessTokenTTL = [];
@@ -48,7 +50,7 @@ class AuthorizationServer implements EmitterAwareInterface
protected $publicKey;
/**
* @var null|ResponseTypeInterface
* @var ResponseTypeInterface
*/
protected $responseType;
@@ -68,7 +70,7 @@ class AuthorizationServer implements EmitterAwareInterface
private $scopeRepository;
/**
* @var string
* @var string|Key
*/
private $encryptionKey;
@@ -84,7 +86,7 @@ class AuthorizationServer implements EmitterAwareInterface
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param ScopeRepositoryInterface $scopeRepository
* @param CryptKey|string $privateKey
* @param string $encryptionKey
* @param string|Key $encryptionKey
* @param null|ResponseTypeInterface $responseType
*/
public function __construct(
@@ -102,8 +104,16 @@ class AuthorizationServer implements EmitterAwareInterface
if ($privateKey instanceof CryptKey === false) {
$privateKey = new CryptKey($privateKey);
}
$this->privateKey = $privateKey;
$this->encryptionKey = $encryptionKey;
if ($responseType === null) {
$responseType = new BearerTokenResponse();
} else {
$responseType = clone $responseType;
}
$this->responseType = $responseType;
}
@@ -111,12 +121,12 @@ class AuthorizationServer implements EmitterAwareInterface
* Enable a grant type on the server.
*
* @param GrantTypeInterface $grantType
* @param null|\DateInterval $accessTokenTTL
* @param null|DateInterval $accessTokenTTL
*/
public function enableGrantType(GrantTypeInterface $grantType, \DateInterval $accessTokenTTL = null)
public function enableGrantType(GrantTypeInterface $grantType, DateInterval $accessTokenTTL = null)
{
if ($accessTokenTTL instanceof \DateInterval === false) {
$accessTokenTTL = new \DateInterval('PT1H');
if ($accessTokenTTL === null) {
$accessTokenTTL = new DateInterval('PT1H');
}
$grantType->setAccessTokenRepository($this->accessTokenRepository);
@@ -203,16 +213,15 @@ class AuthorizationServer implements EmitterAwareInterface
*/
protected function getResponseType()
{
if ($this->responseType instanceof ResponseTypeInterface === false) {
$this->responseType = new BearerTokenResponse();
$responseType = clone $this->responseType;
if ($responseType instanceof AbstractResponseType) {
$responseType->setPrivateKey($this->privateKey);
}
if ($this->responseType instanceof AbstractResponseType === true) {
$this->responseType->setPrivateKey($this->privateKey);
}
$this->responseType->setEncryptionKey($this->encryptionKey);
$responseType->setEncryptionKey($this->encryptionKey);
return $this->responseType;
return $responseType;
}
/**

View File

@@ -9,6 +9,8 @@
namespace League\OAuth2\Server\AuthorizationValidators;
use BadMethodCallException;
use InvalidArgumentException;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\ValidationData;
@@ -17,6 +19,7 @@ use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
class BearerTokenValidator implements AuthorizationValidatorInterface
{
@@ -28,7 +31,7 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
private $accessTokenRepository;
/**
* @var \League\OAuth2\Server\CryptKey
* @var CryptKey
*/
protected $publicKey;
@@ -43,7 +46,7 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
/**
* Set the public key
*
* @param \League\OAuth2\Server\CryptKey $key
* @param CryptKey $key
*/
public function setPublicKey(CryptKey $key)
{
@@ -60,13 +63,17 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
}
$header = $request->getHeader('authorization');
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
$jwt = trim((string) preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
try {
// Attempt to parse and validate the JWT
$token = (new Parser())->parse($jwt);
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified');
try {
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}
} catch (BadMethodCallException $exception) {
throw OAuthServerException::accessDenied('Access token is not signed', null, $exception);
}
// Ensure access token hasn't expired
@@ -88,12 +95,12 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
->withAttribute('oauth_client_id', $token->getClaim('aud'))
->withAttribute('oauth_user_id', $token->getClaim('sub'))
->withAttribute('oauth_scopes', $token->getClaim('scopes'));
} catch (\InvalidArgumentException $exception) {
} catch (InvalidArgumentException $exception) {
// JWT couldn't be parsed so return the request as is
throw OAuthServerException::accessDenied($exception->getMessage());
} catch (\RuntimeException $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
} catch (RuntimeException $exception) {
//JWR couldn't be parsed so return the request as is
throw OAuthServerException::accessDenied('Error while decoding to JSON');
throw OAuthServerException::accessDenied('Error while decoding to JSON', null, $exception);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @author Lukáš Unger <lookymsc@gmail.com>
* @copyright Copyright (c) Lukáš Unger
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\CodeChallengeVerifiers;
interface CodeChallengeVerifierInterface
{
/**
* Return code challenge method.
*
* @return string
*/
public function getMethod();
/**
* Verify the code challenge.
*
* @param string $codeVerifier
* @param string $codeChallenge
*
* @return bool
*/
public function verifyCodeChallenge($codeVerifier, $codeChallenge);
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* @author Lukáš Unger <lookymsc@gmail.com>
* @copyright Copyright (c) Lukáš Unger
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\CodeChallengeVerifiers;
class PlainVerifier implements CodeChallengeVerifierInterface
{
/**
* Return code challenge method.
*
* @return string
*/
public function getMethod()
{
return 'plain';
}
/**
* Verify the code challenge.
*
* @param string $codeVerifier
* @param string $codeChallenge
*
* @return bool
*/
public function verifyCodeChallenge($codeVerifier, $codeChallenge)
{
return hash_equals($codeVerifier, $codeChallenge);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* @author Lukáš Unger <lookymsc@gmail.com>
* @copyright Copyright (c) Lukáš Unger
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\CodeChallengeVerifiers;
class S256Verifier implements CodeChallengeVerifierInterface
{
/**
* Return code challenge method.
*
* @return string
*/
public function getMethod()
{
return 'S256';
}
/**
* Verify the code challenge.
*
* @param string $codeVerifier
* @param string $codeChallenge
*
* @return bool
*/
public function verifyCodeChallenge($codeVerifier, $codeChallenge)
{
return hash_equals(
strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
$codeChallenge
);
}
}

View File

@@ -11,6 +11,9 @@
namespace League\OAuth2\Server;
use LogicException;
use RuntimeException;
class CryptKey
{
const RSA_KEY_PATTERN =
@@ -42,13 +45,13 @@ class CryptKey
}
if (!file_exists($keyPath) || !is_readable($keyPath)) {
throw new \LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath));
throw new LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath));
}
if ($keyPermissionsCheck === true) {
// Verify the permissions of the key
$keyPathPerms = decoct(fileperms($keyPath) & 0777);
if (in_array($keyPathPerms, ['400', '440', '600', '660'], true) === false) {
if (in_array($keyPathPerms, ['400', '440', '600', '640', '660'], true) === false) {
trigger_error(sprintf(
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
$keyPath,
@@ -64,7 +67,7 @@ class CryptKey
/**
* @param string $key
*
* @throws \RuntimeException
* @throws RuntimeException
*
* @return string
*/
@@ -79,19 +82,19 @@ class CryptKey
if (!touch($keyPath)) {
// @codeCoverageIgnoreStart
throw new \RuntimeException(sprintf('"%s" key file could not be created', $keyPath));
throw new RuntimeException(sprintf('"%s" key file could not be created', $keyPath));
// @codeCoverageIgnoreEnd
}
if (file_put_contents($keyPath, $key) === false) {
// @codeCoverageIgnoreStart
throw new \RuntimeException(sprintf('Unable to write key file to temporary directory "%s"', $tmpDir));
throw new RuntimeException(sprintf('Unable to write key file to temporary directory "%s"', $tmpDir));
// @codeCoverageIgnoreEnd
}
if (chmod($keyPath, 0600) === false) {
// @codeCoverageIgnoreStart
throw new \RuntimeException(sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath));
throw new RuntimeException(sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath));
// @codeCoverageIgnoreEnd
}

View File

@@ -1,6 +1,6 @@
<?php
/**
* Public/private key encryption.
* Encrypt/decrypt with encryptionKey.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
@@ -12,54 +12,73 @@
namespace League\OAuth2\Server;
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
use Exception;
use LogicException;
trait CryptTrait
{
/**
* @var string
* @var string|Key|null
*/
protected $encryptionKey;
/**
* Encrypt data with a private key.
* Encrypt data with encryptionKey.
*
* @param string $unencryptedData
*
* @throws \LogicException
* @throws LogicException
*
* @return string
*/
protected function encrypt($unencryptedData)
{
try {
return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey);
} catch (\Exception $e) {
throw new \LogicException($e->getMessage());
if ($this->encryptionKey instanceof Key) {
return Crypto::encrypt($unencryptedData, $this->encryptionKey);
}
if (is_string($this->encryptionKey)) {
return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey);
}
throw new LogicException('Encryption key not set when attempting to encrypt');
} catch (Exception $e) {
throw new LogicException($e->getMessage(), 0, $e);
}
}
/**
* Decrypt data with a public key.
* Decrypt data with encryptionKey.
*
* @param string $encryptedData
*
* @throws \LogicException
* @throws LogicException
*
* @return string
*/
protected function decrypt($encryptedData)
{
try {
return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey);
} catch (\Exception $e) {
throw new \LogicException($e->getMessage());
if ($this->encryptionKey instanceof Key) {
return Crypto::decrypt($encryptedData, $this->encryptionKey);
}
if (is_string($this->encryptionKey)) {
return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey);
}
throw new LogicException('Encryption key not set when attempting to decrypt');
} catch (Exception $e) {
throw new LogicException($e->getMessage(), 0, $e);
}
}
/**
* Set the encryption key
*
* @param string $key
* @param string|Key $key
*/
public function setEncryptionKey($key = null)
{

View File

@@ -9,17 +9,17 @@
namespace League\OAuth2\Server\Entities;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKey;
interface AccessTokenEntityInterface extends TokenInterface
{
/**
* Generate a JWT from the access token
*
* @param CryptKey $privateKey
*
* @return Token
* Set a private key used to encrypt the access token.
*/
public function convertToJWT(CryptKey $privateKey);
public function setPrivateKey(CryptKey $privateKey);
/**
* Generate a string representation of the access token.
*/
public function __toString();
}

View File

@@ -12,7 +12,7 @@ namespace League\OAuth2\Server\Entities;
interface AuthCodeEntityInterface extends TokenInterface
{
/**
* @return string
* @return string|null
*/
public function getRedirectUri();

View File

@@ -33,4 +33,11 @@ interface ClientEntityInterface
* @return string|string[]
*/
public function getRedirectUri();
/**
* Returns true if the client is confidential.
*
* @return bool
*/
public function isConfidential();
}

View File

@@ -9,6 +9,8 @@
namespace League\OAuth2\Server\Entities;
use DateTimeImmutable;
interface RefreshTokenEntityInterface
{
/**
@@ -28,16 +30,16 @@ interface RefreshTokenEntityInterface
/**
* Get the token's expiry date time.
*
* @return \DateTime
* @return DateTimeImmutable
*/
public function getExpiryDateTime();
/**
* Set the date time when the token expires.
*
* @param \DateTime $dateTime
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(\DateTime $dateTime);
public function setExpiryDateTime(DateTimeImmutable $dateTime);
/**
* Set the access token that the refresh token was associated with.

View File

@@ -9,7 +9,9 @@
namespace League\OAuth2\Server\Entities;
interface ScopeEntityInterface extends \JsonSerializable
use JsonSerializable;
interface ScopeEntityInterface extends JsonSerializable
{
/**
* Get the scope's identifier.

View File

@@ -9,6 +9,8 @@
namespace League\OAuth2\Server\Entities;
use DateTimeImmutable;
interface TokenInterface
{
/**
@@ -28,16 +30,16 @@ interface TokenInterface
/**
* Get the token's expiry date time.
*
* @return \DateTime
* @return DateTimeImmutable
*/
public function getExpiryDateTime();
/**
* Set the date time when the token expires.
*
* @param \DateTime $dateTime
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(\DateTime $dateTime);
public function setExpiryDateTime(DateTimeImmutable $dateTime);
/**
* Set the identifier of the user associated with the token.

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
@@ -19,6 +20,19 @@ use League\OAuth2\Server\Entities\ScopeEntityInterface;
trait AccessTokenTrait
{
/**
* @var CryptKey
*/
private $privateKey;
/**
* Set the private key used to encrypt this access token.
*/
public function setPrivateKey(CryptKey $privateKey)
{
$this->privateKey = $privateKey;
}
/**
* Generate a JWT from the access token
*
@@ -26,27 +40,35 @@ trait AccessTokenTrait
*
* @return Token
*/
public function convertToJWT(CryptKey $privateKey)
private function convertToJWT(CryptKey $privateKey)
{
return (new Builder())
->setAudience($this->getClient()->getIdentifier())
->setId($this->getIdentifier(), true)
->setId($this->getIdentifier())
->setIssuedAt(time())
->setNotBefore(time())
->setExpiration($this->getExpiryDateTime()->getTimestamp())
->setSubject($this->getUserIdentifier())
->setSubject((string) $this->getUserIdentifier())
->set('scopes', $this->getScopes())
->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()))
->getToken();
}
/**
* Generate a string representation from the access token
*/
public function __toString()
{
return (string) $this->convertToJWT($this->privateKey);
}
/**
* @return ClientEntityInterface
*/
abstract public function getClient();
/**
* @return \DateTime
* @return DateTimeImmutable
*/
abstract public function getExpiryDateTime();

View File

@@ -17,7 +17,7 @@ trait AuthCodeTrait
protected $redirectUri;
/**
* @return string
* @return string|null
*/
public function getRedirectUri()
{

View File

@@ -21,6 +21,11 @@ trait ClientTrait
*/
protected $redirectUri;
/**
* @var bool
*/
protected $isConfidential = false;
/**
* Get the client's name.
*
@@ -43,4 +48,14 @@ trait ClientTrait
{
return $this->redirectUri;
}
/**
* Returns true if the client is confidential.
*
* @return bool
*/
public function isConfidential()
{
return $this->isConfidential;
}
}

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
trait RefreshTokenTrait
@@ -19,7 +20,7 @@ trait RefreshTokenTrait
protected $accessToken;
/**
* @var \DateTime
* @var DateTimeImmutable
*/
protected $expiryDateTime;
@@ -42,7 +43,7 @@ trait RefreshTokenTrait
/**
* Get the token's expiry date time.
*
* @return \DateTime
* @return DateTimeImmutable
*/
public function getExpiryDateTime()
{
@@ -52,9 +53,9 @@ trait RefreshTokenTrait
/**
* Set the date time when the token expires.
*
* @param \DateTime $dateTime
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(\DateTime $dateTime)
public function setExpiryDateTime(DateTimeImmutable $dateTime)
{
$this->expiryDateTime = $dateTime;
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @author Andrew Millington <andrew@noexceptions.io>
* @copyright Copyright (c) Andrew Millington
* @license http://mit-license.org
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
trait ScopeTrait
{
/**
* Serialize the object to the scopes string identifier when using json_encode().
*
* @return string
*/
public function jsonSerialize()
{
return $this->getIdentifier();
}
/**
* @return string
*/
abstract public function getIdentifier();
}

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
@@ -20,7 +21,7 @@ trait TokenEntityTrait
protected $scopes = [];
/**
* @var \DateTime
* @var DateTimeImmutable
*/
protected $expiryDateTime;
@@ -57,7 +58,7 @@ trait TokenEntityTrait
/**
* Get the token's expiry date time.
*
* @return \DateTime
* @return DateTimeImmutable
*/
public function getExpiryDateTime()
{
@@ -67,9 +68,9 @@ trait TokenEntityTrait
/**
* Set the date time when the token expires.
*
* @param \DateTime $dateTime
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(\DateTime $dateTime)
public function setExpiryDateTime(DateTimeImmutable $dateTime)
{
$this->expiryDateTime = $dateTime;
}

View File

@@ -9,9 +9,12 @@
namespace League\OAuth2\Server\Exception;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
class OAuthServerException extends \Exception
class OAuthServerException extends Exception
{
/**
* @var int
@@ -38,6 +41,11 @@ class OAuthServerException extends \Exception
*/
private $payload;
/**
* @var ServerRequestInterface
*/
private $serverRequest;
/**
* Throw a new exception.
*
@@ -47,17 +55,18 @@ class OAuthServerException extends \Exception
* @param int $httpStatusCode HTTP status code to send (default = 400)
* @param null|string $hint A helper hint
* @param null|string $redirectUri A HTTP URI to redirect the user back to
* @param Throwable $previous Previous exception
*/
public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null)
public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null, Throwable $previous = null)
{
parent::__construct($message, $code);
parent::__construct($message, $code, $previous);
$this->httpStatusCode = $httpStatusCode;
$this->errorType = $errorType;
$this->hint = $hint;
$this->redirectUri = $redirectUri;
$this->payload = [
'error' => $errorType,
'message' => $message,
'error' => $errorType,
'error_description' => $message,
];
if ($hint !== null) {
$this->payload['hint'] = $hint;
@@ -71,7 +80,15 @@ class OAuthServerException extends \Exception
*/
public function getPayload()
{
return $this->payload;
$payload = $this->payload;
// The "message" property is deprecated and replaced by "error_description"
// TODO: remove "message" property
if (isset($payload['error_description']) && !isset($payload['message'])) {
$payload['message'] = $payload['error_description'];
}
return $payload;
}
/**
@@ -84,6 +101,16 @@ class OAuthServerException extends \Exception
$this->payload = $payload;
}
/**
* Set the server request that is responsible for generating the exception
*
* @param ServerRequestInterface $serverRequest
*/
public function setServerRequest(ServerRequestInterface $serverRequest)
{
$this->serverRequest = $serverRequest;
}
/**
* Unsupported grant type error.
*
@@ -92,7 +119,7 @@ class OAuthServerException extends \Exception
public static function unsupportedGrantType()
{
$errorMessage = 'The authorization grant type is not supported by the authorization server.';
$hint = 'Check the `grant_type` parameter';
$hint = 'Check that all required parameters have been provided';
return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint);
}
@@ -102,28 +129,33 @@ class OAuthServerException extends \Exception
*
* @param string $parameter The invalid parameter
* @param null|string $hint
* @param Throwable $previous Previous exception
*
* @return static
*/
public static function invalidRequest($parameter, $hint = null)
public static function invalidRequest($parameter, $hint = null, Throwable $previous = null)
{
$errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' .
'includes a parameter more than once, or is otherwise malformed.';
$hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint;
return new static($errorMessage, 3, 'invalid_request', 400, $hint);
return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous);
}
/**
* Invalid client error.
*
* @param ServerRequestInterface $serverRequest
*
* @return static
*/
public static function invalidClient()
public static function invalidClient(ServerRequestInterface $serverRequest)
{
$errorMessage = 'Client authentication failed';
$exception = new static('Client authentication failed', 4, 'invalid_client', 401);
return new static($errorMessage, 4, 'invalid_client', 401);
$exception->setServerRequest($serverRequest);
return $exception;
}
/**
@@ -163,20 +195,24 @@ class OAuthServerException extends \Exception
/**
* Server error.
*
* @param string $hint
* @param string $hint
* @param Throwable $previous
*
* @return static
*
* @codeCoverageIgnore
*/
public static function serverError($hint)
public static function serverError($hint, Throwable $previous = null)
{
return new static(
'The authorization server encountered an unexpected condition which prevented it from fulfilling'
. ' the request: ' . $hint,
7,
'server_error',
500
500,
null,
null,
$previous
);
}
@@ -184,12 +220,13 @@ class OAuthServerException extends \Exception
* Invalid refresh token.
*
* @param null|string $hint
* @param Throwable $previous
*
* @return static
*/
public static function invalidRefreshToken($hint = null)
public static function invalidRefreshToken($hint = null, Throwable $previous = null)
{
return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint);
return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint, null, $previous);
}
/**
@@ -197,10 +234,11 @@ class OAuthServerException extends \Exception
*
* @param null|string $hint
* @param null|string $redirectUri
* @param Throwable $previous
*
* @return static
*/
public static function accessDenied($hint = null, $redirectUri = null)
public static function accessDenied($hint = null, $redirectUri = null, Throwable $previous = null)
{
return new static(
'The resource owner or authorization server denied the request.',
@@ -208,7 +246,8 @@ class OAuthServerException extends \Exception
'access_denied',
401,
$hint,
$redirectUri
$redirectUri,
$previous
);
}
@@ -269,7 +308,9 @@ class OAuthServerException extends \Exception
$response = $response->withHeader($header, $content);
}
$response->getBody()->write(json_encode($payload, $jsonOptions));
$responseBody = json_encode($payload, $jsonOptions) ?: 'JSON encoding of payload failed';
$response->getBody()->write($responseBody);
return $response->withStatus($this->getHttpStatusCode());
}
@@ -294,19 +335,30 @@ class OAuthServerException extends \Exception
// include the "WWW-Authenticate" response header field
// matching the authentication scheme used by the client.
// @codeCoverageIgnoreStart
if ($this->errorType === 'invalid_client') {
$authScheme = 'Basic';
if (array_key_exists('HTTP_AUTHORIZATION', $_SERVER) !== false
&& strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer') === 0
) {
$authScheme = 'Bearer';
}
if ($this->errorType === 'invalid_client' && $this->serverRequest->hasHeader('Authorization') === true) {
$authScheme = strpos($this->serverRequest->getHeader('Authorization')[0], 'Bearer') === 0 ? 'Bearer' : 'Basic';
$headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"';
}
// @codeCoverageIgnoreEnd
return $headers;
}
/**
* Check if the exception has an associated redirect URI.
*
* Returns whether the exception includes a redirect, since
* getHttpStatusCode() doesn't return a 302 when there's a
* redirect enabled. This helps when you want to override local
* error pages but want to let redirects through.
*
* @return bool
*/
public function hasRedirect()
{
return $this->redirectUri !== null;
}
/**
* Returns the HTTP status code to send when the exceptions is output.
*

View File

@@ -11,6 +11,9 @@ namespace League\OAuth2\Server\Exception;
class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException
{
/**
* @return UniqueTokenIdentifierConstraintViolationException
*/
public static function create()
{
$errorMessage = 'Could not create unique access token identifier';

View File

@@ -10,6 +10,10 @@
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use DateTimeImmutable;
use Error;
use Exception;
use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
@@ -28,7 +32,9 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use TypeError;
/**
* Abstract grant class.
@@ -72,12 +78,12 @@ abstract class AbstractGrant implements GrantTypeInterface
protected $userRepository;
/**
* @var \DateInterval
* @var DateInterval
*/
protected $refreshTokenTTL;
/**
* @var \League\OAuth2\Server\CryptKey
* @var CryptKey
*/
protected $privateKey;
@@ -137,7 +143,7 @@ abstract class AbstractGrant implements GrantTypeInterface
/**
* {@inheritdoc}
*/
public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL)
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL)
{
$this->refreshTokenTTL = $refreshTokenTTL;
}
@@ -145,7 +151,7 @@ abstract class AbstractGrant implements GrantTypeInterface
/**
* Set the private key
*
* @param \League\OAuth2\Server\CryptKey $key
* @param CryptKey $key
*/
public function setPrivateKey(CryptKey $key)
{
@@ -171,54 +177,109 @@ abstract class AbstractGrant implements GrantTypeInterface
*/
protected function validateClient(ServerRequestInterface $request)
{
list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request);
list($clientId, $clientSecret) = $this->getClientCredentials($request);
$clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
if (is_null($clientId)) {
throw OAuthServerException::invalidRequest('client_id');
}
// If the client is confidential require the client secret
$clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
$client = $this->clientRepository->getClientEntity(
$clientId,
$this->getIdentifier(),
$clientSecret,
true
);
if ($client instanceof ClientEntityInterface === false) {
if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
throw OAuthServerException::invalidClient($request);
}
$client = $this->getClientEntityOrFail($clientId, $request);
// If a redirect URI is provided ensure it matches what is pre-registered
$redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
if ($redirectUri !== null) {
if (
is_string($client->getRedirectUri())
&& (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
} elseif (
is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
$this->validateRedirectUri($redirectUri, $client, $request);
}
return $client;
}
/**
* Wrapper around ClientRepository::getClientEntity() that ensures we emit
* an event and throw an exception if the repo doesn't return a client
* entity.
*
* This is a bit of defensive coding because the interface contract
* doesn't actually enforce non-null returns/exception-on-no-client so
* getClientEntity might return null. By contrast, this method will
* always either return a ClientEntityInterface or throw.
*
* @param string $clientId
* @param ServerRequestInterface $request
*
* @return ClientEntityInterface
*/
protected function getClientEntityOrFail($clientId, ServerRequestInterface $request)
{
$client = $this->clientRepository->getClientEntity($clientId);
if ($client instanceof ClientEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
return $client;
}
/**
* Gets the client credentials from the request from the request body or
* the Http Basic Authorization header
*
* @param ServerRequestInterface $request
*
* @return array
*/
protected function getClientCredentials(ServerRequestInterface $request)
{
list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request);
$clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
if (is_null($clientId)) {
throw OAuthServerException::invalidRequest('client_id');
}
$clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
return [$clientId, $clientSecret];
}
/**
* Validate redirectUri from the request.
* If a redirect URI is provided ensure it matches what is pre-registered
*
* @param string $redirectUri
* @param ClientEntityInterface $client
* @param ServerRequestInterface $request
*
* @throws OAuthServerException
*/
protected function validateRedirectUri(
string $redirectUri,
ClientEntityInterface $client,
ServerRequestInterface $request
) {
if (\is_string($client->getRedirectUri())
&& (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
} elseif (\is_array($client->getRedirectUri())
&& \in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
}
/**
* Validate scopes in the request.
*
* @param string $scopes
* @param string $redirectUri
* @param string|array $scopes
* @param string $redirectUri
*
* @throws OAuthServerException
*
@@ -226,13 +287,13 @@ abstract class AbstractGrant implements GrantTypeInterface
*/
public function validateScopes($scopes, $redirectUri = null)
{
$scopesList = array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) {
return !empty($scope);
});
if (!\is_array($scopes)) {
$scopes = $this->convertScopesQueryStringToArray($scopes);
}
$validScopes = [];
foreach ($scopesList as $scopeItem) {
foreach ($scopes as $scopeItem) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
if ($scope instanceof ScopeEntityInterface === false) {
@@ -245,6 +306,20 @@ abstract class AbstractGrant implements GrantTypeInterface
return $validScopes;
}
/**
* Converts a scopes query string to an array to easily iterate for validation.
*
* @param string $scopes
*
* @return array
*/
private function convertScopesQueryStringToArray($scopes)
{
return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) {
return !empty($scope);
});
}
/**
* Retrieve request parameter.
*
@@ -258,7 +333,7 @@ abstract class AbstractGrant implements GrantTypeInterface
{
$requestParameters = (array) $request->getParsedBody();
return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default;
return $requestParameters[$parameter] ?? $default;
}
/**
@@ -339,7 +414,7 @@ abstract class AbstractGrant implements GrantTypeInterface
/**
* Issue an access token.
*
* @param \DateInterval $accessTokenTTL
* @param DateInterval $accessTokenTTL
* @param ClientEntityInterface $client
* @param string|null $userIdentifier
* @param ScopeEntityInterface[] $scopes
@@ -350,7 +425,7 @@ abstract class AbstractGrant implements GrantTypeInterface
* @return AccessTokenEntityInterface
*/
protected function issueAccessToken(
\DateInterval $accessTokenTTL,
DateInterval $accessTokenTTL,
ClientEntityInterface $client,
$userIdentifier,
array $scopes = []
@@ -358,13 +433,8 @@ abstract class AbstractGrant implements GrantTypeInterface
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
$accessToken->setClient($client);
$accessToken->setUserIdentifier($userIdentifier);
$accessToken->setExpiryDateTime((new \DateTime())->add($accessTokenTTL));
foreach ($scopes as $scope) {
$accessToken->addScope($scope);
}
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL));
$accessToken->setPrivateKey($this->privateKey);
while ($maxGenerationAttempts-- > 0) {
$accessToken->setIdentifier($this->generateUniqueIdentifier());
@@ -383,10 +453,10 @@ abstract class AbstractGrant implements GrantTypeInterface
/**
* Issue an auth code.
*
* @param \DateInterval $authCodeTTL
* @param DateInterval $authCodeTTL
* @param ClientEntityInterface $client
* @param string $userIdentifier
* @param string $redirectUri
* @param string|null $redirectUri
* @param ScopeEntityInterface[] $scopes
*
* @throws OAuthServerException
@@ -395,7 +465,7 @@ abstract class AbstractGrant implements GrantTypeInterface
* @return AuthCodeEntityInterface
*/
protected function issueAuthCode(
\DateInterval $authCodeTTL,
DateInterval $authCodeTTL,
ClientEntityInterface $client,
$userIdentifier,
$redirectUri,
@@ -404,10 +474,13 @@ abstract class AbstractGrant implements GrantTypeInterface
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$authCode = $this->authCodeRepository->getNewAuthCode();
$authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL));
$authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL));
$authCode->setClient($client);
$authCode->setUserIdentifier($userIdentifier);
$authCode->setRedirectUri($redirectUri);
if ($redirectUri !== null) {
$authCode->setRedirectUri($redirectUri);
}
foreach ($scopes as $scope) {
$authCode->addScope($scope);
@@ -433,16 +506,21 @@ abstract class AbstractGrant implements GrantTypeInterface
* @throws OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException
*
* @return RefreshTokenEntityInterface
* @return RefreshTokenEntityInterface|null
*/
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
{
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
$refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL));
if ($refreshToken === null) {
return null;
}
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
$refreshToken->setAccessToken($accessToken);
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
while ($maxGenerationAttempts-- > 0) {
$refreshToken->setIdentifier($this->generateUniqueIdentifier());
try {
@@ -471,13 +549,13 @@ abstract class AbstractGrant implements GrantTypeInterface
try {
return bin2hex(random_bytes($length));
// @codeCoverageIgnoreStart
} catch (\TypeError $e) {
throw OAuthServerException::serverError('An unexpected error has occurred');
} catch (\Error $e) {
throw OAuthServerException::serverError('An unexpected error has occurred');
} catch (\Exception $e) {
} catch (TypeError $e) {
throw OAuthServerException::serverError('An unexpected error has occurred', $e);
} catch (Error $e) {
throw OAuthServerException::serverError('An unexpected error has occurred', $e);
} catch (Exception $e) {
// If you get this message, the CSPRNG failed hard.
throw OAuthServerException::serverError('Could not generate a random string');
throw OAuthServerException::serverError('Could not generate a random string', $e);
}
// @codeCoverageIgnoreEnd
}
@@ -508,7 +586,7 @@ abstract class AbstractGrant implements GrantTypeInterface
*/
public function validateAuthorizationRequest(ServerRequestInterface $request)
{
throw new \LogicException('This grant cannot validate an authorization request');
throw new LogicException('This grant cannot validate an authorization request');
}
/**
@@ -516,6 +594,6 @@ abstract class AbstractGrant implements GrantTypeInterface
*/
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
throw new \LogicException('This grant cannot complete an authorization request');
throw new LogicException('This grant cannot complete an authorization request');
}
}

View File

@@ -9,8 +9,13 @@
namespace League\OAuth2\Server\Grant;
use DateInterval;
use DateTimeImmutable;
use Exception;
use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
@@ -19,39 +24,59 @@ use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use stdClass;
class AuthCodeGrant extends AbstractAuthorizeGrant
{
/**
* @var \DateInterval
* @var DateInterval
*/
private $authCodeTTL;
/**
* @var bool
*/
private $enableCodeExchangeProof = false;
private $requireCodeChallengeForPublicClients = true;
/**
* @var CodeChallengeVerifierInterface[]
*/
private $codeChallengeVerifiers = [];
/**
* @param AuthCodeRepositoryInterface $authCodeRepository
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
* @param \DateInterval $authCodeTTL
* @param DateInterval $authCodeTTL
*
* @throws Exception
*/
public function __construct(
AuthCodeRepositoryInterface $authCodeRepository,
RefreshTokenRepositoryInterface $refreshTokenRepository,
\DateInterval $authCodeTTL
DateInterval $authCodeTTL
) {
$this->setAuthCodeRepository($authCodeRepository);
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->authCodeTTL = $authCodeTTL;
$this->refreshTokenTTL = new \DateInterval('P1M');
$this->refreshTokenTTL = new DateInterval('P1M');
if (in_array('sha256', hash_algos(), true)) {
$s256Verifier = new S256Verifier();
$this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
}
$plainVerifier = new PlainVerifier();
$this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
}
public function enableCodeExchangeProof()
/**
* Disable the requirement for a code challenge for public clients.
*/
public function disableRequireCodeChallengeForPublicClients()
{
$this->enableCodeExchangeProof = true;
$this->requireCodeChallengeForPublicClients = false;
}
/**
@@ -59,7 +84,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
*
* @param ServerRequestInterface $request
* @param ResponseTypeInterface $responseType
* @param \DateInterval $accessTokenTTL
* @param DateInterval $accessTokenTTL
*
* @throws OAuthServerException
*
@@ -68,68 +93,42 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
list($clientId) = $this->getClientCredentials($request);
$client = $this->getClientEntityOrFail($clientId, $request);
// Only validate the client if it is confidential
if ($client->isConfidential()) {
$this->validateClient($request);
}
$encryptedAuthCode = $this->getRequestParameter('code', $request, null);
if ($encryptedAuthCode === null) {
throw OAuthServerException::invalidRequest('code');
}
// Validate the authorization code
try {
$authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
if (time() > $authCodePayload->expire_time) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
}
if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
}
$this->validateAuthorizationCode($authCodePayload, $client, $request);
if ($authCodePayload->client_id !== $client->getIdentifier()) {
throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
}
// The redirect URI is required in this request
$redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
throw OAuthServerException::invalidRequest('redirect_uri');
}
if ($authCodePayload->redirect_uri !== $redirectUri) {
throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
}
$scopes = [];
foreach ($authCodePayload->scopes as $scopeId) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
if ($scope instanceof ScopeEntityInterface === false) {
// @codeCoverageIgnoreStart
throw OAuthServerException::invalidScope($scopeId);
// @codeCoverageIgnoreEnd
}
$scopes[] = $scope;
}
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes(
$scopes,
$this->validateScopes($authCodePayload->scopes),
$this->getIdentifier(),
$client,
$authCodePayload->user_id
);
} catch (\LogicException $e) {
throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
} catch (LogicException $e) {
throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
}
// Validate code challenge
if ($this->enableCodeExchangeProof === true) {
if (!empty($authCodePayload->code_challenge)) {
$codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
if ($codeVerifier === null) {
throw OAuthServerException::invalidRequest('code_verifier');
}
@@ -143,42 +142,36 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
);
}
switch ($authCodePayload->code_challenge_method) {
case 'plain':
if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
}
if (property_exists($authCodePayload, 'code_challenge_method')) {
if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
$codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
break;
case 'S256':
if (
hash_equals(
strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
$authCodePayload->code_challenge
) === false
) {
if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
}
// @codeCoverageIgnoreStart
break;
default:
} else {
throw OAuthServerException::serverError(
sprintf(
'Unsupported code challenge method `%s`',
$authCodePayload->code_challenge_method
)
);
// @codeCoverageIgnoreEnd
}
}
}
// Issue and persist access + refresh tokens
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response type
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
$responseType->setRefreshToken($refreshToken);
}
// Revoke used auth code
$this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
@@ -186,6 +179,41 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
return $responseType;
}
/**
* Validate the authorization code.
*
* @param stdClass $authCodePayload
* @param ClientEntityInterface $client
* @param ServerRequestInterface $request
*/
private function validateAuthorizationCode(
$authCodePayload,
ClientEntityInterface $client,
ServerRequestInterface $request
) {
if (time() > $authCodePayload->expire_time) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
}
if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
}
if ($authCodePayload->client_id !== $client->getIdentifier()) {
throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
}
// The redirect URI is required in this request
$redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
throw OAuthServerException::invalidRequest('redirect_uri');
}
if ($authCodePayload->redirect_uri !== $redirectUri) {
throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
}
}
/**
* Return the grant identifier that can be used in matching up requests.
*
@@ -218,43 +246,23 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$request,
$this->getServerParameter('PHP_AUTH_USER', $request)
);
if (is_null($clientId)) {
if ($clientId === null) {
throw OAuthServerException::invalidRequest('client_id');
}
$client = $this->clientRepository->getClientEntity(
$clientId,
$this->getIdentifier(),
null,
false
);
if ($client instanceof ClientEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
$client = $this->getClientEntityOrFail($clientId, $request);
$redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
if ($redirectUri !== null) {
if (
is_string($client->getRedirectUri())
&& (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
} elseif (
is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
} elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
|| empty($client->getRedirectUri())) {
$this->validateRedirectUri($redirectUri, $client, $request);
} elseif (empty($client->getRedirectUri()) ||
(\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
throw OAuthServerException::invalidClient($request);
} else {
$redirectUri = is_array($client->getRedirectUri())
$redirectUri = \is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
}
@@ -270,20 +278,27 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
$authorizationRequest->setRedirectUri($redirectUri);
$authorizationRequest->setState($stateParameter);
if ($stateParameter !== null) {
$authorizationRequest->setState($stateParameter);
}
$authorizationRequest->setScopes($scopes);
if ($this->enableCodeExchangeProof === true) {
$codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
if ($codeChallenge === null) {
throw OAuthServerException::invalidRequest('code_challenge');
}
$codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
if ($codeChallenge !== null) {
$codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
if (in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) {
if (array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
throw OAuthServerException::invalidRequest(
'code_challenge_method',
'Code challenge method must be `plain` or `S256`'
'Code challenge method must be one of ' . implode(', ', array_map(
function ($method) {
return '`' . $method . '`';
},
array_keys($this->codeChallengeVerifiers)
))
);
}
@@ -298,6 +313,8 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$authorizationRequest->setCodeChallenge($codeChallenge);
$authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
} elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
}
return $authorizationRequest;
@@ -309,14 +326,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
}
$finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
? is_array($authorizationRequest->getClient()->getRedirectUri())
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri()
: $authorizationRequest->getRedirectUri();
$finalRedirectUri = $authorizationRequest->getRedirectUri()
?? $this->getClientRedirectUri($authorizationRequest);
// The user approved the client, redirect them back with an auth code
if ($authorizationRequest->isAuthorizationApproved() === true) {
@@ -334,21 +348,23 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
'auth_code_id' => $authCode->getIdentifier(),
'scopes' => $authCode->getScopes(),
'user_id' => $authCode->getUserIdentifier(),
'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'),
'expire_time' => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
'code_challenge' => $authorizationRequest->getCodeChallenge(),
'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
];
$jsonPayload = json_encode($payload);
if ($jsonPayload === false) {
throw new LogicException('An error was encountered when JSON encoding the authorization request response');
}
$response = new RedirectResponse();
$response->setRedirectUri(
$this->makeRedirectUri(
$finalRedirectUri,
[
'code' => $this->encrypt(
json_encode(
$payload
)
),
'code' => $this->encrypt($jsonPayload),
'state' => $authorizationRequest->getState(),
]
)
@@ -368,4 +384,18 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
)
);
}
/**
* Get the client redirect URI if not set in the request.
*
* @param AuthorizationRequest $authorizationRequest
*
* @return string
*/
private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
{
return \is_array($authorizationRequest->getClient()->getRedirectUri())
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri();
}
}

View File

@@ -11,6 +11,9 @@
namespace League\OAuth2\Server\Grant;
use DateInterval;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -25,10 +28,21 @@ class ClientCredentialsGrant extends AbstractGrant
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
DateInterval $accessTokenTTL
) {
list($clientId) = $this->getClientCredentials($request);
$client = $this->getClientEntityOrFail($clientId, $request);
if (!$client->isConfidential()) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
// Validate request
$client = $this->validateClient($request);
$this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
// Finalize the requested scopes
@@ -37,6 +51,9 @@ class ClientCredentialsGrant extends AbstractGrant
// Issue and persist access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);
// Send event to emitter
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
// Inject access token into response type
$responseType->setAccessToken($accessToken);

View File

@@ -11,6 +11,8 @@
namespace League\OAuth2\Server\Grant;
use DateInterval;
use Defuse\Crypto\Key;
use League\Event\EmitterAwareInterface;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
@@ -28,9 +30,9 @@ interface GrantTypeInterface extends EmitterAwareInterface
/**
* Set refresh token TTL.
*
* @param \DateInterval $refreshTokenTTL
* @param DateInterval $refreshTokenTTL
*/
public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL);
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL);
/**
* Return the grant identifier that can be used in matching up requests.
@@ -44,14 +46,14 @@ interface GrantTypeInterface extends EmitterAwareInterface
*
* @param ServerRequestInterface $request
* @param ResponseTypeInterface $responseType
* @param \DateInterval $accessTokenTTL
* @param DateInterval $accessTokenTTL
*
* @return ResponseTypeInterface
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
DateInterval $accessTokenTTL
);
/**
@@ -136,7 +138,7 @@ interface GrantTypeInterface extends EmitterAwareInterface
/**
* Set the encryption key
*
* @param string|null $key
* @param string|Key|null $key
*/
public function setEncryptionKey($key = null);
}

View File

@@ -9,7 +9,7 @@
namespace League\OAuth2\Server\Grant;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use DateInterval;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
@@ -17,12 +17,13 @@ use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
class ImplicitGrant extends AbstractAuthorizeGrant
{
/**
* @var \DateInterval
* @var DateInterval
*/
private $accessTokenTTL;
@@ -32,33 +33,33 @@ class ImplicitGrant extends AbstractAuthorizeGrant
private $queryDelimiter;
/**
* @param \DateInterval $accessTokenTTL
* @param string $queryDelimiter
* @param DateInterval $accessTokenTTL
* @param string $queryDelimiter
*/
public function __construct(\DateInterval $accessTokenTTL, $queryDelimiter = '#')
public function __construct(DateInterval $accessTokenTTL, $queryDelimiter = '#')
{
$this->accessTokenTTL = $accessTokenTTL;
$this->queryDelimiter = $queryDelimiter;
}
/**
* @param \DateInterval $refreshTokenTTL
* @param DateInterval $refreshTokenTTL
*
* @throw \LogicException
* @throw LogicException
*/
public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL)
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL)
{
throw new \LogicException('The Implicit Grant does not return refresh tokens');
throw new LogicException('The Implicit Grant does not return refresh tokens');
}
/**
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
*
* @throw \LogicException
* @throw LogicException
*/
public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
{
throw new \LogicException('The Implicit Grant does not return refresh tokens');
throw new LogicException('The Implicit Grant does not return refresh tokens');
}
/**
@@ -84,16 +85,16 @@ class ImplicitGrant extends AbstractAuthorizeGrant
*
* @param ServerRequestInterface $request
* @param ResponseTypeInterface $responseType
* @param \DateInterval $accessTokenTTL
* @param DateInterval $accessTokenTTL
*
* @return ResponseTypeInterface
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
DateInterval $accessTokenTTL
) {
throw new \LogicException('This grant does not used this method');
throw new LogicException('This grant does not used this method');
}
/**
@@ -118,41 +119,21 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$request,
$this->getServerParameter('PHP_AUTH_USER', $request)
);
if (is_null($clientId)) {
throw OAuthServerException::invalidRequest('client_id');
}
$client = $this->clientRepository->getClientEntity(
$clientId,
$this->getIdentifier(),
null,
false
);
if ($client instanceof ClientEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
$client = $this->getClientEntityOrFail($clientId, $request);
$redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
if ($redirectUri !== null) {
if (
is_string($client->getRedirectUri())
&& (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
} elseif (
is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri(), true) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
}
$this->validateRedirectUri($redirectUri, $client, $request);
} elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
|| empty($client->getRedirectUri())) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient();
throw OAuthServerException::invalidClient($request);
} else {
$redirectUri = is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
@@ -164,21 +145,18 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$redirectUri
);
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes(
$scopes,
$this->getIdentifier(),
$client
);
$stateParameter = $this->getQueryStringParameter('state', $request);
$authorizationRequest = new AuthorizationRequest();
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
$authorizationRequest->setRedirectUri($redirectUri);
$authorizationRequest->setState($stateParameter);
$authorizationRequest->setScopes($finalizedScopes);
if ($stateParameter !== null) {
$authorizationRequest->setState($stateParameter);
}
$authorizationRequest->setScopes($scopes);
return $authorizationRequest;
}
@@ -189,7 +167,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
}
$finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
@@ -200,11 +178,19 @@ class ImplicitGrant extends AbstractAuthorizeGrant
// The user approved the client, redirect them back with an access token
if ($authorizationRequest->isAuthorizationApproved() === true) {
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes(
$authorizationRequest->getScopes(),
$this->getIdentifier(),
$authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier()
);
$accessToken = $this->issueAccessToken(
$this->accessTokenTTL,
$authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier(),
$authorizationRequest->getScopes()
$finalizedScopes
);
$response = new RedirectResponse();
@@ -212,9 +198,9 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$this->makeRedirectUri(
$finalRedirectUri,
[
'access_token' => (string) $accessToken->convertToJWT($this->privateKey),
'access_token' => (string) $accessToken,
'token_type' => 'Bearer',
'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - (new \DateTime())->getTimestamp(),
'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - \time(),
'state' => $authorizationRequest->getState(),
],
$this->queryDelimiter

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\Grant;
use DateInterval;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
@@ -36,7 +37,7 @@ class PasswordGrant extends AbstractGrant
$this->setUserRepository($userRepository);
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new \DateInterval('P1M');
$this->refreshTokenTTL = new DateInterval('P1M');
}
/**
@@ -45,7 +46,7 @@ class PasswordGrant extends AbstractGrant
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
@@ -55,13 +56,18 @@ class PasswordGrant extends AbstractGrant
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
$responseType->setRefreshToken($refreshToken);
}
return $responseType;
}
@@ -77,11 +83,13 @@ class PasswordGrant extends AbstractGrant
protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
{
$username = $this->getRequestParameter('username', $request);
if (is_null($username)) {
throw OAuthServerException::invalidRequest('username');
}
$password = $this->getRequestParameter('password', $request);
if (is_null($password)) {
throw OAuthServerException::invalidRequest('password');
}
@@ -92,10 +100,11 @@ class PasswordGrant extends AbstractGrant
$this->getIdentifier(),
$client
);
if ($user instanceof UserEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
throw OAuthServerException::invalidGrant();
}
return $user;

View File

@@ -11,6 +11,8 @@
namespace League\OAuth2\Server\Grant;
use DateInterval;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
@@ -29,7 +31,7 @@ class RefreshTokenGrant extends AbstractGrant
{
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new \DateInterval('P1M');
$this->refreshTokenTTL = new DateInterval('P1M');
}
/**
@@ -38,7 +40,7 @@ class RefreshTokenGrant extends AbstractGrant
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
@@ -61,13 +63,18 @@ class RefreshTokenGrant extends AbstractGrant
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
$this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);
// Issue and persist new tokens
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
$responseType->setRefreshToken($refreshToken);
}
return $responseType;
}
@@ -90,8 +97,8 @@ class RefreshTokenGrant extends AbstractGrant
// Validate refresh token
try {
$refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (\Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
} catch (Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
}
$refreshTokenData = json_decode($refreshToken, true);

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Middleware;
use Exception;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseInterface;
@@ -43,7 +44,7 @@ class AuthorizationServerMiddleware
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
// @codeCoverageIgnoreStart
} catch (\Exception $exception) {
} catch (Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response);
// @codeCoverageIgnoreEnd

View File

@@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Middleware;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Psr\Http\Message\ResponseInterface;
@@ -34,7 +35,7 @@ class ResourceServerMiddleware
* @param ResponseInterface $response
* @param callable $next
*
* @return \Psr\Http\Message\ResponseInterface
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
@@ -43,7 +44,7 @@ class ResourceServerMiddleware
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
// @codeCoverageIgnoreStart
} catch (\Exception $exception) {
} catch (Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response);
// @codeCoverageIgnoreEnd

View File

@@ -19,13 +19,20 @@ interface ClientRepositoryInterface extends RepositoryInterface
/**
* Get a client.
*
* @param string $clientIdentifier The client's identifier
* @param null|string $grantType The grant type used (if sent)
* @param null|string $clientSecret The client's secret (if sent)
* @param bool $mustValidateSecret If true the client must attempt to validate the secret if the client
* is confidential
* @param string $clientIdentifier The client's identifier
*
* @return ClientEntityInterface
* @return ClientEntityInterface|null
*/
public function getClientEntity($clientIdentifier, $grantType = null, $clientSecret = null, $mustValidateSecret = true);
public function getClientEntity($clientIdentifier);
/**
* Validate a client's secret.
*
* @param string $clientIdentifier The client's identifier
* @param null|string $clientSecret The client's secret (if sent)
* @param null|string $grantType The type of grant the client is using (if sent)
*
* @return bool
*/
public function validateClient($clientIdentifier, $clientSecret, $grantType);
}

View File

@@ -20,7 +20,7 @@ interface RefreshTokenRepositoryInterface extends RepositoryInterface
/**
* Creates a new refresh token
*
* @return RefreshTokenEntityInterface
* @return RefreshTokenEntityInterface|null
*/
public function getNewRefreshToken();

View File

@@ -22,7 +22,7 @@ interface ScopeRepositoryInterface extends RepositoryInterface
*
* @param string $identifier The scope identifier
*
* @return ScopeEntityInterface
* @return ScopeEntityInterface|null
*/
public function getScopeEntityByIdentifier($identifier);

View File

@@ -22,7 +22,7 @@ interface UserRepositoryInterface extends RepositoryInterface
* @param string $grantType The grant type used
* @param ClientEntityInterface $clientEntity
*
* @return UserEntityInterface
* @return UserEntityInterface|null
*/
public function getUserEntityByUserCredentials(
$username,

View File

@@ -18,6 +18,9 @@ class RequestEvent extends Event
const USER_AUTHENTICATION_FAILED = 'user.authentication.failed';
const REFRESH_TOKEN_CLIENT_FAILED = 'refresh_token.client.failed';
const REFRESH_TOKEN_ISSUED = 'refresh_token.issued';
const ACCESS_TOKEN_ISSUED = 'access_token.issued';
/**
* @var ServerRequestInterface
*/

View File

@@ -60,7 +60,7 @@ class AuthorizationRequest
/**
* The state parameter on the authorization request
*
* @var string
* @var string|null
*/
protected $state;
@@ -111,7 +111,7 @@ class AuthorizationRequest
}
/**
* @return UserEntityInterface
* @return UserEntityInterface|null
*/
public function getUser()
{
@@ -175,7 +175,7 @@ class AuthorizationRequest
}
/**
* @return string
* @return string|null
*/
public function getState()
{

View File

@@ -54,7 +54,7 @@ abstract class AbstractResponseType implements ResponseTypeInterface
/**
* Set the private key
*
* @param \League\OAuth2\Server\CryptKey $key
* @param CryptKey $key
*/
public function setPrivateKey(CryptKey $key)
{

View File

@@ -13,6 +13,7 @@ namespace League\OAuth2\Server\ResponseTypes;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use LogicException;
use Psr\Http\Message\ResponseInterface;
class BearerTokenResponse extends AbstractResponseType
@@ -24,32 +25,34 @@ class BearerTokenResponse extends AbstractResponseType
{
$expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp();
$jwtAccessToken = $this->accessToken->convertToJWT($this->privateKey);
$responseParams = [
'token_type' => 'Bearer',
'expires_in' => $expireDateTime - (new \DateTime())->getTimestamp(),
'access_token' => (string) $jwtAccessToken,
'expires_in' => $expireDateTime - \time(),
'access_token' => (string) $this->accessToken,
];
if ($this->refreshToken instanceof RefreshTokenEntityInterface) {
$refreshToken = $this->encrypt(
json_encode(
[
'client_id' => $this->accessToken->getClient()->getIdentifier(),
'refresh_token_id' => $this->refreshToken->getIdentifier(),
'access_token_id' => $this->accessToken->getIdentifier(),
'scopes' => $this->accessToken->getScopes(),
'user_id' => $this->accessToken->getUserIdentifier(),
'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(),
]
)
);
$refreshTokenPayload = json_encode([
'client_id' => $this->accessToken->getClient()->getIdentifier(),
'refresh_token_id' => $this->refreshToken->getIdentifier(),
'access_token_id' => $this->accessToken->getIdentifier(),
'scopes' => $this->accessToken->getScopes(),
'user_id' => $this->accessToken->getUserIdentifier(),
'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(),
]);
$responseParams['refresh_token'] = $refreshToken;
if ($refreshTokenPayload === false) {
throw new LogicException('Error encountered JSON encoding the refresh token payload');
}
$responseParams['refresh_token'] = $this->encrypt($refreshTokenPayload);
}
$responseParams = array_merge($this->getExtraParams($this->accessToken), $responseParams);
$responseParams = json_encode(array_merge($this->getExtraParams($this->accessToken), $responseParams));
if ($responseParams === false) {
throw new LogicException('Error encountered JSON encoding response parameters');
}
$response = $response
->withStatus(200)
@@ -57,7 +60,7 @@ class BearerTokenResponse extends AbstractResponseType
->withHeader('cache-control', 'no-store')
->withHeader('content-type', 'application/json; charset=UTF-8');
$response->getBody()->write(json_encode($responseParams));
$response->getBody()->write($responseParams);
return $response;
}

View File

@@ -11,6 +11,7 @@
namespace League\OAuth2\Server\ResponseTypes;
use Defuse\Crypto\Key;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use Psr\Http\Message\ResponseInterface;
@@ -37,7 +38,7 @@ interface ResponseTypeInterface
/**
* Set the encryption key
*
* @param string|null $key
* @param string|Key|null $key
*/
public function setEncryptionKey($key = null);
}

View File

@@ -2,7 +2,9 @@
namespace LeagueTests;
use DateInterval;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
@@ -29,7 +31,7 @@ class AuthorizationServerTest extends TestCase
{
const DEFAULT_SCOPE = 'basic';
public function setUp()
public function setUp(): void
{
// Make sure the keys have the correct permissions.
chmod(__DIR__ . '/Stubs/private.key', 0600);
@@ -48,7 +50,7 @@ class AuthorizationServerTest extends TestCase
new StubResponseType()
);
$server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M'));
$server->enableGrantType(new ClientCredentialsGrant(), new DateInterval('PT1M'));
try {
$server->respondToAccessTokenRequest(ServerRequestFactory::fromGlobals(), new Response);
@@ -60,8 +62,11 @@ class AuthorizationServerTest extends TestCase
public function testRespondToRequest()
{
$client = new ClientEntity();
$client->setConfidential();
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepository->method('getClientEntity')->willReturn(new ClientEntity());
$clientRepository->method('getClientEntity')->willReturn($client);
$scope = new ScopeEntity();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
@@ -81,7 +86,7 @@ class AuthorizationServerTest extends TestCase
);
$server->setDefaultScope(self::DEFAULT_SCOPE);
$server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M'));
$server->enableGrantType(new ClientCredentialsGrant(), new DateInterval('PT1M'));
$_POST['grant_type'] = 'client_credentials';
$_POST['client_id'] = 'foo';
@@ -109,6 +114,91 @@ class AuthorizationServerTest extends TestCase
$this->assertInstanceOf(BearerTokenResponse::class, $method->invoke($server));
}
public function testGetResponseTypeExtended()
{
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$privateKey = 'file://' . __DIR__ . '/Stubs/private.key';
$encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key';
$server = new AuthorizationServer(
$clientRepository,
$this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(),
$this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(),
'file://' . __DIR__ . '/Stubs/private.key',
'file://' . __DIR__ . '/Stubs/public.key'
);
$abstractGrantReflection = new \ReflectionClass($server);
$method = $abstractGrantReflection->getMethod('getResponseType');
$method->setAccessible(true);
$responseType = $method->invoke($server);
$responseTypeReflection = new \ReflectionClass($responseType);
$privateKeyProperty = $responseTypeReflection->getProperty('privateKey');
$privateKeyProperty->setAccessible(true);
$encryptionKeyProperty = $responseTypeReflection->getProperty('encryptionKey');
$encryptionKeyProperty->setAccessible(true);
// generated instances should have keys setup
$this->assertSame($privateKey, $privateKeyProperty->getValue($responseType)->getKeyPath());
$this->assertSame($encryptionKey, $encryptionKeyProperty->getValue($responseType));
}
public function testMultipleRequestsGetDifferentResponseTypeInstances()
{
$privateKey = 'file://' . __DIR__ . '/Stubs/private.key';
$encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key';
$responseTypePrototype = new class extends BearerTokenResponse {
/* @return null|CryptKey */
public function getPrivateKey()
{
return $this->privateKey;
}
public function getEncryptionKey()
{
return $this->encryptionKey;
}
};
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$server = new AuthorizationServer(
$clientRepository,
$this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(),
$this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(),
$privateKey,
$encryptionKey,
$responseTypePrototype
);
$abstractGrantReflection = new \ReflectionClass($server);
$method = $abstractGrantReflection->getMethod('getResponseType');
$method->setAccessible(true);
$responseTypeA = $method->invoke($server);
$responseTypeB = $method->invoke($server);
// prototype should not get changed
$this->assertNull($responseTypePrototype->getPrivateKey());
$this->assertNull($responseTypePrototype->getEncryptionKey());
// generated instances should have keys setup
$this->assertSame($privateKey, $responseTypeA->getPrivateKey()->getKeyPath());
$this->assertSame($encryptionKey, $responseTypeA->getEncryptionKey());
// all instances should be different but based on the same prototype
$this->assertSame(get_class($responseTypePrototype), get_class($responseTypeA));
$this->assertSame(get_class($responseTypePrototype), get_class($responseTypeB));
$this->assertNotSame($responseTypePrototype, $responseTypeA);
$this->assertNotSame($responseTypePrototype, $responseTypeB);
$this->assertNotSame($responseTypeA, $responseTypeB);
}
public function testCompleteAuthorizationRequest()
{
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
@@ -127,7 +217,7 @@ class AuthorizationServerTest extends TestCase
$grant = new AuthCodeGrant(
$authCodeRepository,
$this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(),
new \DateInterval('PT10M')
new DateInterval('PT10M')
);
$server->enableGrantType($grant);
@@ -148,6 +238,7 @@ class AuthorizationServerTest extends TestCase
{
$client = new ClientEntity();
$client->setRedirectUri('http://foo/bar');
$client->setConfidential();
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
@@ -158,7 +249,7 @@ class AuthorizationServerTest extends TestCase
$grant = new AuthCodeGrant(
$this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(),
$this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(),
new \DateInterval('PT10M')
new DateInterval('PT10M')
);
$grant->setClientRepository($clientRepositoryMock);
@@ -199,7 +290,7 @@ class AuthorizationServerTest extends TestCase
$grant = new AuthCodeGrant(
$this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(),
$this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(),
new \DateInterval('PT10M')
new DateInterval('PT10M')
);
$grant->setClientRepository($clientRepositoryMock);
@@ -234,10 +325,6 @@ class AuthorizationServerTest extends TestCase
}
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 2
*/
public function testValidateAuthorizationRequestUnregistered()
{
$server = new AuthorizationServer(
@@ -248,19 +335,13 @@ class AuthorizationServerTest extends TestCase
'file://' . __DIR__ . '/Stubs/public.key'
);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
'client_id' => 'foo',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'code',
'client_id' => 'foo',
]);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(2);
$server->validateAuthorizationRequest($request);
}

View File

@@ -0,0 +1,38 @@
<?php
namespace LeagueTests\AuthorizationValidators;
use Lcobucci\JWT\Builder;
use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use PHPUnit\Framework\TestCase;
use Zend\Diactoros\ServerRequest;
class BearerTokenValidatorTest extends TestCase
{
public function testThrowExceptionWhenAccessTokenIsNotSigned()
{
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
$unsignedJwt = (new Builder())
->setAudience('client-id')
->setId('token-id', true)
->setIssuedAt(time())
->setNotBefore(time())
->setExpiration(time())
->setSubject('user-id')
->set('scopes', 'scope1 scope2 scope3 scope4')
->getToken();
$request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $unsignedJwt));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(9);
$bearerTokenValidator->validateAuthorization($request);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace LeagueTests\CodeChallengeVerifiers;
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
use PHPUnit\Framework\TestCase;
class PlainVerifierTest extends TestCase
{
public function testGetMethod()
{
$verifier = new PlainVerifier();
$this->assertEquals('plain', $verifier->getMethod());
}
public function testVerifyCodeChallenge()
{
$verifier = new PlainVerifier();
$this->assertTrue($verifier->verifyCodeChallenge('foo', 'foo'));
$this->assertFalse($verifier->verifyCodeChallenge('foo', 'bar'));
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace LeagueTests\CodeChallengeVerifiers;
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
use PHPUnit\Framework\TestCase;
class S256VerifierTest extends TestCase
{
public function testGetMethod()
{
$verifier = new S256Verifier();
$this->assertEquals('S256', $verifier->getMethod());
}
public function testVerifyCodeChallengeSucceeds()
{
$codeChallenge = $this->createCodeChallenge('foo');
$verifier = new S256Verifier();
$this->assertTrue($verifier->verifyCodeChallenge('foo', $codeChallenge));
}
public function testVerifyCodeChallengeFails()
{
$codeChallenge = $this->createCodeChallenge('bar');
$verifier = new S256Verifier();
$this->assertFalse($verifier->verifyCodeChallenge('foo', $codeChallenge));
}
private function createCodeChallenge($codeVerifier)
{
return strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_');
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace LeagueTests\Exception;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use PHPUnit\Framework\TestCase;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
class OAuthServerExceptionTest extends TestCase
{
public function testInvalidClientExceptionSetsAuthenticateHeader()
{
$serverRequest = (new ServerRequest())
->withParsedBody([
'client_id' => 'foo',
])
->withAddedHeader('Authorization', 'Basic fakeauthdetails');
try {
$this->issueInvalidClientException($serverRequest);
} catch (OAuthServerException $e) {
$response = $e->generateHttpResponse(new Response());
$this->assertTrue($response->hasHeader('WWW-Authenticate'));
}
}
public function testInvalidClientExceptionOmitsAuthenticateHeader()
{
$serverRequest = (new ServerRequest())
->withParsedBody([
'client_id' => 'foo',
]);
try {
$this->issueInvalidClientException($serverRequest);
} catch (OAuthServerException $e) {
$response = $e->generateHttpResponse(new Response());
$this->assertFalse($response->hasHeader('WWW-Authenticate'));
}
}
/**
* Issue an invalid client exception
*
* @throws OAuthServerException
*/
private function issueInvalidClientException($serverRequest)
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('validateClient')->willReturn(false);
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setClientRepository($clientRepositoryMock);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$validateClientMethod->invoke($grantMock, $serverRequest);
}
public function testHasRedirect()
{
$exceptionWithRedirect = OAuthServerException::accessDenied('some hint', 'https://example.com/error');
$this->assertTrue($exceptionWithRedirect->hasRedirect());
}
public function testDoesNotHaveRedirect()
{
$exceptionWithoutRedirect = OAuthServerException::accessDenied('Some hint');
$this->assertFalse($exceptionWithoutRedirect->hasRedirect());
}
public function testHasPrevious()
{
$previous = new Exception('This is the previous');
$exceptionWithPrevious = OAuthServerException::accessDenied(null, null, $previous);
$previousMessage = $exceptionWithPrevious->getPrevious() !== null ? $exceptionWithPrevious->getPrevious()->getMessage() : null;
$this->assertSame('This is the previous', $previousMessage);
}
public function testDoesNotHavePrevious()
{
$exceptionWithoutPrevious = OAuthServerException::accessDenied();
$this->assertNull($exceptionWithoutPrevious->getPrevious());
}
}

View File

@@ -2,7 +2,8 @@
namespace LeagueTests\Grant;
use League\Event\Emitter;
use DateInterval;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
@@ -23,21 +24,13 @@ use Zend\Diactoros\ServerRequest;
class AbstractGrantTest extends TestCase
{
public function testGetSet()
{
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setEmitter(new Emitter());
}
public function testHttpBasicWithPassword()
{
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withHeader('Authorization', 'Basic ' . base64_encode('Open:Sesame'));
$serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ' . base64_encode('Open:Sesame'));
$basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials');
$basicAuthMethod->setAccessible(true);
@@ -50,8 +43,7 @@ class AbstractGrantTest extends TestCase
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withHeader('Authorization', 'Basic ' . base64_encode('Open:'));
$serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ' . base64_encode('Open:'));
$basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials');
$basicAuthMethod->setAccessible(true);
@@ -64,8 +56,7 @@ class AbstractGrantTest extends TestCase
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withHeader('Authorization', 'Foo ' . base64_encode('Open:Sesame'));
$serverRequest = (new ServerRequest())->withHeader('Authorization', 'Foo ' . base64_encode('Open:Sesame'));
$basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials');
$basicAuthMethod->setAccessible(true);
@@ -78,8 +69,7 @@ class AbstractGrantTest extends TestCase
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withHeader('Authorization', 'Basic ||');
$serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ||');
$basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials');
$basicAuthMethod->setAccessible(true);
@@ -92,8 +82,7 @@ class AbstractGrantTest extends TestCase
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withHeader('Authorization', 'Basic ' . base64_encode('OpenSesame'));
$serverRequest = (new ServerRequest())->withHeader('Authorization', 'Basic ' . base64_encode('OpenSesame'));
$basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials');
$basicAuthMethod->setAccessible(true);
@@ -113,16 +102,14 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
]);
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$result = $validateClientMethod->invoke($grantMock, $serverRequest, true, true);
$result = $validateClientMethod->invoke($grantMock, $serverRequest);
$this->assertEquals($client, $result);
}
@@ -139,14 +126,12 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'redirect_uri' => 'http://foo/bar',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'redirect_uri' => 'http://foo/bar',
]);
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
@@ -154,9 +139,6 @@ class AbstractGrantTest extends TestCase
$this->assertEquals($client, $result);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateClientMissingClientId()
{
$client = new ClientEntity();
@@ -173,16 +155,15 @@ class AbstractGrantTest extends TestCase
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateClientMissingClientSecret()
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn(null);
$clientRepositoryMock->method('validateClient')->willReturn(false);
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
@@ -190,24 +171,22 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
]);
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateClientInvalidClientSecret()
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn(null);
$clientRepositoryMock->method('validateClient')->willReturn(false);
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
@@ -215,8 +194,7 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'foo',
]);
@@ -224,12 +202,11 @@ class AbstractGrantTest extends TestCase
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateClientInvalidRedirectUri()
{
$client = new ClientEntity();
@@ -243,8 +220,7 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'redirect_uri' => 'http://bar/foo',
]);
@@ -252,12 +228,11 @@ class AbstractGrantTest extends TestCase
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateClientInvalidRedirectUriArray()
{
$client = new ClientEntity();
@@ -271,8 +246,7 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'redirect_uri' => 'http://bar/foo',
]);
@@ -280,16 +254,15 @@ class AbstractGrantTest extends TestCase
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateClientBadClient()
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn(null);
$clientRepositoryMock->method('validateClient')->willReturn(false);
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
@@ -297,8 +270,7 @@ class AbstractGrantTest extends TestCase
$abstractGrantReflection = new \ReflectionClass($grantMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
]);
@@ -306,6 +278,8 @@ class AbstractGrantTest extends TestCase
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$validateClientMethod->invoke($grantMock, $serverRequest, true);
}
@@ -314,8 +288,7 @@ class AbstractGrantTest extends TestCase
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->method('getIdentifier')->willReturn('foobar');
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'grant_type' => 'foobar',
]);
@@ -332,7 +305,7 @@ class AbstractGrantTest extends TestCase
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setRefreshTokenTTL(new \DateInterval('PT1M'));
$grantMock->setRefreshTokenTTL(new DateInterval('PT1M'));
$grantMock->setRefreshTokenRepository($refreshTokenRepoMock);
$abstractGrantReflection = new \ReflectionClass($grantMock);
@@ -346,6 +319,27 @@ class AbstractGrantTest extends TestCase
$this->assertEquals($accessToken, $refreshToken->getAccessToken());
}
public function testIssueNullRefreshToken()
{
$refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepoMock
->expects($this->once())
->method('getNewRefreshToken')
->willReturn(null);
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setRefreshTokenTTL(new \DateInterval('PT1M'));
$grantMock->setRefreshTokenRepository($refreshTokenRepoMock);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken');
$issueRefreshTokenMethod->setAccessible(true);
$accessToken = new AccessTokenEntity();
$this->assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
}
public function testIssueAccessToken()
{
$accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
@@ -353,6 +347,7 @@ class AbstractGrantTest extends TestCase
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grantMock->setAccessTokenRepository($accessTokenRepoMock);
$abstractGrantReflection = new \ReflectionClass($grantMock);
@@ -362,7 +357,7 @@ class AbstractGrantTest extends TestCase
/** @var AccessTokenEntityInterface $accessToken */
$accessToken = $issueAccessTokenMethod->invoke(
$grantMock,
new \DateInterval('PT1H'),
new DateInterval('PT1H'),
new ClientEntity(),
123,
[new ScopeEntity()]
@@ -387,7 +382,7 @@ class AbstractGrantTest extends TestCase
AuthCodeEntityInterface::class,
$issueAuthCodeMethod->invoke(
$grantMock,
new \DateInterval('PT1H'),
new DateInterval('PT1H'),
new ClientEntity(),
123,
'http://foo/bar',
@@ -405,8 +400,7 @@ class AbstractGrantTest extends TestCase
$method = $abstractGrantReflection->getMethod('getCookieParameter');
$method->setAccessible(true);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withCookieParams([
$serverRequest = (new ServerRequest())->withCookieParams([
'foo' => 'bar',
]);
@@ -423,8 +417,7 @@ class AbstractGrantTest extends TestCase
$method = $abstractGrantReflection->getMethod('getQueryStringParameter');
$method->setAccessible(true);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withQueryParams([
$serverRequest = (new ServerRequest())->withQueryParams([
'foo' => 'bar',
]);
@@ -445,9 +438,6 @@ class AbstractGrantTest extends TestCase
$this->assertEquals([$scope], $grantMock->validateScopes('basic '));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testValidateScopesBadScope()
{
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
@@ -457,6 +447,8 @@ class AbstractGrantTest extends TestCase
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setScopeRepository($scopeRepositoryMock);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$grantMock->validateScopes('basic ');
}
@@ -468,7 +460,7 @@ class AbstractGrantTest extends TestCase
$method = $abstractGrantReflection->getMethod('generateUniqueIdentifier');
$method->setAccessible(true);
$this->assertInternalType('string', $method->invoke($grantMock));
$this->assertIsString($method->invoke($grantMock));
}
public function testCanRespondToAuthorizationRequest()
@@ -477,21 +469,21 @@ class AbstractGrantTest extends TestCase
$this->assertFalse($grantMock->canRespondToAuthorizationRequest(new ServerRequest()));
}
/**
* @expectedException \LogicException
*/
public function testValidateAuthorizationRequest()
{
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$this->expectException(\LogicException::class);
$grantMock->validateAuthorizationRequest(new ServerRequest());
}
/**
* @expectedException \LogicException
*/
public function testCompleteAuthorizationRequest()
{
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$this->expectException(\LogicException::class);
$grantMock->completeAuthorizationRequest(new AuthorizationRequest());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
namespace LeagueTests\Grant;
use DateInterval;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
@@ -27,6 +29,8 @@ class ClientCredentialsGrantTest extends TestCase
public function testRespondToRequest()
{
$client = new ClientEntity();
$client->setConfidential();
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
@@ -44,17 +48,15 @@ class ClientCredentialsGrantTest extends TestCase
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setDefaultScope(self::DEFAULT_SCOPE);
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
}

View File

@@ -2,6 +2,7 @@
namespace LeagueTests\Grant;
use DateInterval;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
@@ -30,56 +31,47 @@ class ImplicitGrantTest extends TestCase
*/
protected $cryptStub;
public function setUp()
public function setUp(): void
{
$this->cryptStub = new CryptTraitStub();
}
public function testGetIdentifier()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$this->assertEquals('implicit', $grant->getIdentifier());
}
public function testCanRespondToAccessTokenRequest()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$this->assertFalse(
$grant->canRespondToAccessTokenRequest(new ServerRequest())
);
}
/**
* @expectedException \LogicException
*/
public function testRespondToAccessTokenRequest()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$this->expectException(\LogicException::class);
$grant->respondToAccessTokenRequest(
new ServerRequest(),
new StubResponseType(),
new \DateInterval('PT10M')
new DateInterval('PT10M')
);
}
public function testCanRespondToAuthorizationRequest()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'token',
'client_id' => 'foo',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'token',
'client_id' => 'foo',
]);
$this->assertTrue($grant->canRespondToAuthorizationRequest($request));
}
@@ -94,27 +86,17 @@ class ImplicitGrantTest extends TestCase
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeEntity = new ScopeEntity();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setDefaultScope(self::DEFAULT_SCOPE);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://foo/bar',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://foo/bar',
]);
$this->assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request));
}
@@ -129,91 +111,55 @@ class ImplicitGrantTest extends TestCase
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeEntity = new ScopeEntity();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setDefaultScope(self::DEFAULT_SCOPE);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://foo/bar',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://foo/bar',
]);
$this->assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 3
*/
public function testValidateAuthorizationRequestMissingClientId()
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
]
);
$request = (new ServerRequest())->withQueryParams(['response_type' => 'code']);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(3);
$grant->validateAuthorizationRequest($request);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 4
*/
public function testValidateAuthorizationRequestInvalidClientId()
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn(null);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
'client_id' => 'foo',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'code',
'client_id' => 'foo',
]);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(4);
$grant->validateAuthorizationRequest($request);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 4
*/
public function testValidateAuthorizationRequestBadRedirectUriString()
{
$client = new ClientEntity();
@@ -221,31 +167,21 @@ class ImplicitGrantTest extends TestCase
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://bar',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://bar',
]);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(4);
$grant->validateAuthorizationRequest($request);
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 4
*/
public function testValidateAuthorizationRequestBadRedirectUriArray()
{
$client = new ClientEntity();
@@ -253,50 +189,50 @@ class ImplicitGrantTest extends TestCase
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock);
$request = new ServerRequest(
[],
[],
null,
null,
'php://input',
$headers = [],
$cookies = [],
$queryParams = [
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://bar',
]
);
$request = (new ServerRequest())->withQueryParams([
'response_type' => 'code',
'client_id' => 'foo',
'redirect_uri' => 'http://bar',
]);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(4);
$grant->validateAuthorizationRequest($request);
}
public function testCompleteAuthorizationRequest()
{
$client = new ClientEntity();
$client->setIdentifier('identifier');
$authRequest = new AuthorizationRequest();
$authRequest->setAuthorizationApproved(true);
$authRequest->setClient(new ClientEntity());
$authRequest->setClient($client);
$authRequest->setGrantTypeId('authorization_code');
$authRequest->setUser(new UserEntity());
$accessToken = new AccessTokenEntity();
$accessToken->setClient($client);
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 9
*/
public function testCompleteAuthorizationRequestDenied()
{
$authRequest = new AuthorizationRequest();
@@ -309,38 +245,51 @@ class ImplicitGrantTest extends TestCase
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(9);
$grant->completeAuthorizationRequest($authRequest);
}
public function testAccessTokenRepositoryUniqueConstraintCheck()
{
$client = new ClientEntity();
$client->setIdentifier('identifier');
$authRequest = new AuthorizationRequest();
$authRequest->setAuthorizationApproved(true);
$authRequest->setClient(new ClientEntity());
$authRequest->setClient($client);
$authRequest->setGrantTypeId('authorization_code');
$authRequest->setUser(new UserEntity());
/** @var AccessTokenRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $accessTokenRepositoryMock */
$accessToken = new AccessTokenEntity();
$accessToken->setClient($client);
/** @var AccessTokenRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject $accessTokenRepositoryMock */
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
$accessTokenRepositoryMock->expects($this->at(0))->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create());
$accessTokenRepositoryMock->expects($this->at(1))->method('persistNewAccessToken')->willReturnSelf();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 7
*/
public function testAccessTokenRepositoryFailToPersist()
{
$authRequest = new AuthorizationRequest();
@@ -349,22 +298,25 @@ class ImplicitGrantTest extends TestCase
$authRequest->setGrantTypeId('authorization_code');
$authRequest->setUser(new UserEntity());
/** @var AccessTokenRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $accessTokenRepositoryMock */
/** @var AccessTokenRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject $accessTokenRepositoryMock */
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(OAuthServerException::serverError('something bad happened'));
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(7);
$grant->completeAuthorizationRequest($authRequest);
}
/**
* @expectedException \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
* @expectedExceptionCode 100
*/
public function testAccessTokenRepositoryFailToPersistUniqueNoInfiniteLoop()
{
$authRequest = new AuthorizationRequest();
@@ -373,43 +325,51 @@ class ImplicitGrantTest extends TestCase
$authRequest->setGrantTypeId('authorization_code');
$authRequest->setUser(new UserEntity());
/** @var AccessTokenRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $accessTokenRepositoryMock */
/** @var AccessTokenRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject $accessTokenRepositoryMock */
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create());
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->expectException(\League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException::class);
$this->expectExceptionCode(100);
$grant->completeAuthorizationRequest($authRequest);
}
/**
* @expectedException \LogicException
*/
public function testSetRefreshTokenTTL()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant->setRefreshTokenTTL(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$this->expectException(\LogicException::class);
$grant->setRefreshTokenTTL(new DateInterval('PT10M'));
}
/**
* @expectedException \LogicException
*/
public function testSetRefreshTokenRepository()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$this->expectException(\LogicException::class);
$grant->setRefreshTokenRepository($refreshTokenRepositoryMock);
}
/**
* @expectedException \LogicException
*/
public function testCompleteAuthorizationRequestNoUser()
{
$grant = new ImplicitGrant(new \DateInterval('PT10M'));
$grant = new ImplicitGrant(new DateInterval('PT10M'));
$this->expectException(\LogicException::class);
$grant->completeAuthorizationRequest(new AuthorizationRequest());
}
}

View File

@@ -2,6 +2,8 @@
namespace LeagueTests\Grant;
use DateInterval;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Grant\PasswordGrant;
@@ -60,27 +62,65 @@ class PasswordGrantTest extends TestCase
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setDefaultScope(self::DEFAULT_SCOPE);
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'foo',
'password' => 'bar',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'foo',
'password' => 'bar',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken());
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testRespondToRequestNullRefreshToken()
{
$client = new ClientEntity();
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
$userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock();
$userEntity = new UserEntity();
$userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity);
$scope = new ScopeEntity();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null);
$grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock);
$grant->setClientRepository($clientRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setDefaultScope(self::DEFAULT_SCOPE);
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'foo',
'password' => 'bar',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertNull($responseType->getRefreshToken());
}
public function testRespondToRequestMissingUsername()
{
$client = new ClientEntity();
@@ -97,21 +137,18 @@ class PasswordGrantTest extends TestCase
$grant->setClientRepository($clientRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
]
);
$serverRequest = (new ServerRequest())->withQueryParams([
'client_id' => 'foo',
'client_secret' => 'bar',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testRespondToRequestMissingPassword()
{
$client = new ClientEntity();
@@ -128,22 +165,19 @@ class PasswordGrantTest extends TestCase
$grant->setClientRepository($clientRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'alex',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'alex',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/
public function testRespondToRequestBadCredentials()
{
$client = new ClientEntity();
@@ -161,17 +195,18 @@ class PasswordGrantTest extends TestCase
$grant->setClientRepository($clientRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'alex',
'password' => 'whisky',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'alex',
'password' => 'whisky',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(10);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
}

View File

@@ -2,6 +2,7 @@
namespace LeagueTests\Grant;
use DateInterval;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
@@ -26,7 +27,7 @@ class RefreshTokenGrantTest extends TestCase
*/
protected $cryptStub;
public function setUp()
public function setUp(): void
{
$this->cryptStub = new CryptTraitStub();
}
@@ -79,8 +80,63 @@ class RefreshTokenGrantTest extends TestCase
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
'scopes' => ['foo'],
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken());
}
public function testRespondToRequestNullRefreshToken()
{
$client = new ClientEntity();
$client->setIdentifier('foo');
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
$scopeEntity = new ScopeEntity();
$scopeEntity->setIdentifier('foo');
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->expects($this->once())->method('persistNewAccessToken')->willReturnSelf();
$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null);
$refreshTokenRepositoryMock->expects($this->never())->method('persistNewRefreshToken');
$grant = new RefreshTokenGrant($refreshTokenRepositoryMock);
$grant->setClientRepository($clientRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setEncryptionKey($this->cryptStub->getKey());
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$oldRefreshToken = $this->cryptStub->doEncrypt(
json_encode(
[
'client_id' => 'foo',
'refresh_token_id' => 'zyxwvu',
'access_token_id' => 'abcdef',
'scopes' => ['foo'],
'user_id' => 123,
'expire_time' => time() + 3600,
]
)
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
@@ -91,7 +147,7 @@ class RefreshTokenGrantTest extends TestCase
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken());
$this->assertNull($responseType->getRefreshToken());
}
public function testRespondToReducedScopes()
@@ -134,27 +190,20 @@ class RefreshTokenGrantTest extends TestCase
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
'scope' => 'foo',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
'scope' => 'foo',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken());
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 5
*/
public function testRespondToUnexpectedScope()
{
$client = new ClientEntity();
@@ -193,24 +242,21 @@ class RefreshTokenGrantTest extends TestCase
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
'scope' => 'foobar',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
'scope' => 'foobar',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(5);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 3
*/
public function testRespondToRequestMissingOldToken()
{
$client = new ClientEntity();
@@ -227,22 +273,19 @@ class RefreshTokenGrantTest extends TestCase
$grant->setEncryptionKey($this->cryptStub->getKey());
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(3);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 8
*/
public function testRespondToRequestInvalidOldToken()
{
$client = new ClientEntity();
@@ -261,23 +304,20 @@ class RefreshTokenGrantTest extends TestCase
$oldRefreshToken = 'foobar';
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(8);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 8
*/
public function testRespondToRequestClientMismatch()
{
$client = new ClientEntity();
@@ -310,23 +350,20 @@ class RefreshTokenGrantTest extends TestCase
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(8);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 8
*/
public function testRespondToRequestExpiredToken()
{
$client = new ClientEntity();
@@ -356,23 +393,20 @@ class RefreshTokenGrantTest extends TestCase
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(8);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
/**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException
* @expectedExceptionCode 8
*/
public function testRespondToRequestRevokedToken()
{
$client = new ClientEntity();
@@ -403,16 +437,17 @@ class RefreshTokenGrantTest extends TestCase
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]
);
$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(8);
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M'));
}
}

View File

@@ -2,6 +2,7 @@
namespace LeagueTests\Middleware;
use DateInterval;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
@@ -23,8 +24,11 @@ class AuthorizationServerMiddlewareTest extends TestCase
public function testValidResponse()
{
$client = new ClientEntity();
$client->setConfidential();
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepository->method('getClientEntity')->willReturn(new ClientEntity());
$clientRepository->method('getClientEntity')->willReturn($client);
$scopeEntity = new ScopeEntity;
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
@@ -66,7 +70,7 @@ class AuthorizationServerMiddlewareTest extends TestCase
public function testOAuthErrorResponse()
{
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepository->method('getClientEntity')->willReturn(null);
$clientRepository->method('validateClient')->willReturn(false);
$server = new AuthorizationServer(
$clientRepository,
@@ -77,7 +81,7 @@ class AuthorizationServerMiddlewareTest extends TestCase
new StubResponseType()
);
$server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M'));
$server->enableGrantType(new ClientCredentialsGrant(), new DateInterval('PT1M'));
$_POST['grant_type'] = 'client_credentials';
$_POST['client_id'] = 'foo';
@@ -104,7 +108,7 @@ class AuthorizationServerMiddlewareTest extends TestCase
$response = $exception->generateHttpResponse(new Response());
$this->assertEquals(302, $response->getStatusCode());
$this->assertEquals('http://foo/bar?error=invalid_scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope',
$this->assertEquals('http://foo/bar?error=invalid_scope&error_description=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed',
$response->getHeader('location')[0]);
}
@@ -114,7 +118,7 @@ class AuthorizationServerMiddlewareTest extends TestCase
$response = $exception->generateHttpResponse(new Response(), true);
$this->assertEquals(302, $response->getStatusCode());
$this->assertEquals('http://foo/bar#error=invalid_scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope',
$this->assertEquals('http://foo/bar#error=invalid_scope&error_description=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed',
$response->getHeader('location')[0]);
}
}

View File

@@ -2,6 +2,8 @@
namespace LeagueTests\Middleware;
use DateInterval;
use DateTimeImmutable;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Middleware\ResourceServerMiddleware;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
@@ -27,13 +29,13 @@ class ResourceServerMiddlewareTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('test');
$accessToken->setUserIdentifier(123);
$accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$token = $accessToken->convertToJWT(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$token = (string) $accessToken;
$request = new ServerRequest();
$request = $request->withHeader('authorization', sprintf('Bearer %s', $token));
$request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $token));
$middleware = new ResourceServerMiddleware($server);
$response = $middleware->__invoke(
@@ -62,13 +64,13 @@ class ResourceServerMiddlewareTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('test');
$accessToken->setUserIdentifier(123);
$accessToken->setExpiryDateTime((new \DateTime())->sub(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$token = $accessToken->convertToJWT(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$token = (string) $accessToken;
$request = new ServerRequest();
$request = $request->withHeader('authorization', sprintf('Bearer %s', $token));
$request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $token));
$middleware = new ResourceServerMiddleware($server);
$response = $middleware->__invoke(
@@ -91,8 +93,7 @@ class ResourceServerMiddlewareTest extends TestCase
'file://' . __DIR__ . '/../Stubs/public.key'
);
$request = new ServerRequest();
$request = $request->withHeader('authorization', '');
$request = (new ServerRequest())->withHeader('authorization', '');
$middleware = new ResourceServerMiddleware($server);
$response = $middleware->__invoke(

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace LeagueTests\PHPStan;
use League\OAuth2\Server\Grant\AbstractGrant;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
final class AbstractGrantExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return AbstractGrant::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), [
'getRequestParameter',
'getQueryStringParameter',
'getCookieParameter',
], true);
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
return TypeCombinator::union(...[
new StringType(),
isset($methodCall->args[2]) ? $scope->getType($methodCall->args[2]->value) : new NullType(),
]);
}
}

View File

@@ -2,6 +2,8 @@
namespace LeagueTests\ResponseTypes;
use DateInterval;
use DateTimeImmutable;
use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Exception\OAuthServerException;
@@ -32,14 +34,15 @@ class BearerResponseTypeTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('abcdef');
$accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->addScope($scope);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$refreshToken = new RefreshTokenEntity();
$refreshToken->setIdentifier('abcdef');
$refreshToken->setAccessToken($accessToken);
$refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -54,7 +57,7 @@ class BearerResponseTypeTest extends TestCase
$response->getBody()->rewind();
$json = json_decode($response->getBody()->getContents());
$this->assertAttributeEquals('Bearer', 'token_type', $json);
$this->assertEquals('Bearer', $json->token_type);
$this->assertObjectHasAttribute('expires_in', $json);
$this->assertObjectHasAttribute('access_token', $json);
$this->assertObjectHasAttribute('refresh_token', $json);
@@ -74,14 +77,15 @@ class BearerResponseTypeTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('abcdef');
$accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->addScope($scope);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$refreshToken = new RefreshTokenEntity();
$refreshToken->setIdentifier('abcdef');
$refreshToken->setAccessToken($accessToken);
$refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -96,13 +100,13 @@ class BearerResponseTypeTest extends TestCase
$response->getBody()->rewind();
$json = json_decode($response->getBody()->getContents());
$this->assertAttributeEquals('Bearer', 'token_type', $json);
$this->assertEquals('Bearer', $json->token_type);
$this->assertObjectHasAttribute('expires_in', $json);
$this->assertObjectHasAttribute('access_token', $json);
$this->assertObjectHasAttribute('refresh_token', $json);
$this->assertObjectHasAttribute('foo', $json);
$this->assertAttributeEquals('bar', 'foo', $json);
$this->assertEquals('bar', $json->foo);
}
public function testDetermineAccessTokenInHeaderValidToken()
@@ -117,13 +121,14 @@ class BearerResponseTypeTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('abcdef');
$accessToken->setUserIdentifier(123);
$accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$refreshToken = new RefreshTokenEntity();
$refreshToken->setIdentifier('abcdef');
$refreshToken->setAccessToken($accessToken);
$refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -137,8 +142,7 @@ class BearerResponseTypeTest extends TestCase
$authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
$request = new ServerRequest();
$request = $request->withHeader('authorization', sprintf('Bearer %s', $json->access_token));
$request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $json->access_token));
$request = $authorizationValidator->validateAuthorization($request);
@@ -162,13 +166,14 @@ class BearerResponseTypeTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('abcdef');
$accessToken->setUserIdentifier(123);
$accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$refreshToken = new RefreshTokenEntity();
$refreshToken->setIdentifier('abcdef');
$refreshToken->setAccessToken($accessToken);
$refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -179,8 +184,7 @@ class BearerResponseTypeTest extends TestCase
$authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
$request = new ServerRequest();
$request = $request->withHeader('authorization', sprintf('Bearer %s', $json->access_token . 'foo'));
$request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $json->access_token . 'foo'));
try {
$authorizationValidator->validateAuthorization($request);
@@ -204,13 +208,14 @@ class BearerResponseTypeTest extends TestCase
$accessToken = new AccessTokenEntity();
$accessToken->setIdentifier('abcdef');
$accessToken->setUserIdentifier(123);
$accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$accessToken->setClient($client);
$accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$refreshToken = new RefreshTokenEntity();
$refreshToken->setIdentifier('abcdef');
$refreshToken->setAccessToken($accessToken);
$refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H')));
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
@@ -224,8 +229,7 @@ class BearerResponseTypeTest extends TestCase
$authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
$request = new ServerRequest();
$request = $request->withHeader('authorization', sprintf('Bearer %s', $json->access_token));
$request = (new ServerRequest())->withHeader('authorization', sprintf('Bearer %s', $json->access_token));
try {
$authorizationValidator->validateAuthorization($request);
@@ -248,8 +252,7 @@ class BearerResponseTypeTest extends TestCase
$authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
$request = new ServerRequest();
$request = $request->withHeader('authorization', 'Bearer blah');
$request = (new ServerRequest())->withHeader('authorization', 'Bearer blah');
try {
$authorizationValidator->validateAuthorization($request);
@@ -272,8 +275,7 @@ class BearerResponseTypeTest extends TestCase
$authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
$request = new ServerRequest();
$request = $request->withHeader('authorization', 'Bearer blah.blah.blah');
$request = (new ServerRequest())->withHeader('authorization', 'Bearer blah.blah.blah');
try {
$authorizationValidator->validateAuthorization($request);

View File

@@ -15,8 +15,8 @@ class ClientEntity implements ClientEntityInterface
$this->redirectUri = $uri;
}
public function setName($name)
public function setConfidential()
{
$this->name = $name;
$this->isConfidential = true;
}
}

View File

@@ -7,11 +7,10 @@ use PHPUnit\Framework\TestCase;
class CryptKeyTest extends TestCase
{
/**
* @expectedException \LogicException
*/
public function testNoFile()
{
$this->expectException(\LogicException::class);
new CryptKey('undefined file');
}
@@ -27,6 +26,11 @@ class CryptKeyTest extends TestCase
public function testKeyFileCreation()
{
$keyContent = file_get_contents(__DIR__ . '/../Stubs/public.key');
if (!is_string($keyContent)) {
$this->fail('The public key stub is not a string');
}
$key = new CryptKey($keyContent);
$this->assertEquals(
@@ -35,6 +39,11 @@ class CryptKeyTest extends TestCase
);
$keyContent = file_get_contents(__DIR__ . '/../Stubs/private.key.crlf');
if (!is_string($keyContent)) {
$this->fail('The private key (crlf) stub is not a string');
}
$key = new CryptKey($keyContent);
$this->assertEquals(

View File

@@ -2,22 +2,34 @@
namespace LeagueTests\Utils;
use Defuse\Crypto\Key;
use LeagueTests\Stubs\CryptTraitStub;
use PHPUnit\Framework\TestCase;
class CryptTraitTest extends TestCase
{
/**
* @var \LeagueTests\Stubs\CryptTraitStub
*/
protected $cryptStub;
public function setUp()
protected function setUp(): void
{
$this->cryptStub = new CryptTraitStub;
$this->cryptStub = new CryptTraitStub();
}
public function testEncryptDecrypt()
public function testEncryptDecryptWithPassword()
{
$this->cryptStub->setEncryptionKey(base64_encode(random_bytes(36)));
$this->encryptDecrypt();
}
public function testEncryptDecryptWithKey()
{
$this->cryptStub->setEncryptionKey(Key::createNewRandomKey());
$this->encryptDecrypt();
}
private function encryptDecrypt()
{
$payload = 'alex loves whisky';
$encrypted = $this->cryptStub->doEncrypt($payload);