-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve authz and authn mechanics #43
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, add docs!
src/Dispatcher.php
Outdated
@@ -52,6 +54,16 @@ public function addResponseMiddleware(callable $callback): self | |||
return $this; | |||
} | |||
|
|||
public function setAuthenticationProvider(Interfaces\AuthenticationProviderInterface $provider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be preferable to combine these two setters into one, requiring either both or neither. It will rarely make sense to authenticate without authorizing, and a misconfiguration could result in resource protection failing open.
src/Dispatcher.php
Outdated
if ($isSRI && $this->authenticationProvider && $endpoint instanceof Interfaces\AuthenticatedEndpointInterface) { | ||
$auth = $this->authenticationProvider->authenticate($this->request); | ||
$endpoint->setAuthentication($auth); | ||
if ($this->authorizationProvider) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per above - this check would be removed, instead always running authorize.
use Psr\Container\ContainerInterface; | ||
|
||
/** | ||
* SHOULD use FQCN for get/has - etc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fill out a proper description here - but it's primarily for best practices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: the driving factor here is that authentication isn't always as simple as "do I have a user?", which is why this is so generic. Use cases:
- Provide time of authentication to the authorizer
- Provide method(s)/factor(s) of authentication to the authorizer
- Provide both the user and application from a bearer token
- Allow for weird admin panel type work, such as user impersonation
*/ | ||
interface AuthenticationContainerInterface extends ContainerInterface | ||
{ | ||
public function isAuthenticated(): bool; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be too simple; instead, it may be better to just kick all of the logic into the container values.
Given that, it would be possible to drop this interface entirely and rename the consumers, but the additional semantic value from the name might be worthwhile.
See also: AuthenticationProvider comment
|
||
interface AuthorizationProviderInterface | ||
{ | ||
public function authorize( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is somewhat limited since it performs all logic pre-execute. This means that in some situations, the endpoint will need to perform additional logic - namely, IDOR prevention. Not sure if there's much to do here beyond best practices docs.
src/extras.php
Outdated
} | ||
} | ||
return new class implements AuthenticationInterface { | ||
public function isAuthenticated(): bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs get/has methods, too. Delete this, (re)throw
|
||
interface AuthenticationProviderInterface | ||
{ | ||
public function authenticate(ServerRequestInterface $request): AuthenticationContainerInterface; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How to indicate no authentication was performed (or successful) is currently UB. At the very least, a doc comment should be added.
From a fail-closed perspective, I'm thinking it should throw a (new) AuthenticationException
if auth fails in any way - malformed or missing headers, signature verification failing, etc. If an endpoint is requesting authn, there's no use case for returning a not-authn result.
This means that, beyond documentation, an AuthenticationProviderInterfaceTestTrait
should be added for success and fail cases, with the latter asserting an appropriate exception is thrown (An abstract dataprovider should suffice)
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
interface AuthenticationProviderInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(generally, I don't like how verbose these names are)
src/Dispatcher.php
Outdated
$this->authenticationProvider = $provider; | ||
} | ||
|
||
public function setAuthorizationProvider(Interfaces\AuthorizationProviderInterface $provider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and this one)
src/Dispatcher.php
Outdated
@@ -143,7 +155,17 @@ public function dispatch(): ResponseInterface | |||
); | |||
} | |||
|
|||
$isSRI = $this->request instanceof ServerRequestInterface; | |||
// Soon: issue a warning if !isSRI |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This warning would be a separate pull
Codecov Report
@@ Coverage Diff @@
## master #43 +/- ##
============================================
+ Coverage 95.94% 95.96% +0.01%
- Complexity 96 109 +13
============================================
Files 23 24 +1
Lines 296 322 +26
============================================
+ Hits 284 309 +25
- Misses 12 13 +1
Continue to review full report at Codecov.
|
Did a quick test of integrating this into a real application A first rough pass took about an hour, which included only basic additions, some copy-paste, and no cleanup or testing. And that time is still a bit misleading since some in-application structures made it easier and more centralized (endpoints were implementing an internal interface which itself extended the EndpointInterface; and used an auth trait in the same way - which influenced the design somewhat), but is still incredibly promising. The total diff (excluding composer.lock obviously) was only about 100 lines. What worked:
What was annoying:
Thoughts/takeaways:
|
Last round of changes was intended to address some annoyances from implementation:
|
Only remaining things I found during a real implementation were:
|
From usability and functionality standpoints, I'm pretty happy with this when it comes to actually using the new tools. There's always room for improvement especially around adding more helpers but I don't want to block moving forward on that (and it will be easier to review and test those separately) |
Today, an
EndpointInterface
must implement anauthorize(RequestInterface): self
method to integrate both authentication and authorization logic. In practice, the mechanics of this have been tedious to use - even with the provided traits - typically resulting in a lot of misdirection in code flow.The goal of this is to support centralized authn and authz, since they tend to have nearly-identical requirements across all requests and endpoints, resulting in more straightforward and less duplicated code.
In 3.1.0 (or the next minor version):
Endpoints may choose to implement
AuthenticatedEndpointInterface
, which extendsEndpointInterface
. When the routed endpoint does so, theAuthenticationProviderInterface
will run, being given aServerRequestInterface
.Implementations should look at request headers (typically) for e.g. a Bearer token, cookie, HTTP basic auth, etc., and return an
AuthenticationContainerInterface
. The returnedAuthenticationContainerInterface
will be provided to the endpoint beforeexecute()
is called. Additionally, if anAuthorizationProviderInterface
is given to the dispatcher, it will be run AFTER authentication but BEFORE execute; If authn fails, execute will not run.AuthorizationProviderInterface
implementations have complete freedom around how resource protection is performed - it could be a simple class map, RBAC, asking for additional data from the protected endpoint (which should use its owninstanceof
checks), etc.Endpoints implementing the new interface SHOULD switch their authentication trait to
None
(if one is in use) to avoid redundant authentication work.In 4.0.0:
authenticate()
will be removed from the EndpointInterface, and no longer called even if present.