Skip to content
Merged
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
1 change: 1 addition & 0 deletions .azure-pipelines/command-metadata-refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ extends:
targetType: inline
script: |
. "$(System.DefaultWorkingDirectory)\tools\Versions\BumpModuleVersion.ps1" -BumpV1Module -BumpBetaModule -BumpAuthModule -Debug
- template: .azure-pipelines/common-templates/get-github-app-token.yml@self
- task: Bash@3
displayName: Push version bump changes
env:
Expand Down
2 changes: 2 additions & 0 deletions .azure-pipelines/common-templates/create-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ parameters:
default: ""

steps:
- template: ./get-github-app-token.yml

- task: PowerShell@2
displayName: Create Pull Request for generated build
env:
Expand Down
2 changes: 2 additions & 0 deletions .azure-pipelines/common-templates/download-openapi-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ steps:
script: dotnet run
workingDirectory: "$(System.DefaultWorkingDirectory)/tools/OpenApiInfoGenerator/OpenApiInfoGenerator"

- template: ./get-github-app-token.yml

- task: Bash@3
displayName: Commit downloaded files
condition: and(succeeded(), ne(variables['ModuleGenerationList'], ''))
Expand Down
45 changes: 45 additions & 0 deletions .azure-pipelines/common-templates/get-github-app-token.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

# Fetches the Microsoft Graph DevX GitHub App credentials from Key Vault and mints a short-lived
# installation access token, exposing it to subsequent steps as the secret variable GITHUB_TOKEN.
#
# Include this template immediately before any step that pushes to GitHub or creates a pull
# request. Downstream steps continue to reference $(GITHUB_TOKEN) exactly as before, but the value
# is now a GitHub App installation token instead of a long-lived PAT. Because installation tokens
# expire after ~1 hour, generate the token right before it is used rather than once per job.

parameters:
- name: RepoName
type: string
default: microsoftgraph/msgraph-sdk-powershell

steps:
- task: AzureKeyVault@2
displayName: "Azure Key Vault: Get GitHub App secrets"
inputs:
azureSubscription: "Federated AKV Managed Identity Connection"
KeyVaultName: akv-prod-eastus
SecretsFilter: "microsoft-graph-devx-bot-appid,microsoft-graph-devx-bot-privatekey"

- task: PowerShell@2
displayName: "Generate GitHub App installation token"
env:
GhAppId: $(microsoft-graph-devx-bot-appid)
GhAppKey: $(microsoft-graph-devx-bot-privatekey)
inputs:
pwsh: true
targetType: inline
errorActionPreference: stop
script: |
$token = & "$(System.DefaultWorkingDirectory)/scripts/Generate-Github-Token.ps1" `
-AppClientId $env:GhAppId `
-AppPrivateKeyContents $env:GhAppKey `
-Repository "${{ parameters.RepoName }}"
if ([string]::IsNullOrWhiteSpace($token)) {
throw "Failed to generate GitHub App installation token (empty result)."
}
# Mask the token so it can never surface in pipeline logs, then expose it to later steps.
Write-Host "##vso[task.setsecret]$token"
Write-Host "##vso[task.setvariable variable=GITHUB_TOKEN;issecret=true]$token"
Write-Host "Generated GitHub App installation token and set GITHUB_TOKEN."
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ steps:
script: |
. $(System.DefaultWorkingDirectory)/tools/PostGeneration/AuthModuleMetadata.ps1
- template: ../common-templates/get-github-app-token.yml

- task: Bash@3
displayName: Push command metadata
enabled: true
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions .azure-pipelines/weekly-examples-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ extends:
targetType: 'filePath'
pwsh: true
filePath: tools\ImportExamples.ps1
- template: .azure-pipelines/common-templates/get-github-app-token.yml@self
- task: PowerShell@2
displayName: Pushing to github
env:
Expand Down
213 changes: 213 additions & 0 deletions scripts/Generate-Github-Token.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

# Generates a short-lived GitHub App installation access token for the specified repository.
#
# The token is minted by:
# 1. Building an RS256-signed JWT from the App's client id and private key.
# 2. Resolving the App installation id for the owner (org, then repo, then user scope).
# 3. Exchanging the JWT for a repository-scoped installation access token.
#
# The resulting token is valid for ~1 hour and should be generated immediately before it is
# used (e.g. right before a git push or PR creation). Emitted on the pipeline via Write-Output.

[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$AppClientId,
[Parameter(Mandatory = $true)]
[string]
$AppPrivateKeyContents,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$', ErrorMessage = "Repository must be in the format 'owner/repo' (e.g. 'octocat/hello-world')")]
[string]
$Repository
)

$ErrorActionPreference = "Stop"

function Generate-AppToken {
param (
[string]
$ClientId,
[string]
$PrivateKeyContents
)

$header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
alg = "RS256"
typ = "JWT"
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');

$payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds()
exp = [System.DateTimeOffset]::UtcNow.AddMinutes(1).ToUnixTimeSeconds()
iss = $ClientId
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');

$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem($PrivateKeyContents)

$signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$header.$payload.$signature"

return $jwt
}

function Generate-InstallationToken {
param (
[string]
$AppToken,
[string]
$InstallationId,
[string]
$Repository
)

$uri = "https://api.github.com/app/installations/$InstallationId/access_tokens"
$headers = @{
Authorization = "Bearer $AppToken"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}

$body = @{
repositories = @($Repository)
}

$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body (ConvertTo-Json -InputObject $body -Compress -Depth 10)

return $response.token
}

function Get-OrganizationInstallationId {
param (
[string]
$AppToken,
[string]
$Organization
)

$uri = "https://api.github.com/orgs/$Organization/installation"
$headers = @{
Authorization = "Bearer $AppToken"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}

try {
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers

return $response.id
}
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
return $null
}

throw
}
}

function Get-RepositoryInstallationId {
param (
[string]
$AppToken,
[string]
$Repository
)

$uri = "https://api.github.com/repos/$Repository/installation"
$headers = @{
Authorization = "Bearer $AppToken"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}

try {
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers

return $response.id
}
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
return $null
}

throw
}
}

function Get-UserInstallationId {
param (
[string]
$AppToken,
[string]
$Username
)

$uri = "https://api.github.com/users/$Username/installation"
$headers = @{
Authorization = "Bearer $AppToken"
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}

try {
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers

return $response.id
}
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
return $null
}

throw
}
}

function Get-InstallationId {
param (
[string]
$AppToken,
[string]
$Owner,
[string]
$Repo
)

$orgInstallationId = Get-OrganizationInstallationId -AppToken $AppToken -Organization $Owner

if ($null -eq $orgInstallationId) {
$repoInstallationId = Get-RepositoryInstallationId -AppToken $AppToken -Repository "$Owner/$Repo"
}
else {
return $orgInstallationId
}

if ($null -eq $repoInstallationId) {
$userInstallationId = Get-UserInstallationId -AppToken $AppToken -Username $Owner
}
else {
return $repoInstallationId
}

if ($null -eq $userInstallationId) {
throw "Installation not found for repository '$Repo'"
}
else {
return $userInstallationId
}
}

$owner, $repo = $Repository -split '/'

$AppToken = Generate-AppToken -ClientId $AppClientId -PrivateKeyContents $AppPrivateKeyContents

$InstallationId = Get-InstallationId -AppToken $AppToken -Owner $owner -Repo $repo

$InstallationToken = Generate-InstallationToken -AppToken $AppToken -InstallationId $InstallationId -Repository $repo

Write-Output $InstallationToken
Loading