Skip to content

Commit

Permalink
Eregcsc 2012 initial logins from eua (#991)
Browse files Browse the repository at this point in the history
* EREGCSC-2012 - initial login from eua
  • Loading branch information
peggles2 authored Oct 16, 2023
1 parent 5728992 commit 1116715
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 29 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,31 @@ To better support Rapid Prototyping, a VueJS Single Page Application (SPA) has b
3. edit files in `/regulations/static/prototype` to make changes
4. changes should be reflected in running prototype via hot reloading
5. `make prototype:clean` to tear down Docker container

## Setting local to use EUA
1. Update your Dockerfile with the following environment variables
```
ENV OIDC_RP_CLIENT_ID=<your client id>
ENV OIDC_RP_CLIENT_SECRET=<your client secret>
ENV OIDC_OP_AUTHORIZATION_ENDPOINT=<authorization endpoint>
ENV OIDC_OP_TOKEN_ENDPOINT=<token endpoint>
ENV OIDC_OP_USER_ENDPOINT=<user endpoint>
ENV OIDC_OP_JWKS_ENDPOINT=<jwks endpoint>
ENV EUA_FEATUREFLAG=<set to 'true' if you want to see the eua link on admin login page>
```
These values can be found on AWS Parameter store.

## Register to test idp idm
- Sign into the URL [https://test.idp.idm.cms.gov/](https://test.idp.idm.cms.gov/) to access the CMS IDP (Identity Provider) portal.
- Set up Multi-Factor Authentication (MFA) for your account. Follow the provided prompts and instructions to complete the MFA setup process.
- Once your account has been successfully set up with MFA, please notify the CMS Okta team.
- Inform the CMS Okta team that you need to be added to the eRegs group.

Please note that the provided URL (https://test.idp.idm.cms.gov/) may require a valid CMS IDP account to access.

## Trouble shooting tips
- Issue: Setting OIDC_OP_AUTHORIZATION_ENDPOINT not found
This error indicates that the environment variables are not properly set.
- Solution:
- On your local environment verify that the DJANGO_SETTINGS_MODULE environment variable is set to ${DJANGO_SETTINGS_MODULE:-cmcs_regulations.settings.euasettings}. You can modify your docker-compose.yml file to include this setting: DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-cmcs_regulations.settings.euasettings}.
- On dev,val,prod ensure that DJANGO_SETTINGS_MODULE is set correctly in AWS Param Store.
5 changes: 3 additions & 2 deletions solution/backend/cmcs_regulations/settings/euasettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
OIDC_RP_IDP_SIGN_KEY = os.environ.get("OIDC_RP_IDP_SIGN_KEY", None)

AUTHENTICATION_BACKENDS = (
'regulations.admin.OidcAdminAuthenticationBackend',
'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
'django.contrib.auth.backends.ModelBackend',
'regulations.admin.OidcAdminAuthenticationBackend',
)

STAGE_ENV = os.environ.get("STAGE_ENV", "")
Expand All @@ -22,6 +22,7 @@
OIDC_OP_TOKEN_ENDPOINT = os.environ.get("OIDC_OP_TOKEN_ENDPOINT", None)
OIDC_OP_USER_ENDPOINT = os.environ.get("OIDC_OP_USER_ENDPOINT", None)
OIDC_OP_JWKS_ENDPOINT = os.environ.get("OIDC_OP_JWKS_ENDPOINT", None)
OIDC_REDIRECT_URL = "/admin/oidc/callback/"
OIDC_RP_SIGN_ALGO = 'RS256'
LOGIN_REDIRECT_URL = '/admin/'
LOGOUT_REDIRECT_URL = '/'
Expand All @@ -30,6 +31,6 @@
if re.match(r'^dev\d*$', STAGE_ENV):
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"
elif STAGE_ENV == 'dev' or 'val':
elif STAGE_ENV == 'dev' or STAGE_ENV == 'val':
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"
25 changes: 25 additions & 0 deletions solution/backend/cmcs_regulations/settings/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .base import * # noqa
import re
import os

USE_AWS_TOKEN = True
AWS_ACCESS_KEY_ID = os.environ.get("FILE_MANAGER_AWS_ACCESS_KEY_ID", 'test')
AWS_SECRET_ACCESS_KEY = os.environ.get("FILE_MANAGER_AWS_SECRET_ACCESS_KEY", 'test')
Expand All @@ -12,6 +14,29 @@
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

# TODO - this should be removed after we merge euasettings.py with base.py in teh future

STAGE_ENV = os.environ.get("STAGE_ENV", "")
BASE_URL = os.environ.get("BASE_URL", "")
OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", None)
OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", None)
OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get("OIDC_OP_AUTHORIZATION_ENDPOINT", None)
OIDC_OP_TOKEN_ENDPOINT = os.environ.get("OIDC_OP_TOKEN_ENDPOINT", None)
OIDC_OP_USER_ENDPOINT = os.environ.get("OIDC_OP_USER_ENDPOINT", None)
OIDC_OP_JWKS_ENDPOINT = "/example/jwks/endpoint/"
OIDC_REDIRECT_URL = "/admin/oidc/callback/"
OIDC_RP_SIGN_ALGO = 'RS256'
LOGIN_REDIRECT_URL = '/admin/'
LOGOUT_REDIRECT_URL = '/'
EUA_FEATUREFLAG = bool(os.getenv('EUA_FEATUREFLAG', 'False').lower() == 'true')

if re.match(r'^dev\d*$', STAGE_ENV):
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"
elif STAGE_ENV == 'dev' or STAGE_ENV == 'val':
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
Expand Down
58 changes: 31 additions & 27 deletions solution/backend/regulations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,42 +70,46 @@ def roman_to_int(roman):
return result


class OktaClaim:
def __init__(self, email, name, preferred_username, jobcodes):
self.email = email
self.name = name
self.preferred_username = preferred_username
self.jobcodes = jobcodes


class OidcAdminAuthenticationBackend(OIDCAuthenticationBackend):
def verify_claims(self, claims: OktaClaim) -> bool:
def verify_claims(self, claims) -> bool:
return (
super().verify_claims(claims)
and claims.get("email_verified", False)
)

@transaction.atomic
def create_user(self, claims: OktaClaim) -> User:
print(f"Creating user {claims.get('email')}")
print(f"Jobcodes: {claims.get('jobcodes')}")
print(f"Name: {claims.get('name')}")
print(f"Preferred username: {claims.get('preferred_username')}")
user: User = self.UserModel.objects.create_user(
claims.get("email"),
None, # password
first_name=claims["given_name"],
last_name=claims["family_name"],
)
user.save()

return user
def create_user(self, claims) -> User:
if claims.get("jobcodes"):
with transaction.atomic():
try:
# Attempt to get the user by email
user = self.UserModel.objects.get(email=claims.get("email"))
except User.DoesNotExist:
# User does not exist, create a new one
user = self.UserModel(
email=claims.get("email"),
username=claims.get("email")
)

# Set user fields from claims
return self.update_user(user, claims)
return None

@transaction.atomic
def update_user(self, user: User, claims: OktaClaim) -> User:
def update_user(self, user: User, claims) -> User:
"""Update existing user with new claims, if necessary save, and return user"""
user.first_name = claims["given_name"]
user.last_name = claims["family_name"]
first_name = claims.get("firstName")
if first_name:
user.first_name = first_name

last_name = claims.get("lastName")
if last_name:
user.last_name = last_name

jobcodes = claims.get("jobcodes")
if jobcodes:
user.is_active = True
else:
user.is_active = False
user.save()

return user
Expand Down
47 changes: 47 additions & 0 deletions solution/backend/regulations/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import unittest
from unittest.mock import patch

from django.contrib.auth import get_user_model

from ..admin import OidcAdminAuthenticationBackend

User = get_user_model()


class OidcAdminAuthenticationBackendTest(unittest.TestCase):
def setUp(self):
self.backend = OidcAdminAuthenticationBackend()
self.mock_claims = {
"sub": "00u1234567891234297",
"name": "Homer Simpson",
"lastName": "Simpson",
"firstName": "Homer",
"email": "[email protected]",
"email_verified": True,
"jobcodes": "cn=EREGS_ADMIN,ou=Groups,dc=cms,dc=hhs,dc=gov,cn=EXAMPLE_TEST,ou=Groups,dc=cms,dc=hhs,dc=gov"
}

@patch.object(OidcAdminAuthenticationBackend, 'create_user', return_value=User(email='[email protected]'))
def test_verify_claims(self, mock_create_user):
result = self.backend.verify_claims(self.mock_claims)
self.assertTrue(result)

invalid_claims = dict(self.mock_claims)
invalid_claims["email_verified"] = False
result = self.backend.verify_claims(invalid_claims)
self.assertFalse(result)

@patch.object(OidcAdminAuthenticationBackend, 'create_user')
def test_user_is_active_if_have_jobcodes(self, mock_create_user):
mock_create_user.return_value = User(email='[email protected]')
user = self.backend.create_user(self.mock_claims)
self.assertTrue(user.is_active)

def test_user_is_not_created_if_no_jobcodes(self):
self.mock_claims["jobcodes"] = ""
user = self.backend.create_user(self.mock_claims)
self.assertIsNone(user)


if __name__ == '__main':
unittest.main()

0 comments on commit 1116715

Please sign in to comment.