def _verify_jws(self, payload, key): """Verify the given JWS payload with the given key and return the payload""" jws = JWS.from_compact(payload) try: alg = jws.signature.combined.alg.name except KeyError: msg = 'No alg value found in header' raise SuspiciousOperation(msg) if alg != self.OIDC_RP_SIGN_ALGO: msg = "The provider algorithm {!r} does not match the client's " \ "OIDC_RP_SIGN_ALGO.".format(alg) raise SuspiciousOperation(msg) if isinstance(key, six.string_types): # Use smart_bytes here since the key string comes from settings. jwk = JWK.load(smart_bytes(key)) else: # The key is a json returned from the IDP JWKS endpoint. jwk = JWK.from_json(key) if not jws.verify(jwk): msg = 'JWS token verification failed.' raise SuspiciousOperation(msg) return jws.payload
def test_from_json(self): from josepy.jwk import JWK self.assertEqual( self.jwk256, JWK.from_json(self.jwk256json)) self.assertEqual( self.jwk512, JWK.from_json(self.jwk512json)) self.assertEqual(self.private, JWK.from_json(self.private_json))
def test_encode_y_leading_zero_p256(self): import josepy from josepy.jwk import JWK, JWKEC data = b"""-----BEGIN EC PRIVATE KEY----- MHcCAQEEICZ7LCI99Na2KZ/Fq8JmJROakGJ5+J7rHiGSPoO36kOAoAoGCCqGSM49 AwEHoUQDQgAEGS5RvStca15z2FEanCM3juoX7tE/LB7iD44GWawGE40APAl/iZuH 31wQfst4glTZpxkpEI/MzNZHjiYnqrGeSw== -----END EC PRIVATE KEY-----""" key = JWKEC.load(data) data = key.to_partial_json() y = josepy.json_util.decode_b64jose(data['y']) self.assertEqual(y[0], 0) self.assertEqual(len(y), 32) JWK.from_json(data)
def verify_token(self, token, **kwargs): """Validate the token signature.""" token = force_bytes(token) jws = JWS.from_compact(token) header = json.loads(jws.signature.protected) try: header.get("alg") except KeyError: msg = "No alg value found in header" raise SuspiciousOperation(msg) jwk_json = self.retrieve_matching_jwk(header) jwk = JWK.from_json(jwk_json) if not jws.verify(jwk): msg = "JWS token verification failed." raise SuspiciousOperation(msg) # The 'token' will always be a byte string since it's # the result of base64.urlsafe_b64decode(). # The payload is always the result of base64.urlsafe_b64decode(). # In Python 3 and 2, that's always a byte string. # In Python3.6, the json.loads() function can accept a byte string # as it will automagically decode it to a unicode string before # deserializing https://bugs.python.org/issue17909 return json.loads(jws.payload.decode("utf-8"))
def test_signature_size(self): from josepy.jwa import ES512 from josepy.jwk import JWK key = JWK.from_json( { 'd': 'Af9KP6DqLRbtit6NS_LRIaCP_-NdC5l5R2ugbILdfpv6dS9R4wUPNxiGw' '-vVWumA56Yo1oBnEm8ZdR4W-u1lPHw5', 'x': 'AD4i4STyJ07iZJkHkpKEOuICpn6IHknzwAlrf-1w1a5dqOsRe30EECSN4vFxae' 'AmtdBSCKBwCq7h1q4bPgMrMUvF', 'y': 'AHAlXxrabjcx_yBxGObnm_DkEQMJK1E69OHY3x3VxF5VXoKc93CG4GLoaPvphZQv' 'Znt5EfExQoPktwOMIVhBHaFR', 'crv': 'P-521', 'kty': 'EC' }) with mock.patch("josepy.jwa.decode_dss_signature") as decode_patch: decode_patch.return_value = (0, 0) sig = ES512.sign(key.key, b"test") self.assertEqual(len(sig), 2 * 66)
def _verify_jws(self, payload, key): """Verify the given JWS payload with the given key and return the payload""" jws = JWS.from_compact(payload) jwk = JWK.load(key) if not jws.verify(jwk): msg = 'JWS token verification failed.' raise SuspiciousOperation(msg) try: alg = jws.signature.combined.alg.name if alg != self.OIDC_RP_SIGN_ALGO: msg = 'The specified alg value is not allowed' raise SuspiciousOperation(msg) except KeyError: msg = 'No alg value found in header' raise SuspiciousOperation(msg) return jws.payload
def _verified(self): try: jwk = JWK.load(self.public_key) self.jws_obj = JWS.from_compact(self.jws) if self._signed(jwk) is False: logger.warning( 'The public key signature was not valid for jws {jws}'. format(jws=self.jws)) self.jws_data = json.loads(self.jws.payload) self.jws_data['code'] = 'invalid' return False else: self.jws_data = json.loads(self.jws_obj.payload.decode()) logger.info('Loaded JWS data.') self.jws_data['connection_name'] = self._get_connection_name( self.jws_data['connection']) return True except UnicodeDecodeError: return False
def _verified(self): try: jwk = JWK.load(self.public_key) self.jws = JWS.from_compact(self.jws) if self._signed(jwk) is False: logger.warning( 'The public key signature was not valid for jws {jws}'. format(jws=self.jws)) self.jws_data = json.loads(self.jws.payload) self.jws_data['code'] = 'invalid' return False else: self.jws_data = json.loads(self.jws.payload) logger.info('Loaded JWS data.') self.jws_data['connection_name'] = self._get_connection_name( self.jws_data['connection_name']) return True except Exception as e: logger.warning( 'JWS could not be decoded due to {error}'.format(error=e)) return False
def _verify_jws(self, payload, key): """Verify the given JWS payload with the given key and return the payload""" jws = JWS.from_compact(payload) try: alg = jws.signature.combined.alg.name except KeyError: raise exceptions.AuthenticationFailed( "No alg value found in header") if alg != settings.OIDC_RP_SIGN_ALGO: raise exceptions.AuthenticationFailed( "The provider algorithm {!r} does not match the client's " "OIDC_RP_SIGN_ALGO.".format(alg)) jwk = JWK.from_json(key) if not jws.verify(jwk): raise exceptions.AuthenticationFailed( "JWS token verification failed.") return jws.payload
def get(self, request): """Callback handler for OIDC authorization code flow. This is based on the mozilla-django-oidc library. This callback is used to verify the identity added by the user. Users are already logged in and we do not care about authentication. The JWT token is used to prove the identity of the user. """ profile = request.user.userprofile # This is a difference nonce than the one used to login! nonce = request.session.get('oidc_verify_nonce') if nonce: # Make sure that nonce is not used twice del request.session['oidc_verify_nonce'] # Check for all possible errors and display a message to the user. errors = [ 'code' not in request.GET, 'state' not in request.GET, 'oidc_verify_state' not in request.session, request.GET['state'] != request.session['oidc_verify_state'] ] if any(errors): msg = 'Something went wrong, account verification failed.' messages.error(request, msg) return redirect('phonebook:profile_edit') token_payload = { 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'client_secret': self.OIDC_RP_VERIFICATION_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': request.GET['code'], 'redirect_uri': absolutify(self.request, nonprefixed_url('phonebook:verify_identity_callback')), } response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings( 'OIDC_VERIFY_SSL', True)) response.raise_for_status() token_response = response.json() id_token = token_response.get('id_token') # Verify JWT jws = JWS.from_compact(force_bytes(id_token)) jwk = JWK.load(smart_bytes(self.OIDC_RP_VERIFICATION_CLIENT_SECRET)) verified_token = None if jws.verify(jwk): verified_token = jws.payload # Create the new Identity Profile. if verified_token: user_info = json.loads(verified_token) email = user_info['email'] if not user_info.get('email_verified'): msg = 'Account verification failed: Email is not verified.' messages.error(request, msg) return redirect('phonebook:profile_edit') user_q = {'auth0_user_id': user_info['sub'], 'email': email} # If we are linking GitHub we need to save # the username too. if 'github|' in user_info['sub']: user_q['username'] = user_info['nickname'] # Check that the identity doesn't exist in another Identity profile # or in another mozillians profile error_msg = '' if IdpProfile.objects.filter(**user_q).exists(): error_msg = 'Account verification failed: Identity already exists.' elif User.objects.filter(email__iexact=email).exclude( pk=profile.user.pk).exists(): error_msg = 'The email in this identity is used by another user.' if error_msg: messages.error(request, error_msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect( next_url or reverse('phonebook:profile_edit')) # Save the new identity to the IdpProfile user_q['profile'] = profile idp, created = IdpProfile.objects.get_or_create(**user_q) current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # The new identity is stronger than the one currently used. Let's swap append_msg = '' # We need to check for equality too in the case a user updates the primary email in # the same identity (matching auth0_user_id). If there is an addition of the same type # we are not swapping login identities if ((current_idp and current_idp.type < idp.type) or (current_idp and current_idp.auth0_user_id == idp.auth0_user_id) or (not current_idp and created and idp.type >= IdpProfile.PROVIDER_GITHUB)): IdpProfile.objects.filter(profile=profile).exclude( pk=idp.pk).update(primary=False) idp.primary = True idp.save() # Also update the primary email of the user update_email_in_basket(profile.user.email, idp.email) User.objects.filter(pk=profile.user.id).update(email=idp.email) append_msg = ' You need to use this identity the next time you will login.' send_userprofile_to_cis.delay(profile.pk) if created: msg = 'Account successfully verified.' if append_msg: msg += append_msg messages.success(request, msg) else: msg = 'Account verification failed: Identity already exists.' messages.error(request, msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit'))
def verify_jws(token, client_id, openid_configuration): jws = JWS.from_compact(token.encode("ascii")) json_header = jws.signature.protected header = Header.json_loads(json_header) # alg alg = jws.signature.combined.alg.name.upper() if "NONE" in alg: raise SuspiciousOperation("ID token is not signed") if alg not in (alg.upper() for alg in openid_configuration["id_token_signing_alg_values_supported"]): raise SuspiciousOperation("Unexpected ID token signature algorithm") # retrieve signature key # TODO cache jwks_response = requests.get(openid_configuration["jwks_uri"], headers={"Accept": "application/json"}) jwks_response.raise_for_status() jwk = None for jwk_json in jwks_response.json()["keys"]: if jwk_json["kid"] == header.kid: jwk = JWK.from_json(jwk_json) break if not jwk: raise SuspiciousOperation("Could not find ID token signing key") # verify signature if not jws.verify(jwk): raise SuspiciousOperation("Invalid ID token signature") payload = json.loads(jws.payload.decode('utf-8')) # iss if payload.get("iss") != openid_configuration["issuer"]: raise SuspiciousOperation("Invalid ID token 'iss'") # aud if payload.get("aud") != client_id: raise SuspiciousOperation("Invalid ID token 'aud'") timestamp = int(time.time()) # nbf nbf = payload.get("nbf") if nbf is not None: try: nbf = int(nbf) except (TypeError, ValueError): raise SuspiciousOperation("Invalid ID token 'nbf'") if timestamp < nbf - TIMESTAMP_LEEWAY: raise SuspiciousOperation("ID token not valid yet") # exp exp = payload.get("exp") if exp is not None: try: exp = int(exp) except (TypeError, ValueError): raise SuspiciousOperation("Invalid ID token 'exp'") if timestamp > exp + TIMESTAMP_LEEWAY: raise SuspiciousOperation("ID token has expired") return payload
def get(self, request): """Callback handler for OIDC authorization code flow. This is based on the mozilla-django-oidc library. This callback is used to verify the identity added by the user. Users are already logged in and we do not care about authentication. The JWT token is used to prove the identity of the user. """ profile = request.user.userprofile # This is a difference nonce than the one used to login! nonce = request.session.get('oidc_verify_nonce') if nonce: # Make sure that nonce is not used twice del request.session['oidc_verify_nonce'] # Check for all possible errors and display a message to the user. errors = [ 'code' not in request.GET, 'state' not in request.GET, 'oidc_verify_state' not in request.session, request.GET['state'] != request.session['oidc_verify_state'] ] if any(errors): msg = 'Something went wrong, account verification failed.' messages.error(request, msg) return redirect('phonebook:profile_edit') token_payload = { 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'client_secret': self.OIDC_RP_VERIFICATION_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': request.GET['code'], 'redirect_uri': absolutify( self.request, nonprefixed_url('phonebook:verify_identity_callback') ), } response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings('OIDC_VERIFY_SSL', True)) response.raise_for_status() token_response = response.json() id_token = token_response.get('id_token') # Verify JWT jws = JWS.from_compact(force_bytes(id_token)) jwk = JWK.load(smart_bytes(self.OIDC_RP_VERIFICATION_CLIENT_SECRET)) verified_token = None if jws.verify(jwk): verified_token = jws.payload # Create the new Identity Profile. if verified_token: user_info = json.loads(verified_token) email = user_info['email'] if not user_info.get('email_verified'): msg = 'Account verification failed: Email is not verified.' messages.error(request, msg) return redirect('phonebook:profile_edit') user_q = { 'auth0_user_id': user_info['sub'], 'email': email } # If we are linking GitHub we need to save # the username too. if 'github|' in user_info['sub']: user_q['username'] = user_info['nickname'] # Check that the identity doesn't exist in another Identity profile # or in another mozillians profile error_msg = '' if IdpProfile.objects.filter(**user_q).exists(): error_msg = 'Account verification failed: Identity already exists.' elif User.objects.filter(email__iexact=email).exclude(pk=profile.user.pk).exists(): error_msg = 'The email in this identity is used by another user.' if error_msg: messages.error(request, error_msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit')) # Save the new identity to the IdpProfile user_q['profile'] = profile idp, created = IdpProfile.objects.get_or_create(**user_q) current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # The new identity is stronger than the one currently used. Let's swap append_msg = '' # We need to check for equality too in the case a user updates the primary email in # the same identity (matching auth0_user_id). If there is an addition of the same type # we are not swapping login identities if ((current_idp and current_idp.type < idp.type) or (current_idp and current_idp.auth0_user_id == idp.auth0_user_id) or (not current_idp and created and idp.type >= IdpProfile.PROVIDER_GITHUB)): IdpProfile.objects.filter(profile=profile).exclude(pk=idp.pk).update(primary=False) idp.primary = True idp.save() # Also update the primary email of the user update_email_in_basket(profile.user.email, idp.email) User.objects.filter(pk=profile.user.id).update(email=idp.email) append_msg = ' You need to use this identity the next time you will login.' send_userprofile_to_cis.delay(profile.pk) if created: msg = 'Account successfully verified.' if append_msg: msg += append_msg messages.success(request, msg) else: msg = 'Account verification failed: Identity already exists.' messages.error(request, msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit'))
def test_from_json_hashable(self): from josepy.jwk import JWK hash(JWK.from_json(self.jwk256json))
def test_from_json_private_small(self): from josepy.jwk import JWK self.assertEqual(self.private, JWK.from_json(self.private_json_small))