> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twine.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Entra ID

export const IntegrationMeta = ({homepageUrl, apiReferenceUrls, contactEmail, transport, status, domains, capabilities, notes}) => {
  const hasLinks = homepageUrl || apiReferenceUrls && apiReferenceUrls.length > 0 || contactEmail;
  const hasDomains = domains && domains.length > 0;
  const hasTransport = !!transport;
  const hasStatus = status === 'planned' || status === 'deprecated';
  const hasCapabilities = capabilities && capabilities.length > 0;
  const hasNotes = !!notes;
  if (!hasLinks && !hasDomains && !hasTransport && !hasStatus && !hasCapabilities && !hasNotes) return null;
  const pill = (bg, color, text) => <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    fontSize: '11px',
    fontWeight: 600,
    lineHeight: 1,
    padding: '3px 8px',
    borderRadius: '999px',
    background: bg,
    color: color,
    whiteSpace: 'nowrap'
  }}>
  {text}
</span>;
  const capabilityColors = {
    'read/write': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'read-write': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'read': ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)'],
    'write': ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)']
  };
  const transportColors = {
    'file': ['var(--colors-warning-light, #fef3c7)', 'var(--colors-warning, #b45309)'],
    'api': ['var(--colors-accent-light, #ede9fe)', 'var(--colors-accent, #7c3aed)'],
    'webhook': ['var(--colors-success-light, #dcfce7)', 'var(--colors-success, #16a34a)']
  };
  const statusConfig = {
    'planned': {
      colors: ['var(--colors-warning-light, #fef3c7)', 'var(--colors-warning, #b45309)'],
      text: 'Planned'
    },
    'deprecated': {
      colors: ['rgb(var(--primary-light) / .15)', 'var(--colors-content-secondary, #6b7280)'],
      text: 'Deprecated'
    }
  };
  const label = text => <span style={{
    fontSize: '11px',
    fontWeight: 600,
    textTransform: 'uppercase',
    letterSpacing: '0.05em',
    color: 'var(--colors-content-secondary, #6b7280)',
    whiteSpace: 'nowrap',
    alignSelf: 'start',
    paddingTop: '2px'
  }}>
  {text}
</span>;
  const cell = children => <div style={{
    display: 'flex',
    alignItems: 'center',
    flexWrap: 'wrap',
    gap: '8px'
  }}>
  {children}
</div>;
  return <>
<style>{`
  .integration-meta-link {
    font-size: 13px;
    color: var(--colors-primary);
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
    position: relative;
  }
  .integration-meta-link:hover {
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
  }
  .integration-meta-link::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: -1px;
    width: 100%;
    height: 1px;
    background: rgb(var(--primary-light));
    opacity: 0.5;
    transition: opacity 0.15s;
  }
  .integration-meta-link:hover::after {
    opacity: 1;
  }
`}</style>
<div style={{
    display: 'grid',
    gridTemplateColumns: 'max-content 1fr',
    alignItems: 'center',
    gap: '8px 16px',
    padding: '14px 16px',
    borderRadius: '8px',
    background: 'rgb(var(--primary-light) / .05)',
    border: '1px solid rgb(var(--primary-light) / .1)',
    marginBottom: '32px'
  }}>
{hasLinks && <>
{label('Links')}
{cell(<>
{homepageUrl && <a href={homepageUrl} target="\_blank" rel="noopener noreferrer" className="integration-meta-link">
Website
</a>}
{apiReferenceUrls && apiReferenceUrls.map(({url, label: l}) => <a key={url} href={url} target="\_blank" rel="noopener noreferrer" className="integration-meta-link">
{l || 'API Reference'}
</a>)}
{contactEmail && <a href={`mailto:${contactEmail}`} className="integration-meta-link">
{contactEmail}
</a>}
</>)}
</>}

      {hasStatus && <>
        {label('Status')}
        {cell(pill(...statusConfig[status].colors, statusConfig[status].text))}
      </>}

      {hasTransport && <>
        {label('Transport')}
        {cell(pill(...transportColors[transport] || transportColors['api'], transport === 'file' ? 'File' : transport === 'webhook' ? 'Webhook' : 'API'))}
      </>}

      {hasDomains && <>
        {label('Domains')}
        {cell(domains.map(({domain, capability}) => {
    const [capBg, capColor] = capabilityColors[capability] || capabilityColors['read'];
    return <span key={domain} style={{
      display: 'inline-flex',
      alignItems: 'stretch',
      borderRadius: '999px',
      overflow: 'hidden',
      whiteSpace: 'nowrap',
      fontSize: '12px',
      fontWeight: 600,
      lineHeight: 1
    }}>
              <span style={{
      padding: '4px 8px',
      background: 'rgb(var(--primary-light) / .3)',
      color: 'inherit',
      fontFamily: 'monospace'
    }}>{domain}</span>
              <span style={{
      padding: '4px 8px',
      background: capBg,
      color: capColor
    }}>{capability}</span>
            </span>;
  }))}
      </>}

      {hasCapabilities && <>
        {label('Capabilities')}
        <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '4px'
  }}>
          {capabilities.map(({label: capLabel, supported}) => <div key={capLabel} style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    fontSize: '13px'
  }}>
              <span style={{
    fontWeight: 700,
    fontSize: '12px',
    color: supported ? 'var(--colors-success, #16a34a)' : 'var(--colors-error, #dc2626)',
    width: '12px',
    flexShrink: 0
  }}>{supported ? '✓' : '✗'}</span>
              <span style={{
    color: 'inherit'
  }}>{capLabel}</span>
            </div>)}
        </div>
      </>}

      {hasNotes && <>
        {label('Notes')}
        <span style={{
    fontSize: '13px',
    color: 'var(--colors-content-secondary, #6b7280)',
    fontStyle: 'italic'
  }}>{notes}</span>
      </>}
    </div>

</>;
};

export const IntegrationHeader = ({name, category, logoUrl}) => <div style={{
  display: 'flex',
  alignItems: 'center',
  gap: '16px',
  marginBottom: '24px'
}}>
    {logoUrl && <div style={{
  width: '64px',
  height: '64px',
  borderRadius: '12px',
  background: '#ffffff',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  flexShrink: 0,
  boxShadow: '0 0 0 1px rgba(0,0,0,0.08)'
}}>
        <img src={logoUrl} alt={`${name} logo`} style={{
  width: '40px',
  height: '40px',
  objectFit: 'contain'
}} />
      </div>}
    <div>
      <h1 style={{
  margin: 0
}}>{name}</h1>
      <span style={{
  color: 'var(--colors-content-secondary)',
  fontSize: '14px'
}}>
        {category}
      </span>
    </div>
  </div>;

<IntegrationHeader name="Entra ID" category="Identity Providers" logoUrl="https://kbmni.upcloudobjects.com/public/systemlogos/entra-id-logo.svg" />

<IntegrationMeta
  homepageUrl="https://www.microsoft.com/security/business/microsoft-entra"
  apiReferenceUrls={[
{
  label: 'Microsoft Graph API',
  url: 'https://learn.microsoft.com/en-us/graph/overview',
},
]}
  transport="api"
  domains={[
{ domain: "org_unit", capability: "read" },
{ domain: "employee", capability: "read/write" },
]}
/>

The Entra ID integration uses the **Microsoft Graph API** to read and write user data directly against an Entra tenant. It is the right choice for customers whose user directory lives entirely in Entra. For customers running a hybrid setup with an on-premises Active Directory, the Twine [SCIM integration](/integrations/identity-providers/scim) should be used instead.

## Supported data

The integration covers the **Employee** domain. Within that domain, three categories of fields can be fetched:

* **Base fields** - the standard properties on the Microsoft Graph `user` resource (display name, mail, job title, department, account status, and so on).
* **Extension attributes** - the legacy `onPremisesExtensionAttributes` (`extensionAttribute1` through `extensionAttribute15`).
* **Custom security attributes** - the structured custom attributes feature in Entra.

### Configuring what is fetched

Which categories Twine fetches is configured at the System Integration level, separately from property mappings. This is because some categories, in particular custom security attributes, cannot be safely enumerated unless the integration has explicit permission to access them - so they have to be opted into before they become available for mapping.

A field that has not been enabled for fetching cannot be referenced in a property mapping for this integration.

## Authentication

There are two supported authentication modes. The right choice depends on whether the customer prefers to manage the Entra application themselves or let Twine manage it on their behalf. Certificate-based authentication is not currently supported for Entra.

### Customer-managed application

The customer creates an Entra application in their own tenant, grants it the necessary Microsoft Graph permissions, and provides the resulting credentials to Twine. In this mode the customer assumes full responsibility for the application's lifecycle, including key and secret rotation.

<Info>
  The exact set of fields the configuration form requires will be documented here.
</Info>

### Twine-managed application

Twine creates a custom Entra application in **its own tenant**, scoped to only the Microsoft Graph permissions needed for the configured fetch configuration. The customer is then redirected to the Microsoft consent flow to approve delegated access on behalf of their tenant.

In this mode Twine handles client secret rotation automatically. Secrets are deliberately kept short-lived: they are rotated every **14 days**.

## Permissions

Only the Microsoft Graph permissions strictly required by the configured fetch settings are requested. Enabling additional categories such as custom security attributes adds the corresponding permissions to the application; disabling them removes those permissions on the next configuration change.

## Limitations

* Only the **Employee** domain is supported. Groups, administrative units, and other directory objects are not currently fetched or written.
* **Hybrid Active Directory** environments (on-premises AD synced into Entra) should not use this integration. Use the [SCIM integration](/integrations/identity-providers/scim) instead.
* **Certificate-based authentication** is not currently supported.
