Repository: cal-itp/benefits

Issue:
Model settings for SSO
## Background

We'll need to use a configuration object for a JavaScript library that helps implement the Login.gov flow. 

### Configuration data

Here's what the configuration object eventually needs to look like in JavaScript:

```js
// base address of Benefits application
const baseAddress = "https://localhost:8000";

// configuration object
var config = {
  // authorization server address
  authority: "https://login.gov",
  
  // this is registered application id with auth server
  client_id: "UUID",
  
  // post-login, user is redirected here
  redirect_uri: baseAddress + "/callback.html",
  
  // post-logout, user is redirected here
  post_logout_redirect_uri: baseAddress + "/index.html",
  
  // must be "code" to use the authorization-code flow
  response_type: "code",
  
  // space-delimited list
  // since you do not need any information (claims) about the user, "openid" is the only needed scope.
  // If you need refresh tokens, you must add the "offline_access" scope
  scope: "openid",
};
```

## Tasks

* [x] Build out the list of necessary config items (keys, URLs, endpoints, etc.) - *see above ⬆️*
* [x] Create a model like `AuthProvider` with text fields for the above list, all required
  * [ ] Consider storing URL parameters as the name of the app route, e.g. `eligibility:confirm` so we can use the Django helpers like `reverse()` and `{% url %}` and not duplicate raw paths in data 
* [x] Add a `ForeignKey` for `AuthProvider` to the `EligibilityVerifier` model, make it optional
* [x] Create a wrapper bool attribute on the `EligibilityVerifier` that denotes whether Auth is configured (the **"feature flag"**) by checking if there is a linked provider or not
* [x] Create attributes to track the Sign in + Sign out buttons  `msgid`s


Discussion:

Resolving patch:
diff --git a/benefits/core/migrations/0001_initial.py b/benefits/core/migrations/0001_initial.py
--- a/benefits/core/migrations/0001_initial.py
+++ b/benefits/core/migrations/0001_initial.py
@@ -25,6 +25,25 @@ class Migration(migrations.Migration):
                 ("label", models.TextField(help_text="Human description of the PEM data.")),
             ],
         ),
+        migrations.CreateModel(
+            name="AuthProvider",
+            fields=[
+                ("id", models.AutoField(primary_key=True, serialize=False)),
+                ("authority", models.TextField(help_text="Authorization server address")),
+                ("client_id", models.TextField(help_text="The application ID registered with auth server")),
+                ("redirect_uri", models.TextField(help_text="Post-login, the user is redirected here")),
+                ("post_logout_redirect_uri", models.TextField(help_text="Post-logout, the user is redirected here")),
+                ("response_type", models.TextField(help_text='Must be "code" to use the authorization-code flow')),
+                (
+                    "scope",
+                    models.TextField(
+                        help_text='Space-delimited list. If you need refresh tokens, you must add the "offline access" scope'
+                    ),
+                ),
+                ("sign_in_button_label", models.TextField()),
+                ("sign_out_button_label", models.TextField()),
+            ],
+        ),
         migrations.CreateModel(
             name="EligibilityVerifier",
             fields=[
@@ -39,6 +58,7 @@ class Migration(migrations.Migration):
                 ("public_key", models.ForeignKey(help_text="The Verifier's public key, used to encrypt requests targeted at this Verifier and to verify signed responses from this verifier.", on_delete=models.deletion.PROTECT, related_name="+", to="core.PemData")),  # noqa: E501
                 ("jwe_cek_enc", models.TextField(help_text="The JWE-compatible Content Encryption Key (CEK) key-length and mode")),  # noqa: E501
                 ("jwe_encryption_alg", models.TextField(help_text="The JWE-compatible encryption algorithm")),
+                ("auth_provider", models.ForeignKey(null=True, on_delete=models.deletion.PROTECT, to="core.authprovider")),
                 # fmt: on
                 ("jws_signing_alg", models.TextField(help_text="The JWS-compatible signing algorithm")),
                 ("eligibility_types", models.ManyToManyField(to="core.EligibilityType")),
diff --git a/benefits/core/models.py b/benefits/core/models.py
--- a/benefits/core/models.py
+++ b/benefits/core/models.py
@@ -29,6 +29,22 @@ def jwk(self):
         return jwk.JWK.from_pem(pem_bytes)
 
 
+class AuthProvider(models.Model):
+    """An entity that provides authentication for eligibility verifiers."""
+
+    # fmt: off
+    id = models.AutoField(primary_key=True)
+    authority = models.TextField(help_text="Authorization server address")
+    client_id = models.TextField(help_text="The application ID registered with auth server")
+    redirect_uri = models.TextField(help_text="Post-login, the user is redirected here")
+    post_logout_redirect_uri = models.TextField(help_text="Post-logout, the user is redirected here")
+    response_type = models.TextField(help_text="Must be \"code\" to use the authorization-code flow")
+    scope = models.TextField(help_text="Space-delimited list. If you need refresh tokens, you must add the \"offline access\" scope")  # noqa: 503
+    sign_in_button_label = models.TextField()
+    sign_out_button_label = models.TextField()
+    # fmt: on
+
+
 class EligibilityType(models.Model):
     """A single conditional eligibility type."""
 
@@ -67,6 +83,7 @@ class EligibilityVerifier(models.Model):
     jwe_cek_enc = models.TextField(help_text="The JWE-compatible Content Encryption Key (CEK) key-length and mode")
     jwe_encryption_alg = models.TextField(help_text="The JWE-compatible encryption algorithm")
     jws_signing_alg = models.TextField(help_text="The JWS-compatible signing algorithm")
+    auth_provider = models.ForeignKey(AuthProvider, on_delete=models.PROTECT, null=True)
     selection_label = models.TextField()
     selection_label_description = models.TextField(null=True)
     start_content_title = models.TextField()
@@ -95,6 +112,10 @@ def public_jwk(self):
         """jwcrypto.jwk.JWK instance of this Verifier's public key"""
         return self.public_key.jwk
 
+    @property
+    def requires_authentication(self):
+        return self.auth_provider is not None
+
     @staticmethod
     def by_id(id):
         """Get an EligibilityVerifier instance by its ID."""


Test patch: