Commit c9453309 authored by Pawel Kowalik's avatar Pawel Kowalik
Browse files

Merge branch 'feature/customizedregistration' into 'master'

Feature/customizedregistration

See merge request !16
parents ec3e01a1 1deafe9a
Pipeline #374895374 passed with stage
in 46 seconds
## CHANGELOG:
| version | date | changes |
| ------- | -----------| ------ |
| 0.0.24 | 2019-11-26 | NEW FEATURE: Support "none" as "alg" of id_token<br>NEW FEATURE: allow override of authority lookup<br>NEW FEATURE: userinfo_signing_required now can be configured in client code<br>NEW FEATURE: RS384 and RS512 added as supported signature options<br>REFACTORING: cleaned up the code of dynamic client registration<br>BUGFIX: using 'none' algorithms only if supported |
| 0.0.23 | 2019-10-04 | NEW FEATURE: plain JSON user info support added<br>NEW FEATURE: use of scope parameter instead of claims if not supported by IdP<br>NEW: example code included |
| 0.0.22 | 2019-07-29 | BUGFIX: id4me_rp_client.helper not exported to the release library |
| 0.0.21 | 2019-07-29 | BUGFIX: YXDOMAIN case not properly handled<br>BUGFIX: avoid trying to resolve empty domain names<br>BUGFIX: added better handling when state is empty<br>LOGGING: added logging of all exceptions (debug level) |
......
......@@ -130,6 +130,7 @@ Example
## CHANGELOG:
| version | date | changes |
| ------- | -----------| ------ |
| 0.0.24 | 2019-11-26 | NEW FEATURE: Support "none" as "alg" of id_token<br>NEW FEATURE: allow override of authority lookup<br>NEW FEATURE: userinfo_signing_required now can be configured in client code<br>NEW FEATURE: RS384 and RS512 added as supported signature options<br>REFACTORING: cleaned up the code of dynamic client registration<br>BUGFIX: using 'none' algorithms only if supported |
| 0.0.23 | 2019-10-04 | NEW FEATURE: plain JSON user info support added<br>NEW FEATURE: use of scope parameter instead of claims if not supported by IdP<br>NEW: example code included |
| 0.0.22 | 2019-07-29 | BUGFIX: id4me_rp_client.helper not exported to the release library |
| 0.0.21 | 2019-07-29 | BUGFIX: YXDOMAIN case not properly handled<br>BUGFIX: avoid trying to resolve empty domain names<br>BUGFIX: added better handling when state is empty<br>LOGGING: added logging of all exceptions (debug level) |
......
......@@ -129,7 +129,10 @@ class ID4meClient(object):
network_context=None,
requireencryption=None,
register_client_dynamically=True,
use_scope_if_claims_not_supported=True):
use_scope_if_claims_not_supported=True,
require_id_token_signing=True,
require_user_info_signing=None,
authority_lookup_override=None):
"""
Constructor of ID4me Client, providing the data for client registration if needed
:type register_client_dynamically: object
......@@ -163,6 +166,17 @@ class ID4meClient(object):
:type register_client_dynamically: bool
:param use_scope_if_claims_not_supported: specify if scopes shall be used to get user claims if IdP does not specify claims_parameter_supported
:type use_scope_if_claims_not_supported: bool
:param require_id_token_signing: specify, if always use id_token_signed_response_alg parameter when registering
client. true - parameter is always set to supported alg. False - parameter is set to "none"
None - parameter is not used (IdP defaults are taken)
:type require_id_token_signing: bool
:param require_user_info_signing: specify, if always use user_info_signed_response_alg parameter when registering
client. true - parameter is always set to supported alg. False - parameter is set to "none"
None - parameter is not used (IdP defaults are taken)
:type require_user_info_signing: bool
:param authority_lookup_override: callback which allows to override the DNS authority lookup ``callback(identifier->str)->str``
If None returned, then regular DNS lookup will be perfomed
:type authority_lookup_override: function
:rtype: ID4meClient
"""
self.validateUrl = validate_url
......@@ -195,6 +209,9 @@ class ID4meClient(object):
self.save_client_registration = save_client_registration
self.register_client_dynamically = register_client_dynamically
self.use_scope_if_claims_not_supported = use_scope_if_claims_not_supported
self.require_id_token_signing = require_id_token_signing
self.require_user_info_signing = require_user_info_signing
self.authority_lookup_override = authority_lookup_override
@staticmethod
def _get_domain_name_to_lookup(id4me):
......@@ -226,6 +243,12 @@ class ID4meClient(object):
:return: identity authority identifier (FQDN or URL)
:rtype: str
"""
if self.authority_lookup_override is not None:
overridden_lookup = self.authority_lookup_override(id4me)
if overridden_lookup is not None:
return overridden_lookup
parts = self._get_domain_name_to_lookup(id4me).split('.')
first_exception = None
for idx in range(0, len(parts)):
......@@ -360,21 +383,48 @@ class ID4meClient(object):
identity_authority_config = self._get_openid_configuration(identity_authority)
logger.info('registering with new identity authority ({})'.format(identity_authority))
if 'id_token_signing_alg_values_supported' not in identity_authority_config \
or 'RS256' not in identity_authority_config['id_token_signing_alg_values_supported']:
raise ID4meRelyingPartyRegistrationException(
'Required signature algorithm for id_token RS256 not supported by Authority')
if 'userinfo_signing_alg_values_supported' not in identity_authority_config \
or 'RS256' not in identity_authority_config['userinfo_signing_alg_values_supported']:
raise ID4meRelyingPartyRegistrationException(
'Required signature algorithm for userinfo RS256 not supported by Authority')
request = {
'redirect_uris': ['{}'.format(self.validateUrl)],
'id_token_signed_response_alg': 'RS256',
'userinfo_signed_response_alg': 'RS256',
}
if self.require_id_token_signing is not None:
if self.require_id_token_signing:
if 'id_token_signing_alg_values_supported' in identity_authority_config \
and {'RS256', 'RS384', 'RS512'}\
.intersection(set(identity_authority_config['id_token_signing_alg_values_supported'])):
request['id_token_signed_response_alg'] = \
max({'RS256', 'RS384', 'RS512'}\
.intersection(set(identity_authority_config['id_token_signing_alg_values_supported'])))
else:
raise ID4meRelyingPartyRegistrationException(
'Required signature algorithm for id_token RS256, RS384 or RS512 not supported by Authority')
else:
if 'id_token_signing_alg_values_supported' in identity_authority_config \
and 'none' in identity_authority_config['id_token_signing_alg_values_supported']:
request['id_token_signed_response_alg'] = 'none'
else:
raise ID4meRelyingPartyRegistrationException(
'Required signature algorithm for id_token none not supported by Authority')
if self.require_user_info_signing is not None:
if self.require_user_info_signing:
if 'userinfo_signing_alg_values_supported' in identity_authority_config \
and {'RS256', 'RS384', 'RS512'}\
.intersection(set(identity_authority_config['userinfo_signing_alg_values_supported'])):
request['userinfo_signed_response_alg'] = \
max({'RS256', 'RS384', 'RS512'}\
.intersection(set(identity_authority_config['userinfo_signing_alg_values_supported'])))
else:
raise ID4meRelyingPartyRegistrationException(
'Required signature algorithm for user_info RS256, RS384 or RS512 not supported by Authority')
else:
if 'userinfo_signing_alg_values_supported' in identity_authority_config \
and 'none' in identity_authority_config['userinfo_signing_alg_values_supported']:
request['userinfo_signed_response_alg'] = 'none'
else:
raise ID4meRelyingPartyRegistrationException(
'Required signature algorithm for user_info none not supported by Authority')
if self.jwksUrl is not None:
request['jwks_uri'] = self.jwksUrl
elif self.private_jwks is not None:
......@@ -407,6 +457,7 @@ class ID4meClient(object):
if self.app_type is not None:
request['application_type'] = str(self.app_type)
try:
registration = json.loads(
post_json(
......@@ -764,7 +815,7 @@ class ID4meClient(object):
raise ID4meTokenException('Invalid userinfo signature algorithm. Expected: {0}, Received: {1}'.format(
registration['userinfo_signed_response_alg'], head['alg']))
if 'kid' not in head and 'alg' in head and head['alg'] in _jws_alg_map:
if 'kid' not in head and 'alg' in head and head['alg'] != 'none' and head['alg'] in _jws_alg_map:
success = False
for k in keys:
if (k.get_op_key('verify') is not None) and k.key_type == _jws_alg_map[head['alg']]:
......@@ -779,7 +830,13 @@ class ID4meClient(object):
logger.debug('None of keys is able to verify signature for {}'.format(context.id))
raise ID4meTokenException("None of keys is able to verify signature")
else:
tokenproc.deserialize(token, keys)
if head['alg'] == 'none':
tokenproc.deserialize(token)
tokenproc.header = tokenproc.token.jose_header
tokenproc.token.objects['valid'] = True
tokenproc.claims = tokenproc.token.payload.decode('utf-8')
else:
tokenproc.deserialize(token, keys)
except JWException as ex:
logger.debug('Cannot decode token for {} ({})'.format(context.id, ex))
raise ID4meTokenException("Cannot decode token: {}".format(ex))
......
......@@ -158,7 +158,8 @@ class TestID4meClient(TestCase):
"ES512",
"HS256",
"HS384",
"HS512"
"HS512",
"none"
],
"userinfo_encryption_alg_values_supported": [
"RSA1_5",
......@@ -605,8 +606,8 @@ class TestID4meClient(TestCase):
client = TestID4meClientTestCommon._get_test_client(get_client_registration=None)
client.resolver = mock.create_autospec(Resolver)
client.resolver.query.return_value = ['v=OID1;iss=test1.auth;clp=api-identity.ionos.com']
auth = client._get_identity_authority('testionos.domainid.community')
client.resolver.query.assert_called_with('_openid.testionos.domainid.community.', 'TXT')
auth = client._get_identity_authority('this-is-sparta.test.adwords.google.com')
client.resolver.query.assert_called_with('_openid.this-is-sparta.test.adwords.google.com.', 'TXT')
assert auth == 'test1.auth', 'Wrong Authority discovered'
client.resolver = mock.create_autospec(Resolver)
......@@ -640,6 +641,29 @@ class TestID4meClient(TestCase):
except ID4meDNSResolverException:
pass
def test__get_identity_authority_with_override(self):
client = TestID4meClientTestCommon._get_test_client(get_client_registration=None)
client.resolver = mock.create_autospec(Resolver)
client.resolver.query.return_value = ['v=OID1;iss=test1.auth;clp=api-identity.ionos.com']
client.authority_lookup_override = \
lambda identifier: 'overruled.auth' if identifier.endswith('.google.com') else None
#with dot identifier
auth = client._get_identity_authority('this-is-sparta.test.adwords.google.com')
client.resolver.query.assert_not_called()
assert auth == 'overruled.auth', 'Wrong Authority discovered'
#with dot identifier assure not matching IDs still resolve over DNS
client.resolver = mock.create_autospec(Resolver)
client.resolver.query.side_effect = [NXDOMAIN, ['v=OID1;iss=test2.auth;clp=api-identity.ionos.com']]
auth = client._get_identity_authority('testionos.domainid.community')
client.resolver.query.assert_has_calls([call('_openid.testionos.domainid.community.', 'TXT'),
call('_openid.domainid.community.', 'TXT')])
assert auth == 'test2.auth', 'Wrong Authority discovered'
def test__get_identity_authority_once(self):
client = TestID4meClientTestCommon._get_test_client(get_client_registration=None)
......@@ -746,8 +770,102 @@ class TestID4meClient(TestCase):
'POST',
'https://id.test.denic.de/clients',
'{"redirect_uris": ["http://localhost:8090/validate"], '
'"id_token_signed_response_alg": "RS256", '
'"userinfo_signed_response_alg": "RS256", '
'"id_token_signed_response_alg": "RS512", '
'"jwks": ' + json.dumps(json.loads(JWKSet.from_json(privkey).export(private_keys=False))) + ', '
'"client_name": "Test", '
'"logo_uri": "http://localhost:8090/logo.png", '
'"policy_uri": "http://localhost:8090/about", '
'"tos_uri": "http://localhost:8090/documents", '
'"application_type": "native"}',
None, None, 'application/json', None, None, None)
def test__register_identity_authority_no_signing(self):
auth_store = dict()
privkey = ID4meClient.generate_new_private_keys_set();
client = TestID4meClientTestCommon._get_test_client(
get_client_registration=lambda authname: auth_store[authname],
save_client_registration=lambda authname, authval: auth_store.__setitem__(authname, authval),
generate_private_keys=False, private_jwks_json=privkey,
require_id_token_signing=False,
require_user_info_signing=False)
client._get_identity_authority = MagicMock(return_value='id.test.denic.de')
client._get_openid_configuration = MagicMock(return_value=self._denic_openid_config)
with mock.patch('id4me_rp_client.network._http_request_raw') as http_request_mock:
http_request_mock.return_value = json.dumps(self._denic_registration).encode(), 200, 'application/json'
registration = client._register_identity_authority('id.test.denic.de')
assert registration == self._denic_registration
http_request_mock.assert_called_with(
client.networkContext,
'POST',
'https://id.test.denic.de/clients',
'{"redirect_uris": ["http://localhost:8090/validate"], '
'"id_token_signed_response_alg": "none", '
'"userinfo_signed_response_alg": "none", '
'"jwks": ' + json.dumps(json.loads(JWKSet.from_json(privkey).export(private_keys=False))) + ', '
'"client_name": "Test", '
'"logo_uri": "http://localhost:8090/logo.png", '
'"policy_uri": "http://localhost:8090/about", '
'"tos_uri": "http://localhost:8090/documents", '
'"application_type": "native"}',
None, None, 'application/json', None, None, None)
def test__register_identity_authority_enforced_signing(self):
auth_store = dict()
privkey = ID4meClient.generate_new_private_keys_set();
client = TestID4meClientTestCommon._get_test_client(
get_client_registration=lambda authname: auth_store[authname],
save_client_registration=lambda authname, authval: auth_store.__setitem__(authname, authval),
generate_private_keys=False, private_jwks_json=privkey,
require_id_token_signing=True,
require_user_info_signing=True)
client._get_identity_authority = MagicMock(return_value='id.test.denic.de')
client._get_openid_configuration = MagicMock(return_value=self._denic_openid_config)
with mock.patch('id4me_rp_client.network._http_request_raw') as http_request_mock:
http_request_mock.return_value = json.dumps(self._denic_registration).encode(), 200, 'application/json'
registration = client._register_identity_authority('id.test.denic.de')
assert registration == self._denic_registration
http_request_mock.assert_called_with(
client.networkContext,
'POST',
'https://id.test.denic.de/clients',
'{"redirect_uris": ["http://localhost:8090/validate"], '
'"id_token_signed_response_alg": "RS512", '
'"userinfo_signed_response_alg": "RS512", '
'"jwks": ' + json.dumps(json.loads(JWKSet.from_json(privkey).export(private_keys=False))) + ', '
'"client_name": "Test", '
'"logo_uri": "http://localhost:8090/logo.png", '
'"policy_uri": "http://localhost:8090/about", '
'"tos_uri": "http://localhost:8090/documents", '
'"application_type": "native"}',
None, None, 'application/json', None, None, None)
def test__register_identity_authority_default_signing(self):
auth_store = dict()
privkey = ID4meClient.generate_new_private_keys_set();
client = TestID4meClientTestCommon._get_test_client(
get_client_registration=lambda authname: auth_store[authname],
save_client_registration=lambda authname, authval: auth_store.__setitem__(authname, authval),
generate_private_keys=False, private_jwks_json=privkey,
require_id_token_signing=None,
require_user_info_signing=None)
client._get_identity_authority = MagicMock(return_value='id.test.denic.de')
client._get_openid_configuration = MagicMock(return_value=self._denic_openid_config)
with mock.patch('id4me_rp_client.network._http_request_raw') as http_request_mock:
http_request_mock.return_value = json.dumps(self._denic_registration).encode(), 200, 'application/json'
registration = client._register_identity_authority('id.test.denic.de')
assert registration == self._denic_registration
http_request_mock.assert_called_with(
client.networkContext,
'POST',
'https://id.test.denic.de/clients',
'{"redirect_uris": ["http://localhost:8090/validate"], '
'"jwks": ' + json.dumps(json.loads(JWKSet.from_json(privkey).export(private_keys=False))) + ', '
'"client_name": "Test", '
'"logo_uri": "http://localhost:8090/logo.png", '
......@@ -774,8 +892,7 @@ class TestID4meClient(TestCase):
http_request_mock.assert_called_once_with(
client.networkContext, 'POST', 'https://id.test.denic.de/clients',
'{"redirect_uris": ["http://localhost:8090/validate"], '
'"id_token_signed_response_alg": "RS256", '
'"userinfo_signed_response_alg": "RS256", '
'"id_token_signed_response_alg": "RS512", '
'"jwks_uri": "https://foo.com/jwks.json", '
'"id_token_encrypted_response_alg": "RSA-OAEP-256", '
'"userinfo_encrypted_response_alg": "RSA-OAEP-256", '
......@@ -867,8 +984,7 @@ class TestID4meClient(TestCase):
http_request_mock.assert_called_once_with(
client.networkContext, 'POST', 'https://id.test.denic.de/clients',
'{"redirect_uris": ["http://localhost:8090/validate"], '
'"id_token_signed_response_alg": "RS256", '
'"userinfo_signed_response_alg": "RS256", '
'"id_token_signed_response_alg": "RS512", '
'"client_name": "Test", "logo_uri": "http://localhost:8090/logo.png", '
'"policy_uri": "http://localhost:8090/about", '
'"tos_uri": "http://localhost:8090/documents", "application_type": "native"}',
......@@ -926,6 +1042,8 @@ class TestID4meClient(TestCase):
# All values valid
client.requireencryption = True
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'id_token_signing_alg_values_supported': ['RS256'],
......@@ -938,20 +1056,29 @@ class TestID4meClient(TestCase):
# Missing id_token_signing_alg_values_supported in config
client.requireencryption = False
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_identity_authority = MagicMock(return_value='id.test.denic.de')
client._get_openid_configuration = MagicMock(return_value=
{
'userinfo_signing_alg_values_supported': ['RS256'],
'registration_endpoint': 'https://foo.com'
})
registered_exception = None
try:
client._register_identity_authority('id.test.denic.de')
assert False, 'Exception expected: Missing id_token_signing_alg_values_supported in config'
except ID4meRelyingPartyRegistrationException:
pass
except ID4meRelyingPartyRegistrationException as e:
registered_exception = e
self.assertIsNotNone(registered_exception)
self.assertEqual(registered_exception.message, "Required signature algorithm for id_token RS256, RS384 or RS512"
" not supported by Authority")
# No valid value for id_token_signing_alg_values_supported in config
client.requireencryption = False
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_identity_authority = MagicMock(return_value='id.test.denic.de')
client._get_openid_configuration = MagicMock(return_value=
{
......@@ -967,6 +1094,8 @@ class TestID4meClient(TestCase):
# Missing userinfo_signing_alg_values_supported in config
client.requireencryption = False
client.require_user_info_signing = True
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'id_token_signing_alg_values_supported': ['RS256'],
......@@ -978,8 +1107,10 @@ class TestID4meClient(TestCase):
except ID4meRelyingPartyRegistrationException:
pass
# No valid value for userinfo_signing_alg_values_supported in config
# No valid value for userinfo_signing_alg_values_supported in config
client.requireencryption = False
client.require_user_info_signing = True
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'userinfo_signing_alg_values_supported': ['foo', 'bar'],
......@@ -992,8 +1123,46 @@ class TestID4meClient(TestCase):
except ID4meRelyingPartyRegistrationException:
pass
# None algorithm not supported and requested by the client config for userinfo
client.requireencryption = False
client.require_user_info_signing = False
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'userinfo_signing_alg_values_supported': ['RS512'],
'id_token_signing_alg_values_supported': ['RS256'],
'registration_endpoint': 'https://foo.com'
})
registered_exception = None
try:
client._register_identity_authority('id.test.denic.de')
except ID4meRelyingPartyRegistrationException as e:
registered_exception = e
self.assertIsNotNone(registered_exception, "Exception expected")
self.assertEqual(registered_exception.message, "Required signature algorithm for user_info none not supported by Authority")
# None algorithm not supported and requested by the client config for id_token
client.requireencryption = False
client.require_user_info_signing = None
client.require_id_token_signing = False
client._get_openid_configuration = MagicMock(return_value=
{
'userinfo_signing_alg_values_supported': ['RS512'],
'id_token_signing_alg_values_supported': ['RS256'],
'registration_endpoint': 'https://foo.com'
})
registered_exception = None
try:
client._register_identity_authority('id.test.denic.de')
except ID4meRelyingPartyRegistrationException as e:
registered_exception = e
self.assertIsNotNone(registered_exception)
self.assertEqual(registered_exception.message, "Required signature algorithm for id_token none not supported by Authority")
# Missing userinfo_encryption_alg_values_supported in config
client.requireencryption = True
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'id_token_signing_alg_values_supported': ['RS256'],
......@@ -1009,6 +1178,8 @@ class TestID4meClient(TestCase):
# No valid value for userinfo_encryption_alg_values_supported in config
client.requireencryption = True
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'id_token_signing_alg_values_supported': ['RS256'],
......@@ -1025,6 +1196,8 @@ class TestID4meClient(TestCase):
# Missing id_token_encryption_alg_values_supported in config
client.requireencryption = True
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'id_token_signing_alg_values_supported': ['RS256'],
......@@ -1040,6 +1213,8 @@ class TestID4meClient(TestCase):
# No valid value for id_token_encryption_alg_values_supported in config
client.requireencryption = True
client.require_user_info_signing = None
client.require_id_token_signing = True
client._get_openid_configuration = MagicMock(return_value=
{
'id_token_signing_alg_values_supported': ['RS256'],
......
......@@ -129,6 +129,34 @@ class TestID4meClient_IDTokenVerification(TestCase):
except ID4meTokenException as e:
assert False, '[test_Test0_1_Good_token_all_features] Unexpected exception: {}'.format(e.message)
def test_Test0_2_Good_token_not_signed(self):
try:
token = "eyJhbGciOiJub25lIn0.eyJhdWQiOiJsYmdka215N2xnbzRzIiwic3ViIjoiNG5SWWJqeXNxTDVPdm01cElXRjcwakIwZ2hsZmtqRDBfY2V5aF9yNGZ1MCIsImF1dGhfdGltZSI6MTU3NDM1NTUwNCwiaXNzIjoiaHR0cHM6Ly9icm9rZXIubmV0aWQuZGUvIiwiZXhwIjoxNTc0MzU2NDI1LCJpYXQiOjE1NzQzNTU1MjUsIm5vbmNlIjoiYzI0ZTRmYzgtYmM4ZS00OTYxLWJjN2QtYjhiMjIxNGRhMGI2In0."
'''
{
"alg": "none"
}
{
"aud": "34c5c94a-af5f-432c-a1ef-b37e40d79629",
"sub": "4nRYbjysqL5Ovm5pIWF70jB0ghlfkjD0_ceyh_r4fu0",
"auth_time": 1574355504,
"iss": "https://broker.netid.de/",
"exp": 1574356425,
"iat": 1574355525,
"nonce": "c24e4fc8-bc8e-4961-bc7d-b8b2214da0b6"
}
'''
ctx = ID4meContext(
id4me='id200.connect.domains',
identity_authority='broker.netid.de',
issuer='https://broker.netid.de/'
)
ctx.nonce = 'c24e4fc8-bc8e-4961-bc7d-b8b2214da0b6'
self.client._decode_token(token, ctx, self.registration, 'https://broker.netid.de/', TokenDecodeType.IDToken, leeway=datetime.timedelta(days=100 * 365), verify_aud=True)
except ID4meTokenException as e:
assert False, '[test_Test0_2_Good_token_not_signed] Unexpected exception: {}'.format(e.message)
def test_Test1_Empty_token(self):
try:
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vvssBSxwPSgjgE3R1oCXBSchRquU6bMesaQ17J7vUIsvGn-O4fDZWgKhG_YGwkJ-eLNAFkhoX6xNLCbmrbJ-b2RPaRRxaMRGvUnKjVCw6rJPaJ5vd-orylGuI2IsM7gZy3kWq0K7hnYmofKDHKhtbQMsZ35GJW9u9kVfxyIdOs-HNaYO7iBWTa92lYnpNgDoGpTY5-BUPqmIzN89NIMUu0Sj4wSy9KLz89bEGN0hMcSNuQi5DvzY61N6WKmHp6grKUBSe0IORd_e5QV1sCFy_EcbrJdrIgnP0bhhj2JypZBqojSw55FgA_7Ki_tPQs9UuJZSueSBaDz_vdhlr80R2w"
......
......@@ -11,7 +11,7 @@ __status__ = "Beta"
from setuptools import setup
setup(name='id4me-rp-client',
version='0.0.23',
version='0.0.24',
description='Python client library for ID4me protocol - Relying Party side. See: https://id4me.org',
long_description_content_type="text/markdown",
long_description=open('README.md').read(),
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment