Give the appplication a name, upload a logo, and click Continue.
Setting up the App details
2. Google Identity Provider details
Copy the details to GoodAccess - (3) Identity Provider links, and click Continue.
Sign in URL - SSO URL
Entity ID - Entity ID
X509 signing certificate - Certificate
Setting up the Google Identity Provider details
3. Service provider details
Copy the details from GoodAccess - (2) GoodAccess links, and click Continue.
ACS URL - Assertion Consumer Service URL
Entity ID -Entity ID
Start URL - Login URL
Name ID format - UNSPECIFIED
Name ID - Basic Information > Primary email
Setting up the Service provider details
4. Attribute mapping
Click ADD MAPPING, and add two attributes as follows:
Google Directory attributes
App attributes
Primary email
"email" (without quotes)
First name
"name" (without quotes)
Click Finish to confirm your settings.
Setting up Attribute mapping
If you want to set up SCIM, save the Provider ID for the next step, and click Submit.
If you don't want to set up SCIM, skip the next step in GoodAccess, and click Submit to finish the configuration.
You have now successfully set up your Google Workspace SSO with GoodAccess.
Step 3 (optional) - Setting up SCIM using an API
Since SCIM for Google Workspace is not currently supported for public use, it's necessary to use a combination of Google Apps Script and GoodAccess API Integration for complete user management.
The user provisioning time period depends on the Trigger settings (default is 1 hour).
Delete any placeholder code in the editor (e.g., function myFunction() {...}). Copy the following code snippet and paste it into the code editor:
Configuration Required
Before running the script, you must update the configuration variables at the top of the file:
Mandatory replacements
Replace these placeholders with your specific values:
<DOMAIN_NAME> - The verified domain of your organization in Google Workspace (e.g. goodaccess.com).
<PROVIDER_ID> - The Provider ID you obtained in the final step of the GoodAccess SSO configuration form.
<TOKEN> - The Token you obtained when creating the GoodAccess API Integration.
Define who gets synced (Sync Group)
How it works: Only users belonging to the designated Sync Group are provisioned into GoodAccess. However, all other Google Workspace groups (unless excluded below) are still synchronized, and your imported users will automatically be assigned to them based on their Google Workspace memberships.
You have two options to set up the Sync Group:
Use the default setup: Create a group in your Google Admin console named exactly sync-to-goodaccess and add the users you want to provision.
Use an existing group: If you already have a group for this purpose (e.g., vpn-users), change the prefix in the SYNC_GROUP_EMAIL variable. For example, change 'sync-to-goodaccess@' to 'vpn-users@' .
Optional filtering
Review and modify this list to exclude specific groups from synchronization:
EXCLUDED_GROUPS - Defines groups to ignore. You can use specific names (e.g., ga-example) or naming patterns with wildcards (e.g., gcp-* excludes any group starting with "gcp-").
3. Authorizing the script
Before automating the synchronization, you must save the script and run it manually once to grant the necessary permissions.
Click the Save project icon, and ensure the function syncUsers is selected.
Click the Run button, and click Review permissions.
You might see a screen asking to "Select what [Project Name] can access".
Ensure you check the "Select All" box to grant all the required permissions (view groups, see users, connect to external service).
If you instead see a "Google hasn't verified this app" warning, click Advanced and then Go to [Project Name] (unsafe).
Click Continue or Allow.
Wait for the execution log to start. If configured correctly, you should see "Execution started" and "Execution completed" at the bottom of the screen.
Authorizing the script
4. Creating a trigger for the script
In the left menu, go to Triggers, and click + Add Trigger.
Choose which function to run- syncUsers
Choose which deployment should run - Head
Select event source - Time-driven
Select type of time based trigger - Hour timer
Select hour interval - Every hour
Click Save.
Creating a trigger for the script
You have now successfully set up Google Workspace SCIM with GoodAccess.
Step 4 - Managing user access
In the application click User access.
Choose who should have access, select ON, and click Save.
/**
* CONFIGURATION
*/
const DOMAIN = '<DOMAIN_NAME>'; // The verified domain of your organization in Google Workspace (e.g. goodaccess.com)
const PROVIDER_ID = '<PROVIDER_ID>'; // The Provider ID you obtained in the final step of the GoodAccess SSO configuration form
const INTEGRATION_TOKEN = '<TOKEN>'; // The Token you obtained when creating the GoodAccess API Integration
/**
* SYNC GROUP:
* Only users belonging to this specific group will be synchronized to GoodAccess.
* Example: '[email protected]'
*/
const SYNC_GROUP_EMAIL = 'sync-to-goodaccess@' + DOMAIN;
/**
* EXCLUDED GROUPS:
* List of group names or patterns to ignore. Supports '*' as a wildcard.
* Examples: 'gcp-*', 'internal-testing', '*-temp'
*/
const EXCLUDED_GROUPS = [
"gcp-*"
];
/**
* MAIN FUNCTION
* Orchestrates the synchronization process.
*/
function syncUsers() {
// 1. Get IDs of all users who are members of the mandatory sync group
const authorizedUserIds = getAuthorizedUserIds();
if (authorizedUserIds.size === 0) {
Logger.log('No users found in the sync group: ' + SYNC_GROUP_EMAIL);
return;
}
// 2. Fetch detailed info for those authorized users
const users = getAllAuthorizedUsers(authorizedUserIds);
// 3. Fetch all groups and their members (filtered by exclusion list and authorization)
const groups = getFilteredGroups(authorizedUserIds);
// 4. Send data to GoodAccess
sendRequest(users, groups);
}
/**
* Retrieves a Set of user IDs that belong to the "sync-to-goodaccess" group.
*/
function getAuthorizedUserIds() {
const authorizedIds = new Set();
let pageToken;
try {
do {
const response = AdminDirectory.Members.list(SYNC_GROUP_EMAIL, {
pageToken: pageToken,
maxResults: 200 // Maximum allowed by API for members
});
if (response.members) {
response.members.forEach(member => {
// We only care about users, not nested groups or customers
if (member.type === 'USER') {
authorizedIds.add(member.id);
}
});
}
pageToken = response.nextPageToken;
} while (pageToken);
} catch (e) {
Logger.log('Error fetching sync group members: ' + e.message);
}
return authorizedIds;
}
/**
* Fetches user details (email, name) but only for users in the authorized set.
*/
function getAllAuthorizedUsers(authorizedUserIds) {
const members = {};
let pageToken;
do {
const response = AdminDirectory.Users.list({
domain: DOMAIN,
pageToken: pageToken,
maxResults: 500 // Increased limit for better performance
});
if (response.users) {
response.users.forEach(user => {
// Only include user if they are in the sync group
if (authorizedUserIds.has(user.id)) {
members[user.id] = {
email: user.primaryEmail,
name: user.name.fullName
};
}
});
}
pageToken = response.nextPageToken;
} while (pageToken);
return members;
}
/**
* Fetches all domain groups, filters them by exclusion patterns,
* and includes only members who are also in the authorized sync group.
*/
function getFilteredGroups(authorizedUserIds) {
const groups = {};
let pageToken;
do {
const response = AdminDirectory.Groups.list({
domain: DOMAIN,
pageToken: pageToken,
maxResults: 200
});
if (response.groups) {
response.groups.forEach(group => {
const groupEmail = group.getEmail();
const groupName = groupEmail.split("@")[0];
// Skip if group matches any excluded pattern
if (isExcluded(groupName)) {
return;
}
const members = getGroupMembers(groupEmail, authorizedUserIds);
// Only add group if it has at least one authorized member
if (members.length > 0) {
groups[group.id] = {
name: groupName,
members: members
};
}
});
}
pageToken = response.nextPageToken;
} while (pageToken);
return groups;
}
/**
* Gets members of a specific group, filtered by the authorized user list.
*/
function getGroupMembers(groupEmail, authorizedUserIds) {
let members = [];
let pageToken;
do {
const response = AdminDirectory.Members.list(groupEmail, {
pageToken: pageToken,
maxResults: 200
});
if (response.members) {
response.members.forEach(member => {
// Only add member if they are a user and exist in the authorized sync group
if (member.id != null && authorizedUserIds.has(member.id)) {
members.push(member.id);
}
});
}
pageToken = response.nextPageToken;
} while (pageToken);
return members;
}
/**
* Checks if a group name matches any of the excluded patterns (supports *).
*/
function isExcluded(groupName) {
return EXCLUDED_GROUPS.some(pattern => {
const regexPattern = pattern.replace(/\*/g, '.*');
const regex = new RegExp("^" + regexPattern + "$", "i");
return regex.test(groupName);
});
}
/**
* Sends the collected data to the GoodAccess API.
*/
function sendRequest(users, groups) {
const apiUrl = 'https://integration.goodaccess.com/api/v2/google-workspace/sync-users';
const payload = {
'domain': DOMAIN,
'users': users,
'groups': groups,
'provider_id': PROVIDER_ID
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': {
'authorization': INTEGRATION_TOKEN
},
'payload': JSON.stringify(payload),
'muteHttpExceptions': true
};
try {
const response = UrlFetchApp.fetch(apiUrl, options);
if (response.getResponseCode() == 200) {
Logger.log('The API request was successfully sent.');
} else {
Logger.log("Error sending API request. Code: " + response.getResponseCode() + " Body: " + response.getContentText());
}
} catch (e) {
Logger.log("Critical error in UrlFetchApp: " + e.message);
}
}