Skip to content

Add New-EntraServicePrincipalKeyCredential #1487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
function New-EntraServicePrincipalKeyCredential {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, HelpMessage = "Specifies the unique identifier (ObjectId) of the service principal to which the key credential will be added.")]
[Alias("ObjectId")]
[ValidateNotNullOrEmpty()]
[System.String]$ServicePrincipalId,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
HelpMessage = "Specifies the value for the public key encoded in Base64.")]
[System.String]$Value,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "Specifies the type of key credential (e.g., AsymmetricX509Cert, Symmetric).")]
[ValidateSet('AsymmetricX509Cert', 'X509CertAndPassword')]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only set?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -> https://learn.microsoft.com/en-us/graph/api/serviceprincipal-addkey?view=graph-rest-1.0&tabs=http

Supported key types are:
AsymmetricX509Cert: The usage must be Verify.
X509CertAndPassword: The usage must be Sign

[System.String]$Type,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "Specifies the usage of the key credential (e.g., Sign, Verify).")]
[ValidateSet('Sign', 'Verify')]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only set?

[System.String]$Usage,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "A self-signed JWT token used as a proof of possession of the existing keys. This JWT token must be signed with a private key that corresponds to one of the existing valid certificates associated with the servicePrincipal.")]
[System.String]$Proof,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This param is not needed


[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Contain the password for the key. This property is required for keys of type X509CertAndPassword.")]
[System.String]$PasswordCredential,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This param is not needed.
Check the mapping below
image

New-EntraServicePrincipalKeyCredential should map to New-AzureADServicePrincipalKeyCredential.

Below are the params for New-AzureADServicePrincipalKeyCredential
https://github.com/Azure/azure-docs-powershell-azuread/blob/main/azureadps-2.0/AzureAD/New-AzureADServicePrincipalKeyCredential.md#syntax


[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Specifies a custom identifier for the key credential.")]
[System.String] $CustomKeyIdentifier,

[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Specifies the time when the key becomes valid as a DateTime object.")]
[System.Nullable[System.DateTime]] $StartDate,

[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
HelpMessage = "Specifies the time when the key becomes invalid as a DateTime object.")]
[System.Nullable[System.DateTime]] $EndDate
)

begin {
# Ensure connection to Microsoft Entra
if (-not (Get-EntraContext)) {
$errorMessage = "Not connected to Microsoft Graph. Use 'Connect-Entra -Scopes User.Read.All' to authenticate."
Write-Error -Message $errorMessage -ErrorAction Stop
return
}
}

PROCESS {

$customHeaders = New-EntraCustomHeaders -Command $MyInvocation.MyCommand
$customHeaders['Content-Type'] = 'application/json'
$baseUri = '/v1.0/servicePrincipals'
$URI = "$baseUri/$ServicePrincipalId/addKey"

$params = @{
keyCredential = @{
type = $Type
usage = $Usage
key = $Value
dateTimeStart = $StartDate
dateTimeEnd = $EndDate
customKeyIdentifier = $CustomKeyIdentifier
}
passwordCredential = $null
proof = $Proof
}

if ($Type -eq 'X509CertAndPassword') {
if ([string]::IsNullOrWhiteSpace($PSBoundParameters["PasswordCredential"])) {
$errorMessage = "The 'CustomKeyIdentifier' property is required for keys of type X509CertAndPassword"
Write-Error -Message $errorMessage -ErrorAction Stop
}
$params["PasswordCredential"] = @{
secretText = $PSBoundParameters["PasswordCredential"]
}
}

Write-Debug("============================ TRANSFORMATIONS ============================")
$params.Keys | ForEach-Object { "$_ : $($params[$_])" } | Write-Debug
Write-Debug("=========================================================================`n")

try {
$response = Invoke-GraphRequest -Headers $customHeaders -Uri $URI -Method "POST" -Body ($params | ConvertTo-Json -Depth 4)

$targetTypeList = @()
foreach ($data in $response) {
$target = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphKeyCredential
$data.PSObject.Properties | ForEach-Object {
$propertyName = $_.Name.Substring(0, 1).ToUpper() + $_.Name.Substring(1)
$propertyValue = $_.Value
$target | Add-Member -MemberType NoteProperty -Name $propertyName -Value $propertyValue -Force
}
$targetTypeList += $target
}
$targetTypeList
}
catch {
Write-Error "Failed to add key credential: $($_.Exception.Message)"
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
function New-EntraBetaServicePrincipalKeyCredential {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, HelpMessage = "Specifies the unique identifier (ObjectId) of the service principal to which the key credential will be added.")]
[Alias("ObjectId")]
[ValidateNotNullOrEmpty()]
[System.String]$ServicePrincipalId,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
HelpMessage = "Specifies the value for the public key encoded in Base64.")]
[System.String]$Value,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "Specifies the type of key credential (e.g., AsymmetricX509Cert, Symmetric).")]
[ValidateSet('AsymmetricX509Cert', 'X509CertAndPassword')]
[System.String]$Type,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "Specifies the usage of the key credential (e.g., Sign, Verify).")]
[ValidateSet('Sign', 'Verify')]
[System.String]$Usage,

[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = "A self-signed JWT token used as a proof of possession of the existing keys. This JWT token must be signed with a private key that corresponds to one of the existing valid certificates associated with the servicePrincipal.")]
[System.String]$Proof,

[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Contain the password for the key. This property is required for keys of type X509CertAndPassword.")]
[System.String]$PasswordCredential,

[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Specifies a custom identifier for the key credential.")]
[System.String] $CustomKeyIdentifier,

[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Specifies the time when the key becomes valid as a DateTime object.")]
[System.Nullable[System.DateTime]] $StartDate,

[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
HelpMessage = "Specifies the time when the key becomes invalid as a DateTime object.")]
[System.Nullable[System.DateTime]] $EndDate
)

begin {
# Ensure connection to Microsoft Entra
if (-not (Get-EntraContext)) {
$errorMessage = "Not connected to Microsoft Graph. Use 'Connect-Entra -Scopes User.Read.All' to authenticate."
Write-Error -Message $errorMessage -ErrorAction Stop
return
}
}

PROCESS {

$customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand
$customHeaders['Content-Type'] = 'application/json'
$baseUri = '/beta/servicePrincipals'
$URI = "$baseUri/$ServicePrincipalId/addKey"

$params = @{
keyCredential = @{
type = $Type
usage = $Usage
key = $Value
dateTimeStart = $StartDate
dateTimeEnd = $EndDate
customKeyIdentifier = $CustomKeyIdentifier
}
passwordCredential = $null
proof = $Proof
}

if ($Type -eq 'X509CertAndPassword') {
if ([string]::IsNullOrWhiteSpace($PSBoundParameters["PasswordCredential"])) {
$errorMessage = "The 'CustomKeyIdentifier' property is required for keys of type X509CertAndPassword"
Write-Error -Message $errorMessage -ErrorAction Stop
}
$params["PasswordCredential"] = @{
secretText = $PSBoundParameters["PasswordCredential"]
}
}

Write-Debug("============================ TRANSFORMATIONS ============================")
$params.Keys | ForEach-Object { "$_ : $($params[$_])" } | Write-Debug
Write-Debug("=========================================================================`n")

try {
$response = Invoke-GraphRequest -Headers $customHeaders -Uri $URI -Method "POST" -Body ($params | ConvertTo-Json -Depth 4)
$targetTypeList = @()
foreach ($data in $response) {
$target = New-Object Microsoft.Graph.Beta.PowerShell.Models.MicrosoftGraphKeyCredential
$data.PSObject.Properties | ForEach-Object {
$propertyName = $_.Name.Substring(0, 1).ToUpper() + $_.Name.Substring(1)
$propertyValue = $_.Value
$target | Add-Member -MemberType NoteProperty -Name $propertyName -Value $propertyValue -Force
}
$targetTypeList += $target
}
$targetTypeList
}
catch {
Write-Error "Failed to add key credential: $($_.Exception.Message)"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
# ------------------------------------------------------------------------------
BeforeAll {
if ((Get-Module -Name Microsoft.Entra.Applications) -eq $null) {
Import-Module Microsoft.Entra.Applications
}
Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force

$response = @{
"@odata.context" = 'https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.keyCredential'
"customKeyIdentifier" = $null
"endDateTime" = "01/15/2027 14:22:00"
"key" = $null
"keyId" = "aaaaaaaa-0b0b-1c1c-2d2d-333333"
"startDateTime" = "01/15/2025 14:22:00"
"type" = "AsymmetricX509Cert"
"usage" = "Verify"
}

Mock -CommandName Invoke-MgGraphRequest -MockWith { $response } -ModuleName Microsoft.Entra.Applications
Mock -CommandName Get-EntraContext -MockWith { @{Scopes = @("Application.ReadWrite.All") } } -ModuleName Microsoft.Entra.Applications
}

Describe "New-EntraServicePrincipalKeyCredential" {
Context "Test for New-EntraServicePrincipalKeyCredential" {

It "Should fail when ServicePrincipalId is null" {
{ New-EntraServicePrincipalKeyCredential -ServicePrincipalId -Value "U29mdHdhcmU=" -Type "AsymmetricX509Cert" -Usage "Verify" -Proof "c29tZVByb29m" } | Should -Throw "Missing an argument for parameter 'ServicePrincipalId'*"
}

It "Should fail when ServicePrincipalId is empty" {
{ New-EntraServicePrincipalKeyCredential -ServicePrincipalId ""} | Should -Throw "Cannot validate argument on parameter 'ServicePrincipalId'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again."
}

It "Should return created service principal key credential when ServicePrincipalId is valid and all required parameters are provided" {
$result = New-EntraServicePrincipalKeyCredential -ServicePrincipalId "aaaaaaaa-2222-1111-1111-cccccccccccc" -Value "U29mdHdhcmU=" -Type "AsymmetricX509Cert" -Usage "Verify" -Proof "c29tZVByb29m"
$result | Should -Not -BeNullOrEmpty

Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Applications -Times 1
}

It "Should fail when key type is X509CertAndPassword and PasswordCredential hasn't been provided" {
{ New-EntraServicePrincipalKeyCredential -ServicePrincipalId "aaaaaaaa-2222-1111-1111-cccccccccccc" -Value "U29mdHdhcmU=" -Type "X509CertAndPassword" -Usage "Sign" -Proof "c29tZVByb29m" } | Should -Throw "The 'CustomKeyIdentifier' property is required for keys of type X509CertAndPassword"
}

It "Should return created service principal key credential when PasswordCredential is provided for key type X509CertAndPassword" {
$result = New-EntraServicePrincipalKeyCredential -ServicePrincipalId "aaaaaaaa-2222-1111-1111-cccccccccccc" -Value "U29mdHdhcmU=" -Type "X509CertAndPassword" -Usage "Sign" -Proof "c29tZVByb29m" -PasswordCredential "MySecretPassword"
$result | Should -Not -BeNullOrEmpty

Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Applications -Times 1
}

It "Should contain 'User-Agent' header" {
$userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion New-EntraServicePrincipalKeyCredential"
$result = New-EntraServicePrincipalKeyCredential -ServicePrincipalId "aaaaaaaa-2222-1111-1111-cccccccccccc" -Value "U29mdHdhcmU=" -Type "AsymmetricX509Cert" -Usage "Verify" -Proof "c29tZVByb29m"
$result | Should -Not -BeNullOrEmpty
$userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion New-EntraServicePrincipalKeyCredential"
Should -Invoke -CommandName Invoke-GraphRequest -ModuleName Microsoft.Entra.Applications -Times 1 -ParameterFilter {
$Headers.'User-Agent' | Should -Be $userAgentHeaderValue
$true
}
}

It "Should execute successfully without throwing an error" {
# Disable confirmation prompts
$originalDebugPreference = $DebugPreference
$DebugPreference = 'Continue'
try {
# Act & Assert: Ensure the function doesn't throw an exception
{ New-EntraServicePrincipalKeyCredential -ServicePrincipalId "aaaaaaaa-2222-1111-1111-cccccccccccc" -Value "U29mdHdhcmU=" -Type "AsymmetricX509Cert" -Usage "Verify" -Proof "c29tZVByb29m" -Debug } | Should -Not -Throw
}
finally {
# Restore original confirmation preference
$DebugPreference = $originalDebugPreference
}
}
}

}
Loading