azure-ad-sso

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Azure AD SSO Integration Skill

Azure AD SSO集成技能

Overview

概述

This skill provides comprehensive guidance for implementing Azure AD (Entra ID) OAuth2/OIDC Single Sign-On for applications deployed on Kubernetes clusters, including access restriction by Azure AD groups.
本技能为部署在Kubernetes集群上的应用提供实现Azure AD(Entra ID)OAuth2/OIDC单点登录的全面指导,包括通过Azure AD组限制访问的配置方法。

Quick Reference

快速参考

Supported Applications

支持的应用程序

ApplicationProviderRedirect URI PatternGroup Sync
DefectDojo
azuread-tenant-oauth2
/complete/azuread-tenant-oauth2/
Yes
Grafana
azuread
/login/azuread
Yes
ArgoCD
microsoft
(Dex)
/api/dex/callback
Yes
Harbor
oidc
/c/oidc/callback
Yes
SonarQube
saml
or
oidc
/oauth2/callback/saml
Yes
OAuth2 Proxy
azure
/oauth2/callback
Yes
Keycloak
oidc
/realms/{realm}/broker/azure/endpoint
Yes
应用程序提供者重定向URI模式组同步
DefectDojo
azuread-tenant-oauth2
/complete/azuread-tenant-oauth2/
Grafana
azuread
/login/azuread
ArgoCD
microsoft
(Dex)
/api/dex/callback
Harbor
oidc
/c/oidc/callback
SonarQube
saml
or
oidc
/oauth2/callback/saml
OAuth2 Proxy
azure
/oauth2/callback
Keycloak
oidc
/realms/{realm}/broker/azure/endpoint

Authentication Flow Decision

认证流程决策

┌─────────────────────────────────────────────────────────────────┐
│                    Access Control Decision                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Q: Who should access this application?                         │
│                                                                  │
│  ├─ Everyone in tenant ──► appRoleAssignmentRequired=false      │
│  │                                                               │
│  └─ Specific groups ────► appRoleAssignmentRequired=true        │
│                           + Assign groups to Enterprise App     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                    访问控制决策                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Q: 哪些用户可以访问此应用?                         │
│                                                                  │
│  ├─ 租户内所有用户 ──► appRoleAssignmentRequired=false      │
│  │                                                               │
│  └─ 特定组 ────► appRoleAssignmentRequired=true        │
│                           + 将组分配到企业应用     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Implementation Workflow

实施工作流

Phase 1: Azure AD App Registration

阶段1:Azure AD应用注册

bash
undefined
bash
undefined

1. Create App Registration

1. 创建应用注册

APP_NAME="<application>-<environment>" REDIRECT_URI="https://<app-domain>/complete/<provider>/"
APP_ID=$(az ad app create
--display-name "$APP_NAME"
--sign-in-audience "AzureADMyOrg"
--web-redirect-uris "$REDIRECT_URI"
--query appId -o tsv)
echo "Application (client) ID: $APP_ID"
APP_NAME="<application>-<environment>" REDIRECT_URI="https://<app-domain>/complete/<provider>/"
APP_ID=$(az ad app create
--display-name "$APP_NAME"
--sign-in-audience "AzureADMyOrg"
--web-redirect-uris "$REDIRECT_URI"
--query appId -o tsv)
echo "应用(客户端)ID: $APP_ID"

2. Get Tenant ID

2. 获取租户ID

TENANT_ID=$(az account show --query tenantId -o tsv) echo "Directory (tenant) ID: $TENANT_ID"
TENANT_ID=$(az account show --query tenantId -o tsv) echo "目录(租户)ID: $TENANT_ID"

3. Create Client Secret

3. 创建客户端密钥

SECRET=$(az ad app credential reset
--id $APP_ID
--append
--years 1
--query password -o tsv)
echo "Client Secret: $SECRET" # Save immediately!
undefined
SECRET=$(az ad app credential reset
--id $APP_ID
--append
--years 1
--query password -o tsv)
echo "客户端密钥: $SECRET" # 请立即保存!
undefined

Phase 2: Enable Group Claims

阶段2:启用组声明

bash
undefined
bash
undefined

Enable security group claims in tokens

在令牌中启用安全组声明

az ad app update --id $APP_ID --set groupMembershipClaims=SecurityGroup
az ad app update --id $APP_ID --set groupMembershipClaims=SecurityGroup

Add Group.Read.All permission (delegated)

添加Group.Read.All权限(委托型)

az ad app permission add
--id $APP_ID
--api 00000003-0000-0000-c000-000000000000
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope
az ad app permission add
--id $APP_ID
--api 00000003-0000-0000-c000-000000000000
--api-permissions 5f8c59db-677d-491f-a6b8-5f174b11ec1d=Scope

Grant admin consent

授予管理员同意

az ad app permission admin-consent --id $APP_ID
undefined
az ad app permission admin-consent --id $APP_ID
undefined

Phase 3: Restrict Access by Group (CRITICAL)

阶段3:按组限制访问(关键步骤)

bash
undefined
bash
undefined

Get Service Principal object ID

获取服务主体对象ID

SP_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)
SP_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)

Enable user assignment requirement

启用用户分配要求

az ad sp update --id $SP_ID --set appRoleAssignmentRequired=true
az ad sp update --id $SP_ID --set appRoleAssignmentRequired=true

Get the group ID to restrict access

获取用于限制访问的组ID

GROUP_ID=$(az ad group show --group "G-Usuarios-<App>-Admin" --query id -o tsv)
GROUP_ID=$(az ad group show --group "G-Usuarios-<App>-Admin" --query id -o tsv)

Assign group to the application (only these users can login)

将组分配到应用(仅该组内的用户可登录)

az rest --method POST
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID/appRoleAssignments"
--headers "Content-Type=application/json"
--body "{ "principalId": "$GROUP_ID", "principalType": "Group", "appRoleId": "00000000-0000-0000-0000-000000000000", "resourceId": "$SP_ID" }"
undefined
az rest --method POST
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID/appRoleAssignments"
--headers "Content-Type=application/json"
--body "{ "principalId": "$GROUP_ID", "principalType": "Group", "appRoleId": "00000000-0000-0000-0000-000000000000", "resourceId": "$SP_ID" }"
undefined

Phase 4: Store Secret in Key Vault

阶段4:在Key Vault中存储密钥

bash
az keyvault secret set \
  --vault-name "<keyvault-name>" \
  --name "<app>-azuread-client-secret" \
  --value "$SECRET"
bash
az keyvault secret set \
  --vault-name "<keyvault-name>" \
  --name "<app>-azuread-client-secret" \
  --value "$SECRET"

Secret Management

密钥管理

SecretProviderClass Template

SecretProviderClass模板

yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: <app>-secrets
  namespace: <namespace>
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: "<managed-identity-client-id>"
    keyvaultName: "<keyvault-name>"
    tenantId: "<azure-tenant-id>"
    objects: |
      array:
        - |
          objectName: <app>-azuread-client-secret
          objectType: secret
          objectAlias: AZURE_AD_CLIENT_SECRET
  secretObjects:
    - secretName: <app>-azure-ad
      type: Opaque
      data:
        - objectName: AZURE_AD_CLIENT_SECRET
          key: client-secret
yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: <app>-secrets
  namespace: <namespace>
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: "<managed-identity-client-id>"
    keyvaultName: "<keyvault-name>"
    tenantId: "<azure-tenant-id>"
    objects: |
      array:
        - |
          objectName: <app>-azuread-client-secret
          objectType: secret
          objectAlias: AZURE_AD_CLIENT_SECRET
  secretObjects:
    - secretName: <app>-azure-ad
      type: Opaque
      data:
        - objectName: AZURE_AD_CLIENT_SECRET
          key: client-secret

Pod Volume Mount

Pod卷挂载

yaml
volumes:
  - name: secrets-store
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: "<app>-secrets"

volumeMounts:
  - name: secrets-store
    mountPath: "/mnt/secrets-store"
    readOnly: true
yaml
volumes:
  - name: secrets-store
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: "<app>-secrets"

volumeMounts:
  - name: secrets-store
    mountPath: "/mnt/secrets-store"
    readOnly: true

Application Configurations

应用程序配置

DefectDojo

DefectDojo

yaml
undefined
yaml
undefined

Enable SSO

启用SSO

extraEnv:
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_ENABLED value: "True"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY value: "<client-id>"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID value: "<tenant-id>"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET valueFrom: secretKeyRef: name: defectdojo key: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET

Group sync

  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GET_GROUPS value: "True"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS value: "True"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER value: "^G-Usuarios-DefectDojo-.*"

CRITICAL: For apps behind reverse proxy

  • name: DD_SECURE_PROXY_SSL_HEADER value: "True"
undefined
extraEnv:
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_ENABLED value: "True"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY value: "<client-id>"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID value: "<tenant-id>"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET valueFrom: secretKeyRef: name: defectdojo key: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET

组同步

  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GET_GROUPS value: "True"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS value: "True"
  • name: DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER value: "^G-Usuarios-DefectDojo-.*"

关键配置:反向代理后的应用需设置

  • name: DD_SECURE_PROXY_SSL_HEADER value: "True"
undefined

Grafana

Grafana

yaml
grafana.ini:
  auth.azuread:
    enabled: true
    name: Azure AD
    allow_sign_up: true
    client_id: "<client-id>"
    client_secret: "${GF_AUTH_AZUREAD_CLIENT_SECRET}"
    scopes: openid email profile
    auth_url: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
    token_url: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
    allowed_groups: "<admin-group-id> <viewer-group-id>"
    role_attribute_path: contains(groups[*], '<admin-group-id>') && 'Admin' || 'Viewer'
yaml
grafana.ini:
  auth.azuread:
    enabled: true
    name: Azure AD
    allow_sign_up: true
    client_id: "<client-id>"
    client_secret: "${GF_AUTH_AZUREAD_CLIENT_SECRET}"
    scopes: openid email profile
    auth_url: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
    token_url: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
    allowed_groups: "<admin-group-id> <viewer-group-id>"
    role_attribute_path: contains(groups[*], '<admin-group-id>') && 'Admin' || 'Viewer'

ArgoCD (via Dex)

ArgoCD(通过Dex)

yaml
configs:
  cm:
    dex.config: |
      connectors:
        - type: microsoft
          id: microsoft
          name: Azure AD
          config:
            clientID: "<client-id>"
            clientSecret: $dex.azure.clientSecret
            tenant: "<tenant-id>"
            redirectURI: https://<argocd-domain>/api/dex/callback
            groups:
              - <admin-group-id>
  rbac:
    policy.csv: |
      g, <admin-group-id>, role:admin
yaml
configs:
  cm:
    dex.config: |
      connectors:
        - type: microsoft
          id: microsoft
          name: Azure AD
          config:
            clientID: "<client-id>"
            clientSecret: $dex.azure.clientSecret
            tenant: "<tenant-id>"
            redirectURI: https://<argocd-domain>/api/dex/callback
            groups:
              - <admin-group-id>
  rbac:
    policy.csv: |
      g, <admin-group-id>, role:admin

Harbor

Harbor

yaml
externalURL: https://harbor.<domain>
core:
  oidc:
    name: "azure"
    endpoint: "https://login.microsoftonline.com/<tenant-id>/v2.0"
    clientId: "<client-id>"
    clientSecret: "<from-secret>"
    scope: "openid,profile,email"
    groupsClaim: "groups"
    adminGroup: "<admin-group-id>"
    autoOnboard: true
yaml
externalURL: https://harbor.<domain>
core:
  oidc:
    name: "azure"
    endpoint: "https://login.microsoftonline.com/<tenant-id>/v2.0"
    clientId: "<client-id>"
    clientSecret: "<from-secret>"
    scope: "openid,profile,email"
    groupsClaim: "groups"
    adminGroup: "<admin-group-id>"
    autoOnboard: true

Troubleshooting

故障排查

Error Reference

错误参考

Error CodeDescriptionSolution
AADSTS50011Reply URL mismatchVerify exact redirect URI including trailing slash
AADSTS50105User not assignedAdd user/group to Enterprise App assignments
AADSTS700016App not foundCheck client ID and tenant ID
AADSTS7000218Secret expiredRotate secret in Key Vault, restart pods
AADSTS90102Invalid redirect_uriCheck
DD_SECURE_PROXY_SSL_HEADER=True
for reverse proxy
AADSTS65001Consent not grantedRun
az ad app permission admin-consent
错误代码描述解决方案
AADSTS50011回复URL不匹配验证重定向URI是否完全一致,包括末尾斜杠
AADSTS50105用户未被分配权限将用户/组添加到企业应用的分配列表中
AADSTS700016应用未找到检查客户端ID和租户ID是否正确
AADSTS7000218密钥已过期在Key Vault中轮换密钥,重启Pod
AADSTS90102无效的redirect_uri对于反向代理后的应用,检查是否设置了
DD_SECURE_PROXY_SSL_HEADER=True
AADSTS65001未授予同意权限执行
az ad app permission admin-consent
命令

Common Issues

常见问题

Malformed redirect_uri (Django apps behind proxy)

格式错误的redirect_uri(反向代理后的Django应用)

Symptom:
redirect_uri=https,%20https://...
Root cause:
DD_SECURE_PROXY_SSL_HEADER
set incorrectly
Fix:
yaml
- name: DD_SECURE_PROXY_SSL_HEADER
  value: "True"  # NOT "HTTP_X_FORWARDED_PROTO,https"
症状:
redirect_uri=https,%20https://...
根因:
DD_SECURE_PROXY_SSL_HEADER
配置错误
修复方案:
yaml
- name: DD_SECURE_PROXY_SSL_HEADER
  value: "True"  # 不要设置为"HTTP_X_FORWARDED_PROTO,https"

Groups not syncing

组未同步

bash
undefined
bash
undefined

Verify group claims enabled

验证组声明是否启用

az ad app show --id <app-id> --query groupMembershipClaims
az ad app show --id <app-id> --query groupMembershipClaims

Check API permissions

检查API权限

az ad app permission list --id <app-id>
az ad app permission list --id <app-id>

Verify group exists and user is member

验证组是否存在且用户为组成员

az ad group member check --group "<group-name>" --member-id "<user-object-id>"
undefined
az ad group member check --group "<group-name>" --member-id "<user-object-id>"
undefined

Secret not syncing from Key Vault

密钥未从Key Vault同步

bash
undefined
bash
undefined

Check SecretProviderClass

检查SecretProviderClass配置

kubectl describe secretproviderclass <name> -n <namespace>
kubectl describe secretproviderclass <name> -n <namespace>

Check CSI driver pods

检查CSI驱动Pod状态

kubectl get pods -n kube-system | grep secrets-store
kubectl get pods -n kube-system | grep secrets-store

Check managed identity access

验证托管身份的访问权限

az keyvault show --name <vault> --query properties.accessPolicies
undefined
az keyvault show --name <vault> --query properties.accessPolicies
undefined

Diagnostic Commands

诊断命令

bash
undefined
bash
undefined

Test OAuth redirect

测试OAuth重定向

curl -sS -k -D - -o /dev/null "https://<app>/login/<provider>/" 2>&1 | grep -i location
curl -sS -k -D - -o /dev/null "https://<app>/login/<provider>/" 2>&1 | grep -i location

Check environment variables in pod

检查Pod中的环境变量

kubectl exec -n <ns> deploy/<app> -c <container> -- env | grep -i azure
kubectl exec -n <ns> deploy/<app> -c <container> -- env | grep -i azure

Decode JWT token (after login, from browser dev tools)

解码JWT令牌(登录后从浏览器开发者工具获取)

Use https://jwt.io to decode and verify claims

undefined
undefined

Security Best Practices

安全最佳实践

  1. Never hardcode secrets - Always use Key Vault + CSI Driver
  2. Use managed identities - Avoid service principal credentials
  3. Restrict access by group - Enable
    appRoleAssignmentRequired=true
  4. Rotate secrets - Set calendar reminders before expiration
  5. Use HTTPS only - All redirect URIs must use HTTPS
  6. Single tenant - Never use multi-tenant for internal apps
  7. Audit logging - Enable Azure AD sign-in logs
  1. 切勿硬编码密钥 - 始终使用Key Vault + CSI驱动
  2. 使用托管身份 - 避免使用服务主体凭据
  3. 按组限制访问 - 启用
    appRoleAssignmentRequired=true
  4. 定期轮换密钥 - 在密钥过期前设置日历提醒
  5. 仅使用HTTPS - 所有重定向URI必须使用HTTPS
  6. 单租户模式 - 内部应用切勿使用多租户模式
  7. 启用审计日志 - 开启Azure AD登录日志

Environment Reference

环境参考

EnvironmentKey VaultManaged IdentityTenant ID
cafehyna-dev
kv-cafehyna-dev-hlg
f1a14a8f-6d38-40a0-a935-3cdd91a25f47
3f7a3df4-f85b-4ca8-98d0-08b1034e6567
cafehyna-hub
kv-cafehyna-default
f1a14a8f-6d38-40a0-a935-3cdd91a25f47
3f7a3df4-f85b-4ca8-98d0-08b1034e6567
cafehyna-prd
kv-cafehyna-prd
f1a14a8f-6d38-40a0-a935-3cdd91a25f47
3f7a3df4-f85b-4ca8-98d0-08b1034e6567
环境Key Vault托管身份租户ID
cafehyna-dev
kv-cafehyna-dev-hlg
f1a14a8f-6d38-40a0-a935-3cdd91a25f47
3f7a3df4-f85b-4ca8-98d0-08b1034e6567
cafehyna-hub
kv-cafehyna-default
f1a14a8f-6d38-40a0-a935-3cdd91a25f47
3f7a3df4-f85b-4ca8-98d0-08b1034e6567
cafehyna-prd
kv-cafehyna-prd
f1a14a8f-6d38-40a0-a935-3cdd91a25f47
3f7a3df4-f85b-4ca8-98d0-08b1034e6567

Detailed Reference

详细参考

For complete implementation examples:
  • references/azure-ad-sso-guide.md - Full guide with manifests
  • references/app-configs.md - Application-specific configurations
  • references/troubleshooting.md - Extended troubleshooting guide
完整的实施示例请查看:
  • references/azure-ad-sso-guide.md - 包含清单文件的完整指南
  • references/app-configs.md - 应用程序专属配置
  • references/troubleshooting.md - 扩展故障排查指南