Okta SCIM provisioning
With AWS Lambda Function
Why introducing AWS Lambda function
The main reason the integration is leveraging a Lamdba function is to overcome the lack of support in Okta for OAuth client credentials.
Source:
https://developer.okta.com/docs/concepts/scim/faqs/
Q: How should I be managing authentication to my SCIM API?
Okta recommends using the OAuth 2.0 Authorization Code grant flow. Okta doesn't support the Client Credentials or Resource Owner Password Credentials Authorization grant flows for SCIM. The Authorization Code grant flow is more common in SaaS and cloud integrations and is also more secure.
Comment: Okta is stating that Authorization Code grant flow is more secure, but it means that when Okta SCIM App needs to authenticate to Password Safe, it would actually be Password Safe that would have to authenticate back to Okta, using Client ID and Client Secret generated by Okta. This translate into PAM trusting the Directory, when it should be the other around, from a Security Best Practices point of view.
Another reason is to enable the support for Okta Group Push. Password Safe requires a description attribute at creation time, and Okta only provides a displayName attribute, which makes the Okta Push provisioning request to fail, being an invalid request missing the mandatory attribute description.
Important
Third-party documentation is subject to change. Updates might not be reflected in BeyondTrust documentation. For the most up-to-date information, visit https://docs.aws.amazon.com/lambda/latest/dg/welcome.html.
AWS Lambda function
Prerequisites and configuration
- AWS instance or AWS free tier: https://aws.amazon.com/free
- Nodejs axios layer: https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html
- Function code:
import axios from 'axios';
export const handler = async (event) => {
// ####### Extract host and endpoint from Request Url ####################
console.log('event INCOMING = '+JSON.stringify(event));
const method = event.requestContext.http.method;
console.log('event method = '+method);
const endpoint = 'https:/'+event.rawPath;
let url = '';
if(event.rawQueryString){
url = endpoint+'?'+event.rawQueryString;
} else{
url = endpoint;
}
var host = event.rawPath.substring(1);
host = host.substring(0,host.indexOf('/'));
console.log('host = '+host+' endpoint = '+endpoint);
// ####### Extract values from base64 token ####################
const base64token = event.headers.authorization.substring(7);
const base64payload = Buffer.from(base64token, 'base64').toString('ascii');
console.log('base64token = '+base64token+'. base64payload = '+base64payload);
const base64obj = JSON.parse(base64payload);
const authUrl = base64obj.authURL;
const client_id = base64obj.client_id;
const client_secret = base64obj.client_secret;
const target = base64obj.target;
console.log('target = '+target+' authUrl = '+authUrl+'. client_id = '+client_id+' client_secret = '+client_secret);
// Change Header host value for gateway
event.headers.host = host;
// ####### Authenticate ####################
const data = 'grant_type=client_credentials';
const authorization = 'Basic ' + Buffer.from(client_id+':'+client_secret).toString('base64');
const authConfig = {
method: 'post',
rejectUnauthorized: false,
url: authUrl,
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': authorization
}
};
console.log('authConfig = '+JSON.stringify(authConfig));
let authRes = await axios(authConfig);
console.log('authRes = '+JSON.stringify(authRes.data));
const access_token = authRes.data.access_token;
// ####### GET/POST/PATCH/PUT/DELETE data from target #########
If (target == "PasswordSafe" && endpoint == 'https://'+host+'/scim/v2/Groups' && method == 'POST'){
console.log('We have a Password Safe Create Group call body : '+JSON.stringify(event.body));
const body = JSON.parse(event.body);
const configG = {
method: method,
url: url,
headers: {
'content-type': event.headers['content-type'],
accept: event.headers.accept,
authorization: 'Bearer '+access_token,
},
"data": {
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"displayName": body.displayName,
"description": body.displayName,
"members": []
}
};
console.log('Password Safe Create Group: configG = '+JSON.stringify(configG));
let resG = await axios(configG);
// Response - For testing only
resG.headers['content-type'] = 'application/scim+json';
console.log('axios res data = '+JSON.stringify(resG.data));
console.log('axios res headers = '+JSON.stringify(resG.headers));
const response = {
statusCode: 200,
body: JSON.stringify(resG.data),
headers: resG.headers
};
return response;
} else{
if(event.headers['content-type']) {'Content-Type = '+console.log(event.headers['content-type']);}
let config = {};
if(event.body) {
console.log('body = '+JSON.stringify(event.body));
config = {
method: method,
url: url,
headers: {
'content-type': event.headers['content-type'],
accept: event.headers.accept,
authorization: 'Bearer '+access_token,
},
data: JSON.parse(event.body)
};
} else {
config = {
method: method,
url: url,
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer '+access_token
}
};
}
console.log('axios config = '+JSON.stringify(config));
let res = await axios(config);
// Response – For Okta
res.headers['content-type'] = 'application/scim+json';
console.log('axios res data = '+JSON.stringify(res.data));
console.log('axios res headers = '+JSON.stringify(res.headers));
const response = {
statusCode: 200,
body: JSON.stringify(res.data),
headers: res.headers
};
return response;
}
};
AWS Lambda function
The Lambda function code includes multiple console.log() items to facilitate troubleshooting via AWS CloudWatch.
Note
You must configure a Function Url with authentication set to NONE.
Before configuring Okta SCIM
Before configuration Okta SCIM App
SCIM 2.0 Base Url and OAuth Bearer Token
The SCIM 2.0 Base Url is the Function URL with the Password Safe Url (minus https://) appended to it, for example:
- We need to create a SCIM Service Account for Okta in Password Safe, and add it to a group with the following Features and Smart Group permissions:
- Read-only permissions to all Managed Account Smart Groups is required.
Note
Only the Managed Account Smart Group with also the Category defined as Managed Account are visible via SCIM:
To create the Bearer Token for Okta, we must first create a JSON document:
{"target":"PasswordSafe","authURL":"https://pws_instance.ps.beyondtrustcloud.com/scim/oauth/token","client_id":"b0d00123456abcde4a7611b9","client_secret":"63eee16d6b3456789012vfdes1bc5e6"}
Then we need to create a base64 encoded version, e.g. by using https://base64encode.org:
eyJ0YXJnZXQiOiJQYXNzd29yZFNhZmUiLCJhdXRoVVJMIjoiaHR0cHM6Ly9wd3NfaW5zdGFuY2UucHMuYmV5b25kdHJ1c3RjbG91ZC5jb20vc2NpbS9vYXV0aC90b2tlbiIsImNsaWVudF9pZCI6ImIwZDAwMTIzNDU2YWJjZGU0YTc2MTFiOSIsImNsaWVudF9zZWNyZXQiOiI2M2VlZTE2ZDZiMzQ1Njc4OTAxMnZmZGVzMWJjNWU2In0=
Configure Okta SCIM app
SCIM 2.0 Base Url and OAuth Bearer Token
- Under Applications, select Browse Catalog, and select SCIM 2.0 Test App (OAuth Bearer Token).
- Under Provisioning/Integration, provide the URL and token.
- Under Settings/To App, check all the boxes and select the Password Sync option.
You will have to modify the Password Policy under Security|Authentication to match the Password Safe password policy for Users:
- Under Sign On, select Email prefix for Username.
- You may need to modify the Profile Mapping for the App.
Supported use cases
Import and create in Okta.
- Push group.
- Assign Okta Users (Create in Password Safe).
- Import Password Safe Groups and add/remove members that exist on both sides.
Updated 6 days ago