You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ga...@apache.org on 2018/08/09 21:33:22 UTC

[trafficserver] branch master updated: access_control plugin

This is an automated email from the ASF dual-hosted git repository.

gancho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 02bc5c5  access_control plugin
02bc5c5 is described below

commit 02bc5c516989c1274d52e70e5770ed8506d80dca
Author: Gancho Tenev <ga...@apache.org>
AuthorDate: Thu Dec 7 17:01:17 2017 -0800

    access_control plugin
    
    The access_control plugin covers some common use-cases related
    to providing access control to the objects stored in CDN cache.
    
    The authentication and authorization are to be performed only by the
    application (origin) using any Identity Management or Directory services.
    The CDN is to be concerned only with the access approval (the
    function of actually granting or rejecting access) to the objects
    in its own caches based on an access token provided by the application
    (origin). It is assumed that the origin will perform its own access control.
---
 doc/admin-guide/plugins/access_control.en.rst      | 478 ++++++++++++++++
 doc/admin-guide/plugins/index.en.rst               |   4 +
 plugins/Makefile.am                                |   1 +
 plugins/experimental/access_control/.gitignore     |   1 +
 plugins/experimental/access_control/Makefile.inc   |  38 ++
 plugins/experimental/access_control/README.md      |   1 +
 .../experimental/access_control/access_control.cc  | 497 +++++++++++++++++
 .../experimental/access_control/access_control.h   | 294 ++++++++++
 plugins/experimental/access_control/common.cc      |  51 ++
 plugins/experimental/access_control/common.h       |  69 +++
 plugins/experimental/access_control/config.cc      | 380 +++++++++++++
 plugins/experimental/access_control/config.h       |  70 +++
 plugins/experimental/access_control/headers.cc     | 213 +++++++
 plugins/experimental/access_control/headers.h      |  32 ++
 plugins/experimental/access_control/pattern.cc     | 579 +++++++++++++++++++
 plugins/experimental/access_control/pattern.h      | 148 +++++
 plugins/experimental/access_control/plugin.cc      | 617 +++++++++++++++++++++
 .../unit-tests/test_access_control.cc              | 171 ++++++
 .../access_control/unit-tests/test_utils.cc        | 265 +++++++++
 plugins/experimental/access_control/utils.cc       | 497 +++++++++++++++++
 plugins/experimental/access_control/utils.h        |  57 ++
 21 files changed, 4463 insertions(+)

diff --git a/doc/admin-guide/plugins/access_control.en.rst b/doc/admin-guide/plugins/access_control.en.rst
new file mode 100644
index 0000000..c7b9a3e
--- /dev/null
+++ b/doc/admin-guide/plugins/access_control.en.rst
@@ -0,0 +1,478 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+
+
+.. include:: ../../common.defs
+
+.. _admin-plugins-access_control:
+
+
+Access Control Plugin
+*********************
+
+Description
+===========
+The `access_control` plugin covers common use-cases related to providing access control to the objects stored in CDN_ cache.
+
+
+Requirements and features
+=========================
+
+#. _`Cache Access Control` - cached objects to be served only to properly authenticated and authorized users.
+#. _`Authorizing Multiple Requests` - all requests from a particular UA_ in a defined time period to be authenticated and authorized only once.
+#. _`Multiple Page Versions` - objects may have the same name (URI) but different content for each `target audience`_ and must be served only to the corresponding `target audience`_.
+#. _`Proxy Only Mode` - CDN_ proxies the request to the origin_. In case of access control failure at the Edge_, the UA_ would not be redirected to other services, the failed request would be forwarded to the origin_.
+
+Participants
+============
+
+* _`UA` - User Agent used by a user whose requests are subject to access control.
+* _`Target audience` - an application specific audience (role, group of users, etc) to which the authenticated and authorized user belongs and which defines user's permissions.
+* _`CDN` / _`Edge` - Content Delivery Network, sometimes CDN and Edge are used interchangeably
+* _`IdMS` - Identity Management Services
+* _`DS` - Directory services
+* _`Origin` / _`Application` - origin server which is part of the particular application, sometimes origin and application are used interchangeably
+
+
+Design considerations and limitations
+=====================================
+
+The following are some thoughts and ideas behind the access control flow and the plugin design.
+
+
+**Existing standards**
+
+  OAuth [1]_ [2]_ and SAML [3]_ were considered looking for use cases matching standard-based solutions but none seemed to match the requirements completely, also supporting some of the mentioned standards would require a more involved design and implementation.
+
+**Closed integration**
+
+  Closed integration with existing `IdMS`_ and `DS`_ was considered in earlier design stages which resulted in a too specific and unnecessarily complicated access control flow, inflexible, with higher maintenance cost, possibly not scaling and not coping well with future changes.
+
+**Cache access approval only**
+
+  Authentication and Authorization are to be performed only by the application_ using various services like IdMS_, DS_, etc. The application_ knows more about its users, their requests and permissions and need to deal with it regardless. It is assumed that the origin_ will perform its own access control. The CDN_ is to be concerned only with the _`access approval` (the function of actually granting or rejecting access) to the objects in its own caches based on an access token provided by [...]
+
+**Access token**
+
+  The access token should be a compact and self-contained token containing the information required for properly enforcing the access control. It can be extracted from HTTP headers, cookies, URI query parameters which would allow us to support various `access approval`_ use cases.
+
+**Access token subject**
+
+  The subject of the token in some of the use cases would signify a separate `target audience`_ and should be opaque to CDN_ which would allow CDN_ to be taken out of the authorization and authentication equation. In those use cases the `subject` will be added to the cache key as a mechanism to support `multiple page versions`_. Special considerations should be given to the cache-hit ratio and the distinct `target audience`_ count. The bigger the number of audiences the lesser the cache- [...]
+
+**Cache key modifications**
+
+  This plugin will not modify the cache key directly but rather rely on plugins like :ref:`admin-plugins-cachekey`.
+
+**TLS only**
+
+  To combat the risk of man in the middle attacks, spoofing elements of the requests, unexpectedly leaking security information only TLS will be used when requesting protected assets and exchanging access tokens.
+
+
+Use cases
+=========
+
+Let us say CDN_'s domain name is ``example-cdn.com`` and origin_'s domain name is ``example.com``, ``<access_token>`` is an access token value created by the origin_ and validated by CDN_. When necessary the access token is passed from the origin_ to the CDN_ by using a response header ``TokenRespHdr`` and is stored at the UA_ in a persistent cookie ``TokenCookie``.
+
+.. uml::
+
+        @startuml
+
+        participant UA
+        participant CDN
+        participant Origin
+
+        autonumber "<b><00>"
+        UA -> CDN : GET https://example-cdn.com/path/object\n[Cookie: TokenCookie=<access_token>]
+        activate CDN
+
+        CDN->CDN:validate access_token
+
+        alt valid access_token
+          CDN -> CDN: extract access_token //subject//\nand add it to cache key
+        else missing or invalid token
+          CDN -> CDN: skip cache (same as cache-miss)
+        end
+
+        alt invalid access_token OR cache-miss
+          alt config:use_redirect=//true//
+            CDN -> Origin : HEAD https://example.com/path/object
+            activate Origin
+          else config:use_returect=//false//
+            CDN -> Origin : GET https://example.com/path/object
+            deactivate CDN
+          end
+
+          Origin -> Origin: trigger authentication\n+ authorization flow
+          note over UA,Origin #white
+            Origin <=> UA authentication and authorization flow using IdMS and DS, etc.
+          endnote
+
+          alt user unauthorized
+            Origin -> CDN : 401 Unauthorized
+            activate CDN
+            CDN -> UA : 401 Unauthorized
+          else user authorized
+
+            Origin -> Origin:create or reuse\naccess_token
+
+            Origin -> CDN: 200 OK\nTokenRespHdr: <access_token>
+            deactivate Origin
+
+            CDN->CDN:validate access_token
+
+            alt invalid access_token
+              CDN -> UA : 520 Error
+            else
+              alt config:use_redirect=//true//
+                CDN -> UA : 302 Redirect\nSet-Cookie: TokenCookie=<access_token>\nLocation: https://example-cdn.com/path/object
+              else config:use_redirect=//false//
+                CDN -> UA : 200 OK\nSet-Cookie: TokenCookie=<access_token>
+              end
+            end
+
+          end
+        else valid access_token AND cache-hit
+          CDN -> UA : 200 OK
+          deactivate CDN
+        end
+
+
+        @enduml
+
+
+
+_`Use Case 1`: Proxy only mode using HTTP cookies.
+--------------------------------------------------
+
+
+**<01-02>**. When a request from the UA_ is received at the CDN_ the value of ``TokenCookie`` is extracted and the access token is validated (missing cookie or access token is same as invalid).
+
+**<03>**. If the access token is valid its opaque `subject` is extracted, added to the cache key and a cache lookup is performed.
+
+**<06>**. Missing or invalid access token or a cache-miss leads to forwarding the request to the origin_ either for user authorization and/or for fetching the object from origin_.
+
+**<07>**. The origin_ performs authentication and authorization of the user using IdMS_, DS_, etc. All related authentication and authorization flows are out of scope of this document.
+
+**<08-09>**. If the user is unauthorized then ``401 Unauthorized`` is passed from the origin_ to the CDN_ and then to UA_.
+
+**<10-11>**. If the user is authorized then an access token is returned by a response header ``TokenRespHdr`` to the CDN_ and gets validated at the Edge_ before setting the ``TokenCookie``.
+
+**<12-13>**. If the validation of the access token received from the origin_ fails the origin_ response is considered invalid and ``520 Error`` is returned to UA_.
+
+**<15>**. If the validation of the access token received from the origin_ succeeds then the object is returned to UA_ and a ``TokenCookie`` is set to the new access token with the CDN_ response.
+
+**<16>**. If the access token initial UA_ request is valid and there is a cache-hit the corresponding object is delivered to the UA_.
+
+In this use case the request with a missing or invalid token is never cached (cache skipped) since we don't have or cannot trust the `subject` from the access token to do a cache lookup and since Apache Traffic Server does not have the ability to change the cache key when it receives the Origin_ response it is impossible to cache the object based on the just received new valid token from the Origin_.
+
+All subsequent requests having a valid token will be cached normally and if the access token is valid for long enough time not caching just the first request should not be a problem, for use cases where we cannot afford not caching the first request please see `use case 2`_.
+
+
+
+_`Use Case 2`: Proxy only mode using HTTP cookies and redirects.
+----------------------------------------------------------------
+
+This use case is similar to `use case 1`_ but makes sure all (cacheable) requests are cached (even the one that does not have a valid access token in the UA_ request).
+
+**<05>** In case of invalid access token instead of forwarding the original HTTP request to the origin_ a ``HEAD`` request with all headers from the original request is sent to the origin_.
+
+**<14>** When the origin_ responds with a valid access token in ``TokenRespHdr`` the CDN_ sets the ``TokenCookie`` by using a ``302 Redirect`` response with a ``Location`` header containing the URI of original UA_ request.
+
+In this way the after the initial failure the UA_ request is repeated with a valid access token and can be safely cached in the CDN_ cache (if the object is catchable)
+
+The support of this use case is still not implemented.
+
+
+Access token
+============
+
+The access token could contain the following claims:
+
+* _`subject` - the subject of the access token is an opaque string provided by the application_, in `use case 1`_ and `use case 2`_ the subject signifies a `target audience`_
+* _`expiration` - `Unix time <https://en.wikipedia.org/wiki/Unix_time>`_ after which the access token is not considered valid (expired)
+* _`not before time` - `Unix time <https://en.wikipedia.org/wiki/Unix_time>`_ before which the access token is not considered valid (used before its time)
+* _`issued at time` - `Unix time <https://en.wikipedia.org/wiki/Unix_time>`_ the access token was issued
+* _`token id` - unique opaque token id for debugging and tracking assigned by the application_,
+* _`version` - access token version
+* _`scope` - defines the scope in which this subject is valid
+* _`key id` - the key in a map or database of secrets to be used to calculate the digest
+* _`signature type`  - name of the HMAC hash function / cryptographic signature scheme to be used for calculating the message digest, supported signature types: ``HMAC-SHA-256``, ``HMAC-SHA-512``, ``RSA-PSS`` (still not implemented)
+* _`message digest` - the message digest that signs the access token
+
+
+To make the plugin more configurable and to support more use cases various formats could be supported in the future, i.e `Named Claim` formats , `Positional Claim` formats, `JWT` [4]_, etc.
+
+The format of the access token will be specifiable **only** through the plugin configuration by design and not the access token since migrations from one format to another during upgrades are not expected in normal circumstances.
+
+Changes in claim names (claim positions in `Positional Claims`), in their interpretation, adding new claims, removing claims, switching from `required` to `optional` and vice versa will be handled by having a `version`_ claim in the token.
+
+`Version`_ and `signature type`_ claims are part of the token ("user input") to allow easier migration between different versions and signature types, but they could be overridable through configuration in future versions to force the usage only to specific versions or signature types (in which case the corresponding claim could be omitted from the token and would be ignored if specified).
+
+
+The following `Named Claim` format is the only one currently supported.
+
+
+Query-Param-Style Named Claim format
+------------------------------------
+
+* claim names
+   * ``sub`` for `subject`_, `required`
+   * ``exp`` for `expiration`_, `required`
+   * ``nbf`` for `not before time`_, `optional`
+   * ``iat`` for `issued at time`_, `optional`
+   * ``tid`` for `token id`_, `optional`
+   * ``ver`` for `version`_, `optional`, defaults to ``ver=1`` if not specified.
+   * ``scope`` for `scope`_, `optional`, ignored by by the current version of the plugin, still not finalized (more applications and their use cases need to be studied to finalize the format)
+   * ``kid`` for `key id`_, `required` (tokens to be always signed)
+   * ``st`` for `signature type`_, `optional` (default would be ``SHA-256`` if not specified)
+   * ``md`` for `message digest`_ - this claim is `required` and expected to be always the last claim.
+* delimiters
+   * claims are separated by ``&``
+   * keys and values in each claim are separated by ``=``
+* notes and limitations
+   * if any claim value contains ``&`` or ``=`` escaping would be necessary (i.e. through Percent-Encoding [6]_)
+   * the size of the access token cannot be larger then 4K to limit the amount of data the application_ could fit in the opaque claims, in general the access token is meant to be small since it could end up stored in a cookie and be sent as part of lots and lots of requests.
+   * during signing the access token payload should end with ``&md=`` and the calculated `message digest`_ would be appended directly to the payload to form the token (see the example below).
+
+
+Let us say we have a user `Kermit the Frog <https://en.wikipedia.org/wiki/Kermit_the_Frog>`_ and a user `Michigan J. Frog <https://en.wikipedia.org/wiki/Michigan_J._Frog>`_ who are part of a `target audience`_ ``frogs-in-a-well`` and a user `Nemo the Clownfish <https://en.wikipedia.org/wiki/Finding_Nemo>`_ who is part of a `target audience`_ ``fish-in-a-sea``.
+
+Both users `Kermit the Frog <https://en.wikipedia.org/wiki/Kermit_the_Frog>`_ and `Michigan J. Frog <https://en.wikipedia.org/wiki/Michigan_J._Frog>`_ will be authorized with the following access token:
+
+.. code-block:: bash
+
+  sub="frogs-in-a-well"   # opaque id assigned by origin
+  exp="1577836800"       # token expires   : 01/01/2020 @ 12:00am (UTC)
+  nbf="1514764800"       # don't use before: 01/01/2018 @ 12:00am (UTC)
+  iat="1514160000"       # token issued at : 12/25/2017 @ 12:00am (UTC)
+  tid="1234567890"       # unique opaque id assigned by origin (i.e UUID)
+  kid="key1"             # secret corresponding to this key is 'PEIFtmunx9'
+
+
+Constructing the access token using `openssl` tool (from `bash`):
+
+.. code-block:: bash
+
+  payload='sub=frogs-in-a-well&exp=1577836800&nbf=1514764800&iat=1514160000&tid=1234567890&kid=key1&st=HMAC-SHA-256&md='
+  signature=$(echo -n ${payload} | openssl dgst -sha256 -hmac "PEIFtmunx9")
+  access_token=${payload}${signature}
+
+
+The application_ would create and send the access token in a response header ``TokenRespHdr``:
+
+.. code-block:: bash
+
+  TokenRespHdr: sub=frogs-in-a-well&exp=1577836800&nbf=1514764800&iat=1514160000&tid=1234567890&kid=key1&st=HMAC-SHA-256&md=8879af98ab6071315a7ab55e5245cbe1c106303bcc4690cbfc807a4402d11ab3
+
+
+CDN_ would set a cookie ``TokenCookie``:
+
+.. code-block:: bash
+
+  TokenCookie=c3ViPWZyb2dzLWluLWEtd2VsbCZleHA9MTU3NzgzNjgwMCZuYmY9MTUxNDc2NDgwMCZpYXQ9MTUxNDE2MDAwMCZ0aWQ9MTIzNDU2Nzg5MCZraWQ9a2V5MSZzdD1ITUFDLVNIQS0yNTYmbWQ9ODg3OWFmOThhYjYwNzEzMTVhN2FiNTVlNTI0NWNiZTFjMTA2MzAzYmNjNDY5MGNiZmM4MDdhNDQwMmQxMWFiMw; Expires=Wed, 01 Jan 2020 00:00:00 GMT; Secure; HttpOnly
+
+
+The value of the cookie is the access token provided by the origin_ encoded with a `base64url Encoding without Padding` [4]_
+
+The following attributes are added to the `Set-Cookie` header [5]_:
+
+* ``Secure`` - instructs the UA_ to include the cookie in an HTTP request only if the request is transmitted over a secure channel, typically HTTP over Transport Layer Security (TLS)
+* ``HttpOnly`` - instructs the UA_ to omit the cookie when providing access to cookies via "non-HTTP" APIs such as a web browser API that exposes cookies to scripts
+* ``Expires`` - this attribute will be set to the time specified in the `expiration`_ claim
+
+Just for a reference the following access token would be assigned to the user `Nemo the Clownfish <https://en.wikipedia.org/wiki/Finding_Nemo>`_:
+
+.. code-block:: bash
+
+  sub=fish-in-a-sea&exp=1577836800&nbf=1514764800&iat=1514160000&tid=2345678901&kid=key1&st=HMAC-SHA-256&md=a43d8a46804d9e9319b7d1337007eed73daf37105f1feaae1d68567389654f88
+
+
+Plugin configuration
+====================
+
+* Specify where to look for the access token
+   * ``--check-cookie=<cookie_name>`` (`optional`, default:empty/unused) - specifies the name of the cookie that contains the access token, although it is optional if not specified the plugin does not perform access control since this is the only currently supported access token source.
+   * ``--token-response-header=<header_name>`` (`optional`, default:empty/unused) - specifies the origin_ response header name that contains the access token passed from the origin_ to the CDN, although it is optional this is the only currently supported way to get the access token from the origin_.
+
+
+* Signify some common failures through HTTP status code.
+   * ``--invalid-syntax-status-code=<http_status_code>`` (`optional`, default:``400``) - access token bad syntax error
+   * ``--invalid-signature-status-code=<http_status_code>`` (`optional`, default:``401``) - invalid access token signature
+   * ``--invalid-timing-status-code=<http_status_code>`` (`optional`, default:``403``) - bad timing when validating the access token (expired, or too early)
+   * ``--invalid-scope-status-code=<http_status_code>`` (`optional`, default:``403``) - access token scope validation failed
+   * ``--invalid-origin-response=<http_status_code>`` (`optional`, default:``520``) - origin_ response did not look right, i.e. the access token provided by origin_ is not valid.
+   * ``--internal-error-status-code=<http_status_code>`` (`optional`, default:``500``) - unexpected internal error (should not happened ideally)
+
+* Extract information into a request header
+   * ``--extract-subject-to-header=<header_name>`` (`optional`, default:empty/unused) - extract the access token `subject` claim into a request header with ``<header_name>`` for debugging purposes and logging or to be able to modify the cache key by using :ref:`admin-plugins-cachekey` plugin.
+   * ``--extract-tokenid-to-header=<header_name>`` (`optional`, default:empty/unused) - extract the access token `token id` claim into a request header with ``<header_name>`` for debugging purposes and logging
+   * ``--extract-status-to-header=<header_name>`` (`optional`, default:empty/unused) - extract the access token validation statusa request header with ``<header_name>`` for debugging purposes and logging
+
+
+* Plugin setup related
+   * ``--symmetric-keys-map=<txt_file_name>`` (`optional`, default: empty/unused) - the name of a file containing a map of symmetric encrypt secrets, secrets are expected one per line in format ``key_name_N=secret_value_N`` (key names are used in access token signature validation, multiple keys would be useful for key rotation). Although it is `optional` this is the only source of secrets supported and if not specified / used access token validation would constantly fail.
+   * ``--include-uri-paths-file`` (`optional`, default:empty/unused) - a file containing a list of regex expressions to be matched against URI paths. The access control is applied to paths that match.
+   * ``--exclude-uri-paths-file`` (`optional`, default:empty/unused) - a file containing a list of regex expressions to be matched against URI paths. The access control is applied to paths that do not match.
+
+* Behavior modificators to support various use-cases
+   * ``--reject-invalid-token-requests`` (`optional`, default:``false``) - reject invalid token requests instead of forwarding them to origin_.
+   * ``--use-redirects`` (`optional`, default:``false``) - used to configure `use case 2`_, not implemented yet.
+
+
+
+Configuration and Troubleshooting examples
+------------------------------------------
+
+The following configuration can be used to implement `use case 1`_
+
+Configuration files
+~~~~~~~~~~~~~~~~~~~
+
+* Apache traffic server ``remap.config``
+
+:ref:`admin-plugins-cachekey` is used to add the access token `subject` into the cache key (``@TokenSubject``). and should always follow the :ref:`admin-plugins-access_control` in the remap rule in order for this mechanism to work properly.
+
+.. code-block:: bash
+
+  map https://example-cdn.com http://example.com \
+      @plugin=access_control.so \
+          @pparam=--symmetric-keys-map=hmac_keys.txt \
+          @pparam=--check-cookie=TokenCookie \
+          @pparam=--extract-subject-to-header=@TokenSubject \
+          @pparam=--extract-tokenid-to-header=@TokenId \
+          @pparam=--extract-status-to-header=@TokenStatus \
+          @pparam=--token-response-header=TokenRespHdr \
+      @plugin=cachekey.so \
+          @pparam=--static-prefix=views \
+          @pparam=--include-headers=@TokenSubject
+
+
+
+* Secrets map ``hmac_keys.txt``
+
+.. code-block:: bash
+
+  $ cat etc/trafficserver/hmac_keys.txt
+  key1=PEIFtmunx9
+  key2=BtYjpTbH6a
+  key3=SS75kgYonh
+  key4=qMmCV2vUsu
+  key5=YfMxMaygax
+  key6=tVeuPtfJP8
+  key7=oplEZT5CpB
+
+
+* Format the ``access_control.log``
+
+.. code-block:: bash
+
+  access_control_format = format {
+    Format = '%<cqtq> sub=%<{@TokenSubject}cqh> tid=%<{@TokenId}cqh> status=%<{@TokenStatus}cqh> cache=%<{x-cache}psh> key=%<{x-cache-key}psh>'  }
+
+  log.ascii {
+    Filename = 'access_control',
+    Format = access_control_format
+  }
+
+
+* X-Debug plugin added to ``plugin.config``
+
+.. code-block:: bash
+
+  $ cat etc/trafficserver/plugin.config
+  xdebug.so
+
+
+
+Configuration tests and troubleshooting
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let us assume that the origin_ responds with the access tokens considered in `Query-Param-Style Named Claim format`_ corresponding to user `Kermit the Frog <https://en.wikipedia.org/wiki/Kermit_the_Frog>`_ and user `Nemo the Clownfish <https://en.wikipedia.org/wiki/Finding_Nemo>`_
+
+If user `Kermit the Frog <https://en.wikipedia.org/wiki/Kermit_the_Frog>`_ sends a request without a valid token, i.e ``TokenCookie`` is missing from the request. Cache key would be ``/views/object`` but never used since the cache is always ``skipped``.
+
+After the origin_ responds with a valid access token (assuming the user authentication and authorization succeeded) the plugin will respond with ``Set-Cookie`` header containing the new access token.
+
+.. code-block:: bash
+
+  $ curl -sD - https://example-cdn.com/object \
+      -H 'X-Debug:X-Cache, X-Cache-Key' \
+    | grep -ie 'x-cache' -e 'tokencookie'
+
+  set-cookie: TokenCookie=c3ViPWZyb2dzLWluLWEtd2VsbCZleHA9MTU3NzgzNjgwMCZuYmY9MTUxNDc2NDgwMCZpYXQ9MTUxNDE2MDAwMCZ0aWQ9MTIzNDU2Nzg5MCZraWQ9a2V5MSZzdD1ITUFDLVNIQS0yNTYmbWQ9ODg3OWFmOThhYjYwNzEzMTVhN2FiNTVlNTI0NWNiZTFjMTA2MzAzYmNjNDY5MGNiZmM4MDdhNDQwMmQxMWFiMw; Expires=Wed, 01 Jan 2020 00:00:00 GMT; Secure; HttpOnly
+  x-cache-key: /views/object
+  x-cache: skipped
+
+
+Now let us send the same request with a valid access token, add the ``TokenCookie`` to the request. Cache key will be ``/views/@TokenSubject:frogs-in-a-well/object`` but since the object is not in the cache we get ``miss``.
+
+.. code-block:: bash
+
+  $ curl -sD - https://example-cdn.com/object \
+      -H 'X-Debug:X-Cache, X-Cache-Key' \
+      -H 'cookie: TokenCookie=c3ViPWZyb2dzLWluLWEtd2VsbCZleHA9MTU3NzgzNjgwMCZuYmY9MTUxNDc2NDgwMCZpYXQ9MTUxNDE2MDAwMCZ0aWQ9MTIzNDU2Nzg5MCZraWQ9a2V5MSZzdD1ITUFDLVNIQS0yNTYmbWQ9ODg3OWFmOThhYjYwNzEzMTVhN2FiNTVlNTI0NWNiZTFjMTA2MzAzYmNjNDY5MGNiZmM4MDdhNDQwMmQxMWFiMw; Expires=Wed, 01 Jan 2020 00:00:00 GMT; Secure; HttpOnly' \
+    | grep -ie 'x-cache' -e 'tokencookie'
+
+  x-cache-key: /views/@TokenSubject:frogs-in-a-well/object
+  x-cache: miss
+
+
+Now let us send the same request again and since the object is in cache we get ``hit-fresh``.
+
+
+.. code-block:: bash
+
+  $ curl -sD - https://example-cdn.com/object \
+      -H 'X-Debug:X-Cache, X-Cache-Key' \
+      -H 'cookie: TokenCookie=c3ViPWZyb2dzLWluLWEtd2VsbCZleHA9MTU3NzgzNjgwMCZuYmY9MTUxNDc2NDgwMCZpYXQ9MTUxNDE2MDAwMCZ0aWQ9MTIzNDU2Nzg5MCZraWQ9a2V5MSZzdD1ITUFDLVNIQS0yNTYmbWQ9ODg3OWFmOThhYjYwNzEzMTVhN2FiNTVlNTI0NWNiZTFjMTA2MzAzYmNjNDY5MGNiZmM4MDdhNDQwMmQxMWFiMw; Expires=Wed, 01 Jan 2020 00:00:00 GMT; Secure; HttpOnly' \
+    | grep -ie 'x-cache' -e 'tokencookie'
+
+  x-cache-key: /views/@TokenSubject:frogs-in-a-well/object
+  x-cache: hit-fresh
+
+
+The previous activity should result in the following log (as defined in ``logging.config``)
+
+.. code-block:: bash
+
+  1521588755.424 sub=- tid=- status=U_UNUSED,O_VALID cache=skipped key=/views/object
+  1521588986.262 sub=frogs-in-a-well tid=this-year-frog-view status=U_VALID,O_UNUSED cache=miss key=/views/@TokenSubject:frogs-in-a-well
+  1521589276.535 sub=frogs-in-a-well tid=this-year-frog-view status=U_VALID,O_UNUSED cache=hit-fresh key=/views/@TokenSubject:frogs-in-a-well
+
+
+Just for a reference the same request for user `Nemo the Clownfish <https://en.wikipedia.org/wiki/Finding_Nemo>`_, with a different subject/target audience ``fish-in-a-sea``, will end up having cache key ``/views/@TokenSubject:fish-in-a-sea/object`` and would never match the same object cached for users in the ``frogs-in-a-well`` audience as they use cache key ``/views/@TokenSubject:frogs-in-a-well/object``.
+
+
+---------------------------
+
+
+References
+==========
+
+.. [1] "The OAuth 1.0 Protocol", `RFC 5849 <https://tools.ietf.org/html/rfc5849>`_, April 2010.
+
+.. [2] "The OAuth 2.0 Authorization Framework", `RFC 6749 <https://tools.ietf.org/html/rfc6749>`_, October 2012
+
+.. [3] "Security Assertion Markup Language 2.0 (SAML 2.0)" `OASIS <https://wiki.oasis-open.org>`_, March 2005.
+
+.. [4] "JSON Web Signature (JWS)", Appendix C. "Notes on Implementing base64url Encoding without Padding", `RFC 7515 <https://tools.ietf.org/html/rfc7515#appendix-C>`_, May 2015
+
+.. [5] "HTTP State Management Mechanism", 4.1 "Set-Cookie", `RFC 6225 <https://tools.ietf.org/html/rfc6265#section-4.1>`_, April 2011
+
+.. [6] "Uniform Resource Identifier (URI): Generic Syntax", 2.1. "Percent-Encoding", `RFC 3986 <https://tools.ietf.org/html/rfc3986#section-2.1>`_, January 2005.
+
diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst
index d88d9fc..566eecf 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -141,6 +141,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
 .. toctree::
    :hidden:
 
+   Access Control <access_control.en>
    Balancer <balancer.en>
    Buffer Upload <buffer_upload.en>
    Collapsed-Forwarding <collapsed_forwarding.en>
@@ -161,6 +162,9 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
    System Statistics <system_stats.en>
    WebP Transform <webp_transform.en>
 
+:doc:`Access Control <access_control.en>`
+   Access control plugin that handles various access control use-cases.
+
 :doc:`Balancer <balancer.en>`
    Balances requests across multiple origin servers.
 
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index f13a9ee..b955a71 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -52,6 +52,7 @@ include test_cppapi/Makefile.inc
 
 if BUILD_EXPERIMENTAL_PLUGINS
 
+include experimental/access_control/Makefile.inc
 include experimental/acme/Makefile.inc
 include experimental/balancer/Makefile.inc
 include experimental/buffer_upload/Makefile.inc
diff --git a/plugins/experimental/access_control/.gitignore b/plugins/experimental/access_control/.gitignore
new file mode 100644
index 0000000..69d3a7a
--- /dev/null
+++ b/plugins/experimental/access_control/.gitignore
@@ -0,0 +1 @@
+test_access_control
diff --git a/plugins/experimental/access_control/Makefile.inc b/plugins/experimental/access_control/Makefile.inc
new file mode 100644
index 0000000..fc4c16b
--- /dev/null
+++ b/plugins/experimental/access_control/Makefile.inc
@@ -0,0 +1,38 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+pkglib_LTLIBRARIES += experimental/access_control/access_control.la
+
+experimental_access_control_access_control_la_SOURCES = \
+  experimental/access_control/access_control.cc \
+  experimental/access_control/common.cc \
+  experimental/access_control/config.cc \
+  experimental/access_control/headers.cc \
+  experimental/access_control/pattern.cc \
+  experimental/access_control/plugin.cc \
+  experimental/access_control/utils.cc
+
+check_PROGRAMS +=  experimental/access_control/test_access_control
+
+experimental_access_control_test_access_control_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DACCESS_CONTROL_UNIT_TEST
+experimental_access_control_test_access_control_LDADD = $(OPENSSL_LIBS)
+experimental_access_control_test_access_control_SOURCES = \
+    experimental/access_control/unit-tests/test_access_control.cc \
+    experimental/access_control/unit-tests/test_utils.cc \
+    experimental/access_control/access_control.cc \
+    experimental/access_control/common.cc \
+    experimental/access_control/utils.cc
+
diff --git a/plugins/experimental/access_control/README.md b/plugins/experimental/access_control/README.md
new file mode 100644
index 0000000..564aeae
--- /dev/null
+++ b/plugins/experimental/access_control/README.md
@@ -0,0 +1 @@
+The access_control plugin covers common use-cases related to providing access control to the objects stored in CDN cache, for more details please see [Access Control Plugin documentation](../../../doc/admin-guide/plugins/access_control.en.rst)
diff --git a/plugins/experimental/access_control/access_control.cc b/plugins/experimental/access_control/access_control.cc
new file mode 100644
index 0000000..7ec8a98
--- /dev/null
+++ b/plugins/experimental/access_control/access_control.cc
@@ -0,0 +1,497 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file access_control.cc
+ * @brief access control utilities
+ */
+
+#include <iostream>
+#include <string>
+
+#include "access_control.h"
+
+size_t calcMessageDigest(const StringView hf, const char *secret, const char *message, size_t messageLen, char *buffer, size_t len);
+const char *getSecretMap(const StringMap &map, const StringView &key, size_t &secretSize);
+
+/* AccessToken ***************************************************************************************************** */
+
+AccessToken::AccessToken(const StringMap &secretsMap, bool enableDebug) : _debug(enableDebug), _secretsMap(secretsMap) {}
+
+AccessTokenStatus
+AccessToken::validate(const StringView token, time_t time)
+{
+  if (token.empty()) {
+    /* Empty token is likely not valid, so short-cut here. */
+    return _state = INVALID_SYNTAX;
+  }
+
+  /* Parse and validate syntax */
+  if (VALID != parse(token)) {
+    return _state;
+  }
+
+  /* Validate field semantics and set defaults */
+  if (VALID != validateSemantics()) {
+    return _state;
+  }
+
+  /* Validate signature */
+  if (VALID != validateSignature()) {
+    return _state;
+  }
+
+  /* Now after we validated the signature, check timing */
+  if (VALID != validateTiming(time)) {
+    return _state;
+  }
+
+  /** @todo: validate scope eventually */
+
+  return _state;
+}
+
+AccessTokenStatus
+AccessToken::validateSemantics()
+{
+  /* Check for required or incompatible fields, set defaults */
+  if (_subject.empty()) {
+    ERROR_OUT("missing subject field, what are we signing and validating?");
+    return _state = MISSING_REQUIRED_FIELD;
+  }
+
+  if (_expiration.empty()) {
+    ERROR_OUT("missing expiration field, have to limit the life of the token");
+    return _state = MISSING_REQUIRED_FIELD;
+  }
+
+  if (_keyId.empty()) {
+    ERROR_OUT("missing keyId field, at least one key should be specified");
+    return _state = MISSING_REQUIRED_FIELD;
+  }
+
+  if (_messageDigest.empty()) {
+    ERROR_OUT("missing md field");
+    return _state = MISSING_REQUIRED_FIELD;
+  }
+
+  /* Semantics checked and defaults set successfully */
+  return _state;
+}
+
+AccessTokenStatus
+AccessToken::validateSignature()
+{
+  /* Get secret needed for the signature */
+  size_t secretLen   = 0;
+  const char *secret = getSecretMap(_secretsMap, _keyId, secretLen);
+  if (nullptr == secret || 0 == secretLen) {
+    ERROR_OUT("failed to find the secret for key id: '" << _keyId << "'");
+    return _state = INVALID_SECRET;
+  }
+
+  /* Calculate signature. */
+  char computedMd[MAX_MSGDIGEST_BUFFER_SIZE];
+  size_t computedMdLen = 0;
+  computedMdLen = calcMessageDigest(_hashFunction, secret, _payload.data(), _payload.size(), computedMd, MAX_MSGDIGEST_BUFFER_SIZE);
+  if (0 == computedMdLen) {
+    ERROR_OUT("failed to calculate message digest");
+    return _state = INVALID_SIGNATURE;
+  }
+
+  /* Convert MD from the package into binary before comparing */
+  char tokenMd[MAX_MSGDIGEST_BUFFER_SIZE];
+  memset(tokenMd, 0, MAX_MSGDIGEST_BUFFER_SIZE);
+  size_t tokenMdLen = hexDecode(_messageDigest.data(), _messageDigest.size(), tokenMd, MAX_MSGDIGEST_BUFFER_SIZE);
+  if (0 == tokenMdLen) {
+    DEBUG_OUT("failed to hex-decode token md");
+    return _state = INVALID_FIELD_VALUE;
+  }
+  DEBUG_OUT("token md=" << _messageDigest);
+
+  /* Signature check first */
+  if (!cryptoMessageDigestEqual((const char *)tokenMd, tokenMdLen, (const char *)computedMd, computedMdLen)) {
+    ERROR_OUT("invalid signature");
+    return _state = INVALID_SIGNATURE;
+  }
+
+  /* Valid signature (MD) */
+  return _state;
+}
+
+AccessTokenStatus
+AccessToken::validateTiming(time_t time)
+{
+  time_t t = 0;
+
+  _validationTime = time; /* saved for debugging / troubleshooting */
+
+  /* Validate and check not before timestamp */
+  if (!_notBefore.empty()) {
+    if (0 == (t = string2int(_notBefore))) {
+      return _state = INVALID_FIELD_VALUE;
+    } else {
+      if (time <= t) {
+        return _state = TOO_EARLY;
+      }
+    }
+  }
+
+  /* Validate and check expiration timestamp */
+  if (!_expiration.empty()) {
+    if (0 == (t = string2int(_expiration))) {
+      return _state = INVALID_FIELD_VALUE;
+    } else {
+      if (time > t) {
+        return _state = TOO_LATE;
+      }
+    }
+  }
+
+  /* "issued at" time-stamp is currently only for info, so check if the time-stamp is valid only */
+  if (!_issuedAt.empty() && 0 == (t = string2int(_issuedAt))) {
+    return _state = INVALID_FIELD_VALUE;
+  }
+
+  return _state;
+}
+
+/* KvpAccessToken ************************************************************************************************** */
+
+KvpAccessToken::KvpAccessToken(const KvpAccessTokenConfig &tokenConfig, const StringMap &secretsMap, bool enableDebug)
+  : AccessToken(secretsMap, enableDebug), _tokenConfig(tokenConfig)
+{
+}
+
+AccessTokenStatus
+KvpAccessToken::parse(const StringView token)
+{
+  /* Initializing it here, clear the unused state, assume VALID and try to find problems */
+  _state = VALID;
+  _token = token;
+
+  DEBUG_OUT("token:'" << _token << "'");
+
+  size_t payloadSize = 0;
+  size_t prev        = 0;
+  size_t pos         = 0;
+  do {
+    /* Look for the next KVP */
+    pos              = _token.find(_tokenConfig.pairDelimiter, prev);
+    StringView kvp   = _token.substr(prev, pos - prev);
+    size_t equalsign = kvp.find(_tokenConfig.kvDeliiter);
+    if (kvp.npos == equalsign) {
+      ERROR_OUT("invalid key-value-pair, missing key-value delimiter");
+      return _state = INVALID_SYNTAX;
+    }
+    StringView key   = kvp.substr(0, equalsign);
+    StringView value = equalsign != kvp.npos ? kvp.substr(equalsign + 1) : "";
+
+    DEBUG_OUT("kvp:'" << kvp << "', key:'" << key << "', value:'" << value << "'");
+
+    /* Initialize the corresponding member */
+    payloadSize = prev;
+    if (_tokenConfig.subjectName == key) {
+      _subject = value;
+    } else if (_tokenConfig.expirationName == key) {
+      _expiration = value;
+    } else if (_tokenConfig.notBeforeName == key) {
+      _notBefore = value;
+    } else if (_tokenConfig.issuedAtName == key) {
+      _issuedAt = value;
+    } else if (_tokenConfig.tokenIdName == key) {
+      _tokenId = value;
+    } else if (_tokenConfig.versionName == key) {
+      _version = value;
+    } else if (_tokenConfig.scopeName == key) {
+      _scope = value;
+    } else if (_tokenConfig.keyIdName == key) {
+      _keyId = value;
+    } else if (_tokenConfig.hashFunctionName == key) {
+      _hashFunction = value;
+    } else if (_tokenConfig.messageDigestName == key) {
+      _messageDigest = value;
+    } else {
+      ERROR_OUT("failed to construct a valid access token");
+      return _state = INVALID_FIELD;
+    }
+
+    prev = pos + _tokenConfig.kvDeliiter.size();
+  } while (pos != token.npos);
+
+  /* Now identify the pay-load which was signed */
+  payloadSize += _tokenConfig.messageDigestName.size() + _tokenConfig.kvDeliiter.size();
+  _payload = _token.substr(0, payloadSize);
+
+  DEBUG_OUT("payload:'" << _payload << "'");
+
+  /* successful parsing */
+  return _state;
+}
+
+/* AccessTokenBuilder ********************************************************************************************** */
+
+KvpAccessTokenBuilder::KvpAccessTokenBuilder(const KvpAccessTokenConfig &config, const StringMap &secretsMap)
+  : _config(config), _secretsMap(secretsMap)
+{
+  cryptoMagicInit();
+}
+
+void
+KvpAccessTokenBuilder::appendKeyValuePair(const StringView &key, const StringView value)
+{
+  _buffer.append(_buffer.empty() ? "" : _config.pairDelimiter);
+  _buffer.append(key).append(_config.kvDeliiter).append(value);
+}
+
+void
+KvpAccessTokenBuilder::addSubject(const StringView sub)
+{
+  appendKeyValuePair(_config.subjectName, sub);
+}
+void
+KvpAccessTokenBuilder::addExpiration(time_t exp)
+{
+  appendKeyValuePair(_config.expirationName, std::to_string(exp));
+}
+void
+KvpAccessTokenBuilder::addNotBefore(time_t nbf)
+{
+  appendKeyValuePair(_config.notBeforeName, std::to_string(nbf));
+}
+void
+KvpAccessTokenBuilder::addIssuedAt(time_t iat)
+{
+  appendKeyValuePair(_config.issuedAtName, std::to_string(iat));
+}
+void
+KvpAccessTokenBuilder::addTokenId(const StringView tid)
+{
+  appendKeyValuePair(_config.tokenIdName, tid);
+}
+void
+KvpAccessTokenBuilder::addVersion(const StringView ver)
+{
+  appendKeyValuePair(_config.versionName, ver);
+}
+void
+KvpAccessTokenBuilder::addScope(const StringView scope)
+{
+  appendKeyValuePair(_config.scopeName, scope);
+}
+void
+KvpAccessTokenBuilder::sign(const StringView kid, const StringView hf)
+{
+  appendKeyValuePair(_config.keyIdName, kid);
+  appendKeyValuePair(_config.hashFunctionName, hf);
+  appendKeyValuePair(_config.messageDigestName, ""); /* add an empty message digest and append the actual digest later */
+
+  char md[MAX_MSGDIGEST_BUFFER_SIZE];
+  size_t secretLen   = 0;
+  const char *secret = getSecretMap(_secretsMap, kid, secretLen);
+  if (nullptr == secret || 0 == secretLen) {
+    ERROR_OUT("failed to find the secret for kid='" << kid << "'");
+    return;
+  }
+
+  size_t mdLen = calcMessageDigest(hf, secret, _buffer.data(), _buffer.size(), md, MAX_MSGDIGEST_BUFFER_SIZE);
+  if (0 == mdLen) {
+    DEBUG_OUT("failed to calculate message digest");
+  } else {
+    /* Hex-encode signature. */
+    char mdHexLenMax = 2 * mdLen + 1;
+    char mdHex[mdHexLenMax];
+    size_t mdHexLen = hexEncode(md, mdLen, mdHex, mdHexLenMax);
+    if (0 == mdHexLen) {
+      DEBUG_OUT("failed to hex-encode new MD");
+    } else {
+      DEBUG_OUT(_config.messageDigestName << "=" << StringView(mdHex, mdHexLen) << " (" << (int)mdHexLen << ")");
+      _buffer.append(mdHex, mdHexLen);
+    }
+  }
+}
+
+const char *
+KvpAccessTokenBuilder::get()
+{
+  return _buffer.c_str();
+}
+/* Crypto related ********************************************************************************************** */
+
+/* OpenSSL library hash function names */
+#define LIBSSL_HASH_SHA256 "SHA256"
+#define LIBSSL_HASH_SHA512 "SHA512"
+
+/* encryption digest algorithm to openssl digest algorithm names. */
+static std::map<String, String>
+createStaticDigestAlgoMap()
+{
+  std::map<String, String> algos;
+  algos[WDN_HASH_SHA256] = LIBSSL_HASH_SHA256;
+  algos[WDN_HASH_SHA512] = LIBSSL_HASH_SHA512;
+  return algos;
+};
+
+/**
+ * A static map that maps well-defined name to openssl names.
+ */
+static const std::map<String, String> _digestAlgosMap = createStaticDigestAlgoMap();
+
+/**
+ * @brief Calculates message digest
+ *
+ * @param hf Hash Function (HF) [optional]
+ * @param secret secret
+ * @param message input message
+ * @param messageLen input message lenght
+ * @param buffer output buffer for storing the message digest
+ * @param len output buffer length
+ * @return number of characters actually written to the output buffer.
+ */
+size_t
+calcMessageDigest(const StringView hf, const char *secret, const char *message, size_t messageLen, char *buffer, size_t len)
+{
+  if (hf.empty()) {
+    return cryptoMessageDigestGet(LIBSSL_HASH_SHA256, message, messageLen, secret, strlen(secret), buffer, len);
+  } else {
+    std::map<String, String>::const_iterator it = _digestAlgosMap.find(String(hf.data(), hf.size()));
+    if (_digestAlgosMap.end() == it) {
+      AccessControlError("Unsupported digest name '%.*s'", (int)hf.size(), hf.data());
+      return 0;
+    }
+
+    return cryptoMessageDigestGet(it->second.c_str(), message, messageLen, secret, strlen(secret), buffer, len);
+  }
+}
+
+/**
+ * @brief Get a secret from a map of secrets based on an index (i.e. KID)
+ *
+ * @param map string map containing secrets
+ * @param key string containing the key (i.e. KID)
+ * @return ptr to NULL-terminated C-string with the secret.
+ */
+const char *
+getSecretMap(const StringMap &map, const StringView &key, size_t &secretSize)
+{
+  secretSize = 0;
+
+  if (map.empty()) {
+    DEBUG_OUT("secrets map is empty");
+    return nullptr;
+  }
+  const char *result = nullptr;
+  StringMap::const_iterator it;
+  it = map.find(String(key));
+  if (map.end() != it) {
+    result     = it->second.c_str();
+    secretSize = it->second.size();
+#ifdef ACCESS_CONTROL_LOG_SECRETS
+    DEBUG_OUT("secrets[" << key << "] = '" << result << "'");
+#endif
+  } else {
+    DEBUG_OUT("secrets[" << key << "] does not exist");
+  }
+  return result;
+}
+
+/**
+ * Access token validation status converted string representation.
+ */
+const char *
+accessTokenStatusToString(const AccessTokenStatus &state)
+{
+  const char *s = nullptr;
+  switch (state) {
+  case VALID:
+    s = "VALID";
+    break;
+  case UNUSED:
+    s = "UNUSED";
+    break;
+  case INVALID_SYNTAX:
+    s = "PARSING_FAILURE";
+    break;
+  case MISSING_REQUIRED_FIELD:
+    s = "MISSING_REQUIRED_FIELD";
+    break;
+  case INVALID_FIELD:
+    s = "UNEXPECTED_FIELD";
+    break;
+  case INVALID_FIELD_VALUE:
+    s = "INVALID_FIELD_VALUE";
+    break;
+  case INVALID_VERSION:
+    s = "UNSUPORTED_VERSION";
+    break;
+  case INVALID_SECRET:
+    s = "NO_SECRET_SPECIFIED";
+    break;
+  case INVALID_SIGNATURE:
+    s = "INVALID_SIGNATURE";
+    break;
+  case TOO_EARLY:
+    s = "TOO_EARLY";
+    break;
+  case TOO_LATE:
+    s = "TOO_LATE";
+    break;
+  case INVALID_SCOPE:
+    s = "INVALID_SCOPE";
+    break;
+  case OUT_OF_SCOPE:
+    s = "OUT_OF_SCOPE";
+    break;
+  case INVALID_KEYID:
+    s = "INVALID_KEYID";
+    break;
+  case INVALID_HASH_FUNCTION:
+    s = "UNSUPORTED_HASH_FUNCTION";
+    break;
+  default:
+    s = "";
+    break;
+  }
+  return s;
+}
+
+/* Debug dump of the token */
+std::ostream &
+operator<<(std::ostream &os, const AccessToken &token)
+{
+  os << "=== debug ==============================" << std::endl;
+  os << "(d) token     : '" << token._token << "'" << std::endl;
+  os << "(d) state     : " << accessTokenStatusToString(token._state) << std::endl;
+  os << "(d) checked-at: " << token._validationTime << std::endl;
+  os << "=== claims =============================" << std::endl;
+  os << "(r) subject   : '" << token._subject << "'" << std::endl;
+  os << "--- timing -----------------------------" << std::endl;
+  os << "(o) expiration: '" << token._expiration << "' (" << token.getExpiration() << ")" << std::endl;
+  os << "(o) not-before: '" << token._notBefore << "' (" << token.getNotBefore() << ")" << std::endl;
+  os << "(o) issued-at : '" << token._issuedAt << "' (" << token.getIssuedAt() << ")" << std::endl;
+  os << "----------------------------------------" << std::endl;
+  os << "(o) token-id  : '" << token._tokenId << "'" << std::endl;
+  os << "(o) version   : '" << token._version << "'" << std::endl;
+  os << "(o) scope     : '" << token._scope << "'" << std::endl;
+  os << "--- signature related ------------------" << std::endl;
+  os << "(o) key-id    : '" << token._keyId << "'" << std::endl;
+  os << "(o) hash-func : '" << token._hashFunction << "'" << std::endl;
+  os << "(r) digest    : '" << token._messageDigest << "'" << std::endl;
+
+  return os;
+}
diff --git a/plugins/experimental/access_control/access_control.h b/plugins/experimental/access_control/access_control.h
new file mode 100644
index 0000000..dbc9f6f
--- /dev/null
+++ b/plugins/experimental/access_control/access_control.h
@@ -0,0 +1,294 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file access_control.h
+ * @brief access control include file
+ */
+
+#pragma once
+
+#include <ctime>
+#include <map>
+#include <iostream>
+
+#include "common.h"
+#include "utils.h"
+
+/* Quick and dirty development only output, @todo will do something more useful later so we can use it in production debugging */
+#define DEBUG_OUTPUT_ENABLED false
+
+#define DEBUG_PRINT(x)          \
+  do {                          \
+    if (DEBUG_OUTPUT_ENABLED) { \
+      std::cerr << x;           \
+    }                           \
+  } while (0)
+
+#define DEBUG_START(x)                                 \
+  do {                                                 \
+    if (DEBUG_OUTPUT_ENABLED) {                        \
+      std::cerr << __FILE__ << ":" << __LINE__ << " "; \
+    }                                                  \
+  } while (0)
+
+#define DEBUG_END(x)            \
+  do {                          \
+    if (DEBUG_OUTPUT_ENABLED) { \
+      std::cerr << std::endl;   \
+    }                           \
+  } while (0)
+
+#define DEBUG_OUT(x) \
+  do {               \
+    DEBUG_START(x);  \
+    DEBUG_PRINT(x);  \
+    DEBUG_END(x);    \
+  } while (0);
+
+#define ERROR_OUT DEBUG_OUT
+
+/**
+ *  The names of the first version of access token, have it here so we can make it reconfigurable later.
+ */
+struct KvpAccessTokenConfig {
+  const String subjectName     = "sub";
+  StringView expirationName    = "exp";
+  StringView notBeforeName     = "nbf";
+  StringView issuedAtName      = "iat";
+  StringView tokenIdName       = "tid";
+  StringView versionName       = "ver";
+  StringView scopeName         = "scope";
+  StringView keyIdName         = "kid";
+  StringView hashFunctionName  = "st";
+  StringView messageDigestName = "md";
+
+  String pairDelimiter = "&";
+  String kvDeliiter    = "=";
+};
+
+/**
+ * Access token validation status.
+ */
+enum AccessTokenStatus {
+  VALID,
+  UNUSED,
+  INVALID_SYNTAX,
+  INVALID_FIELD,
+  INVALID_FIELD_VALUE,
+  MISSING_REQUIRED_FIELD,
+  INVALID_VERSION,
+  INVALID_HASH_FUNCTION,
+  INVALID_KEYID,
+  INVALID_SECRET,
+  INVALID_SIGNATURE,
+  INVALID_SCOPE,
+  OUT_OF_SCOPE,
+  TOO_EARLY,
+  TOO_LATE,
+  MAX,
+};
+
+const char *accessTokenStatusToString(const AccessTokenStatus &state);
+
+/**
+ *  Base Access Token class / interface + some basic implementations.
+ */
+class AccessToken
+{
+  friend std::ostream &operator<<(std::ostream &os, const AccessToken &token);
+
+public:
+  AccessToken(const StringMap &secretsMap, bool enableDebug = false);
+  virtual ~AccessToken() {}
+  StringView
+  getSubject() const
+  {
+    return _subject;
+  }
+
+  time_t
+  getExpiration() const
+  {
+    return string2int(_expiration);
+  }
+
+  time_t
+  getNotBefore() const
+  {
+    return string2int(_notBefore);
+  }
+
+  time_t
+  getIssuedAt() const
+  {
+    return string2int(_issuedAt);
+  }
+
+  StringView
+  getTokenId() const
+  {
+    return _tokenId;
+  }
+
+  StringView
+  getVersion() const
+  {
+    return _version;
+  }
+
+  StringView
+  getScope() const
+  {
+    return _scope;
+  }
+
+  StringView
+  getKeyId() const
+  {
+    return _keyId;
+  }
+
+  StringView
+  getHashFunction() const
+  {
+    return _hashFunction;
+  }
+
+  AccessTokenStatus
+  getState()
+  {
+    return _state;
+  }
+
+  AccessTokenStatus validate(const StringView token, time_t time);
+  virtual AccessTokenStatus parse(const StringView token) = 0;
+
+protected:
+  AccessTokenStatus validateSemantics();
+  AccessTokenStatus validateSignature();
+  AccessTokenStatus validateTiming(time_t time);
+
+  /* Initial setup members */
+  bool _debug = false;               /** @brief collect and print more debugging info */
+  const StringMap &_secretsMap;      /** @brief map with secret for signing the package*/
+  AccessTokenStatus _state = UNUSED; /** @brief token state */
+  time_t _validationTime   = 0;      /** @brief validation time used for debugging */
+
+  /* Helper members */
+  StringView _token   = ""; /** @brief whole token */
+  StringView _payload = ""; /** @brief payload signed by the signature */
+
+  /* Fields extracted from the token string */
+  StringView _subject    = ""; /** @brief subject - this is what we are signing and validating, required */
+  StringView _expiration = ""; /** @brief expiration time-stamp, not required */
+  StringView _notBefore  = ""; /** @brief not before time-stamp, not required */
+  StringView _issuedAt   = ""; /** @brief time-stamp when token was issued, not required */
+  StringView _tokenId    = ""; /** @brief unique token id for debugging and tracking, not required */
+  StringView _version    = ""; /** @brief version, not required, still @todo */
+  StringView _scope      = ""; /** @brief scope of subject, not required, still @todo */
+
+  /** Signature, extracted from the token string */
+  StringView _keyId         = ""; /** @brief the key in the secrets map to be used to calculate the digest */
+  StringView _hashFunction  = ""; /** @brief name of the hash function to be used for the digest */
+  StringView _messageDigest = ""; /** @brief the message digest that signs the token */
+};
+
+/**
+ * Key-value-pair access token
+ */
+class KvpAccessToken : public AccessToken
+{
+public:
+  KvpAccessToken(const KvpAccessTokenConfig &tokenConfig, const StringMap &secretsMap, bool enableDebug = false);
+  AccessTokenStatus parse(const StringView token);
+
+protected:
+  const KvpAccessTokenConfig &_tokenConfig; /** @brief description of keys' names and delimiters */
+};
+
+class KvpAccessTokenBuilder
+{
+public:
+  KvpAccessTokenBuilder(const KvpAccessTokenConfig &config, const StringMap &secretsMap);
+
+  void appendKeyValuePair(const StringView &key, const StringView value);
+  void addSubject(const StringView sub);
+  void addExpiration(time_t exp);
+  void addNotBefore(time_t nbf);
+  void addIssuedAt(time_t iat);
+  void addTokenId(const StringView tid);
+  void addVersion(const StringView ver);
+  void addScope(const StringView scope);
+  void sign(const StringView kid, const StringView hf);
+  const char *get();
+
+private:
+  const KvpAccessTokenConfig &_config;
+  String _buffer;
+
+  const StringMap &_secretsMap; /** @brief map with secret for signing the package*/
+};
+
+/**
+ * Instantiate various types of Access Tokens from a single place.
+ * @todo see how it goes when adding new token kinds and redesign / re-implement later.
+ */
+class AccessTokenFactory
+{
+public:
+  enum TokenType {
+    Unknown,
+    KeyValuePair,
+  };
+
+  AccessTokenFactory(const KvpAccessTokenConfig &tokenConfig, const StringMap &secretsMap, bool enableDebug)
+    : _kvpAccessTokenConfig(tokenConfig), _secretMap(secretsMap), _enableDebug(enableDebug)
+  {
+    cryptoMagicInit();
+    _desiredType = KeyValuePair;
+  }
+
+  AccessToken *
+  getAccessToken()
+  {
+    switch (_desiredType) {
+    case KeyValuePair: {
+      return new KvpAccessToken(_kvpAccessTokenConfig, _secretMap, _enableDebug);
+      break;
+    }
+    default: {
+      break;
+    }
+    }
+    return nullptr;
+  }
+
+private:
+  TokenType _desiredType = Unknown; /* Remember for each (only one) token type the factory was initialized */
+  const KvpAccessTokenConfig &_kvpAccessTokenConfig;
+  const StringMap &_secretMap;
+  bool _enableDebug = false;
+
+  AccessTokenFactory() = delete;
+};
+
+/* Define user friendly names for supported hash functions and cryptographic signature schemes */
+#define WDN_HASH_SHA256 "HMAC-SHA-256"
+#define WDN_HASH_SHA512 "HMAC-SHA-512"
+#define WDN_RSA_PSS "RSA_PSS"
diff --git a/plugins/experimental/access_control/common.cc b/plugins/experimental/access_control/common.cc
new file mode 100644
index 0000000..ed1360a
--- /dev/null
+++ b/plugins/experimental/access_control/common.cc
@@ -0,0 +1,51 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file common.cc
+ * @brief Common declarations and definitions.
+ * @see common.h
+ */
+
+#include "common.h"
+
+#ifdef ACCESS_CONTROL_UNIT_TEST
+
+void
+PrintToStdErr(const char *fmt, ...)
+{
+  va_list args;
+  va_start(args, fmt);
+  vfprintf(stderr, fmt, args);
+  va_end(args);
+}
+
+#endif /* ACCESS_CONTROL_UNIT_TEST */
+
+int
+string2int(const StringView &s)
+{
+  time_t t = 0;
+  try {
+    t = (time_t)std::stoi(String(s));
+  } catch (...) {
+    /* Failed to convert return impossible value */
+    return 0;
+  }
+  return t;
+}
diff --git a/plugins/experimental/access_control/common.h b/plugins/experimental/access_control/common.h
new file mode 100644
index 0000000..7daa076
--- /dev/null
+++ b/plugins/experimental/access_control/common.h
@@ -0,0 +1,69 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file common.h
+ * @brief Common declarations and definitions (header file).
+ */
+
+#pragma once
+
+#define PLUGIN_NAME "access_control"
+
+#include <functional>
+#include <string>
+#include <set>
+#include <list>
+#include <vector>
+#include <map>
+#include <sstream>
+#include <iostream>
+
+typedef std::string String;
+typedef std::string_view StringView;
+typedef std::set<std::string> StringSet;
+typedef std::list<std::string> StringList;
+typedef std::vector<std::string> StringVector;
+typedef std::map<std::string, std::string> StringMap;
+
+#ifdef ACCESS_CONTROL_UNIT_TEST
+#include <stdio.h>
+#include <stdarg.h>
+
+#define AccessControlDebug(fmt, ...) \
+  PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", PLUGIN_NAME, __FILE__, __LINE__, __func__, ##__VA_ARGS__)
+#define AccessControlError(fmt, ...) \
+  PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", PLUGIN_NAME, __FILE__, __LINE__, __func__, ##__VA_ARGS__)
+void PrintToStdErr(const char *fmt, ...);
+
+#else /* ACCESS_CONTROL_UNIT_TEST */
+#include "ts/ts.h"
+
+#define AccessControlDebug(fmt, ...)                                                      \
+  do {                                                                                    \
+    TSDebug(PLUGIN_NAME, "%s:%d:%s() " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); \
+  } while (0)
+
+#define AccessControlError(fmt, ...)                                                      \
+  do {                                                                                    \
+    TSError("(%s) " fmt, PLUGIN_NAME, ##__VA_ARGS__);                                     \
+    TSDebug(PLUGIN_NAME, "%s:%d:%s() " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); \
+  } while (0)
+#endif /* ACCESS_CONTROL_UNIT_TEST */
+
+int string2int(const StringView &s);
diff --git a/plugins/experimental/access_control/config.cc b/plugins/experimental/access_control/config.cc
new file mode 100644
index 0000000..bf133c1
--- /dev/null
+++ b/plugins/experimental/access_control/config.cc
@@ -0,0 +1,380 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file config.cc
+ * @brief Access Control Plug-in Configuration.
+ * @see config.h
+ */
+
+#include <algorithm> /* find_if */
+#include <fstream>   /* std::ifstream */
+#include <getopt.h>  /* getopt_long() */
+
+#include "common.h"
+#include "config.h"
+
+static bool
+isTrue(const char *arg)
+{
+  return (nullptr == arg || 0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
+}
+
+/**
+ * trim from start
+ */
+static inline std::string &
+ltrim(std::string &s)
+{
+  s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
+  return s;
+}
+
+/**
+ *  trim from end
+ */
+static inline std::string &
+rtrim(std::string &s)
+{
+  s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
+  return s;
+}
+
+/**
+ * trim from both ends
+ */
+static inline std::string &
+trim(std::string &s)
+{
+  return ltrim(rtrim(s));
+}
+
+/**
+ * @brief Rebase a relative path onto the configuration directory.
+ */
+static String
+makeConfigPath(const String &path)
+{
+  if (path.empty() || path[0] == '/') {
+    return path;
+  }
+
+  return String(TSConfigDirGet()) + "/" + path;
+}
+
+/**
+ * @brief parse and load a single line (base template - does nothing, see specializations for maps and vectors below)
+ */
+template <class T>
+void
+loadLine(T &container, const String &line)
+{
+}
+
+/**
+ * @brief parse and load a single line into a map.
+ */
+template <>
+void
+loadLine<StringMap>(StringMap &map, const String &line)
+{
+  String key;
+  String value;
+  std::istringstream ss(line);
+  std::getline(ss, key, '=');
+  std::getline(ss, value, '=');
+  trim(key);
+  trim(value);
+  map[key] = value;
+
+#ifdef ACCESS_CONTROL_LOG_SECRETS
+  AccessControlDebug("Adding secrets[%s]='%s'", key.c_str(), value.c_str());
+#endif
+}
+
+/**
+ * @brief parse and load a single line into a vector.
+ */
+template <>
+void
+loadLine<StringVector>(StringVector &vector, const String &line)
+{
+  String trimmedLine(line);
+  trim(trimmedLine);
+  vector.push_back(trimmedLine);
+
+#ifdef ACCESS_CONTROL_LOG_SECRETS
+  AccessControlDebug("Adding secrets[%d]='%s'", (int)(vector.size() - 1), trimmedLine.c_str());
+#endif
+}
+
+/**
+ * @brief parse and load a secrets into a container (i.e. map or vector).
+ */
+template <typename T>
+static bool
+load(T &container, const String &filename)
+{
+  String line;
+  String::size_type pos;
+
+  String path(makeConfigPath(filename));
+
+  AccessControlDebug("reading file %s", path.c_str());
+
+  std::ifstream infile;
+  infile.open(path.c_str());
+  if (!infile.is_open()) {
+    AccessControlError("failed to load file '%s'", path.c_str());
+    return false;
+  }
+
+  while (std::getline(infile, line)) {
+    // Allow #-prefixed comments.
+    pos = line.find_first_of('#');
+    if (pos != String::npos) {
+      line.resize(pos);
+    }
+    if (line.empty()) {
+      continue;
+    }
+
+    loadLine(container, line);
+  }
+  infile.close();
+
+  return true;
+}
+
+/**
+ * @brief initializes plugin configuration.
+ * @param argc number of plugin parameters
+ * @param argv plugin parameters
+ */
+bool
+AccessControlConfig::init(int argc, char *argv[])
+{
+  static const struct option longopt[] = {{const_cast<char *>("invalid-syntax-status-code"), optional_argument, 0, 'a'},
+                                          {const_cast<char *>("invalid-signature-status-code"), optional_argument, 0, 'b'},
+                                          {const_cast<char *>("invalid-timing-status-code"), optional_argument, 0, 'c'},
+                                          {const_cast<char *>("invalid-scope-status-code"), optional_argument, 0, 'd'},
+                                          {const_cast<char *>("invalid-origin-response"), optional_argument, 0, 'e'},
+                                          {const_cast<char *>("internal-error-status-code"), optional_argument, 0, 'f'},
+                                          {const_cast<char *>("check-cookie"), optional_argument, 0, 'g'},
+                                          {const_cast<char *>("symmetric-keys-map"), optional_argument, 0, 'h'},
+                                          {const_cast<char *>("reject-invalid-token-requests"), optional_argument, 0, 'i'},
+                                          {const_cast<char *>("extract-subject-to-header"), optional_argument, 0, 'j'},
+                                          {const_cast<char *>("extract-tokenid-to-header"), optional_argument, 0, 'k'},
+                                          {const_cast<char *>("extract-status-to-header"), optional_argument, 0, 'l'},
+                                          {const_cast<char *>("token-response-header"), optional_argument, 0, 'm'},
+                                          {const_cast<char *>("use-redirects"), optional_argument, 0, 'n'},
+                                          {const_cast<char *>("include-uri-paths-file"), optional_argument, nullptr, 'o'},
+                                          {const_cast<char *>("exclude-uri-paths-file"), optional_argument, nullptr, 'p'},
+                                          {0, 0, 0, 0}};
+
+  bool status = true;
+  optind      = 0;
+
+  /* argv contains the "to" and "from" URLs. Skip the first so that the second one poses as the program name. */
+  argc--;
+  argv++;
+
+  for (;;) {
+    int opt;
+    opt = getopt_long(argc, (char *const *)argv, "", longopt, nullptr);
+
+    if (opt == -1) {
+      break;
+    }
+    AccessControlDebug("processing %s", argv[optind - 1]);
+
+    switch (opt) {
+    case 'a': /* invalid-syntax-status-code */
+    {
+      _invalidSignature = (TSHttpStatus)string2int(optarg);
+    } break;
+    case 'b': /* invalid-signature-status-code */
+    {
+      _invalidSignature = (TSHttpStatus)string2int(optarg);
+    } break;
+    case 'c': /* invalid-timing-status-code */
+    {
+      _invalidTiming = (TSHttpStatus)string2int(optarg);
+    } break;
+    case 'd': /* invalid-scope-status-code */
+    {
+      _invalidScope = (TSHttpStatus)string2int(optarg);
+    } break;
+    case 'e': /* invalid-origin-response */
+    {
+      _invalidOriginResponse = (TSHttpStatus)string2int(optarg);
+    } break;
+    case 'f': /* internal-error-status-code */
+    {
+      _internalError = (TSHttpStatus)string2int(optarg);
+    } break;
+    case 'g': /* check-cookie */
+    {
+      _cookieName.assign(optarg);
+    } break;
+    case 'h': /* symmetric-keys-map */
+    {
+      load(_symmetricKeysMap, optarg);
+    } break;
+    case 'i': /* reject-invalid-token-requests */
+    {
+      _rejectRequestsWithInvalidTokens = ::isTrue(optarg);
+    } break;
+    case 'j': /* extract-subject-to-header */
+    {
+      _extrSubHdrName.assign(optarg);
+    } break;
+    case 'k': /* extract-tokenid-to-header */
+    {
+      _extrTokenIdHdrName.assign(optarg);
+    } break;
+    case 'l': /* extract-status-to-header */
+    {
+      _extrValidationHdrName.assign(optarg);
+    } break;
+    case 'm': /* token-response-header */
+    {
+      _respTokenHeaderName.assign(optarg);
+    } break;
+    case 'n': /* use-redirects */
+    {
+      _useRedirects = ::isTrue(optarg);
+    } break;
+    case 'o': /* include-uri-paths-file */
+      if (!loadMultiPatternsFromFile(optarg, /* blacklist = */ false)) {
+        AccessControlError("failed to load uri-path multi-pattern white-list '%s'", optarg);
+        status = false;
+      }
+      break;
+    case 'p': /* exclude-uri-paths-file */
+      if (!loadMultiPatternsFromFile(optarg, /* blacklist = */ true)) {
+        AccessControlError("failed to load uri-path multi-pattern black-list '%s'", optarg);
+        status = false;
+      }
+      break;
+
+    default: {
+      status = false;
+    }
+    }
+  }
+
+  /* Make sure at least 1 secret source is specified */
+  if (_symmetricKeysMap.empty()) {
+    AccessControlDebug("no secrets' source provided");
+    return false;
+  }
+
+  /* Support only KeyValuePair syntax for now */
+  _tokenFactory = new AccessTokenFactory(_kvpAccessTokenConfig, _symmetricKeysMap, _debugLevel);
+  if (nullptr == _tokenFactory) {
+    AccessControlDebug("failed to initialize the access token factory");
+    return false;
+  }
+  return status;
+}
+
+/**
+ * @brief a helper function which loads the classifier from files.
+ * @param filename file name
+ * @param blacklist true - load as a blacklist of patterns, false - white-list of patterns
+ * @return true if successful, false otherwise.
+ */
+bool
+AccessControlConfig::loadMultiPatternsFromFile(const String &filename, bool blacklist)
+{
+  if (filename.empty()) {
+    AccessControlError("filename cannot be empty");
+    return false;
+  }
+
+  String path(makeConfigPath(filename));
+
+  std::ifstream ifstr;
+  String regex;
+  unsigned lineno = 0;
+
+  ifstr.open(path.c_str());
+  if (!ifstr) {
+    AccessControlError("failed to load uri-path multi-pattern from '%s'", path.c_str());
+    return false;
+  }
+
+  /* Have the multiplattern be named as same as the filename, would be used only for debugging. */
+  MultiPattern *multiPattern;
+  if (blacklist) {
+    multiPattern = new NonMatchingMultiPattern(filename);
+    AccessControlDebug("NonMatchingMultiPattern('%s')", filename.c_str());
+  } else {
+    multiPattern = new MultiPattern(filename);
+    AccessControlDebug("MultiPattern('%s')", filename.c_str());
+  }
+  if (nullptr == multiPattern) {
+    AccessControlError("failed to allocate multi-pattern from '%s'", filename.c_str());
+    return false;
+  }
+
+  AccessControlDebug("loading multi-pattern '%s' from '%s'", filename.c_str(), path.c_str());
+
+  while (std::getline(ifstr, regex)) {
+    Pattern *p;
+    String::size_type pos;
+
+    ++lineno;
+
+    // Allow #-prefixed comments.
+    pos = regex.find_first_of('#');
+    if (pos != String::npos) {
+      regex.resize(pos);
+    }
+
+    if (regex.empty()) {
+      continue;
+    }
+
+    p = new Pattern();
+
+    if (nullptr != p && p->init(regex)) {
+      if (blacklist) {
+        AccessControlDebug("Added pattern '%s' to black list uri-path multi-pattern '%s'", regex.c_str(), filename.c_str());
+        multiPattern->add(p);
+      } else {
+        AccessControlDebug("Added pattern '%s' to white list uri-path multi-pattern '%s'", regex.c_str(), filename.c_str());
+        multiPattern->add(p);
+      }
+    } else {
+      AccessControlError("%s:%u: failed to parse regex '%s'", path.c_str(), lineno, regex.c_str());
+      delete p;
+    }
+  }
+
+  ifstr.close();
+
+  if (!multiPattern->empty()) {
+    _uriPathScope.add(multiPattern);
+  } else {
+    delete multiPattern;
+  }
+
+  return true;
+}
diff --git a/plugins/experimental/access_control/config.h b/plugins/experimental/access_control/config.h
new file mode 100644
index 0000000..6611c70
--- /dev/null
+++ b/plugins/experimental/access_control/config.h
@@ -0,0 +1,70 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file config.h
+ * @brief Access Control Plug-in Configuration (Headers).
+ * @see config.h
+ */
+
+#pragma once
+
+#include "common.h"
+#include "access_control.h"
+#include "pattern.h"
+
+/**
+ * Access control plugin configuration.
+ */
+class AccessControlConfig
+{
+public:
+  AccessControlConfig() {}
+  virtual ~AccessControlConfig() { delete _tokenFactory; }
+  bool init(int argc, char *argv[]);
+
+  bool loadMultiPatternsFromFile(const String &filename, bool blacklist = true);
+
+  StringMap _symmetricKeysMap; /** @brief a map secrets accessible by key string (KID) */
+
+  /* Predefined and plugin parameter configurable HTTP responses. */
+  TSHttpStatus _invalidSignature      = TS_HTTP_STATUS_UNAUTHORIZED;
+  TSHttpStatus _invalidTiming         = TS_HTTP_STATUS_FORBIDDEN;
+  TSHttpStatus _invalidScope          = TS_HTTP_STATUS_FORBIDDEN;
+  TSHttpStatus _invalidSyntax         = TS_HTTP_STATUS_BAD_REQUEST;
+  TSHttpStatus _invalidRequest        = TS_HTTP_STATUS_BAD_REQUEST;
+  TSHttpStatus _invalidOriginResponse = static_cast<TSHttpStatus>(520); /* catch all response for unexpected origin responses,
+                                                                           although TS_HTTP_STATUS_BAD_GATEWAY seems more
+                                                                           appropriate it is too widely used */
+  TSHttpStatus _internalError = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
+
+  KvpAccessTokenConfig _kvpAccessTokenConfig;
+  bool _debugLevel = false;
+
+  String _cookieName = "cdn_auth"; /** @brief name of the cookie containing the token to be verified */
+
+  AccessTokenFactory *_tokenFactory = nullptr;
+
+  bool _rejectRequestsWithInvalidTokens = false; /** reject versa forward to the origin if access token is invalid */
+  String _respTokenHeaderName;   /** @brief name of header used by origin to provide the access token in its response */
+  String _extrSubHdrName;        /** @brief header name to extract the token subject content, if empty => no extraction */
+  String _extrTokenIdHdrName;    /** @brief header name to extract the token id, if empty => no extraction */
+  String _extrValidationHdrName; /** @brief header name to extract the token validation status, if empty => no extraction */
+  bool _useRedirects = false;    /** @brief true - use redirect to set the access token cookie, @todo not used yet */
+  Classifier _uriPathScope; /**< @brief blacklist (exclude) and white-list (include) whcih path should have the access control */
+};
diff --git a/plugins/experimental/access_control/headers.cc b/plugins/experimental/access_control/headers.cc
new file mode 100644
index 0000000..2bb70bb
--- /dev/null
+++ b/plugins/experimental/access_control/headers.cc
@@ -0,0 +1,213 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file headers.cc
+ * @brief HTTP headers manipulation.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "headers.h"
+#include "common.h"
+
+/**
+ * @brief Remove a header (fully) from an TSMLoc / TSMBuffer.
+ *
+ * @param bufp request's buffer
+ * @param hdrLoc request's header location
+ * @param header header name
+ * @param headerlen header name length
+ * @return the number of fields (header values) we removed.
+ */
+int
+removeHeader(TSMBuffer bufp, TSMLoc hdrLoc, const char *header, int headerlen)
+{
+  TSMLoc fieldLoc = TSMimeHdrFieldFind(bufp, hdrLoc, header, headerlen);
+  int cnt         = 0;
+
+  while (fieldLoc) {
+    TSMLoc tmp = TSMimeHdrFieldNextDup(bufp, hdrLoc, fieldLoc);
+
+    ++cnt;
+    TSMimeHdrFieldDestroy(bufp, hdrLoc, fieldLoc);
+    TSHandleMLocRelease(bufp, hdrLoc, fieldLoc);
+    fieldLoc = tmp;
+  }
+
+  return cnt;
+}
+
+/**
+ * @brief Checks if the header exists.
+ *
+ * @param bufp request's buffer
+ * @param hdrLoc request's header location
+ * @return true - exists, false - does not exist
+ */
+bool
+headerExist(TSMBuffer bufp, TSMLoc hdrLoc, const char *header, int headerlen)
+{
+  TSMLoc fieldLoc = TSMimeHdrFieldFind(bufp, hdrLoc, header, headerlen);
+  if (TS_NULL_MLOC != fieldLoc) {
+    TSHandleMLocRelease(bufp, hdrLoc, fieldLoc);
+    return true;
+  }
+  return false;
+}
+
+/**
+ * @brief Get the header value
+ *
+ * @param bufp request's buffer
+ * @param hdrLoc request's header location
+ * @param header header name
+ * @param headerlen header name length
+ * @param value buffer for the value
+ * @param valuelen lenght of the buffer for the value
+ * @return pointer to the string with the value.
+ */
+char *
+getHeader(TSMBuffer bufp, TSMLoc hdrLoc, const char *header, int headerlen, char *value, int *valuelen)
+{
+  TSMLoc fieldLoc = TSMimeHdrFieldFind(bufp, hdrLoc, header, headerlen);
+  char *dst       = value;
+  while (fieldLoc) {
+    TSMLoc next = TSMimeHdrFieldNextDup(bufp, hdrLoc, fieldLoc);
+
+    int count = TSMimeHdrFieldValuesCount(bufp, hdrLoc, fieldLoc);
+    for (int i = 0; i < count; ++i) {
+      const char *v = nullptr;
+      int vlen      = 0;
+      v             = TSMimeHdrFieldValueStringGet(bufp, hdrLoc, fieldLoc, i, &vlen);
+      if (v == nullptr || vlen == 0) {
+        continue;
+      }
+      /* append the field content to the output buffer if enough space, plus space for ", " */
+      bool first      = (dst == value);
+      int neededSpace = ((dst - value) + vlen + (dst == value ? 0 : 2));
+      if (neededSpace < *valuelen) {
+        if (!first) {
+          memcpy(dst, ", ", 2);
+          dst += 2;
+        }
+        memcpy(dst, v, vlen);
+        dst += vlen;
+      }
+    }
+    TSHandleMLocRelease(bufp, hdrLoc, fieldLoc);
+    fieldLoc = next;
+  }
+
+  *valuelen = dst - value;
+  return value;
+}
+
+/**
+ * @brief Set a header to a specific value.
+ *
+ * This will avoid going to through a remove / add sequence in case of an existing header but clean.
+ *
+ * @param bufp request's buffer
+ * @param hdrLoc request's header location
+ * @param header header name
+ * @param headerlen header name len
+ * @param value the new value
+ * @param valuelen lenght of the value
+ * @return true - OK, false - failed
+ */
+bool
+setHeader(TSMBuffer bufp, TSMLoc hdrLoc, const char *header, int headerlen, const char *value, int valuelen)
+{
+  if (!bufp || !hdrLoc || !header || headerlen <= 0 || !value || valuelen <= 0) {
+    return false;
+  }
+
+  bool ret        = false;
+  TSMLoc fieldLoc = TSMimeHdrFieldFind(bufp, hdrLoc, header, headerlen);
+
+  if (!fieldLoc) {
+    // No existing header, so create one
+    if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(bufp, hdrLoc, header, headerlen, &fieldLoc)) {
+      if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(bufp, hdrLoc, fieldLoc, -1, value, valuelen)) {
+        TSMimeHdrFieldAppend(bufp, hdrLoc, fieldLoc);
+        ret = true;
+      }
+      TSHandleMLocRelease(bufp, hdrLoc, fieldLoc);
+    }
+  } else {
+    TSMLoc tmp = nullptr;
+    bool first = true;
+
+    while (fieldLoc) {
+      if (first) {
+        first = false;
+        if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(bufp, hdrLoc, fieldLoc, -1, value, valuelen)) {
+          ret = true;
+        }
+      } else {
+        TSMimeHdrFieldDestroy(bufp, hdrLoc, fieldLoc);
+      }
+      tmp = TSMimeHdrFieldNextDup(bufp, hdrLoc, fieldLoc);
+      TSHandleMLocRelease(bufp, hdrLoc, fieldLoc);
+      fieldLoc = tmp;
+    }
+  }
+
+  return ret;
+}
+
+/**
+ * @brief Dump a header on stderr
+ *
+ * Useful together with TSDebug().
+ *
+ * @param bufp request's buffer
+ * @param hdrLoc request's header location
+ */
+void
+dumpHeaders(TSMBuffer bufp, TSMLoc hdrLoc)
+{
+  TSIOBuffer output_buffer;
+  TSIOBufferReader reader;
+  TSIOBufferBlock block;
+  const char *block_start;
+  int64_t block_avail;
+
+  output_buffer = TSIOBufferCreate();
+  reader        = TSIOBufferReaderAlloc(output_buffer);
+
+  /* This will print  just MIMEFields and not the http request line */
+  TSMimeHdrPrint(bufp, hdrLoc, output_buffer);
+
+  /* We need to loop over all the buffer blocks, there can be more than 1 */
+  block = TSIOBufferReaderStart(reader);
+  do {
+    block_start = TSIOBufferBlockReadStart(block, reader, &block_avail);
+    if (block_avail > 0) {
+      AccessControlDebug("Headers are:\n%.*s", static_cast<int>(block_avail), block_start);
+    }
+    TSIOBufferReaderConsume(reader, block_avail);
+    block = TSIOBufferReaderStart(reader);
+  } while (block && block_avail != 0);
+
+  /* Free up the TSIOBuffer that we used to print out the header */
+  TSIOBufferReaderFree(reader);
+  TSIOBufferDestroy(output_buffer);
+}
diff --git a/plugins/experimental/access_control/headers.h b/plugins/experimental/access_control/headers.h
new file mode 100644
index 0000000..d3ad443
--- /dev/null
+++ b/plugins/experimental/access_control/headers.h
@@ -0,0 +1,32 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file headers.h
+ * @brief HTTP headers manipulation (header file).
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+int removeHeader(TSMBuffer bufp, TSMLoc hdr_loc, const char *header, int len);
+bool headerExist(TSMBuffer bufp, TSMLoc hdr_loc, const char *header, int len);
+char *getHeader(TSMBuffer bufp, TSMLoc hdr_loc, const char *header, int headerlen, char *value, int *valuelen);
+bool setHeader(TSMBuffer bufp, TSMLoc hdr_loc, const char *header, int len, const char *val, int val_len);
+void dumpHeaders(TSMBuffer bufp, TSMLoc hdr_loc);
diff --git a/plugins/experimental/access_control/pattern.cc b/plugins/experimental/access_control/pattern.cc
new file mode 100644
index 0000000..2c04b6a
--- /dev/null
+++ b/plugins/experimental/access_control/pattern.cc
@@ -0,0 +1,579 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file pattern.cc
+ * @brief PRCE related classes.
+ * @see pattern.h
+ */
+
+#include "pattern.h"
+
+static void
+replaceString(String &str, const String &from, const String &to)
+{
+  if (from.empty()) {
+    return;
+  }
+
+  String::size_type start_pos = 0;
+  while ((start_pos = str.find(from, start_pos)) != String::npos) {
+    str.replace(start_pos, from.length(), to);
+    start_pos += to.length();
+  }
+}
+
+Pattern::Pattern() : _re(nullptr), _extra(nullptr), _pattern(""), _replacement(""), _replace(false), _tokenCount(0) {}
+
+/**
+ * @brief Initializes PCRE pattern by providing the subject and replacement strings.
+ * @param pattern PCRE pattern, a string containing PCRE patterns, capturing groups.
+ * @param replacement PCRE replacement, a string where $0 ... $9 will be replaced with the corresponding capturing groups
+ * @return true if successful, false if failure
+ */
+bool
+Pattern::init(const String &pattern, const String &replacenemt, bool replace)
+{
+  pcreFree();
+
+  _pattern.assign(pattern);
+  _replacement.assign(replacenemt);
+  _replace = replace;
+
+  _tokenCount = 0;
+
+  if (!compile()) {
+    AccessControlDebug("failed to initialize pattern:'%s', replacement:'%s'", pattern.c_str(), replacenemt.c_str());
+    pcreFree();
+    return false;
+  }
+
+  return true;
+}
+
+/**
+ * @brief Initializes PCRE pattern by providing the pattern only or pattern+replacement in a single configuration string.
+ * @see init()
+ * @param config PCRE pattern <pattern> or PCRE pattern + replacement in format /<pattern>/<replacement>/
+ * @return true if successful, false if failure
+ */
+bool
+Pattern::init(const String &config)
+{
+  if (config[0] == '/') {
+    /* This is a config in format /regex/replacement/ */
+    String pattern;
+    String replacement;
+
+    size_t start   = 1;
+    size_t current = 0;
+    size_t next    = 1;
+    do {
+      current = next + 1;
+      next    = config.find_first_of('/', current);
+    } while (next != String::npos && '\\' == config[next - 1]);
+
+    if (next != String::npos) {
+      pattern = config.substr(start, next - start);
+    } else {
+      /* Error, no closing '/' */
+      AccessControlError("failed to parse the pattern in '%s'", config.c_str());
+      return false;
+    }
+
+    start = next + 1;
+    do {
+      current = next + 1;
+      next    = config.find_first_of('/', current);
+    } while (next != String::npos && '\\' == config[next - 1]);
+
+    if (next != String::npos) {
+      replacement = config.substr(start, next - start);
+    } else {
+      /* Error, no closing '/' */
+      AccessControlError("failed to parse the replacement in '%s'", config.c_str());
+      return false;
+    }
+
+    // Remove '\' which escaped '/' inside the pattern and replacement strings.
+    ::replaceString(pattern, "\\/", "/");
+    ::replaceString(replacement, "\\/", "/");
+
+    return this->init(pattern, replacement, /* replace */ true);
+  } else {
+    return this->init(config, /* replacement */ "", /*replace */ false);
+  }
+
+  /* Should never get here. */
+  return false;
+}
+
+/**
+ * @brief get the pattern string.
+ */
+inline String
+Pattern::getPattern()
+{
+  return _pattern;
+}
+
+/**
+ * @brief Checks if the pattern object was initialized with a meaningful regex pattern.
+ * @return true if initialized, false if not.
+ */
+bool
+Pattern::empty() const
+{
+  return _pattern.empty() || nullptr == _re;
+}
+
+/**
+ * @brief Frees PCRE library related resources.
+ */
+void
+Pattern::pcreFree()
+{
+  if (_re) {
+    pcre_free(_re);
+    _re = nullptr;
+  }
+
+  if (_extra) {
+    pcre_free(_extra);
+    _extra = nullptr;
+  }
+}
+
+/**
+ * @bried Destructor, frees PCRE related resources.
+ */
+Pattern::~Pattern()
+{
+  pcreFree();
+}
+
+/**
+ * @brief Capture or capture-and-replace depending on whether a replacement string is specified.
+ * @see replace()
+ * @see capture()
+ * @param subject PCRE subject string
+ * @param result vector of strings where the result of captures or the replacements will be returned.
+ * @return true if there was a match and capture or replacement succeeded, false if failure.
+ */
+bool
+Pattern::process(const String &subject, StringVector &result)
+{
+  if (_replace) {
+    /* Replacement pattern was provided in the configuration - capture and replace. */
+    String element;
+    if (replace(subject, element)) {
+      result.push_back(element);
+    } else {
+      return false;
+    }
+  } else {
+    /* Replacement was not provided so return all capturing groups except the group zero. */
+    StringVector captures;
+    if (capture(subject, captures)) {
+      if (captures.size() == 1) {
+        result.push_back(captures[0]);
+      } else {
+        StringVector::iterator it = captures.begin() + 1;
+        for (; it != captures.end(); it++) {
+          result.push_back(*it);
+        }
+      }
+    } else {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+/**
+ * @brief PCRE matches a subject string against the the regex pattern.
+ * @param subject PCRE subject
+ * @return true - matched, false - did not.
+ */
+bool
+Pattern::match(const String &subject)
+{
+  int matchCount;
+  AccessControlDebug("matching '%s' to '%s'", _pattern.c_str(), subject.c_str());
+
+  if (!_re) {
+    return false;
+  }
+
+  matchCount = pcre_exec(_re, _extra, subject.c_str(), subject.length(), 0, PCRE_NOTEMPTY, nullptr, 0);
+  if (matchCount < 0) {
+    if (matchCount != PCRE_ERROR_NOMATCH) {
+      AccessControlError("matching error %d", matchCount);
+    }
+    return false;
+  }
+
+  return true;
+}
+
+/**
+ * @brief Return all PCRE capture groups that matched in the subject string
+ * @param subject PCRE subject string
+ * @param result reference to vector of strings containing all capture groups
+ */
+bool
+Pattern::capture(const String &subject, StringVector &result)
+{
+  int matchCount;
+  int ovector[OVECOUNT];
+
+  AccessControlDebug("capturing '%s' from '%s'", _pattern.c_str(), subject.c_str());
+
+  if (!_re) {
+    AccessControlError("regular expression not initialized");
+    return false;
+  }
+
+  matchCount = pcre_exec(_re, nullptr, subject.c_str(), subject.length(), 0, PCRE_NOTEMPTY, ovector, OVECOUNT);
+  if (matchCount < 0) {
+    if (matchCount != PCRE_ERROR_NOMATCH) {
+      AccessControlError("matching error %d", matchCount);
+    }
+    return false;
+  }
+
+  for (int i = 0; i < matchCount; i++) {
+    int start  = ovector[2 * i];
+    int length = ovector[2 * i + 1] - ovector[2 * i];
+
+    String dst(subject, start, length);
+
+    AccessControlDebug("capturing '%s' %d[%d,%d]", dst.c_str(), i, ovector[2 * i], ovector[2 * i + 1]);
+    result.push_back(dst);
+  }
+
+  return true;
+}
+
+/**
+ * @brief Replaces all replacements found in the replacement string with what matched in the PCRE capturing groups.
+ * @param subject PCRE subject string
+ * @param result reference to A string where the result of the replacement will be stored
+ * @return true - success, false - nothing matched or failure.
+ */
+bool
+Pattern::replace(const String &subject, String &result)
+{
+  int matchCount;
+  int ovector[OVECOUNT];
+
+  AccessControlDebug("replacing:'%s' in pattern:'%s', subject:'%s'", _replacement.c_str(), _pattern.c_str(), subject.c_str());
+
+  if (!_re || !_replace) {
+    AccessControlError("regular expression not initialized or not configured to replace");
+    return false;
+  }
+
+  matchCount = pcre_exec(_re, nullptr, subject.c_str(), subject.length(), 0, PCRE_NOTEMPTY, ovector, OVECOUNT);
+  if (matchCount < 0) {
+    if (matchCount != PCRE_ERROR_NOMATCH) {
+      AccessControlError("matching error %d", matchCount);
+    }
+    return false;
+  }
+
+  /* Verify the replacement has the right number of matching groups */
+  for (int i = 0; i < _tokenCount; i++) {
+    if (_tokens[i] >= matchCount) {
+      AccessControlError("invalid reference in replacement string: $%d", _tokens[i]);
+      return false;
+    }
+  }
+
+  int previous = 0;
+  for (int i = 0; i < _tokenCount; i++) {
+    int replIndex = _tokens[i];
+    int start     = ovector[2 * replIndex];
+    int length    = ovector[2 * replIndex + 1] - ovector[2 * replIndex];
+
+    String src(_replacement, _tokenOffset[i], 2);
+    String dst(subject, start, length);
+
+    AccessControlDebug("replacing '%s' with '%s'", src.c_str(), dst.c_str());
+
+    result.append(_replacement, previous, _tokenOffset[i] - previous);
+    result.append(dst);
+
+    previous = _tokenOffset[i] + 2; /* 2 is the size of $0 or $1 or $2, ... or $9 */
+  }
+
+  result.append(_replacement, previous, _replacement.length() - previous);
+
+  AccessControlDebug("replacing '%s' resulted in '%s'", _replacement.c_str(), result.c_str());
+
+  return true;
+}
+
+/**
+ * @brief PCRE compiles the regex, called only during initialization.
+ * @return true if successful, false if not.
+ */
+bool
+Pattern::compile()
+{
+  const char *errPtr; /* PCRE error */
+  int errOffset;      /* PCRE error offset */
+
+  AccessControlDebug("compiling pattern:'%s', replace: %s, replacement:'%s'", _pattern.c_str(), _replace ? "true" : "false",
+                     _replacement.c_str());
+
+  _re = pcre_compile(_pattern.c_str(), /* the pattern */
+                     0,                /* options */
+                     &errPtr,          /* for error message */
+                     &errOffset,       /* for error offset */
+                     nullptr);         /* use default character tables */
+
+  if (nullptr == _re) {
+    AccessControlError("compile of regex '%s' at char %d: %s", _pattern.c_str(), errOffset, errPtr);
+
+    return false;
+  }
+
+  _extra = pcre_study(_re, 0, &errPtr);
+
+  if ((nullptr == _extra) && (nullptr != errPtr) && (0 != *errPtr)) {
+    AccessControlError("failed to study regex '%s': %s", _pattern.c_str(), errPtr);
+
+    pcre_free(_re);
+    _re = nullptr;
+    return false;
+  }
+
+  if (!_replace) {
+    /* No replacement necessary - we are done. */
+    return true;
+  }
+
+  _tokenCount  = 0;
+  bool success = true;
+
+  for (unsigned i = 0; i < _replacement.length(); i++) {
+    if (_replacement[i] == '$') {
+      if (_tokenCount >= TOKENCOUNT) {
+        AccessControlError("too many tokens in replacement string: %s", _replacement.c_str());
+
+        success = false;
+        break;
+      } else if (_replacement[i + 1] < '0' || _replacement[i + 1] > '9') {
+        AccessControlError("invalid replacement token $%c in %s: should be $0 - $9", _replacement[i + 1], _replacement.c_str());
+
+        success = false;
+        break;
+      } else {
+        /* Store the location of the replacement */
+        /* Convert '0' to 0 */
+        _tokens[_tokenCount]      = _replacement[i + 1] - '0';
+        _tokenOffset[_tokenCount] = i;
+        _tokenCount++;
+        /* Skip the next char */
+        i++;
+      }
+    }
+  }
+
+  if (!success) {
+    pcreFree();
+  }
+
+  return success;
+}
+
+/**
+ * @brief Destructor, deletes all patterns.
+ */
+MultiPattern::~MultiPattern()
+{
+  for (auto &p : this->_list) {
+    delete p;
+  }
+}
+
+/**
+ * @brief Check if empty.
+ * @return true if the classification contains any patterns, false otherwise
+ */
+bool
+MultiPattern::empty() const
+{
+  return _list.empty();
+}
+
+/**
+ * @brief Adds a pattern to the multi-pattern
+ *
+ * The order of addition matters during the classification
+ * @param pattern pattern pointer
+ */
+void
+MultiPattern::add(Pattern *pattern)
+{
+  this->_list.push_back(pattern);
+}
+
+/**
+ * @brief Matches the subject string against all patterns.
+ * @param subject subject string.
+ * @return true if any matches, false if nothing matches.
+ */
+bool
+MultiPattern::match(const String &subject) const
+{
+  for (auto p : this->_list) {
+    if (nullptr != p && p->match(subject)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+/**
+ * @brief Matches the subject string against all patterns.
+ * @param subject subject string.
+ * @param pattern last checked regex pattern string that matched.
+ * @return true if any matches, false if nothing matches.
+ */
+bool
+MultiPattern::match(const String &subject, String &pattern) const
+{
+  for (auto p : this->_list) {
+    if (nullptr != p && p->match(subject)) {
+      pattern = p->getPattern();
+      return true;
+    }
+  }
+  return false;
+}
+
+/**
+ * @brief Returns the name of the multi-pattern (set during the instantiation only).
+ */
+const String &
+MultiPattern::name() const
+{
+  return _name;
+}
+
+/**
+ * @brief Destructor, deletes all multi-patterns.
+ */
+Classifier::~Classifier()
+{
+  for (auto &p : _list) {
+    delete p;
+  }
+}
+
+/**
+ * @brief Classifies a subject string by matching against the vector of named multi-patterns
+ * in the order they were added and returns the first matching multi-pattern name.
+ * @param subject string subject being classified.
+ * @param name reference to a string where the name of the class that matched first will be stored.
+ * @return true if something matched, false otherwise.
+ */
+bool
+Classifier::classify(const String &subject, String &name) const
+{
+  bool matched = false;
+  for (auto p : _list) {
+    if (p->empty()) {
+      continue;
+    } else if (p->match(subject)) {
+      name    = p->name();
+      matched = true;
+      break;
+    }
+  }
+  return matched;
+}
+
+/**
+ * @brief Classifies a subject string by matching against the vector of named multi-patterns
+ * in the order they were added and returns the first matching multi-pattern name.
+ * @param subject string subject being classified.
+ * @param name reference to a string where the name of the class that matched first will be stored.
+ * @param pattern last checked pattern from the multi-pattern that matched
+ * @return true if something matched, false otherwise.
+ */
+bool
+Classifier::classify(const String &subject, String &name, String &pattern) const
+{
+  bool matched = false;
+  for (auto p : _list) {
+    if (p->empty()) {
+      continue;
+    } else if (p->match(subject, pattern)) {
+      name    = p->name();
+      matched = true;
+      break;
+    }
+  }
+  return matched;
+}
+
+/**
+ * @brief Matches a subject string by matching against the vector of named multi-patterns
+ * in the order they were added and returns the first non-matching multi-pattern name.
+ * @param subject string subject being classified.
+ * @param name reference to a string where the name of the class that matched first will be stored.
+ * @param pattern last checked pattern from the multi-pattern that did not match
+ * @return true if all multi-patterns matched, false otherwise.
+ */
+bool
+Classifier::matchAll(const String &subject, String &name, String &pattern) const
+{
+  bool matched = true;
+  for (auto p : _list) {
+    if (p->empty()) {
+      continue;
+    } else if (!p->match(subject, pattern)) {
+      name    = p->name();
+      matched = false;
+      break;
+    }
+  }
+  return matched;
+}
+
+/**
+ * @brief Adds a multi-pattern to the classifier.
+ *
+ * The order of addition matters during the classification
+ * @param pattern multi-pattern pointer
+ */
+void
+Classifier::add(MultiPattern *pattern)
+{
+  _list.push_back(pattern);
+}
+
+bool
+Classifier::empty()
+{
+  return _list.empty();
+}
diff --git a/plugins/experimental/access_control/pattern.h b/plugins/experimental/access_control/pattern.h
new file mode 100644
index 0000000..57f2d9f
--- /dev/null
+++ b/plugins/experimental/access_control/pattern.h
@@ -0,0 +1,148 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file pattern.h
+ * @brief PRCE related classes (header file).
+ */
+
+#pragma once
+
+#ifdef HAVE_PCRE_PCRE_H
+#include <pcre/pcre.h>
+#else
+#include <pcre.h>
+#endif
+
+#include "common.h"
+
+/**
+ * @brief PCRE matching, capturing and replacing
+ */
+class Pattern
+{
+public:
+  static const int TOKENCOUNT = 10;             /**< @brief Capturing groups $0..$9 */
+  static const int OVECOUNT   = TOKENCOUNT * 3; /**< @brief pcre_exec() array count, handle 10 capture groups */
+
+  Pattern();
+  virtual ~Pattern();
+
+  bool init(const String &pattern, const String &replacenemt, bool replace);
+  bool init(const String &config);
+  bool empty() const;
+  bool match(const String &subject);
+  bool capture(const String &subject, StringVector &result);
+  bool replace(const String &subject, String &result);
+  bool process(const String &subject, StringVector &result);
+  String getPattern();
+
+private:
+  bool compile();
+  void pcreFree();
+
+  pcre *_re;          /**< @brief PCRE compiled info structure, computed during initialization */
+  pcre_extra *_extra; /**< @brief PCRE study data block, computed during initialization */
+
+  String _pattern;     /**< @brief PCRE pattern string, containing PCRE patterns and capturing groups. */
+  String _replacement; /**< @brief PCRE replacement string, containing $0..$9 to be replaced with content of the capturing groups */
+
+  bool _replace; /**< @brief true if a replacement is needed, false if not, this is to distinguish between an empty replacement
+                    string and no replacement needed case */
+
+  int _tokenCount;              /**< @brief number of replacements $0..$9 found in the replacement string if not empty */
+  int _tokens[TOKENCOUNT];      /**< @brief replacement index 0..9, since they can be used in the replacement string in any order */
+  int _tokenOffset[TOKENCOUNT]; /**< @brief replacement offset inside the replacement string */
+};
+
+/**
+ * @brief Named list of regular expressions.
+ */
+class MultiPattern
+{
+public:
+  MultiPattern(const String name = "") : _name(name) {}
+  virtual ~MultiPattern();
+
+  bool empty() const;
+  void add(Pattern *pattern);
+  virtual bool match(const String &subject) const;
+  virtual bool match(const String &subject, String &pattern) const;
+  const String &name() const;
+
+protected:
+  std::vector<Pattern *> _list; /**< @brief vector which dictates the order of the pattern evaluation. */
+  String _name;                 /**< @brief multi-pattern name */
+
+  // noncopyable
+  MultiPattern(const MultiPattern &) = delete;            // disallow
+  MultiPattern &operator=(const MultiPattern &) = delete; // disallow
+};
+
+/**
+ * @brief Named list of non-matching regular expressions.
+ */
+class NonMatchingMultiPattern : public MultiPattern
+{
+public:
+  NonMatchingMultiPattern(const String &name) { _name = name; }
+  /*
+   * @brief Matches the subject string against all patterns.
+   * @param subject subject string
+   * @return return false if any of the patterns matches, true otherwise.
+   */
+  virtual bool
+  match(const String &subject) const
+  {
+    return !MultiPattern::match(subject);
+  }
+
+  virtual bool
+  match(const String &subject, String &pattern) const
+  {
+    return !MultiPattern::match(subject, pattern);
+  }
+
+  // noncopyable
+  NonMatchingMultiPattern(const NonMatchingMultiPattern &) = delete;            // disallow
+  NonMatchingMultiPattern &operator=(const NonMatchingMultiPattern &) = delete; // disallow
+};
+
+/**
+ * @brief Simple classifier which classifies a subject string using a list of named multi-patterns.
+ */
+class Classifier
+{
+public:
+  Classifier() {}
+  ~Classifier();
+
+  bool classify(const String &subject, String &name) const;
+  bool classify(const String &subject, String &name, String &pattern) const;
+  bool matchAll(const String &subject, String &name, String &pattern) const;
+
+  void add(MultiPattern *pattern);
+  bool empty();
+
+  // noncopyable
+  Classifier(const Classifier &) = delete;            // disallow
+  Classifier &operator=(const Classifier &) = delete; // disallow
+
+private:
+  std::vector<MultiPattern *> _list; /**< @brief vector which dictates the multi-pattern evaluation order */
+};
diff --git a/plugins/experimental/access_control/plugin.cc b/plugins/experimental/access_control/plugin.cc
new file mode 100644
index 0000000..c9f060a
--- /dev/null
+++ b/plugins/experimental/access_control/plugin.cc
@@ -0,0 +1,617 @@
+/*
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file plugin.cc
+ * @brief traffic server plugin entry points.
+ */
+
+#include <ctime> /* strftime */
+
+#include "common.h"         /* Common definitions */
+#include "config.h"         /* AccessControlConfig */
+#include "access_control.h" /* AccessToken */
+#include "ts/remap.h"       /* TSRemapInterface, TSRemapStatus, apiInfo */
+#include "ts/ts.h"          /* ATS API */
+#include "utils.h"          /* cryptoBase64Decode.* functions */
+#include "headers.h"        /* getHeader, setHeader, removeHeader */
+
+static const char *
+getEventName(TSEvent event)
+{
+  switch (event) {
+  case TS_EVENT_HTTP_CONTINUE:
+    return "TS_EVENT_HTTP_CONTINUE";
+  case TS_EVENT_HTTP_ERROR:
+    return "TS_EVENT_HTTP_ERROR";
+  case TS_EVENT_HTTP_READ_REQUEST_HDR:
+    return "TS_EVENT_HTTP_READ_REQUEST_HDR";
+  case TS_EVENT_HTTP_OS_DNS:
+    return "TS_EVENT_HTTP_OS_DNS";
+  case TS_EVENT_HTTP_SEND_REQUEST_HDR:
+    return "TS_EVENT_HTTP_SEND_REQUEST_HDR";
+  case TS_EVENT_HTTP_READ_CACHE_HDR:
+    return "TS_EVENT_HTTP_READ_CACHE_HDR";
+  case TS_EVENT_HTTP_READ_RESPONSE_HDR:
+    return "TS_EVENT_HTTP_READ_RESPONSE_HDR";
+  case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
+    return "TS_EVENT_HTTP_SEND_RESPONSE_HDR";
+  case TS_EVENT_HTTP_REQUEST_TRANSFORM:
+    return "TS_EVENT_HTTP_REQUEST_TRANSFORM";
+  case TS_EVENT_HTTP_RESPONSE_TRANSFORM:
+    return "TS_EVENT_HTTP_RESPONSE_TRANSFORM";
+  case TS_EVENT_HTTP_SELECT_ALT:
+    return "TS_EVENT_HTTP_SELECT_ALT";
+  case TS_EVENT_HTTP_TXN_START:
+    return "TS_EVENT_HTTP_TXN_START";
+  case TS_EVENT_HTTP_TXN_CLOSE:
+    return "TS_EVENT_HTTP_TXN_CLOSE";
+  case TS_EVENT_HTTP_SSN_START:
+    return "TS_EVENT_HTTP_SSN_START";
+  case TS_EVENT_HTTP_SSN_CLOSE:
+    return "TS_EVENT_HTTP_SSN_CLOSE";
+  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
+    return "TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE";
+  case TS_EVENT_HTTP_PRE_REMAP:
+    return "TS_EVENT_HTTP_PRE_REMAP";
+  case TS_EVENT_HTTP_POST_REMAP:
+    return "TS_EVENT_HTTP_POST_REMAP";
+  default:
+    return "UNHANDLED";
+  }
+  return "UNHANDLED";
+}
+
+/**
+ * @brief Plugin transaction data.
+ */
+class AccessControlTxnData
+{
+public:
+  AccessControlTxnData(AccessControlConfig *config) : _config(config) {}
+  const AccessControlConfig *_config;      /** @brief pointer to the plugin config */
+  String _subject                = "";     /** @brief subject for debugging purposes */
+  AccessTokenStatus _vaState     = UNUSED; /** @brief VA access control token validation status */
+  AccessTokenStatus _originState = UNUSED; /** @brief Origin access control token validation status */
+};
+
+/**
+ * @brief Plugin initialization.
+ * @param apiInfo remap interface info pointer
+ * @param errBuf error message buffer
+ * @param errBufSize error message buffer size
+ * @return always TS_SUCCESS.
+ */
+TSReturnCode
+TSRemapInit(TSRemapInterface *apiInfo, char *errBuf, int erroBufSize)
+{
+  return TS_SUCCESS;
+}
+
+/**
+ * @brief Plugin new instance entry point.
+ *
+ * Processes the configuration and initializes the plugin instance.
+ * @param argc plugin arguments number
+ * @param argv plugin arguments
+ * @param instance new plugin instance pointer (initialized in this function)
+ * @param errBuf error message buffer
+ * @param errBufSize error message buffer size
+ * @return TS_SUCCES if success or TS_ERROR if failure
+ */
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **instance, char *errBuf, int errBufSize)
+{
+  AccessControlConfig *config = new AccessControlConfig();
+  if (nullptr != config && config->init(argc, argv)) {
+    *instance = config;
+  } else {
+    AccessControlDebug("failed to initialize the " PLUGIN_NAME " plugin");
+    *instance = nullptr;
+    delete config;
+    return TS_ERROR;
+  }
+  return TS_SUCCESS;
+}
+
+/**
+ * @brief Plugin instance deletion clean-up entry point.
+ * @param plugin instance pointer.
+ */
+void
+TSRemapDeleteInstance(void *instance)
+{
+  AccessControlConfig *config = (AccessControlConfig *)instance;
+  delete config;
+}
+
+/**
+ * @brief A mapping between various failures and HTTP status code and message to be returned to the UA.
+ * @param state Access Token validation status
+ * @param config pointer to the plugin configuration to get the desired response for each failure.
+ * @return HTTP status
+ */
+static TSHttpStatus
+accessTokenStateToHttpStatus(AccessTokenStatus state, AccessControlConfig *config)
+{
+  TSHttpStatus httpStatus = TS_HTTP_STATUS_NONE;
+  const char *message     = "VALID";
+  switch (state) {
+  case VALID:
+    break;
+  case INVALID_SIGNATURE:
+    httpStatus = config->_invalidSignature;
+    message    = "invalid signature";
+    break;
+  case UNUSED:
+    httpStatus = config->_internalError;
+    message    = "uninitialized token";
+    break;
+  case INVALID_SECRET:
+    httpStatus = config->_internalError;
+    message    = "failed to find secrets";
+    break;
+  case INVALID_SYNTAX:
+  case MISSING_REQUIRED_FIELD:
+  case INVALID_FIELD:
+  case INVALID_FIELD_VALUE:
+  case INVALID_VERSION:
+  case INVALID_HASH_FUNCTION:
+  case INVALID_KEYID:
+    httpStatus = config->_invalidSyntax;
+    message    = "invalid syntax";
+    break;
+  case INVALID_SCOPE:
+  case OUT_OF_SCOPE:
+    httpStatus = config->_invalidScope;
+    message    = "invalid scope";
+    break;
+  case TOO_EARLY:
+  case TOO_LATE:
+    httpStatus = config->_invalidTiming;
+    message    = "invalid timing ";
+    break;
+  default:
+    /* Validation failed. */
+    httpStatus = config->_invalidRequest;
+    message    = "unknown error";
+    break;
+  }
+  AccessControlDebug("token validation: %s", message);
+
+  return httpStatus;
+}
+
+/**
+ * @brief a quick utility function to trim leading spaces.
+ */
+static void
+ltrim(String &target)
+{
+  String::size_type p(target.find_first_not_of(' '));
+
+  if (p != target.npos) {
+    target.erase(0, p);
+  }
+}
+
+/**
+ * @brief a quick utility function to get next duplicate header.
+ */
+static TSMLoc
+nextDuplicate(TSMBuffer buffer, TSMLoc hdr, TSMLoc field)
+{
+  TSMLoc next = TSMimeHdrFieldNextDup(buffer, hdr, field);
+  TSHandleMLocRelease(buffer, hdr, field);
+  return next;
+}
+
+/**
+ * @brief Append cookies by following the rules specified in the cookies config object.
+ * @param config cookies-related configuration containing information about which cookies need to be appended to the key.
+ * @note Add the cookies to "hier-part" (RFC 3986), always sort them in the cache key.
+ */
+bool
+getCookieByName(TSHttpTxn txn, TSMBuffer buf, TSMLoc hdrs, const String &cookieName, String &cookieValue)
+{
+  TSMLoc field;
+
+  for (field = TSMimeHdrFieldFind(buf, hdrs, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE); field != TS_NULL_MLOC;
+       field = ::nextDuplicate(buf, hdrs, field)) {
+    int count = TSMimeHdrFieldValuesCount(buf, hdrs, field);
+
+    for (int i = 0; i < count; ++i) {
+      const char *val;
+      int len;
+
+      val = TSMimeHdrFieldValueStringGet(buf, hdrs, field, i, &len);
+      if (val == nullptr || len == 0) {
+        continue;
+      }
+
+      String cookie;
+      std::istringstream istr(String(val, len));
+
+      while (std::getline(istr, cookie, ';')) {
+        ::ltrim(cookie); // Trim leading spaces.
+
+        String::size_type pos(cookie.find_first_of('='));
+        String name(cookie.substr(0, pos == String::npos ? cookie.size() : pos));
+
+        AccessControlDebug("cookie name: %s", name.c_str());
+
+        if (0 == cookieName.compare(name)) {
+          cookieValue.assign(cookie.substr(pos == String::npos ? pos : pos + 1));
+          return true;
+        }
+      }
+    }
+  }
+  return false;
+}
+
+/**
+ * @brief Handle token validation failures.
+ * @param txn transaction handle
+ * @param data transaction data
+ * @param reject true - reject requests if configured, false - don't reject
+ * @param httpStatus HTTP status
+ * @param status Access Token validation status.
+ */
+static TSRemapStatus
+handleInvalidToken(TSHttpTxn txnp, AccessControlTxnData *data, bool reject, const TSHttpStatus httpStatus, AccessTokenStatus status)
+{
+  TSRemapStatus resultStatus = TSREMAP_NO_REMAP;
+  if (reject) {
+    TSHttpTxnSetHttpRetStatus(txnp, httpStatus);
+    resultStatus = TSREMAP_DID_REMAP;
+  } else {
+    data->_vaState = status;
+  }
+  TSHttpTxnConfigIntSet(txnp, TS_CONFIG_HTTP_CACHE_HTTP, 0);
+
+  return resultStatus;
+}
+
+/**
+ * @brief Formats the time stamp into expires cookie field format
+ * @param expires Unix Time
+ * @return String containing the date in the appropriate format for the Expires cookie attribute.
+ */
+String
+getCookieExpiresTime(time_t expires)
+{
+  struct tm tm;
+  char dateTime[1024];
+  size_t dateTimeLen = 1024;
+
+  size_t len = strftime(dateTime, dateTimeLen, "%a, %d %b %Y %H:%M:%S GMT", gmtime_r(&expires, &tm));
+  return String(dateTime, len);
+}
+
+/**
+ * @brief Callback function that handles cache lookup complete state where the access token is checked before serving from cache.
+ *
+ * If cache-miss or cache-skip don't validate - request will be forwarded to the origin and will be validated anyway.
+ * If cache-hit or cache-hit-stale - validate access token and if validation fails force a cache-miss so request will be forwarded
+ * to origin and validated.
+ *
+ * @param contp continuation associated with this function.
+ * @param event corresponding event triggered at different hooks.
+ * @param edata HTTP transaction structures (access control plugin config).
+ * @return always 0
+ */
+int
+contHandleAccessControl(const TSCont contp, TSEvent event, void *edata)
+{
+  TSHttpTxn txnp                    = static_cast<TSHttpTxn>(edata);
+  AccessControlTxnData *data        = static_cast<AccessControlTxnData *>(TSContDataGet(contp));
+  const AccessControlConfig *config = data->_config;
+  TSEvent resultEvent               = TS_EVENT_HTTP_CONTINUE;
+
+  AccessControlDebug("event: '%s'", getEventName(event));
+
+  switch (event) {
+  case TS_EVENT_HTTP_SEND_RESPONSE_HDR: {
+    if (VALID != data->_vaState && !config->_respTokenHeaderName.empty() && !config->_cookieName.empty()) {
+      /* Set the cookie only if
+       * - the initial client cookie validation failed (missing or invalid cookie)
+       * - we expect a new access token from the origin
+       * - provided token from the origin is valid
+       * - and we know the name of the cookie to do set-cookie */
+
+      TSMBuffer clientRespBufp;
+      TSMLoc clientRespHdrLoc;
+      if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &clientRespBufp, &clientRespHdrLoc)) {
+        TSMBuffer serverRespBufp;
+        TSMLoc serverRespHdrLoc;
+        if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &serverRespBufp, &serverRespHdrLoc)) {
+          AccessControlDebug("got the response now create the cookie");
+
+          static const size_t MAX_HEADER_LEN = 4096;
+
+          int tokenHdrValueLen = MAX_HEADER_LEN;
+          char tokenHdrValue[MAX_HEADER_LEN];
+
+          getHeader(serverRespBufp, serverRespHdrLoc, config->_respTokenHeaderName.c_str(), config->_respTokenHeaderName.size(),
+                    tokenHdrValue, &tokenHdrValueLen);
+
+          if (0 < tokenHdrValueLen) {
+            AccessControlDebug("origin response access token '%.*s'", tokenHdrValueLen, tokenHdrValue);
+
+            AccessToken *token = config->_tokenFactory->getAccessToken();
+            if (nullptr != token &&
+                VALID == (data->_originState = token->validate(StringView(tokenHdrValue, tokenHdrValueLen), time(0)))) {
+              /*
+               * From RFC 6265 "HTTP State Management Mechanism":
+               * To maximize compatibility with user agents, servers that wish to
+               * store arbitrary data in a cookie-value SHOULD encode that data, for
+               * example, using Base64 [RFC4648].
+               */
+              int b64TokenHdrValueLen = cryptoBase64EncodedSize(tokenHdrValueLen);
+              char b64TokenHdrValue[b64TokenHdrValueLen];
+              size_t b64CookieLen =
+                cryptoModifiedBase64Encode(tokenHdrValue, tokenHdrValueLen, b64TokenHdrValue, b64TokenHdrValueLen);
+
+              String cookieValue;
+              cookieValue.append(config->_cookieName).append("=").append(b64TokenHdrValue, b64CookieLen).append("; ");
+
+              /** Currently Access Token implementation requires expiration to be set but the following is still a good
+               * consideration. Set the cookie Expires field to the token expiration field set by the origin if the time specified
+               * is invalid or not specified then don't set Expires attribute.
+               * @todo TBD may be adding a default / overriding Expires attribute configured by parameter would make sense ? */
+              time_t t = token->getExpiration();
+              if (0 != t) {
+                cookieValue.append("Expires=").append(getCookieExpiresTime(t)).append("; ");
+              }
+
+              /* Secure   - instructs the UA to include the cookie in an HTTP request only if the request is transmitted over
+               *            a secure channel, typically HTTP over Transport Layer Security (TLS)
+               * HttpOnly - instructs the UA to omit the cookie when providing access to cookies via “non-HTTP” APIs such as a web
+               *            browser API that exposes cookies to scripts */
+              cookieValue.append("Secure; HttpOnly");
+
+              AccessControlDebug("%.*s: %s", TS_MIME_LEN_SET_COOKIE, TS_MIME_FIELD_SET_COOKIE, cookieValue.c_str());
+              setHeader(clientRespBufp, clientRespHdrLoc, TS_MIME_FIELD_SET_COOKIE, TS_MIME_LEN_SET_COOKIE, cookieValue.c_str(),
+                        cookieValue.size());
+
+              delete token;
+            } else {
+              AccessControlDebug("failed to construct a valid origin access token, did not set-cookie with it");
+              /* Don't set any cookie, fail the request here returning appropriate status code and body.*/
+              TSHttpTxnSetHttpRetStatus(txnp, config->_invalidOriginResponse);
+              static const char *body = "Unexpected Response From the Origin Server\n";
+              char *buf               = (char *)TSmalloc(strlen(body) + 1);
+              sprintf(buf, "%s", body);
+              TSHttpTxnErrorBodySet(txnp, buf, strlen(buf), nullptr);
+
+              resultEvent = TS_EVENT_HTTP_ERROR;
+              break;
+            }
+          } else {
+            AccessControlDebug("no access token response header found");
+          }
+
+          /* Remove the origin response access token header. */
+          int numberOfFields = removeHeader(clientRespBufp, clientRespHdrLoc, config->_respTokenHeaderName.c_str(),
+                                            config->_respTokenHeaderName.size());
+          AccessControlDebug("removed %d %s client response header(s)", numberOfFields, config->_respTokenHeaderName.c_str());
+
+          TSHandleMLocRelease(serverRespBufp, TS_NULL_MLOC, serverRespHdrLoc);
+        } else {
+          AccessControlError("failed to retrieve server response header");
+        }
+
+        TSHandleMLocRelease(clientRespBufp, TS_NULL_MLOC, clientRespHdrLoc);
+      } else {
+        AccessControlError("failed to retrieve client response header");
+      }
+    }
+  } break;
+
+  case TS_EVENT_HTTP_TXN_CLOSE: {
+    if (!config->_extrValidationHdrName.empty()) {
+      TSMBuffer clientRespBufp;
+      TSMLoc clientRespHdrLoc;
+
+      /* Add some debugging / logging to the client request so it can be extracted through headers */
+      if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &clientRespBufp, &clientRespHdrLoc)) {
+        String statusHeader;
+        StringView vaState(accessTokenStatusToString(data->_vaState));
+        StringView originState(accessTokenStatusToString(data->_originState));
+
+        /* UC_ = UA Cookie, to store the token validation status when extracted from HTTP cookie */
+        if (!vaState.empty()) {
+          statusHeader.append("UC_").append(vaState);
+        }
+        /* OH_ = origin response header, to store the token validation status when extracted from origin response header. */
+        if (!originState.empty()) {
+          statusHeader.append(vaState.empty() ? "" : ",");
+          statusHeader.append("OH_").append(originState);
+        }
+        AccessControlDebug("adding header %s: '%s'", config->_extrValidationHdrName.c_str(), statusHeader.c_str());
+        setHeader(clientRespBufp, clientRespHdrLoc, config->_extrValidationHdrName.c_str(), config->_extrValidationHdrName.size(),
+                  statusHeader.c_str(), statusHeader.length());
+
+      } else {
+        AccessControlError("failed to retrieve client response header");
+      }
+    }
+
+    /* Destroy the txn continuation and its data */
+    delete data;
+    TSContDestroy(contp);
+  } break;
+  default:
+    break;
+  }
+
+  TSHttpTxnReenable(txnp, resultEvent);
+  return 0;
+}
+
+/**
+ * @brief Enforces access control, currently supports access token from a cookie.
+ *
+ * @param instance plugin instance pointer
+ * @param txn transaction handle
+ * @param rri remap request info pointer
+ * @param config pointer to the plugin configuration
+ * @return TSREMAP_NO_REMAP (access validation = success)
+ * TSREMAP_DID_REMAP (access validation = failure and rejection of failed requests is configured)
+ */
+TSRemapStatus
+enforceAccessControl(TSHttpTxn txnp, TSRemapRequestInfo *rri, AccessControlConfig *config)
+{
+  if (config->_cookieName.empty()) {
+    /* For now only checking a cookie is supported and if its name is unknown (checking cookie disabled) then do nothing. */
+    return TSREMAP_NO_REMAP;
+  }
+
+  TSRemapStatus remapStatus = TSREMAP_NO_REMAP;
+
+  /* Create txn data and register hooks */
+  AccessControlTxnData *data = new AccessControlTxnData(config);
+  TSCont cont                = TSContCreate(contHandleAccessControl, TSMutexCreate());
+  TSContDataSet(cont, static_cast<void *>(data));
+  TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, cont);
+  TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, cont);
+
+  /* Validate the token */
+  bool reject = config->_rejectRequestsWithInvalidTokens;
+  String cookie;
+  bool found = getCookieByName(txnp, rri->requestBufp, rri->requestHdrp, config->_cookieName, cookie);
+  if (found) {
+    AccessControlDebug("%s cookie: '%s'", config->_cookieName.c_str(), cookie.c_str());
+
+    /*
+     * From RFC 6265 "HTTP State Management Mechanism":
+     * To maximize compatibility with user agents, servers that wish to
+     * store arbitrary data in a cookie-value SHOULD encode that data, for
+     * example, using Base64 [RFC4648].
+     */
+    size_t decodedCookieBufferSize = cryptoBase64DecodeSize(cookie.c_str(), cookie.size());
+    char decodedCookie[decodedCookieBufferSize];
+    size_t decryptedCookieSize = cryptoModifiedBase64Decode(cookie.c_str(), cookie.size(), decodedCookie, decodedCookieBufferSize);
+    if (0 < decryptedCookieSize) {
+      AccessToken *token = config->_tokenFactory->getAccessToken();
+      if (nullptr != token) {
+        data->_vaState = token->validate(StringView(decodedCookie, decryptedCookieSize), time(0));
+        if (VALID != data->_vaState) {
+          remapStatus =
+            handleInvalidToken(txnp, data, reject, accessTokenStateToHttpStatus(data->_vaState, config), data->_vaState);
+        } else {
+          /* Valid token, if configured extract the token subject to a header,
+           * only if we can trust it - token is valid to prevent using it by mistake */
+          if (!config->_extrSubHdrName.empty()) {
+            String sub(token->getSubject());
+            setHeader(rri->requestBufp, rri->requestHdrp, config->_extrSubHdrName.c_str(), config->_extrSubHdrName.size(),
+                      sub.c_str(), sub.size());
+          }
+        }
+        /* If configure extract the UA token id into a header likely for debugging,
+         * extract it even if token validation fails and we don't trust it */
+        if (!config->_extrTokenIdHdrName.empty()) {
+          String tokeId(token->getTokenId());
+          setHeader(rri->requestBufp, rri->requestHdrp, config->_extrTokenIdHdrName.c_str(), config->_extrTokenIdHdrName.size(),
+                    tokeId.c_str(), tokeId.size());
+        }
+        delete token;
+      } else {
+        AccessControlDebug("failed to construct access token");
+        remapStatus = handleInvalidToken(txnp, data, reject, config->_internalError, UNUSED);
+      }
+    } else {
+      AccessControlDebug("failed to decode cookie value");
+      remapStatus = handleInvalidToken(txnp, data, reject, config->_invalidRequest, UNUSED);
+    }
+  } else {
+    AccessControlDebug("failed to find cookie %s", config->_cookieName.c_str());
+    remapStatus = handleInvalidToken(txnp, data, reject, config->_invalidRequest, UNUSED);
+  }
+
+  return remapStatus;
+}
+
+/**
+ * @brief Remap and sets up access control based on whether access control is required, failed, etc.
+ *
+ * @param instance plugin instance pointer
+ * @param txn transaction handle
+ * @param rri remap request info pointer
+ * @return TSREMAP_NO_REMAP (access validation = success)
+ * TSREMAP_DID_REMAP (access validation = failure and rejection of failed requests is configured)
+ */
+TSRemapStatus
+TSRemapDoRemap(void *instance, TSHttpTxn txnp, TSRemapRequestInfo *rri)
+{
+  TSRemapStatus remapStatus   = TSREMAP_NO_REMAP;
+  AccessControlConfig *config = (AccessControlConfig *)instance;
+
+  if (nullptr != config) {
+    /* Plugin is designed to be used only with TLS, check the scheme */
+    int schemeLen      = 0;
+    const char *scheme = TSUrlSchemeGet(rri->requestBufp, rri->requestUrl, &schemeLen);
+    if (nullptr != scheme) {
+      if (/* strlen("https") */ 5 == schemeLen && 0 == strncmp(scheme, "https", schemeLen)) {
+        AccessControlDebug("validate the access token");
+
+        String reqPath;
+        int pathLen      = 0;
+        const char *path = TSUrlPathGet(rri->requestBufp, rri->requestUrl, &pathLen);
+        if (nullptr != path && 0 < pathLen) {
+          reqPath.assign(path, pathLen);
+        }
+        /* Check if any of the uri-path multi-pattern matched and if yes enforce access control. */
+        String filename;
+        String pattern;
+        if (config->_uriPathScope.empty()) {
+          /* Scope match enforce access control */
+          AccessControlError("no plugin scope specified, enforcing access control");
+          remapStatus = enforceAccessControl(txnp, rri, config);
+        } else {
+          if (true == config->_uriPathScope.matchAll(reqPath, filename, pattern)) {
+            AccessControlDebug("matched plugin scope enforcing access control for path %s", reqPath.c_str());
+
+            /* Scope match enforce access control */
+            remapStatus = enforceAccessControl(txnp, rri, config);
+          } else {
+            AccessControlError("not matching plugin scope (file: %s, pattern %s), skipping access control for path '%s'",
+                               filename.c_str(), pattern.c_str(), reqPath.c_str());
+          }
+        }
+      } else {
+        TSHttpTxnSetHttpRetStatus(txnp, config->_invalidRequest);
+        AccessControlError("https is the only allowed scheme (plugin should be used only with TLS)");
+        remapStatus = TSREMAP_DID_REMAP;
+      }
+    } else {
+      TSHttpTxnSetHttpRetStatus(txnp, config->_internalError);
+      AccessControlError("failed to get request uri-scheme");
+      remapStatus = TSREMAP_DID_REMAP;
+    }
+  } else {
+    /* Something is terribly wrong, we cannot get the configuration */
+    TSHttpTxnSetHttpRetStatus(txnp, TS_HTTP_STATUS_INTERNAL_SERVER_ERROR);
+    AccessControlError("configuration unavailable");
+    remapStatus = TSREMAP_DID_REMAP;
+  }
+
+  return remapStatus;
+}
diff --git a/plugins/experimental/access_control/unit-tests/test_access_control.cc b/plugins/experimental/access_control/unit-tests/test_access_control.cc
new file mode 100644
index 0000000..5bbd6c5
--- /dev/null
+++ b/plugins/experimental/access_control/unit-tests/test_access_control.cc
@@ -0,0 +1,171 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file test_access_control.cc
+ * @brief Unit tests for functions implementing access control
+ */
+
+#define CATCH_CONFIG_MAIN      /* include main function */
+#include <catch.hpp>           /* catch unit-test framework */
+#include "../access_control.h" /* access_control utility */
+
+/* AccessToken ***************************************************************************************************************** */
+
+StringMap secrets = {{"1", "1234567890"}};
+
+bool enableDebug = true;
+
+TEST_CASE("AssetToken: simple test", "[AssetToken][access_control][utility]")
+{
+  KvpAccessTokenConfig tokenConfig;
+
+  class UnitKvpAccessToken : public KvpAccessToken
+  {
+  public:
+    using KvpAccessToken::KvpAccessToken;
+    using KvpAccessToken::validateSemantics;
+  };
+
+  KvpAccessTokenBuilder atb(tokenConfig, secrets);
+  atb.addSubject("ABCDEFG");
+  atb.addExpiration(1234567);
+  atb.addNotBefore(2345678);
+  atb.addIssuedAt(3456789);
+  atb.addTokenId("tokenidvalue");
+  atb.addVersion("1");
+  atb.addScope("scopevalue");
+  atb.sign("1", WDN_HASH_SHA256);
+
+  UnitKvpAccessToken token(tokenConfig, secrets, enableDebug);
+  CHECK(VALID == token.parse(atb.get()));
+  CHECK(VALID == token.validateSemantics());
+  CHECK(VALID == token.getState());
+
+  CHECK(token.getSubject() == "ABCDEFG");
+  CHECK(token.getExpiration() == 1234567);
+  CHECK(token.getNotBefore() == 2345678);
+  CHECK(token.getIssuedAt() == 3456789);
+  CHECK(token.getTokenId() == "tokenidvalue");
+  CHECK(token.getVersion() == "1");
+  CHECK(token.getScope() == "scopevalue");
+  CHECK(token.getKeyId() == "1");
+  CHECK(token.getHashFunction() == WDN_HASH_SHA256);
+
+  // DEBUG_OUT(token);
+}
+
+TEST_CASE("AssetToken: empty token", "[AssetToken][access_control][utility]")
+{
+  KvpAccessTokenConfig tokenConfig;
+  KvpAccessToken token(tokenConfig, secrets, enableDebug);
+  CHECK(INVALID_SYNTAX == token.parse(""));
+}
+
+TEST_CASE("AssetToken: invalid field", "[AssetToken][access_control][utility]")
+{
+  KvpAccessTokenConfig tokenConfig;
+
+  KvpAccessToken token(tokenConfig, secrets, enableDebug);
+  CHECK(INVALID_FIELD == token.parse("NOTVALID=1234567"));
+}
+
+TEST_CASE("AssetToken: empty field", "[AssetToken][access_control][utility]")
+{
+  KvpAccessTokenConfig tokenConfig;
+  KvpAccessTokenBuilder atb(tokenConfig, secrets);
+  atb.addSubject("ABCDEFG");
+  atb.addExpiration(1234567);
+
+  /* prepend a key-value-pair separator to a valid token */
+  KvpAccessToken token1(tokenConfig, secrets, enableDebug);
+  CHECK(INVALID_SYNTAX == token1.parse(String(tokenConfig.pairDelimiter).append(atb.get())));
+
+  KvpAccessToken token2(tokenConfig, secrets, enableDebug);
+  CHECK(INVALID_SYNTAX == token1.parse(String(atb.get()).append(tokenConfig.pairDelimiter)));
+
+  KvpAccessToken token3(tokenConfig, secrets, enableDebug);
+  CHECK(INVALID_SYNTAX == token1.parse(tokenConfig.pairDelimiter));
+}
+
+TEST_CASE("AssetToken: missing required fields", "[AssetToken][access_control][utility]")
+{
+  KvpAccessTokenConfig tokenConfig;
+  KvpAccessTokenBuilder atb(tokenConfig, secrets);
+
+  class UnitKvpAccessToken : public KvpAccessToken
+  {
+  public:
+    using KvpAccessToken::KvpAccessToken;
+    using KvpAccessToken::validateSemantics;
+  };
+
+  UnitKvpAccessToken token(tokenConfig, secrets, enableDebug);
+  CHECK(MISSING_REQUIRED_FIELD == token.validateSemantics());
+
+  /* add subject */
+  atb.addSubject("ABCDEFG");
+  CHECK(VALID == token.parse(atb.get()));
+  CHECK(MISSING_REQUIRED_FIELD == token.validateSemantics());
+
+  /* add expiration */
+  atb.addExpiration(1234567);
+  CHECK(VALID == token.parse(atb.get()));
+  CHECK(MISSING_REQUIRED_FIELD == token.validateSemantics());
+
+  /* add kid and md */
+  atb.sign("1", WDN_HASH_SHA256);
+  CHECK(VALID == token.parse(atb.get()));
+  CHECK(VALID == token.validateSemantics());
+}
+
+TEST_CASE("AssetToken: simple HMAC SHA256 signature test", "[AssetToken][access_control][utility]")
+{
+  KvpAccessTokenConfig tokenConfig;
+
+  KvpAccessTokenBuilder atb(tokenConfig, secrets);
+  atb.addSubject("ABCDEFG");
+  atb.addExpiration(1234567);
+  atb.addNotBefore(2345678);
+  atb.addIssuedAt(3456789);
+  atb.addTokenId("tokenidvalue");
+  atb.addVersion("1");
+  atb.addScope("scopevalue");
+  atb.sign("1", WDN_HASH_SHA256);
+
+  class UnitKvpAccessToken : public KvpAccessToken
+  {
+  public:
+    using KvpAccessToken::KvpAccessToken;
+    using KvpAccessToken::validateSemantics;
+    using KvpAccessToken::validateSignature;
+    using KvpAccessToken::_messageDigest;
+  };
+
+  UnitKvpAccessToken token(tokenConfig, secrets, enableDebug);
+  CHECK(VALID == token.parse(atb.get()));
+  CHECK(VALID == token.validateSignature());
+
+  // DEBUG_OUT(token);
+
+  /* Now break the signature and test for failure */
+  token._messageDigest = "invalid12345";
+  CHECK_FALSE(MISSING_REQUIRED_FIELD == token.validateSemantics());
+  CHECK(INVALID_SIGNATURE == token.validateSignature());
+  // DEBUG_OUT("Dumping token" << std::endl << token);
+}
diff --git a/plugins/experimental/access_control/unit-tests/test_utils.cc b/plugins/experimental/access_control/unit-tests/test_utils.cc
new file mode 100644
index 0000000..9cc69aa
--- /dev/null
+++ b/plugins/experimental/access_control/unit-tests/test_utils.cc
@@ -0,0 +1,265 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file test_utils.cc test
+ * @brief Unit tests for functions used in utils.cc
+ */
+
+#include <catch.hpp> /* catch unit-test framework */
+#include "../utils.h"
+#include "../common.h"
+
+/*********************************************************************************************************************
+ * Base64 related test
+ * @note the purpose of these test is not to test Base64 itself since it is provided by openssl library,
+ * the idea is to test the usage and some corner cases.
+ ********************************************************************************************************************/
+
+TEST_CASE("Base64: estimate buffer size needed to encode a message", "[Base64][access_control][utility]")
+{
+  size_t encodedLen;
+
+  /* Test with a zero decoded message lenght */
+  encodedLen = cryptoBase64EncodedSize(0);
+  CHECK(0 == encodedLen);
+
+  /* Test with a random non-zero decoded message length */
+  encodedLen = cryptoBase64EncodedSize(64);
+  CHECK(88 == encodedLen);
+
+  /* Test the space for padding. Size of encoding that would result in 2 x "=" padding */
+  encodedLen = cryptoBase64EncodedSize(strlen("176a1620e31b14782ba2b66de3edc5b3cb19630475b2ce2ee292d5fd0fe41c3abc"));
+  CHECK(88 == encodedLen);
+
+  /* Test the space for padding. Size of encoding that would result in 1 x "=" padding */
+  encodedLen = cryptoBase64EncodedSize(strlen("176a1620e31b14782ba2b66de3edc5b3cb19630475b2ce2ee292d5fd0fe41c3ab"));
+  CHECK(88 == encodedLen);
+
+  /* Test the space for padding. Size of encoding that would result in no padding */
+  encodedLen = cryptoBase64EncodedSize(strlen("176a1620e31b14782ba2b66de3edc5b3cb19630475b2ce2ee292d5fd0fe41c3a"));
+  CHECK(88 == encodedLen);
+}
+
+TEST_CASE("Base64: estimate buffer size needed to decode a message", "[Base64][access_control][utility]")
+{
+  size_t encodedLen;
+  const char *encoded;
+
+  /* Padding with 2 x '=' */
+  encoded    = "MTc2YTE2MjBlMzFiMTQ3ODJiYTJiNjZkZTNlZGM1YjNjYjE5NjMwNDc1YjJjZTJlZTI5MmQ1ZmQwZmU0MWMzYQ==";
+  encodedLen = cryptoBase64DecodeSize(encoded, strlen(encoded));
+  CHECK(64 == encodedLen);
+
+  /* Padding with 1 x '=' */
+  encoded    = "MTc2YTE2MjBlMzFiMTQ3ODJiYTJiNjZkZTNlZGM1YjNjYjE5NjMwNDc1YjJjZTJlZTI5MmQ1ZmQwZmU0MWMzYWI=";
+  encodedLen = cryptoBase64DecodeSize(encoded, strlen(encoded));
+  CHECK(65 == encodedLen);
+
+  /* Padding with 0 x "=" */
+  encoded    = "MTc2YTE2MjBlMzFiMTQ3ODJiYTJiNjZkZTNlZGM1YjNjYjE5NjMwNDc1YjJjZTJlZTI5MmQ1ZmQwZmU0MWMzYWJj";
+  encodedLen = cryptoBase64DecodeSize(encoded, strlen(encoded));
+  CHECK(66 == encodedLen);
+
+  /* Test empty encoded message caclulation */
+  encoded    = "";
+  encodedLen = cryptoBase64DecodeSize(encoded, strlen(encoded));
+  CHECK(0 == encodedLen);
+
+  /* Test empty encoded message caclulation */
+  encoded    = nullptr;
+  encodedLen = cryptoBase64DecodeSize(encoded, 0);
+  CHECK(0 == encodedLen);
+}
+
+TEST_CASE("Base64: quick encode / decode", "[Base64][access_control][utility]")
+{
+  const char message[] = "176a1620e31b14782ba2b66de3edc5b3cb19630475b2ce2ee292d5fd0fe41c3a";
+  size_t messageLen    = strlen(message);
+  CHECK(64 == messageLen);
+
+  size_t encodedMessageEstimatedLen = cryptoBase64EncodedSize(messageLen);
+  CHECK(88 == encodedMessageEstimatedLen);
+  char encodedMessage[encodedMessageEstimatedLen];
+
+  size_t encodedMessageLen = cryptoBase64Encode(message, messageLen, encodedMessage, encodedMessageEstimatedLen);
+  CHECK(88 == encodedMessageLen);
+  CHECK(0 == strncmp(encodedMessage, "MTc2YTE2MjBlMzFiMTQ3ODJiYTJiNjZkZTNlZGM1YjNjYjE5NjMwNDc1YjJjZTJlZTI5MmQ1ZmQwZmU0MWMzYQ==",
+                     encodedMessageLen));
+
+  size_t decodedMessageEstimatedLen = cryptoBase64DecodeSize(encodedMessage, encodedMessageLen);
+  CHECK(64 == decodedMessageEstimatedLen);
+  char decodedMessage[encodedMessageEstimatedLen];
+  size_t decodedMessageLen = cryptoBase64Decode(encodedMessage, encodedMessageLen, decodedMessage, encodedMessageLen);
+
+  CHECK(64 == decodedMessageLen);
+  CHECK(0 == strncmp(decodedMessage, message, messageLen));
+}
+
+TEST_CASE("Base64: encode empty message into empty buffer", "[Base64][access_control][utility]")
+{
+  /* Encode empty message */
+  const char *message               = "";
+  size_t messageLen                 = strlen(message);
+  size_t encodedMessageEstimatedLen = 0;
+  char encodedMessage[encodedMessageEstimatedLen];
+
+  size_t encodedMessageLen = cryptoBase64Encode(message, messageLen, encodedMessage, encodedMessageEstimatedLen);
+
+  CHECK(0 == encodedMessageLen);
+  CHECK(0 == strncmp(encodedMessage, "", encodedMessageLen));
+}
+
+TEST_CASE("Base64: encode null message into null buffer", "[Base64][access_control][utility]")
+{
+  /* Encode using nullptr pointer and 0 sizes */
+  char *message                     = nullptr;
+  size_t messageLen                 = 0;
+  char *encodedMessage              = nullptr;
+  size_t encodedMessageEstimatedLen = 0;
+
+  size_t encodedMessageLen = cryptoBase64Encode(message, messageLen, encodedMessage, encodedMessageEstimatedLen);
+
+  CHECK(0 == encodedMessageLen);
+  CHECK(nullptr == encodedMessage);
+}
+
+TEST_CASE("Base64: decode empty message into empty buffer", "[Base64][access_control][utility]")
+{
+  const char *encodedMessage        = "";
+  size_t encodedMessageLen          = strlen(encodedMessage);
+  size_t decodedMessageEstimatedLen = 0;
+  char decodedMessage[decodedMessageEstimatedLen];
+
+  size_t decodedMessageLen = cryptoBase64Decode(encodedMessage, encodedMessageLen, decodedMessage, encodedMessageLen);
+
+  CHECK(0 == decodedMessageLen);
+  CHECK(0 == strncmp(decodedMessage, "", decodedMessageLen));
+}
+
+TEST_CASE("Base64: decode null message into null buffer", "[Base64][access_control][utility]")
+{
+  const char *encodedMessage = nullptr;
+  size_t encodedMessageLen   = 0;
+  char *decodedMessage       = nullptr;
+
+  size_t decodedMessageLen = cryptoBase64Decode(encodedMessage, encodedMessageLen, decodedMessage, encodedMessageLen);
+
+  CHECK(0 == decodedMessageLen);
+  CHECK(nullptr == decodedMessage);
+}
+
+TEST_CASE("Base64: quick encode / decode with '+', '/' and various paddings", "[Base64][access_control][utility]")
+{
+  const char *decoded[] = {"ts>ts?ts!!!!", "ts>ts?ts!!!", "ts>ts?ts!!"};
+  const char *encoded[] = {"dHM+dHM/dHMhISEh", "dHM+dHM/dHMhISE=", "dHM+dHM/dHMhIQ=="};
+
+  for (int i = 0; i < 3; i++) {
+    /* Encode */
+    const char *message               = decoded[i];
+    size_t messageLen                 = strlen(message);
+    size_t encodedMessageEstimatedLen = cryptoBase64EncodedSize(messageLen);
+    char encodedMessage[encodedMessageEstimatedLen];
+    size_t encodedMessageLen = cryptoBase64Encode(message, messageLen, encodedMessage, encodedMessageEstimatedLen);
+    CHECK(strlen(encoded[i]) == encodedMessageLen);
+    CHECK(0 == strncmp(encodedMessage, encoded[i], encodedMessageLen));
+
+    /* Decode */
+    size_t decodedMessageEstimatedLen = cryptoBase64DecodeSize(encodedMessage, encodedMessageLen);
+    CHECK(strlen(decoded[i]) == decodedMessageEstimatedLen);
+    char decodedMessage[encodedMessageEstimatedLen];
+    size_t decodedMessageLen = cryptoBase64Decode(encodedMessage, encodedMessageLen, decodedMessage, encodedMessageLen);
+    CHECK(strlen(decoded[i]) == decodedMessageLen);
+    CHECK(0 == strncmp(decodedMessage, message, messageLen));
+  }
+}
+
+/*********************************************************************************************************************
+ * Modified Base64 related test
+ * @note since Modified Base64 function use Base64 which use openssl the idea is to test the modification itself
+ * (for more info see the comment in the implementation) + some corner cases.
+ ********************************************************************************************************************/
+
+TEST_CASE("Base64: modified encode / decode with '+', '/' and various paddings", "[Base64][access_control][utility]")
+{
+  const char *decoded[] = {"ts>ts?ts!!!!", "ts>ts?ts!!!", "ts>ts?ts!!"};
+  const char *encoded[] = {"dHM-dHM_dHMhISEh", "dHM-dHM_dHMhISE", "dHM-dHM_dHMhIQ"};
+
+  for (int i = 0; i < 3; i++) {
+    /* Encode */
+    const char *message               = decoded[i];
+    size_t messageLen                 = strlen(message);
+    size_t encodedMessageEstimatedLen = cryptoBase64EncodedSize(messageLen);
+    char encodedMessage[encodedMessageEstimatedLen];
+    size_t encodedMessageLen = cryptoModifiedBase64Encode(message, messageLen, encodedMessage, encodedMessageEstimatedLen);
+    CHECK(strlen(encoded[i]) == encodedMessageLen);
+    CHECK(0 == strncmp(encodedMessage, encoded[i], encodedMessageLen));
+
+    /* Decode */
+    size_t decodedMessageEstimatedLen = cryptoBase64DecodeSize(encodedMessage, encodedMessageLen);
+    char decodedMessage[encodedMessageEstimatedLen];
+    size_t decodedMessageLen =
+      cryptoModifiedBase64Decode(encodedMessage, encodedMessageLen, decodedMessage, decodedMessageEstimatedLen);
+    CHECK(strlen(decoded[i]) == decodedMessageLen);
+    CHECK(0 == strncmp(decodedMessage, message, messageLen));
+  }
+}
+
+/*********************************************************************************************************************
+ * Digest calculation related test
+ * @note since Modified Base64 function use Base64 which use openssl the idea is to test the modification itself
+ * (for more info see the comment in the implementation) + some corner cases.
+ ********************************************************************************************************************/
+
+TEST_CASE("HMAC Digest: test various supported/unsupported types", "[MAC][access_control][utility]")
+{
+  cryptoMagicInit();
+
+  const String key  = "1234567890";
+  const String data = "calculate a message digest on this";
+
+  char out[MAX_MSGDIGEST_BUFFER_SIZE];
+  char hexOut[MAX_MSGDIGEST_BUFFER_SIZE];
+
+  StringList types   = {"MD4", "MD5", "RIPEMD160", "SHA1", "SHA224", "SHA256", "SHA384", "SHA512"};
+  StringList digests = {"6b3057137a6e17613883ac25a628b1b3",
+                        "820117c62fa161804efb3743cc838b81",
+                        "ccf3230972bcf229fb3b16741495c74a72bbdd14",
+                        "0e3dfdfb04a3dfcd4d195cb1a5e4186feab2e0c1",
+                        "00a6f43962e2b35cb2491f81d59ef2268309c8cde744891188c9b855",
+                        "149333e1db61f9a18a91a13aca0370b89cec4c546360b85530ae2da97b7b1cb9",
+                        "da500bdc5318bfce7a8a094b9da1d8ac901e145d73cc7039e41c6bff4451734269689465ca39e861b9026b481d3cc9db",
+                        "e075c8b0637bc4fb82cdca66a2b72e3c1734f4f78c803e5db7ca879f85f16b2e057fa62bdd09eef5bbea562990d52a671927033056"
+                        "314c19092263f753ecd019"};
+
+  StringList::iterator digestIter = digests.begin();
+  for (String digestType : types) {
+    size_t outLen = cryptoMessageDigestGet(digestType.c_str(), data.c_str(), data.length(), key.c_str(), key.length(), out,
+                                           MAX_MSGDIGEST_BUFFER_SIZE);
+    CHECK(0 < outLen);
+    if (0 < outLen) {
+      size_t hexOutLen = hexDecode(digestIter->c_str(), digestIter->length(), hexOut, MAX_MSGDIGEST_BUFFER_SIZE);
+      CHECK(0 < hexOutLen);
+      CHECK(cryptoMessageDigestEqual(hexOut, hexOutLen, out, outLen));
+    }
+
+    digestIter++;
+  }
+
+  cryptoMagicCleanup();
+}
diff --git a/plugins/experimental/access_control/utils.cc b/plugins/experimental/access_control/utils.cc
new file mode 100644
index 0000000..cfddac7
--- /dev/null
+++ b/plugins/experimental/access_control/utils.cc
@@ -0,0 +1,497 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file utils.cc
+ * @brief Various utility functions.
+ * @see utils.h
+ */
+
+#include <errno.h>          /* errno */
+#include <limits.h>         /* LONG_MIN, LONG_MAX */
+#include <openssl/bio.h>    /* BIO I/O abstraction */
+#include <openssl/buffer.h> /* buf_mem_st */
+#include <openssl/err.h>    /* ERR_get_error() and ERR_error_string_n() */
+
+#include "common.h"
+#include "utils.h"
+
+/**
+ * @brief Parse a counted string containing a long integer
+ *
+ * @param s ptr to the counted string
+ * @param lenght character count
+ * @param val where the long integer to be stored
+ * @return true - success, false - failed to parse
+ */
+bool
+parseStrLong(const char *s, size_t length, long &val)
+{
+  // Make an extra copy since strtol expects NULL-terminated strings.
+  char str[length + 1];
+  strncpy(str, s, length);
+  str[length] = 0;
+
+  errno = 0;
+  char *temp;
+  val = strtol(str, &temp, 0);
+
+  if (temp == str || *temp != '\0' || ((val == LONG_MIN || val == LONG_MAX) && errno == ERANGE)) {
+    AccessControlError("Could not convert '%s' to a long integer and leftover string is: '%s'", str, temp);
+    return false;
+  }
+  return true;
+}
+
+/* ******* Encoding/Decoding functions ******* */
+
+/**
+ * @brief Encode a character counted string into hexadecimal format
+ *
+ * @param in ptr to an input counted string
+ * @param inLen input character count
+ * @param out ptr to a buffer to store the hexadecimal string
+ * @param outLen output character count (2 * inLen + 1)
+ * @return the number of character actually added to the output buffer.
+ */
+size_t
+hexEncode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  const char *src    = in;
+  const char *srcEnd = in + inLen;
+  char *dst          = out;
+  char *dstEnd       = out + outLen;
+
+  while (src < srcEnd && dst < dstEnd && 2 == sprintf(dst, "%02x", (unsigned char)*src)) {
+    dst += 2;
+    src++;
+  }
+  return dst - out;
+}
+
+/**
+ * @brief Convert character containing [0-1], [A-F], [a-f] to unsigned char.
+ *
+ * @param c character to be converted.
+ * @return the unsigned character if success or FF if failure.
+ */
+static unsigned char
+hex2uchar(char c)
+{
+  if (c >= '0' && c <= '9')
+    return c - '0';
+  if (c >= 'a' && c <= 'f')
+    return c - 'a' + 10;
+  if (c >= 'A' && c <= 'F')
+    return c - 'A' + 10;
+  return 255;
+}
+
+/**
+ * @brief Decode from hexadecimal format into character counted string
+ *
+ * @param in ptr to an input counted string in hexadecimal format
+ * @param inLen input character count
+ * @param out ptr to a buffer to store the decoded counted string
+ * @param outLen output character count (inLen/2)
+ * @return the number of character actually added to the output buffer.
+ */
+size_t
+hexDecode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  const char *src    = in;
+  const char *srcEnd = in + inLen;
+  char *dst          = out;
+  char *dstEnd       = out + outLen;
+
+  while (src < (srcEnd - 1) && dst < dstEnd) {
+    *dst++ = hex2uchar(*src) << 4 | hex2uchar(*(src + 1));
+    src += 2;
+  }
+  return dst - out;
+}
+
+/**
+ * @brief URL(percent)-encode a counted string
+ *
+ * @param in ptr to an input decoded counted string
+ * @param inLen input character count
+ * @param out ptr to an output buffer where the encoded string will be stored.
+ * @param outLen output character count (output max size, should be 3 x inLen + 1)
+ * @return the number of character actually added to the output buffer.
+ */
+size_t
+urlEncode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  const char *src = in;
+  char *dst       = out;
+  while ((size_t)(src - in) < inLen && (size_t)(dst - out) < outLen) {
+    if (isalnum(*src) || *src == '-' || *src == '_' || *src == '.' || *src == '~') {
+      *dst++ = *src;
+    } else if (*src == ' ') {
+      *dst++ = '+';
+    } else {
+      *dst++ = '%';
+      sprintf(dst, "%02x", (unsigned char)*src);
+      dst += 2;
+    }
+    src++;
+  }
+  return dst - out;
+}
+
+/**
+ * @brief URL(percent)-decode a counted string
+ *
+ * @param in ptr to an input encoded counted string
+ * @param inLen input character count
+ * @param out ptr to an output buffer where the decoded string will be stored.
+ * @param outLen output character count (output max size, should be inLen + 1)
+ * @return the number of character actually added to the output buffer.
+ */
+size_t
+urlDecode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  const char *src = in;
+  char *dst       = out;
+  while ((size_t)(src - in) < inLen && (size_t)(dst - out) < outLen) {
+    if (*src == '%') {
+      if (src[1] && src[2]) {
+        int u  = hex2uchar(*(src + 1)) << 4 | hex2uchar(*(src + 2));
+        *dst++ = (char)u;
+        src += 2;
+      }
+    } else if (*src == '+') {
+      *dst++ = ' ';
+    } else {
+      *dst++ = *src;
+    }
+    src++;
+  }
+  return dst - out;
+}
+
+/* ******* Functions using OpenSSL library ******* */
+
+void
+cryptoMagicInit()
+{
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  OpenSSL_add_all_digests(); /* needed for EVP_get_digestbyname() */
+#endif
+}
+
+void
+cryptoMagicCleanup()
+{
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  EVP_cleanup();
+#endif
+}
+
+/**
+ * @brief a helper function to get a human-readable error message in a buffer.
+ *
+ * @param buffer pointer to a char buffer
+ * @param bufferLen - max buffer length (length should be >= 256)
+ * @return buffer, filled with the error message (null-terminated)
+ */
+static char *
+cryptoErrStr(char *buffer, size_t bufferLen)
+{
+  /* man ERR_error_string expects 256-byte buffer */
+  if (nullptr == buffer || 256 > bufferLen) {
+    return nullptr;
+  }
+  unsigned long err = ERR_get_error();
+  if (0 == err) {
+    buffer[0] = 0;
+    return buffer;
+  }
+  ERR_error_string_n(err, buffer, bufferLen);
+  return buffer;
+}
+
+/**
+ * @brief Calculate message digest
+ *
+ * @param digestType digest name
+ * @param data ptr to input message for calculating the digest
+ * @param dataLen message length
+ * @param key ptr to a counted string containing the key (secret)
+ * @param keyLen key length
+ * @param out ptr to where to store the digest
+ * @param outLen length of the out buffer (must be at least MAX_MSGDIGEST_BUFFER_SIZE)
+ * @return the number of character actually written to the buffer.
+ */
+size_t
+cryptoMessageDigestGet(const char *digestType, const char *data, size_t dataLen, const char *key, size_t keyLen, char *out,
+                       size_t outLen)
+{
+  EVP_MD_CTX *ctx  = nullptr;
+  const EVP_MD *md = nullptr;
+  EVP_PKEY *pkey   = nullptr;
+  size_t len       = outLen;
+  char buffer[256];
+
+  size_t result = 0;
+  if (!(ctx = EVP_MD_CTX_create())) {
+    AccessControlError("failed to create EVP message digest context: %s", cryptoErrStr(buffer, sizeof(buffer)));
+  } else {
+    if (!(pkey = EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, nullptr, (const unsigned char *)key, keyLen))) {
+      AccessControlError("failed to create EVP private key. %s", cryptoErrStr(buffer, sizeof(buffer)));
+      EVP_MD_CTX_destroy(ctx);
+    } else {
+      do {
+        if (!(md = EVP_get_digestbyname(digestType))) {
+          AccessControlError("failed to get digest by name %s. %s", digestType, cryptoErrStr(buffer, sizeof(buffer)));
+          break;
+        }
+
+        if (1 != EVP_DigestSignInit(ctx, nullptr, md, nullptr, pkey)) {
+          AccessControlError("failed to set up signing context. %s", cryptoErrStr(buffer, sizeof(buffer)));
+          break;
+        }
+
+        if (1 != EVP_DigestSignUpdate(ctx, data, dataLen)) {
+          AccessControlError("failed to update the signing hash. %s", cryptoErrStr(buffer, sizeof(buffer)));
+          break;
+        }
+
+        if (1 != EVP_DigestSignFinal(ctx, (unsigned char *)out, &len)) {
+          AccessControlError("failed to finalize the signing hash. %s", cryptoErrStr(buffer, sizeof(buffer)));
+        }
+
+        /* success */
+        result = len;
+      } while (0);
+
+      EVP_PKEY_free(pkey);
+      EVP_MD_CTX_destroy(ctx);
+    }
+  }
+
+  return result;
+}
+
+/**
+ * @brief Check if 2 message digests are the same using a constant-time openssl function to avoid timing attacks.
+ *
+ * @param md1 first message digest to compare
+ * @param md1Len md1 length
+ * @param md2 first message digest to compare
+ * @param md2Len md2 length
+ * @return true - the same, false - different
+ */
+bool
+cryptoMessageDigestEqual(const char *md1, size_t md1Len, const char *md2, size_t md2Len)
+{
+  if (md1Len != md2Len) {
+    return false;
+  }
+  if (0 == CRYPTO_memcmp((const void *)md1, (const void *)md2, md1Len)) {
+    /* Verification success */
+    return true;
+  }
+  /* Verify failures */
+  return false;
+}
+
+/**
+ * @brief Calculates the size of the output buffer needed to base64 encode a message
+ *
+ * @param decodedSize the size of the message to encode.
+ * @return output buffer size
+ */
+size_t
+cryptoBase64EncodedSize(size_t decodedSize)
+{
+  return (((4 * decodedSize) / 3) + 3) & ~3;
+}
+
+/**
+ * @brief Calculates the size of the output buffer needed to base64 decode a message
+ *
+ * @param messageSize the size of the message to decode.
+ * @return output buffer size
+ */
+size_t
+cryptoBase64DecodeSize(const char *encoded, size_t encodedLen)
+{
+  if (nullptr == encoded || 0 == encodedLen) {
+    return 0;
+  }
+
+  size_t padding  = 0;
+  const char *end = encoded + encodedLen;
+
+  if ('=' == *(--end)) {
+    padding++;
+  }
+
+  if ('=' == *(--end)) {
+    padding++;
+  }
+
+  return (3 * encodedLen) / 4 - padding;
+}
+
+/**
+ * @brief Base64 encode
+ *
+ * @param in input buffer to encode
+ * @param inLen input buffer length
+ * @param output buffer
+ * @param output buffer length
+ * @return number of character actually written to the output buffer.
+ */
+size_t
+cryptoBase64Encode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  if ((nullptr == in) || (0 == inLen) || (nullptr == out) || 0 == outLen) {
+    return 0;
+  }
+
+  BIO *head, *b64, *bmem;
+  BUF_MEM *bptr;
+  size_t len = 0;
+
+  head = b64 = BIO_new(BIO_f_base64());
+  if (nullptr != b64) {
+    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+    bmem = BIO_new(BIO_s_mem());
+    if (nullptr != bmem) {
+      head = BIO_push(b64, bmem);
+
+      BIO_write(head, in, inLen);
+      (void)BIO_flush(head);
+
+      BIO_get_mem_ptr(head, &bptr);
+      len = bptr->length < outLen ? bptr->length : outLen;
+      strncpy(out, bptr->data, len);
+    }
+    BIO_free_all(head);
+  }
+  return len;
+}
+
+/**
+ * @brief Base64 decode
+ *
+ * @param in input buffer to encode
+ * @param inLen input buffer length
+ * @param output buffer
+ * @param output buffer length
+ * @return number of character actually written to the output buffer.
+ */
+size_t
+cryptoBase64Decode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  if ((nullptr == in) || (0 == inLen) || (nullptr == out) || 0 == outLen) {
+    return 0;
+  }
+
+  BIO *head, *bmem, *b64;
+  size_t len = 0;
+
+  head = b64 = BIO_new(BIO_f_base64());
+  if (nullptr != b64) {
+    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+    bmem = BIO_new_mem_buf((void *)in, inLen);
+    if (nullptr != bmem) {
+      head = BIO_push(b64, bmem);
+      len  = BIO_read(head, out, outLen);
+    }
+    BIO_free_all(head);
+  }
+
+  return len;
+}
+
+/**
+ * For more information see wikipedia|http://en.wikipedia.org/wiki/Base64 and
+ * RFC 7515 |https://tools.ietf.org/html/rfc7515#appendix-C
+ */
+size_t
+cryptoModifiedBase64Encode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  size_t len = cryptoBase64Encode(in, inLen, out, outLen);
+
+  char *cur            = out;
+  const char *end      = out + len;
+  const char *padStart = out + len;
+  bool foundPadStart   = false;
+  while (cur < end) {
+    if ('+' == *cur) {
+      *cur = '-';
+    } else if ('/' == *cur) {
+      *cur = '_';
+    } else if (*cur == '=' && !foundPadStart) {
+      padStart      = cur;
+      foundPadStart = true;
+    }
+    cur++;
+  }
+  return padStart - out;
+}
+
+/**
+ * For more information see wikipedia: http://en.wikipedia.org/wiki/Base64 and
+ * RFC 7515 |https://tools.ietf.org/html/rfc7515#appendix-C
+ */
+size_t
+cryptoModifiedBase64Decode(const char *in, size_t inLen, char *out, size_t outLen)
+{
+  size_t bufferLen = inLen;
+  switch (inLen % 4) {
+  case 0: /* no padding */
+    break;
+  case 2: /* need space for '==' */
+    bufferLen += 2;
+    break;
+  case 3: /* need space for '=' */
+    bufferLen += 1;
+    break;
+  case 4:     /* malformed base64 */
+    return 0; /* nothing will be written to the output buffer */
+    break;
+  }
+
+  /* Since 'in' would like to be unmodifiable to add the padding will need a copy */
+  const char *cur = in;
+  const char *end = in + inLen;
+  char buffer[bufferLen];
+  char *dst = buffer;
+  while (cur < end) {
+    if ('-' == *cur) {
+      *dst++ = '+';
+    } else if ('_' == *cur) {
+      *dst++ = '/';
+    } else {
+      *dst++ = *cur;
+    }
+    cur++;
+  }
+
+  /* Add the padding '=' to the end of the buffer */
+  while (dst < buffer + bufferLen) {
+    *dst++ = '=';
+  }
+
+  return cryptoBase64Decode(buffer, bufferLen, out, outLen);
+}
diff --git a/plugins/experimental/access_control/utils.h b/plugins/experimental/access_control/utils.h
new file mode 100644
index 0000000..02f027e
--- /dev/null
+++ b/plugins/experimental/access_control/utils.h
@@ -0,0 +1,57 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+/**
+ * @file utils.h
+ * @brief Various utility functions (Headers).
+ * @see utils.cc
+ */
+
+#pragma once
+
+#include <string_view>   /* std:string_view */
+#include <openssl/evp.h> /* EVP_* constants, structures and functions. */
+#include <string.h>      /* strlen, strncmp, strncpy, memset, size_t */
+
+#define MAX_MSGDIGEST_BUFFER_SIZE EVP_MAX_MD_SIZE
+
+bool parseStrLong(const char *s, size_t len, long &val);
+
+/* ******* Encoding/Decoding functions ******* */
+
+size_t hexEncode(const char *in, size_t inLen, char *out, size_t outLen);
+size_t hexDecode(const char *in, size_t inLen, char *out, size_t outLen);
+
+size_t urlEncode(const char *in, size_t inLen, char *out, size_t outLen);
+size_t urlDecode(const char *in, size_t inLen, char *out, size_t outLen);
+
+/* ******* Functions using OpenSSL library ******* */
+
+void cryptoMagicInit();
+void cryptoMagicCleanup();
+
+size_t cryptoMessageDigestGet(const char *digestType, const char *data, size_t dataLen, const char *key, size_t keyLen, char *out,
+                              size_t outLen);
+bool cryptoMessageDigestEqual(const char *md1, size_t md1Len, const char *md2, size_t md2Len);
+
+size_t cryptoBase64EncodedSize(size_t decodedSize);
+size_t cryptoBase64DecodeSize(const char *encoded, size_t encodedLen);
+size_t cryptoBase64Encode(const char *in, size_t inLen, char *out, size_t outLen);
+size_t cryptoBase64Decode(const char *in, size_t inLen, char *out, size_t outLen);
+size_t cryptoModifiedBase64Encode(const char *in, size_t inLen, char *out, size_t outLen);
+size_t cryptoModifiedBase64Decode(const char *in, size_t inLen, char *out, size_t outLen);