The info on this page applies to Grouper v4 and above.
For Azure provisioning in Grouper versions before Grouper 2.6, see this page.
Grouper versioning information is here.

Demo

  • Movie:  Grouper provisioning framework Azure demo (v2.6.15):  https://youtu.be/abTkJVBMr1M
  • Config (grouper-loader.properties from version 2.6.15).  Note, you should configure this in the provisioning configuration wizard.

    grouper.azureConnector.myAzure.clientId = 7e590d54-d6af-4b07-b2aXXXXXXX
    grouper.azureConnector.myAzure.clientSecret = *******
    grouper.azureConnector.myAzure.graphEndpoint = https://graph.microsoft.com
    grouper.azureConnector.myAzure.graphVersion = beta
    grouper.azureConnector.myAzure.loginEndpoint = https://login.microsoftonline.com/
    grouper.azureConnector.myAzure.resource = https://graph.microsoft.com
    grouper.azureConnector.myAzure.resourceEndpoint = https://graph.microsoft.com/beta/
    grouper.azureConnector.myAzure.tenantId = 5e7fa4df-8d24-4c33-bdaXXXXXXXXXXX
    
    changeLog.consumer.provisioner_incremental_azureProvisioner.class = edu.internet2.middleware.grouper.changeLog.esb.consumer.EsbConsumer
    changeLog.consumer.provisioner_incremental_azureProvisioner.provisionerConfigId = azureProvisioner
    changeLog.consumer.provisioner_incremental_azureProvisioner.publisher.class = edu.internet2.middleware.grouper.app.provisioning.ProvisioningConsumer
    changeLog.consumer.provisioner_incremental_azureProvisioner.publisher.debug = false
    changeLog.consumer.provisioner_incremental_azureProvisioner.quartzCron = 0 * * * * ?
    
    otherJob.provisioner_full_azureProvisioner.class = edu.internet2.middleware.grouper.app.provisioning.GrouperProvisioningFullSyncJob
    otherJob.provisioner_full_azureProvisioner.provisionerConfigId = azureProvisioner
    otherJob.provisioner_full_azureProvisioner.quartzCron = 0 36 6 * * ?
    
    provisioner.azureProvisioner.addDisabledFullSyncDaemon = true
    provisioner.azureProvisioner.addDisabledIncrementalSyncDaemon = true
    provisioner.azureProvisioner.azureExternalSystemConfigId = myAzure
    provisioner.azureProvisioner.azureGroupType = true
    provisioner.azureProvisioner.class = edu.internet2.middleware.grouper.app.azure.GrouperAzureProvisioner
    provisioner.azureProvisioner.entityAttributeValueCache0entityAttribute = id
    provisioner.azureProvisioner.entityAttributeValueCache0has = true
    provisioner.azureProvisioner.entityAttributeValueCache0source = target
    provisioner.azureProvisioner.entityAttributeValueCache0type = entityAttribute
    provisioner.azureProvisioner.entityAttributeValueCacheHas = true
    provisioner.azureProvisioner.entityMatchingAttribute0name = userPrincipalName
    provisioner.azureProvisioner.entityMatchingAttributeCount = 1
    provisioner.azureProvisioner.groupAttributeValueCache0groupAttribute = id
    provisioner.azureProvisioner.groupAttributeValueCache0has = true
    provisioner.azureProvisioner.groupAttributeValueCache0source = target
    provisioner.azureProvisioner.groupAttributeValueCache0type = groupAttribute
    provisioner.azureProvisioner.groupAttributeValueCacheHas = true
    provisioner.azureProvisioner.groupMatchingAttribute0name = displayName
    provisioner.azureProvisioner.groupMatchingAttributeCount = 1
    provisioner.azureProvisioner.hasTargetEntityLink = true
    provisioner.azureProvisioner.hasTargetGroupLink = true
    provisioner.azureProvisioner.logAllObjectsVerbose = true
    provisioner.azureProvisioner.logCommandsAlways = true
    provisioner.azureProvisioner.numberOfEntityAttributes = 2
    provisioner.azureProvisioner.numberOfGroupAttributes = 3
    provisioner.azureProvisioner.operateOnGrouperEntities = true
    provisioner.azureProvisioner.operateOnGrouperGroups = true
    provisioner.azureProvisioner.operateOnGrouperMemberships = true
    provisioner.azureProvisioner.provisioningType = membershipObjects
    provisioner.azureProvisioner.selectAllEntities = true
    provisioner.azureProvisioner.showAdvanced = true
    provisioner.azureProvisioner.startWith = this is start with read only
    provisioner.azureProvisioner.subjectSourcesToProvision = jdbc
    provisioner.azureProvisioner.targetEntityAttribute.0.name = id
    provisioner.azureProvisioner.targetEntityAttribute.1.name = userPrincipalName
    provisioner.azureProvisioner.targetEntityAttribute.1.translateExpression = ${ grouperProvisioningEntity.subjectId + '@mchyzergmail.onmicrosoft.com' }
    provisioner.azureProvisioner.targetEntityAttribute.1.translateExpressionType = translationScript
    provisioner.azureProvisioner.targetGroupAttribute.0.insert = false
    provisioner.azureProvisioner.targetGroupAttribute.0.name = id
    provisioner.azureProvisioner.targetGroupAttribute.0.showAdvancedAttribute = true
    provisioner.azureProvisioner.targetGroupAttribute.0.showAttributeCrud = true
    provisioner.azureProvisioner.targetGroupAttribute.0.update = false
    provisioner.azureProvisioner.targetGroupAttribute.1.name = displayName
    provisioner.azureProvisioner.targetGroupAttribute.1.translateExpressionType = grouperProvisioningGroupField
    provisioner.azureProvisioner.targetGroupAttribute.1.translateFromGrouperProvisioningGroupField = extension
    provisioner.azureProvisioner.targetGroupAttribute.2.name = mailNickname
    provisioner.azureProvisioner.targetGroupAttribute.2.translateExpressionType = grouperProvisioningGroupField
    provisioner.azureProvisioner.targetGroupAttribute.2.translateFromGrouperProvisioningGroupField = extension
    
    

External system

grouper-loader.properties for local testing

grouper.azureConnector.myAzure.clientId = 51e6dc4f-a85d-41c7-9569-8ac1b3159801
grouper.azureConnector.myAzure.clientSecret = *******
grouper.azureConnector.myAzure.graphEndpoint = https://graph.microsoft.com
grouper.azureConnector.myAzure.graphVersion = beta
grouper.azureConnector.myAzure.groupLookupAttribute = displayName
grouper.azureConnector.myAzure.groupLookupValueFormat = ${group.getName()}
grouper.azureConnector.myAzure.loginEndpoint = https://login.microsoftonline.com/
grouper.azureConnector.myAzure.resource = https://graph.microsoft.com
grouper.azureConnector.myAzure.resourceEndpoint = https://graph.microsoft.com/beta/
grouper.azureConnector.myAzure.tenantId = 455754be-3a2b-40c9-acef-c425a92d7276


Provisioning fields and attributes

ItemTypeDescription
idfielduuid from Azure
displayNamefieldgroup name in Azure



Azure group types

Documentation

Configure group owners in Azure

provisioner.myAzureProvisioner.groupOwnersManage = true

provisioner.myAzureProvisioner.targetGroupAttribute.5.multiValued = true
provisioner.myAzureProvisioner.targetGroupAttribute.5.name = groupOwners
provisioner.myAzureProvisioner.targetGroupAttribute.5.showAdvancedAttribute = true
provisioner.myAzureProvisioner.targetGroupAttribute.5.showAttributeValueSettings = true
provisioner.myAzureProvisioner.targetGroupAttribute.5.translateExpression = ${provisioningGroupWrapper.thisGroupPrivilegeHolders('admins', 'entityAttributeValueCache2')}
provisioner.myAzureProvisioner.targetGroupAttribute.5.translateExpressionType = translationScript

provisioner.myAzureProvisioner.entityAttributeValueCache2entityAttribute = id
provisioner.myAzureProvisioner.entityAttributeValueCache2has = true
provisioner.myAzureProvisioner.entityAttributeValueCache2source = target
provisioner.myAzureProvisioner.entityAttributeValueCache2type = entityAttribute
provisioner.myAzureProvisioner.entityAttributeValueCacheHas = true


Grouper development team testing

Set this in grouper.hibernate.properties (or set env var: GROUPER_MOCK_SERVICES=true)

grouper.is.mockServices = true

test config

grouper.azureConnector.azureTest.clientId = fd805xxxxdfb
grouper.azureConnector.azureTest.clientSecret = *******
grouper.azureConnector.azureTest.graphEndpoint = https://graph.microsoft.com
grouper.azureConnector.azureTest.graphVersion = v1.0
grouper.azureConnector.azureTest.loginEndpoint = http://localhost:8400/grouper/mockServices/azure/auth/
grouper.azureConnector.azureTest.resource = https://graph.microsoft.com
grouper.azureConnector.azureTest.resourceEndpoint = http://localhost:8400/grouper/mockServices/azure/
grouper.azureConnector.azureTest.tenantId = 6c4dxxx0d


Set up Azure

  1. Sign up with Azure
  2. On the left menu, go to Azure Active Directory
  3. Create a new app registration
    1. Select: Who can use this app: Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)
  4. After the app is registered, click on API Permissions and give Microsoft graph access
    1. Give full permissions for Directory, Group, User, and GroupMember
    2. Grant admin consent for default directory
    3. Check https://jwt.ms with the token, should see
    4. Permissions look like this.  Note you can clamp down these permissions as needed




    5. From basic testing, if using read-only entities, the following Admin consent permissions seems to work (should not need any User consent grants):

    6. For even tight permissions, see below for setting the Grouper service account as the owner for new groups
  5. On the left, under Certificates and Secrets, create a new secret
  6. When testing using Postman, you will only need the secret value to get access token which will be used to call the graph API
  7. To get an access token, make a POST call to https://login.microsoftonline.com/a98c57b9-a771-4c01-b69b-83cceb36c834/oauth2/v2.0/token (id is the directory tenant id)
  8. Under form data send these four key values. client_id = clientId, scope = https://graph.microsoft.com/.default, client_secret = clientSecret, grant_type=client_credentials
    1. Content-type: application/x-www-form-urlencoded
    2. Post body looks like this:

      client_id=aea2eb2a-bc4f-4ae5-a315-3XXXXX&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&grant_type=client_credentials&client_secret=ewC8Q~yGN4dyBaSYBrOXXXXXXXXXX


    3. Configure external system in grouper-loader.properties

      grouper.azureConnector.azure.clientId = aea2eb2a-bc4f-4ae5-a315-38XXXXX
      grouper.azureConnector.azure.clientSecret = ewC8Q~yXXXXX
      grouper.azureConnector.azure.graphEndpoint = https://graph.microsoft.com
      grouper.azureConnector.azure.graphVersion = beta
      grouper.azureConnector.azure.loginEndpoint = https://login.microsoftonline.com/
      grouper.azureConnector.azure.resource = https://graph.microsoft.com
      grouper.azureConnector.azure.resourceEndpoint = https://graph.microsoft.com/beta/
      grouper.azureConnector.azure.tenantId = 5e7fa4df-8d24XXXXXXX


  9. The client id is the Application (client) ID next to Directory tenant id on the Overview page of the app.
  10. The response from the above POST call will give you an access token in the body which we will use to access graph APIs like https://graph.microsoft.com/v1.0/groups
  11. For the above request, send Authorization header with value Bearer <access token>


Add Microsoft certificate for graph apis

  1. Go to https://graph.microsoft.com/applications in your browser and download the certificate by clicking on the padlock sign in the address bar.
  2. Find out the path to the security directory inside the jre. e.g. /Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/jre/lib/security
  3. From the terminal run "sudo keytool -import -alias microsoft.graph -keystore cacerts -file ~/graph.microsoft.com.cer

  4. For the password enter: changeit

Setting the Grouper service account as the owner of new groups (to reduce Azure privileges) v4.2.0+

If the service account Grouper uses for Graph API calls can be set as the owner of a managed group, the Azure application no longer needs the privileges to update all groups and memberships. It only needs the Group.Create privilege to create a new group, Group.Read.All to find groups to match with Grouper, and User.Read.All to resolve target entities. While there is an option in the Azure provisioner to set the groupOwner attribute with one or more entities, the ability to add non-users (i.e. service accounts) is only available  v4.2.0.

The "groupOwners" attribute is normally expected to be a Grouper subject ID or identifier used for search/match to resolve to an Azure object URL. But this lookup always uses the /users api endpoint, which means the service account can't be added this way. Since v4.2.0, an already-resolved URL can be entered in this field, and this URL can be either for a user or a service account. There are two ways to specify the account.

1) https://graph.microsoft.com/v1.0/servicePrincipals/{id}

2) https://graph.microsoft.com/v1.0/servicePrincipals(appId='{appId}')

where {id} is the "Object ID" for the object in the Enterprise Applications page (it's not the App registrations page, but you can get to it from there by clicking "Managed application in local directory"). The {appId} is the application ID, also called the client ID, and is easier to find, being on the Enterprise applications page, the App registrations page, and various other pages when you look at the service account details. The appId is also the "Client ID" value in the external system configuration for Azure.

Azure Enterprise application properties