Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

Implement per-API authentication #59

Merged
merged 13 commits into from
Mar 16, 2015
Merged

Conversation

weierophinney
Copy link
Member

This patch provides the mechanics necessary to implement per-API authentication.

First, I've changed the priority at which each of the authentication and authentication.post events occur. Prior to these changes, they happened at 500 and 499, respectively; they now happen at -25 and -26 (i.e., after routing, but prior to other Apigility-related listeners).

Second, you can now create a "map" key in the "authentication" configuration. The value of this should be a set of API/module + version / authentication type pairs. As an example:

return [
    'authentication' => [
        'map' => [
            'Status\V2' => 'oauth2',
            'Ping\V1' => 'basic',
            'Images\V3' => 'digest',
        ],
    ],
];

In the above, v2 of the "Status" module/API would use the configured OAuth2 authentication, v1 of the "Ping" module/API would use the configured HTTP Basic authentication, and v3 of the "Images" module/API woulduse the configured HTTP Digest authentication.

In order to retain backwards compatibility, the following rules apply:

  • if
    • no authentication schemes are defined
    • then do not perform authentication
  • if
    • a single authentication scheme is defined
    • and no map is present
    • then use the defined authentication
  • if
    • multiple authentication schemes are defined
    • and no map is present
    • then do not perform authentication
  • if
    • one or more authentication schemes are defined
    • and a map is present
    • and the current API/module does not have an entry in the map
    • then do not perform authentication
  • if
    • one or more authentication schemes are defined
    • and a map is present
    • and the current API/module has an entry in the map
    • and the entry does not match one of the authentication schemes
    • then do not perform authentication
  • if
    • one or more authentication schemes are defined
    • and a map is present
    • and the current API/module has an entry in the map
    • and that entry matches one of the authentication schemes
    • then use the defined authentication

All existing tests continue to pass, and tests were added for each of the above scenarios.

Allowing user-configured authentication types

One place this falls down is if a user has a custom authentication type, as currently that means they will not be able to specify their custom authentication type for an API/module. While we likely will not want to allow configuring custom adapters via the UI, having the UI aware of the type does make sense, as it allows mapping APIs to those types.

The best way to make this happen is to refactor the DefaultAuthenticationListener to be aware of authentication adapters. This approach would do the following:

  • Separate the implementation specific details into their own adapters, slimming down the code in the listener.
  • Provide a clean, simple, and documented API for providing additional authentication adapters other than writing an event listener.
  • Provide a mechanism for exposing authentication types for purposes of mapping authentication to APIs.

The proposed authentication adapter interface is:

namespace ZF\MvcAuth\Authentication;

use Zend\Http\Request;
use Zend\Http\Response;
use ZF\MvcAuth\Identity\IdentityInterface;

interface AdapterInterface
{
    /**
     * @return array Array of types this adapter can handle
     */
    public function provides();

    /**
     * Attempt to retrieve the authentication type based on the request.
     *
     * Allows an adapter to have custom logic for detecting if a request
     * might be providing credentials it's interested in.
     *
     * @param Request $request
     * @return false|string
     */
    public function getTypeFromRequest(Request $request);

    /**
     * Perform pre-flight authentication operations.
     *
     * Use case would be for providing authentication challenge headers.
     *
     * @param Request $request
     * @param Response $response
     * @return void|Response
     */
    public function preAuth(Request $request, Response $response);

    /**
     * Attempt to authenticate the current request.
     *
     * @param Request $request
     * @param Response $response
     * @return false|IdentityInterface False on failure, IdentityInterface
     *     otherwise
     */
    public function authenticate(Request $request, Response $response);
}

The current logic specific to HTTP and OAuth2 authentication would be moved into discrete adapter implementations, and the DefaultAuthenticationListenerFactory would be updated to selectively create these adapters and attach them to the listener.

For those wanting to use custom authentication event listeners, we will provide a configuration key for specifying type names that these listeners support; however, the logic will be such that if no adapter can satisfy the type, a GuestIdentity will be returned; it will be up to the custom listener to perform the appropriate checks to see if it should be used, and to return an identity following the authentication check.

The DefaultAuthenticationListener will be updated to add the following methods:

/**
 * @return array
 */
public function attach(AdapterInterface $adapter);

/**
 * @return array Array of all supported authentication types, including both
 *     those provided by adapters and those provided via configuration.
 */
public function getAuthenticationTypes();

Tasklist

  • Implement authentication maps (API/Module -> authentication type)
  • Implement custom authentication awareness
    • Create authentication adapter interface
    • Modify DefaultAuthenticationListener logic to delegate to adapters
      • getTypeFromRequest() to delegate to adapters for type, if none in authorization header
      • Call preAuth() on all adapters if no type found
      • Retrieve adapter by type and use it to authenticate
  • Create functionality for determining configured types
    • Pull from configured adapters (delegator factories can be used to add more adapters, so pulling the service will allow querying adapters)
    • Pull from a special configuration key

Added a "map" key to the "authentication" subkey to allow mapping
modules to specific authentication types.
Currently, authentication happens at 500 (499 for post-), which is
before routing happens. Since versioning listens at -40, and
authentication should happen earlier, moved to -25 (and -26 for post-).
The authentication map is a map of API/module => authentication type.
This allows per-API authentication.

However, in order to keep this backwards compatible with pre-1.1, the
following scenarios needed to pass:

- if
  - no authentication schemes are defined
  - do not perform authentication
- if
  - a single authentication scheme is defined
  - and no map is present
  - use the defined authentication
- if
  - multiple authentication schemes are defined
  - and no map is present
  - do not perform authentication
- if
  - one or more authentication schemes are defined
  - and a map is present
  - and the current API/module does not have an entry in the map
  - do not perform authentication
- if
  - one or more authentication schemes are defined
  - and a map is present
  - and the current API/module has an entry in the map
  - that does not match one of the authentication schemes
  - do not perform authentication
- if
  - one or more authentication schemes are defined
  - and a map is present
  - and the current API/module has an entry in the map
  - that matches one of the authentication schemes
  - use the defined authentication

This will keep current applications working, while simultaneously allowing them
to start adopting the new mapping functionality. The admin API will soon be
updated to write in this format, and will adapt an existing application to
setup an authentication map for each defined API and version.
@weierophinney weierophinney added this to the 1.1.0 milestone Mar 11, 2015
We allow "basic", "digest", or "oauth2" as map values.
@weierophinney weierophinney changed the title Implement per-API authentication [WIP] Implement per-API authentication Mar 12, 2015
- [X] Implement custom authentication awareness
  - [X] Create authentication adapter interface
  - [X] Modify DefaultAuthenticationListener logic to delegate to adapters
    - [X] `getTypeFromRequest()` to delegate to adapters for type, if none in authorization header
    - [X] Call `preAuth()` on all adapters if no type found
    - [X] Retrieve adapter by type and use it to authenticate
- [ ] Create functionality for determining configured types
  - [X] Pull from configured adapters (delegator factories can be used to add more adapters, so pulling the service will allow querying adapters)
  - [ ] Pull from a special configuration key
- allows proper deprecation of the old "setHttpAdapter" and
  "setOAuth2Server" methods, as users will now seamlessly be utilizing
  only adapters.
- Added `DefaultAuthenticationListener::addAuthenticationTypes`, with
  related tests.
New key, zf-mvc-auth.authentication.types, which should be an array of
authentication type strings. The factory now picks up this value and
passes it to the listener.
@weierophinney weierophinney changed the title [WIP] Implement per-API authentication Implement per-API authentication Mar 15, 2015
@weierophinney
Copy link
Member Author

@ezimuel This patch is now ready for review; includes tests and documentation!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants