Skip to content

Commit

Permalink
Merge pull request #7 from dancinnamon-okta/idp_metadata
Browse files Browse the repository at this point in the history
Fixed issues with metadata for CSP registration
  • Loading branch information
TomLoomis-Evernorth authored Jan 4, 2024
2 parents a1a6508 + 6edd233 commit d6f762b
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 24 deletions.
7 changes: 4 additions & 3 deletions aws/tdcr_udap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions deploy/aws/auth0/serverless-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
24 changes: 24 additions & 0 deletions deploy/aws/okta/serverless-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion deploy/okta/okta_object_models.js
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 3 additions & 2 deletions lib/authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
Expand Down
23 changes: 19 additions & 4 deletions lib/okta/okta_object_models.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ module.exports.newUdapIdpModel = {
"url": ""
}
},
"scopes": [
"openid", "udap", "email", "profile"
],
"scopes": [],
"type": "OIDC",
"credentials": {
"client": {
Expand Down Expand Up @@ -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.",
Expand Down
40 changes: 35 additions & 5 deletions lib/okta/udap_okta.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const querystring = require('querystring')
//getAuthzPolicyDetails
//createAuthzPolicy
//updateAuthzPolicy
//updateIdpMapping

/*
PRIVATE METHODS
Expand Down Expand Up @@ -52,7 +53,6 @@ const getAuthzPolicyDetails = async (authorizationServerId, clientId, apiClient)
else {
return null
}

}

const createAuthzPolicy = async (authorizationServerId, clientId, grantTypes, scopes, apiClient) => {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -158,15 +184,18 @@ 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
idpModel.protocol.endpoints.userInfo.url = idpDetail.userInfoUrl
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 = {
Expand All @@ -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:')
Expand All @@ -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})
Expand All @@ -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',
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 10 additions & 8 deletions lib/tdcr_udap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 == '')
Expand Down
2 changes: 1 addition & 1 deletion lib/udap_well_known.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit d6f762b

Please sign in to comment.