Links

Authentication Providers

CiviForm supports applicant and admin authentication via OpenID Connect (OIDC). We use pac4j for auth which hides most of the gritty details. But many auth providers deviate from the OpenID specifications. In those cases, we need to dig into the spec to debug and fix issues. You can find debugging tips at the end of this page.
Below, we'll go over the implementation and configuration steps for currently supported authentication providers. A CiviForm deployment should have exactly one admin authentication provider and one applicant authentication provider configured.

Getting started exercise

To familiarize yourself with OIDC, it is useful to go through a setup using any OIDC provider. Setup across all providers roughly resemble one another. For practice purposes, use https://auth0.com. CiviForm uses it on staging instances. Steps:
  1. 1.
    Get access to CiviForm auth0.com account. Ask someone on the team to add you to the account (Settings -> Tenant Members -> Add member).
  2. 2.
    Create test app. Create a test app on auth0.com. Take a look at "CiviForm AWS Staging" app as example. Use http://localhost:9000 as your domain.
  3. 3.
    Configure local CiviForm. Set the necessary settings in application.conf. They include applicant_generic_oidc.client_id, applicant_generic_oidc.discovery_uri.
  4. 4.
    Run CiviForm and test log in. You should be redirected to auth0.com and go through the login process. At the end, you should be redirected back to CiviForm.

Admin Authentication

Azure AD and ADFS (OIDC)

Azure Active Directory (Azure AD) is an identity provider designed by Microsoft. Active Directory Federation Services (ADFS) is a companion tool for single sign on.
Below, you'll find instructions on how to use Azure AD to authenticate applicants and Program Admins in CiviForm. High-level steps are to create an app in Azure AD, create a group that contains CiviForm admins, and finally update the CiviForm server config to use the app and the group we created. These instructions are one example of how to get basic authentication working. Administrators are encouraged to adapt Azure AD to fit their needs.

Configure Azure AD

  1. 1.
    Login to Azure Portal, go to "Active Directory" => "App Registrations" and create a new app. During creation, set the Redirect URI to https://your-civiform-domain.gov/callback/AdClient replacing the domain with your actual domain. The path /callback/AdClient is mandatory.
Screenshots
image
image
  1. 2.
    Go to the newly created app => Authentication and enable ID tokens.
Screenshots
image
  1. 3.
    Go to the "Token configuration" section and add the following claims:
    • Optional claims: acct, email. These determine what information about the user will be sent to CiviForm.
    • Groups claims: Security groups. These allows CiviForm to authenticate via a security group.
Screenshots
image
image
  1. 4.
    Go to "Certificates & secrets" and create a new secret that will be used by CiviForm when it talks to Azure AD. Write down the secret value somewhere temporarily. You will need to provide it to CiviForm. If you don't write it down and refresh the page, the value won't be accessible anymore, but you can always create a new secret.
Screenshots
image
  1. 5.
    Go to "Overview" and write down Application (client) ID and OpenID Connect metadata document. They will be used later.
Screenshots
image
  1. 6.
    We are done with setting up the Azure AD app. Now go to Azure "Groups" and create a new security group. That group will contain members that have CiviForm Admin access when they log into CiviForm. Other users, who are not members of that group, will be considered Program Admins. They need to be assigned to particular programs by a CiviForm Admin to see programs, since by default they don't have access to any programs. Once you created the group, write down its ID. It will be used later.
Screenshots
image
Below is the list of variables that we need after setting up Azure AD to integrate with CiviForm.
  • OpenID Connect metadata document URL from step 5.
  • Application (client) ID from step 5.
  • Client Secret from step 4.
  • Admin Group object ID from step 6.

Configure CiviForm

Now we need to update the CiviForm server to use the values we used earlier.
  1. 1.
    Open the civiform_config.sh file and set the following variables:
# Set to the "OpenID Connect metadata document" URL from step 5.
export ADFS_DISCOVERY_URI="https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555/v2.0/.well-known/openid-configuration"
# Set to the group Object ID from step 6.
export ADFS_ADMIN_GROUP="4294249d-6d31-4ba1-871a-0cefc3f6327f"
# Set the following variables to these values to make it work with Azure AD.
export ADFS_ADDITIONAL_SCOPES=""
export AD_GROUPS_ATTRIBUTE_NAME="groups"
  1. 2.
    Update Client ID and Client Secret. They are not exposed in the config and can be found in the Secrets Manager. Find secrets that end with adfs_client_id and adfs_secret.
  • Update adfs_client_id to be Application (client) ID from step 5.
  • Update adfs_secret to Client Secret value from step 4.
  1. 3.
    Redeploy CiviForm to pickup the updated value. Ensure that it starts healthy.

Test admin authentication

To test admin authentication, try the following:
  1. 1.
    In Azure, add yourself to the security group you created in "Configure Azure AD" step 6. Go to the CiviForm login page and click Admin login. It should take you through the Microsoft login flow and redirect back to CiviForm. You should see tabs like "Programs" and "Questions" indicating that you logged in as a CiviForm Admin.
  2. 2.
    Logout from CiviForm. In Azure, remove yourself from the security group and try logging in as an admin again. It should take you through the Microsoft login flow and redirect back to CiviForm. You should see "Your programs" and no tabs like "Programs" or "Questions".
If authentication is not working - take a look at Debugging tips below.

Applicant Authentication

Oracle IDCS

Oracle IDCS (Identity and Cloud Service) is a cloud service for identity and access management. It provides single sign on services.

Logout

Logout integration for IDCS does not use the normal flow where the logout URL is read from the discovery metadata file. Instead we override the logout URL using APPLICANT_OIDC_OVERRIDE_LOGOUT_URL set to a hardcoded value.

Generic OIDC (OIDC)

You can use the generic OIDC implementation with any OIDC-based Authentication provider. See the full config in code.
Important values in your civiform_config.sh:
export APPLICANT_AUTH_PROTOCOL='oidc' # this is a terraform configuration, to make sure resources are configured properly
export CIVIFORM_APPLICANT_IDP='generic-oidc' # tell civiform to use the generic OIDC adaptor, enabling the `APPLICANT_OIDC_` config values
export APPLICANT_OIDC_PROVIDER_NAME='provider_name' # this value will be appended to callback URLs
export APPLICANT_OIDC_CLIENT_ID='...' # comes from a secrets manager
export APPLICANT_OIDC_CLIENT_SECRET='....' # comes from a secrets manager
export APPLICANT_OIDC_DISCOVERY_URI='https://{auth_provider_hostname}/.well-known/openid-configuration' # provided by your OIDC provider
# Different modes (defaults shown):
export APPLICANT_OIDC_RESPONSE_MODE='form_post'
export APPLICANT_OIDC_RESPONSE_TYPE='id_token token'
export APPLICANT_OIDC_ADDITIONAL_SCOPES=''

Identity Provider Configuration

Most Identity providers will need these URLs:
  • Login: https://{your_civiform_url}/loginForm
  • Callback: https://{your_civiform_url}/callback/${APPLICANT_OIDC_PROVIDER_NAME}. # substitute provider name from your config.
  • Logout: https://{your_civiform_url}/logout

Login.gov (OIDC)

Here, you'll find instructions for how to setup login.gov authentication. It assumes that you have access to the login.gov sandbox. First, you'll need to create an app in the sandbox and configure it to work with your CiviForm instance.
  1. 1.
    Create a new app.
Screenshots
image
  1. 2.
    Use the following settings:
  • Authentication Protocol - OpenID Connect PKCE.
  • Attribute bundle - email.
  • Level of service and Default Authentication Assurance Level are up to you.
Screenshots
image
  1. 3.
    Decide on an Issuer string. It will be used later as the client_id variable. No need to upload certificates as we are not using protocols relying on private keys.
Screenshots
image
  1. 4.
    Add redirect URIs. You should add 2 URIs: https://your-civiform-domain.gov/callback/LoginGov and https://your-civiform-domain.gov/logout.
Screenshots
image
  1. 5.
    Save the app. The page might return an error on saving. Still, the data is saved (you can refresh the page to see the app you just created).
  2. 6.
    Update the Client ID variable in your CiviForm deployment. AWS deployment: that variable is not exposed in the civiform_config.sh. Instead it can be found in the AWS Secrets Manager. Find the secret that ends with applicant_oidc_client_id and set it to the Issuer string you used in step 3.
  3. 7.
    Update civiform_config.sh:
  • Set CIVIFORM_APPLICANT_IDP to "login-gov".
  • Set APPLICANT_OIDC_DISCOVERY_URI to "https://idp.int.identitysandbox.gov/.well-known/openid-configuration". Mentioned here. For production deployment that value will needs to be updated.
  1. 8.
    Redeploy CiviForm to pickup the updated value. Ensure that it starts healthy.
  2. 9.
    Test applicant login flow. If it is not working, take a look at Debugging tips below.

Logout

Login.gov requires setting the state param in the logout request, even though in the docs it specified as optional.

LoginRadius (SAML)

SAML authentication involves an exchange between an Identity Provider or IdP (LoginRadius), and a Service Provider or SP (CiviForm). In our application, we use SP-initiated SAML authentication, which means our application signs and sends a SAML request to LoginRadius to begin the auth process.

Service Provider Configuration

Follow the steps below to configure LoginRadius SAML auth on the SP side for a local dev instance:
  • First, create a keystore using the Java keytool with the following command. Take note of the keystore password and private key password used, and set the LOGIN_RADIUS_PRIVATE_KEY_PASS and LOGIN_RADIUS_KEYSTORE_PASS environment variables.
keytool -genkeypair -alias civiform-saml -keypass <private-key-password> -keystore civiformSamlKeystore.jks -storepass <keystore-password> -keyalg RSA -keysize 2048 -validity 3650
  • Next, navigate to the LoginRadius Dashboard. Click on "Get Your API Key and Secret", copy the API key, and set the LOGIN_RADIUS_API_KEY environment variable to the copied value.
  • Finally set the LOGIN_RADIUS_METADATA_URI environment variable to the link https://<login-radius-site-url>/service/saml/idp/metadata (e.g. https://civiform-staging.hub.loginradius.com/service/saml/idp/metadata).

Identity Provider Configuration

To configure SAML on the IDP side for LoginRadius, navigate to the "Integration" section on the left sidebar, add a SAML outbound SSO integration and follow the instructions linked here.
  • When configuring the integration, make note of the SAML App Name field, and set the LOGIN_RADIUS_SAML_APP_NAME environment variable.
image
  • For the service provider certificate field, export the locally generated public certificate, and then copy the contents of the exported file:
keytool -exportcert -alias civiform-saml -keystore civiformSamlKeystore.jks -rfc -file test.cert
pbcopy < test.cert
  • Set the following attributes. Each attribute should be in the format urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
image
  • For local testing/development, the Service Provider Details section should have the following values.
image

Via Terraform Setup

The Terraform setup script should walk you through each step of this process so the manual set up is less necessary. If you mess up the initial IdP set up, you just have to generate the "Generate LoginRadius' Certificate and Key" via open ssl commands documented here -- these get stored in the Id Provider Certificate Key and the Id Provider Certificate. They don't need to be stored anywhere on the CiviForm side.
After that, you will need to redeploy Terraform and regenerate the Service Provider certificate containing the SP public key, which is given to the Login Radius, or the IdP (this is prompted in the setup). The setup script also takes the private key portion of the cert and puts it in a storage bucket, which is mounted as a volume that the application can access. This was implemented in PR #2007.
When we generate the Service Provider secret/public key we password encrypt it with a "saml-keystore-pass." The private key password and the keystore password are the same value. Pac4j uses this password to protect the secret value and then knows how to grab the value via the following lines:
config.setKeystoreResourceFilepath(configuration.getString("login_radius.keystore_name"));
config.setKeystorePassword(configuration.getString("login_radius.keystore_password"));
config.setPrivateKeyPassword(configuration.getString("login_radius.private_key_password"));

Logout

CiviForm by default supports central logout, meaning that when applicant logs out from CiviForm, they will be redirected to the auth provider logout page so that they can be logged out from the auth provider as well. This feature is especially important on shared computers. Logout integrations turned out to be somewhat complicated and each auth provider required special treatment. It's possible that new auth providers will need additional debugging/adjusting as well. If central logout is not working and blocking other work, it can be disabled by setting APPLICANT_OIDC_PROVIDER_LOGOUT=false.

Authentication code structure

AdminAuthClient and ApplicantAuthClient

AdminAuthClient and ApplicantAuthClient are the two annotation interfaces, the former for admin auth and the latter for applicant. They both implement the IndirectClient interface. This is a Pac4j abstract class. Saml2Client and OidcClient extend this parent class, so the ApplicantAuthClient and AdminAuthClient can be either, based on the environment variables CIVIFORM_APPLICANT_IDP and CIVIFORM_ADMIN_IDP. The AdminAuthClient and ApplicantAuthClient are bound in SecurityModule to Provider classes. Currently, we are only checking the environment variables for binding admin authentication, because ADFS/AD (similar Azure auth providers) are the only supported admin IdPs. To add more admin IdPs, future devs can follow the example of the bindApplicantIdpProvider method. That binds to either the LoginRadiusSamlProvider class or the IdcsOidcProvider class based on the CIVIFORM_APPLICANT_IDP environment variable.

Provider classes

Provider classes implement Guice's provider interface for supplying values. The auth provider classes are found in app/auth. There is the LoginRadiusSamlProvider, which is used to create and provide the Saml2Client which can then be bound to the ApplicantAuthClient. The IdcsOidcProvider provides the OIDC client for IDCS. The AdOidcProvider is used to provide the OIDC client for Azure AD, the admin IdP. The enum AuthIdentityProviderName defines the various IdP names. This can be used in the code base to determine which applicant and admin IdPs are being used.

Adding a new IdP

Adding a new IdP is fairly straightforward. First, add a new enum to the AuthIdentityProviderName. In the app/auth folder, add a Provider for the new IdP, either in the OIDC or SAML folder. Bind that in the SecurityModule. Finally, if needed, create a new ProfileAdapter, though this should be unlikely since we already support OIDC and SAML profile adapters.

Authority ID

Users are keyed using the authority_id, which is unique and stable per authentication provider.
For OIDC, the authority_id is generated by combining the iss (issuer) and sub (subject). The issuer is fetched from the OIDCProfile using oidcProfile.getAttribute("iss", String.class). The subject is fetched by oidcProfile.getId(). The subject identifies the specific user within the issuer. It has to be fetched this way because Pac4j treats the subject as special, and users can't just get the sub claim. They are combined as iss: [issuer] sub: [subject].
For SAML, the authority_id is also generated by combining the issuer and the subject (NameID). The issuer is fetched from the Saml2Profile with profile.getIssuerEntityID() and the subject is fetched with profile.getId(). It is formatted as Issuer: [issuerId] NameID: [NameID].
Previously we were keying users with their email addresses, but that goes against the OIDC spec.

LoginRadius NameID

For SAML, which is the protocol format we are using with LoginRadius, the NameID (also known as the subject) follows a format. This is set in the LoginRadius console. We want the NameID to be in the persistent format. The persistent NameID is, in theory, supposed to be stable and shouldn't be something that can be linked to the user. A transient identifier is temporary and will change. Both of these NameID formats are, according to the SAML 2.0 spec, supposed to be private pseudonyms that protect a users' anonymity. There are a few other NameID formats that are defined by the SAML spec. The other supported formats for LoginRadius are email and an unspecified format. For some reason, no matter what NameID format is chosen, LoginRadius appears to be using email address as the NameID. This isn't something we can fix but it is something to be aware of. We have selected the persistent NameID format, but it appears that email address is being used to key the users.

Profile adapters

SamlCiviformProfileAdapter

The SamlCiviformProfileAdapter is used to augment the CiviformProfile with information that is found in a user’s SAML2Profile. The SAML2Profile is returned by the SAML2Client after a user successfully authenticates using SSO. The SamlCiviformProfileAdapter extends the AuthenticatorProfileCreator and therefore inherits the create() method, which creates a profile based on credentials. In this case, the created profile will be a SAML2Profile, because the user is authenticating using LoginRadius configured for SAML2. When the SAML2Profile is returned, we check for authority ID (used to identify each user), email, locale, first name, middle name and last name. Any information that is present is then added to the CiviformProfile. If the user doesn't have a corresponding authority_id, we should throw an exception. We then set the corresponding Roles for this user. Because LoginRadius with SAML is currently only supported for applicants, we only have to add the applicant and sometimes the trusted intermediary role, as this user will never be an admin.

Testing

OIDC for the IDCS applicant flow can be tested locally. Out of the box, bin/run-dev runs a dev-oidc container and CiviForm allows you to log in using it.
The logged out landing page has a Log In button that will redirect to the dev OIDC server. Enter any user/pass you like and accept the subsequent claims page. You'll then be redirected back to CiviForm.
Note:
  • You need to have a local IP route for the dev-oidc hostname in your /etc/hosts file so your browser can find the container: EG 127.0.0.1 dev-oidc
  • You may need to disable any proxy setup in your browser if you can't access the login page when you click 'Log In'
  • The Login page will ask for an artbitrary User ID and email, enter anything you like. The User ID will be used as your Account ID.
  • The second page in the login is confirming the claims. Just accept the request.

Debugging

Debugging authentication is challenging as it involves external systems that are not often well documented, and authentication protocols have many variations and flavors. Here are a few general tips. Some of them require some familiarity with the authentication protocols.
  • Verify that CiviForm uses correct IDs and URLs In browser devtools, open the "Network" tab and go through the login flow. You'll see CiviForm redirecting to the authentication provider and back. Look at the first redirect request and make sure that it redirects to the correct provider, sending the correct client ID and secret. If they are not correct, check your civiform_config.sh and secrets and redeploy CiviForm.
  • Check authentication provider errors In the "Network" tab, check requests where the auth provider redirects back to the CiviForm. It might contain an error message explaining a configuration issue.
  • Use jwt.io to decode the token OIDC uses a JSON Web Token to send data from the auth provider to the CiviForm. It is sent as the id_token param in the redirect POST request from the auth provider. You can decode it using jwt.io to see what it contains. For example, for the ADFS flow, you'll see what groups the user belongs to.
  • Test local changes on production auth Sometimes we need to test auth provider code changes that we don't have an easy way to test locally. For example, testing IDCS (Seattle) integration where we don't have access to Seattle's IDCS page and can't create our own app to test with. For cases like this, we can test by imitating the production setup locally using a proxy. Essentially, we set up the environment such that the browser thinks that it is using production CiviForm while, in fact, local CiviForm is used. Let's say we want to emulate staging-aws.civiform.dev auth configuration locally. Here are the steps:
    1. 1.
      Set necessary auth variables for your local CiviForm server The variables might be applicant_generic_oidc.client_id, applicant_generic_oidc.discovery_uri, and others. These variables need to be copied from the production config. Note that they are generally not sensitive. For example, client_id is passed as a URL param during the auth flow. If you don't know values of these variables, ask a point of contact in the corresponding deployments (Seattle, Bloomington, State of Arkansas).
    2. 2.
      Set base_url to the production domain In our example, it should be set to https://staging-aws.civiform.dev. It is required so that local CiviForm uses the production domain in redirect auth URLs.
    3. 3.
      Set up a proxy locally that redirects all traffic from the production host In our example, a proxy should redirect https://staging-aws.civiform.dev to http://localhost:9000 where CiviForm is running. There are multiple proxies suited for this purpose. One example is Charles proxy, which Google employees have a license for (search internally if you are one).
    4. 4.
      Start a new chrome instance that uses the proxy Example command for Mac assuming the proxy is running on port 8888: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --proxy-server=localhost:8888 --user-data-dir=$HOME/test_data --ignore-certificate-errors --allow-running-insecure-content.
    5. 5.
      Go to the production URL and test In our example, the URL is https://staging-aws.civiform.dev. You should see your local CiviForm. Now you can test auth. To make sure the setup is correct, it's recommended to ensure that auth behavior matches production by removing all local changes to auth Java code.