Skip to content

Sprint 23 #790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions backend/compact-connect/app_clients/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Add a new app client yaml file to `/app_clients` following the schema of the exa

##### **Compact-Level Scopes:**
These are the scopes that are scoped to a specific compact. Granting these scopes will allow the app client to perform actions across all jurisdictions within that compact.
Generally, the only scope that should be granted at the compact level is the `{compact}/readGeneral` scope.
Generally, the only scope that should be granted at the compact level is the `{compact}/readGeneral` scope if needed.

The following scopes are available at the compact level:
```
Expand Down Expand Up @@ -59,7 +59,10 @@ Add a new app client yaml file to `/app_clients` following the schema of the exa

This command creates an app client with the ability to generate access tokens that expire after 15 minutes. This expiration time can be adjusted according to the needs of the consuming team up to 1 day (see [AccessTokenValidity](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_CreateUserPoolClient.html#CognitoUserPools-CreateUserPoolClient-request-AccessTokenValidity)), though it is strongly recommended to keep this value as short as possible to limit the amount of time an access token is valid for if it is compromised. The consuming team will need to implement logic to generate new access tokens before previous tokens expire.

If the consuming team plans to use both the test and production environments, you will need to create two separate app clients, one in each respective AWS accounts for those environments.
If the consuming team plans to use both the beta and production environments, you will need to create two separate app clients- one in each respective AWS account.

The cognito token URL for the beta environment is `https://compact-connect-staff-beta.auth.us-east-1.amazoncognito.com/oauth2/token`
The cognito token URL for the prod environment is `https://compact-connect-staff.auth.us-east-1.amazoncognito.com/oauth2/token`


### 4. **Send Credentials to Consuming Team**
Expand All @@ -74,18 +77,17 @@ Add a new app client yaml file to `/app_clients` following the schema of the exa
}
```

These credentials should be securely transmitted to the consuming team via a encrypted channel. You will also need to provide them with the user pool domain for the Staff Users user pool, which can be determined using the following cli command.
```
aws cognito-idp describe-user-pool --user-pool-id '<user pool id>' --query 'UserPool.Domain' --output text
These credentials should be securely transmitted to the consuming team via an encrypted channel (i.e., a one-time use link) in the following format:
```json
{
"clientId": "<client id>",
"clientSecret": "<client secret>"
}
```

If the team has questions about generating access tokens, refer them to Amazon's [documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html). You can also give them the following curl example for generating an access token (Instruct them to replace the `<user pool domain>`, `<client id>`, `<client secret>` with the values you send them. The `<jurisdiction>` and `<compact>` scope values should be the state abbreviation/postal code and compact abbreviation they are requesting access to such as `ky` for Kentucky and `aslp` for the ASLP compact).
```
curl --location --request POST 'https://<user pool domain>.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=client_credentials&client_id=<client id>&client_secret=<client secret>&scope=<jurisdiction>%2F<compact>.write' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Accept: application/json'
```

#### Email Instructions for consuming team
As part of the email message sent to the consuming team, be sure to attach the onboarding instructions document ("Compact Connect Automated License Upload Instructions.txt").

## Rotating App Client Credentials
Unfortunately, AWS Cognito does not support rotating app client credentials for an existing app client. The only way to rotate credentials is to create a new app client with a new clientId and clientSecret and then delete the old one. The following process should be performed if credentials are accidentally exposed or in the event of a security breach where the old credentials are compromised.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# CompactConnect Automated License Data Upload Instructions (Beta Release)

## Overview

CompactConnect is a centralized platform that facilitates interstate license recognition for healthcare professionals
through occupational licensing compacts. These compacts allow practitioners with licenses in good standing to work
across state lines without obtaining additional licenses.

As a state IT department responsible for managing professional license data, your role is crucial in this process.
This document provides instructions for integrating your existing licensing systems with CompactConnect through its API.

By automating license data uploads, your state will:

- **Ensure Timely Data Synchronization**: Keep the compact database up-to-date with your state's latest license information
- **Reduce Manual Work**: Eliminate the need for manual license data entry by staff
- **Improve Accuracy**: Minimize human error in license data transmission
- **Support Interstate Mobility**: Enable qualified professionals to practice in participating states
- **Meet Compact Obligations**: Fulfill your state's requirements as a compact member

This document outlines the technical process for setting up machine-to-machine authentication and automated license data
uploads to CompactConnect's API. Following these instructions will help you establish a secure, reliable connection
between your licensing systems and the CompactConnect platform.

## Credential Security

You have received a one-time use link to access your API credentials. After retrieving the credentials, please:

1. Store the credentials securely in a password manager or secrets management system
2. Do not share these credentials with unauthorized personnel
3. Do not hardcode these credentials in source code repositories

> **Important**: If the link provided has already been used when you attempt to access the credentials, please contact the individual who sent the link to you as the credentials will need to be regenerated and sent using another link.

> Likewise, if these credentials are ever accidentally shared or compromised, please inform the CompactConnect team as soon as possible, so the credentials can be deactivated and regenerated to prevent abuse of the system.

The credentials will be sent to you in this format:

```json
{
"clientId": "<client id>",
"clientSecret": "<client secret>"
}
```

## Authentication Process for Uploading License Data

Follow these steps to obtain an access token and make requests to the CompactConnect License API:

### Step 1: Generate an Access Token

You must first obtain an access token to authenticate your API requests. The access token will be used in the
Authorization header of subsequent API calls. While the following curl command demonstrates how to generate a token for
the **beta** environment, you should implement this authentication flow in your application's programming language using
appropriate OAuth/HTTP libraries:

> **Note**: When copying commands, be careful of line breaks. You may need to remove any extra spaces or
> line breaks that occur when pasting.

```bash
curl --location --request POST 'https://compact-connect-staff-beta.auth.us-east-1.amazoncognito.com/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Accept: application/json' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=<clientId>' \
--data-urlencode 'client_secret=<clientSecret>' \
--data-urlencode 'scope=<jurisdiction>/<compact>.write'
```

Replace:
- `<clientId>` with your client ID
- `<clientSecret>` with your client secret
- `<jurisdiction>` with your lower-cased two-letter state code (e.g., `ky` for Kentucky)
- `<compact>` with the lower-cased compact abbreviation (`octp` for the 'Occupational Therapy' Compact,
`aslp` for 'Audiology and Speech Language Pathology' Compact, or `coun` for the 'Counseling' Compact)

Example response:
```json
{
"access_token": "eyJraWQiOiJleGFtcGxlS2V5SWQiLCJhbGciOiJSUzI1NiJ9...",
"expires_in": 900,
"token_type": "Bearer"
}
```

For more information about this authentication process, please see the following
AWS documentation: https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html

**Important Notes**:
- For security reasons, the access token is valid for 15 minutes from the time it is generated (900 seconds)
- Your application should request a new token before the current one expires
- Store the `access_token` value for use in API requests

### Step 2: Upload License Data to the Beta Environment

The CompactConnect License API can be called through a POST REST endpoint which takes in a list of license record objects.
The following curl command example demonstrates how to upload license data into the **beta** environment, but you should
implement this API call in your application's programming language using appropriate HTTP libraries. You will need to
replace the example payload with valid license data that includes the correct license types for your
specific compact (you can send up to 100 license records per request):

```bash
curl --location --request POST 'https://api.beta.compactconnect.org/v1/compacts/<compact>/jurisdictions/<jurisdiction>/licenses' \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '[{"ssn":"123-45-6789","licenseNumber":"LIC123456","licenseStatusName":"Active","licenseStatus":"active","compactEligibility":"eligible","licenseType":"audiologist","givenName":"Jane","middleName":"Marie","familyName":"Smith","dateOfIssuance":"2023-01-15","dateOfRenewal":"2023-01-15","dateOfExpiration":"2025-01-14","dateOfBirth":"1980-05-20","homeAddressStreet1":"123 Main Street","homeAddressStreet2":"Apt 4B","homeAddressCity":"Louisville","homeAddressState":"KY","homeAddressPostalCode":"40202","emailAddress":"[email protected]","phoneNumber":"+15555551234","npi":"1234567890"}]'
```

Replace:
- `<access_token>` with the access token from Step 1
- `<compact>` with the lower-cased compact abbreviation (e.g., `aslp`, `octp`, or `coun`)
- `<jurisdiction>` with your lower-cased two-letter state code (e.g., `ky`)
- The example payload shown here with your test license data

### Step 2 Alternative: Upload License Data via CSV File

In addition to calling the POST endpoint, there is also an option to upload license data in a CSV file format.
This method may be preferable for larger datasets or for systems that already generate CSV exports.

For detailed documentation on CSV file uploads, including required fields, formatting requirements, and the upload process, please refer to:
https://github.com/csg-org/CompactConnect/tree/main/backend/compact-connect/docs#machine-to-machine-automated-uploads

## License Data Schema Requirements

For the latest information about the license data field requirements, along with descriptions of each field, please see:
https://github.com/csg-org/CompactConnect/tree/main/backend/compact-connect/docs#field-descriptions

**Important Notes**:
- If `licenseStatus` is "inactive", `compactEligibility` cannot be "eligible"
- `licenseType` must match exactly with one of the valid types for the specified compact
- All date fields must use the `YYYY-MM-DD` format

## Verification that License Records are Uploaded

After submitting license data to the API, you can verify that your records were successfully uploaded by checking the API response:

### 1. Successful Upload
If the API responds with a 200 status code, your request was successful and the license data is being processed
by the CompactConnect System. The response will return the following body:

```json
{
"message": "OK"
}
```

### 2. Error Responses
If you receive an error response, check the status code and message:
- **400**: Bad Request - Your request data is invalid (check the response body for validation errors)
- **401**: Unauthorized - Your access token is invalid or expired
- **403**: Forbidden - Your app client doesn't have permission to upload to the specified jurisdiction/compact
- **502**: Internal Server Error - There was a problem processing your request

### 3. Validation Errors
If your license data fails validation, the API will return a 400 status code with details about the
validation errors in the response body.

> **Note**: Successful API responses (200 status code) indicate that the license data has been accepted for processing, but
> actual processing happens asynchronously. The data will be validated and processed by the CompactConnect System after acceptance.

## Troubleshooting Common Issues

### 1. "Unknown error parsing request body"
- Ensure your JSON data is properly formatted with no trailing commas
- Check that all quoted strings use double quotes, not single quotes
- Verify that your payload is a valid JSON array, even for a single license record

### 2. Authentication errors (401)
- Your access token might have expired - generate a new one
- Make sure you're including the "Bearer" prefix before the token in the Authorization header

### 3. Validation errors (400)
- Check the error response for specific validation issues
- Ensure all required fields are present and formatted correctly
- Verify that `licenseType` matches exactly one of the valid types for the compact

## Implementation Recommendations

1. Implement token refresh logic to get a new token before the current one expires
2. Implement error handling for API responses
3. Configure your application to securely store and access the client credentials, do not store the credentials in your
application code.

## Support and Feedback

If you encounter any issues, have questions, or would like to provide feedback based on your experience working with
the CompactConnect API, please contact the individual which sent you the credentials.
39 changes: 32 additions & 7 deletions backend/compact-connect/bin/create_staff_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
schema = UserRecordSchema()


def create_compact_ed_user(*, email: str, compact: str, user_attributes: dict):
sys.stdout.write(f"Creating Compact ED user, '{email}', in {compact}")
sub = create_cognito_user(email=email)
def create_compact_ed_user(*, email: str, compact: str, user_attributes: dict, permanent_password: str | None = None):
sys.stdout.write(f"Creating Compact ED user, '{email}', in {compact}\n")
sub = create_cognito_user(email=email, permanent_password=permanent_password)
user_table.put_item(
Item=schema.dump(
{
Expand All @@ -62,9 +62,11 @@ def create_compact_ed_user(*, email: str, compact: str, user_attributes: dict):
)


def create_board_ed_user(*, email: str, compact: str, jurisdiction: str, user_attributes: dict):
sys.stdout.write(f"Creating Board ED user, '{email}', in {compact}/{jurisdiction}")
sub = create_cognito_user(email=email)
def create_board_ed_user(
*, email: str, compact: str, jurisdiction: str, user_attributes: dict, permanent_password: str | None = None
):
sys.stdout.write(f"Creating Board ED user, '{email}', in {compact}/{jurisdiction}\n")
sub = create_cognito_user(email=email, permanent_password=permanent_password)
user_table.put_item(
Item=schema.dump(
{
Expand All @@ -79,26 +81,49 @@ def create_board_ed_user(*, email: str, compact: str, jurisdiction: str, user_at
)


def create_cognito_user(*, email: str):
def create_cognito_user(*, email: str, permanent_password: str | None):
"""Create a Cognito user with the given email address and password.

If provided, sets the password as the user's permanent password. Since this circumvents default password policies
(i.e., password reset), this should only be used in testing/sandbox environments.
"""

def get_sub_from_attributes(user_attributes: list):
for attribute in user_attributes:
if attribute['Name'] == 'sub':
return attribute['Value']
raise ValueError('Failed to find user sub!')

try:
# By including the TemporaryPassword on user creation, we avoid creating a user if the desired permanent
# password does not adhere to the password policy. Either no user is created, or a user is created with
# the desired password.
kwargs = {'TemporaryPassword': permanent_password} if permanent_password is not None else {}
user_data = cognito_client.admin_create_user(
UserPoolId=USER_POOL_ID,
Username=email,
UserAttributes=[{'Name': 'email', 'Value': email}],
DesiredDeliveryMediums=['EMAIL'],
**kwargs,
)

if permanent_password is not None:
cognito_client.admin_set_user_password(
UserPoolId=USER_POOL_ID, Username=email, Password=permanent_password, Permanent=True
)
return get_sub_from_attributes(user_data['User']['Attributes'])

except ClientError as e:
if e.response['Error']['Code'] == 'UsernameExistsException':
sys.stdout.write('User already exists, returning existing user')
user_data = cognito_client.admin_get_user(UserPoolId=USER_POOL_ID, Username=email)
return get_sub_from_attributes(user_data['UserAttributes'])
if e.response['Error']['Code'] == 'InvalidPasswordException':
sys.stderr.write(f'Invalid password: {e.response["Error"]["Message"]}')
sys.exit(2)
else:
sys.stderr.write(f'Failed to create user: {e.response["Error"]["Message"]}')
sys.exit(2)


if __name__ == '__main__':
Expand Down
Loading
Loading