Skip to content

Implement Social Auth #18037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ module = [
"pyqrcode.*",
"requests_aws4auth.*", # https://github.com/tedder/requests-aws4auth/issues/53
"rfc3986.*", # https://github.com/python-hyper/rfc3986/issues/122
"social_pyramid.models",
"transaction.*",
"ua_parser.*", # https://github.com/ua-parser/uap-python/issues/110
"venusian.*",
Expand Down
1 change: 1 addition & 0 deletions requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ rfc3986
sentry-sdk
setuptools
pypi-attestations==0.0.25
social-auth-app-pyramid
sqlalchemy[asyncio]>=2.0,<3.0
stdlib-list
stripe
Expand Down
42 changes: 42 additions & 0 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ cryptography==44.0.2 \
# pypi-attestations
# rfc3161-client
# sigstore
# social-auth-core
# webauthn
cssselect==1.3.0 \
--hash=sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d \
Expand All @@ -530,6 +531,12 @@ datadog==0.51.0 \
--hash=sha256:3279534f831ae0b4ae2d8ce42ef038b4ab38e667d7ed6ff7437982d7a0cf5250 \
--hash=sha256:a9764f091c96af4e0996d4400b168fc5fba380f911d6d672c9dcd4773e29ea3f
# via -r requirements/main.in
defusedxml==0.7.1 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
# via
# python3-openid
# social-auth-core
deprecated==1.2.18 \
--hash=sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d \
--hash=sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec
Expand Down Expand Up @@ -1458,6 +1465,12 @@ nh3==0.2.21 \
# via
# -r requirements/main.in
# readme-renderer
oauthlib==3.2.2 \
--hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \
--hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918
# via
# requests-oauthlib
# social-auth-core
openapi-core==0.19.5 \
--hash=sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3 \
--hash=sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f
Expand Down Expand Up @@ -1790,6 +1803,7 @@ pyjwt[crypto]==2.10.1 \
# -r requirements/main.in
# pyjwt
# sigstore
# social-auth-core
pymacaroons==0.13.0 \
--hash=sha256:1e6bba42a5f66c245adf38a5a4006a99dcc06a0703786ea636098667d42903b8 \
--hash=sha256:3e14dff6a262fdbf1a15e769ce635a8aea72e6f8f91e408f9a97166c53b91907
Expand Down Expand Up @@ -1883,6 +1897,10 @@ python-slugify==8.0.4 \
--hash=sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8 \
--hash=sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856
# via -r requirements/main.in
python3-openid==3.2.0 \
--hash=sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf \
--hash=sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b
# via social-auth-core
pytz==2025.2 \
--hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
--hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
Expand Down Expand Up @@ -1983,7 +2001,9 @@ requests==2.32.3 \
# pypi-attestations
# requests-aws4auth
# requests-file
# requests-oauthlib
# sigstore
# social-auth-core
# stripe
# tldextract
requests-aws4auth==1.3.1 \
Expand All @@ -1994,6 +2014,10 @@ requests-file==2.1.0 \
--hash=sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658 \
--hash=sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c
# via tldextract
requests-oauthlib==2.0.0 \
--hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \
--hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9
# via social-auth-core
rfc3161-client==1.0.1 \
--hash=sha256:081211a1b602b6dff7feb314d39ca2229c8db4e8cf55eef0c35b460470f4b2bb \
--hash=sha256:0d3db059fe08d8b6b06aff89e133fcc352ffea1a1dafadb116dda9dae59d0689 \
Expand Down Expand Up @@ -2183,6 +2207,23 @@ six==1.17.0 \
# pymacaroons
# python-dateutil
# rfc3339-validator
# social-auth-app-pyramid
# social-auth-storage-sqlalchemy
social-auth-app-pyramid==2.0.0 \
--hash=sha256:1bf21f0ff51a338cd6ca944d49509d042903dbae76b51bd54ce6c44eea9906b4 \
--hash=sha256:6f3a0f35ad0d226c7d23bec1e17790e36a7758aed3946a79cd94e1f917a6dbe0
# via -r requirements/main.in
social-auth-core==4.6.0 \
--hash=sha256:6d940c458c529d3689f2ceca2d944911a4b02d76e82d154688d1d3f75f12d168 \
--hash=sha256:ecf9ae1e2e5bb52741cedcaede943fe89a8ada23f9aad018f350d4839a9d3a90
# via
# social-auth-app-pyramid
# social-auth-storage-sqlalchemy
social-auth-storage-sqlalchemy==1.1.0 \
--hash=sha256:0f408106bacf22794628e42d95e104f29044cf21e7b894c3913e76d2ec0eaa3b \
--hash=sha256:3598835f33719e76a846eac69f1f49820f4f2c8edc86cc6479d5099457ec6121 \
--hash=sha256:6ccfe0502e07086eccdb606875106d7e2a29b9804d3c7b31d0f53ba6a50e8e29
# via social-auth-app-pyramid
sqlalchemy[asyncio]==2.0.40 \
--hash=sha256:00a494ea6f42a44c326477b5bee4e0fc75f6a80c01570a32b57e89cf0fbef85a \
--hash=sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d \
Expand Down Expand Up @@ -2246,6 +2287,7 @@ sqlalchemy[asyncio]==2.0.40 \
# alembic
# alembic-postgresql-enum
# paginate-sqlalchemy
# social-auth-storage-sqlalchemy
# zope-sqlalchemy
stdlib-list==0.11.1 \
--hash=sha256:9029ea5e3dfde8cd4294cfd4d1797be56a67fc4693c606181730148c3fd1da29 \
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def __init__(self):
whitenoise_serve_static=pretend.call_recorder(lambda *a, **kw: None),
whitenoise_add_files=pretend.call_recorder(lambda *a, **kw: None),
whitenoise_add_manifest=pretend.call_recorder(lambda *a, **kw: None),
scan=pretend.call_recorder(lambda categories, ignore: None),
scan=pretend.call_recorder(lambda categories, ignore=False: None),
commit=pretend.call_recorder(lambda: None),
add_view_deriver=pretend.call_recorder(lambda *a, **kw: None),
)
Expand Down Expand Up @@ -429,6 +429,7 @@ def __init__(self):
pretend.call(".cache"),
pretend.call(".email"),
pretend.call(".accounts"),
pretend.call("social_pyramid"),
pretend.call(".macaroons"),
pretend.call(".oidc"),
pretend.call(".attestations"),
Expand Down Expand Up @@ -520,6 +521,7 @@ def __init__(self):
pretend.call(
categories=(
"pyramid",
"social_pyramid",
"warehouse",
),
ignore=["warehouse.migrations.env", "warehouse.celery", "warehouse.wsgi"],
Expand Down
11 changes: 11 additions & 0 deletions warehouse/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
from pyramid.httpexceptions import HTTPBadRequest
from pyramid.tweens import EXCVIEW
from pyramid_rpc.xmlrpc import XMLRPCRenderer
from social_pyramid.models import init_social

from warehouse.authnz import Permissions
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE, ONE_GIB, ONE_MIB
from warehouse.db import ModelBase, Session, metadata
from warehouse.utils.static import ManifestCacheBuster
from warehouse.utils.wsgi import ProxyFixer, VhmRootRemover

Expand Down Expand Up @@ -821,6 +823,14 @@ def configure(settings=None):
# Register our authentication support.
config.include(".accounts")

# https://python-social-auth.readthedocs.io/en/latest/configuration/settings.html
config.include("social_pyramid")
config.registry.settings["SOCIAL_AUTH_USER_MODEL"] = (
"warehouse.accounts.models.User"
)
if "social_auth_usersocialauth" not in metadata.tables:
init_social(config, ModelBase, Session)

# Register support for Macaroon based authentication
config.include(".macaroons")

Expand Down Expand Up @@ -951,6 +961,7 @@ def configure(settings=None):
config.scan(
categories=(
"pyramid",
"social_pyramid",
"warehouse",
),
ignore=["warehouse.migrations.env", "warehouse.celery", "warehouse.wsgi"],
Expand Down
112 changes: 112 additions & 0 deletions warehouse/migrations/versions/fcb2e13374bb_social_auth_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
social auth models

Revision ID: fcb2e13374bb
Revises: c8384ca429fc
Create Date: 2025-04-27 16:01:36.308879
"""

import social_sqlalchemy
import sqlalchemy as sa

from alembic import op

revision = "fcb2e13374bb"
down_revision = "13c1c0ac92e9"


def upgrade():
op.create_table(
"social_auth_association",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("server_url", sa.String(length=255), nullable=True),
sa.Column("handle", sa.String(length=255), nullable=True),
sa.Column("secret", sa.String(length=255), nullable=True),
sa.Column("issued", sa.Integer(), nullable=True),
sa.Column("lifetime", sa.Integer(), nullable=True),
sa.Column("assoc_type", sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("server_url", "handle"),
)
op.create_table(
"social_auth_code",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(length=200), nullable=True),
sa.Column("code", sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("code", "email"),
)
op.create_index(
op.f("ix_social_auth_code_code"), "social_auth_code", ["code"], unique=False
)
op.create_table(
"social_auth_nonce",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("server_url", sa.String(length=255), nullable=True),
sa.Column("timestamp", sa.Integer(), nullable=True),
sa.Column("salt", sa.String(length=40), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("server_url", "timestamp", "salt"),
)
op.create_table(
"social_auth_partial",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("token", sa.String(length=32), nullable=True),
sa.Column("data", social_sqlalchemy.storage.JSONType(), nullable=True),
sa.Column("next_step", sa.Integer(), nullable=True),
sa.Column("backend", sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_social_auth_partial_token"),
"social_auth_partial",
["token"],
unique=False,
)
op.create_table(
"social_auth_usersocialauth",
sa.Column("uid", sa.String(length=255), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("provider", sa.String(length=32), nullable=True),
sa.Column("extra_data", social_sqlalchemy.storage.JSONType(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("provider", "uid"),
)
op.create_index(
op.f("ix_social_auth_usersocialauth_user_id"),
"social_auth_usersocialauth",
["user_id"],
unique=False,
)


def downgrade():
op.drop_index(
op.f("ix_social_auth_usersocialauth_user_id"),
table_name="social_auth_usersocialauth",
)
op.drop_table("social_auth_usersocialauth")
op.drop_index(
op.f("ix_social_auth_partial_token"), table_name="social_auth_partial"
)
op.drop_table("social_auth_partial")
op.drop_table("social_auth_nonce")
op.drop_index(op.f("ix_social_auth_code_code"), table_name="social_auth_code")
op.drop_table("social_auth_code")
op.drop_table("social_auth_association")
27 changes: 27 additions & 0 deletions warehouse/templates/manage/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,33 @@ <h2>{% trans %}API tokens{% endtrans %}</h2>

<hr>

<section id="account-associations">
<h2>{% trans %}Third-Party Account Associations{% endtrans %}</h2>
<p>
{% trans %}
Associating Third-Party Accounts with your PyPI User provides a mechanism for PyPI Support and Administrators
to validate your identity in the event that you lose access to your email, 2FA, or recovery codes.
{% endtrans %}
</p>
<p>
{% trans %}
These associations <b>do not permit login to PyPI</b> and are <i>only</i> used for verification.
{% endtrans %}
</p>

{% for provider in social_auth_providers_available %}

{% endfor %}

<a href="#" class="button button--primary"><i class="fa-brands fa-github"></i> GitHub</a>
<a href="#" class="button button--primary"><i class="fa-brands fa-gitlab"></i> GitLab</a>
<a href="#" class="button button--primary"><i class="fa-brands fa-bitbucket"></i> Bitbucket</a>
<a href="#" class="button button--primary"><i class="fa-solid fa-mug-saucer"></i> Gitea</a>

</section>

<hr>

<section id="account-events">
<h2>{% trans %}Security history{% endtrans %}</h2>

Expand Down
Loading