diff --new-file -ur keystone/auth/plugins/jwt_token.py keystone/auth/plugins/jwt_token.py
--- keystone/auth/plugins/jwt_token.py	1970-01-01 01:00:00.000000000 +0100
+++ keystone/auth/plugins/jwt_token.py	2021-03-28 22:05:16.752052107 +0200
@@ -0,0 +1,85 @@
+#
+# JWT token support
+# author: pregusia
+#
+
+from oslo_log import log
+import six
+import jwt
+import flask
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+
+from keystone.auth.plugins import base
+from keystone.auth.plugins import mapped
+from keystone.common import provider_api
+from keystone import exception
+from keystone.i18n import _
+
+import keystone.conf
+
+LOG = log.getLogger(__name__)
+
+CONF = keystone.conf.CONF
+PROVIDERS = provider_api.ProviderAPIs
+MODULE_NAME='jwt_token'
+
+
+class JWTToken(base.AuthMethodHandler):
+
+    def __init__(self):
+        self._public_key = None
+
+    def authenticate(self, auth_payload):
+        """Performs authentication process using JWT token supplied in 'Authorization' header
+          Firstly check if header is valid - that means, if is in format 'Authorization: Bearer XXXX'
+          Then parses JWT token in header and verify it agains - expiration time, issuer and check signature (using RSA)
+          Issuer and RSA key path is supplied via configuration
+          If everything is ok, payload from token is extracted and then passed to maping engine.
+          JWT token algorithm must be RS256/RS512
+        """
+
+        authorization_header_value = flask.request.headers.get('Authorization')
+        if not authorization_header_value:
+            raise exception.ValidationError(attribute='Authorization', target='request.headers')
+
+        authorization_header_arr = authorization_header_value.split(' ')
+        if len(authorization_header_arr) != 2:
+            raise ValueError('Invalid authorization header value')
+
+        if authorization_header_arr[0] != 'Bearer':
+            raise ValueError('Invalid authorization header value')
+
+        payload = jwt.decode(authorization_header_arr[1],
+                key=self._load_public_key(),
+                algorithms=["RS256", "RS512"],
+                issuer=CONF.jwt_token.issuer,
+                leeway=CONF.jwt_token.leeway,
+                options={
+                    'require_exp': True,
+                    'verify_aud': False,
+                    'verify_exp': True,
+                    'verify_iss': True,
+                })
+
+
+        response_data = mapped.handle_unscoped_token(auth_payload,
+                                              PROVIDERS.resource_api,
+                                              PROVIDERS.federation_api,
+                                              PROVIDERS.identity_api,
+                                              PROVIDERS.assignment_api,
+                                              PROVIDERS.role_api,
+                                              payload)
+
+        return base.AuthHandlerResponse(status=True, response_body=None,
+                                        response_data=response_data)
+
+
+    def _load_public_key(self):
+        if not self._public_key:
+            with open(CONF.jwt_token.public_key_path, "rb") as key_file:
+                self._public_key = serialization.load_pem_public_key(key_file.read(), backend=default_backend())
+
+        return self._public_key
+
diff --new-file -ur keystone/auth/plugins/mapped.py keystone/auth/plugins/mapped.py
--- keystone/auth/plugins/mapped.py	2021-03-28 23:38:57.091106287 +0200
+++ keystone/auth/plugins/mapped.py	2021-03-28 22:42:41.078936834 +0200
@@ -25,6 +25,7 @@
 from keystone.federation import utils
 from keystone.i18n import _
 from keystone import notifications
+from keystone import assignment
 
 LOG = log.getLogger(__name__)
 
@@ -50,6 +51,7 @@
         ``OS-FEDERATION:protocol``
 
         """
+
         if 'id' in auth_payload:
             token_ref = self._get_token_ref(auth_payload)
             response_data = handle_scoped_token(token_ref,
@@ -109,7 +111,7 @@
 
 
 def handle_unscoped_token(auth_payload, resource_api, federation_api,
-                          identity_api, assignment_api, role_api):
+                          identity_api, assignment_api, role_api, custom_assertion=None):
 
     def validate_shadow_mapping(shadow_projects, existing_roles, idp_domain_id,
                                 idp_id):
@@ -147,36 +149,56 @@
                                      existing_roles, user, assignment_api,
                                      resource_api):
         for shadow_project in shadow_projects:
-            try:
-                # Check and see if the project already exists and if it
-                # does not, try to create it.
-                project = resource_api.get_project_by_name(
-                    shadow_project['name'], idp_domain_id
-                )
-            except exception.ProjectNotFound:
-                LOG.info(
-                    'Project %(project_name)s does not exist. It will be '
-                    'automatically provisioning for user %(user_id)s.',
-                    {'project_name': shadow_project['name'],
-                     'user_id': user['id']}
-                )
-                project_ref = {
-                    'id': uuid.uuid4().hex,
-                    'name': shadow_project['name'],
-                    'domain_id': idp_domain_id
-                }
-                project = resource_api.create_project(
-                    project_ref['id'],
-                    project_ref
-                )
+            project = None
+            if 'id' in shadow_project:
+                try:
+                    project = resource_api.get_project(shadow_project['id'])
+                except exception.ProjectNotFound as e:
+                    LOG.error(
+                        'Project %(project_id)s does not exist. '
+                        'User %(user_id) login aborted.',
+                        {'project_id': shadow_project['id'],
+                         'user_id': user['id']}
+                    )
+                    raise e
+                        
+            elif 'name' in shadow_project:
+                try:
+                    # Check and see if the project already exists and if it
+                    # does not, try to create it.
+                    project = resource_api.get_project_by_name(
+                        shadow_project['name'], idp_domain_id
+                    )
+                except exception.ProjectNotFound:
+                    LOG.info(
+                        'Project %(project_name)s does not exist. It will be '
+                        'automatically provisioning for user %(user_id)s.',
+                        {'project_name': shadow_project['name'],
+                         'user_id': user['id']}
+                    )
+                    project_ref = {
+                        'id': uuid.uuid4().hex,
+                        'name': shadow_project['name'],
+                        'domain_id': idp_domain_id
+                    }
+                    project = resource_api.create_project(
+                        project_ref['id'],
+                        project_ref
+                    )
 
-            shadow_roles = shadow_project['roles']
-            for shadow_role in shadow_roles:
-                assignment_api.create_grant(
-                    existing_roles[shadow_role['name']]['id'],
-                    user_id=user['id'],
-                    project_id=project['id']
-                )
+            if project:
+                shadow_roles = shadow_project['roles']
+                for shadow_role in shadow_roles:
+                    assignment_api.create_grant(
+                        existing_roles[shadow_role['name']]['id'],
+                        user_id=user['id'],
+                        project_id=project['id']
+                    )
+                    LOG.info('Add assignment for project %(project_id) user %(user_id) role %(role_name).', {
+                        'project_id': str(project['id']),
+                        'user_id': str(user['id']),
+                        'role_name': str(shadow_role['name'])
+                    })
 
     def is_ephemeral_user(mapped_properties):
         return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL
@@ -199,7 +221,11 @@
 
         return resp
 
-    assertion = extract_assertion_data()
+    if custom_assertion != None:
+        assertion = custom_assertion
+    else:
+        assertion = extract_assertion_data()
+
     try:
         identity_provider = auth_payload['identity_provider']
     except KeyError:
@@ -242,6 +268,8 @@
                                                       display_name,
                                                       email)
 
+            PROVIDERS.assignment_api.delete_user_assignments(user['id'])
+
             if 'projects' in mapped_properties:
                 idp_domain_id = federation_api.get_idp(
                     identity_provider

diff --new-file -ur keystone/conf/__init__.py keystone/conf/__init__.py
--- keystone/conf/__init__.py	2021-03-28 23:38:55.515143428 +0200
+++ keystone/conf/__init__.py	2021-03-28 21:52:38.233809218 +0200
@@ -50,6 +50,7 @@
 from keystone.conf import trust
 from keystone.conf import unified_limit
 from keystone.conf import wsgi
+from keystone.conf import jwt_token
 
 CONF = cfg.CONF
 
@@ -85,7 +86,8 @@
     tokenless_auth,
     trust,
     unified_limit,
-    wsgi
+    wsgi,
+    jwt_token
 ]
 
 
diff --new-file -ur keystone/conf/jwt_token.py keystone/conf/jwt_token.py
--- keystone/conf/jwt_token.py	1970-01-01 01:00:00.000000000 +0100
+++ keystone/conf/jwt_token.py	2021-03-28 21:52:26.170091710 +0200
@@ -0,0 +1,50 @@
+#
+# Configuration for JWT token authorization mechanism
+#
+# author: pregusia
+#
+
+from oslo_config import cfg
+
+from keystone.conf import utils
+
+
+public_key_path = cfg.StrOpt(
+    'public_key_path',
+    help=utils.fmt("""
+Path for public key for RS* JWT algorithms.
+Destination file must be im PEM format
+"""))
+
+issuer = cfg.StrOpt(
+    'issuer',
+    help=utils.fmt("""
+Valid name for JWT issuer claim
+"""))
+
+
+leeway = cfg.IntOpt(
+    'leeway',
+    min=0,
+    default=30,
+    help=utils.fmt("""
+Number of seconds for expiration check (JWT leeway)
+"""))
+
+GROUP_NAME = __name__.split('.')[-1]
+ALL_OPTS = [
+    public_key_path,
+    issuer,
+    leeway,
+]
+
+
+def register_opts(conf):
+    conf.register_opts(ALL_OPTS, group=GROUP_NAME)
+
+
+def list_opts():
+    return {GROUP_NAME: ALL_OPTS}
+
+
+
diff --new-file -ur keystone/federation/utils.py keystone/federation/utils.py
--- keystone/federation/utils.py	2021-03-28 23:38:55.431145408 +0200
+++ keystone/federation/utils.py	2021-03-28 21:51:37.667227522 +0200
@@ -90,14 +90,18 @@
                                     "type": "array",
                                     "items": {
                                         "type": "object",
-                                        "required": ["name", "roles"],
+                                        "required": ["roles"],
                                         "additionalProperties": False,
                                         "properties": {
                                             "name": {"type": "string"},
+                                            "id": {"type": "string"},
                                             "roles": ROLE_PROPERTIES
                                         }
                                     }
                                 },
+                                "projects_spec": {
+                                    "type": "string"
+                                },
                                 "group": {
                                     "type": "object",
                                     "oneOf": [
@@ -599,6 +603,24 @@
                     'id': CONF.federation.federated_domain_name
                 }
 
+        def parse_projects_spec(spec, projects):
+            for e in spec.split(','):
+                e = e.strip()
+                if e != '':
+                    arr = e.split(':')
+                    if len(arr) > 1:
+                        project_id = arr[0]
+                        roles = [ ]
+                        for rr in arr[1:]:
+                            roles.append({
+                                'name': rr
+                            })
+
+                        projects.append({
+                            'id': project_id,
+                            'roles': roles
+                        })
+
         # initialize the group_ids as a set to eliminate duplicates
         user = {}
         group_ids = set()
@@ -664,6 +686,8 @@
                     group_ids.update([identity_value['group_ids']])
             if 'projects' in identity_value:
                 projects = identity_value['projects']
+            if 'projects_spec' in identity_value:
+                parse_projects_spec(identity_value['projects_spec'], projects)
 
         normalize_user(user)
 

