From abf4fc9bf73f3c4d11523485c024b110da7e0e51 Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Fri, 4 Aug 2023 20:27:17 +0000 Subject: [PATCH 1/7] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c59f099 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +sf_private_key.pem \ No newline at end of file From cffd268e672f8e5b6c2b43ff395536c6c3a4ed61 Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Fri, 4 Aug 2023 21:30:04 +0000 Subject: [PATCH 2/7] initial working package --- .gitignore | 166 +++++++++++++++++- Lambda/Dockerfile | 24 --- Lambda/lambda_function.py | 139 --------------- Lambda/requirements.txt | 3 - pyproject.toml | 22 +++ src/successfactors_auth/__init__.py | 0 sf_auth.py => src/successfactors_auth/auth.py | 7 +- .../successfactors_auth/sf_saml_template.xml | 0 8 files changed, 191 insertions(+), 170 deletions(-) delete mode 100644 Lambda/Dockerfile delete mode 100644 Lambda/lambda_function.py delete mode 100644 Lambda/requirements.txt create mode 100644 pyproject.toml create mode 100644 src/successfactors_auth/__init__.py rename sf_auth.py => src/successfactors_auth/auth.py (97%) rename sf_saml_template.xml => src/successfactors_auth/sf_saml_template.xml (100%) diff --git a/.gitignore b/.gitignore index c59f099..f665a33 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,165 @@ -sf_private_key.pem \ No newline at end of file +sf_private_key.pem +test.py +auth_test.py +test/* + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/Lambda/Dockerfile b/Lambda/Dockerfile deleted file mode 100644 index f03302f..0000000 --- a/Lambda/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM public.ecr.aws/lambda/python:3.9 - -# Install the function's dependencies using file requirements.txt -# from your project folder. -RUN yum install -y \ -gcc \ -gcc-c++ \ -Cython \ -make \ -libxml2 \ -libxslt \ -xmlsec1 \ -xmlsec1-devel \ -xmlsec1-openssl \ -libtool-ltdl-devel - -COPY requirements.txt . -RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" - -# Copy function code -COPY lambda_function/* ${LAMBDA_TASK_ROOT}/ - -# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) -CMD [ "lambda_function.lambda_handler" ] \ No newline at end of file diff --git a/Lambda/lambda_function.py b/Lambda/lambda_function.py deleted file mode 100644 index 6b36022..0000000 --- a/Lambda/lambda_function.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 - -import base64 -import json -import sys -import os -import requests -import xmlsec -import boto3 -from lxml import etree -from datetime import datetime, timedelta - - -region = os.environ.get('AWS_REGION') -secret_id= os.environ.get('SECRET_ID') -template_file = 'sf_saml_template.xml' -private_keyfile = '/tmp/successfactors-private.pem' - -def get_secret(region, secret_name, session): - - client = session.client( - service_name='secretsmanager', - region_name=region - ) - - try: - get_secret_value_response = client.get_secret_value( - SecretId=secret_name - ) - - if 'SecretString' in get_secret_value_response: - secret = get_secret_value_response['SecretString'] - - except Exception as x: - print(x) - sys.exit(1) - - return secret - - -def get_access_token(sf_url, company_id, client_id, assertion): - - token_request = dict( - client_id=client_id, - company_id=company_id, - grant_type='urn:ietf:params:oauth:grant-type:saml2-bearer', - assertion=assertion - ) - response = requests.post(f"{sf_url}/oauth/token", data=token_request) - token_data = response.json() - return token_data['access_token'] - - -def generate_assertion(sf_root_url, user_id, client_id, template_file): - issue_instant = datetime.utcnow() - auth_instant = issue_instant - not_valid_before = issue_instant - timedelta(minutes=10) - not_valid_after = issue_instant + timedelta(minutes=10) - - audience = 'www.successfactors.com' - - context = dict( - issue_instant=issue_instant.isoformat(), - auth_instant=auth_instant.isoformat(), - not_valid_before=not_valid_before.isoformat(), - not_valid_after=not_valid_after.isoformat(), - sf_root_url=sf_root_url, - audience=audience, - user_id=user_id, - client_id=client_id, - session_id='mock_session_index', - ) - saml_template = open(template_file).read() - - return saml_template.format(**context) - - -def sign_assertion(xml_string, private_key): - key = xmlsec.Key.from_file(private_key, xmlsec.KeyFormat.PEM) - - root = etree.fromstring(xml_string) - signature_node = xmlsec.tree.find_node(root, xmlsec.Node.SIGNATURE) - - sign_context = xmlsec.SignatureContext() - sign_context.key = key - sign_context.sign(signature_node) - - return etree.tostring(root) - - -def auth(sf_url, sf_company_id, sf_oauth_client_id, - sf_admin_user, sf_saml_private_key, template_file): - - unsigned_assertion = generate_assertion(sf_url, - sf_admin_user, - sf_oauth_client_id, - template_file) - - signed_assertion = sign_assertion(unsigned_assertion, sf_saml_private_key) - signed_assertion_b64 = base64.b64encode(signed_assertion).replace(b'\n', b'') - access_token = get_access_token(sf_url, - sf_company_id, - sf_oauth_client_id, - signed_assertion_b64) - - return access_token - - -def lambda_handler(event, context): - - session = boto3.session.Session() - - print(event) - - if event['rawPath'] == '/token': - body = json.loads(event['body']) - sf_url = body['odata_url'] - sf_company_id = body['company_id'] - sf_oauth_client_id = body['oauth_client_id'] - sf_admin_user = body['admin_user'] - - private_key = get_secret(region, - secret_id, - session) - - with open(private_keyfile, 'w') as f: - f.write(private_key) - - token = auth(sf_url, sf_company_id, sf_oauth_client_id, sf_admin_user, - private_keyfile, template_file) - - payload = { - "token": token - } - - return { - 'statusCode': 200, - 'body': json.dumps(payload) - } diff --git a/Lambda/requirements.txt b/Lambda/requirements.txt deleted file mode 100644 index 3e0594e..0000000 --- a/Lambda/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -lxml -xmlsec \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0546fae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "successfactors_auth" +version = "0.0.2" +authors = [ + { name="Ray Lyon", email="ray@raylyon.net" }, +] +description = "Authenticate to the SuccessFactors API." +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://github.com/skoobasteeve/successfactors-python" +"Bug Tracker" = "https://github.com/skoobasteeve/successfactors-python/issues" \ No newline at end of file diff --git a/src/successfactors_auth/__init__.py b/src/successfactors_auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_auth.py b/src/successfactors_auth/auth.py similarity index 97% rename from sf_auth.py rename to src/successfactors_auth/auth.py index 72447df..0013379 100644 --- a/sf_auth.py +++ b/src/successfactors_auth/auth.py @@ -11,7 +11,6 @@ Bearer token. Derived from: https://github.com/mtrdesign/python-saml-example This script requires the following additional files: --SAML template XML -Private key file for a previously created SuccessFactors OAuth2 application Required packages: @@ -28,13 +27,13 @@ token = sf_auth.auth( SF_OAUTH_CLIENT_ID, SF_ADMIN_USER, SF_OAUTH_PRIVATE_KEY_FILE, - SAML_TEMPLATE_FILE ) ''' import base64 import requests import xmlsec +import importlib.resources from lxml import etree from datetime import datetime, timedelta @@ -103,7 +102,9 @@ def sign_assertion(xml_string, private_key): def auth(sf_url, sf_company_id, sf_oauth_client_id, - sf_admin_user, sf_saml_private_key, template_file): + sf_admin_user, sf_saml_private_key): + + template_file = "sf_saml_template.xml" # Generate SAML assertion XML from template file unsigned_assertion = generate_assertion(sf_url, diff --git a/sf_saml_template.xml b/src/successfactors_auth/sf_saml_template.xml similarity index 100% rename from sf_saml_template.xml rename to src/successfactors_auth/sf_saml_template.xml From ad9a5b7e0a567ac6e28587f6c1f4f5271ecda7b3 Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Sat, 5 Aug 2023 14:27:34 +0000 Subject: [PATCH 3/7] include template in package --- pyproject.toml | 2 +- src/successfactors_auth/auth.py | 6 ++++-- .../{ => templates}/sf_saml_template.xml | 0 3 files changed, 5 insertions(+), 3 deletions(-) rename src/successfactors_auth/{ => templates}/sf_saml_template.xml (100%) diff --git a/pyproject.toml b/pyproject.toml index 0546fae..55d4187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "successfactors_auth" -version = "0.0.2" +version = "0.0.3" authors = [ { name="Ray Lyon", email="ray@raylyon.net" }, ] diff --git a/src/successfactors_auth/auth.py b/src/successfactors_auth/auth.py index 0013379..72f7ac4 100644 --- a/src/successfactors_auth/auth.py +++ b/src/successfactors_auth/auth.py @@ -33,9 +33,11 @@ token = sf_auth.auth( import base64 import requests import xmlsec -import importlib.resources from lxml import etree from datetime import datetime, timedelta +from importlib import resources as impresources + +from . import templates # Send POST request to SuccessFactors containing the generated @@ -104,7 +106,7 @@ def sign_assertion(xml_string, private_key): def auth(sf_url, sf_company_id, sf_oauth_client_id, sf_admin_user, sf_saml_private_key): - template_file = "sf_saml_template.xml" + template_file = (impresources.files(templates) / 'sf_saml_template.xml') # Generate SAML assertion XML from template file unsigned_assertion = generate_assertion(sf_url, diff --git a/src/successfactors_auth/sf_saml_template.xml b/src/successfactors_auth/templates/sf_saml_template.xml similarity index 100% rename from src/successfactors_auth/sf_saml_template.xml rename to src/successfactors_auth/templates/sf_saml_template.xml From 75b0fedbd586c463f5abb8aea83049f060a41953 Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Sat, 5 Aug 2023 14:43:00 +0000 Subject: [PATCH 4/7] linting --- src/successfactors_auth/auth.py | 52 +++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/successfactors_auth/auth.py b/src/successfactors_auth/auth.py index 72f7ac4..831866a 100644 --- a/src/successfactors_auth/auth.py +++ b/src/successfactors_auth/auth.py @@ -10,18 +10,20 @@ Bearer token. Derived from: https://github.com/mtrdesign/python-saml-example -This script requires the following additional files: +This module requires the following additional files: -Private key file for a previously created SuccessFactors OAuth2 application Required packages: -pip install requests lxml xmlsec +requests +lxml +xmlsec Example: #!/usr/bin/env python3 -import sf_auth +import successfactors_auth -token = sf_auth.auth( +token = successfactors_auth.auth( SF_URL, SF_COMPANY_ID, SF_OAUTH_CLIENT_ID, @@ -31,19 +33,21 @@ token = sf_auth.auth( ''' import base64 +from datetime import datetime, timedelta +from importlib import resources as impresources import requests import xmlsec from lxml import etree -from datetime import datetime, timedelta -from importlib import resources as impresources from . import templates -# Send POST request to SuccessFactors containing the generated -# SAML assertion and other details, then receive a token in response -def get_access_token(sf_url, company_id, client_id, assertion): - +def get_access_token(sf_url: str, company_id: str, client_id: str, + assertion: str) -> str: + """ + Send POST request to SuccessFactors containing the generated + SAML assertion and other details, then receive a token in response + """ # Request body token_request = dict( client_id=client_id, @@ -51,13 +55,17 @@ def get_access_token(sf_url, company_id, client_id, assertion): grant_type='urn:ietf:params:oauth:grant-type:saml2-bearer', assertion=assertion ) - response = requests.post(f"{sf_url}/oauth/token", data=token_request) + response = requests.post(f"{sf_url}/oauth/token", data=token_request, + timeout=15) token_data = response.json() return token_data['access_token'] -# Generate SAML assertion from the template XML -def generate_assertion(sf_root_url, user_id, client_id, template_file): +def generate_assertion(sf_root_url: str, user_id: str, client_id: str, + template_file: str) -> str: + """ + Generate SAML assertion from the template XML + """ # Calculate valid time values for the assertion's validity issue_instant = datetime.utcnow() auth_instant = issue_instant @@ -79,14 +87,16 @@ def generate_assertion(sf_root_url, user_id, client_id, template_file): session_id='mock_session_index', ) # Open the template file - saml_template = open(template_file).read() + saml_template = open(template_file, encoding="utf-8").read() # Fill the values into the template and return in return saml_template.format(**context) -# Sign the SAML assertion using a private key file -def sign_assertion(xml_string, private_key): +def sign_assertion(xml_string: str, private_key: str) -> str: + """ + Sign the SAML assertion using a private key file + """ # Import key file key = xmlsec.Key.from_file(private_key, xmlsec.KeyFormat.PEM) @@ -103,10 +113,14 @@ def sign_assertion(xml_string, private_key): return etree.tostring(root) -def auth(sf_url, sf_company_id, sf_oauth_client_id, - sf_admin_user, sf_saml_private_key): +def auth(sf_url: str, sf_company_id: str, sf_oauth_client_id: str, + sf_admin_user: str, sf_saml_private_key: str) -> str: + """ + Request an API access token by generating a signed SAML assertion + and using it to authenticate with SuccessFactors. + """ - template_file = (impresources.files(templates) / 'sf_saml_template.xml') + template_file = impresources.files(templates) / 'sf_saml_template.xml' # Generate SAML assertion XML from template file unsigned_assertion = generate_assertion(sf_url, From 62ffe0d4915bf31420d08ecb8c0230aa962260b2 Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Sat, 5 Aug 2023 15:03:00 +0000 Subject: [PATCH 5/7] update readme --- README.md | 75 +++++++++++++++++++++++++++++--------------------- pyproject.toml | 6 ++-- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4a1ad18..f755dcb 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,62 @@ -# SuccessFactors + Python -Authenticate with and use the SAP SuccessFactors API with Python. +# SuccessFactors Auth +Authenticate with the SAP SuccessFactors API with OAuth2 and Python. -## Authentcation - -### How to use +## How to use 1. Create an OAuth application in SuccessFactors. 2. Download the private key and copy the Client ID. -3. Clone this repo and copy `sf_auth.py` and `sf_saml_template.xml` into your Python project directory. -4. Import `sf_auth` into your project. -5. Install all the requirements listed in `requirements.txt` in this repo. -6. Call the `sf_auth.auth()` function in your Python project. You'll need to pass the following parameters: +3. Install the Python module: + ``` shell + pip install successfactors_auth + ``` +4. Import `successfactors_auth` into your Python >=3.9 project. +5. Call the `successfactors_auth.auth()` function in your Python project. You'll need to pass the following parameters: - `sf_url`: Base API url of your SuccessFactors instance, e.g. "https://api55.sapsf.eu". - `sf_company_id`: SuccessFactors company ID. - `sf_oauth_client_id`: The Client ID for the OAuth application you created earlier. - `sf_admin_user`: An admin user in SuccessFactors that has access to the OAuth application. - `sf_saml_private_key`: Path to the private key file you downloaded when you created the OAuth application. - - `template_file`: Path to the template file from this repo. - Example: - ``` python - #!/usr/bin/env python +### Example - import requests - import sf_auth +``` python +#!/usr/bin/env python - sf_url = 'https://your.base.url.com' - sf_company_id = 'your-company-id' - sf_oauth_client_id = 'OAUTH-CLIENT-ID' - sf_admin_user = 'your_admin_user' - sf_saml_private_key = 'your_app_private_key.pem' - template_file = 'sf_saml_template.xml' +import requests +import successfactors_auth - token = sf_auth.auth(sf_url, sf_company_id, sf_oauth_client_id, sf_admin_user, sf_saml_private_key, sf_saml_template) +sf_url = 'https://your.base.url.com' +sf_company_id = 'your-company-id' +sf_oauth_client_id = 'your_app_client_id' +sf_admin_user = 'your_admin_user' +sf_saml_private_key = 'your_app_private_key.pem' - headers = { - "Accept: application/json", - f"Authorization: {token}" - } +token = successfactors_auth.auth( + sf_url, + sf_company_id, + sf_oauth_client_id, + sf_admin_user, + sf_saml_private_key, + sf_saml_template + ) - request = requests.get(f"{sf_url}/User", headers=headers) +headers = { + "Accept: application/json", + f"Authorization: {token}" +} - print(request.json()) - ``` +request = requests.get(f"{sf_url}/User", headers=headers) +user = request.json() -### Using as an AWS Lambda function w/ API Gateway +print(user) +``` -Coming soon... \ No newline at end of file +## Background + +I wrote this module because I was forced to deal with the *horrific* SAP SuccessFactors API at my job, and I wanted to make sure other devs/sysadmins wouldn't have to feel the pain that I felt. + +Once you get authenticated, getting the information you want is a whole new level of suffering. I hope to publish some more examples in the form of a blog post or docs in this repo. + +## Contributing + +All contributions welcome! Feel free to file an [issue](https://github.com/skoobasteeve/successfactors_auth/issues) or open a [pull request](https://github.com/skoobasteeve/successfactors_auth/pulls). \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 55d4187..643bc71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "hatchling.build" [project] name = "successfactors_auth" -version = "0.0.3" +version = "0.0.4" authors = [ { name="Ray Lyon", email="ray@raylyon.net" }, ] description = "Authenticate to the SuccessFactors API." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", @@ -19,4 +19,4 @@ classifiers = [ [project.urls] "Homepage" = "https://github.com/skoobasteeve/successfactors-python" -"Bug Tracker" = "https://github.com/skoobasteeve/successfactors-python/issues" \ No newline at end of file +"Bug Tracker" = "https://github.com/skoobasteeve/successfactors-python/issues" From d5dd13bd096890a3a01571347df4b0cbf608311a Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Sat, 5 Aug 2023 15:04:41 +0000 Subject: [PATCH 6/7] rev --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 643bc71..05d1e93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "successfactors_auth" -version = "0.0.4" +version = "0.0.5" authors = [ { name="Ray Lyon", email="ray@raylyon.net" }, ] From fc7b910a9d2e0197994e0c94a650d1f80ead4abe Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Sat, 5 Aug 2023 15:14:59 +0000 Subject: [PATCH 7/7] update repo link --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05d1e93..e76af11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,5 +18,5 @@ classifiers = [ ] [project.urls] -"Homepage" = "https://github.com/skoobasteeve/successfactors-python" -"Bug Tracker" = "https://github.com/skoobasteeve/successfactors-python/issues" +"Homepage" = "https://github.com/skoobasteeve/successfactors_auth" +"Bug Tracker" = "https://github.com/skoobasteeve/successfactors_auth/issues"