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.
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.Get access to CiviForm auth0.com account. Ask someone on the team to add you to the account (Settings -> Tenant Members -> Add member).
- 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.Configure local CiviForm. Set the necessary settings in application.conf. They include
applicant_generic_oidc.client_id
,applicant_generic_oidc.discovery_uri
. - 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.
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.
- 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.
- 2.Go to the newly created app => Authentication and enable
ID tokens
.
- 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.
- 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.
- 5.Go to "Overview" and write down
Application (client) ID
andOpenID Connect metadata document
. They will be used later.
- 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.
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.
Now we need to update the CiviForm server to use the values we used earlier.
- 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"
- 2.Update
Client ID
andClient Secret
. They are not exposed in the config and can be found in the Secrets Manager. Find secrets that end withadfs_client_id
andadfs_secret
.
- Update
adfs_client_id
to beApplication (client) ID
from step 5. - Update
adfs_secret
toClient Secret
value from step 4.
- 3.Redeploy CiviForm to pickup the updated value. Ensure that it starts healthy.
To test admin authentication, try the following:
- 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.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".
Oracle IDCS (Identity and Cloud Service) is a cloud service for identity and access management. It provides single sign on services.
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.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=''
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
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.Create a new app.
- 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.
- 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.
- 4.Add redirect URIs. You should add 2 URIs: https://your-civiform-domain.gov/callback/LoginGov and https://your-civiform-domain.gov/logout.
- 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).
- 6.Update the
Client ID
variable in your CiviForm deployment. AWS deployment: that variable is not exposed in theciviform_config.sh
. Instead it can be found in the AWS Secrets Manager. Find the secret that ends withapplicant_oidc_client_id
and set it to the Issuer string you used in step 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.
- 8.Redeploy CiviForm to pickup the updated value. Ensure that it starts healthy.
- 9.
Login.gov requires setting the
state
param in the logout request, even though in the docs it specified as optional.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.
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
andLOGIN_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 linkhttps://<login-radius-site-url>/service/saml/idp/metadata
(e.g.https://civiform-staging.hub.loginradius.com/service/saml/idp/metadata
).
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 theLOGIN_RADIUS_SAML_APP_NAME
environment variable.
- 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
- For local testing/development, the Service Provider Details section should have the following values.
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"));
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
.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 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 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.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.
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.
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.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: EG127.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 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.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.Set
base_url
to the production domain In our example, it should be set tohttps://staging-aws.civiform.dev
. It is required so that local CiviForm uses the production domain in redirect auth URLs. - 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
tohttp://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.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.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.
Last modified 13d ago