Skip to content

Commit

Permalink
Refactor: AuthProvider to ClaimsProvider (#2256)
Browse files Browse the repository at this point in the history
  • Loading branch information
angela-tran authored Aug 5, 2024
2 parents 63e24ca + 7c042a5 commit 9ce852c
Show file tree
Hide file tree
Showing 27 changed files with 336 additions and 223 deletions.
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
111 changes: 111 additions & 0 deletions benefits/core/migrations/0017_refactor_authprovider_claimsprovider.py
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",
"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

0 comments on commit 9ce852c

Please sign in to comment.