def test_no_key_wrapping(self): data = jwe.encrypt(b"Just Some Data", jwe.kdf(b"key", b"Salt")).split(b".") data[1] = b"cmFwcGE=" with pytest.raises(jwe.exceptions.UnsupportedOption) as e: jwe.decrypt(b".".join(data), jwe.kdf(b"key", b"Salt")) assert e.value.args[0] == "Key wrapping is currently not supported"
def test_improper_key(self): key = jwe.kdf(b"Testing", b"Pepper") data = b"Just some data" encrypted = jwe.encrypt(data, key) with pytest.raises(InvalidTag): # TODO make this a custom exception jwe.decrypt(encrypted, jwe.kdf(b"somekey", b"Salt")) == data
def test_improper_key(self): key = jwe.kdf(b'Testing', b'Pepper') data = b'Just some data' encrypted = jwe.encrypt(data, key) with pytest.raises(InvalidTag): # TODO make this a custom exception jwe.decrypt(encrypted, jwe.kdf(b'somekey', b'Salt')) == data
def test_invalid_header_json(self): with pytest.raises(jwe.exceptions.MalformedData) as e: jwe.decrypt( jwe.encrypt(b"Just Some Data", jwe.kdf(b"key", b"Salt"))[3:], # Cut out some of the JSON jwe.kdf(b"key", b"Salt"), ) assert e.value.args[0] == "Header is not valid JSON"
def test_no_key_wrapping(self): data = jwe.encrypt(b'Just Some Data', jwe.kdf(b'key', b'Salt')).split(b'.') data[1] = b'cmFwcGE=' with pytest.raises(jwe.exceptions.UnsupportedOption) as e: jwe.decrypt(b'.'.join(data), jwe.kdf(b'key', b'Salt')) assert e.value.args[0] == 'Key wrapping is currently not supported'
def test_invalid_header_json(self): with pytest.raises(jwe.exceptions.MalformedData) as e: jwe.decrypt( jwe.encrypt(b'Just Some Data', jwe.kdf(b'key', b'Salt'))[3:], # Cut out some of the JSON jwe.kdf(b'key', b'Salt')) assert e.value.args[0] == 'Header is not valid JSON'
def test_encrypt_decrypt(self): key = jwe.kdf(b'Testing', b'Pepper') data = b'Just some data' encrypted = jwe.encrypt(data, key) assert encrypted != data assert jwe.decrypt(encrypted, key) == data
async def make_request(self, params, headers, cookies): try: # Note: with simple request whose response is handled right afterwards without "being passed # further along", use the context manager so WB doesn't need to handle the sessions. async with aiohttp.request( 'get', settings.API_URL, params=params, headers=headers, cookies=cookies, ) as response: if response.status != 200: try: data = await response.json() except (ValueError, ContentTypeError): data = await response.read() raise exceptions.AuthError(data, code=response.status) try: raw = await response.json() signed_jwt = jwe.decrypt(raw['payload'].encode(), JWE_KEY) data = jwt.decode(signed_jwt, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM, options={'require_exp': True}) return data['data'] except (jwt.InvalidTokenError, KeyError): raise exceptions.AuthError(data, code=response.status) except ClientError: raise exceptions.AuthError('Unable to connect to auth sever', code=503)
def make_request(self, params, headers, cookies): try: response = yield from aiohttp.request( 'get', settings.API_URL, params=params, headers=headers, cookies=cookies, ) except aiohttp.errors.ClientError: raise exceptions.AuthError('Unable to connect to auth sever', code=503) if response.status != 200: try: data = yield from response.json() except ValueError: data = yield from response.read() raise exceptions.AuthError(data, code=response.status) try: raw = yield from response.json() signed_jwt = jwe.decrypt(raw['payload'].encode(), JWE_KEY) data = jwt.decode(signed_jwt, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM, options={'require_exp': True}) return data['data'] except (jwt.InvalidTokenError, KeyError): raise exceptions.AuthError(data, code=response.status)
def to_python(self, output_json): if not output_json: return None output_json = json.loads(jwe.decrypt(bytes(output_json[len(self.prefix):]), settings.SENSITIVE_DATA_KEY).decode('utf-8')) return output_json
def test_encrypt_decrypt(self): key = jwe.kdf(b"Testing", b"Pepper") data = b"Just some data" encrypted = jwe.encrypt(data, key) assert encrypted != data assert jwe.decrypt(encrypted, key) == data
async def make_request(self, params, headers, cookies): try: response = await aiohttp.request( 'get', settings.API_URL, params=params, headers=headers, cookies=cookies, ) except aiohttp.errors.ClientError: raise exceptions.AuthError('Unable to connect to auth sever', code=503) if response.status != 200: try: data = await response.json() except ValueError: data = await response.read() raise exceptions.AuthError(data, code=response.status) try: raw = await response.json() signed_jwt = jwe.decrypt(raw['payload'].encode(), JWE_KEY) data = jwt.decode(signed_jwt, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM, options={'require_exp': True}) return data['data'] except (jwt.InvalidTokenError, KeyError): raise exceptions.AuthError(data, code=response.status)
def authenticate(self, request): try: payload = jwt.decode(jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256') except (jwt.InvalidTokenError, TypeError): raise AuthenticationFailed # The JWT `data` payload is expected in the following structure. # # {"provider": { # "idp": "https://login.circle.edu/idp/shibboleth", # "id": "CIR", # "user": { # "middleNames": "", # "familyName": "", # "givenName": "", # "fullname": "Circle User", # "suffix": "", # "username": "******" # } # }} data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed( 'Invalid institution id specified "{}"'.format(provider['id'])) username = provider['user']['username'] fullname = provider['user']['fullname'] user, created = get_or_create_user(fullname, username, reset_password=False) if created: user.given_name = provider['user'].get('givenName') user.middle_names = provider['user'].get('middleNames') user.family_name = provider['user'].get('familyName') user.suffix = provider['user'].get('suffix') user.date_last_login = datetime.utcnow() user.save() # User must be saved in order to have a valid _id user.register(username) send_mail(to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user) if institution not in user.affiliated_institutions: user.affiliated_institutions.append(institution) user.save() return user, None
def authenticate(self, request): try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256' ) except (jwt.InvalidTokenError, TypeError): raise AuthenticationFailed # The JWT `data` payload is expected in the following structure. # # {"provider": { # "idp": "https://login.circle.edu/idp/shibboleth", # "id": "CIR", # "user": { # "middleNames": "", # "familyName": "", # "givenName": "", # "fullname": "Circle User", # "suffix": "", # "username": "******" # } # }} data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed('Invalid institution id specified "{}"'.format(provider['id'])) username = provider['user']['username'] fullname = provider['user']['fullname'] user, created = get_or_create_user(fullname, username, reset_password=False) if created: user.given_name = provider['user'].get('givenName') user.middle_names = provider['user'].get('middleNames') user.family_name = provider['user'].get('familyName') user.suffix = provider['user'].get('suffix') user.date_last_login = datetime.utcnow() user.save() # User must be saved in order to have a valid _id user.register(username) send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user ) if institution not in user.affiliated_institutions: user.affiliated_institutions.append(institution) user.save() return user, None
def to_python(self, value): if value and value.startswith(self.prefix): value = ensure_bytes(value) if not settings.RUNNING_MIGRATION: # don't decrypt things if we're migrating. value = jwe.decrypt(bytes(value[len(self.prefix):]), SENSITIVE_DATA_KEY) else: return value[6:] return value
def test_auth_download(self): url = self.build_url() res = self.app.get(url, auth=self.user.auth) data = jwt.decode(jwe.decrypt(res.json['payload'].encode('utf-8'), self.JWE_KEY), settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM)['data'] assert_equal(data['auth'], views.make_auth(self.user)) assert_equal(data['credentials'], self.node_addon.serialize_waterbutler_credentials()) assert_equal(data['settings'], self.node_addon.serialize_waterbutler_settings()) expected_url = furl.furl(self.node.api_url_for('create_waterbutler_log', _absolute=True, _internal=True)) observed_url = furl.furl(data['callback_url']) observed_url.port = expected_url.port assert_equal(expected_url, observed_url)
def to_python(self, value): if value and value.startswith(self.prefix): value = ensure_bytes(value) try: value = jwe.decrypt(bytes(value[len(self.prefix):]), SENSITIVE_DATA_KEY) except InvalidTag: # Allow use of an encrypted DB locally without decrypting fields if settings.DEBUG_MODE: pass else: raise return value
def test_database_is_encrypted(self): eaf = ExternalAccountFactory(**self.encrypted_field_dict) ea = ExternalAccount.objects.get(id=eaf.id) ea.reload() sql = """ SELECT %s FROM osf_externalaccount WHERE id = %s; """ with connection.cursor() as cursor: cursor.execute(sql, [AsIs(', '.join(self.encrypted_field_dict.keys())), ea.id]) row = cursor.fetchone() for blicky in row: assert jwe.decrypt(bytes(blicky[len(EncryptedTextField.prefix):]), SENSITIVE_DATA_KEY) == self.magic_string
def decrypt_string(value, prefix='jwe:::'): prefix = ensure_bytes(prefix) if value: value = ensure_bytes(value) if value.startswith(prefix): try: value = jwe.decrypt(value[len(prefix):], SENSITIVE_DATA_KEY).decode() except InvalidTag: # Allow use of an encrypted DB locally without decrypting fields if settings.DEBUG_MODE: pass else: raise return value
def test_auth_download(self): url = self.build_url() res = self.app.get(url, auth=self.user.auth) data = jwt.decode( jwe.decrypt(res.json["payload"].encode("utf-8"), self.JWE_KEY), settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM, )["data"] assert_equal(data["auth"], views.make_auth(self.user)) assert_equal(data["credentials"], self.node_addon.serialize_waterbutler_credentials()) assert_equal(data["settings"], self.node_addon.serialize_waterbutler_settings()) expected_url = furl.furl(self.node.api_url_for("create_waterbutler_log", _absolute=True)) observed_url = furl.furl(data["callback_url"]) observed_url.port = expected_url.port assert_equal(expected_url, observed_url)
def decorated(*args, **kwargs): auth = request.headers.get('Authorization', None) if not auth: return handle_error({'message': 'authorization_header_missing', 'description': 'Authorization header is expected'}, 401) parts = auth.split() if parts[0].lower() != 'bearer': return handle_error({'message': 'invalid_header', 'description': 'Authorization header must start with' 'Bearer'}, 401) elif len(parts) == 1: return handle_error({'message': 'invalid_header', 'description': 'Token not found'}, 401) elif len(parts) > 2: return handle_error({'message': 'invalid_header', 'description': 'Authorization header must be' 'Bearer + \s + token'}, 401) token = parts[1] try: payload = jwe.decrypt( token.encode("utf-8"), getEncode() ) dPayLoad = json.loads(payload.decode("utf-8")) if not datetime.strptime(dPayLoad['expires'], "%Y-%m-%d %H:%M:%S.%f") > datetime.now(): raise Exception("token expired") if 'accountname' not in dPayLoad: raise Exception("Invalid Token") # if 'userlevel' not in dPayLoad or dPayLoad['userlevel'] >= 5: # raise Exception("Status Pending") except Exception as e: return handle_error({'message': str(e), 'description': 'Unable to parse authentication : '+str(e)+ '' ' token.'}, 400) _app_ctx_stack.top.current_user = dPayLoad return f(*args, **kwargs)
def decrypt(self, cookie): # Decoding is the reverse of what Accounts does to encode a cookie: # Accounts first signs the payload w/ the signature private key, then # it next symmetric encrypts that result w/ the encryption private key. try: decrypted_payload = jwe.decrypt( cookie.encode(), self.encryption_private_key.encode()) decoded_payload = jwt.decode(decrypted_payload, self.signature_public_key, audience="OpenStax", algorithms=[self.signature_algorithm]) return Payload(decoded_payload) except Exception: if self.logging_enabled: import logging logging.exception("Could not decrypt cookie") return None
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", } } } :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed if authentication fails """ try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError): raise AuthenticationFailed data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed('Invalid institution id specified "{}"'.format(provider['id'])) username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') # use given name and family name to build full name if not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # institution must provide `fullname`, otherwise we fail the authentication and inform sentry if not fullname: message = 'Institution login failed: fullname required' \ ' for user {} from institution {}'.format(username, provider['id']) sentry.log_message(message) raise AuthenticationFailed(message) # `get_or_create_user()` guesses names from fullname # replace the guessed ones if the names are provided from the authentication user, created = get_or_create_user(fullname, username, reset_password=False) if created: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix user.update_date_last_login() # Relying on front-end validation until `accepted_tos` is added to the JWT payload user.accepted_terms_of_service = timezone.now() # save and register user user.save() user.register(username) # send confirmation email send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active(request, features.STORAGE_I18N), ) if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() return user, None
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = OSFUser.load(cas_resp.user) try: data = jwt.decode(jwe.decrypt( request.args.get('payload', '').encode('utf-8'), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={'require_exp': True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM)['data'] except (jwt.InvalidTokenError, KeyError) as err: sentry.log_message(str(err)) raise HTTPError(http_status.HTTP_403_FORBIDDEN) if not auth.user: auth.user = OSFUser.from_cookie(data.get('cookie', '')) try: action = data['action'] node_id = data['nid'] provider_name = data['provider'] except KeyError: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) node = AbstractNode.load(node_id) or Preprint.load(node_id) if node and node.is_deleted: raise HTTPError(http_status.HTTP_410_GONE) elif not node: raise HTTPError(http_status.HTTP_404_NOT_FOUND) check_access(node, auth, action, cas_resp) provider_settings = None if hasattr(node, 'get_addon'): provider_settings = node.get_addon(provider_name) if not provider_settings: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) path = data.get('path') credentials = None waterbutler_settings = None fileversion = None if provider_name == 'osfstorage': if path: file_id = path.strip('/') # check to see if this is a file or a folder filenode = OsfStorageFileNode.load(path.strip('/')) if filenode and filenode.is_file: # default to most recent version if none is provided in the response version = int(data['version']) if data.get( 'version') else filenode.versions.count() try: fileversion = FileVersion.objects.filter( basefilenode___id=file_id, identifier=version).select_related('region').get() except FileVersion.DoesNotExist: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) if auth.user: # mark fileversion as seen FileVersionUserMetadata.objects.get_or_create( user=auth.user, file_version=fileversion) if not node.is_contributor_or_group_member(auth.user): from_mfr = download_is_from_mfr(request, payload=data) # version index is 0 based version_index = version - 1 if action == 'render': update_analytics(node, filenode, version_index, 'view') elif action == 'download' and not from_mfr: update_analytics(node, filenode, version_index, 'download') if waffle.switch_is_active(features.ELASTICSEARCH_METRICS): if isinstance(node, Preprint): metric_class = get_metric_class_for_action( action, from_mfr=from_mfr) if metric_class: sloan_flags = { 'sloan_id': request.cookies.get(SLOAN_ID_COOKIE_NAME) } for flag_name in SLOAN_FLAGS: value = request.cookies.get( f'dwf_{flag_name}_custom_domain' ) or request.cookies.get( f'dwf_{flag_name}') if value: sloan_flags[flag_name.replace( '_display', '')] = strtobool(value) try: metric_class.record_for_preprint( preprint=node, user=auth.user, version=fileversion.identifier if fileversion else None, path=path, **sloan_flags) except es_exceptions.ConnectionError: log_exception() if fileversion and provider_settings: region = fileversion.region credentials = region.waterbutler_credentials waterbutler_settings = fileversion.serialize_waterbutler_settings( node_id=provider_settings.owner._id, root_id=provider_settings.root_node._id, ) # If they haven't been set by version region, use the NodeSettings or Preprint directly if not (credentials and waterbutler_settings): credentials = node.serialize_waterbutler_credentials(provider_name) waterbutler_settings = node.serialize_waterbutler_settings( provider_name) if isinstance(credentials.get('token'), bytes): credentials['token'] = credentials.get('token').decode() return { 'payload': jwe.encrypt( jwt.encode( { 'exp': timezone.now() + datetime.timedelta( seconds=settings.WATERBUTLER_JWT_EXPIRATION), 'data': { 'auth': make_auth( auth.user ), # A waterbutler auth dict not an Auth object 'credentials': credentials, 'settings': waterbutler_settings, 'callback_url': node.api_url_for( ('create_waterbutler_log' if not getattr(node, 'is_registration', False) else 'registration_callbacks'), _absolute=True, _internal=True) } }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY).decode() }
def decrypt(value): if value: return jwe.decrypt(value.encode('utf-8'), SENSITIVE_DATA_KEY) return None
def encrypt_key(document, key): database['externalaccount'].find_and_modify( {'_id': document['_id']}, {'$set': { key: jwe.encrypt(bytes(jwe.decrypt(document[key].encode('utf-8'), SENSITIVE_DATA_KEY)), SENSITIVE_DATA_KEY) }})
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = User.load(cas_resp.user) try: data = jwt.decode( jwe.decrypt(request.args.get('payload', '').encode('utf-8'), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={'require_exp': True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM )['data'] except (jwt.InvalidTokenError, KeyError): raise HTTPError(httplib.FORBIDDEN) if not auth.user: auth.user = User.from_cookie(data.get('cookie', '')) try: action = data['action'] node_id = data['nid'] provider_name = data['provider'] except KeyError: raise HTTPError(httplib.BAD_REQUEST) node = Node.load(node_id) if not node: raise HTTPError(httplib.NOT_FOUND) check_access(node, auth, action, cas_resp) provider_settings = node.get_addon(provider_name) if not provider_settings: raise HTTPError(httplib.BAD_REQUEST) try: credentials = provider_settings.serialize_waterbutler_credentials() waterbutler_settings = provider_settings.serialize_waterbutler_settings() except exceptions.AddonError: log_exception() raise HTTPError(httplib.BAD_REQUEST) return {'payload': jwe.encrypt(jwt.encode({ 'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=settings.WATERBUTLER_JWT_EXPIRATION), 'data': { 'auth': make_auth(auth.user), # A waterbutler auth dict not an Auth object 'credentials': credentials, 'settings': waterbutler_settings, 'callback_url': node.api_url_for( ('create_waterbutler_log' if not node.is_registration else 'registration_callbacks'), _absolute=True, ), } }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY)}
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", } } } :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed if authentication fails """ # Verify / decrypt / decode the payload try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError, jwe.exceptions.MalformedData): raise AuthenticationFailed # Load institution and user data data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed('Invalid institution id: "{}"'.format(provider['id'])) username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') department = provider['user'].get('department') # Use given name and family name to build full name if it is not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # Non-empty full name is required. Fail the auth and inform sentry if not provided. if not fullname: message = 'Institution login failed: fullname required for ' \ 'user "{}" from institution "{}"'.format(username, provider['id']) sentry.log_message(message) raise AuthenticationFailed(message) # Get an existing user or create a new one. If a new user is created, the user object is # confirmed but not registered,which is temporarily of an inactive status. If an existing # user is found, it is also possible that the user is inactive (e.g. unclaimed, disabled, # unconfirmed, etc.). user, created = get_or_create_user(fullname, username, reset_password=False) # Existing but inactive users need to be either "activated" or failed the auth activation_required = False new_password_required = False if not created: try: drf.check_user(user) logger.info('Institution SSO: active user "{}"'.format(username)) except exceptions.UnclaimedAccountError: # Unclaimed user (i.e. a user that has been added as an unregistered contributor) user.unclaimed_records = {} activation_required = True # Unclaimed users have an unusable password when being added as an unregistered # contributor. Thus a random usable password must be assigned during activation. new_password_required = True logger.info('Institution SSO: unclaimed contributor "{}"'.format(username)) except exceptions.UnconfirmedAccountError: if user.has_usable_password(): # Unconfirmed user from default username / password signup user.email_verifications = {} activation_required = True # Unconfirmed users already have a usable password set by the creator during # sign-up. However, it must be overwritten by a new random one so the creator # (if he is not the real person) can not access the account after activation. new_password_required = True logger.info('Institution SSO: unconfirmed user "{}"'.format(username)) else: # Login take-over has not been implemented for unconfirmed user created via # external IdP login (ORCiD). message = 'Institution SSO is not eligible for an unconfirmed account ' \ 'created via external IdP login: username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None except exceptions.DeactivatedAccountError: # Deactivated user: login is not allowed for deactivated users message = 'Institution SSO is not eligible for a deactivated account: ' \ 'username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None except exceptions.MergedAccountError: # Merged user: this shouldn't happen since merged users do not have an email message = 'Institution SSO is not eligible for a merged account: ' \ 'username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None except exceptions.InvalidAccountError: # Other invalid status: this shouldn't happen unless the user happens to be in a # temporary state. Such state requires more updates before the user can be saved # to the database. (e.g. `get_or_create_user()` creates a temporary-state user.) message = 'Institution SSO is not eligible for an inactive account with ' \ 'an unknown or invalid status: username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None else: logger.info('Institution SSO: new user "{}"'.format(username)) # The `department` field is updated each login when it was changed. if department and user.department != department: user.department = department user.save() # Both created and activated accounts need to be updated and registered if created or activation_required: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix # Users claimed or confirmed via institution SSO should have their full name updated if activation_required: user.fullname = fullname user.update_date_last_login() # Relying on front-end validation until `accepted_tos` is added to the JWT payload user.accepted_terms_of_service = timezone.now() # Register and save user password = str(uuid.uuid4()) if new_password_required else None user.register(username, password=password) user.save() # Send confirmation email for all three: created, confirmed and claimed send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active(request, features.STORAGE_I18N), ) # Affiliate the user if not previously affiliated if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() return user, None
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", "department": "", "isMemberOf": "", # Shared SSO "selectiveSsoFilter": "", # Selective SSO } } } Note that if authentication failed, HTTP 403 Forbidden is returned no matter what type of exception is raised. In this method, we use `AuthenticationFailed` when the payload is not correctly encrypted/encoded since it is the "authentication" between CAS and this endpoint. We use `PermissionDenied` for all other exceptions that happened afterwards. :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed or PermissionDenied if authentication fails """ # Verify / decrypt / decode the payload try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError, jwe.exceptions.MalformedData): raise AuthenticationFailed( detail='InstitutionSsoRequestNotAuthorized') # Load institution and user data data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: message = 'Institution SSO Error: invalid institution ID [{}]'.format( provider['id']) logger.error(message) sentry.log_message(message) raise PermissionDenied(detail='InstitutionSsoInvalidInstitution') username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') department = provider['user'].get('department') selective_sso_filter = provider['user'].get('selectiveSsoFilter') # Check selective login first if provider['id'] in INSTITUTION_SELECTIVE_SSO_MAP: if selective_sso_filter != INSTITUTION_SELECTIVE_SSO_MAP[ provider['id']]: message = f'Institution SSO Error: user [email={username}] is not allowed for ' \ f'institution SSO [id={institution._id}] due to selective SSO rules' logger.error(message) sentry.log_message(message) raise PermissionDenied( detail='InstitutionSsoSelectiveNotAllowed') logger.info( f'Institution SSO: selective SSO verified for user [email={username}] ' f'at institution [id={institution._id}]', ) # Check secondary institutions which uses the SSO of primary ones secondary_institution = None if provider['id'] in INSTITUTION_SHARED_SSO_MAP: switch_map = INSTITUTION_SHARED_SSO_MAP[provider['id']] criteria_type = switch_map.get('criteria') if criteria_type == 'attribute': attribute_name = switch_map.get('attribute') attribute_value = provider['user'].get(attribute_name) if attribute_value: secondary_institution_id = switch_map.get( 'institutions', {}, ).get(attribute_value) logger.info( 'Institution SSO: primary=[{}], secondary=[{}], ' 'username=[{}]'.format(provider['id'], secondary_institution_id, username)) secondary_institution = Institution.load( secondary_institution_id) if not secondary_institution: # Log errors and inform Sentry but do not raise an exception if OSF fails # to load the secondary institution from database message = 'Institution SSO Error: invalid secondary institution [{}]; ' \ 'primary=[{}], username=[{}]'.format(attribute_value, provider['id'], username) logger.error(message) sentry.log_message(message) else: # SSO from primary institution only logger.info( 'Institution SSO: primary=[{}], secondary=[None], ' 'username=[{}]'.format(provider['id'], username)) else: message = 'Institution SSO Error: invalid criteria [{}]; ' \ 'primary=[{}], username=[{}]'.format(criteria_type, provider['id'], username) logger.error(message) sentry.log_message(message) # Use given name and family name to build full name if it is not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # Non-empty full name is required. Fail the auth and inform sentry if not provided. if not fullname: message = 'Institution SSO Error: missing fullname ' \ 'for user [{}] from institution [{}]'.format(username, provider['id']) logger.error(message) sentry.log_message(message) raise PermissionDenied(detail='InstitutionSsoMissingUserNames') # Get an existing user or create a new one. If a new user is created, the user object is # confirmed but not registered,which is temporarily of an inactive status. If an existing # user is found, it is also possible that the user is inactive (e.g. unclaimed, disabled, # unconfirmed, etc.). user, created = get_or_create_user(fullname, username, reset_password=False) # Existing but inactive users need to be either "activated" or failed the auth activation_required = False new_password_required = False if not created: try: drf.check_user(user) logger.info( 'Institution SSO: active user [{}]'.format(username)) except exceptions.UnclaimedAccountError: # Unclaimed user (i.e. a user that has been added as an unregistered contributor) user.unclaimed_records = {} activation_required = True # Unclaimed users have an unusable password when being added as an unregistered # contributor. Thus a random usable password must be assigned during activation. new_password_required = True logger.warning( 'Institution SSO: unclaimed contributor [{}]'.format( username)) except exceptions.UnconfirmedAccountError: if user.has_usable_password(): # Unconfirmed user from default username / password signup user.email_verifications = {} activation_required = True # Unconfirmed users already have a usable password set by the creator during # sign-up. However, it must be overwritten by a new random one so the creator # (if he is not the real person) can not access the account after activation. new_password_required = True logger.warning( 'Institution SSO: unconfirmed user [{}]'.format( username)) else: # Login take-over has not been implemented for unconfirmed user created via # external IdP login (ORCiD). message = 'Institution SSO Error: SSO is not eligible for an unconfirmed account [{}] ' \ 'created via IdP login'.format(username) sentry.log_message(message) logger.error(message) raise PermissionDenied( detail='InstitutionSsoAccountNotConfirmed') except exceptions.DeactivatedAccountError: # Deactivated user: login is not allowed for deactivated users message = 'Institution SSO Error: SSO is not eligible for a deactivated account: [{}]'.format( username) sentry.log_message(message) logger.error(message) raise PermissionDenied(detail='InstitutionSsoAccountDisabled') except exceptions.MergedAccountError: # Merged user: this shouldn't happen since merged users do not have an email message = 'Institution SSO Error: SSO is not eligible for a merged account: [{}]'.format( username) sentry.log_message(message) logger.error(message) raise PermissionDenied(detail='InstitutionSsoAccountMerged') except exceptions.InvalidAccountError: # Other invalid status: this shouldn't happen unless the user happens to be in a # temporary state. Such state requires more updates before the user can be saved # to the database. (e.g. `get_or_create_user()` creates a temporary-state user.) message = 'Institution SSO Error: SSO is not eligible for an inactive account [{}] ' \ 'with an unknown or invalid status'.format(username) sentry.log_message(message) logger.error(message) raise PermissionDenied(detail='InstitutionSsoInvalidAccount') else: logger.info('Institution SSO: new user [{}]'.format(username)) # The `department` field is updated each login when it was changed. user_guid = user.guids.first()._id if department: if user.department != department: user.department = department user.save() logger.info( 'Institution SSO: user w/ dept: user=[{}], email=[{}], inst=[{}], ' 'dept=[{}]'.format(user_guid, username, institution._id, department)) else: logger.info( 'Institution SSO: user w/o dept: user=[{}], email=[{}], ' 'inst=[{}]'.format(user_guid, username, institution._id)) # Both created and activated accounts need to be updated and registered if created or activation_required: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix # Users claimed or confirmed via institution SSO should have their full name updated if activation_required: user.fullname = fullname user.update_date_last_login() # Register and save user password = str(uuid.uuid4()) if new_password_required else None user.register(username, password=password) user.save() # Send confirmation email for all three: created, confirmed and claimed send_mail( to_addr=user.username, mail=WELCOME_OSF4I, user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active( request, features.STORAGE_I18N), ) # Affiliate the user to the primary institution if not previously affiliated if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() # Affiliate the user to the secondary institution if not previously affiliated if secondary_institution and not user.is_affiliated_with_institution( secondary_institution): user.affiliated_institutions.add(secondary_institution) user.save() return user, None
def decrypt(value): if value: return jwe.decrypt(bytes(value), SENSITIVE_DATA_KEY) return None
def test_invalid_data(self): with pytest.raises(jwe.exceptions.MalformedData): jwe.decrypt(b"junkdata", jwe.kdf(b"key", b"Salt"))
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = OSFUser.load(cas_resp.user) try: data = jwt.decode( jwe.decrypt(request.args.get('payload', '').encode('utf-8'), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={'require_exp': True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM )['data'] except (jwt.InvalidTokenError, KeyError) as err: sentry.log_message(str(err)) raise HTTPError(httplib.FORBIDDEN) if not auth.user: auth.user = OSFUser.from_cookie(data.get('cookie', '')) try: action = data['action'] node_id = data['nid'] provider_name = data['provider'] except KeyError: raise HTTPError(httplib.BAD_REQUEST) node = AbstractNode.load(node_id) or Preprint.load(node_id) if not node: raise HTTPError(httplib.NOT_FOUND) check_access(node, auth, action, cas_resp) provider_settings = None if hasattr(node, 'get_addon'): provider_settings = node.get_addon(provider_name) if not provider_settings: raise HTTPError(httplib.BAD_REQUEST) try: path = data.get('path') version = data.get('version') credentials = None waterbutler_settings = None fileversion = None if provider_name == 'osfstorage': if path and version: # check to see if this is a file or a folder filenode = OsfStorageFileNode.load(path.strip('/')) if filenode and filenode.is_file: try: fileversion = FileVersion.objects.filter( basefilenode___id=path.strip('/'), identifier=version ).select_related('region').get() except FileVersion.DoesNotExist: raise HTTPError(httplib.BAD_REQUEST) # path and no version, use most recent version elif path: filenode = OsfStorageFileNode.load(path.strip('/')) if filenode and filenode.is_file: fileversion = FileVersion.objects.filter( basefilenode=filenode ).select_related('region').order_by('-created').first() if fileversion: region = fileversion.region credentials = region.waterbutler_credentials waterbutler_settings = fileversion.serialize_waterbutler_settings( node_id=provider_settings.owner._id if provider_settings else node._id, root_id=provider_settings.root_node._id if provider_settings else node.root_folder._id, ) # If they haven't been set by version region, use the NodeSettings region if not (credentials and waterbutler_settings): credentials = node.serialize_waterbutler_credentials(provider_name) waterbutler_settings = node.serialize_waterbutler_settings(provider_name) except exceptions.AddonError: log_exception() raise HTTPError(httplib.BAD_REQUEST) # TODO: Add a signal here? if waffle.switch_is_active(features.ELASTICSEARCH_METRICS): user = auth.user if isinstance(node, Preprint) and not node.is_contributor(user): metric_class = get_metric_class_for_action(action) if metric_class: try: metric_class.record_for_preprint( preprint=node, user=user, version=fileversion.identifier if fileversion else None, path=path ) except es_exceptions.ConnectionError: log_exception() return {'payload': jwe.encrypt(jwt.encode({ 'exp': timezone.now() + datetime.timedelta(seconds=settings.WATERBUTLER_JWT_EXPIRATION), 'data': { 'auth': make_auth(auth.user), # A waterbutler auth dict not an Auth object 'credentials': credentials, 'settings': waterbutler_settings, 'callback_url': node.api_url_for( ('create_waterbutler_log' if not getattr(node, 'is_registration', False) else 'registration_callbacks'), _absolute=True, _internal=True ) } }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY)}
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", } } } :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed if authentication fails """ try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError, jwe.exceptions.MalformedData): raise AuthenticationFailed data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed( 'Invalid institution id specified "{}"'.format(provider['id'])) username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') # use given name and family name to build full name if not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # institution must provide `fullname`, otherwise we fail the authentication and inform sentry if not fullname: message = 'Institution login failed: fullname required' \ ' for user {} from institution {}'.format(username, provider['id']) sentry.log_message(message) raise AuthenticationFailed(message) # `get_or_create_user()` guesses names from fullname # replace the guessed ones if the names are provided from the authentication user, created = get_or_create_user(fullname, username, reset_password=False) if created: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix user.update_date_last_login() # Relying on front-end validation until `accepted_tos` is added to the JWT payload user.accepted_terms_of_service = timezone.now() # save and register user user.save() user.register(username) # send confirmation email send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active( request, features.STORAGE_I18N), ) if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() return user, None
def decrypt(value): if value: value = ensure_bytes(value) return jwe.decrypt(bytes(value), SENSITIVE_DATA_KEY) return None
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = OSFUser.load(cas_resp.user) try: data = jwt.decode(jwe.decrypt( request.args.get('payload', '').encode('utf-8'), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={'require_exp': True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM)['data'] except (jwt.InvalidTokenError, KeyError) as err: sentry.log_message(str(err)) raise HTTPError(httplib.FORBIDDEN) if not auth.user: auth.user = OSFUser.from_cookie(data.get('cookie', '')) try: action = data['action'] node_id = data['nid'] provider_name = data['provider'] except KeyError: raise HTTPError(httplib.BAD_REQUEST) node = AbstractNode.load(node_id) if not node: raise HTTPError(httplib.NOT_FOUND) check_access(node, auth, action, cas_resp) provider_settings = node.get_addon(provider_name) if not provider_settings: raise HTTPError(httplib.BAD_REQUEST) try: credentials = provider_settings.serialize_waterbutler_credentials() waterbutler_settings = provider_settings.serialize_waterbutler_settings( ) except exceptions.AddonError: log_exception() raise HTTPError(httplib.BAD_REQUEST) return { 'payload': jwe.encrypt( jwt.encode( { 'exp': timezone.now() + datetime.timedelta( seconds=settings.WATERBUTLER_JWT_EXPIRATION), 'data': { 'auth': make_auth( auth.user ), # A waterbutler auth dict not an Auth object 'credentials': credentials, 'settings': waterbutler_settings, 'callback_url': node.api_url_for(('create_waterbutler_log' if not node.is_registration else 'registration_callbacks'), _absolute=True, _internal=True), } }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY) }
def test_invalid_data(self): with pytest.raises(jwe.exceptions.MalformedData): jwe.decrypt(b'junkdata', jwe.kdf(b'key', b'Salt'))
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get("Authorization") if authorization and authorization.startswith("Bearer "): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = User.load(cas_resp.user) try: data = jwt.decode( jwe.decrypt(request.args.get("payload", "").encode("utf-8"), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={"require_exp": True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM, )["data"] except (jwt.InvalidTokenError, KeyError): raise HTTPError(httplib.FORBIDDEN) if not auth.user: auth.user = User.from_cookie(data.get("cookie", "")) try: action = data["action"] node_id = data["nid"] provider_name = data["provider"] except KeyError: raise HTTPError(httplib.BAD_REQUEST) node = Node.load(node_id) if not node: raise HTTPError(httplib.NOT_FOUND) check_access(node, auth, action, cas_resp) provider_settings = node.get_addon(provider_name) if not provider_settings: raise HTTPError(httplib.BAD_REQUEST) try: credentials = provider_settings.serialize_waterbutler_credentials() waterbutler_settings = provider_settings.serialize_waterbutler_settings() except exceptions.AddonError: log_exception() raise HTTPError(httplib.BAD_REQUEST) return { "payload": jwe.encrypt( jwt.encode( { "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=settings.WATERBUTLER_JWT_EXPIRATION), "data": { "auth": make_auth(auth.user), # A waterbutler auth dict not an Auth object "credentials": credentials, "settings": waterbutler_settings, "callback_url": node.api_url_for( ("create_waterbutler_log" if not node.is_registration else "registration_callbacks"), _absolute=True, ), }, }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM, ), WATERBUTLER_JWE_KEY, ) }