terraform-provider-azuread icon indicating copy to clipboard operation
terraform-provider-azuread copied to clipboard

Create a SSO SAML Signing Certificate

Open sseekamp0 opened this issue 3 years ago • 25 comments

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritise this request
  • Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritise the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

Description

I would like to open a new feature request to enable creation of a SSO Signing Certificate. This functionality currently exists via the API, but seems to be missing in the terraform provider.

API Reference

Azure AD service principle certificate only provides importing a certificate. This request would be for creating a SSO certificate.

New or Affected Resource(s)

  • azuread_service_principal_certificate

References

  • https://github.com/hashicorp/terraform-provider-azuread/issues/173 - there is a reference here to creating a certificate.

sseekamp0 avatar Jun 13 '22 18:06 sseekamp0

It is possible to create the certificate, but apparently, the activation has to be done manually or using Azure CLI. Also for some unknown reason the azuread_service_principal.this.saml_metadata_url is always null when trying to read it.

My code so far:

data "azuread_client_config" "current" {}

resource "random_uuid" "oauth2_permission_scope" {
  keepers = {
    domain_name = var.saml_identifier_uri
  }
}

resource "random_uuid" "app_role_user" {
  keepers = {
    domain_name = var.saml_identifier_uri
  }
}

resource "random_uuid" "app_role_msiam_access" {
  keepers = {
    domain_name = var.saml_identifier_uri
  }
}

resource "azuread_application" "this" {
  display_name = var.display_name
  owners       = [data.azuread_client_config.current.object_id]
  identifier_uris = [var.saml_identifier_uri]
  api {
    oauth2_permission_scope {
      id = random_uuid.oauth2_permission_scope.id
      admin_consent_description = "Allow the application to access ${var.display_name} on behalf of the signed-in user."
      admin_consent_display_name = "Access ${var.display_name}"
      user_consent_description = "Allow the application to access ${var.display_name} on behalf of the signed-in user."
      user_consent_display_name = "Access ${var.display_name}"
      enabled = true
      type = "User"
      value = "user_impersonation"
    }
  }

  app_role {
    allowed_member_types = ["User"]
    description          = "User"
    display_name         = "User"
    id                   = random_uuid.app_role_user.id
  }
  app_role {
    allowed_member_types = ["User"]
    description          = "msiam_access"
    display_name         = "msiam_access"
    id                   = random_uuid.app_role_msiam_access.id
  }

  optional_claims {
    saml2_token {
      essential = true
      name = "email"
      additional_properties = ["sam_account_name"]
    }
  }

  web {
    homepage_url = "https://${var.app_fqdn}"
    logout_url = "https://${var.app_fqdn}"
    redirect_uris = [
      "https://${var.app_fqdn}/saml/acs"
    ]

    implicit_grant {
      access_token_issuance_enabled = false
      id_token_issuance_enabled     = true
    }
  }
}

resource "azuread_service_principal" "this" {
  application_id               = azuread_application.this.application_id
  app_role_assignment_required = false
  owners                       = [data.azuread_client_config.current.object_id]

  feature_tags {
    enterprise = true
    gallery = true
    custom_single_sign_on = true
  }

  preferred_single_sign_on_mode = "saml"

  notification_email_addresses  = var.notification_emails
}

resource "tls_private_key" "this" {
  algorithm = "RSA"
  rsa_bits  = 2048
}

resource "tls_self_signed_cert" "this" {
  allowed_uses          = ["client_auth", "server_auth"]
  key_algorithm         = "RSA"
  private_key_pem       = tls_private_key.this.private_key_pem
  validity_period_hours = 4321
  subject {
    common_name = azuread_application.this.display_name
    organization = var.organisation_name
  }
}

resource "azuread_service_principal_certificate" "this" {
  service_principal_id = azuread_service_principal.this.id
  type                  = "AsymmetricX509Cert"
  value                 = tls_self_signed_cert.this.cert_pem
  end_date_relative     = "4320h"
}

resource "null_resource" "manual_certificate_approve" {
  provisioner "local-exec" {
    command = "echo '\n\n-= Please activate the SSO certificate  THEN RUN `touch /tmp/ididit`; I WILL WAIT HERE =-\n\n'; while ! test -f /tmp/ididit; do sleep 1; done"
  }
  depends_on = [azuread_service_principal_certificate.this]
}

resource "azuread_claims_mapping_policy" "this" {
  definition   = [
    jsonencode(
      {
        ClaimsMappingPolicy = {
          ClaimsSchema = [
            {
              ID            = "employeeid"
              JwtClaimType  = "name"
              SamlClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
              Source        = "user"
            },
            {
              ID            = "mail"
              JwtClaimType  = "mail"
              SamlClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
              Source        = "user"
            },
            {
              ID            = "groups"
              JwtClaimType  = "groups"
              SamlClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
              source        = "user"
            }
          ]
          IncludeBasicClaimSet = "true"
          Version              = 1
        }
      }
    ),
  ]
  display_name = "${var.display_name}_cmp"
}

resource "azuread_service_principal_claims_mapping_policy_assignment" "this" {
  claims_mapping_policy_id = azuread_claims_mapping_policy.this.id
  service_principal_id     = azuread_service_principal.this.id
}

data "azuread_service_principal" "this" {
  object_id = azuread_service_principal.this.id
}

data "http" "idp_metadata" {
  url = data.azuread_service_principal.this.saml_metadata_url
  request_headers = {
    Accept = "application/xml"
  }
  depends_on = [
    azuread_service_principal.this,
    azuread_service_principal_certificate.this
  ]
}

rajish avatar Jun 22 '22 10:06 rajish

I tried the certificate from the computer and generated with tls_self_signed_cert. It really needs to be activated manually on the SSO page. But it does not show up in "Federation Metadata". If you manually import the PFX certificate, it is displayed in "Federation Metadata".

I tried @rajish code. The certificate did not show up in "Federation Metadata"

NEViLLLLL avatar Jun 23 '22 06:06 NEViLLLLL

Regarding the metadata link, there's a workaround that conforms the Azure Graph documentation:

data "http" "idp_metadata" {
  url = "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/federationmetadata/2007-06/federationmetadata.xml?appid=${azuread_application.this.application_id}"
  request_headers = {
    Accept = "application/xml"
  }
  depends_on = [
    azuread_service_principal.this,
    azuread_service_principal_certificate.this
  ]
}

But the certificate activation is a pain for two reasons, see the commented out code:

resource "null_resource" "manual_certificate_approve" {
  provisioner "local-exec" {
    command = "echo '\n\n-= Please activate the SSO certificate  THEN RUN `touch /tmp/ididit`; I WILL WAIT HERE =-\n\n'; while ! test -f /tmp/ididit; do sleep 1; done"
    # TODO no thumbprint here
    # command = "az ad sp update --id ${azuread_application.this.application_id} --set preferredTokenSigningKeyThumbprint=${tls_self_signed_cert.this.thumbprint}"
  }
  depends_on = [azuread_service_principal_certificate.this]
}
  1. There's no way to retrieve the thumprint from the certificate.
  2. Even when I tried running the command in a terminal with manually pasted values I get an error:
This command or command group has been migrated to Microsoft Graph API. Please carefully review all breaking changes introduced during this migration: https://docs.microsoft.com/cli/azure/microsoft-graph-migration
The command failed with an unexpected error. Here is the traceback:
'GraphClient' object has no attribute 'service_principals'
Traceback (most recent call last):
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/knack/cli.py", line 231, in invoke
    cmd_result = self.invocation.execute(args)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 663, in execute
    raise ex
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 726, in _run_jobs_serially
    results.append(self._run_job(expanded_arg, cmd_copy))
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 718, in _run_job
    return cmd_copy.exception_handler(ex)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/commands.py", line 54, in graph_err_handler
    raise ex
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 697, in _run_job
    result = cmd_copy(params)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 333, in __call__
    return self.handler(*args, **kwargs)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/command_operation.py", line 240, in handler
    result = cached_put(self.cmd, setter, **setterargs)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 452, in cached_put
    return _put_operation()
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 446, in _put_operation
    result = operation(**kwargs)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/custom.py", line 988, in patch_service_principal
    object_id = _resolve_service_principal(graph_client.service_principals, identifier)
AttributeError: 'GraphClient' object has no attribute 'service_principals'
To open an issue, please run: 'az feedback'

rajish avatar Jun 23 '22 09:06 rajish

@rajish, not sure if this is a recent change from Microsoft but when I call this endpoint, and point to an existing service principal I've created with Terraform (that doesn't have a cert) it generates a certificate and activates it. Not sure if this is new or just the way my app registration and service principal are configured? the script I used to call the endpoint https://gist.github.com/brodster2/16dfc11cdb55e4a84e3903dfab9f4bf4

brodster2 avatar Jul 04 '22 09:07 brodster2

I tried the certificate from the computer and generated with tls_self_signed_cert. It really needs to be activated manually on the SSO page. But it does not show up in "Federation Metadata". If you manually import the PFX certificate, it is displayed in "Federation Metadata".

I tried @rajish code. The certificate did not show up in "Federation Metadata"

I'm actually having exactly the same issue, did you ever find a resolution?

Basically if I provide my own cert via a azuread_service_principal_certificate resource for some reason it just doesn't show up in the App Federation Metadata, but if I deactivate and add a new one that's been automatically generated via the Azure Portal that cert does show up?!

dcopestake avatar Jul 15 '22 16:07 dcopestake

Using the snippet from @rajish I was able to activate a token by using openssl to generate the thumbprint.

resource "null_resource" "manual_certificate_approve" {
  provisioner "local-exec" {
    command = "echo \"${tls_self_signed_cert.example.cert_pem}\" > /tmp/${tls_self_signed_cert.example.id}.pem"
    interpreter = ["/bin/bash", "-c"]
  }
  provisioner "local-exec" {
    command = "az ad sp update --id ${azuread_application.example.application_id} --set preferredTokenSigningKeyThumbprint=$(openssl x509 -in /tmp/${tls_self_signed_cert.example.id}.pem -noout -fingerprint | grep -oE '[:0-9A-F]{59}' | sed -e 's/://g')"
  }
  provisioner "local-exec" {
    command = "rm -rf /tmp/${tls_self_signed_cert.example.id}.pem"
    interpreter = ["/bin/bash", "-c"]
  }
  depends_on = [azuread_service_principal_certificate.example]
}

vschum avatar Jul 29 '22 16:07 vschum

Hey @vschum which az cli version did you run the exec command on? Doesn't seem to work on

{
  "azure-cli": "2.39.0",
  "azure-cli-core": "2.39.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}

Annihilatopia avatar Aug 03 '22 13:08 Annihilatopia

Worked for me with the following version.

{
  "azure-cli": "2.38.0",
  "azure-cli-core": "2.38.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}

vschum avatar Aug 03 '22 16:08 vschum

That is strange, I've downgraded az cli to 2.38.0 and I'm still getting a empty list response when trying to update preferredTokenSigningKeyThumbprint.

Couldn't find 'preferredTokenSigningKeyThumbprint=<REDACTED>' in ''. Available options: []

Annihilatopia avatar Aug 04 '22 05:08 Annihilatopia

@vschum I tried your solution and still the cert needed to be activated manually ,, does anyone have another solution maybe?

sherifkayad avatar Aug 12 '22 14:08 sherifkayad

"az ad sp update --id ${azuread_application.example.application_id} --set preferredTokenSigningKeyThumbprint=$(openssl x509 -in /tmp/${tls_self_signed_cert.example.id}.pem -noout -fingerprint | grep -oE '[:0-9A-F]{59}' | sed -e 's/://g')"

i'm also getting the following error when using the above code.

The command failed with an unexpected error. Here is the traceback: 'GraphClient' object has no attribute 'service_principals' Traceback (most recent call last): File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/knack/cli.py", line 231, in invoke cmd_result = self.invocation.execute(args) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 663, in execute raise ex File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 726, in _run_jobs_serially results.append(self._run_job(expanded_arg, cmd_copy)) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 718, in _run_job return cmd_copy.exception_handler(ex) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/commands.py", line 54, in graph_err_handler raise ex File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 697, in _run_job result = cmd_copy(params) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 333, in __call__ return self.handler(*args, **kwargs) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/command_operation.py", line 240, in handler result = cached_put(self.cmd, setter, **setterargs) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 452, in cached_put return _put_operation() File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 446, in _put_operation result = operation(**kwargs) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/custom.py", line 988, in patch_service_principal object_id = _resolve_service_principal(graph_client.service_principals, identifier) AttributeError: 'GraphClient' object has no attribute 'service_principals'

anwickes avatar Aug 17 '22 13:08 anwickes

This is working for me. Be interested to see if others are able to use it too.

resource "azuread_service_principal_certificate" "cert" {
  service_principal_id          = azuread_service_principal.app.id
  type                          = "AsymmetricX509Cert"
  value                         = file("sp.pem")
  end_date                      = var.sp_cert_end_date
}

resource "null_resource" "activate_saml_cert" {
  triggers = {
    value = azuread_service_principal_certificate.cert[0].value
    end_date = azuread_service_principal_certificate.cert[0].end_date
  }

  provisioner "local-exec" {
    command = <<EOT
    az login --service-principal --tenant $ARM_TENANT_ID -u $ARM_CLIENT_ID -p cert.pem --allow-no-subscriptions --output none
    thumbprint=$(openssl x509 -in sp.pem -noout -fingerprint | sed 's/SHA1 Fingerprint=//g; s/://g')
    az rest --method PATCH --uri 'https://graph.microsoft.com/v1.0/servicePrincipals/${azuread_service_principal.app.object_id}' --body "{'preferredTokenSigningKeyThumbprint':'$thumbprint'}" --headers Content-Type=application/json
    EOT
  }
}

anwickes avatar Aug 17 '22 14:08 anwickes

This is working for me. Be interested to see if others are able to use it too.

resource "azuread_service_principal_certificate" "cert" {
  service_principal_id          = azuread_service_principal.app.id
  type                          = "AsymmetricX509Cert"
  value                         = file("sp.pem")
  end_date                      = var.sp_cert_end_date
}

resource "null_resource" "activate_saml_cert" {
  triggers = {
    value = azuread_service_principal_certificate.cert[0].value
    end_date = azuread_service_principal_certificate.cert[0].end_date
  }

  provisioner "local-exec" {
    command = <<EOT
    az login --service-principal --tenant $ARM_TENANT_ID -u $ARM_CLIENT_ID -p cert.pem --allow-no-subscriptions --output none
    thumbprint=$(openssl x509 -in sp.pem -noout -fingerprint | sed 's/SHA1 Fingerprint=//g; s/://g')
    az rest --method PATCH --uri 'https://graph.microsoft.com/v1.0/servicePrincipals/${azuread_service_principal.app.object_id}' --body "{'preferredTokenSigningKeyThumbprint':'$thumbprint'}" --headers Content-Type=application/json
    EOT
  }
}

This sort of worked for me. While it seems to have activated my cert according to the Azure Portal, the cert is still not configured completely it seems. I receive the error "AADSTS500031: Cannot find signing certificate configured." when trying to test SSO, and the metadata does not contain the cert.

cruikshj avatar Aug 21 '22 01:08 cruikshj

I compared what is created by the UI and what is created by the azuread_service_principal_certificate resource. The difference is that the UI creates a certificate and adds it twice to the principal, once with the usage value of "Verify" and the other with "Sign". The resource only creates the "Verify" credential. I suspect a part of the problem is the assumption this resource provider is making here: https://github.com/hashicorp/terraform-provider-azuread/blob/cc4d2ced1633c23db9e9750f9a33903760e0928f/internal/helpers/credentials.go#L111. It is hard coded to only create credentials with "Verify" as the usage.

cruikshj avatar Aug 21 '22 03:08 cruikshj

Wow, great find John. Anyone with the skill to add the 2nd API call to the resource creation?

anwickes avatar Aug 21 '22 03:08 anwickes

I wonder if it is enough to make Usage configurable on azuread_service_principal_certificate.

cruikshj avatar Aug 21 '22 15:08 cruikshj

Bit of a novice on how this stuff all sits together but i'm assuming you would need to issue the API call twice upon resource creation? Once for the "verify", another for "sign"?

anwickes avatar Aug 21 '22 23:08 anwickes

I've had a bit of poke into this - the existing resource provider for certificates is available here -

-> https://github.com/hashicorp/terraform-provider-azuread/blob/main/internal/services/serviceprincipals/service_principal_certificate_resource.go

The last update to this was on Nov 12, 2021, but the underlying client it uses to controls resource was only updated to have addTokenSigningCertificate and set the thumbprint (although Im not sure setting the thumbprint is required - i think this might be done automatically by the aforementioned API call and this is somewhat validate by @brodster2) in February. See below for the commit.

-> https://github.com/manicminer/hamilton/commit/221ac23e6a123c6edb242590f0e9150c5f01e288

The existing resource provider uses a more involved method of making direct calls to the key endpoint (which addTokenSigningCertificate effectively wraps) and then doesn't set the thumbprint.

The two approaches are actually outlined in step 4 of the below of the documentation

  • https://learn.microsoft.com/en-us/graph/application-saml-sso-configure-api?tabs=go%2Cpowershell-script#activate-the-custom-signing-key

It should be enough to modify the resource creation routine to use addTokenSigningCertificate instead - the existing routines to may not need to change - e.g. deletion would still involve needing to look the key up by the key ID.

@manicminer I can look into prepping a PR to fix this if you would like? I'm currently fixing this in a private provider and once I've 100% confirmed this fixes behavior I can look into preparing an appropriate change?

EDIT:

This is effectively covered by the following issues & draft pull request

https://github.com/hashicorp/terraform-provider-azuread/issues/732 https://github.com/hashicorp/terraform-provider-azuread/pull/741

matt-tyler avatar Nov 28 '22 05:11 matt-tyler

hey hey 👋

thanks for your input, @cruikshj I followed your steps and I got the same result, my metadata file doesn't contain the certificate.

What is the status of this issue?

karol-treeline avatar Dec 14 '22 12:12 karol-treeline

This should be closed with https://github.com/hashicorp/terraform-provider-azuread/pull/968

https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal_token_signing_certificate

tagur87 avatar Jan 19 '23 17:01 tagur87

Tested and approved !

No need of https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal_certificate anymore

jallaix avatar Jan 19 '23 17:01 jallaix

Thanks for your work @tagur87, we've started this week creating an AD App for SAML singing and we needed this.

We've done this for including the rotation, in case it is useful for someone else:

resource "time_rotating" "saml-certificate" {
  rotation_years = 3
}

resource "azuread_service_principal_token_signing_certificate" "saml-certificate" {
  service_principal_id = azuread_service_principal.app.id
  display_name         = "CN=${var.app_name} SSO Certificate"
  end_date             = time_rotating.saml-certificate.rotation_rfc3339

  provisioner "local-exec" {
    command = <<-SHELL
      az ad sp update \
        --id ${self.service_principal_id} \
        --set preferredTokenSigningKeyThumbprint=${self.thumbprint}
    SHELL
  }
}

skgsergio avatar Jan 20 '23 08:01 skgsergio

I have been using the azuread_service_principal_token_signing_certificate but i cannot figure out how to set the Signing Option. Its default is Sign SAML Response, but i need to set it to Sign SAML Response and Assertion but cannot see any terraform option to do it.

Has anyone been successful with this?

Thanks

ojc97 avatar May 31 '23 16:05 ojc97

any updates on this issue? We are facing the same problem. Thx.

valentinahermann avatar Feb 16 '24 08:02 valentinahermann