diff --git a/aws/tdcr_udap.js b/aws/tdcr_udap.js index 578ee49..ef9a555 100644 --- a/aws/tdcr_udap.js +++ b/aws/tdcr_udap.js @@ -12,6 +12,7 @@ module.exports.clientRegistrationHandler = async (event, context) => { var clientId = null var oauthPlatform = null + var dataHolderOrIdpMode = event.requestContext.path == '/idp/register' ? 'idp' : 'dataholder' if(process.env.OAUTH_PLATFORM == 'okta') { oauthPlatform = require('../lib/okta/udap_okta') } @@ -20,7 +21,7 @@ module.exports.clientRegistrationHandler = async (event, context) => { } const oauthPlatformManagementClient = oauthPlatform.getAPIClient(process.env.OAUTH_ORG, process.env.OAUTH_CLIENT_ID, process.env.OAUTH_PRIVATE_KEY_FILE) - var validatedRegistrationData = await tdcr_udapLib.validateUdapCommonRegistrationRequest(event.body) + var validatedRegistrationData = await tdcr_udapLib.validateUdapCommonRegistrationRequest(event.body, dataHolderOrIdpMode) if(validatedRegistrationData.verifiedJwt) { try { @@ -30,7 +31,7 @@ module.exports.clientRegistrationHandler = async (event, context) => { if (result == null) { //new registration - await tdcr_udapLib.validateClientRegistrationMetaData(validatedRegistrationData.verifiedJwt, false) + await tdcr_udapLib.validateClientRegistrationMetaData(validatedRegistrationData.verifiedJwt, false, dataHolderOrIdpMode) clientId = await oauthPlatform.createClientApp(validatedRegistrationData.verifiedJwt, validatedRegistrationData.verifiedJwtJwks, oauthPlatformManagementClient) //TODO: Scope handling needs to happen somewhere in here. await updateSanRegistry(validatedRegistrationData.subjectAlternativeName, clientId) @@ -39,7 +40,7 @@ module.exports.clientRegistrationHandler = async (event, context) => { else if(validatedRegistrationData.verifiedJwt.body.grant_types.length > 0) { //update/edit registration - await tdcr_udapLib.validateClientRegistrationMetaData(validatedRegistrationData.verifiedJwt, true) + await tdcr_udapLib.validateClientRegistrationMetaData(validatedRegistrationData.verifiedJwt, true, dataHolderOrIdpMode) clientId = result.client_application_id //TODO: Scope handling needs to happen somewhere in here. diff --git a/deploy/aws/auth0/serverless-example.yml b/deploy/aws/auth0/serverless-example.yml index 5d30183..2501546 100644 --- a/deploy/aws/auth0/serverless-example.yml +++ b/deploy/aws/auth0/serverless-example.yml @@ -82,6 +82,7 @@ provider: INTROSPECT_ENDPOINT: https://${param:BASE_DOMAIN}${param:INTROSPECT_PATH} REVOKE_ENDPOINT: https://${param:BASE_DOMAIN}${param:REVOKE_PATH} REGISTRATION_ENDPOINT: https://${param:BASE_DOMAIN}/register + IDP_REGISTRATION_ENDPOINT: https://${param:BASE_DOMAIN}/idp/register TIERED_OAUTH_REDIRECT_ENDPOINT: https://${param:BASE_DOMAIN}${param:TIERED_OAUTH_REDIRECT_PATH} OAUTH_ORG_VANITY_URL_TOKEN_ENDPOINT: https://${param:OAUTH_CUSTOM_DOMAIN_NAME_BACKEND}${param:TOKEN_PATH} @@ -160,6 +161,14 @@ functions: path: /register method: POST +##Trusted UDAP DCR Proxy - CSP/IDP Mode + tdcr_udap_idp: + handler: ${self:provider.name}/tdcr_udap.clientRegistrationHandler + events: + - http: + path: /idp/register + method: POST + ##AUTHORIZE PROXY THAT PERFORMS TIERED-OAUTH2 authorize-proxy: handler: ${self:provider.name}/authorize.authorizeHandler @@ -311,6 +320,22 @@ resources: SmoothStreaming: false TargetOriginId: ${param:API_GATEWAY_DOMAIN_NAME_BACKEND} ViewerProtocolPolicy: "https-only" + - + AllowedMethods: + - "HEAD" + - "DELETE" + - "POST" + - "GET" + - "OPTIONS" + - "PUT" + - "PATCH" + Compress: false + CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" + OriginRequestPolicyId: "216adef6-5c7f-47e4-b989-5492eafa07d3" + PathPattern: /idp/register + SmoothStreaming: false + TargetOriginId: ${param:API_GATEWAY_DOMAIN_NAME_BACKEND} + ViewerProtocolPolicy: "https-only" - AllowedMethods: - "HEAD" diff --git a/deploy/aws/okta/serverless-example.yml b/deploy/aws/okta/serverless-example.yml index 0c7625e..826077f 100644 --- a/deploy/aws/okta/serverless-example.yml +++ b/deploy/aws/okta/serverless-example.yml @@ -83,6 +83,7 @@ provider: INTROSPECT_ENDPOINT: https://${param:BASE_DOMAIN}${param:INTROSPECT_PATH} REVOKE_ENDPOINT: https://${param:BASE_DOMAIN}${param:REVOKE_PATH} REGISTRATION_ENDPOINT: https://${param:BASE_DOMAIN}/register + IDP_REGISTRATION_ENDPOINT: https://${param:BASE_DOMAIN}/idp/register TIERED_OAUTH_REDIRECT_ENDPOINT: https://${param:BASE_DOMAIN}${param:TIERED_OAUTH_REDIRECT_PATH} OAUTH_ORG_VANITY_URL_TOKEN_ENDPOINT: https://${param:OAUTH_CUSTOM_DOMAIN_NAME_BACKEND}${param:TOKEN_PATH} @@ -160,6 +161,13 @@ functions: - http: path: /register method: POST +##Trusted UDAP DCR Proxy operating in CSP/IDP Mode. + tdcr_udap_idp: + handler: ${self:provider.name}/tdcr_udap.clientRegistrationHandler + events: + - http: + path: /idp/register + method: POST ##AUTHORIZE PROXY THAT PERFORMS TIERED-OAUTH2 authorize-proxy: @@ -293,6 +301,22 @@ resources: SmoothStreaming: false TargetOriginId: ${param:API_GATEWAY_DOMAIN_NAME_BACKEND} ViewerProtocolPolicy: "https-only" + - + AllowedMethods: + - "HEAD" + - "DELETE" + - "POST" + - "GET" + - "OPTIONS" + - "PUT" + - "PATCH" + Compress: false + CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" + OriginRequestPolicyId: "216adef6-5c7f-47e4-b989-5492eafa07d3" + PathPattern: /idp/register + SmoothStreaming: false + TargetOriginId: ${param:API_GATEWAY_DOMAIN_NAME_BACKEND} + ViewerProtocolPolicy: "https-only" - AllowedMethods: - "HEAD" diff --git a/deploy/okta/okta_object_models.js b/deploy/okta/okta_object_models.js index 80311e8..1600275 100644 --- a/deploy/okta/okta_object_models.js +++ b/deploy/okta/okta_object_models.js @@ -1,4 +1,4 @@ -module.exports.oktaAPIM2MClientScopes = ['okta.apps.manage','okta.apps.read','okta.idps.read','okta.idps.manage', 'okta.authorizationServers.read', 'okta.authorizationServers.manage'] +module.exports.oktaAPIM2MClientScopes = ['okta.apps.manage','okta.apps.read','okta.idps.read','okta.idps.manage', 'okta.authorizationServers.read', 'okta.authorizationServers.manage', 'okta.profileMappings.read', 'okta.profileMappings.manage'] module.exports.oktaAPIM2MClient = { "name": "oidc_client", diff --git a/lib/authorize.js b/lib/authorize.js index 834b7c3..d5eea5e 100644 --- a/lib/authorize.js +++ b/lib/authorize.js @@ -189,7 +189,7 @@ async function registerOAuthIDP(idpUri, validatedIDPMetadata, oidcMetadata, oaut response_types: ['code'], redirect_uris: [process.env.TIERED_OAUTH_REDIRECT_ENDPOINT], logo_uri:process.env.LOGO_URI, - scope: "fhirUser udap openid", + scope: "fhirUser udap openid email profile", san: process.env.SERVER_SAN } @@ -211,7 +211,8 @@ async function registerOAuthIDP(idpUri, validatedIDPMetadata, oidcMetadata, oaut userInfoUrl: oidcMetadata.userinfo_endpoint, jwksUrl: oidcMetadata.jwks_uri, clientId: externalIdpData.data.client_id, - idpIssuer: oidcMetadata.issuer + idpIssuer: oidcMetadata.issuer, + scope: authzCodeRegistrationObject.scope } const oauthIdpDetail = await oauthPlatform.createIdp(idpDetail, oauthPlatformManagementClient) diff --git a/lib/okta/okta_object_models.js b/lib/okta/okta_object_models.js index b8958d7..4b6ec3d 100644 --- a/lib/okta/okta_object_models.js +++ b/lib/okta/okta_object_models.js @@ -50,9 +50,7 @@ module.exports.newUdapIdpModel = { "url": "" } }, - "scopes": [ - "openid", "udap", "email", "profile" - ], + "scopes": [], "type": "OIDC", "credentials": { "client": { @@ -89,13 +87,30 @@ module.exports.newUdapIdpModel = { "maxClockSkew": 120000, "subject": { "userNameTemplate": { - "template": "idpuser.email" + "template": "" }, "matchType": "USERNAME" } } } +module.exports.newUdapIdpAttributeMappingModel = { + "properties": { + "email": { + "expression": "", + "pushStatus": "DONT_PUSH" + }, + "login": { + "expression": "", + "pushStatus": "DONT_PUSH" + }, + "displayName": { + "expression": "appuser.displayName", + "pushStatus": "DONT_PUSH" + } + } +} + module.exports.newUdapAppScopePolicyModel = { "name": "Authorization Policy", "description": "Ensures that the application can only request the scopes they were approved for at registration time.", diff --git a/lib/okta/udap_okta.js b/lib/okta/udap_okta.js index 997cde2..5517774 100644 --- a/lib/okta/udap_okta.js +++ b/lib/okta/udap_okta.js @@ -19,6 +19,7 @@ const querystring = require('querystring') //getAuthzPolicyDetails //createAuthzPolicy //updateAuthzPolicy +//updateIdpMapping /* PRIVATE METHODS @@ -52,7 +53,6 @@ const getAuthzPolicyDetails = async (authorizationServerId, clientId, apiClient) else { return null } - } const createAuthzPolicy = async (authorizationServerId, clientId, grantTypes, scopes, apiClient) => { @@ -94,6 +94,32 @@ const updateAuthzPolicy = async (authorizationServerId, policyId, policyRuleId, return updatedPolicyRule.id } +const setNewIdpMappingDefaults = async (idpId, apiClient) => { + //Get IDP mapping + //Update IDP mapping + var existingMappingId = "" + var defaultIdpMappingModel = oktaModels.newUdapIdpAttributeMappingModel + defaultIdpMappingModel.properties.email.expression = `(appuser.email != null) ? appuser.email : (appuser.externalId + '@${process.env.BASE_DOMAIN}')` + defaultIdpMappingModel.properties.login.expression = `(appuser.email != null) ? appuser.email : (appuser.externalId + '@${process.env.BASE_DOMAIN}')` + + const idpMappingIds = await apiClient.profileMappingApi.listProfileMappings({sourceId: idpId}) + await idpMappingIds.each(idpMappingId => { + if(idpMappingId.source.id == idpId) { + existingMappingId = idpMappingId.id + console.log(`Found a matching IDP profile mapping id: ${existingMappingId}`) + } + }) + + if (existingMappingId) { + console.log('Setting default inbound profile mappings for tiered oauth...') + const defaultMappingResponse = await apiClient.profileMappingApi.updateProfileMapping({mappingId: existingMappingId, profileMapping: defaultIdpMappingModel}) + console.log(`Updated default mappings in Okta for mapping id: ${defaultMappingResponse.id}`) + } + else { + throw new error("Unable to complete the creation of the identity provider in Okta. No valid Okta profile mappings were found.") + } +} + /* PUBLIC METHODS */ @@ -158,6 +184,7 @@ module.exports.getIdpIdByUri = async (idpUri, apiClient) => { //TODO: At time of writing the SDK doesn't support registering private_key_jwt IDPs. Update once support is available. module.exports.createIdp = async (idpDetail, apiClient) => { var idpModel = oktaModels.newUdapIdpModel + idpModel.name = idpDetail.idpUri idpModel.protocol.endpoints.authorization.url = idpDetail.authorizeUrl idpModel.protocol.endpoints.token.url = idpDetail.tokenUrl @@ -165,8 +192,10 @@ module.exports.createIdp = async (idpDetail, apiClient) => { idpModel.protocol.endpoints.jwks.url = idpDetail.jwksUrl idpModel.protocol.credentials.client.client_id = idpDetail.clientId idpModel.protocol.issuer.url = idpDetail.idpIssuer + idpModel.protocol.scopes = idpDetail.scope.split(" ") + idpModel.policy.subject.userNameTemplate.template = `(idpuser.email != null) ? idpuser.email : (idpuser.externalId + '@${process.env.BASE_DOMAIN}')` - console.log("Invoking the Okta idps endpoint to create the IDP endpoint.") + console.log("Invoking the Okta idps endpoint to create the IDP endpoint.") console.log("IDP Model:") console.log(JSON.stringify(idpModel)) const createRequest = { @@ -176,7 +205,6 @@ module.exports.createIdp = async (idpDetail, apiClient) => { } const createUrl = `${apiClient.baseUrl}/api/v1/idps`; const httpCreateResponse = await apiClient.http.http(createUrl, createRequest) - //var createResponse = await apiClient.identityProviderApi.createIdentityProvider({identityProvider: idpModel}) var createResponse = await httpCreateResponse.json() console.log('Create Response from Okta:') @@ -189,6 +217,9 @@ module.exports.createIdp = async (idpDetail, apiClient) => { delete createResponse.created delete createResponse.lastUpdated + //Setting default profile mappings for the IDP... + await setNewIdpMappingDefaults(idpId, apiClient) + //Getting the public key generated by Okta... console.log("Invoking the Okta idp credential endpoint to get the public key generated by Okta.") const keysResponse = await apiClient.identityProviderApi.getIdentityProviderSigningKey({idpId: idpId, keyId: publicKeyId}) @@ -199,7 +230,6 @@ module.exports.createIdp = async (idpDetail, apiClient) => { console.log("Updating the token endpoint on the IDP to the proper outbound proxy URL.") createResponse.protocol.endpoints.token.url = "https://" + process.env.BASE_DOMAIN + "/" + idpId + "/tiered_client/token" - //const updateResponse = await apiClient.identityProviderApi.replaceIdentityProvider({idpId: idpId, identityProvider: createResponse}) const updateRequest = { method: 'PUT', @@ -344,7 +374,7 @@ module.exports.getAPIClient = (oktaOrg, clientId, privateKeyFile) => { authorizationMode: 'PrivateKey', clientId: clientId, privateKey: signingKeyPem, - scopes: ['okta.apps.manage','okta.apps.read','okta.idps.read','okta.idps.manage', 'okta.authorizationServers.read', 'okta.authorizationServers.manage'] + scopes: ['okta.apps.manage','okta.apps.read','okta.idps.read','okta.idps.manage', 'okta.authorizationServers.read', 'okta.authorizationServers.manage', 'okta.profileMappings.read', 'okta.profileMappings.manage'] } return new ManagementClient(options) diff --git a/lib/tdcr_udap.js b/lib/tdcr_udap.js index dea9dbc..8edf2f8 100644 --- a/lib/tdcr_udap.js +++ b/lib/tdcr_udap.js @@ -10,7 +10,7 @@ const axios = require('axios') //This method is taked with general validation that's required no matter what type of request (edit/create/delete). //This includes basic request formatting checks as well as general JWT validations. //This EXCLUDES full softwarestatement metadata checks since those can be a little different for edit/create/delete. -module.exports.validateUdapCommonRegistrationRequest = async (clientRegisterRequestBody) => { +module.exports.validateUdapCommonRegistrationRequest = async (clientRegisterRequestBody, dataHolderOrIdpMode) => { console.log('Validating signed software statement') console.log("Client request body:") console.log(clientRegisterRequestBody) @@ -24,7 +24,7 @@ module.exports.validateUdapCommonRegistrationRequest = async (clientRegisterRequ //Validate the proper UDAP signed software statement token. try { const trustAnchorObject = udapCommon.parseTrustAnchorPEM(process.env.COMMUNITY_CERT) - const softwareStatementDetail = await validateUdapCommonSoftwareStatement(inboundSoftwareStatement, trustAnchorObject) + const softwareStatementDetail = await validateUdapCommonSoftwareStatement(inboundSoftwareStatement, trustAnchorObject, dataHolderOrIdpMode) const jwks = udapCommon.getPublicKeyJWKS(inboundSoftwareStatement) console.log("Cert:") @@ -57,7 +57,7 @@ module.exports.validateUdapCommonRegistrationRequest = async (clientRegisterRequ } } -async function validateUdapCommonSoftwareStatement(inboundSoftwareStatement, trustAnchorObject) { +async function validateUdapCommonSoftwareStatement(inboundSoftwareStatement, trustAnchorObject, dataHolderOrIdpMode) { //Validate Client software statement // 1 validate signature using public key from x5c parameter in JOSE header // 2 Validate/Construct certificate chain @@ -97,7 +97,7 @@ async function validateUdapCommonSoftwareStatement(inboundSoftwareStatement, tru console.log('JWT Body to validate:') console.log(jwtDetails.verifiedJwt.body) - const firstSAN = validateUdapCommonSignedSoftwareBody(jwtDetails) + const firstSAN = validateUdapCommonSignedSoftwareBody(jwtDetails, dataHolderOrIdpMode) console.log("First SAN: " + firstSAN) @@ -109,7 +109,7 @@ async function validateUdapCommonSoftwareStatement(inboundSoftwareStatement, tru } //These are basic JWT checks to ensure that the JWT is intended for our endpoint, it's not expired, and is signed with a valid certificate. -function validateUdapCommonSignedSoftwareBody(jwtDetails) { +function validateUdapCommonSignedSoftwareBody(jwtDetails, dataHolderOrIdpMode) { var error = new Error() error.code = 'invalid_software_statement' const ssJwtBody = jwtDetails.verifiedJwt.body @@ -130,7 +130,7 @@ function validateUdapCommonSignedSoftwareBody(jwtDetails) { } //We need to ensure the aud is set properly. - if (ssJwtBody.aud == "" || ssJwtBody.aud != process.env.REGISTRATION_ENDPOINT) + if (ssJwtBody.aud == "" || ssJwtBody.aud != (dataHolderOrIdpMode == 'dataholder' ? process.env.REGISTRATION_ENDPOINT : process.env.IDP_REGISTRATION_ENDPOINT)) { error.message = 'Invalid aud value||' console.error(error) @@ -158,12 +158,14 @@ function validateUdapCommonSignedSoftwareBody(jwtDetails) { } //Now that we have a valid JWT that's intended for our endpoint, signed with valid cert, etc. Now let's ensure all the metadata of the request is valid. -module.exports.validateClientRegistrationMetaData = async (jwtDetails, editMode) => { +module.exports.validateClientRegistrationMetaData = async (jwtDetails, editMode, dataHolderOrIdpMode) => { console.log('Checking client metadata') var error = new Error() error.code = 'invalid_client_metadata' const ssJwtBody = jwtDetails.body - const metadata = await wellKnown.getFHIRServerWellKnown() + const metadata = dataHolderOrIdpMode == 'dataholder' ? await wellKnown.getFHIRServerWellKnown() : wellKnown.getUDAPConfiguration().body + + console.log(`Validating the request in ${dataHolderOrIdpMode} mode.`) //REQUIRED ATTRIBUTES CHECKS FIRST if (!ssJwtBody.hasOwnProperty('client_name') || ssJwtBody.client_name == '') diff --git a/lib/udap_well_known.js b/lib/udap_well_known.js index 0c09644..171323c 100644 --- a/lib/udap_well_known.js +++ b/lib/udap_well_known.js @@ -18,7 +18,7 @@ module.exports.getUDAPConfiguration = () => { sub: serverCertSAN, authorize_endpoint: process.env.AUTHORIZE_ENDPOINT, token_endpoint: process.env.TOKEN_ENDPOINT, - registration_endpoint: process.env.REGISTRATION_ENDPOINT + registration_endpoint: process.env.IDP_REGISTRATION_ENDPOINT } return { "statusCode": 200,