From d1fe56078733c4f187d1f284cfeaefdb069ffdba Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:55:50 -0600 Subject: [PATCH 01/32] WIP change CharFields to TextFields, note missing specs --- apps/dot_ext/models.py | 42 ++++++++++++++++++++++------------ apps/fhir/bluebutton/models.py | 7 ++++++ apps/mymedicare_cb/models.py | 3 ++- apps/pkce/models.py | 2 ++ 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index bec6f10d9..c0cc7e3f0 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -43,9 +43,9 @@ class InternalApplicationLabels(models.Model): - label = models.CharField(max_length=255, default='', unique=True) - slug = models.CharField(max_length=1024, default='', unique=True) - description = models.TextField(max_length=10240, blank=True, default='') + label = models.TextField(max_length=255, default='', unique=True) + slug = models.TextField(max_length=1024, default='', unique=True) + description = models.TextField(blank=True, default='') def __str__(self): return self.label @@ -70,16 +70,18 @@ class Application(AbstractApplication): agree = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - op_tos_uri = models.CharField(default=settings.TOS_URI, blank=True, max_length=512) - op_policy_uri = models.CharField(default='', blank=True, max_length=512) + op_tos_uri = models.TextField(default=settings.TOS_URI, blank=True, max_length=512) + op_policy_uri = models.TextField(default='', blank=True, max_length=512) # oauth2_provider upgraded and there is a breaking change on Application.client_secret field # see migration file 0005_alter_application_client_secret.py # field added to save client_secret in plain text before Application.save() # where the client_secret is hashed ireversible + # TODO spec? client_secret_plain = models.CharField(default='', blank=True, max_length=255) # client_uri is depreciated but will continued to be referenced until it can be removed safely - client_uri = models.URLField( + # TODO validator? + client_uri = models.TextField( default='', blank=True, null=True, @@ -89,7 +91,8 @@ class Application(AbstractApplication): 'For example, https://www.example.org or http://www.example.org .', ) - website_uri = models.URLField( + # TODO possible validator + website_uri = models.TextField( default='', blank=True, null=True, @@ -109,16 +112,16 @@ class Application(AbstractApplication): redirect_uris = models.TextField(help_text=help_text, blank=True) - logo_uri = models.CharField(default='', blank=True, max_length=512, verbose_name='Logo URI') + logo_uri = models.TextField(default='', blank=True, max_length=512, verbose_name='Logo URI') - tos_uri = models.CharField( + tos_uri = models.TextField( default='', blank=True, max_length=512, verbose_name="Client's Terms of Service URI", ) - policy_uri = models.CharField( + policy_uri = models.TextField( default='', blank=True, max_length=512, @@ -126,7 +129,7 @@ class Application(AbstractApplication): help_text='This can be a model privacy notice or other policy document.', ) - software_id = models.CharField( + software_id = models.TextField( default='', blank=True, max_length=128, @@ -136,12 +139,12 @@ class Application(AbstractApplication): contacts = models.TextField( default='', blank=True, - max_length=512, verbose_name="Client's Contacts", help_text='This is typically an email', ) - support_email = models.EmailField(blank=True, null=True) + # TODO possible validator + support_email = models.TextField(blank=True, null=True) # FROM https://stackoverflow.com/questions/19130942/whats-the-best-way-to-store-phone-number-in-django-models phone_regex = RegexValidator( @@ -149,6 +152,7 @@ class Application(AbstractApplication): message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", ) + # TODO support_phone_number = models.CharField(validators=[phone_regex], max_length=17, blank=True, null=True) description = models.TextField( @@ -176,6 +180,7 @@ class Application(AbstractApplication): ) # Type related to data access limits. + # TODO enum? data_access_type = models.CharField( default='THIRTEEN_MONTH', choices=APPLICATION_TYPE_CHOICES, @@ -197,6 +202,7 @@ class Application(AbstractApplication): ), ) + # TODO enum? allowed_auth_type = models.CharField( max_length=40, choices=APPLICATION_AUTH_CHOICES, @@ -384,7 +390,7 @@ class Meta: class ApplicationLabel(models.Model): - name = models.CharField(max_length=255, unique=True) + name = models.TextField(max_length=255, unique=True) slug = models.SlugField(db_index=True, unique=True) description = models.TextField() applications = models.ManyToManyField(Application, blank=True) @@ -460,6 +466,7 @@ class ArchivedToken(models.Model): db_constraint=False, related_name='%(app_label)s_%(class)s', ) + # spec token = models.CharField( max_length=255, unique=True, @@ -486,6 +493,7 @@ class ExpiresIn(models.Model): issued to the user. """ + # TODO spec? key = models.CharField(max_length=64, unique=True) expires_in = models.IntegerField() @@ -523,9 +531,13 @@ class AuthFlowUuid(models.Model): """ auth_uuid = models.UUIDField(primary_key=True, unique=True) + # TODO should this also be TextField? state = models.CharField(max_length=64, null=True, unique=True, db_index=True) + # TODO spec? code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib + # TODO spec? client_id = models.CharField(max_length=100, null=True) + # TODO spec? auth_pkce_method = models.CharField(max_length=16, null=True) created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) @@ -552,6 +564,8 @@ class AuthFlowUuidCopy(models.Model): auth_share_demographic_scopes - Bene demographic sharing choice from consent page/form """ + # TODO same changes as above? + # TODO could this just inherit from the above? auth_uuid = models.UUIDField(primary_key=True, unique=True) state = models.CharField(max_length=64, null=True, unique=True, db_index=True) code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index 63c95739b..3152d4aab 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -90,6 +90,7 @@ class Crosswalk(models.Model): settings.AUTH_USER_MODEL, on_delete=CASCADE, ) + # TODO spec? fhir_id_v2 = models.CharField( max_length=80, null=True, @@ -98,6 +99,7 @@ class Crosswalk(models.Model): db_index=True, validators=[MinLengthValidator(1)], ) + # TODO spec? fhir_id_v3 = models.CharField( max_length=80, null=True, @@ -131,6 +133,7 @@ class Crosswalk(models.Model): db_column='user_mbi_hash', db_index=True, ) + # TODO spec _user_mbi = models.CharField( max_length=11, verbose_name='Unhashed MBI', @@ -226,6 +229,7 @@ class ArchivedCrosswalk(models.Model): create (crosswalk): static method to create an ArchivedCrosswalk from a Crosswalk instance """ + # TODO spec? username = models.CharField( max_length=150, null=False, @@ -234,6 +238,7 @@ class ArchivedCrosswalk(models.Model): db_column='username', db_index=True, ) + # TODO spec? fhir_id_v2 = models.CharField( max_length=80, null=True, @@ -241,6 +246,7 @@ class ArchivedCrosswalk(models.Model): db_column='fhir_id_v2', db_index=True, ) + # TODO spec? fhir_id_v3 = models.CharField( max_length=80, null=True, @@ -272,6 +278,7 @@ class ArchivedCrosswalk(models.Model): db_column='user_mbi_hash', db_index=True, ) + # TODO spec _user_mbi = models.CharField( max_length=11, verbose_name='Unhashed MBI', diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index da84308db..486a99d38 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -440,7 +440,8 @@ def _validate_asserts(logger, log_dict, asserts): class AnonUserState(models.Model): - state = models.CharField(default='', max_length=64, db_index=True) + # TODO should this still have a max length? + state = models.TextField(default='', max_length=64, db_index=True) next_uri = models.TextField(default='') def __str__(self): diff --git a/apps/pkce/models.py b/apps/pkce/models.py index 7e762a3a2..b52c41b39 100644 --- a/apps/pkce/models.py +++ b/apps/pkce/models.py @@ -10,5 +10,7 @@ class CodeChallenge(models.Model): on_delete=models.CASCADE, primary_key=True, ) + # TODO spec challenge = models.CharField(max_length=255, default=None) + # TODO spec challenge_method = models.CharField(max_length=255, default="S256") From 3342d459c44b3563545eee6545f0c5d69e142491 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:05:16 -0600 Subject: [PATCH 02/32] WIP adding specs --- apps/dot_ext/models.py | 11 +++++++---- apps/fhir/bluebutton/models.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index c0cc7e3f0..0e6d911fa 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -76,7 +76,9 @@ class Application(AbstractApplication): # see migration file 0005_alter_application_client_secret.py # field added to save client_secret in plain text before Application.save() # where the client_secret is hashed ireversible + # # TODO spec? + # It appears that 128 is the length that's generated in the code, so why 255? client_secret_plain = models.CharField(default='', blank=True, max_length=255) # client_uri is depreciated but will continued to be referenced until it can be removed safely @@ -152,7 +154,7 @@ class Application(AbstractApplication): message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", ) - # TODO + # TODO what should we do here with the length discrepancy? support_phone_number = models.CharField(validators=[phone_regex], max_length=17, blank=True, null=True) description = models.TextField( @@ -466,7 +468,7 @@ class ArchivedToken(models.Model): db_constraint=False, related_name='%(app_label)s_%(class)s', ) - # spec + # TODO spec token = models.CharField( max_length=255, unique=True, @@ -493,7 +495,8 @@ class ExpiresIn(models.Model): issued to the user. """ - # TODO spec? + # hex digest from sha-256, which is always 64 characters long + # https://csrc.nist.gov/pubs/fips/180-4/upd1/final key = models.CharField(max_length=64, unique=True) expires_in = models.IntegerField() @@ -537,7 +540,7 @@ class AuthFlowUuid(models.Model): code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib # TODO spec? client_id = models.CharField(max_length=100, null=True) - # TODO spec? + # TODO spec? should this be a TextField? auth_pkce_method = models.CharField(max_length=16, null=True) created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index 3152d4aab..06835125b 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -133,7 +133,7 @@ class Crosswalk(models.Model): db_column='user_mbi_hash', db_index=True, ) - # TODO spec + # spec: https://www.cms.gov/training-education/partner-outreach-resources/new-medicare-card/medical-beneficiary-identifiers-mbis _user_mbi = models.CharField( max_length=11, verbose_name='Unhashed MBI', @@ -278,7 +278,7 @@ class ArchivedCrosswalk(models.Model): db_column='user_mbi_hash', db_index=True, ) - # TODO spec + # spec: https://www.cms.gov/training-education/partner-outreach-resources/new-medicare-card/medical-beneficiary-identifiers-mbis _user_mbi = models.CharField( max_length=11, verbose_name='Unhashed MBI', From c3eff5f1d4847b7c7cd46251bf6ba63c178a05f4 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:04:41 -0600 Subject: [PATCH 03/32] align phone number validation between accounts and dot_ext Moved `phone_regex` validator up to the apps level and use it for in both `accounts/models.py` and `dot_ext/models.py`. --- apps/accounts/models.py | 4 +++- apps/dot_ext/models.py | 11 ++--------- apps/validators.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 apps/validators.py diff --git a/apps/accounts/models.py b/apps/accounts/models.py index f03a7850e..96992775c 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -1,4 +1,5 @@ from apps.constants import USER_CHOICES, USER_TYPE_DEV +from apps.validators import phone_regex from apps.accounts.constants import ( AAL_CHOICES, ADDITION, @@ -100,7 +101,8 @@ class UserProfile(models.Model): ) mobile_phone_number = models.CharField( - max_length=12, + validators=[phone_regex], + max_length=16, blank=True, help_text=_('US numbers only.'), ) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 0e6d911fa..7ff734da1 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -9,7 +9,6 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.files.storage import default_storage -from django.core.validators import RegexValidator from django.db import models from django.db.models import Q from django.db.models.signals import ( @@ -27,6 +26,7 @@ from oauth2_provider.settings import oauth2_settings from waffle import switch_is_active +from apps.validators import phone_regex import apps.logging.request_logger as logging from apps.capabilities.models import ProtectedCapability from apps.dot_ext.constants import ( @@ -148,14 +148,7 @@ class Application(AbstractApplication): # TODO possible validator support_email = models.TextField(blank=True, null=True) - # FROM https://stackoverflow.com/questions/19130942/whats-the-best-way-to-store-phone-number-in-django-models - phone_regex = RegexValidator( - regex=r'^\+?1?\d{9,15}$', - message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", - ) - - # TODO what should we do here with the length discrepancy? - support_phone_number = models.CharField(validators=[phone_regex], max_length=17, blank=True, null=True) + support_phone_number = models.CharField(validators=[phone_regex], max_length=16, blank=True, null=True) description = models.TextField( default='', diff --git a/apps/validators.py b/apps/validators.py new file mode 100644 index 000000000..9c074234f --- /dev/null +++ b/apps/validators.py @@ -0,0 +1,10 @@ +from django.core.validators import RegexValidator + + +# FROM https://stackoverflow.com/questions/19130942/whats-the-best-way-to-store-phone-number-in-django-models +# removed the `1?` because E.164 phone numbers can be up to 15 digits long, +# which *includes* the country code +phone_regex = RegexValidator( + regex=r'^\+?\d{9,15}$', + message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", +) From 2aca3692634a89a5d19a3cd3e449f0f01885d2fb Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:15:40 -0600 Subject: [PATCH 04/32] change AuthFlowUuid.state to TextField --- apps/dot_ext/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 7ff734da1..68da4ac21 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -527,8 +527,7 @@ class AuthFlowUuid(models.Model): """ auth_uuid = models.UUIDField(primary_key=True, unique=True) - # TODO should this also be TextField? - state = models.CharField(max_length=64, null=True, unique=True, db_index=True) + state = models.TextField(max_length=64, null=True, unique=True, db_index=True) # TODO spec? code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib # TODO spec? From c7e1ed202e688eaa3bd4b723463b9ca05481e63e Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:16:31 -0600 Subject: [PATCH 05/32] add spec for FHIR IDs --- apps/fhir/bluebutton/models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index 06835125b..3a1d456d6 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -90,7 +90,8 @@ class Crosswalk(models.Model): settings.AUTH_USER_MODEL, on_delete=CASCADE, ) - # TODO spec? + # https://www.hl7.org/fhir/R4/datatypes.html#id + # TODO if spec says 64, why 80? fhir_id_v2 = models.CharField( max_length=80, null=True, @@ -99,7 +100,8 @@ class Crosswalk(models.Model): db_index=True, validators=[MinLengthValidator(1)], ) - # TODO spec? + # https://www.hl7.org/fhir/R4/datatypes.html#id + # TODO if spec says 64, why 80? fhir_id_v3 = models.CharField( max_length=80, null=True, @@ -238,7 +240,8 @@ class ArchivedCrosswalk(models.Model): db_column='username', db_index=True, ) - # TODO spec? + # https://www.hl7.org/fhir/R4/datatypes.html#id + # TODO if spec says 64, why 80? fhir_id_v2 = models.CharField( max_length=80, null=True, @@ -246,7 +249,8 @@ class ArchivedCrosswalk(models.Model): db_column='fhir_id_v2', db_index=True, ) - # TODO spec? + # https://www.hl7.org/fhir/R4/datatypes.html#id + # TODO if spec says 64, why 80? fhir_id_v3 = models.CharField( max_length=80, null=True, From a323841952e8ecdd505b74321eb060f17a7f8239 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:10:17 -0600 Subject: [PATCH 06/32] change fhir_id_v2 and fhir_id_v3 fields to max_length=64 This is to align to the spec. Also, add a TODO comment about something confusing --- apps/fhir/bluebutton/models.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index 3a1d456d6..1678fc1bb 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -91,9 +91,8 @@ class Crosswalk(models.Model): on_delete=CASCADE, ) # https://www.hl7.org/fhir/R4/datatypes.html#id - # TODO if spec says 64, why 80? fhir_id_v2 = models.CharField( - max_length=80, + max_length=64, null=True, unique=False, db_column='fhir_id_v2', @@ -101,9 +100,8 @@ class Crosswalk(models.Model): validators=[MinLengthValidator(1)], ) # https://www.hl7.org/fhir/R4/datatypes.html#id - # TODO if spec says 64, why 80? fhir_id_v3 = models.CharField( - max_length=80, + max_length=64, null=True, unique=False, db_column='fhir_id_v3', @@ -218,7 +216,7 @@ class ArchivedCrosswalk(models.Model): This is performed via code in the '__get_and_update_user()' function in apps/mymedicare_cb/models.py Attributes: - user: auth_user.id + user: auth_user.id TODO but there is no user field in this model below? fhir_id_v2: v1/v2 BFD fhir patient id fhir_id_v3: v3 BFD fhir patient id user_id_type: value is to be set to the type of lookup used MBI or HICN, TODO remove during BB2-3143 @@ -241,18 +239,16 @@ class ArchivedCrosswalk(models.Model): db_index=True, ) # https://www.hl7.org/fhir/R4/datatypes.html#id - # TODO if spec says 64, why 80? fhir_id_v2 = models.CharField( - max_length=80, + max_length=64, null=True, unique=False, db_column='fhir_id_v2', db_index=True, ) # https://www.hl7.org/fhir/R4/datatypes.html#id - # TODO if spec says 64, why 80? fhir_id_v3 = models.CharField( - max_length=80, + max_length=64, null=True, unique=False, db_column='fhir_id_v3', From de00e0243cf27014428ce2460c832ad08a554077 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:28:50 -0600 Subject: [PATCH 07/32] add spec for pkce code challenge, change field length --- apps/pkce/models.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/pkce/models.py b/apps/pkce/models.py index b52c41b39..e96f16b9d 100644 --- a/apps/pkce/models.py +++ b/apps/pkce/models.py @@ -10,7 +10,12 @@ class CodeChallenge(models.Model): on_delete=models.CASCADE, primary_key=True, ) - # TODO spec + # up to 128 characters in plain transformation method, and + # shorter if S256 + # https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 challenge = models.CharField(max_length=255, default=None) - # TODO spec - challenge_method = models.CharField(max_length=255, default="S256") + + # TODO enum? + # either "S256" or "plain" by spec + # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 + challenge_method = models.CharField(max_length=5, default="S256", choices=["S256", "plain"]) From 5b4ddd345dad4ce2c9b74a93fa5cd899a8532037 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:37:04 -0600 Subject: [PATCH 08/32] change length of challenge field to match spec --- apps/pkce/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pkce/models.py b/apps/pkce/models.py index e96f16b9d..c0c2099a7 100644 --- a/apps/pkce/models.py +++ b/apps/pkce/models.py @@ -13,7 +13,7 @@ class CodeChallenge(models.Model): # up to 128 characters in plain transformation method, and # shorter if S256 # https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 - challenge = models.CharField(max_length=255, default=None) + challenge = models.CharField(max_length=128, default=None) # TODO enum? # either "S256" or "plain" by spec From a925d502add4166632541e24b95d16dfdd89bcd4 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:04:56 -0600 Subject: [PATCH 09/32] add note --- apps/dot_ext/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 68da4ac21..9aaa87426 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -529,8 +529,10 @@ class AuthFlowUuid(models.Model): auth_uuid = models.UUIDField(primary_key=True, unique=True) state = models.TextField(max_length=64, null=True, unique=True, db_index=True) # TODO spec? + # matches what is in django rest framework + # https://github.com/django-oauth/django-oauth-toolkit/blob/d422eeab79052c04a91d124f938ddc22c841c38c/oauth2_provider/models.py#L333 code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib - # TODO spec? + # TODO spec? why 100? client_id = models.CharField(max_length=100, null=True) # TODO spec? should this be a TextField? auth_pkce_method = models.CharField(max_length=16, null=True) From afb2a0410edfeb2288f4ffb9c788f85b8cf49e94 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:31:35 -0600 Subject: [PATCH 10/32] add note --- apps/dot_ext/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 9aaa87426..d4abd45d9 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -83,6 +83,7 @@ class Application(AbstractApplication): # client_uri is depreciated but will continued to be referenced until it can be removed safely # TODO validator? + # why not use URLField? Wouldn't that be more explicit? client_uri = models.TextField( default='', blank=True, From 707b62406851d4683bce282f2a8582dac0eccca5 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:49:41 -0600 Subject: [PATCH 11/32] mark places where validators might be helpful --- apps/dot_ext/models.py | 7 +++++++ apps/mymedicare_cb/models.py | 1 + 2 files changed, 8 insertions(+) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index d4abd45d9..36ed0e952 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -70,7 +70,9 @@ class Application(AbstractApplication): agree = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + # TODO validator? op_tos_uri = models.TextField(default=settings.TOS_URI, blank=True, max_length=512) + # TODO validator? op_policy_uri = models.TextField(default='', blank=True, max_length=512) # oauth2_provider upgraded and there is a breaking change on Application.client_secret field # see migration file 0005_alter_application_client_secret.py @@ -115,8 +117,10 @@ class Application(AbstractApplication): redirect_uris = models.TextField(help_text=help_text, blank=True) + # TODO validator? logo_uri = models.TextField(default='', blank=True, max_length=512, verbose_name='Logo URI') + # TODO validator? tos_uri = models.TextField( default='', blank=True, @@ -124,6 +128,7 @@ class Application(AbstractApplication): verbose_name="Client's Terms of Service URI", ) + # TODO validator? policy_uri = models.TextField( default='', blank=True, @@ -206,6 +211,8 @@ class Application(AbstractApplication): ) # New fields for CMS Aligned Networks epic + # TODO validator? + # TODO to TextField? jwks_uri = models.URLField( default=None, blank=True, diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index 486a99d38..4133be7a2 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -442,6 +442,7 @@ def _validate_asserts(logger, log_dict, asserts): class AnonUserState(models.Model): # TODO should this still have a max length? state = models.TextField(default='', max_length=64, db_index=True) + # TODO validator? next_uri = models.TextField(default='') def __str__(self): From 22b2226c6e014e60087c40bfd5b9c3ddc9c93e89 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:08:32 -0600 Subject: [PATCH 12/32] add validators for urls, change jwks_uri to TextField This should make all the models consistently using TextField + URLValidator for URLs --- apps/dot_ext/models.py | 15 +++++++++++---- apps/mymedicare_cb/models.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 36ed0e952..61986a160 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -8,6 +8,7 @@ from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib.auth import get_user_model +from django.core.validators import URLValidator from django.core.files.storage import default_storage from django.db import models from django.db.models import Q @@ -71,9 +72,10 @@ class Application(AbstractApplication): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) # TODO validator? - op_tos_uri = models.TextField(default=settings.TOS_URI, blank=True, max_length=512) + # shouldn't this really be named 'url'? or are we really allowing uri's? + op_tos_uri = models.TextField(default=settings.TOS_URI, blank=True, validators=[URLValidator()], max_length=512) # TODO validator? - op_policy_uri = models.TextField(default='', blank=True, max_length=512) + op_policy_uri = models.TextField(default='', blank=True, max_length=512, validators=[URLValidator()]) # oauth2_provider upgraded and there is a breaking change on Application.client_secret field # see migration file 0005_alter_application_client_secret.py # field added to save client_secret in plain text before Application.save() @@ -90,6 +92,7 @@ class Application(AbstractApplication): default='', blank=True, null=True, + validators=[URLValidator()], max_length=512, verbose_name='Client URI', help_text='This is typically a home/download website for the application. ' @@ -101,6 +104,7 @@ class Application(AbstractApplication): default='', blank=True, null=True, + validators=[URLValidator()], max_length=512, verbose_name='Website URI', help_text='This is typically a home/download website for the application. ' @@ -118,12 +122,13 @@ class Application(AbstractApplication): redirect_uris = models.TextField(help_text=help_text, blank=True) # TODO validator? - logo_uri = models.TextField(default='', blank=True, max_length=512, verbose_name='Logo URI') + logo_uri = models.TextField(default='', blank=True, max_length=512, verbose_name='Logo URI', validators=[URLValidator()]) # TODO validator? tos_uri = models.TextField( default='', blank=True, + validators=[URLValidator()], max_length=512, verbose_name="Client's Terms of Service URI", ) @@ -132,6 +137,7 @@ class Application(AbstractApplication): policy_uri = models.TextField( default='', blank=True, + validators=[URLValidator()], max_length=512, verbose_name="Client's Policy URI", help_text='This can be a model privacy notice or other policy document.', @@ -213,10 +219,11 @@ class Application(AbstractApplication): # New fields for CMS Aligned Networks epic # TODO validator? # TODO to TextField? - jwks_uri = models.URLField( + jwks_uri = models.TextField( default=None, blank=True, null=True, + validators=[URLValidator()], # Went with 512 as that is the established pattern we have for URLFields # and to allow for extremely long URLs. 200 is default max_length=512, diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index 4133be7a2..bab036c3e 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -1,3 +1,4 @@ +from django.core.validators import URLValidator import apps.logging.request_logger as logging from django.contrib.auth.models import User, Group @@ -443,7 +444,7 @@ class AnonUserState(models.Model): # TODO should this still have a max length? state = models.TextField(default='', max_length=64, db_index=True) # TODO validator? - next_uri = models.TextField(default='') + next_uri = models.TextField(default='', validators=[URLValidator()]) def __str__(self): return '%s %s' % (self.state, self.next_uri) From a2e9f8e5acd254dc795803f822170dcea38cba0d Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:11:03 -0600 Subject: [PATCH 13/32] set max_length=512 for next_uri to be consistent This matches all the other URLs --- apps/mymedicare_cb/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index bab036c3e..0022e4074 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -444,7 +444,7 @@ class AnonUserState(models.Model): # TODO should this still have a max length? state = models.TextField(default='', max_length=64, db_index=True) # TODO validator? - next_uri = models.TextField(default='', validators=[URLValidator()]) + next_uri = models.TextField(default='', validators=[URLValidator()], max_length=512) def __str__(self): return '%s %s' % (self.state, self.next_uri) From fe8b1988f0ff355feec2e28d56e563d39384fa8a Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:18:26 -0600 Subject: [PATCH 14/32] fix choices argument --- apps/pkce/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pkce/models.py b/apps/pkce/models.py index c0c2099a7..10010bd2c 100644 --- a/apps/pkce/models.py +++ b/apps/pkce/models.py @@ -18,4 +18,4 @@ class CodeChallenge(models.Model): # TODO enum? # either "S256" or "plain" by spec # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 - challenge_method = models.CharField(max_length=5, default="S256", choices=["S256", "plain"]) + challenge_method = models.CharField(max_length=5, default="S256", choices={"S256": "S256", "plain": "plain"}) From 25790f60716a7b600e7c5a476587b8793cadfa52 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:42:13 -0600 Subject: [PATCH 15/32] add migrations for this branch so far Best practice is to commit migrations in the same commit as the changes to the models, so I will try to do this going forward. --- ...6_alter_userprofile_mobile_phone_number.py | 19 ++++ ...2_alter_application_client_uri_and_more.py | 99 +++++++++++++++++++ ...r_archivedcrosswalk_fhir_id_v2_and_more.py | 34 +++++++ ...4_alter_anonuserstate_next_uri_and_more.py | 24 +++++ ..._alter_codechallenge_challenge_and_more.py | 23 +++++ 5 files changed, 199 insertions(+) create mode 100644 apps/accounts/migrations/0006_alter_userprofile_mobile_phone_number.py create mode 100644 apps/dot_ext/migrations/0012_alter_application_client_uri_and_more.py create mode 100644 apps/fhir/bluebutton/migrations/0012_alter_archivedcrosswalk_fhir_id_v2_and_more.py create mode 100644 apps/mymedicare_cb/migrations/0004_alter_anonuserstate_next_uri_and_more.py create mode 100644 apps/pkce/migrations/0003_alter_codechallenge_challenge_and_more.py diff --git a/apps/accounts/migrations/0006_alter_userprofile_mobile_phone_number.py b/apps/accounts/migrations/0006_alter_userprofile_mobile_phone_number.py new file mode 100644 index 000000000..599ad98ce --- /dev/null +++ b/apps/accounts/migrations/0006_alter_userprofile_mobile_phone_number.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.2 on 2026-04-20 18:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_alter_userprofile_user_type'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='mobile_phone_number', + field=models.CharField(blank=True, help_text='US numbers only.', max_length=16, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", regex='^\\+?\\d{9,15}$')]), + ), + ] diff --git a/apps/dot_ext/migrations/0012_alter_application_client_uri_and_more.py b/apps/dot_ext/migrations/0012_alter_application_client_uri_and_more.py new file mode 100644 index 000000000..fc5857774 --- /dev/null +++ b/apps/dot_ext/migrations/0012_alter_application_client_uri_and_more.py @@ -0,0 +1,99 @@ +# Generated by Django 6.0.2 on 2026-04-20 18:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0011_remove_application_allow_client_credentials_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='client_uri', + field=models.TextField(blank=True, default='', help_text='This is typically a home/download website for the application. For example, https://www.example.org or http://www.example.org .', max_length=512, null=True, validators=[django.core.validators.URLValidator()], verbose_name='Client URI'), + ), + migrations.AlterField( + model_name='application', + name='contacts', + field=models.TextField(blank=True, default='', help_text='This is typically an email', verbose_name="Client's Contacts"), + ), + migrations.AlterField( + model_name='application', + name='jwks_uri', + field=models.TextField(blank=True, default=None, max_length=512, null=True, validators=[django.core.validators.URLValidator()], verbose_name='JSON Web Key Set URI'), + ), + migrations.AlterField( + model_name='application', + name='logo_uri', + field=models.TextField(blank=True, default='', max_length=512, validators=[django.core.validators.URLValidator()], verbose_name='Logo URI'), + ), + migrations.AlterField( + model_name='application', + name='op_policy_uri', + field=models.TextField(blank=True, default='', max_length=512, validators=[django.core.validators.URLValidator()]), + ), + migrations.AlterField( + model_name='application', + name='op_tos_uri', + field=models.TextField(blank=True, default='https://bluebutton.cms.gov/terms', max_length=512, validators=[django.core.validators.URLValidator()]), + ), + migrations.AlterField( + model_name='application', + name='policy_uri', + field=models.TextField(blank=True, default='', help_text='This can be a model privacy notice or other policy document.', max_length=512, validators=[django.core.validators.URLValidator()], verbose_name="Client's Policy URI"), + ), + migrations.AlterField( + model_name='application', + name='software_id', + field=models.TextField(blank=True, default='', help_text='A unique identifier for an application defined by its creator.', max_length=128), + ), + migrations.AlterField( + model_name='application', + name='support_email', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='application', + name='support_phone_number', + field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", regex='^\\+?\\d{9,15}$')]), + ), + migrations.AlterField( + model_name='application', + name='tos_uri', + field=models.TextField(blank=True, default='', max_length=512, validators=[django.core.validators.URLValidator()], verbose_name="Client's Terms of Service URI"), + ), + migrations.AlterField( + model_name='application', + name='website_uri', + field=models.TextField(blank=True, default='', help_text='This is typically a home/download website for the application. For example, https://www.example.org or http://www.example.org .', max_length=512, null=True, validators=[django.core.validators.URLValidator()], verbose_name='Website URI'), + ), + migrations.AlterField( + model_name='applicationlabel', + name='name', + field=models.TextField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name='authflowuuid', + name='state', + field=models.TextField(db_index=True, max_length=64, null=True, unique=True), + ), + migrations.AlterField( + model_name='internalapplicationlabels', + name='description', + field=models.TextField(blank=True, default=''), + ), + migrations.AlterField( + model_name='internalapplicationlabels', + name='label', + field=models.TextField(default='', max_length=255, unique=True), + ), + migrations.AlterField( + model_name='internalapplicationlabels', + name='slug', + field=models.TextField(default='', max_length=1024, unique=True), + ), + ] diff --git a/apps/fhir/bluebutton/migrations/0012_alter_archivedcrosswalk_fhir_id_v2_and_more.py b/apps/fhir/bluebutton/migrations/0012_alter_archivedcrosswalk_fhir_id_v2_and_more.py new file mode 100644 index 000000000..d7d807c1f --- /dev/null +++ b/apps/fhir/bluebutton/migrations/0012_alter_archivedcrosswalk_fhir_id_v2_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 6.0.2 on 2026-04-20 18:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bluebutton', '0011_alter_crosswalk__user_id_hash_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='archivedcrosswalk', + name='fhir_id_v2', + field=models.CharField(db_column='fhir_id_v2', db_index=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='archivedcrosswalk', + name='fhir_id_v3', + field=models.CharField(db_column='fhir_id_v3', db_index=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='crosswalk', + name='fhir_id_v2', + field=models.CharField(db_column='fhir_id_v2', db_index=True, max_length=64, null=True, validators=[django.core.validators.MinLengthValidator(1)]), + ), + migrations.AlterField( + model_name='crosswalk', + name='fhir_id_v3', + field=models.CharField(db_column='fhir_id_v3', db_index=True, max_length=64, null=True, validators=[django.core.validators.MinLengthValidator(1)]), + ), + ] diff --git a/apps/mymedicare_cb/migrations/0004_alter_anonuserstate_next_uri_and_more.py b/apps/mymedicare_cb/migrations/0004_alter_anonuserstate_next_uri_and_more.py new file mode 100644 index 000000000..9e781c87d --- /dev/null +++ b/apps/mymedicare_cb/migrations/0004_alter_anonuserstate_next_uri_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.2 on 2026-04-20 18:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mymedicare_cb', '0003_alter_anonuserstate_next_uri'), + ] + + operations = [ + migrations.AlterField( + model_name='anonuserstate', + name='next_uri', + field=models.TextField(default='', max_length=512, validators=[django.core.validators.URLValidator()]), + ), + migrations.AlterField( + model_name='anonuserstate', + name='state', + field=models.TextField(db_index=True, default='', max_length=64), + ), + ] diff --git a/apps/pkce/migrations/0003_alter_codechallenge_challenge_and_more.py b/apps/pkce/migrations/0003_alter_codechallenge_challenge_and_more.py new file mode 100644 index 000000000..b7b8c41ea --- /dev/null +++ b/apps/pkce/migrations/0003_alter_codechallenge_challenge_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.2 on 2026-04-20 18:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pkce', '0001_squashed_0002_auto_20210513_1812'), + ] + + operations = [ + migrations.AlterField( + model_name='codechallenge', + name='challenge', + field=models.CharField(default=None, max_length=128), + ), + migrations.AlterField( + model_name='codechallenge', + name='challenge_method', + field=models.CharField(choices=[('S256', 'S256'), ('plain', 'plain')], default='S256', max_length=5), + ), + ] From acc8c5bfc34a8f4299164c33d3228cfa3ddd704a Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:24:13 -0600 Subject: [PATCH 16/32] set URLs max_length to be 2048 This is the default for URLValidator, but we set it explicitly on the TextField to make it clear. --- ...3_alter_application_client_uri_and_more.py | 44 +++++++++++++++++++ apps/dot_ext/models.py | 22 ++++------ .../0005_alter_anonuserstate_next_uri.py | 19 ++++++++ apps/mymedicare_cb/models.py | 3 +- 4 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 apps/dot_ext/migrations/0013_alter_application_client_uri_and_more.py create mode 100644 apps/mymedicare_cb/migrations/0005_alter_anonuserstate_next_uri.py diff --git a/apps/dot_ext/migrations/0013_alter_application_client_uri_and_more.py b/apps/dot_ext/migrations/0013_alter_application_client_uri_and_more.py new file mode 100644 index 000000000..f48286fa4 --- /dev/null +++ b/apps/dot_ext/migrations/0013_alter_application_client_uri_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 6.0.2 on 2026-04-21 19:17 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0012_alter_application_client_uri_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='client_uri', + field=models.TextField(blank=True, default='', help_text='This is typically a home/download website for the application. For example, https://www.example.org or http://www.example.org .', max_length=2048, null=True, validators=[django.core.validators.URLValidator()], verbose_name='Client URI'), + ), + migrations.AlterField( + model_name='application', + name='jwks_uri', + field=models.TextField(blank=True, default=None, max_length=2048, null=True, validators=[django.core.validators.URLValidator()], verbose_name='JSON Web Key Set URI'), + ), + migrations.AlterField( + model_name='application', + name='logo_uri', + field=models.TextField(blank=True, default='', max_length=2048, validators=[django.core.validators.URLValidator()], verbose_name='Logo URI'), + ), + migrations.AlterField( + model_name='application', + name='policy_uri', + field=models.TextField(blank=True, default='', help_text='This can be a model privacy notice or other policy document.', max_length=2048, validators=[django.core.validators.URLValidator()], verbose_name="Client's Policy URI"), + ), + migrations.AlterField( + model_name='application', + name='tos_uri', + field=models.TextField(blank=True, default='', max_length=2048, validators=[django.core.validators.URLValidator()], verbose_name="Client's Terms of Service URI"), + ), + migrations.AlterField( + model_name='application', + name='website_uri', + field=models.TextField(blank=True, default='', help_text='This is typically a home/download website for the application. For example, https://www.example.org or http://www.example.org .', max_length=2048, null=True, validators=[django.core.validators.URLValidator()], verbose_name='Website URI'), + ), + ] diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 61986a160..7994793f0 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -83,29 +83,30 @@ class Application(AbstractApplication): # # TODO spec? # It appears that 128 is the length that's generated in the code, so why 255? + # TODO go ahead and change this + # TODO add test client_secret_plain = models.CharField(default='', blank=True, max_length=255) # client_uri is depreciated but will continued to be referenced until it can be removed safely - # TODO validator? # why not use URLField? Wouldn't that be more explicit? + # TODO I chose to go with TextField with URLValidator, and an explicit max_length of 2048. Counterproposals: ... client_uri = models.TextField( default='', blank=True, null=True, validators=[URLValidator()], - max_length=512, + max_length=2048, verbose_name='Client URI', help_text='This is typically a home/download website for the application. ' 'For example, https://www.example.org or http://www.example.org .', ) - # TODO possible validator website_uri = models.TextField( default='', blank=True, null=True, validators=[URLValidator()], - max_length=512, + max_length=2048, verbose_name='Website URI', help_text='This is typically a home/download website for the application. ' 'For example, https://www.example.org or http://www.example.org .', @@ -121,24 +122,21 @@ class Application(AbstractApplication): redirect_uris = models.TextField(help_text=help_text, blank=True) - # TODO validator? - logo_uri = models.TextField(default='', blank=True, max_length=512, verbose_name='Logo URI', validators=[URLValidator()]) + logo_uri = models.TextField(default='', blank=True, max_length=2048, verbose_name='Logo URI', validators=[URLValidator()]) - # TODO validator? tos_uri = models.TextField( default='', blank=True, validators=[URLValidator()], - max_length=512, + max_length=2048, verbose_name="Client's Terms of Service URI", ) - # TODO validator? policy_uri = models.TextField( default='', blank=True, validators=[URLValidator()], - max_length=512, + max_length=2048, verbose_name="Client's Policy URI", help_text='This can be a model privacy notice or other policy document.', ) @@ -217,8 +215,6 @@ class Application(AbstractApplication): ) # New fields for CMS Aligned Networks epic - # TODO validator? - # TODO to TextField? jwks_uri = models.TextField( default=None, blank=True, @@ -226,7 +222,7 @@ class Application(AbstractApplication): validators=[URLValidator()], # Went with 512 as that is the established pattern we have for URLFields # and to allow for extremely long URLs. 200 is default - max_length=512, + max_length=2048, verbose_name='JSON Web Key Set URI', ) diff --git a/apps/mymedicare_cb/migrations/0005_alter_anonuserstate_next_uri.py b/apps/mymedicare_cb/migrations/0005_alter_anonuserstate_next_uri.py new file mode 100644 index 000000000..c371dd04a --- /dev/null +++ b/apps/mymedicare_cb/migrations/0005_alter_anonuserstate_next_uri.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.2 on 2026-04-21 19:17 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mymedicare_cb', '0004_alter_anonuserstate_next_uri_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='anonuserstate', + name='next_uri', + field=models.TextField(default='', max_length=2048, validators=[django.core.validators.URLValidator()]), + ), + ] diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index 0022e4074..3d7c030f7 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -443,8 +443,7 @@ def _validate_asserts(logger, log_dict, asserts): class AnonUserState(models.Model): # TODO should this still have a max length? state = models.TextField(default='', max_length=64, db_index=True) - # TODO validator? - next_uri = models.TextField(default='', validators=[URLValidator()], max_length=512) + next_uri = models.TextField(default='', validators=[URLValidator()], max_length=2048) def __str__(self): return '%s %s' % (self.state, self.next_uri) From 0967b230a9c9769eb936633430b9a90e48a8862a Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:34:42 -0600 Subject: [PATCH 17/32] change the last few fields that needed 2048 max length This does suggest that it might be easy for the different fields to become out of sync. --- ...lter_application_op_policy_uri_and_more.py | 24 +++++++++++++++++++ apps/dot_ext/models.py | 7 ++---- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 apps/dot_ext/migrations/0014_alter_application_op_policy_uri_and_more.py diff --git a/apps/dot_ext/migrations/0014_alter_application_op_policy_uri_and_more.py b/apps/dot_ext/migrations/0014_alter_application_op_policy_uri_and_more.py new file mode 100644 index 000000000..cc179907a --- /dev/null +++ b/apps/dot_ext/migrations/0014_alter_application_op_policy_uri_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.2 on 2026-04-21 19:27 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0013_alter_application_client_uri_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='op_policy_uri', + field=models.TextField(blank=True, default='', max_length=2048, validators=[django.core.validators.URLValidator()]), + ), + migrations.AlterField( + model_name='application', + name='op_tos_uri', + field=models.TextField(blank=True, default='https://bluebutton.cms.gov/terms', max_length=2048, validators=[django.core.validators.URLValidator()]), + ), + ] diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 7994793f0..dbe14a841 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -71,11 +71,8 @@ class Application(AbstractApplication): agree = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - # TODO validator? - # shouldn't this really be named 'url'? or are we really allowing uri's? - op_tos_uri = models.TextField(default=settings.TOS_URI, blank=True, validators=[URLValidator()], max_length=512) - # TODO validator? - op_policy_uri = models.TextField(default='', blank=True, max_length=512, validators=[URLValidator()]) + op_tos_uri = models.TextField(default=settings.TOS_URI, blank=True, validators=[URLValidator()], max_length=2048) + op_policy_uri = models.TextField(default='', blank=True, max_length=2048, validators=[URLValidator()]) # oauth2_provider upgraded and there is a breaking change on Application.client_secret field # see migration file 0005_alter_application_client_secret.py # field added to save client_secret in plain text before Application.save() From 0e5929eb666eed136489cbed3247504aee260eb4 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:47:33 -0600 Subject: [PATCH 18/32] Update dot_ext/forms.py to reflect new model fields Change the max_length to match the associated field, and use the same validator as in the model. --- apps/dot_ext/constants.py | 5 ---- apps/dot_ext/forms.py | 27 +++++++++--------- apps/dot_ext/tests/test_validators.py | 41 --------------------------- apps/dot_ext/validators.py | 10 ------- 4 files changed, 14 insertions(+), 69 deletions(-) delete mode 100644 apps/dot_ext/tests/test_validators.py diff --git a/apps/dot_ext/constants.py b/apps/dot_ext/constants.py index 6c2bf631b..7d12087cb 100644 --- a/apps/dot_ext/constants.py +++ b/apps/dot_ext/constants.py @@ -48,11 +48,6 @@ TOKEN_ENDPOINT_V2_KEY = 'token-v2' TOKEN_ENDPOINT_V3_KEY = 'token-v3' -URL_REGEX = re.compile( - r'^(https:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.' - r'[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$' -) - """ Test Cases for demographic scopes related testing Dictionaries included: diff --git a/apps/dot_ext/forms.py b/apps/dot_ext/forms.py index 6e6fbb731..0fb48e996 100644 --- a/apps/dot_ext/forms.py +++ b/apps/dot_ext/forms.py @@ -3,6 +3,7 @@ from django import forms from django.conf import settings +from django.core.validators import URLValidator from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from oauth2_provider.forms import AllowForm as DotAllowForm @@ -12,7 +13,7 @@ from apps.dot_ext.constants import PRINTABLE_SPECIAL_ASCII from apps.dot_ext.scopes import CapabilitiesScopes from apps.dot_ext.models import Application, InternalApplicationLabels -from apps.dot_ext.validators import validate_logo_image, validate_notags, validate_url +from apps.dot_ext.validators import validate_logo_image, validate_notags from django.contrib.auth.models import Group, User from django.forms.widgets import URLInput @@ -50,22 +51,22 @@ class CustomRegisterApplicationForm(forms.ModelForm): policy_uri = forms.CharField( required=False, - max_length=512, - validators=[validate_url], + max_length=2048, + validators=[URLValidator()], widget=URLInput, ) tos_uri = forms.CharField( required=False, - max_length=512, - validators=[validate_url], + max_length=2048, + validators=[URLValidator()], widget=URLInput, ) website_uri = forms.CharField( required=False, - max_length=512, - validators=[validate_url], + max_length=2048, + validators=[URLValidator()], widget=URLInput, ) @@ -219,22 +220,22 @@ class CreateNewApplicationForm(forms.ModelForm): policy_uri = forms.CharField( required=False, - max_length=512, - validators=[validate_url], + max_length=2048, + validators=[URLValidator()], widget=URLInput, ) tos_uri = forms.CharField( required=False, - max_length=512, - validators=[validate_url], + max_length=2048, + validators=[URLValidator()], widget=URLInput, ) website_uri = forms.CharField( required=False, - max_length=512, - validators=[validate_url], + max_length=2048, + validators=[URLValidator()], widget=URLInput, ) diff --git a/apps/dot_ext/tests/test_validators.py b/apps/dot_ext/tests/test_validators.py deleted file mode 100644 index 60a14a483..000000000 --- a/apps/dot_ext/tests/test_validators.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.test import TestCase -from django.core.exceptions import ValidationError -from apps.dot_ext.validators import validate_url - - -class ValidateURLTests(TestCase): - def test_valid_urls(self): - valid_urls = [ - "https://example.com", - "https://foo.bar/baz", - "https://sub.domain.co.uk/path?query=1", - ] - for url in valid_urls: - try: - validate_url(url) - except ValidationError: - self.fail(f"validate_url() raised ValidationError unexpectedly for {url}") - - def test_invalid_urls(self): - invalid_urls = [ - "not-a-url", - "example", - "http://", - "://example.com", - "javascript:alert(document.cookie)", - "javascript:alert(document.domain)", - "https://localhost", - "http://localhost", - " ", - ] - for url in invalid_urls: - with self.assertRaises(ValidationError, msg=f"Expected failure for {url}"): - validate_url(url) - - def test_empty_value_allowed(self): - """Empty values should pass without raising error""" - try: - validate_url("") - validate_url(None) - except ValidationError: - self.fail("validate_url() raised ValidationError for empty value") diff --git a/apps/dot_ext/validators.py b/apps/dot_ext/validators.py index 4f0221248..2158042c7 100644 --- a/apps/dot_ext/validators.py +++ b/apps/dot_ext/validators.py @@ -7,7 +7,6 @@ from django.utils.html import strip_tags from django.utils.translation import gettext_lazy as _ from os import path as ospath -from apps.dot_ext.constants import URL_REGEX class RedirectURIValidator(URIValidator): @@ -34,15 +33,6 @@ def validate_uris(value): v(uri) -def validate_url(value: str): - """Validate that the value is a syntactically valid URL.""" - if not value: - return - value = value.strip() - if not URL_REGEX.match(value): - raise ValidationError('Enter a valid URL') - - # Validate that there are no HTML tags def validate_notags(value): if value != strip_tags(value): From 5529106fc27383ce6f192c3df667c9df21933e11 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:34:29 -0600 Subject: [PATCH 19/32] change client_secret_plain max_length to 128 --- ...15_alter_application_client_secret_plain.py | 18 ++++++++++++++++++ apps/dot_ext/models.py | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 apps/dot_ext/migrations/0015_alter_application_client_secret_plain.py diff --git a/apps/dot_ext/migrations/0015_alter_application_client_secret_plain.py b/apps/dot_ext/migrations/0015_alter_application_client_secret_plain.py new file mode 100644 index 000000000..1f5415d76 --- /dev/null +++ b/apps/dot_ext/migrations/0015_alter_application_client_secret_plain.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-04-21 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0014_alter_application_op_policy_uri_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='client_secret_plain', + field=models.CharField(blank=True, default='', max_length=128), + ), + ] diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index dbe14a841..161e5fbb2 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -77,12 +77,12 @@ class Application(AbstractApplication): # see migration file 0005_alter_application_client_secret.py # field added to save client_secret in plain text before Application.save() # where the client_secret is hashed ireversible - # - # TODO spec? - # It appears that 128 is the length that's generated in the code, so why 255? - # TODO go ahead and change this - # TODO add test - client_secret_plain = models.CharField(default='', blank=True, max_length=255) + # + # In ouath2_provider, the default length of the generated secret is 128, + # but the max_length of AbstractApplication.client_secret is 255. Going with + # 128 here because that is the secret's actual length and the length expected + # by code and tests. + client_secret_plain = models.CharField(default='', blank=True, max_length=128) # client_uri is depreciated but will continued to be referenced until it can be removed safely # why not use URLField? Wouldn't that be more explicit? From 8a37eb2697da8db3eaf38666c602fcc76d64470c Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:01:51 -0600 Subject: [PATCH 20/32] remove todos that will be handled in PR review --- apps/dot_ext/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 161e5fbb2..c9f3fdbc6 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -85,8 +85,6 @@ class Application(AbstractApplication): client_secret_plain = models.CharField(default='', blank=True, max_length=128) # client_uri is depreciated but will continued to be referenced until it can be removed safely - # why not use URLField? Wouldn't that be more explicit? - # TODO I chose to go with TextField with URLValidator, and an explicit max_length of 2048. Counterproposals: ... client_uri = models.TextField( default='', blank=True, From 7ba240d8bd70b86d751baed80b6a9aa38ea9961b Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:11:29 -0600 Subject: [PATCH 21/32] remove todos that can be handled in future work --- apps/dot_ext/models.py | 3 --- apps/pkce/models.py | 1 - 2 files changed, 4 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index c9f3fdbc6..fcb7e6319 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -150,7 +150,6 @@ class Application(AbstractApplication): help_text='This is typically an email', ) - # TODO possible validator support_email = models.TextField(blank=True, null=True) support_phone_number = models.CharField(validators=[phone_regex], max_length=16, blank=True, null=True) @@ -180,7 +179,6 @@ class Application(AbstractApplication): ) # Type related to data access limits. - # TODO enum? data_access_type = models.CharField( default='THIRTEEN_MONTH', choices=APPLICATION_TYPE_CHOICES, @@ -202,7 +200,6 @@ class Application(AbstractApplication): ), ) - # TODO enum? allowed_auth_type = models.CharField( max_length=40, choices=APPLICATION_AUTH_CHOICES, diff --git a/apps/pkce/models.py b/apps/pkce/models.py index 10010bd2c..a7deac977 100644 --- a/apps/pkce/models.py +++ b/apps/pkce/models.py @@ -15,7 +15,6 @@ class CodeChallenge(models.Model): # https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 challenge = models.CharField(max_length=128, default=None) - # TODO enum? # either "S256" or "plain" by spec # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 challenge_method = models.CharField(max_length=5, default="S256", choices={"S256": "S256", "plain": "plain"}) From 0679f1f5001f8ffa3e3eec76114737ef03c907b4 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:34:29 -0600 Subject: [PATCH 22/32] resolve some todo comments --- apps/dot_ext/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index fcb7e6319..b3981be21 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -464,7 +464,7 @@ class ArchivedToken(models.Model): db_constraint=False, related_name='%(app_label)s_%(class)s', ) - # TODO spec + # The length of the AccessToken's created by oauth2_provider is 255 token = models.CharField( max_length=255, unique=True, @@ -494,6 +494,7 @@ class ExpiresIn(models.Model): # hex digest from sha-256, which is always 64 characters long # https://csrc.nist.gov/pubs/fips/180-4/upd1/final key = models.CharField(max_length=64, unique=True) + expires_in = models.IntegerField() objects = ExpiresInManager() @@ -531,14 +532,17 @@ class AuthFlowUuid(models.Model): auth_uuid = models.UUIDField(primary_key=True, unique=True) state = models.TextField(max_length=64, null=True, unique=True, db_index=True) - # TODO spec? + # matches what is in django rest framework # https://github.com/django-oauth/django-oauth-toolkit/blob/d422eeab79052c04a91d124f938ddc22c841c38c/oauth2_provider/models.py#L333 code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib + # TODO spec? why 100? client_id = models.CharField(max_length=100, null=True) + # TODO spec? should this be a TextField? auth_pkce_method = models.CharField(max_length=16, null=True) + created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) auth_share_demographic_scopes = models.BooleanField(null=True) From 67453dc9e823938894d6f5175e6d2dc7952fad27 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:11:03 -0600 Subject: [PATCH 23/32] change client_id max length to 40 --- .../0016_alter_authflowuuid_client_id.py | 18 ++++++++++++++++++ apps/dot_ext/models.py | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 apps/dot_ext/migrations/0016_alter_authflowuuid_client_id.py diff --git a/apps/dot_ext/migrations/0016_alter_authflowuuid_client_id.py b/apps/dot_ext/migrations/0016_alter_authflowuuid_client_id.py new file mode 100644 index 000000000..0685ebacb --- /dev/null +++ b/apps/dot_ext/migrations/0016_alter_authflowuuid_client_id.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-04-21 22:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0015_alter_application_client_secret_plain'), + ] + + operations = [ + migrations.AlterField( + model_name='authflowuuid', + name='client_id', + field=models.CharField(max_length=40, null=True), + ), + ] diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index b3981be21..82dd252cd 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -538,7 +538,14 @@ class AuthFlowUuid(models.Model): code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib # TODO spec? why 100? - client_id = models.CharField(max_length=100, null=True) + # 100 might be because that is what oauth2_provider/models.py:AbstractApplication.client_id has for max_length + # actual length is 40 + # 40 because this is what is hard-coded into oauth2_provider/generators.py:ClientIdGenerator + # + # In ouath2_provider, the length of the generated client_id is 40, + # but the max_length of AbstractApplication.client_id is 100. Going with + # 40 here because that is the ID's actual length. + client_id = models.CharField(max_length=40, null=True) # TODO spec? should this be a TextField? auth_pkce_method = models.CharField(max_length=16, null=True) From f60a212ad11b3bdbeceaff7358c439181e25c240 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:15:04 -0600 Subject: [PATCH 24/32] remove todo comment Will be mentioned to reviewers --- apps/dot_ext/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 82dd252cd..5411c4dc1 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -537,11 +537,6 @@ class AuthFlowUuid(models.Model): # https://github.com/django-oauth/django-oauth-toolkit/blob/d422eeab79052c04a91d124f938ddc22c841c38c/oauth2_provider/models.py#L333 code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib - # TODO spec? why 100? - # 100 might be because that is what oauth2_provider/models.py:AbstractApplication.client_id has for max_length - # actual length is 40 - # 40 because this is what is hard-coded into oauth2_provider/generators.py:ClientIdGenerator - # # In ouath2_provider, the length of the generated client_id is 40, # but the max_length of AbstractApplication.client_id is 100. Going with # 40 here because that is the ID's actual length. From dd69cd65780e4c7476caf265fdc1f3e0b6f00d49 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:01:41 -0600 Subject: [PATCH 25/32] align auth_pkce_method with spec, transfer changes to AuthFlowUuidCopy --- ..._authflowuuid_auth_pkce_method_and_more.py | 38 +++++++++++++++++++ apps/dot_ext/models.py | 26 +++++++++---- 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 apps/dot_ext/migrations/0017_alter_authflowuuid_auth_pkce_method_and_more.py diff --git a/apps/dot_ext/migrations/0017_alter_authflowuuid_auth_pkce_method_and_more.py b/apps/dot_ext/migrations/0017_alter_authflowuuid_auth_pkce_method_and_more.py new file mode 100644 index 000000000..25c7b04ea --- /dev/null +++ b/apps/dot_ext/migrations/0017_alter_authflowuuid_auth_pkce_method_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 6.0.2 on 2026-04-22 11:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0016_alter_authflowuuid_client_id'), + ] + + operations = [ + migrations.AlterField( + model_name='authflowuuid', + name='auth_pkce_method', + field=models.CharField(choices=[('S256', 'S256'), ('plain', 'plain')], max_length=16, null=True), + ), + migrations.AlterField( + model_name='authflowuuidcopy', + name='auth_pkce_method', + field=models.CharField(choices=[('S256', 'S256'), ('plain', 'plain')], max_length=16, null=True), + ), + migrations.AlterField( + model_name='authflowuuidcopy', + name='client_id', + field=models.CharField(max_length=40, null=True), + ), + migrations.AlterField( + model_name='authflowuuidcopy', + name='created', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='authflowuuidcopy', + name='state', + field=models.TextField(db_index=True, max_length=64, null=True, unique=True), + ), + ] diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 5411c4dc1..95146da09 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -542,8 +542,9 @@ class AuthFlowUuid(models.Model): # 40 here because that is the ID's actual length. client_id = models.CharField(max_length=40, null=True) - # TODO spec? should this be a TextField? - auth_pkce_method = models.CharField(max_length=16, null=True) + # code challenge method, either "S256" or "plain" by spec + # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 + auth_pkce_method = models.CharField(max_length=16, null=True, choices={"S256": "S256", "plain": "plain"}) created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) @@ -570,14 +571,25 @@ class AuthFlowUuidCopy(models.Model): auth_share_demographic_scopes - Bene demographic sharing choice from consent page/form """ - # TODO same changes as above? # TODO could this just inherit from the above? + auth_uuid = models.UUIDField(primary_key=True, unique=True) - state = models.CharField(max_length=64, null=True, unique=True, db_index=True) + state = models.TextField(max_length=64, null=True, unique=True, db_index=True) + + # matches what is in django rest framework + # https://github.com/django-oauth/django-oauth-toolkit/blob/d422eeab79052c04a91d124f938ddc22c841c38c/oauth2_provider/models.py#L333 code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib - client_id = models.CharField(max_length=100, null=True) - auth_pkce_method = models.CharField(max_length=16, null=True) - created = models.DateTimeField(null=True) + + # In ouath2_provider, the length of the generated client_id is 40, + # but the max_length of AbstractApplication.client_id is 100. Going with + # 40 here because that is the ID's actual length. + client_id = models.CharField(max_length=40, null=True) + + # code challenge method, either "S256" or "plain" by spec + # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 + auth_pkce_method = models.CharField(max_length=16, null=True, choices={"S256": "S256", "plain": "plain"}) + + created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) auth_share_demographic_scopes = models.BooleanField(null=True) From 4de3ff0619765d0fb959e1b101ed27008523b42a Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:17:39 -0600 Subject: [PATCH 26/32] add note confirming why username has max_length=150 --- apps/fhir/bluebutton/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index 1678fc1bb..a9085f199 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -229,7 +229,7 @@ class ArchivedCrosswalk(models.Model): create (crosswalk): static method to create an ArchivedCrosswalk from a Crosswalk instance """ - # TODO spec? + # The max_length of django's AbstractUser.username field is 150 username = models.CharField( max_length=150, null=False, From a7104ecb7e9253e0535dce3526f760975d5f0d58 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:19:13 -0600 Subject: [PATCH 27/32] remove comment that will be handled in review --- apps/mymedicare_cb/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index 3d7c030f7..7950d837f 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -441,7 +441,6 @@ def _validate_asserts(logger, log_dict, asserts): class AnonUserState(models.Model): - # TODO should this still have a max length? state = models.TextField(default='', max_length=64, db_index=True) next_uri = models.TextField(default='', validators=[URLValidator()], max_length=2048) From e102d58fe74bd8133a19b56983037e5635dfaf7d Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:42:56 -0600 Subject: [PATCH 28/32] remove unused import --- apps/dot_ext/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dot_ext/constants.py b/apps/dot_ext/constants.py index 7d12087cb..1d8a0d649 100644 --- a/apps/dot_ext/constants.py +++ b/apps/dot_ext/constants.py @@ -1,4 +1,3 @@ -import re from apps.constants import LAUNCH_SCOPE, OPENID_SCOPE # REGEX of paths that should be updated with auth flow info in hhs_oauth_server.request_logging.py From 676674b91e8e2274a8b9ed117648c0c9db7801b8 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:03:16 -0600 Subject: [PATCH 29/32] add ruff formatting changes for only lines I already changed --- apps/dot_ext/models.py | 8 +++++--- apps/pkce/models.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 95146da09..529363312 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -117,7 +117,9 @@ class Application(AbstractApplication): redirect_uris = models.TextField(help_text=help_text, blank=True) - logo_uri = models.TextField(default='', blank=True, max_length=2048, verbose_name='Logo URI', validators=[URLValidator()]) + logo_uri = models.TextField( + default='', blank=True, max_length=2048, verbose_name='Logo URI', validators=[URLValidator()] + ) tos_uri = models.TextField( default='', @@ -544,7 +546,7 @@ class AuthFlowUuid(models.Model): # code challenge method, either "S256" or "plain" by spec # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 - auth_pkce_method = models.CharField(max_length=16, null=True, choices={"S256": "S256", "plain": "plain"}) + auth_pkce_method = models.CharField(max_length=16, null=True, choices={'S256': 'S256', 'plain': 'plain'}) created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) @@ -587,7 +589,7 @@ class AuthFlowUuidCopy(models.Model): # code challenge method, either "S256" or "plain" by spec # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 - auth_pkce_method = models.CharField(max_length=16, null=True, choices={"S256": "S256", "plain": "plain"}) + auth_pkce_method = models.CharField(max_length=16, null=True, choices={'S256': 'S256', 'plain': 'plain'}) created = models.DateTimeField(auto_now_add=True, null=True) auth_crosswalk_action = models.CharField(max_length=1, null=True) diff --git a/apps/pkce/models.py b/apps/pkce/models.py index a7deac977..fd3a87ab6 100644 --- a/apps/pkce/models.py +++ b/apps/pkce/models.py @@ -17,4 +17,4 @@ class CodeChallenge(models.Model): # either "S256" or "plain" by spec # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 - challenge_method = models.CharField(max_length=5, default="S256", choices={"S256": "S256", "plain": "plain"}) + challenge_method = models.CharField(max_length=5, default='S256', choices={'S256': 'S256', 'plain': 'plain'}) From b3301381322db835f6a83271248b6780e9f905a5 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:22:13 -0600 Subject: [PATCH 30/32] fix doc comment --- apps/fhir/bluebutton/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index a9085f199..b5fbd8928 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -216,7 +216,7 @@ class ArchivedCrosswalk(models.Model): This is performed via code in the '__get_and_update_user()' function in apps/mymedicare_cb/models.py Attributes: - user: auth_user.id TODO but there is no user field in this model below? + username: auth_user.username fhir_id_v2: v1/v2 BFD fhir patient id fhir_id_v3: v3 BFD fhir patient id user_id_type: value is to be set to the type of lookup used MBI or HICN, TODO remove during BB2-3143 From 4c4ad92aae9eff3f23086e80e24ef8ba995cc804 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:26:05 -0600 Subject: [PATCH 31/32] make AuthFlowUuidCopy just inherit from AuthFlowUuid DRY. --- apps/dot_ext/models.py | 43 ++---------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 529363312..07b24f5f5 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -556,47 +556,8 @@ def __str__(self): return str(self.auth_uuid) -class AuthFlowUuidCopy(models.Model): - """ - An instance used to persist the beneficiary authorization flow - auth_uuid across the auth flow when there are breaks in the - session and for logging the resulting access token that is granted. - - Fields: - - auth_uuid - The beneficiary authorization flow tracing UUID - state - The state noance used in the Medicare.gov login and callback - code - The authorization code generated by the authorization server - client_id - The application client id - auth_pkce_method - PKCE method used - auth_crosswalk_action - Action taken with regard to the crosswalk model (retreived/created) - auth_share_demographic_scopes - Bene demographic sharing choice from consent page/form - """ - - # TODO could this just inherit from the above? - - auth_uuid = models.UUIDField(primary_key=True, unique=True) - state = models.TextField(max_length=64, null=True, unique=True, db_index=True) - - # matches what is in django rest framework - # https://github.com/django-oauth/django-oauth-toolkit/blob/d422eeab79052c04a91d124f938ddc22c841c38c/oauth2_provider/models.py#L333 - code = models.CharField(max_length=255, null=True, unique=True, db_index=True) # code comes from oauthlib - - # In ouath2_provider, the length of the generated client_id is 40, - # but the max_length of AbstractApplication.client_id is 100. Going with - # 40 here because that is the ID's actual length. - client_id = models.CharField(max_length=40, null=True) - - # code challenge method, either "S256" or "plain" by spec - # https://datatracker.ietf.org/doc/html/rfc7636#section-4.3 - auth_pkce_method = models.CharField(max_length=16, null=True, choices={'S256': 'S256', 'plain': 'plain'}) - - created = models.DateTimeField(auto_now_add=True, null=True) - auth_crosswalk_action = models.CharField(max_length=1, null=True) - auth_share_demographic_scopes = models.BooleanField(null=True) - - def __str__(self): - return str(self.auth_uuid) +class AuthFlowUuidCopy(AuthFlowUuid): + pass def get_application_counts(): From 909cf2be2f3ced5954ac0f96a5914d7cc46e6f68 Mon Sep 17 00:00:00 2001 From: Anna Montare <267455234+annamontare-nava@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:06:14 -0600 Subject: [PATCH 32/32] make abstract base class that AuthFlowUuid and AuthFlowUuidCopy inherit from --- apps/dot_ext/models.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 07b24f5f5..c26eac531 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -515,7 +515,11 @@ def archive_token(sender, instance=None, **kwargs): ) -class AuthFlowUuid(models.Model): +# Make an abstract class that both versions inherit from, because just using normal +# inheritance would have Django create links in the database between them, which is +# not what we want. +# See https://docs.djangoproject.com/en/6.0/topics/db/models/#model-inheritance +class AbstractAuthFlowUuid(models.Model): """ An instance used to persist the beneficiary authorization flow auth_uuid across the auth flow when there are breaks in the @@ -555,8 +559,15 @@ class AuthFlowUuid(models.Model): def __str__(self): return str(self.auth_uuid) + class Meta: + abstract = True + + +class AuthFlowUuid(AbstractAuthFlowUuid): + pass + -class AuthFlowUuidCopy(AuthFlowUuid): +class AuthFlowUuidCopy(AbstractAuthFlowUuid): pass