|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | +Generates a constrained multi-tenant OAuth authorization URL for an authorized consent-screen training demonstration. |
| 4 | +
|
| 5 | +.DESCRIPTION |
| 6 | +Builds an OAuth 2.0 authorization URL for a multi-tenant Microsoft Entra application owned by the operator. |
| 7 | +It demonstrates that an application registered in one tenant can request sign-in or consent from users in another. |
| 8 | +The application must already be configured for multiple organizations and the redirect URI must already be registered. |
| 9 | +The script does not create or modify applications, open a browser, deliver the URL, exchange an authorization code, |
| 10 | +acquire tokens, or request mail, file, directory, chat, Teams, site, calendar, write, or persistent-access scopes. |
| 11 | +#> |
| 12 | +[CmdletBinding()] |
| 13 | +param( |
| 14 | + [Parameter(Mandatory)] |
| 15 | + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] |
| 16 | + [string]$ClientId, |
| 17 | + |
| 18 | + [ValidateSet('organizations','common')] |
| 19 | + [string]$Authority = 'organizations', |
| 20 | + |
| 21 | + [ValidateScript({ |
| 22 | + $parsed = $null |
| 23 | + if (-not [uri]::TryCreate($_, [UriKind]::Absolute, [ref]$parsed)) { throw 'RedirectUri must be absolute.' } |
| 24 | + if ($parsed.Scheme -eq 'https' -or ($parsed.Scheme -eq 'http' -and $parsed.IsLoopback)) { return $true } |
| 25 | + throw 'RedirectUri must use HTTPS or HTTP loopback.' |
| 26 | + })] |
| 27 | + [string]$RedirectUri = 'https://example.invalid/oauth-training-multitenant.html', |
| 28 | + |
| 29 | + [ValidateSet('openid','profile','email','User.Read')] |
| 30 | + [string[]]$Scopes = @('openid','profile','User.Read'), |
| 31 | + |
| 32 | + [switch]$CopyToClipboard, |
| 33 | + |
| 34 | + [Parameter(Mandatory)] |
| 35 | + [switch]$AcknowledgeAuthorizedTraining |
| 36 | +) |
| 37 | + |
| 38 | +Set-StrictMode -Version Latest |
| 39 | +$ErrorActionPreference = 'Stop' |
| 40 | +if (-not $AcknowledgeAuthorizedTraining) { throw 'Authorized-training acknowledgement is required.' } |
| 41 | + |
| 42 | +$scopesNormalized = @('openid') + @($Scopes | Where-Object { $_ -ne 'openid' }) |
| 43 | +$scopesNormalized = @($scopesNormalized | Sort-Object -Unique) |
| 44 | +$stateBytes = [byte[]]::new(32) |
| 45 | +[Security.Cryptography.RandomNumberGenerator]::Fill($stateBytes) |
| 46 | +$state = [Convert]::ToBase64String($stateBytes).TrimEnd('=').Replace('+','-').Replace('/','_') |
| 47 | +$query = [ordered]@{ |
| 48 | + client_id = $ClientId |
| 49 | + response_type = 'code' |
| 50 | + redirect_uri = $RedirectUri |
| 51 | + response_mode = 'query' |
| 52 | + scope = ($scopesNormalized -join ' ') |
| 53 | + state = $state |
| 54 | + prompt = 'consent' |
| 55 | +} |
| 56 | +$encoded = @($query.GetEnumerator() | ForEach-Object { |
| 57 | + '{0}={1}' -f [uri]::EscapeDataString([string]$_.Key), [uri]::EscapeDataString([string]$_.Value) |
| 58 | +}) -join '&' |
| 59 | +$url = "https://login.microsoftonline.com/$Authority/oauth2/v2.0/authorize?$encoded" |
| 60 | + |
| 61 | +[pscustomobject]@{ |
| 62 | + Scenario = 'Multi-tenant application' |
| 63 | + Authority = $Authority |
| 64 | + ClientId = $ClientId |
| 65 | + RedirectUri = $RedirectUri |
| 66 | + Scopes = $scopesNormalized -join ' ' |
| 67 | + TokenAcquisition = 'Not implemented' |
| 68 | + AuthorizationUrl = $url |
| 69 | +} | Format-List |
| 70 | + |
| 71 | +if ($CopyToClipboard) { |
| 72 | + if (Get-Command Set-Clipboard -ErrorAction SilentlyContinue) { |
| 73 | + Set-Clipboard $url |
| 74 | + Write-Host 'URL copied.' -ForegroundColor Cyan |
| 75 | + } else { |
| 76 | + Write-Warning 'Set-Clipboard is unavailable.' |
| 77 | + } |
| 78 | +} |
| 79 | +Write-Warning 'Training URL only. No browser launch, delivery, code exchange, or token acquisition is implemented.' |
0 commit comments