Skip to content
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

Refactor: AuthProvider to ClaimsProvider #2256

Merged
merged 9 commits into from
Aug 5, 2024
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DJANGO_DB_DIR=.
DJANGO_DB_FILE=django.db
DJANGO_DB_FIXTURES="benefits/core/migrations/local_fixtures.json"

auth_provider_client_id=benefits-oauth-client-id
claims_provider_client_id=benefits-oauth-client-id
agency_card_verifier_api_auth_key=server-auth-token
client_private_key='-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1pt0ZoOuPEVPJJS+5r884zcjZLkZZ2GcPwr79XOLDbOi46on\nCa79kjRnhS0VUK96SwUPS0z9J5mDA5LSNL2RoxFb5QGaevnJY828NupzTNdUd0sY\nJK3kRjKUggHWuB55hwJcH/Dx7I3DNH4NL68UAlK+VjwJkfYPrhq/bl5z8ZiurvBa\n5C1mDxhFpcTZlCfxQoas7D1d+uPACF6mEMbQNd3RaIaSREO50NvNywXIIt/OmCiR\nqI7JtOcn4eyh1I4j9WtlbMhRJLfwPMAgY5epTsWcURmhVofF2wVoFbib3JGCfA7t\nz/gmP5YoEKnf/cumKmF3e9LrZb8zwm7bTHUViwIDAQABAoIBAQCIv0XMjNvZS9DC\nXoXGQtVpcxj6dXfaiDgnc7hZDubsNCr3JtT5NqgdIYdVNQUABNDIPNEiCkzFjuwM\nuuF2+dRzM/x6UCs/cSsCjXYBCCOwMwV/fjpEJQnwMQqwTLulVsXZYYeSUtXVBf/8\n0tVULRty34apLFhsyX30UtboXQdESfpmm5ZsqsZJlYljw+M7JxRMneQclI19y/ya\nhPWlfhLB9OffVEJXGaWx1NSYnKoCMKqE/+4krROr6V62xXaNyX6WtU6XiT7C6R5A\nPBxfhmoeFdVCF6a+Qq0v2fKThYoZnV4sn2q2An9YPfynFYnlgzdfnAFSejsqxQd0\nfxYLOtMBAoGBAP1jxjHDJngZ1N+ymw9MIpRgr3HeuMP5phiSTbY2tu9lPzQd+TMX\nfhr1bQh2Fd/vU0u7X0yPnTWtUrLlCdGnWPpXivx95GNGgUUIk2HStFdrRx+f2Qvk\nG8vtLgmSbjQ26UiHzxi9Wa0a41PWIA3TixkcFrS2X29Qc4yd6pVHmicfAoGBANjR\nZ8aaDkSKLkq5Nk1T7I0E1+mtPoH1tPV/FJClXjJrvfDuYHBeOyUpipZddnZuPGWA\nIW2tFIsMgJQtgpvgs52NFI7pQGJRUPK/fTG+Ycocxo78TkLr/RIj8Kj5brXsbZ9P\n3/WBX5GAISTSp1ab8xVgK/Tm07hGupKVqnY2lCAVAoGAIql0YjhE2ecGtLcU+Qm8\nLTnwpg4GjmBnNTNGSCfB7IuYEsQK489R49Qw3xhwM5rkdRajmbCHm+Eiz+/+4NwY\nkt5I1/NMu7vYUR40MwyEuPSm3Q+bvEGu/71pL8wFIUVlshNJ5CN60fA8qqo+5kVK\n4Ntzy7Kq6WpC9Dhh75vE3ZcCgYEAty99uXtxsJD6+aEwcvcENkUwUztPQ6ggAwci\nje9Z/cmwCj6s9mN3HzfQ4qgGrZsHpk4ycCK655xhilBFOIQJ3YRUKUaDYk4H0YDe\nOsf6gTP8wtQDH2GZSNlavLk5w7UFDYQD2b47y4fw+NaOEYvjPl0p5lmb6ebAPZb8\nFbKZRd0CgYBC1HTbA+zMEqDdY4MWJJLC6jZsjdxOGhzjrCtWcIWEGMDF7oDDEoix\nW3j2hwm4C6vaNkH9XX1dr5+q6gq8vJQdbYoExl22BGMiNbfI3+sLRk0zBYL//W6c\ntSREgR4EjosqQfbkceLJ2JT1wuNjInI0eR9H3cRugvlDTeWtbdJ5qA==\n-----END RSA PRIVATE KEY-----'
client_public_key='-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pt0ZoOuPEVPJJS+5r88\n4zcjZLkZZ2GcPwr79XOLDbOi46onCa79kjRnhS0VUK96SwUPS0z9J5mDA5LSNL2R\noxFb5QGaevnJY828NupzTNdUd0sYJK3kRjKUggHWuB55hwJcH/Dx7I3DNH4NL68U\nAlK+VjwJkfYPrhq/bl5z8ZiurvBa5C1mDxhFpcTZlCfxQoas7D1d+uPACF6mEMbQ\nNd3RaIaSREO50NvNywXIIt/OmCiRqI7JtOcn4eyh1I4j9WtlbMhRJLfwPMAgY5ep\nTsWcURmhVofF2wVoFbib3JGCfA7tz/gmP5YoEKnf/cumKmF3e9LrZb8zwm7bTHUV\niwIDAQAB\n-----END PUBLIC KEY-----'
Expand Down
6 changes: 3 additions & 3 deletions benefits/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
admin.site.register(models.PemData)


@admin.register(models.AuthProvider)
class AuthProviderAdmin(admin.ModelAdmin): # pragma: no cover
@admin.register(models.ClaimsProvider)
class ClaimsProviderAdmin(admin.ModelAdmin): # pragma: no cover
def get_exclude(self, request, obj=None):
if not request.user.is_superuser:
return ["client_id_secret_name"]
Expand Down Expand Up @@ -80,7 +80,7 @@ def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return [
"api_url",
"auth_provider",
"claims_provider",
"selection_label_template",
"start_template",
"unverified_template",
Expand Down
6 changes: 3 additions & 3 deletions benefits/core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ def authentication(request):
"logged_in": session.logged_in(request),
}

if verifier.is_auth_required:
data["sign_out_button_template"] = verifier.auth_provider.sign_out_button_template
data["sign_out_link_template"] = verifier.auth_provider.sign_out_link_template
if verifier.uses_claims_verification:
data["sign_out_button_template"] = verifier.claims_provider.sign_out_button_template
data["sign_out_link_template"] = verifier.claims_provider.sign_out_link_template

return {"authentication": data}
else:
Expand Down
4 changes: 2 additions & 2 deletions benefits/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ class LoginRequired(MiddlewareMixin):
"""Middleware that checks whether a user is logged in."""

def process_view(self, request, view_func, view_args, view_kwargs):
# only require login if verifier requires it
# only require login if verifier uses claims verification
verifier = session.verifier(request)
if not verifier or not verifier.is_auth_required or session.logged_in(request):
if not verifier or not verifier.uses_claims_verification or session.logged_in(request):
# pass through
return None

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Generated by Django 5.0.7 on 2024-08-02 22:52

from django.contrib.auth.management import create_permissions
from django.db import migrations, models

import benefits.core.models
import benefits.secrets


def create_all_permissions(apps, schema_editor):
for app_config in apps.get_app_configs():
app_config.models_module = True
create_permissions(app_config, apps=apps, verbosity=0)
app_config.models_module = None


def update_permissions(apps, schema_editor):
# delete old permissions
Permission = apps.get_model("auth", "Permission")
old_permission_names = [
"Can view auth provider",
"Can change auth provider",
"Can add auth provider",
"Can delete auth provider",
]

for name in old_permission_names:
old_permission = Permission.objects.get(name=name)
old_permission.delete()

# add new permissions to staff group
Group = apps.get_model("auth", "Group")
staff_group = Group.objects.get(name="Cal-ITP")

Permission = apps.get_model("auth", "Permission")
new_permission_names = ["Can view claims provider", "Can change claims provider"]

for name in new_permission_names:
new_permission = Permission.objects.get(name=name)
staff_group.permissions.add(new_permission)


class Migration(migrations.Migration):

dependencies = [
("core", "0016_refactor_paymentprocessor_transitprocessor"),
]

operations = [
migrations.RenameModel(
old_name="AuthProvider",
new_name="ClaimsProvider",
),
migrations.RunPython(create_all_permissions), # this is needed to create the new permissions for the renamed model
migrations.RunPython(update_permissions),
migrations.AlterField(
model_name="claimsprovider",
name="authority",
field=models.TextField(help_text="The fully qualified HTTPS domain name for an OAuth authority server"),
),
migrations.AlterField(
model_name="claimsprovider",
name="claim",
field=models.TextField(
blank=True, help_text="The name of the claim (name/value pair) that is used to verify eligibility", null=True
),
),
migrations.AlterField(
model_name="claimsprovider",
name="client_id_secret_name",
field=benefits.core.models.SecretNameField(
help_text="The name of the secret containing the client ID for this claims provider",
max_length=127,
validators=[benefits.secrets.SecretNameValidator()],
),
),
migrations.AlterField(
model_name="claimsprovider",
name="client_name",
field=models.TextField(help_text="Unique identifier used to register this claims provider with Authlib registry"),
),
migrations.AlterField(
model_name="claimsprovider",
name="scheme",
field=models.TextField(help_text="The authentication scheme to use"),
),
migrations.AlterField(
model_name="claimsprovider",
name="scope",
field=models.TextField(
blank=True,
help_text="A space-separated list of identifiers used to specify what access privileges are being requested",
null=True,
),
),
migrations.AlterField(
model_name="claimsprovider",
name="sign_out_button_template",
field=models.TextField(blank=True, help_text="Template that renders sign-out button", null=True),
),
migrations.AlterField(
model_name="claimsprovider",
name="sign_out_link_template",
field=models.TextField(blank=True, help_text="Template that renders sign-out link", null=True),
),
migrations.RenameField(
model_name="eligibilityverifier",
old_name="auth_provider",
new_name="claims_provider",
),
]
20 changes: 10 additions & 10 deletions benefits/core/migrations/local_fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,41 @@
}
},
{
"model": "core.authprovider",
"model": "core.claimsprovider",
thekaveman marked this conversation as resolved.
Show resolved Hide resolved
"pk": 1,
"fields": {
"sign_out_button_template": "core/includes/button--sign-out--login-gov.html",
"sign_out_link_template": "core/includes/link--sign-out--login-gov.html",
"client_name": "senior-benefits-oauth-client-name",
"client_id_secret_name": "auth-provider-client-id",
"client_id_secret_name": "claims-provider-client-id",
"authority": "https://example.com",
"scope": "verify:senior",
"claim": "senior",
"scheme": "dev-cal-itp_benefits"
}
},
{
"model": "core.authprovider",
"model": "core.claimsprovider",
"pk": 2,
"fields": {
"sign_out_button_template": "core/includes/button--sign-out--login-gov.html",
"sign_out_link_template": "core/includes/link--sign-out--login-gov.html",
"client_name": "veteran-benefits-oauth-client-name",
"client_id_secret_name": "auth-provider-client-id",
"client_id_secret_name": "claims-provider-client-id",
"authority": "https://example.com",
"scope": "verify:veteran",
"claim": "veteran",
"scheme": "vagov"
}
},
{
"model": "core.authprovider",
"model": "core.claimsprovider",
"pk": 3,
"fields": {
"sign_out_button_template": "core/includes/button--sign-out--login-gov.html",
"sign_out_link_template": "core/includes/link--sign-out--login-gov.html",
"client_name": "calfresh-benefits-oauth-client-name",
"client_id_secret_name": "auth-provider-client-id",
"client_id_secret_name": "claims-provider-client-id",
"authority": "https://example.com",
"scope": "verify:calfresh",
"claim": "calfresh",
Expand Down Expand Up @@ -128,7 +128,7 @@
"jwe_cek_enc": null,
"jwe_encryption_alg": null,
"jws_signing_alg": null,
"auth_provider": 1,
"claims_provider": 1,
"selection_label_template": "eligibility/includes/selection-label--senior.html",
"start_template": "eligibility/start--senior.html",
"form_class": null
Expand All @@ -149,7 +149,7 @@
"jwe_cek_enc": null,
"jwe_encryption_alg": null,
"jws_signing_alg": null,
"auth_provider": 2,
"claims_provider": 2,
"selection_label_template": "eligibility/includes/selection-label--veteran.html",
"start_template": "eligibility/start--veteran.html",
"form_class": null
Expand All @@ -170,7 +170,7 @@
"jwe_cek_enc": "A256CBC-HS512",
"jwe_encryption_alg": "RSA-OAEP",
"jws_signing_alg": "RS256",
"auth_provider": null,
"claims_provider": null,
"selection_label_template": "eligibility/includes/selection-label--cst-agency-card.html",
"start_template": "eligibility/start--cst-agency-card.html",
"form_class": "benefits.eligibility.forms.CSTAgencyCard",
Expand All @@ -193,7 +193,7 @@
"jwe_cek_enc": null,
"jwe_encryption_alg": null,
"jws_signing_alg": null,
"auth_provider": 3,
"claims_provider": 3,
"selection_label_template": "eligibility/includes/selection-label--calfresh.html",
"start_template": "eligibility/start--calfresh.html",
"form_class": null,
Expand Down
41 changes: 22 additions & 19 deletions benefits/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,26 @@ def data(self):
return secret_data if secret_data is not None else remote_data


class AuthProvider(models.Model):
"""An entity that provides authentication for eligibility verifiers."""
class ClaimsProvider(models.Model):
"""An entity that provides claims for eligibility verification."""

id = models.AutoField(primary_key=True)
sign_out_button_template = models.TextField(null=True, blank=True)
sign_out_link_template = models.TextField(null=True, blank=True)
client_name = models.TextField()
client_id_secret_name = SecretNameField()
authority = models.TextField()
scope = models.TextField(null=True, blank=True)
claim = models.TextField(null=True, blank=True)
scheme = models.TextField()
sign_out_button_template = models.TextField(null=True, blank=True, help_text="Template that renders sign-out button")
sign_out_link_template = models.TextField(null=True, blank=True, help_text="Template that renders sign-out link")
client_name = models.TextField(help_text="Unique identifier used to register this claims provider with Authlib registry")
client_id_secret_name = SecretNameField(
help_text="The name of the secret containing the client ID for this claims provider"
)
authority = models.TextField(help_text="The fully qualified HTTPS domain name for an OAuth authority server")
scope = models.TextField(
null=True,
blank=True,
help_text="A space-separated list of identifiers used to specify what access privileges are being requested",
)
claim = models.TextField(
null=True, blank=True, help_text="The name of the claim (name/value pair) that is used to verify eligibility"
)
scheme = models.TextField(help_text="The authentication scheme to use")

@property
def supports_claims_verification(self):
Expand Down Expand Up @@ -179,7 +187,7 @@ class EligibilityVerifier(models.Model):
jwe_encryption_alg = models.TextField(null=True, blank=True)
# The JWS-compatible signing algorithm
jws_signing_alg = models.TextField(null=True, blank=True)
auth_provider = models.ForeignKey(AuthProvider, on_delete=models.PROTECT, null=True, blank=True)
claims_provider = models.ForeignKey(ClaimsProvider, on_delete=models.PROTECT, null=True, blank=True)
selection_label_template = models.TextField()
start_template = models.TextField(null=True, blank=True)
# reference to a form class used by this Verifier, e.g. benefits.app.forms.FormClass
Expand All @@ -206,14 +214,9 @@ def public_key_data(self):
return self.public_key.data

@property
def is_auth_required(self):
"""True if this Verifier requires authentication. False otherwise."""
return self.auth_provider is not None

@property
def uses_auth_verification(self):
"""True if this Verifier verifies via the auth provider. False otherwise."""
return self.is_auth_required and self.auth_provider.supports_claims_verification
def uses_claims_verification(self):
"""True if this Verifier verifies via the claims provider. False otherwise."""
return self.claims_provider is not None and self.claims_provider.supports_claims_verification

def form_instance(self, *args, **kwargs):
"""Return an instance of this verifier's form, or None."""
Expand Down
2 changes: 1 addition & 1 deletion benefits/eligibility/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def eligibility_from_api(verifier, form, agency):


def eligibility_from_oauth(verifier, oauth_claim, agency):
if verifier.uses_auth_verification and verifier.auth_provider.claim == oauth_claim:
if verifier.uses_claims_verification and verifier.claims_provider.claim == oauth_claim:
return agency.type_names_to_verify(verifier)
else:
return []
2 changes: 1 addition & 1 deletion benefits/eligibility/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def confirm(request):
types_to_verify = agency.type_names_to_verify(verifier)

# GET for OAuth verification
if request.method == "GET" and verifier.uses_auth_verification:
if request.method == "GET" and verifier.uses_claims_verification:
analytics.started_eligibility(request, types_to_verify)

verified_types = verify.eligibility_from_oauth(verifier, session.oauth_claim(request), agency)
Expand Down
4 changes: 2 additions & 2 deletions benefits/enrollment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def reenrollment_error(request):
if eligibility.reenrollment_error_template is None:
raise Exception(f"Re-enrollment error with null template on: {eligibility.label}")

if session.logged_in(request) and verifier.auth_provider.supports_sign_out:
if session.logged_in(request) and verifier.claims_provider.supports_sign_out:
# overwrite origin for a logged in user
# if they click the logout button, they are taken to the new route
session.update(request, origin=reverse(ROUTE_LOGGED_OUT))
Expand Down Expand Up @@ -285,7 +285,7 @@ def success(request):
eligibility = session.eligibility(request)
verifier = session.verifier(request)

if session.logged_in(request) and verifier.auth_provider.supports_sign_out:
if session.logged_in(request) and verifier.claims_provider.supports_sign_out:
# overwrite origin for a logged in user
# if they click the logout button, they are taken to the new route
session.update(request, origin=reverse(ROUTE_LOGGED_OUT))
Expand Down
4 changes: 2 additions & 2 deletions benefits/oauth/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class OAuthEvent(core.Event):
def __init__(self, request, event_type):
super().__init__(request, event_type)
verifier = session.verifier(request)
if verifier and verifier.uses_auth_verification:
self.update_event_properties(auth_provider=verifier.auth_provider.client_name)
if verifier and verifier.uses_claims_verification:
self.update_event_properties(auth_provider=verifier.claims_provider.client_name)


class OAuthErrorEvent(OAuthEvent):
Expand Down
2 changes: 1 addition & 1 deletion benefits/oauth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _authorize_params(scheme):

def _register_provider(oauth_registry, provider):
"""
Register OAuth clients into the given registry, using configuration from AuthProvider model.
Register OAuth clients into the given registry, using configuration from ClaimsProvider model.

Adapted from https://stackoverflow.com/a/64174413.
"""
Expand Down
4 changes: 2 additions & 2 deletions benefits/oauth/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ def process_request(self, request):

verifier = session.verifier(request)

if verifier.uses_auth_verification:
if verifier.uses_claims_verification:
# all good, the chosen verifier is configured correctly
return None
elif not (verifier.api_url or verifier.form_class):
# the chosen verifier doesn't have Eligibility API config OR auth provider config
# the chosen verifier doesn't have Eligibility API config OR claims provider config
# this is likely a misconfiguration on the backend, not a user error
message = f"Verifier with no API or IDP config: {verifier.name} (id={verifier.id})"
analytics.error(request, message=message, operation=request.path)
Expand Down
Loading
Loading