├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── build.yml
│ └── validate.yml
├── .gitignore
├── EntraAuth
├── EntraAuth.Formats.ps1xml
├── EntraAuth.psd1
├── EntraAuth.psm1
├── LICENSE
├── changelog.md
├── functions
│ ├── Authentication
│ │ ├── Assert-EntraConnection.ps1
│ │ ├── Connect-EntraService.ps1
│ │ ├── Get-EntraService.ps1
│ │ ├── Get-EntraToken.ps1
│ │ ├── Import-EntraToken.ps1
│ │ ├── New-EntraCustomToken.ps1
│ │ ├── Register-EntraService.ps1
│ │ └── Set-EntraService.ps1
│ ├── Core
│ │ └── Invoke-EntraRequest.ps1
│ ├── Other
│ │ ├── New-EntraFilterBuilder.ps1
│ │ └── New-EntraServiceSelector.ps1
│ └── readme.md
└── internal
│ ├── classes
│ ├── EntraToken.ps1
│ ├── Environment.ps1
│ ├── FilterBuilder.ps1
│ └── ServiceSelector.ps1
│ ├── functions
│ ├── authentication
│ │ ├── Connect-ServiceAzure.ps1
│ │ ├── Connect-ServiceBrowser.ps1
│ │ ├── Connect-ServiceCertificate.ps1
│ │ ├── Connect-ServiceClientSecret.ps1
│ │ ├── Connect-ServiceDeviceCode.ps1
│ │ ├── Connect-ServiceIdentity.ps1
│ │ ├── Connect-ServicePassword.ps1
│ │ ├── Connect-ServiceRefreshToken.ps1
│ │ ├── Get-VaultSecret.ps1
│ │ └── Read-AuthResponse.ps1
│ ├── other
│ │ ├── ConvertTo-Base64.ps1
│ │ ├── ConvertTo-Hashtable.ps1
│ │ ├── ConvertTo-QueryString.ps1
│ │ ├── ConvertTo-SignedString.ps1
│ │ ├── Invoke-TerminatingException.ps1
│ │ ├── Read-TokenData.ps1
│ │ ├── Resolve-Certificate.ps1
│ │ ├── Resolve-RequestUri.ps1
│ │ └── Resolve-ScopeName.ps1
│ ├── readme.md
│ └── ux
│ │ ├── Assert-ServiceName.ps1
│ │ └── Get-ServiceCompletion.ps1
│ └── scripts
│ ├── 01-variables.ps1
│ ├── 02-Services.ps1
│ └── readme.md
├── LICENSE
├── build
├── vsts-build.ps1
├── vsts-prerequisites.ps1
└── vsts-validate.ps1
├── docs
├── api-permissions.md
├── application-vs-delegate.md
├── authenticate-browser.md
├── authenticate-certificate.md
├── authenticate-clientsecret.md
├── authenticate-devicecode.md
├── building-on-entraauth.md
├── creating-applications.md
├── managing-applications.md
├── overview.md
└── pictures
│ ├── 01-01-Authentication.png
│ ├── 01-02-Platform.png
│ ├── 01-03-RedirectUri.png
│ ├── 01-04-localhost.png
│ ├── 01-05-Done.png
│ ├── 02-01-Authentication.png
│ ├── 02-02-Platform.png
│ ├── 02-03-Localhost.png
│ ├── 02-04-AdvancedSettings.png
│ ├── 03-01-Certificates.png
│ ├── 03-02-Selection.png
│ ├── 03-03-Completed.png
│ ├── 03-04-Finished.png
│ ├── 04-01-DawnOfASecret.png
│ ├── 04-02-Configuration.png
│ ├── 04-03-Secret.png
│ ├── A-01-AppRegistrations.png
│ ├── A-02-NewRegistration.png
│ ├── A-03-Setup.png
│ ├── A-04-Portal.png
│ ├── C-01-ApiPermission-Portal.png
│ ├── C-02-RequestPermissions.png
│ ├── C-03-ApplicationDelegate.png
│ ├── C-04-ScopesFilter.png
│ ├── C-05-ScopesAssign.png
│ ├── C-06-ConsentPending.png
│ ├── C-07-ConsentGranting.png
│ ├── C-08-ConsentGranted.png
│ ├── C-09-UnknownService.png
│ ├── C-10-SearchingService.png
│ ├── D-01-Overview.png
│ ├── D-02-Properties.png
│ ├── D-03-RememberToSave.png
│ ├── D-04-AssignUsersGroups.png
│ ├── D-05-Selection.png
│ ├── D-06-Selection2.png
│ ├── D-07-Assign.png
│ ├── D-08-Assigned.png
│ └── D-09-SigninLogs.png
├── readme.md
└── tests
├── functions
├── New-EntraFilterBuilder.tests.ps1
└── readme.md
├── general
├── FileIntegrity.Exceptions.ps1
├── FileIntegrity.Tests.ps1
├── Help.Exceptions.ps1
├── Help.Tests.ps1
├── Manifest.Tests.ps1
└── PSScriptAnalyzer.Tests.ps1
├── pester.ps1
└── readme.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github:
4 | FriedrichWeinmann
5 | patreon: # Replace with a single Patreon username
6 | open_collective: # Replace with a single Open Collective username
7 | ko_fi: # Replace with a single Ko-fi username
8 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
9 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
10 | liberapay: # Replace with a single Liberapay username
11 | issuehunt: # Replace with a single IssueHunt username
12 | otechie: # Replace with a single Otechie username
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 | - main
6 |
7 | jobs:
8 | build:
9 |
10 | runs-on: windows-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v1
14 | - name: Install Prerequisites
15 | run: .\build\vsts-prerequisites.ps1
16 | shell: powershell
17 | - name: Validate
18 | run: .\build\vsts-validate.ps1
19 | shell: powershell
20 | - name: Build
21 | run: .\build\vsts-build.ps1 -ApiKey $env:APIKEY
22 | shell: powershell
23 | env:
24 | APIKEY: ${{ secrets.ApiKey }}
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | on: [pull_request]
2 |
3 | jobs:
4 | validate:
5 |
6 | runs-on: windows-latest
7 |
8 | steps:
9 | - uses: actions/checkout@v1
10 | - name: Install Prerequisites
11 | run: .\build\vsts-prerequisites.ps1
12 | shell: powershell
13 | - name: Validate
14 | run: .\build\vsts-validate.ps1
15 | shell: powershell
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | publish
2 | TestResults
3 | experiments
--------------------------------------------------------------------------------
/EntraAuth/EntraAuth.Formats.ps1xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | EntraToken
7 |
8 | EntraToken
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Service
25 |
26 |
27 | Type
28 |
29 |
30 | ValidUntil
31 |
32 |
33 | Scopes
34 |
35 |
36 | ClientID
37 |
38 |
39 | TenantID
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/EntraAuth/EntraAuth.psd1:
--------------------------------------------------------------------------------
1 | @{
2 |
3 | # Script module or binary module file associated with this manifest.
4 | RootModule = 'EntraAuth.psm1'
5 |
6 | # Version number of this module.
7 | ModuleVersion = '1.7.39'
8 |
9 | # Supported PSEditions
10 | # CompatiblePSEditions = @()
11 |
12 | # ID used to uniquely identify this module
13 | GUID = '3d8957b7-643a-48ad-97af-6a342483d578'
14 |
15 | # Author of this module
16 | Author = 'Friedrich Weinmann'
17 |
18 | # Company or vendor of this module
19 | CompanyName = ' '
20 |
21 | # Copyright statement for this module
22 | Copyright = '(c) Friedrich Weinmann. All rights reserved.'
23 |
24 | # Description of the functionality provided by this module
25 | Description = 'Get Tokens from Entra ID'
26 |
27 | # Minimum version of the PowerShell engine required by this module
28 | # PowerShellVersion = ''
29 |
30 | # Name of the PowerShell host required by this module
31 | # PowerShellHostName = ''
32 |
33 | # Minimum version of the PowerShell host required by this module
34 | # PowerShellHostVersion = ''
35 |
36 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
37 | # DotNetFrameworkVersion = ''
38 |
39 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
40 | # ClrVersion = ''
41 |
42 | # Processor architecture (None, X86, Amd64) required by this module
43 | # ProcessorArchitecture = ''
44 |
45 | # Modules that must be imported into the global environment prior to importing this module
46 | # RequiredModules = @()
47 |
48 | # Assemblies that must be loaded prior to importing this module
49 | # RequiredAssemblies = @()
50 |
51 | # Script files (.ps1) that are run in the caller's environment prior to importing this module.
52 | # ScriptsToProcess = @()
53 |
54 | # Type files (.ps1xml) to be loaded when importing this module
55 | # TypesToProcess = @()
56 |
57 | # Format files (.ps1xml) to be loaded when importing this module
58 | FormatsToProcess = @('EntraAuth.Formats.ps1xml')
59 |
60 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
61 | # NestedModules = @()
62 |
63 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
64 | FunctionsToExport = @(
65 | 'Assert-EntraConnection'
66 | 'Connect-EntraService'
67 | 'Get-EntraService'
68 | 'Get-EntraToken'
69 | 'Import-EntraToken'
70 | 'Invoke-EntraRequest'
71 | 'New-EntraCustomToken'
72 | 'New-EntraFilterBuilder'
73 | 'New-EntraServiceSelector'
74 | 'Register-EntraService'
75 | 'Set-EntraService'
76 | )
77 |
78 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
79 | # CmdletsToExport = '*'
80 |
81 | # Variables to export from this module
82 | # VariablesToExport = '*'
83 |
84 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
85 | # AliasesToExport = '*'
86 |
87 | # DSC resources to export from this module
88 | # DscResourcesToExport = @()
89 |
90 | # List of all modules packaged with this module
91 | # ModuleList = @()
92 |
93 | # List of all files packaged with this module
94 | # FileList = @()
95 |
96 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
97 | PrivateData = @{
98 |
99 | PSData = @{
100 |
101 | # Tags applied to this module. These help with module discovery in online galleries.
102 | Tags = @('EntraID', 'Token')
103 |
104 | # A URL to the license for this module.
105 | LicenseUri = 'https://github.com/FriedrichWeinmann/EntraAuth/blob/master/LICENSE'
106 |
107 | # A URL to the main website for this project.
108 | ProjectUri = 'https://github.com/FriedrichWeinmann/EntraAuth'
109 |
110 | # A URL to an icon representing this module.
111 | # IconUri = ''
112 |
113 | # ReleaseNotes of this module
114 | # ReleaseNotes = ''
115 |
116 | # Prerelease string of this module
117 | # Prerelease = ''
118 |
119 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save
120 | # RequireLicenseAcceptance = $false
121 |
122 | # External dependent modules of this module
123 | # ExternalModuleDependencies = @()
124 |
125 | } # End of PSData hashtable
126 |
127 | } # End of PrivateData hashtable
128 |
129 | # HelpInfo URI of this module
130 | # HelpInfoURI = ''
131 |
132 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
133 | # DefaultCommandPrefix = ''
134 |
135 | }
136 |
137 |
--------------------------------------------------------------------------------
/EntraAuth/EntraAuth.psm1:
--------------------------------------------------------------------------------
1 | $script:ModuleRoot = $PSScriptRoot
2 |
3 | foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/classes" -Filter *.ps1 -Recurse) {
4 | . $file.FullName
5 | }
6 |
7 | foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/functions" -Filter *.ps1 -Recurse) {
8 | . $file.FullName
9 | }
10 |
11 | foreach ($file in Get-ChildItem -Path "$PSScriptRoot/functions" -Filter *.ps1 -Recurse) {
12 | . $file.FullName
13 | }
14 |
15 | foreach ($file in Get-ChildItem -Path "$PSScriptRoot/internal/scripts" -Filter *.ps1 -Recurse) {
16 | . $file.FullName
17 | }
--------------------------------------------------------------------------------
/EntraAuth/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Friedrich Weinmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/EntraAuth/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.7.39 (2025-04-15)
4 |
5 | + Upd: New-EntraFilterBuilder - Now supports both AND and OR logic.
6 | + Upd: New-EntraFilterBuilder - Now supports adding other filter builders as nested filter.
7 | + Upd: New-EntraFilterBuilder - Now allows specifying, whether values should be put in quotes.
8 | + Fix: New-EntraFilterBuilder - does not respect order of conditions for custom filters.
9 |
10 | ## 1.7.35 (2025-03-25)
11 |
12 | + New: New-EntraFilterBuilder - Creates a new OData-Filter construction helper.
13 | + New: New-EntraServiceSelector - Creates a helper type designed to help make a module implementing EntraAuth more flexible about what EntraAUth service to use.
14 |
15 | ## 1.6.33 (2025-03-10)
16 |
17 | + New: New-EntraCustomToken - Create a custom token compatible with EntraAuth.
18 | + Upd: Register-EntraService - add `RawOnly` parameter to have all requests to that service use raw processing by default.
19 |
20 | ## 1.5.31 (2025-03-05)
21 |
22 | + Upd: Invoke-EntraRequest - now allows overriding default header entries
23 |
24 | ## 1.5.30 (2025-03-05)
25 |
26 | + Upd: Connect-EntraService - now accepts "Graph" or "Azure" as ClientID, resolving the respective first party App IDs.
27 | + Fix: Connect-EntraService - fails to find a certificate by name, when the cert store contains only a single certificate
28 |
29 | ## 1.5.28 (2025-02-14)
30 |
31 | + New: Import-EntraToken - Imports a token into the local token store.
32 | + Upd: Connect-EntraService - added `-FallbackAzAccount` parameter to allow MSI authentication to fall back to the existing Az Session in case of trouble.
33 | + Upd: Connect-EntraService - enabled multiple secret names to be specified when logging in via Key Vault.
34 | + Upd: Token - default Query values are now copied onto the token from the service configuration.
35 | + Upd: Invoke-EntraRequest - uses default Query values from the token, rather than the service configuration
36 |
37 | ## 1.4.23 (2025-01-14)
38 |
39 | + Upd: Connect-EntraService - removed TenantID requirement for most delegate flows, defaulting the parameter to "organizations". TenantID on the managed token object is now read from the returned token.
40 |
41 | ## 1.4.22 (2024-12-04)
42 |
43 | + Upd: Connect-EntraService - added `-UseRefreshToken` parameter for delegate flows, showing the interactive prompts only if needed.
44 |
45 | ## 1.4.21 (2024-11-26)
46 |
47 | + Upd: Added support for authenticating using an existing refresh token
48 | + Fix: Invoke-EntraRequest - "body not supported with this method" error when using Get requests with a body (#23)
49 |
50 | ## 1.3.19 (2024-10-13)
51 |
52 | + Upd: Invoke-EntraRequest - `-Body` parameter now supports raw string or custom objects.
53 |
54 | ## 1.3.18 (2024-10-08)
55 |
56 | + Upd: Added support for authenticating using the current Az.Accounts session
57 | + Upd: Added support for Sovereign Clouds (USGov, USGovDOD, China) and custom authentication urls.
58 | + Fix: Certificate Logon fails on timezones after UTC
59 |
60 | ## 1.2.15 (2024-07-31)
61 |
62 | + Fix: Refresh token may fail to authenticate to correct application
63 | + Fix: Managed Identity authentication fails on Azure VMs
64 |
65 | ## 1.2.13 (2024-05-22)
66 |
67 | + New: Supporting Managed Identity (User-Assigned or System Managed)
68 | + Upd: Service Configuration - can specify default query parameters.
69 |
70 | ## 1.1.11 (2024-05-21)
71 |
72 | + New: Service configurations - Added configurations for Azure & AzureKeyVault.
73 | + Upd: Connect-EntraService - Added support for direct Key Vault integration.
74 | + Upd: Service configurations - Added capability to require additional parameters that modify the base Service Url.
75 | + Fix: Token Renewal - bad parameter ServiceUrl.
76 | + Fix: Asset-EntraConnection - bad error message when assertion fails.
77 |
78 | ## 1.0.6 (2024-05-15)
79 |
80 | + Upd: Invoke-EntraRequest - added -NoPaging parameter to support disabling paging.
81 | + Upd: Invoke-EntraRequest - added -Raw parameter to support returning unprocessed results.
82 |
83 | ## 1.0.4 (2024-04-19)
84 |
85 | + Fix: Connect-EntraService - fails to register new sessions (#10)
86 |
87 | ## 1.0.3 (2024-03-24)
88 |
89 | + Upd: Connect-EntraService - added -Resource parameter to allow creating tokens without requiring a service (#7)
90 | + Upd: Connect-EntraService - added -BrowserMode parameter to allow pasting the link to whatever browser you prefer (#3)
91 | + Fix: Connect Browser - unknown command when PSFramework is not installed
92 |
93 | ## 1.0.0 (2024-03-20)
94 |
95 | + Initial Release
96 |
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/Assert-EntraConnection.ps1:
--------------------------------------------------------------------------------
1 | function Assert-EntraConnection
2 | {
3 | <#
4 | .SYNOPSIS
5 | Asserts a connection has been established.
6 |
7 | .DESCRIPTION
8 | Asserts a connection has been established.
9 | Fails the calling command in a terminating exception if not connected yet.
10 |
11 | .PARAMETER Service
12 | The service to which a connection needs to be established.
13 |
14 | .PARAMETER Cmdlet
15 | The $PSCmdlet variable of the calling command.
16 | Used to execute the terminating exception in the caller scope if needed.
17 |
18 | .PARAMETER RequiredScopes
19 | Scopes needed, for better error messages.
20 |
21 | .EXAMPLE
22 | PS C:\> Assert-EntraConnection -Service 'Endpoint' -Cmdlet $PSCmdlet
23 |
24 | Silently does nothing if already connected to the specified defender for endpoint service.
25 | Kills the calling command if not yet connected.
26 | #>
27 | [CmdletBinding()]
28 | param (
29 | [ArgumentCompleter({ Get-ServiceCompletion $args })]
30 | [Parameter(Mandatory = $true)]
31 | [string]
32 | $Service,
33 |
34 | [Parameter(Mandatory = $true)]
35 | $Cmdlet,
36 |
37 | [AllowEmptyCollection()]
38 | [string[]]
39 | $RequiredScopes
40 | )
41 |
42 | process
43 | {
44 | if ($script:_EntraTokens["$Service"]) { return }
45 |
46 | $message = "Not connected yet! Use Connect-EntraService to establish a connection to '$Service' first."
47 | if ($RequiredScopes) { $message = $message + " Scopes required for this call: $($RequiredScopes -join ', ')"}
48 | Invoke-TerminatingException -Cmdlet $Cmdlet -Message $message -Category ConnectionError
49 | }
50 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/Get-EntraService.ps1:
--------------------------------------------------------------------------------
1 | function Get-EntraService {
2 | <#
3 | .SYNOPSIS
4 | Returns the list of available Entra ID services that can be connected to.
5 |
6 | .DESCRIPTION
7 | Returns the list of available Entra ID services that can be connected to.
8 | Includes for each the endpoint/service url and the default requested scopes.
9 |
10 | .PARAMETER Name
11 | Name of the service to return.
12 | Defaults to: *
13 |
14 | .EXAMPLE
15 | PS C:\> Get-EntraService
16 |
17 | List all available services.
18 | #>
19 | [CmdletBinding()]
20 | param (
21 | [ArgumentCompleter({ Get-ServiceCompletion $args })]
22 | [string]
23 | $Name = '*'
24 | )
25 | process {
26 | $script:_EntraEndpoints.Values | Where-Object Name -like $Name
27 | }
28 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/Get-EntraToken.ps1:
--------------------------------------------------------------------------------
1 | function Get-EntraToken {
2 | <#
3 | .SYNOPSIS
4 | Returns the session token of an Entra ID connection.
5 |
6 | .DESCRIPTION
7 | Returns the session token of an Entra ID connection.
8 | The main use for those token objects is calling their "GetHeader()" method to get an authentication header
9 | that automatically refreshes tokens as needed.
10 |
11 | .PARAMETER Service
12 | The service for which to retrieve the token.
13 | Defaults to: *
14 |
15 | .EXAMPLE
16 | PS C:\> Get-EntraToken
17 |
18 | Returns all current session tokens
19 | #>
20 | [CmdletBinding()]
21 | param (
22 | [ArgumentCompleter({ Get-ServiceCompletion $args })]
23 | [string]
24 | $Service = '*'
25 | )
26 | process {
27 | $script:_EntraTokens.Values | Where-Object Service -like $Service
28 | }
29 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/Import-EntraToken.ps1:
--------------------------------------------------------------------------------
1 | function Import-EntraToken {
2 | <#
3 | .SYNOPSIS
4 | Imports a token into the local token store.
5 |
6 | .DESCRIPTION
7 | Imports a token into the local token store.
8 | This command is intended for use in a runspace scenario, for passing tokens from the main runspace into background environments.
9 | The input-token object is cloned into a runspace-local token object and registered to its service.
10 |
11 | After performing the conversion, it will try to renew the token object.
12 |
13 | .PARAMETER Token
14 | The token object to import.
15 | Should be a token object created by EntraAuth's Connect-EntraService command.
16 | Usually returned by Get-EntraToken, after finishing the connection.
17 |
18 | .PARAMETER PassThru
19 | Rather than registering the token into the Entra token store in memory, return it as an object.
20 | Useful for ronspace-localizing a token not associated with any given service.
21 |
22 | .PARAMETER NoRenew
23 | Do not renew the token after importing it.
24 | By default, newly localized tokens will try to renew themselves, to avoid parallel use of the same access token instance.
25 |
26 | .EXAMPLE
27 | PS C:\> Import-EntraToken -Token $using:tokens
28 |
29 | Imports all tokens stored in $tokens of the calling runspace.
30 | For use in scenarios such as background runspaces within "ForEach-Object -Parallel"
31 | #>
32 | [CmdletBinding()]
33 | param (
34 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
35 | [object[]]
36 | $Token,
37 |
38 | [switch]
39 | $PassThru,
40 |
41 | [switch]
42 | $NoRenew
43 | )
44 | process {
45 | foreach ($tokenObject in $Token) {
46 | $newToken = [EntraToken]::new()
47 | foreach ($propertyName in $newToken.PSObject.Properties.Name) {
48 | if ($null -eq $tokenObject.$propertyName) { continue }
49 | if ($tokenObject.$propertyName -is [hashtable]) {
50 | $newToken.$propertyName = $tokenObject.$propertyName.Clone()
51 | }
52 | else {
53 | $newToken.$propertyName = $tokenObject.$propertyName
54 | }
55 | }
56 |
57 | if (
58 | -not $newToken.Service -or
59 | -not $newToken.AccessToken -or
60 | -not $newToken.ClientID -or
61 | -not $newToken.TenantID
62 | ) {
63 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Invalid Input Object! An Entra token object must have a Service, AccessToken, ClientID and TenantID. Item received: $tokenObject"
64 | }
65 |
66 | #region Renew Token
67 | if (
68 | -not $NoRenew -and
69 | (
70 | $newToken.Type -notin 'DeviceCode', 'Browser' -or
71 | $newToken.RefreshToken
72 | )
73 | ) {
74 | $newToken.RenewToken()
75 | }
76 | #endregion Renew Token
77 |
78 | if ($PassThru) { $newToken }
79 | else { $script:_EntraTokens[$newToken.Service] = $newToken }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/New-EntraCustomToken.ps1:
--------------------------------------------------------------------------------
1 | function New-EntraCustomToken {
2 | <#
3 | .SYNOPSIS
4 | Create a custom token compatible with EntraAuth.
5 |
6 | .DESCRIPTION
7 | Create a custom token compatible with EntraAuth.
8 | This allows directly integrating APIs that do not operate with default OAuth into the EntraAuth toolset.
9 |
10 | .PARAMETER ServiceUrl
11 | The URL requests are sent against.
12 |
13 | .PARAMETER HeaderCode
14 | The code that calculates the header (including authentication information) to include in the request.
15 | The scriptblock receives one argument: The token itself.
16 | Must return a single hashtable.
17 |
18 | .PARAMETER Service
19 | The name of the service to register the token under.
20 | Does not have to be a formally registered service, can be an arbitrary name.
21 | Specifying this parameter prevents the token from being returned as output, unless combined with -PassThru.
22 |
23 | .PARAMETER PassThru
24 | Return the token as output.
25 | By default, when specifying a service name, this command produces no output.
26 |
27 | .PARAMETER TenantID
28 | TenantID to connect to.
29 | Purely cosmetic, unless accessed from the header code.
30 |
31 | .PARAMETER ClientID
32 | ClientID to connect as.
33 | Purely cosmetic, unless accessed from the header code.
34 |
35 | .PARAMETER Header
36 | Header information to include in all requests against this API.
37 | Additional header entries to include in every request.
38 | Will be added to those returned from the HeaderCode and need not be considered within that code.
39 |
40 | .PARAMETER Query
41 | Additional query parameters to include in all requests against this API.
42 |
43 | .PARAMETER Data
44 | Additional information to store in the token object.
45 | Used by the HeaderCode scriptblock.
46 | Use this parameter to include information such as PATs, API keys or similar pieces of information.
47 |
48 | .PARAMETER RawOnly
49 | All requests throug this token should not use the default response processing.
50 | This will prevent Invoke-EntraRequest from providing most of its usual assistance.
51 |
52 | .EXAMPLE
53 | PS C:\> New-EntraCustomToken -Service AzDevMyProject -ServiceUrl 'https://dev.azure.com/contoso/myproject/_apis/wit' -Data @{ PAT = $pat } -HeaderCode {
54 | param ($Token)
55 | $base64AuthInfo = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(':' + $($Token.Data.PAT | ConvertFrom-SecureString -AsPlainText)))
56 | return @{
57 | Authorization = "Basic $base64AuthInfo"
58 | 'Content-Type' = 'application/json'
59 | }
60 | }
61 |
62 | Registers a new token for Azure DevOps, using PAT to authenticate.
63 | #>
64 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
65 | [CmdletBinding()]
66 | param (
67 | [Parameter(Mandatory = $true)]
68 | [string]
69 | $ServiceUrl,
70 |
71 | [Parameter(Mandatory = $true)]
72 | [scriptblock]
73 | $HeaderCode,
74 |
75 | [string]
76 | $Service,
77 |
78 | [switch]
79 | $PassThru,
80 |
81 | [string]
82 | $TenantID = '',
83 |
84 | [string]
85 | $ClientID = '',
86 |
87 | [hashtable]
88 | $Header = @{},
89 |
90 | [hashtable]
91 | $Query = @{},
92 |
93 | [hashtable]
94 | $Data = @{},
95 |
96 | [switch]
97 | $RawOnly
98 | )
99 | process {
100 | $newToken = [EntraToken]::new()
101 | $newToken.Type = 'Custom'
102 | $newToken.ServiceUrl = $ServiceUrl
103 | $newToken.HeaderCode = $HeaderCode
104 | if ($Service) { $newToken.Service = $Service }
105 | else { $newToken.Service = '' }
106 | $newToken.TenantID = $TenantID
107 | $newToken.ClientID = $ClientID
108 | $newToken.Header = $Header.Clone()
109 | $newToken.Query = $Query.Clone()
110 | $newToken.Data = $Data.Clone()
111 | $newToken.RawOnly = $RawOnly.ToBool()
112 |
113 | if ($Service) {
114 | $script:_EntraTokens[$Service] = $newToken
115 | }
116 | if ($PassThru -or -not $Service) {
117 | $newToken
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/Register-EntraService.ps1:
--------------------------------------------------------------------------------
1 | function Register-EntraService {
2 | <#
3 | .SYNOPSIS
4 | Define a new Entra ID Service to connect to.
5 |
6 | .DESCRIPTION
7 | Define a new Entra ID Service to connect to.
8 | This allows defining new endpoints to connect to ... or overriding existing endpoints to a different configuration.
9 |
10 | .PARAMETER Name
11 | Name of the Service.
12 |
13 | .PARAMETER ServiceUrl
14 | The base Url requests will use.
15 |
16 | .PARAMETER Resource
17 | The Resource ID. Used when connecting to identify which scopes of an App Registration to use.
18 |
19 | .PARAMETER DefaultScopes
20 | Default scopes to request.
21 | Used in interactive delegate flows to provide a good default user experience.
22 | Default scopes should usually include common read scenarios.
23 |
24 | .PARAMETER Header
25 | Header data to include in each request.
26 |
27 | .PARAMETER HelpUrl
28 | Link for more information about this service.
29 | Ideally to documentation that helps setting up the connection.
30 |
31 | .PARAMETER NoRefresh
32 | Delegate authentication flows should not request refresh tokens.
33 | By default, delegate authentication flows will automatically request offline_access to get a refresh token.
34 | This refresh token allows requesting new tokens when the current one is expiring without requiring additional
35 | interactive logon actions.
36 | However, not all services support this scope.
37 |
38 | .PARAMETER Parameters
39 | Extra parameters a request will require.
40 | It expects a hashtable with the key being the parameter name, and the value being a description of that parameter.
41 | The ServiceUrl must include a placeholder for each parameter to insert into it.
42 |
43 | Example:
44 | Parameter: @{ VaultName = 'Name of the Key Vault to execute against' }
45 | ServiceUrl: https://%VAULTNAME%.vault.azure.net
46 |
47 | .PARAMETER Query
48 | Extra Query Parameters to automatically include on all requests.
49 |
50 | .PARAMETER RawOnly
51 | Disable default API response handling.
52 | By default, when executing a request via Invoke-EntraRequest, the response is processed as if it were a default Graph API standard response.
53 | Many other MS APIs follow the same standard, but not all do so.
54 | When enabling this setting on a service, all requests against that service will NOT have that processing applied and instead return raw responses.
55 |
56 | .PARAMETER Environment
57 | What environment this service should connect to.
58 | Defaults to: 'Global'
59 |
60 | .PARAMETER AuthenticationUrl
61 | The url used for the authentication requests to retrieve tokens.
62 | Usually determined by the "Environment" parameter, but may be overridden in case of need.
63 |
64 | .EXAMPLE
65 | PS C:\> Register-EntraService -Name Endpoint -ServiceUrl 'https://api.securitycenter.microsoft.com/api' -Resource 'https://api.securitycenter.microsoft.com'
66 |
67 | Registers the defender for endpoint API as a service.
68 | #>
69 | [CmdletBinding()]
70 | param (
71 | [Parameter(Mandatory = $true)]
72 | [string]
73 | $Name,
74 |
75 | [Parameter(Mandatory = $true)]
76 | [string]
77 | $ServiceUrl,
78 |
79 | [Parameter(Mandatory = $true)]
80 | [string]
81 | $Resource,
82 |
83 | [AllowEmptyCollection()]
84 | [string[]]
85 | $DefaultScopes = @(),
86 |
87 | [hashtable]
88 | $Header = @{},
89 |
90 | [string]
91 | $HelpUrl,
92 |
93 | [switch]
94 | $NoRefresh,
95 |
96 | [hashtable]
97 | $Parameters = @{},
98 |
99 | [Hashtable]
100 | $Query = @{},
101 |
102 | [switch]
103 | $RawOnly,
104 |
105 | [Environment]
106 | $Environment = 'Global',
107 |
108 | [string]
109 | $AuthenticationUrl
110 | )
111 | process {
112 | $command = Get-Command Invoke-EntraRequest
113 | $badParameters = $Parameters.Keys | Where-Object { $_ -in $command.Parameters.Keys }
114 | if ($badParameters) {
115 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Cannot define parameters that collide with Invoke-EntraRequest: $($badParameters -join ', ')"
116 | }
117 | $authUrl = switch ("$Environment") {
118 | 'China' { 'https://login.chinacloudapi.cn' }
119 | 'USGov' { 'https://login.microsoftonline.us' }
120 | 'USGovDOD' { 'https://login.microsoftonline.us' }
121 | default { 'https://login.microsoftonline.com' }
122 | }
123 | if ($AuthenticationUrl) { $authUrl = $AuthenticationUrl.TrimEnd('/') }
124 |
125 | $script:_EntraEndpoints[$Name] = [PSCustomObject]@{
126 | PSTypeName = 'EntraAuth.Service'
127 | Name = $Name
128 | ServiceUrl = $ServiceUrl
129 | Resource = $Resource
130 | DefaultScopes = $DefaultScopes
131 | Header = $Header
132 | HelpUrl = $HelpUrl
133 | NoRefresh = $NoRefresh.ToBool()
134 | Parameters = $Parameters
135 | Query = $Query
136 | RawOnly = $RawOnly.ToBool()
137 | AuthenticationUrl = $authUrl
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Authentication/Set-EntraService.ps1:
--------------------------------------------------------------------------------
1 | function Set-EntraService {
2 | <#
3 | .SYNOPSIS
4 | Modify the settings on an existing Service configuration.
5 |
6 | .DESCRIPTION
7 | Modify the settings on an existing Service configuration.
8 | Service configurations are defined using Register-EntraService and define how connections and requests to a specific API service / endpoint are performed.
9 |
10 | .PARAMETER Name
11 | The name of the already existing Service configuration.
12 |
13 | .PARAMETER ServiceUrl
14 | The base Url requests will use.
15 |
16 | .PARAMETER Resource
17 | The Resource ID. Used when connecting to identify which scopes of an App Registration to use.
18 |
19 | .PARAMETER DefaultScopes
20 | Default scopes to request.
21 | Used in interactive delegate flows to provide a good default user experience.
22 | Default scopes should usually include common read scenarios.
23 |
24 | .PARAMETER Header
25 | Header data to include in each request.
26 |
27 | .PARAMETER HelpUrl
28 | Link for more information about this service.
29 | Ideally to documentation that helps setting up the connection.
30 |
31 | .PARAMETER NoRefresh
32 | Delegate authentication flows should not request refresh tokens.
33 | By default, delegate authentication flows will automatically request offline_access to get a refresh token.
34 | This refresh token allows requesting new tokens when the current one is expiring without requiring additional
35 | interactive logon actions.
36 | However, not all services support this scope.
37 |
38 | .EXAMPLE
39 | PS C:\> Set-EntraService -Name Endpoint -ServiceUrl 'https://api-us.securitycenter.microsoft.com/api'
40 |
41 | Changes the service url for the "Endpoint" service to 'https://api-us.securitycenter.microsoft.com/api'.
42 | Note: It is generally recommened to select the service url most suitable for your tenant, geographically:
43 | https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/api/exposed-apis-list?view=o365-worldwide#versioning
44 | #>
45 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
46 | [CmdletBinding()]
47 | param (
48 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
49 | [ArgumentCompleter({ Get-ServiceCompletion $args })]
50 | [ValidateScript({ Assert-ServiceName -Name $_ })]
51 | [string]
52 | $Name,
53 |
54 | [string]
55 | $ServiceUrl,
56 |
57 | [string]
58 | $Resource,
59 |
60 | [AllowEmptyCollection()]
61 | [string[]]
62 | $DefaultScopes,
63 |
64 | [Hashtable]
65 | $Header,
66 |
67 | [string]
68 | $HelpUrl,
69 |
70 | [switch]
71 | $NoRefresh
72 | )
73 | process {
74 | $service = $script:_EntraEndpoints.$Name
75 | if ($PSBoundParameters.Keys -contains 'ServiceUrl') { $service.ServiceUrl = $ServiceUrl }
76 | if ($PSBoundParameters.Keys -contains 'Resource') { $service.Resource = $Resource }
77 | if ($PSBoundParameters.Keys -contains 'DefaultScopes') { $service.DefaultScopes = $DefaultScopes }
78 | if ($PSBoundParameters.Keys -contains 'Header') { $service.Header = $Header }
79 | if ($PSBoundParameters.Keys -contains 'HelpUrl') { $service.HelpUrl = $HelpUrl }
80 | if ($PSBoundParameters.Keys -contains 'NoRefresh') { $service.HelpUrl = $NoRefresh.ToBool() }
81 | }
82 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Core/Invoke-EntraRequest.ps1:
--------------------------------------------------------------------------------
1 | function Invoke-EntraRequest {
2 | <#
3 | .SYNOPSIS
4 | Executes a web request against an entra-based service
5 |
6 | .DESCRIPTION
7 | Executes a web request against an entra-based service
8 | Handles all the authentication details once connected using Connect-EntraService.
9 |
10 | .PARAMETER Path
11 | The relative path of the endpoint to query.
12 | For example, to retrieve Microsoft Graph users, it would be a plain "users".
13 | To access details on a particular defender for endpoint machine instead it would look thus: "machines/1e5bc9d7e413ddd7902c2932e418702b84d0cc07"
14 |
15 | .PARAMETER Body
16 | Any body content needed for the request.
17 |
18 | .PARAMETER Query
19 | Any query content to include in the request.
20 | In opposite to -Body this is attached to the request Url and usually used for filtering.
21 |
22 | .PARAMETER Method
23 | The Rest Method to use.
24 | Defaults to GET
25 |
26 | .PARAMETER RequiredScopes
27 | Any authentication scopes needed.
28 | Used for documentary purposes only.
29 |
30 | .PARAMETER Header
31 | Any additional headers to include on top of authentication and content-type.
32 |
33 | .PARAMETER Service
34 | Which service to execute against.
35 | Determines the API endpoint called to.
36 | Defaults to "Graph"
37 |
38 | .PARAMETER SerializationDepth
39 | How deeply to serialize the request body when converting it to json.
40 | Defaults to: 99
41 |
42 | .PARAMETER Token
43 | A Token as created and maintained by this module.
44 | If specified, it will override the -Service parameter.
45 |
46 | .PARAMETER NoPaging
47 | Do not automatically page through responses sets.
48 | By default, Invoke-EntraRequest is going to keep retrieving result pages until all data has been retrieved.
49 |
50 | .PARAMETER Raw
51 | Do not process the response object and instead return the raw result returned by the API.
52 |
53 | .EXAMPLE
54 | PS C:\> Invoke-EntraRequest -Path 'alerts' -RequiredScopes 'Alert.Read'
55 |
56 | Return a list of defender alerts.
57 | #>
58 | [CmdletBinding(DefaultParameterSetName = 'default')]
59 | param (
60 | [Parameter(Mandatory = $true)]
61 | [string]
62 | $Path,
63 |
64 | $Body,
65 |
66 | [Hashtable]
67 | $Query = @{ },
68 |
69 | [string]
70 | $Method = 'GET',
71 |
72 | [string[]]
73 | $RequiredScopes,
74 |
75 | [hashtable]
76 | $Header = @{},
77 |
78 | [ArgumentCompleter({ Get-ServiceCompletion $args })]
79 | [ValidateScript({ Assert-ServiceName -Name $_ -IncludeTokens })]
80 | [string]
81 | $Service = $script:_DefaultService,
82 |
83 | [ValidateRange(1, 666)]
84 | [int]
85 | $SerializationDepth = 99,
86 |
87 | [EntraToken]
88 | $Token,
89 |
90 | [switch]
91 | $NoPaging,
92 |
93 | [switch]
94 | $Raw
95 | )
96 |
97 | DynamicParam {
98 | if ($Resource) { return }
99 |
100 | $actualService = $Service
101 | if (-not $actualService) { $actualService = $script:_DefaultService }
102 | $serviceObject = $script:_EntraEndpoints.$actualService
103 | if (-not $serviceObject) { return }
104 | if ($serviceObject.Parameters.Count -lt 1) { return }
105 |
106 | $results = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
107 | foreach ($pair in $serviceObject.Parameters.GetEnumerator()) {
108 | $parameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
109 | $parameterAttribute.ParameterSetName = '__AllParameterSets'
110 | $parameterAttribute.Mandatory = $true
111 | $parameterAttribute.HelpMessage = $pair.Value
112 | $attributesCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
113 | $attributesCollection.Add($parameterAttribute)
114 | $RuntimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($pair.Key, [object], $attributesCollection)
115 |
116 | $results.Add($pair.Key, $RuntimeParam)
117 | }
118 |
119 | $results
120 | }
121 |
122 | begin {
123 | if ($Token) {
124 | $tokenObject = $Token
125 | }
126 | else {
127 | Assert-EntraConnection -Service $Service -Cmdlet $PSCmdlet -RequiredScopes $RequiredScopes
128 | $tokenObject = $script:_EntraTokens.$Service
129 | }
130 |
131 | $serviceObject = $script:_EntraEndpoints.$($tokenObject.Service)
132 | }
133 | process {
134 | $parameters = @{
135 | Method = $Method
136 | Uri = Resolve-RequestUri -TokenObject $tokenObject -ServiceObject $serviceObject -BoundParameters $PSBoundParameters
137 | }
138 |
139 | if ($PSBoundParameters.Keys -contains 'Body') {
140 | if ($Body -is [string]) {
141 | $parameters.Body = $Body
142 | }
143 | else {
144 | $parameters.Body = $Body | ConvertTo-Json -Compress -Depth $SerializationDepth
145 | }
146 | }
147 | # In PS5.1, some methods cannot contain a body
148 | $noBodyMethods = 'Default', 'Get', 'Head'
149 | if ($PSVersionTable.PSVersion.Major -lt 6 -and $Method -in $noBodyMethods) {
150 | $parameters.Remove('Body')
151 | }
152 |
153 | $parameters.Uri += ConvertTo-QueryString -QueryHash $Query -DefaultQuery $tokenObject.Query
154 |
155 | do {
156 | $tempHeader = $tokenObject.GetHeader().Clone() # GetHeader() automatically refreshes expired tokens
157 | foreach ($pair in $Header.GetEnumerator()) { $tempHeader[$pair.Key] = $pair.Value }
158 | $parameters.Headers = $tempHeader
159 | Write-Verbose "Executing Request: $($Method) -> $($parameters.Uri)"
160 | try { $result = Invoke-RestMethod @parameters -ErrorAction Stop }
161 | catch {
162 | $letItBurn = $true
163 | $failure = $_
164 |
165 | if ($_.ErrorDetails.Message) {
166 | $details = $_.ErrorDetails.Message | ConvertFrom-Json
167 | if ($details.Error.Code -eq 'TooManyRequests') {
168 | Write-Verbose "Throttling: $($details.error.message)"
169 | $delay = 1 + ($details.error.message -replace '^.+ (\d+) .+$', '$1' -as [int])
170 | if ($delay -gt 5) { Write-Warning "Request is being throttled for $delay seconds" }
171 | Start-Sleep -Seconds $delay
172 | try {
173 | $result = Invoke-RestMethod @parameters -ErrorAction Stop
174 | $letItBurn = $false
175 | }
176 | catch {
177 | $failure = $_
178 | }
179 | }
180 | }
181 |
182 | if ($letItBurn) {
183 | Write-Warning "Request failed: $($Method) -> $($parameters.Uri)"
184 | $PSCmdlet.ThrowTerminatingError($failure)
185 | }
186 | }
187 | if (-not $Raw -and -not $tokenObject.RawOnly -and $result.PSObject.Properties.Where{ $_.Name -eq 'value' }) { $result.Value }
188 | else { $result }
189 | $parameters.Uri = $result.'@odata.nextLink'
190 | }
191 | while ($parameters.Uri -and -not $NoPaging)
192 | }
193 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Other/New-EntraFilterBuilder.ps1:
--------------------------------------------------------------------------------
1 | function New-EntraFilterBuilder {
2 | <#
3 | .SYNOPSIS
4 | Creates a new OData-Filter construction helper.
5 |
6 | .DESCRIPTION
7 | Creates a new OData-Filter construction helper.
8 | This helper class was designed to simplify the creation of OData filters for APIs such as the Microsoft Graph API.
9 | Call the ".Add(...)" method to specify filter conditions.
10 |
11 | There are three ways to do so:
12 | A) .Add(property, operator, value)
13 | A single property comparison, with a specific operator and value.
14 |
15 | B) .Add(customFilter)
16 | A custom piece of OData filter text.
17 |
18 | C) .Add(filterbuilder)
19 | The output of another filterbuilder becomes part of the conditions for this one.
20 | This allows building complex, nested filter statements.
21 |
22 | Finally, call ".Get()" to retrieve the full OData filter string or ".GetHeader()" to retrieve the filter header hashtable.
23 |
24 | For more in-depth help with the type, call the '.GetHelp()' method on the object.
25 |
26 | Useful Resources:
27 | List of valid filter conditions:
28 | https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http#operators-and-functions-supported-in-filter-expressions
29 |
30 | .PARAMETER Logic
31 | The logic by which individual filter conditions are merged.
32 | Options:
33 | - AND (default)
34 | - OR
35 |
36 | .EXAMPLE
37 | PS C:\> $filter = New-EntraFilterBuilder
38 | PS C:\> $filter.Add('displayName', 'eq', 'John Doe')
39 | PS C:\> $filter.Add('organization', 'in', @('Contoso', 'Fabrikam'))
40 | PS C:\> $filter.Get()
41 |
42 | Will return: "displayName eq 'John Doe' and organization in ('Contoso', 'Fabrikam')"
43 | #>
44 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
45 | #%UNCOMMENT%[OutputType([FilterBuilder])]
46 | [CmdletBinding()]
47 | param (
48 | [ValidateSet('AND','OR')]
49 | [string]
50 | $Logic = 'AND'
51 | )
52 |
53 | process {
54 | [FilterBuilder]::new($Logic)
55 | }
56 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/Other/New-EntraServiceSelector.ps1:
--------------------------------------------------------------------------------
1 | function New-EntraServiceSelector {
2 | <#
3 | .SYNOPSIS
4 | Creates a helper type designed to help make a module implementing EntraAuth more flexible about what EntraAUth service to use.
5 |
6 | .DESCRIPTION
7 | Creates a helper type designed to help make a module implementing EntraAuth more flexible about what EntraAUth service to use.
8 |
9 | While a module can easily define what service to use when calling Invoke-EntraRequest, this has some concerns:
10 | + If multiple modules require the same service, they may interfere with each other by trying to use separate applications or having different scope requirements.
11 | + If each module defines their own service instance (e.g. "Graph.MyModule"), then a script using multiple modules needs to authenticate multiple times, even if they all could use the same connection/token.
12 |
13 | The Service Selector aims to be a simple solution to this problem.
14 | It is intended for _Modules_ that implement EntraAuth, not individual scripts.
15 |
16 | To fully execute on this, you will need to implement this in three locations:
17 | - During Module Import: Declare defaults & Selector.
18 | - At the beginning of your functions: Select chosen services.
19 | - When executing requests: Use service as chosen.
20 |
21 | #=======================================================================================================
22 | # During Module Import
23 | $script:_services = @{ Graph = 'Graph'; MDE = 'Endpoint' }
24 | $script:_serviceSelector = New-EntraServiceSelector -DefaultServices $script:_services
25 |
26 | # During the Begin stage of each function using EntraAuth
27 | begin {
28 | $services = $script:_serviceSelector.GetServiceMap($ServiceMap) # $ServiceMap is a hashtable parameter offered by your function
29 | Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Graph
30 | }
31 |
32 | # When executing the actual request, later in the function
33 | Invoke-EntraService -Service $services.Graph -Path users
34 | #=======================================================================================================
35 |
36 | With this, somebody could call your command - let's call it "Get-DepartmentUser" - like this:
37 | Get-DepartmentUser -ServiceMap @{ Graph = 'GraphBeta' }
38 | And your function would use the beta version of the Graph api, without affecting any other script or module calling your function.
39 |
40 | Example Implementations:
41 | - During Module Import:
42 | https://github.com/FriedrichWeinmann/EntraAuth.Graph.Application/blob/master/EntraAuth.Graph.Application/internal/scripts/variables.ps1
43 | - Used in Functions:
44 | https://github.com/FriedrichWeinmann/EntraAuth.Graph.Application/blob/3c5e9f3de31fd7946e6fe9ebdb938986165ff5ca/EntraAuth.Graph.Application/functions/Get-EAGAppRegistration.ps1#L78
45 |
46 | .PARAMETER DefaultServices
47 | The Default services to use.
48 | Provide a hashtable of Labels mapping to EntraAuth services.
49 | Example:
50 | @{ Graph = 'Graph'; MDE = 'Endpoint' }
51 | The key is what you use in your code as a label, the Value is the actual service in EntraAuth.
52 |
53 | .EXAMPLE
54 | PS C:\> $script:_serviceSelector = New-EntraServiceSelector -DefaultServices $script:_services
55 |
56 | Creates a new ServiceSelector object and stores it in $script:_serviceSelector
57 | #>
58 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
59 | #%UNCOMMENT%[OutputType([ServiceSelector])]
60 | [CmdletBinding()]
61 | param (
62 | [Parameter(Mandatory = $true)]
63 | [hashtable]
64 | $DefaultServices
65 | )
66 | process {
67 | [ServiceSelector]::new($DefaultServices)
68 | }
69 | }
--------------------------------------------------------------------------------
/EntraAuth/functions/readme.md:
--------------------------------------------------------------------------------
1 | # Functions
2 |
3 | Folder for all the functions the user is supposed to have access to.
4 |
--------------------------------------------------------------------------------
/EntraAuth/internal/classes/EntraToken.ps1:
--------------------------------------------------------------------------------
1 | class EntraToken {
2 | #region Token Data
3 | [string]$AccessToken
4 | [System.DateTime]$ValidAfter
5 | [System.DateTime]$ValidUntil
6 | [string[]]$Scopes
7 | [string]$RefreshToken
8 | [string]$Audience
9 | [string]$Issuer
10 | [PSObject]$TokenData
11 | #endregion Token Data
12 |
13 | #region Connection Data
14 | [string]$Service
15 | [string]$Type
16 | [string]$ClientID
17 | [string]$TenantID
18 | [string]$ServiceUrl
19 | [string]$AuthenticationUrl
20 | [Hashtable]$Header = @{}
21 | [Hashtable]$Query = @{}
22 | [bool]$RawOnly
23 |
24 | [string]$IdentityID
25 | [string]$IdentityType
26 |
27 | # Workflow: Client Secret
28 | [System.Security.SecureString]$ClientSecret
29 |
30 | # Workflow: Certificate
31 | [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
32 |
33 | # Workflow: Username & Password
34 | [PSCredential]$Credential
35 |
36 | # Workflow: Key Vault
37 | [string]$VaultName
38 | [string]$SecretName
39 |
40 | # Workflow: Az.Accounts
41 | [string]$ShowDialog
42 |
43 | # Workflow: Custom Token
44 | [scriptblock]$HeaderCode
45 | [hashtable]$Data = @{}
46 |
47 | #endregion Connection Data
48 |
49 | #region Constructors
50 | EntraToken([string]$Service, [string]$ClientID, [string]$TenantID, [Securestring]$ClientSecret, [string]$ServiceUrl, [string]$AuthenticationUrl) {
51 | $this.Service = $Service
52 | $this.ClientID = $ClientID
53 | $this.TenantID = $TenantID
54 | $this.ClientSecret = $ClientSecret
55 | $this.ServiceUrl = $ServiceUrl
56 | $this.AuthenticationUrl = $AuthenticationUrl
57 | $this.Type = 'ClientSecret'
58 | }
59 |
60 | EntraToken([string]$Service, [string]$ClientID, [string]$TenantID, [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [string]$ServiceUrl, [string]$AuthenticationUrl) {
61 | $this.Service = $Service
62 | $this.ClientID = $ClientID
63 | $this.TenantID = $TenantID
64 | $this.Certificate = $Certificate
65 | $this.ServiceUrl = $ServiceUrl
66 | $this.AuthenticationUrl = $AuthenticationUrl
67 | $this.Type = 'Certificate'
68 | }
69 |
70 | EntraToken([string]$Service, [string]$ClientID, [string]$TenantID, [pscredential]$Credential, [string]$ServiceUrl, [string]$AuthenticationUrl) {
71 | $this.Service = $Service
72 | $this.ClientID = $ClientID
73 | $this.TenantID = $TenantID
74 | $this.Credential = $Credential
75 | $this.ServiceUrl = $ServiceUrl
76 | $this.AuthenticationUrl = $AuthenticationUrl
77 | $this.Type = 'UsernamePassword'
78 | }
79 |
80 | EntraToken([string]$Service, [string]$ClientID, [string]$TenantID, [string]$ServiceUrl, [bool]$IsDeviceCode, [string]$AuthenticationUrl) {
81 | $this.Service = $Service
82 | $this.ClientID = $ClientID
83 | $this.TenantID = $TenantID
84 | $this.ServiceUrl = $ServiceUrl
85 | $this.AuthenticationUrl = $AuthenticationUrl
86 | if ($IsDeviceCode) { $this.Type = 'DeviceCode' }
87 | else { $this.Type = 'Browser' }
88 | }
89 |
90 | EntraToken([string]$Service, [string]$ClientID, [string]$TenantID, [string]$ServiceUrl, [string]$VaultName, [string]$SecretName, [string]$AuthenticationUrl) {
91 | $this.Service = $Service
92 | $this.ClientID = $ClientID
93 | $this.TenantID = $TenantID
94 | $this.ServiceUrl = $ServiceUrl
95 | $this.VaultName = $VaultName
96 | $this.SecretName = $SecretName
97 | $this.AuthenticationUrl = $AuthenticationUrl
98 | $this.Type = 'KeyVault'
99 | }
100 |
101 | EntraToken([string]$Service, [string]$ServiceUrl, [string]$IdentityID, [string]$IdentityType) {
102 | $this.Service = $Service
103 | $this.ServiceUrl = $ServiceUrl
104 | $this.Type = 'Identity'
105 |
106 | if ($IdentityID) {
107 | $this.IdentityID = $IdentityID
108 | $this.IdentityType = $IdentityType
109 | }
110 | }
111 |
112 | EntraToken([string]$Service, [string]$ServiceUrl, [string]$ShowDialog) {
113 | $this.Service = $Service
114 | $this.ServiceUrl = $ServiceUrl
115 | $this.ShowDialog = $ShowDialog
116 | $this.Type = 'AzAccount'
117 | }
118 |
119 | # Empty Constructor for Import-EntraToken
120 | EntraToken() {}
121 | #endregion Constructors
122 |
123 | [void]SetTokenMetadata([PSObject] $AuthToken) {
124 | $this.AccessToken = $AuthToken.AccessToken
125 | $this.ValidAfter = $AuthToken.ValidAfter
126 | $this.ValidUntil = $AuthToken.ValidUntil
127 | $this.Scopes = $AuthToken.Scopes
128 | if ($AuthToken.RefreshToken) { $this.RefreshToken = $AuthToken.RefreshToken }
129 |
130 | $tokenPayload = $AuthToken.AccessToken.Split(".")[1].Replace('-', '+').Replace('_', '/')
131 | while ($tokenPayload.Length % 4) { $tokenPayload += "=" }
132 | $bytes = [System.Convert]::FromBase64String($tokenPayload)
133 | $localData = [System.Text.Encoding]::ASCII.GetString($bytes) | ConvertFrom-Json
134 |
135 | if ($localData.roles) { $this.Scopes = $localData.roles }
136 | elseif ($localData.scp) { $this.Scopes = $localData.scp -split " " }
137 |
138 | $this.Audience = $localData.aud
139 | $this.Issuer = $localData.iss
140 | $this.TokenData = $localData
141 | $this.TenantID = $localData.tid
142 | }
143 |
144 | [hashtable]GetHeader() {
145 | if ($this.HeaderCode) {
146 | $newHeader = $this.Header.Clone()
147 | $results = @(& $this.HeaderCode $this)[0]
148 | foreach ($pair in $results.GetEnumerator()) {
149 | $newHeader[$pair.Key] = $pair.Value
150 | }
151 | return $newHeader
152 | }
153 |
154 | if ($this.ValidUntil -lt (Get-Date).AddMinutes(5)) {
155 | $this.RenewToken()
156 | }
157 |
158 | $currentHeader = @{}
159 | if ($this.Header.Count -gt 0) {
160 | $currentHeader = $this.Header.Clone()
161 | }
162 | $currentHeader.Authorization = "Bearer $($this.AccessToken)"
163 |
164 | return $currentHeader
165 | }
166 |
167 | [void]RenewToken() {
168 | $defaultParam = @{
169 | TenantID = $this.TenantID
170 | ClientID = $this.ClientID
171 | Resource = $this.Audience
172 | AuthenticationUrl = $this.AuthenticationUrl
173 | }
174 | switch ($this.Type) {
175 | Certificate {
176 | $result = Connect-ServiceCertificate @defaultParam -Certificate $this.Certificate
177 | $this.SetTokenMetadata($result)
178 | }
179 | ClientSecret {
180 | $result = Connect-ServiceClientSecret @defaultParam -ClientSecret $this.ClientSecret
181 | $this.SetTokenMetadata($result)
182 | }
183 | UsernamePassword {
184 | $result = Connect-ServicePassword @defaultParam -Credential $this.Credential
185 | $this.SetTokenMetadata($result)
186 | }
187 | DeviceCode {
188 | if ($this.RefreshToken) {
189 | Connect-ServiceRefreshToken -Token $this
190 | return
191 | }
192 |
193 | $result = Connect-ServiceDeviceCode @defaultParam
194 | $this.SetTokenMetadata($result)
195 | }
196 | Browser {
197 | if ($this.RefreshToken) {
198 | Connect-ServiceRefreshToken -Token $this
199 | return
200 | }
201 |
202 | $result = Connect-ServiceBrowser @defaultParam -SelectAccount
203 | $this.SetTokenMetadata($result)
204 | }
205 | Refresh {
206 | Connect-ServiceRefreshToken -Token $this
207 | }
208 | KeyVault {
209 | $secret = Get-VaultSecret -VaultName $this.VaultName -SecretName $this.SecretName
210 | $result = switch ($secret.Type) {
211 | Certificate { Connect-ServiceCertificate @defaultParam -Certificate $secret.Certificate }
212 | ClientSecret { Connect-ServiceClientSecret @defaultParam -ClientSecret $secret.ClientSecret }
213 | }
214 | $this.SetTokenMetadata($result)
215 | }
216 | Identity {
217 | $result = Connect-ServiceIdentity -Resource $this.Audience -IdentityID $this.IdentityID -IdentityType $this.IdentityType
218 | $this.SetTokenMetadata($result)
219 | }
220 | AzAccount {
221 | $result = Connect-ServiceAzure -Resource $this.Audience -ShowDialog $this.ShowDialog
222 | $this.SetTokenMetadata($result)
223 | }
224 | }
225 | }
226 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/classes/Environment.ps1:
--------------------------------------------------------------------------------
1 | enum Environment {
2 | Global = 1
3 | USGov = 2
4 | USGovDOD = 3
5 | China = 4
6 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/classes/FilterBuilder.ps1:
--------------------------------------------------------------------------------
1 | class FilterBuilder {
2 | [System.Collections.ArrayList]$Entries = @()
3 | [System.Collections.ArrayList]$CustomFilter = @()
4 |
5 | [ValidateSet('AND','OR')][string]$Logic = 'AND'
6 |
7 | FilterBuilder() { }
8 | FilterBuilder([string]$Logic) {
9 | $this.Logic = $Logic
10 | }
11 |
12 | [void]Add([string]$Property, [string]$Operator, $Value) {
13 | $this.Add($Property, $Operator, $Value, $false)
14 | }
15 | [void]Add([string]$Property, [string]$Operator, $Value, [bool]$NoQuotes) {
16 | $null = $this.Entries.Add(
17 | @{
18 | Property = $Property
19 | Operator = $Operator
20 | Value = $Value
21 | NoQuotes = $NoQuotes
22 | }
23 | )
24 | }
25 | [void]Add([string]$CustomFilter) {
26 | $null = $this.Entries.Add($CustomFilter)
27 | }
28 | [void]Add([FilterBuilder]$NestedFilter) {
29 | $null = $this.Entries.Add($NestedFilter)
30 | }
31 |
32 | [int]Count() {
33 | $myCount = $this.Entries.Count
34 | if ($this.CustomFilter) { $myCount += $this.CustomFilter.Count }
35 | return $myCount
36 | }
37 | [string]GetHelp() {
38 | return @'
39 | OData Filter Builder Guidance
40 |
41 | This tool _mostly_ maps / implements the OData filter system, as adapted by the Microsoft Graph API.
42 | It may be relevant to any other API supporting OData filters, but that's what it was built for.
43 |
44 | Filter Docs:
45 | https://learn.microsoft.com/en-us/graph/filter-query-parameter
46 |
47 | Adding Filter Conditions:
48 | There are two ways to provide filter conditions:
49 |
50 | A) .Add(property, operator, value)
51 | The .Add method adds individual filter clauses, simple comparisons or some special behaviors.
52 | For these special rules, see below.
53 | But any operator that has a simple " " from this list should be valid:
54 | https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http#operators-and-functions-supported-in-filter-expressions
55 | Strings need not be provided in quotes.
56 |
57 | B) .Add(customFilter)
58 | Specifying a single string allows providing custom filter terms/expressions.
59 | This can be any valid fragment of OData filter, giving you greater control ... but requires you to write the filter yourself.
60 |
61 | All conditions from A) and B) are finally combined with an AND condition.
62 |
63 |
64 | Special Rules:
65 |
66 | eq
67 | The "eq" Operator is converted into a 'startswith' or 'endswith' operator, depending on wildcard use.
68 |
69 | leq
70 | The "leq" Operator is not an actual OData operator, but was added by this tool.
71 | It means "Literal Equals" and is converted into an "eq" operator, but without automatic conversion to 'startswith' or 'endswith'.
72 |
73 | any
74 | The "any" Operator is the OData equivalent to the PowerShell -contains.
75 |
76 | none
77 | The "none" fake-Operator is the OData equivalent to the PowerShell -notcontains.
78 | It translates into an OData all(...) logic with the "ne" operator applied.
79 | '@
80 | }
81 | [string]Get() {
82 | $segments = :entries foreach ($entry in $this.Entries) {
83 | # Nested Filters
84 | if ($entry -is [FilterBuilder]) {
85 | '(' + $entry.Get() + ')'
86 | continue
87 | }
88 | # Custom Filters
89 | if ($entry -is [string]) {
90 | $entry
91 | continue
92 | }
93 |
94 |
95 | $quotes = "'"
96 | if ($entry.NoQuotes) { $quotes = "" }
97 |
98 | $valueString = $entry.Value -as [string]
99 | if ($null -eq $entry.Value) { $valueString = "null" }
100 | if (
101 | $entry.Value -is [string] -or
102 | $entry.Value -is [guid]
103 | ) {
104 | $valueString = "$($quotes)$($entry.Value)$($quotes)"
105 | }
106 | if ($entry.Value -is [DateTime]) {
107 | $valueString = $entry.Value.ToString('u') -replace ' ', 'T'
108 | }
109 |
110 | switch ($entry.Operator) {
111 | 'eq' {
112 | # Case: eq with Wildcard
113 | if ($entry.Value -match '\*$' -and $entry.Operator -eq 'eq') {
114 | "startswith($($entry.Property), $($quotes)$($entry.Value.TrimEnd('*'))$($quotes))"
115 | continue entries
116 | }
117 | if ($entry.Value -match '^\*' -and $entry.Operator -eq 'eq') {
118 | "endswith($($entry.Property), $($quotes)$($entry.Value.TrimStart('*'))$($quotes))"
119 | continue entries
120 | }
121 | '{0} eq {1}' -f $entry.Property, $valueString
122 | }
123 | 'leq' {
124 | '{0} eq {1}' -f $entry.Property, $valueString
125 | }
126 | 'in' {
127 | '{0} in ({1})' -f $entry.Property, (@($entry.Value).ForEach{ "$($quotes)$_$($quotes)" } -join ', ')
128 | }
129 | 'any' {
130 | '{0}/any(x:x eq {1})' -f $entry.Property, $valueString
131 | }
132 | 'none' {
133 | '{0}/all(x:x ne {1})' -f $entry.Property, $valueString
134 | }
135 | default {
136 | '{0} {1} {2}' -f $entry.Property, $entry.Operator, $valueString
137 | }
138 | }
139 | }
140 | if ($this.CustomFilter) {
141 | if ($segments) { $segments = @($segments) + $this.CustomFilter }
142 | else { $segments = $this.CustomFilter }
143 | }
144 |
145 | if ($this.Logic -eq 'OR') { return $segments -join ' or ' }
146 | return $segments -join ' and '
147 | }
148 | [hashtable]GetHeader() {
149 | return @{ '$filter' = $this.Get() }
150 | }
151 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/classes/ServiceSelector.ps1:
--------------------------------------------------------------------------------
1 | class ServiceSelector {
2 | [hashtable]$DefaultServices = @{ }
3 |
4 | ServiceSelector([Hashtable]$Services) {
5 | $this.DefaultServices = $Services
6 | }
7 |
8 | [string]GetService([hashtable]$ServiceMap, [string]$Name) {
9 | if ($ServiceMap[$Name]) { return $ServiceMap[$Name] }
10 |
11 | return $this.DefaultServices[$Name]
12 | }
13 | [hashtable]GetServiceMap([hashtable]$ServiceMap) {
14 | $map = $this.DefaultServices.Clone()
15 | if ($ServiceMap) {
16 | foreach ($pair in $ServiceMap.GetEnumerator()) {
17 | $map[$pair.Key] = $pair.Value
18 | }
19 | }
20 | return $map
21 | }
22 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceAzure.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceAzure {
2 | <#
3 | .SYNOPSIS
4 | Authenticates using the established session from Az.Accounts.
5 |
6 | .DESCRIPTION
7 | Authenticates using the established session from Az.Accounts.
8 | This limits the scopes available to what is configured on the Az Application, but makes it easy to authenticate without active interaction.
9 |
10 | Pretty useful for authenticating to custom apps that do not actually implement scopes.
11 |
12 | .PARAMETER Resource
13 | The resource owning the api permissions / scopes requested.
14 |
15 | .PARAMETER ShowDialog
16 | Whether to show a dialog in case of interaction being needed.
17 | Defaults to: auto
18 |
19 | .EXAMPLE
20 | PS C:\> Connect-ServiceAzure -Resource 'https://graph.microsoft.com'
21 |
22 | Connect to graph using the existing az.accounts session.
23 | #>
24 | [CmdletBinding()]
25 | param (
26 | [Parameter(Mandatory = $true)]
27 | [string]
28 | $Resource,
29 |
30 | [ValidateSet('Auto', 'Always', 'Never')]
31 | [string]
32 | $ShowDialog = 'Auto'
33 | )
34 | process {
35 | try { $azContext = Get-AzContext -ErrorAction Stop }
36 | catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message 'Error accessing azure context. Ensure the module "Az.Accounts" is installed and you have connected via "Connect-AzAccount"!' -ErrorRecord $_ }
37 |
38 | try {
39 | $result = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate(
40 | $azContext.Account,
41 | $azContext.Environment,
42 | "$($azContext.Tenant.id)",
43 | $null,
44 | $ShowDialog,
45 | $null,
46 | $Resource
47 | )
48 |
49 | }
50 | catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Error retrieving token from Azure for '$Resource': $_" -ErrorRecord $_ }
51 |
52 | $tokenData = Read-TokenData -Token $result.AccessToken
53 |
54 | # A Fake AuthResponse result - Should keep the same layout as the result of Read-AuthResponse
55 | [PSCustomObject]@{
56 | AccessToken = $result.AccessToken
57 | ValidAfter = Get-Date
58 | ValidUntil = $result.ExpiresOn.LocalDateTime
59 | Scopes = $tokenData.scp -split ' '
60 | RefreshToken = $null
61 |
62 | # For Initial Connect Metadata
63 | ClientID = $tokenData.appid
64 | TenantID = $tokenData.tid
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceBrowser.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceBrowser {
2 | <#
3 | .SYNOPSIS
4 | Interactive logon using the Authorization flow and browser. Supports SSO.
5 |
6 | .DESCRIPTION
7 | Interactive logon using the Authorization flow and browser. Supports SSO.
8 |
9 | This flow requires an App Registration configured for the platform "Mobile and desktop applications".
10 | Its redirect Uri must be "http://localhost"
11 |
12 | On successful authentication
13 |
14 | .PARAMETER ClientID
15 | The ID of the registered app used with this authentication request.
16 |
17 | .PARAMETER TenantID
18 | The ID of the tenant connected to with this authentication request.
19 |
20 | .PARAMETER SelectAccount
21 | Forces account selection on logon.
22 | As this flow supports single-sign-on, it will otherwise not prompt for anything if already signed in.
23 | This could be a problem if you want to connect using another (e.g. an admin) account.
24 |
25 | .PARAMETER Scopes
26 | Generally doesn't need to be changed from the default '.default'
27 |
28 | .PARAMETER LocalPort
29 | The local port that should be redirected to.
30 | In order to process the authentication response, we need to listen to a local web request on some port.
31 | Usually needs not be redirected.
32 | Defaults to: 8080
33 |
34 | .PARAMETER Resource
35 | The resource owning the api permissions / scopes requested.
36 |
37 | .PARAMETER Browser
38 | The path to the browser to use for the authentication flow.
39 | Provide the full path to the executable.
40 | The browser must accept the url to open as its only parameter.
41 | Defaults to your default browser.
42 |
43 | .PARAMETER BrowserMode
44 | How the browser used for authentication is selected.
45 | Options:
46 | + Auto (default): Automatically use the default browser.
47 | + PrintLink: The link to open is printed on console and user selects which browser to paste it into (must be used on the same machine)
48 |
49 | .PARAMETER NoReconnect
50 | Disables automatic reconnection.
51 | By default, this module will automatically try to reaquire a new token before the old one expires.
52 |
53 | .PARAMETER AuthenticationUrl
54 | The url used for the authentication requests to retrieve tokens.
55 |
56 | .EXAMPLE
57 | PS C:\> Connect-ServiceBrowser -ClientID '' -TenantID ''
58 |
59 | Connects to the specified tenant using the specified client, prompting the user to authorize via Browser.
60 | #>
61 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
62 | [CmdletBinding()]
63 | param (
64 | [Parameter(Mandatory = $true)]
65 | [string]
66 | $TenantID,
67 |
68 | [Parameter(Mandatory = $true)]
69 | [string]
70 | $ClientID,
71 |
72 | [Parameter(Mandatory = $true)]
73 | [string]
74 | $Resource,
75 |
76 | [switch]
77 | $SelectAccount,
78 |
79 | [AllowEmptyCollection()]
80 | [string[]]
81 | $Scopes,
82 |
83 | [int]
84 | $LocalPort = 8080,
85 |
86 | [string]
87 | $Browser,
88 |
89 | [Parameter(ParameterSetName = 'Browser')]
90 | [ValidateSet('Auto', 'PrintLink')]
91 | [string]
92 | $BrowserMode = 'Auto',
93 |
94 | [switch]
95 | $NoReconnect,
96 |
97 | [Parameter(Mandatory = $true)]
98 | [string]
99 | $AuthenticationUrl
100 | )
101 | process {
102 | Add-Type -AssemblyName System.Web
103 | if (-not $Scopes) { $Scopes = @('.default') }
104 |
105 | $redirectUri = "http://localhost:$LocalPort"
106 | $actualScopes = $Scopes | Resolve-ScopeName -Resource $Resource
107 |
108 | if (-not $NoReconnect) {
109 | $actualScopes = @($actualScopes) + 'offline_access'
110 | }
111 |
112 | $uri = "$AuthenticationUrl/$TenantID/oauth2/v2.0/authorize?"
113 | $state = Get-Random
114 | $parameters = @{
115 | client_id = $ClientID
116 | response_type = 'code'
117 | redirect_uri = $redirectUri
118 | response_mode = 'query'
119 | scope = $actualScopes -join ' '
120 | state = $state
121 | }
122 | if ($SelectAccount) {
123 | $parameters.prompt = 'select_account'
124 | }
125 |
126 | $paramStrings = foreach ($pair in $parameters.GetEnumerator()) {
127 | $pair.Key, ([System.Web.HttpUtility]::UrlEncode($pair.Value)) -join '='
128 | }
129 | $uriFinal = $uri + ($paramStrings -join '&')
130 | Write-Verbose "Authorize Uri: $uriFinal"
131 |
132 | $redirectTo = 'https://raw.githubusercontent.com/FriedrichWeinmann/MiniGraph/master/nothing-to-see-here.txt'
133 | if ((Get-Random -Minimum 10 -Maximum 99) -eq 66) {
134 | $redirectTo = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
135 | }
136 |
137 | # Start local server to catch the redirect
138 | $http = [System.Net.HttpListener]::new()
139 | $http.Prefixes.Add("$redirectUri/")
140 | try { $http.Start() }
141 | catch { Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Failed to create local http listener on port $LocalPort. Use -LocalPort to select a different port. $_" -Category OpenError }
142 |
143 | switch ($BrowserMode) {
144 | Auto {
145 | # Execute in default browser
146 | if ($Browser) { & $Browser $uriFinal }
147 | else { Start-Process $uriFinal }
148 | }
149 | PrintLink {
150 | Write-Host @"
151 | Ready to authenticate. Paste the following link into the browser of your choice on the local computer:
152 | $uriFinal
153 | "@
154 | }
155 | }
156 |
157 | # Get Result
158 | $task = $http.GetContextAsync()
159 | $authorizationCode, $stateReturn, $sessionState = $null
160 | try {
161 | while (-not $task.IsCompleted) {
162 | Start-Sleep -Milliseconds 200
163 | }
164 | $context = $task.Result
165 | $context.Response.Redirect($redirectTo)
166 | $context.Response.Close()
167 | $authorizationCode, $stateReturn, $sessionState = $context.Request.Url.Query -split "&"
168 | }
169 | finally {
170 | $http.Stop()
171 | $http.Dispose()
172 | }
173 |
174 | if (-not $stateReturn) {
175 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Authentication failed (see browser for details)" -Category AuthenticationError
176 | }
177 |
178 | if ($stateReturn -match '^error_description=') {
179 | $message = $stateReturn -replace '^error_description=' -replace '\+',' '
180 | $message = [System.Web.HttpUtility]::UrlDecode($message)
181 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Error processing the request: $message" -Category InvalidOperation
182 | }
183 |
184 | if ($state -ne $stateReturn.Split("=")[1]) {
185 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Received invalid authentication result. Likely returned from another flow redirecting to the same local port!" -Category InvalidOperation
186 | }
187 |
188 | $actualAuthorizationCode = $authorizationCode.Split("=")[1]
189 |
190 | $body = @{
191 | client_id = $ClientID
192 | scope = $actualScopes -join " "
193 | code = $actualAuthorizationCode
194 | redirect_uri = $redirectUri
195 | grant_type = 'authorization_code'
196 | }
197 | $uri = "$AuthenticationUrl/$TenantID/oauth2/v2.0/token"
198 | try { $authResponse = Invoke-RestMethod -Method Post -Uri $uri -Body $body -ErrorAction Stop }
199 | catch {
200 | if ($_ -notmatch '"error":\s*"invalid_client"') { Invoke-TerminatingException -Cmdlet $PSCmdlet -ErrorRecord $_ }
201 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "The App Registration $ClientID has not been configured correctly. Ensure you have a 'Mobile and desktop applications' platform with redirect to 'http://localhost' configured (and not a 'Web' Platform). $_" -Category $_.CategoryInfo.Category
202 | }
203 | Read-AuthResponse -AuthResponse $authResponse
204 | }
205 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceCertificate.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceCertificate {
2 | <#
3 | .SYNOPSIS
4 | Connects to AAD using a application ID and a certificate.
5 |
6 | .DESCRIPTION
7 | Connects to AAD using a application ID and a certificate.
8 |
9 | .PARAMETER Resource
10 | The resource owning the api permissions / scopes requested.
11 |
12 | .PARAMETER Certificate
13 | The certificate to use for authentication.
14 |
15 | .PARAMETER TenantID
16 | The ID of the tenant/directory to connect to.
17 |
18 | .PARAMETER ClientID
19 | The ID of the registered application used to authenticate as.
20 |
21 | .PARAMETER AuthenticationUrl
22 | The url used for the authentication requests to retrieve tokens.
23 |
24 | .EXAMPLE
25 | PS C:\> Connect-ServiceCertificate -Certificate $cert -TenantID $tenantID -ClientID $clientID
26 |
27 | Connects to the specified tenant using the specified app & cert.
28 |
29 | .LINK
30 | https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
31 | #>
32 | [CmdletBinding()]
33 | param (
34 | [Parameter(Mandatory = $true)]
35 | [string]
36 | $Resource,
37 |
38 | [Parameter(Mandatory = $true)]
39 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
40 | $Certificate,
41 |
42 | [Parameter(Mandatory = $true)]
43 | [string]
44 | $TenantID,
45 |
46 | [Parameter(Mandatory = $true)]
47 | [string]
48 | $ClientID,
49 |
50 | [Parameter(Mandatory = $true)]
51 | [string]
52 | $AuthenticationUrl
53 | )
54 |
55 | #region Build Signature Payload
56 | $jwtHeader = @{
57 | alg = "RS256"
58 | typ = "JWT"
59 | x5t = [Convert]::ToBase64String($Certificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '='
60 | }
61 | $encodedHeader = $jwtHeader | ConvertTo-Json | ConvertTo-Base64
62 | $claims = @{
63 | aud = "$AuthenticationUrl/$TenantID/v2.0"
64 | exp = ((Get-Date).AddMinutes(5).ToUniversalTime() - (Get-Date -Date '1970-01-01')).TotalSeconds -as [int]
65 | iss = $ClientID
66 | jti = "$(New-Guid)"
67 | nbf = ((Get-Date).ToUniversalTime() - (Get-Date -Date '1970-01-01')).TotalSeconds -as [int]
68 | sub = $ClientID
69 | }
70 | $encodedClaims = $claims | ConvertTo-Json | ConvertTo-Base64
71 | $jwtPreliminary = $encodedHeader, $encodedClaims -join "."
72 | $jwtSigned = ($jwtPreliminary | ConvertTo-SignedString -Certificate $Certificate) -replace '\+', '-' -replace '/', '_' -replace '='
73 | $jwt = $jwtPreliminary, $jwtSigned -join '.'
74 | #endregion Build Signature Payload
75 |
76 | $body = @{
77 | client_id = $ClientID
78 | client_assertion = $jwt
79 | client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
80 | scope = '{0}/.default' -f $Resource
81 | grant_type = 'client_credentials'
82 | }
83 | $header = @{
84 | Authorization = "Bearer $jwt"
85 | }
86 | $uri = "$AuthenticationUrl/$TenantID/oauth2/v2.0/token"
87 |
88 | try { $authResponse = Invoke-RestMethod -Method Post -Uri $uri -Body $body -Headers $header -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop }
89 | catch { throw }
90 |
91 | Read-AuthResponse -AuthResponse $authResponse
92 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceClientSecret.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceClientSecret {
2 | <#
3 | .SYNOPSIS
4 | Connets using a client secret.
5 |
6 | .DESCRIPTION
7 | Connets using a client secret.
8 |
9 | .PARAMETER Resource
10 | The resource owning the api permissions / scopes requested.
11 |
12 | .PARAMETER ClientID
13 | The ID of the registered app used with this authentication request.
14 |
15 | .PARAMETER TenantID
16 | The ID of the tenant connected to with this authentication request.
17 |
18 | .PARAMETER ClientSecret
19 | The actual secret used for authenticating the request.
20 |
21 | .PARAMETER AuthenticationUrl
22 | The url used for the authentication requests to retrieve tokens.
23 |
24 | .EXAMPLE
25 | PS C:\> Connect-ServiceClientSecret -ClientID '' -TenantID '' -ClientSecret $secret
26 |
27 | Connects to the specified tenant using the specified client and secret.
28 | #>
29 | [CmdletBinding()]
30 | param (
31 | [Parameter(Mandatory = $true)]
32 | [string]
33 | $Resource,
34 |
35 | [Parameter(Mandatory = $true)]
36 | [string]
37 | $ClientID,
38 |
39 | [Parameter(Mandatory = $true)]
40 | [string]
41 | $TenantID,
42 |
43 | [Parameter(Mandatory = $true)]
44 | [securestring]
45 | $ClientSecret,
46 |
47 | [Parameter(Mandatory = $true)]
48 | [string]
49 | $AuthenticationUrl
50 | )
51 |
52 | process {
53 | $body = @{
54 | resource = $Resource
55 | client_id = $ClientID
56 | client_secret = [PSCredential]::new('NoMatter', $ClientSecret).GetNetworkCredential().Password
57 | grant_type = 'client_credentials'
58 | }
59 | try { $authResponse = Invoke-RestMethod -Method Post -Uri "$AuthenticationUrl/$TenantId/oauth2/token" -Body $body -ErrorAction Stop }
60 | catch { throw }
61 |
62 | Read-AuthResponse -AuthResponse $authResponse
63 | }
64 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceDeviceCode.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceDeviceCode {
2 | <#
3 | .SYNOPSIS
4 | Connects to Azure AD using the Device Code authentication workflow.
5 |
6 | .DESCRIPTION
7 | Connects to Azure AD using the Device Code authentication workflow.
8 |
9 | .PARAMETER Resource
10 | The resource owning the api permissions / scopes requested.
11 |
12 | .PARAMETER ClientID
13 | The ID of the registered app used with this authentication request.
14 |
15 | .PARAMETER TenantID
16 | The ID of the tenant connected to with this authentication request.
17 |
18 | .PARAMETER Scopes
19 | The scopes to request.
20 | Automatically scoped to the service specified via Service Url.
21 | Defaults to ".Default"
22 |
23 | .PARAMETER NoReconnect
24 | Disables automatic reconnection.
25 | By default, this module will automatically try to reaquire a new token before the old one expires.
26 |
27 | .PARAMETER AuthenticationUrl
28 | The url used for the authentication requests to retrieve tokens.
29 |
30 | .EXAMPLE
31 | PS C:\> Connect-ServiceDeviceCode -ServiceUrl $url -ClientID '' -TenantID ''
32 |
33 | Connects to the specified tenant using the specified client, prompting the user to authorize via Browser.
34 | #>
35 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
36 | [CmdletBinding()]
37 | param (
38 | [Parameter(Mandatory = $true)]
39 | [string]
40 | $Resource,
41 |
42 | [Parameter(Mandatory = $true)]
43 | [string]
44 | $ClientID,
45 |
46 | [Parameter(Mandatory = $true)]
47 | [string]
48 | $TenantID,
49 |
50 | [AllowEmptyCollection()]
51 | [string[]]
52 | $Scopes,
53 |
54 | [switch]
55 | $NoReconnect,
56 |
57 | [Parameter(Mandatory = $true)]
58 | [string]
59 | $AuthenticationUrl
60 | )
61 |
62 | if (-not $Scopes) { $Scopes = @('.default') }
63 | $actualScopes = $Scopes | Resolve-ScopeName -Resource $Resource
64 |
65 | if (-not $NoReconnect) {
66 | $actualScopes = @($actualScopes) + 'offline_access'
67 | }
68 |
69 | try {
70 | $initialResponse = Invoke-RestMethod -Method POST -Uri "$AuthenticationUrl/$TenantID/oauth2/v2.0/devicecode" -Body @{
71 | client_id = $ClientID
72 | scope = $actualScopes -join " "
73 | } -ErrorAction Stop
74 | }
75 | catch {
76 | throw
77 | }
78 |
79 | Write-Host $initialResponse.message
80 |
81 | $paramRetrieve = @{
82 | Uri = "$AuthenticationUrl/$TenantID/oauth2/v2.0/token"
83 | Method = "POST"
84 | Body = @{
85 | grant_type = "urn:ietf:params:oauth:grant-type:device_code"
86 | client_id = $ClientID
87 | device_code = $initialResponse.device_code
88 | }
89 | ErrorAction = 'Stop'
90 | }
91 | $limit = (Get-Date).AddSeconds($initialResponse.expires_in)
92 | while ($true) {
93 | if ((Get-Date) -gt $limit) {
94 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Timelimit exceeded, device code authentication failed" -Category AuthenticationError
95 | }
96 | Start-Sleep -Seconds $initialResponse.interval
97 | try { $authResponse = Invoke-RestMethod @paramRetrieve }
98 | catch {
99 | if ($_ -match '"error":\s*"authorization_pending"') { continue }
100 | $PSCmdlet.ThrowTerminatingError($_)
101 | }
102 | if ($authResponse) {
103 | break
104 | }
105 | }
106 |
107 | Read-AuthResponse -AuthResponse $authResponse
108 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceIdentity.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceIdentity {
2 | <#
3 | .SYNOPSIS
4 | Connect as the current Managed Identity.
5 |
6 | .DESCRIPTION
7 | Connect as the current Managed Identity.
8 | Only works from within the context of a managed environment, such as Azure Functions with enabled MSI.
9 |
10 | .PARAMETER Resource
11 | The resource to get a token for.
12 |
13 | .PARAMETER IdentityID
14 | ID of the User-Managed Identity to connect as.
15 |
16 | .PARAMETER IdentityType
17 | Type of the User-Managed Identity.
18 |
19 | .PARAMETER Cmdlet
20 | The $PSCmdlet of the calling command.
21 | If specified, errors are triggered in the caller's context.
22 |
23 | .EXAMPLE
24 | PS C:\> Connect-ServiceIdentity -Resource 'https://vault.azure.net'
25 |
26 | Connect as the current managed identity, retrieving a token for the Azure Key Vault.
27 |
28 | .LINK
29 | https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity
30 | #>
31 | [CmdletBinding()]
32 | param (
33 | [Parameter(Mandatory = $true)]
34 | [string]
35 | $Resource,
36 |
37 | [AllowEmptyString()]
38 | [AllowNull()]
39 | [string]
40 | $IdentityID,
41 |
42 | [AllowEmptyString()]
43 | [AllowNull()]
44 | [string]
45 | $IdentityType,
46 |
47 | $Cmdlet = $PSCmdlet
48 | )
49 | process {
50 | # Logic for Azure VMs
51 | try {
52 | $vmMetadata = $null
53 | $vmMetadata = Invoke-RestMethod -Headers @{Metadata = "true" } -Method GET -NoProxy -Uri "http://169.254.169.254/metadata/instance?api-version=2021-02-01"
54 | }
55 | catch {
56 | $vmMetadata = $null
57 | }
58 | if ($vmMetadata.compute.azEnvironment -like "*Azure*") {
59 | Write-Verbose "We are running on an Azure VM. Setting Environment Variables"
60 | $isAzureVM = $true
61 | $env:IDENTITY_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"
62 | $env:IDENTITY_API_VERSION = "2018-02-01"
63 | }
64 |
65 | if ((-not $env:IDENTITY_ENDPOINT) -or (-not $env:IDENTITY_HEADER)) {
66 | Invoke-TerminatingException -Cmdlet $Cmdlet -Message "Cannot identify a Managed Identity. MSI logon not possible!" -Category ConnectionError
67 | }
68 |
69 | $apiVersion = $env:IDENTITY_API_VERSION
70 | if (-not $apiVersion) { $apiVersion = '2019-08-01' }
71 |
72 | $url = "$($env:IDENTITY_ENDPOINT)?resource=$Resource&api-version=$apiVersion"
73 | if ($IdentityID) {
74 | $labels = @{
75 | ClientID = 'client_id'
76 | ResourceID = 'mi_res_id'
77 | PrincipalID = 'principal_id'
78 | }
79 | $url = $url + "&$($labels[$IdentityType])=$($IdentityID)"
80 | }
81 |
82 | try {
83 | Write-Verbose "$url"
84 | if ($isAzureVM) {
85 | $headers = @{Metadata = 'true' }
86 | }
87 | else {
88 | $headers = @{'X-IDENTITY-HEADER' = $env:IDENTITY_HEADER }
89 | }
90 | $authResponse = Invoke-RestMethod -Uri $url -Headers $headers -ErrorAction Stop
91 | }
92 | catch {
93 | Invoke-TerminatingException -Cmdlet $Cmdlet -Message "Failed to connect via Managed Identity: $_" -ErrorRecord $_
94 | }
95 |
96 | Read-AuthResponse -AuthResponse $authResponse
97 | }
98 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServicePassword.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServicePassword {
2 | <#
3 | .SYNOPSIS
4 | Connect to graph using username and password.
5 |
6 | .DESCRIPTION
7 | Connect to graph using username and password.
8 | This logs into graph as a user, not as an application.
9 | Only cloud-only accounts can be used for this workflow.
10 | Consent to scopes must be granted before using them, as this command cannot show the consent prompt.
11 |
12 | .PARAMETER Resource
13 | The resource owning the api permissions / scopes requested.
14 |
15 | .PARAMETER Credential
16 | Credentials of the user to connect as.
17 |
18 | .PARAMETER TenantID
19 | The Guid of the tenant to connect to.
20 |
21 | .PARAMETER ClientID
22 | The ClientID / ApplicationID of the application to use.
23 |
24 | .PARAMETER Scopes
25 | The permission scopes to request.
26 |
27 | .PARAMETER AuthenticationUrl
28 | The url used for the authentication requests to retrieve tokens.
29 |
30 | .EXAMPLE
31 | PS C:\> Connect-ServicePassword -Credential max@contoso.com -ClientID $client -TenantID $tenant -Scopes 'user.read','user.readbasic.all'
32 |
33 | Connect as max@contoso.com with the rights to read user information.
34 | #>
35 | [CmdletBinding()]
36 | param (
37 | [Parameter(Mandatory = $true)]
38 | [string]
39 | $Resource,
40 |
41 | [Parameter(Mandatory = $true)]
42 | [System.Management.Automation.PSCredential]
43 | $Credential,
44 |
45 | [Parameter(Mandatory = $true)]
46 | [string]
47 | $ClientID,
48 |
49 | [Parameter(Mandatory = $true)]
50 | [string]
51 | $TenantID,
52 |
53 | [string[]]
54 | $Scopes = '.default',
55 |
56 | [Parameter(Mandatory = $true)]
57 | [string]
58 | $AuthenticationUrl
59 | )
60 |
61 | $actualScopes = $Scopes | Resolve-ScopeName -Resource $Resource
62 |
63 | $request = @{
64 | client_id = $ClientID
65 | scope = $actualScopes -join " "
66 | username = $Credential.UserName
67 | password = $Credential.GetNetworkCredential().Password
68 | grant_type = 'password'
69 | }
70 |
71 | try { $authResponse = Invoke-RestMethod -Method POST -Uri "$AuthenticationUrl/$TenantID/oauth2/v2.0/token" -Body $request -ErrorAction Stop }
72 | catch { throw }
73 |
74 | Read-AuthResponse -AuthResponse $authResponse
75 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Connect-ServiceRefreshToken.ps1:
--------------------------------------------------------------------------------
1 | function Connect-ServiceRefreshToken {
2 | <#
3 | .SYNOPSIS
4 | Connect with the refresh token provided previously.
5 |
6 | .DESCRIPTION
7 | Connect with the refresh token provided previously.
8 | Used mostly for delegate authentication flows to avoid interactivity.
9 |
10 | Can also be resolved to from the outside, when trying to get multiple tokens with a single delegate flow.
11 |
12 | .PARAMETER Token
13 | The EntraToken object with the refresh token to use.
14 | The token is then refreshed in-place with no output provided.
15 |
16 | .PARAMETER RefreshToken
17 | The RefreshToken to use for authenticating.
18 |
19 | .PARAMETER TenantID
20 | ID of the tenant to connect to.
21 |
22 | .PARAMETER ClientID
23 | ID of the application to connect as.
24 |
25 | .PARAMETER Resource
26 | Resource we want the scopes for.
27 |
28 | .PARAMETER Scopes
29 | Scopes we want to use.
30 |
31 | .PARAMETER AuthenticationUrl
32 | The url used for the authentication requests to retrieve tokens.
33 |
34 | .EXAMPLE
35 | PS C:\> Connect-ServiceRefreshToken -Token $token
36 |
37 | Connect with the refresh token provided previously.
38 | #>
39 | [CmdletBinding()]
40 | param (
41 | [Parameter(Mandatory = $true, ParameterSetName = 'Token')]
42 | $Token,
43 |
44 | [Parameter(Mandatory = $true, ParameterSetName = 'Details')]
45 | [string]
46 | $RefreshToken,
47 |
48 | [Parameter(Mandatory = $true, ParameterSetName = 'Details')]
49 | [string]
50 | $TenantID,
51 |
52 | [Parameter(Mandatory = $true, ParameterSetName = 'Details')]
53 | [string]
54 | $ClientID,
55 |
56 | [Parameter(Mandatory = $true, ParameterSetName = 'Details')]
57 | [string]
58 | $Resource,
59 |
60 | [Parameter(ParameterSetName = 'Details')]
61 | [string[]]
62 | $Scopes = '.default',
63 |
64 | [Parameter(Mandatory = $true, ParameterSetName = 'Details')]
65 | [string]
66 | $AuthenticationUrl
67 | )
68 | process {
69 | switch ($PSCmdlet.ParameterSetName) {
70 | 'Token' {
71 | if (-not $Token.RefreshToken) {
72 | throw "Failed to refresh token: No refresh token found!"
73 | }
74 |
75 | $effectiveScopes = $Token.Scopes
76 |
77 | $body = @{
78 | client_id = $Token.ClientID
79 | scope = @($effectiveScopes).ForEach{"$($Token.Audience)/$($_)"} -join " "
80 | refresh_token = $Token.RefreshToken
81 | grant_type = 'refresh_token'
82 | }
83 | $uri = "$($Token.AuthenticationUrl)/$($Token.TenantID)/oauth2/v2.0/token"
84 | $authResponse = Invoke-RestMethod -Method Post -Uri $uri -Body $body
85 | $Token.SetTokenMetadata((Read-AuthResponse -AuthResponse $authResponse))
86 | }
87 | 'Details' {
88 | $effectiveScopes = foreach ($scope in $Scopes) {
89 | if ($scope -like "$Resource*") { $scope }
90 | else { "$Resource/$scope" }
91 | }
92 |
93 | $body = @{
94 | client_id = $ClientID
95 | scope = $effectiveScopes -join " "
96 | refresh_token = $RefreshToken
97 | grant_type = 'refresh_token'
98 | }
99 | $uri = "$($AuthenticationUrl)/$($TenantID)/oauth2/v2.0/token"
100 | $authResponse = Invoke-RestMethod -Method Post -Uri $uri -Body $body
101 | Read-AuthResponse -AuthResponse $authResponse
102 | }
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Get-VaultSecret.ps1:
--------------------------------------------------------------------------------
1 | function Get-VaultSecret {
2 | <#
3 | .SYNOPSIS
4 | Retrieve a secret from Azure Key Vault.
5 |
6 | .DESCRIPTION
7 | Retrieve a secret from Azure Key Vault.
8 | Works for both certificates and secrets.
9 |
10 | Requires one of ...
11 | - An established connection with the AzureKeyVault service.
12 | - An established AZ session via Az.Accounts with the Az.KeyVault module present.
13 |
14 | .PARAMETER VaultName
15 | Name of the Vault to query.
16 |
17 | .PARAMETER SecretName
18 | Name of the Secret to retrieve.
19 |
20 | .PARAMETER Cmdlet
21 | The $PSCmdlet object of the caller, enabling errors to happen within the scope of the caller.
22 | Defaults to the current command's $PSCmdlet
23 |
24 | .EXAMPLE
25 | PS C:\> Get-VaultSecret -VaultName myvault -SecretName mysecret
26 |
27 | Retrieves the latest enabled version of mysecret from myvault
28 | #>
29 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
30 | [CmdletBinding()]
31 | param (
32 | [Parameter(Mandatory = $true)]
33 | [string]
34 | $VaultName,
35 |
36 | [Parameter(Mandatory = $true)]
37 | [string]
38 | $SecretName,
39 |
40 | $Cmdlet = $PSCmdlet
41 | )
42 |
43 | process {
44 | #region Via EntraAuth
45 | if (Get-EntraToken -Service AzureKeyVault) {
46 | try {
47 | $secretVersion = Invoke-EntraRequest -Service AzureKeyVault -Path "secrets/$SecretName/versions" -VaultName $VaultName -ErrorAction Stop | Where-Object {
48 | $_.attributes.enabled
49 | } | Sort-Object { $_.attributes.created } -Descending | Select-Object -First 1
50 |
51 | if (-not $secretVersion) {
52 | Invoke-TerminatingException -Cmdlet $Cmdlet -ErrorRecord $_ -Message "Secret '$SecretName' does not exist in vault '$VaultName'! $_"
53 | }
54 | $secretData = Invoke-EntraRequest -Service AzureKeyVault -Path $secretVersion.id -VaultName $VaultName -ErrorAction Stop
55 | }
56 | catch {
57 | Invoke-TerminatingException -Cmdlet $Cmdlet -ErrorRecord $_ -Message "Failed to retrieve secret '$SecretName' from '$VaultName'! $_"
58 | }
59 |
60 | if ($secretVersion.contentType) {
61 | $secretBytes = [convert]::FromBase64String($secretData)
62 | $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($secretBytes)
63 | [PSCustomObject]@{
64 | Type = 'Certificate'
65 | Certificate = $certificate
66 | ClientSecret = $null
67 | }
68 | }
69 | else {
70 | [PSCustomObject]@{
71 | Type = 'ClientSecret'
72 | Certificate = $null
73 | ClientSecret = $secretData | ConvertTo-SecureString -AsPlainText -Force
74 | }
75 | }
76 |
77 | return
78 | }
79 | #endregion Via EntraAuth
80 |
81 | #region Via Az.KeyVault
82 | if ((Get-Module Az.Accounts -ListAvailable) -and (Get-AzContext) -and (Get-Module Az.KeyVault -ListAvailable)) {
83 | try { $secret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName }
84 | catch { Invoke-TerminatingException -Cmdlet $Cmdlet -ErrorRecord $_ -Message "Error accessing the secret '$Secretname' from Vault '$VaultName'. $_" }
85 |
86 | $type = 'Certificate'
87 | if (-not $secret.ContentType) { $type = 'ClientSecret' }
88 |
89 | $certificate = $null
90 | $clientSecret = $secret.SecretValue
91 |
92 | if ($type -eq 'Certificate') {
93 | $certString = [PSCredential]::New("irrelevant", $secret.SecretValue).GetNetworkCredential().Password
94 | $bytes = [convert]::FromBase64String($certString)
95 | $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)
96 | $clientSecret = $null
97 | }
98 |
99 | [PSCustomObject]@{
100 | Type = $type
101 | Certificate = $certificate
102 | ClientSecret = $clientSecret
103 | }
104 |
105 | return
106 | }
107 | #endregion Via Az.KeyVault
108 |
109 | Invoke-TerminatingException -Cmdlet $Cmdlet -Message "Not connected to azure yet! Either use 'Connect-EntraService -Service AzureKeyVault' or 'Connect-AzAccount' before trying to connect via KeyVault!" -Category ConnectionError
110 | }
111 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/authentication/Read-AuthResponse.ps1:
--------------------------------------------------------------------------------
1 | function Read-AuthResponse {
2 | <#
3 | .SYNOPSIS
4 | Produces a standard output representation of the authentication response received.
5 |
6 | .DESCRIPTION
7 | Produces a standard output representation of the authentication response received.
8 | This streamlines the token processing and simplifies the connection code.
9 |
10 | .PARAMETER AuthResponse
11 | The authentication response received.
12 |
13 | .EXAMPLE
14 | PS C:\> Read-AuthResponse -AuthResponse $authResponse
15 |
16 | Reads the authentication details received.
17 | #>
18 | [CmdletBinding()]
19 | param (
20 | $AuthResponse
21 | )
22 | process {
23 | if ($AuthResponse.expires_in) {
24 | $after = (Get-Date).AddMinutes(-5)
25 | $until = (Get-Date).AddSeconds($AuthResponse.expires_in)
26 | }
27 | else {
28 | if ($AuthResponse.not_before) { $after = (Get-Date -Date '1970-01-01').AddSeconds($AuthResponse.not_before).ToLocalTime() }
29 | else { $after = Get-Date }
30 | $until = (Get-Date -Date '1970-01-01').AddSeconds($AuthResponse.expires_on).ToLocalTime()
31 | }
32 | $scopes = @()
33 | if ($AuthResponse.scope) { $scopes = $authResponse.scope -split " " }
34 |
35 | # If updating this layout, also update in Connect-ServiceAzure, which fakes this object
36 | [pscustomobject]@{
37 | AccessToken = $AuthResponse.access_token
38 | ValidAfter = $after
39 | ValidUntil = $until
40 | Scopes = $scopes
41 | RefreshToken = $AuthResponse.refresh_token
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/ConvertTo-Base64.ps1:
--------------------------------------------------------------------------------
1 | function ConvertTo-Base64 {
2 | <#
3 | .SYNOPSIS
4 | Converts the input-string to its base 64 encoded string form.
5 |
6 | .DESCRIPTION
7 | Converts the input-string to its base 64 encoded string form.
8 |
9 | .PARAMETER Text
10 | The text to convert.
11 |
12 | .PARAMETER Encoding
13 | The encoding of the input text.
14 | Used to correctly translate the input string into bytes before converting those to base 64.
15 | Defaults to UTF8
16 |
17 | .EXAMPLE
18 | PS C:\> Get-Content .\code.ps1 -Raw | ConvertTo-Base64
19 |
20 | Reads the input file and converts its content into base64.
21 | #>
22 | [OutputType([string])]
23 | [CmdletBinding()]
24 | param (
25 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
26 | [string[]]
27 | $Text,
28 |
29 | [System.Text.Encoding]
30 | $Encoding = [System.Text.Encoding]::UTF8
31 | )
32 |
33 | process {
34 | foreach ($entry in $Text) {
35 | $bytes = $Encoding.GetBytes($entry)
36 | [Convert]::ToBase64String($bytes)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/ConvertTo-Hashtable.ps1:
--------------------------------------------------------------------------------
1 | function ConvertTo-Hashtable {
2 | <#
3 | .SYNOPSIS
4 | Converts input objects into hashtables.
5 |
6 | .DESCRIPTION
7 | Converts input objects into hashtables.
8 | Allows explicitly including some properties only and remapping key-names as required.
9 |
10 | .PARAMETER Include
11 | Only select the specified properties.
12 |
13 | .PARAMETER Mapping
14 | Remap hashtable/property keys.
15 | This allows you to rename parameters before passing them through to other commands.
16 | Example:
17 | @{ Select = '$select' }
18 | This will map the "Select"-property/key on the input object to be '$select' on the output item.
19 |
20 | .PARAMETER InputObject
21 | The object to convert.
22 |
23 | .EXAMPLE
24 | PS C:\> $__body = $PSBoundParameters | ConvertTo-Hashtable -Include Name, UserID -Mapping $__mapping
25 |
26 | Converts the object $PSBoundParameters into a hashtable, including the keys "Name" and "UserID" and remapping them as specified in $__mapping
27 | #>
28 | [OutputType([hashtable])]
29 | [CmdletBinding()]
30 | param (
31 | [AllowEmptyCollection()]
32 | [string[]]
33 | $Include,
34 |
35 | [Hashtable]
36 | $Mapping = @{ },
37 |
38 | [Parameter(ValueFromPipeline = $true)]
39 | $InputObject
40 | )
41 |
42 | process {
43 | $result = @{ }
44 | if ($InputObject -is [System.Collections.IDictionary]) {
45 | foreach ($pair in $InputObject.GetEnumerator()) {
46 | if ($pair.Key -notin $Include) { continue }
47 | if ($Mapping[$pair.Key]) { $result[$Mapping[$pair.Key]] = $pair.Value }
48 | else { $result[$pair.Key] = $pair.Value }
49 | }
50 | }
51 | else {
52 | foreach ($property in $InputObject.PSObject.Properties) {
53 | if ($property.Name -notin $Include) { continue }
54 | if ($Mapping[$property.Name]) { $result[$Mapping[$property.Name]] = $property.Value }
55 | else { $result[$property.Name] = $property.Value }
56 | }
57 | }
58 | $result
59 | }
60 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/ConvertTo-QueryString.ps1:
--------------------------------------------------------------------------------
1 | function ConvertTo-QueryString {
2 | <#
3 | .SYNOPSIS
4 | Convert conditions in a hashtable to a Query string to append to a webrequest.
5 |
6 | .DESCRIPTION
7 | Convert conditions in a hashtable to a Query string to append to a webrequest.
8 |
9 | .PARAMETER QueryHash
10 | Hashtable of query modifiers - usually filter conditions - to include in a web request.
11 |
12 | .PARAMETER DefaultQuery
13 | Default query parameters defined in the service configuration.
14 | Default query settings are overriden by explicit query parameters.
15 |
16 | .EXAMPLE
17 | PS C:\> ConvertTo-QueryString -QueryHash $Query
18 |
19 | Converts the conditions in the specified hashtable to a Query string to append to a webrequest.
20 | #>
21 | [OutputType([string])]
22 | [CmdletBinding()]
23 | param (
24 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
25 | [Hashtable]
26 | $QueryHash,
27 |
28 | [AllowNull()]
29 | [hashtable]
30 | $DefaultQuery
31 | )
32 |
33 | process {
34 | if ($DefaultQuery) { $query = $DefaultQuery.Clone() }
35 | else { $query = @{} }
36 |
37 | foreach ($key in $QueryHash.Keys) {
38 | $query[$key] = $QueryHash[$key]
39 | }
40 | if ($query.Count -lt 1) { return '' }
41 |
42 |
43 | $elements = foreach ($pair in $query.GetEnumerator()) {
44 | '{0}={1}' -f $pair.Name, ($pair.Value -join ",")
45 | }
46 | '?{0}' -f ($elements -join '&')
47 | }
48 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/ConvertTo-SignedString.ps1:
--------------------------------------------------------------------------------
1 | function ConvertTo-SignedString {
2 | <#
3 | .SYNOPSIS
4 | Signs input string with the offered certificate.
5 |
6 | .DESCRIPTION
7 | Signs input string with the offered certificate.
8 |
9 | .PARAMETER Text
10 | The text to sign.
11 |
12 | .PARAMETER Certificate
13 | The certificate to sign with.
14 | The Private Key must be available.
15 |
16 | .PARAMETER Padding
17 | What RSA Signature padding to use.
18 | Defaults to Pkcs1
19 |
20 | .PARAMETER Algorithm
21 | What algorithm to use for signing.
22 | Defaults to SHA256
23 |
24 | .PARAMETER Encoding
25 | The encoding to use for transforming the text to bytes before signing it.
26 | Defaults to UTF8
27 |
28 | .EXAMPLE
29 | PS C:\> ConvertTo-SignedString -Text $token
30 |
31 | Signs the specified token
32 | #>
33 | [OutputType([string])]
34 | [CmdletBinding()]
35 | param (
36 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
37 | [string[]]
38 | $Text,
39 |
40 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
41 | $Certificate,
42 |
43 | [Security.Cryptography.RSASignaturePadding]
44 | $Padding = [Security.Cryptography.RSASignaturePadding]::Pkcs1,
45 |
46 | [Security.Cryptography.HashAlgorithmName]
47 | $Algorithm = [Security.Cryptography.HashAlgorithmName]::SHA256,
48 |
49 | [System.Text.Encoding]
50 | $Encoding = [System.Text.Encoding]::UTF8
51 | )
52 |
53 | begin {
54 | $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
55 | }
56 | process {
57 | foreach ($entry in $Text) {
58 | $inBytes = $Encoding.GetBytes($entry)
59 | $outBytes = $privateKey.SignData($inBytes, $Algorithm, $Padding)
60 | [convert]::ToBase64String($outBytes)
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/Invoke-TerminatingException.ps1:
--------------------------------------------------------------------------------
1 | function Invoke-TerminatingException {
2 | <#
3 | .SYNOPSIS
4 | Throw a terminating exception in the context of the caller.
5 |
6 | .DESCRIPTION
7 | Throw a terminating exception in the context of the caller.
8 | Masks the actual code location from the end user in how the message will be displayed.
9 |
10 | .PARAMETER Cmdlet
11 | The $PSCmdlet variable of the calling command.
12 |
13 | .PARAMETER Message
14 | The message to show the user.
15 |
16 | .PARAMETER Exception
17 | A nested exception to include in the exception object.
18 |
19 | .PARAMETER Category
20 | The category of the error.
21 |
22 | .PARAMETER ErrorRecord
23 | A full error record that was caught by the caller.
24 | Use this when you want to rethrow an existing error.
25 |
26 | .EXAMPLE
27 | PS C:\> Invoke-TerminatingException -Cmdlet $PSCmdlet -Message 'Unknown calling module'
28 |
29 | Terminates the calling command, citing an unknown caller.
30 | #>
31 | [CmdletBinding()]
32 | Param (
33 | [Parameter(Mandatory = $true)]
34 | $Cmdlet,
35 |
36 | [string]
37 | $Message,
38 |
39 | [System.Exception]
40 | $Exception,
41 |
42 | [System.Management.Automation.ErrorCategory]
43 | $Category = [System.Management.Automation.ErrorCategory]::NotSpecified,
44 |
45 | [System.Management.Automation.ErrorRecord]
46 | $ErrorRecord
47 | )
48 |
49 | process {
50 | if ($ErrorRecord -and -not $Message) {
51 | $Cmdlet.ThrowTerminatingError($ErrorRecord)
52 | }
53 |
54 | $exceptionType = switch ($Category) {
55 | default { [System.Exception] }
56 | 'InvalidArgument' { [System.ArgumentException] }
57 | 'InvalidData' { [System.IO.InvalidDataException] }
58 | 'AuthenticationError' { [System.Security.Authentication.AuthenticationException] }
59 | 'InvalidOperation' { [System.InvalidOperationException] }
60 | }
61 |
62 |
63 | if ($Exception) { $newException = $Exception.GetType()::new($Message, $Exception) }
64 | elseif ($ErrorRecord) {
65 | try { $newException = $ErrorRecord.Exception.GetType()::new($Message, $ErrorRecord.Exception) }
66 | catch { $newException = [System.Exception]::new($Message, $ErrorRecord.Exception) }
67 | }
68 | else { $newException = $exceptionType::new($Message) }
69 | $record = [System.Management.Automation.ErrorRecord]::new($newException, (Get-PSCallStack)[1].FunctionName, $Category, $Target)
70 | $Cmdlet.ThrowTerminatingError($record)
71 | }
72 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/Read-TokenData.ps1:
--------------------------------------------------------------------------------
1 | function Read-TokenData {
2 | <#
3 | .SYNOPSIS
4 | Reads a JWT token and converts it into a custom object showing its properties.
5 |
6 | .DESCRIPTION
7 | Reads a JWT token and converts it into a custom object showing its properties.
8 |
9 | .PARAMETER Token
10 | The JWT Token to parse
11 |
12 | .EXAMPLE
13 | PS C:\> Read-TokenData -Token $authresponse.access_token
14 |
15 | Reads the settings on the returned access token.
16 | #>
17 | [CmdletBinding()]
18 | param (
19 | [Parameter(Mandatory=$true)]
20 | [string]
21 | $Token
22 | )
23 | process {
24 | $tokenPayload = $Token.Split(".")[1].Replace('-', '+').Replace('_', '/')
25 | # Pad with "=" until string length modulus 4 reaches 0
26 | while ($tokenPayload.Length % 4) { $tokenPayload += "=" }
27 | $bytes = [System.Convert]::FromBase64String($tokenPayload)
28 | [System.Text.Encoding]::ASCII.GetString($bytes) | ConvertFrom-Json
29 | }
30 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/Resolve-Certificate.ps1:
--------------------------------------------------------------------------------
1 | function Resolve-Certificate {
2 | <#
3 | .SYNOPSIS
4 | Helper function to resolve certificate input.
5 |
6 | .DESCRIPTION
7 | Helper function to resolve certificate input.
8 | This function expects the full $PSBoundParameters from the calling command and will (in this order) look for these parameter names:
9 |
10 | + Certificate: A full X509Certificate2 object with private key
11 | + CertificateThumbprint: The thumbprint of a certificate to use. Will look first in the user store, then the machine store for it.
12 | + CertificateName: The subject of the certificate to look for. Will look first in the user store, then the machine store for it. Will select the certificate with the longest expiration period.
13 | + CertificatePath: Path to a PFX file to load. Also expects a CertificatePassword parameter to unlock the file.
14 |
15 | .PARAMETER BoundParameters
16 | The $PSBoundParameter variable of the caller to simplify passthrough.
17 | See Description for more details on what the command expects,
18 |
19 | .EXAMPLE
20 | PS C:\> $certificateObject = Resolve-Certificate -BoundParameters $PSBoundParameters
21 |
22 | Resolves the certificate based on the parameters provided to the calling command.
23 | #>
24 | [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
25 | [CmdletBinding()]
26 | param (
27 | $BoundParameters
28 | )
29 |
30 | if ($BoundParameters.Certificate) { return $BoundParameters.Certificate }
31 | if ($BoundParameters.CertificateThumbprint) {
32 | if (Test-Path -Path "cert:\CurrentUser\My\$($BoundParameters.CertificateThumbprint)") {
33 | return Get-Item "cert:\CurrentUser\My\$($BoundParameters.CertificateThumbprint)"
34 | }
35 | if (Test-Path -Path "cert:\LocalMachine\My\$($BoundParameters.CertificateThumbprint)") {
36 | return Get-Item "cert:\LocalMachine\My\$($BoundParameters.CertificateThumbprint)"
37 | }
38 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Unable to find certificate with thumbprint '$($BoundParameters.CertificateThumbprint)'"
39 | }
40 | if ($BoundParameters.CertificateName) {
41 | if ($certificate = @(Get-ChildItem 'Cert:\CurrentUser\My\').Where{ $_.Subject -eq $BoundParameters.CertificateName -and $_.HasPrivateKey }) {
42 | return $certificate | Sort-Object NotAfter -Descending | Select-Object -First 1
43 | }
44 | if ($certificate = @(Get-ChildItem 'Cert:\LocalMachine\My\').Where{ $_.Subject -eq $BoundParameters.CertificateName -and $_.HasPrivateKey }) {
45 | return $certificate | Sort-Object NotAfter -Descending | Select-Object -First 1
46 | }
47 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Unable to find certificate with subject '$($BoundParameters.CertificateName)'"
48 | }
49 | if ($BoundParameters.CertificatePath) {
50 | try { [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($BoundParameters.CertificatePath, $BoundParameters.CertificatePassword) }
51 | catch {
52 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Unable to load certificate from file '$($BoundParameters.CertificatePath)': $_" -ErrorRecord $_
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/Resolve-RequestUri.ps1:
--------------------------------------------------------------------------------
1 | function Resolve-RequestUri {
2 | <#
3 | .SYNOPSIS
4 | Resolves the actual Uri used for a request in Invoke-EntraRequest.
5 |
6 | .DESCRIPTION
7 | Resolves the actual Uri used for a request in Invoke-EntraRequest.
8 | If the path provided is a full url, it will be returned as is.
9 | Otherwise, any present parameters will be resolved in the base service url before merging it with the specified path.
10 |
11 | .PARAMETER TokenObject
12 | The object representing the token used for the request.
13 |
14 | .PARAMETER ServiceObject
15 | The service object (if any) used with the request.
16 | The parameters to be inserted into the query will be read from here.
17 |
18 | .PARAMETER BoundParameters
19 | The parameters provided to Invoke-EntraRequest.
20 |
21 | .EXAMPLE
22 | PS C:\> Resolve-RequestUri -TokenObject $tokenObject -ServiceObject $script:_EntraEndpoints.$($tokenObject.Service) -BoundParameters $PSBoundParameters
23 |
24 | Resolves the uri for the needed request based on token, service and parameters provided
25 | #>
26 | [OutputType([string])]
27 | [CmdletBinding()]
28 | param (
29 | [Parameter(Mandatory = $true)]
30 | $TokenObject,
31 |
32 | [Parameter(Mandatory = $true)]
33 | [AllowNull()]
34 | $ServiceObject,
35 |
36 | [Parameter(Mandatory = $true)]
37 | $BoundParameters
38 | )
39 | process {
40 | if ($BoundParameters.Path -match '^https{0,1}://') {
41 | return $BoundParameters.Path
42 | }
43 |
44 | $serviceUrlBase = $TokenObject.ServiceUrl.Trim()
45 | foreach ($key in $ServiceObject.Parameters.Keys) {
46 | $serviceUrlBase = $serviceUrlBase -replace "%$key%", $BoundParameters.$key
47 | }
48 |
49 | "$($serviceUrlBase.TrimEnd('/'))/$($Path.TrimStart('/'))"
50 | }
51 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/other/Resolve-ScopeName.ps1:
--------------------------------------------------------------------------------
1 | function Resolve-ScopeName {
2 | <#
3 | .SYNOPSIS
4 | Normalizes scope names.
5 |
6 | .DESCRIPTION
7 | Normalizes scope names.
8 | To help manage correct scopes naming with services that don't map directly to their urls.
9 |
10 | .PARAMETER Scopes
11 | The scopes to normalize.
12 |
13 | .PARAMETER Resource
14 | The Resource the scopes are meant for.
15 |
16 | .EXAMPLE
17 | PS C:\> $scopes | Resolve-ScopeName -Resource $Resource
18 |
19 | Resolves all them scopes
20 | #>
21 | [CmdletBinding()]
22 | param (
23 | [Parameter(ValueFromPipeline = $true)]
24 | [string[]]
25 | $Scopes,
26 |
27 | [Parameter(Mandatory = $true)]
28 | [string]
29 | $Resource
30 | )
31 | process {
32 | foreach ($scope in $Scopes) {
33 | foreach ($scope in $Scopes) {
34 | if ($scope -like 'https://*/*') { $scope }
35 | elseif ($scope -like 'api:/') { $scope }
36 | else { "{0}/{1}" -f $Resource, $scope }
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/readme.md:
--------------------------------------------------------------------------------
1 | # Internal > Functions
2 |
3 | Folder for all the functions you want the user to not see.
4 |
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/ux/Assert-ServiceName.ps1:
--------------------------------------------------------------------------------
1 | function Assert-ServiceName {
2 | <#
3 | .SYNOPSIS
4 | Asserts a service name actually exists.
5 |
6 | .DESCRIPTION
7 | Asserts a service name actually exists.
8 | Used in validation scripts to ensure proper service names were provided.
9 |
10 | .PARAMETER Name
11 | The name of the service to verify.
12 |
13 | .PARAMETER IncludeTokens
14 | Also include registered token's services in the assertion.
15 | By default, the assertion will only verify the existence of registered services.
16 |
17 | .EXAMPLE
18 | PS C:\> Assert-ServiceName -Name $_
19 |
20 | Returns $true if the service exists and throws a terminating exception if not so.
21 | #>
22 | [OutputType([bool])]
23 | [CmdletBinding()]
24 | param (
25 | [Parameter(Mandatory = $true)]
26 | [AllowEmptyString()]
27 | [AllowNull()]
28 | [string]
29 | $Name,
30 |
31 | [switch]
32 | $IncludeTokens
33 | )
34 | process {
35 | if ($script:_EntraEndpoints.Keys -contains $Name) { return $true }
36 | if ($IncludeTokens -and $script:_EntraTokens.Keys -contains $Name) { return $true }
37 |
38 | $serviceNames = $script:_EntraEndpoints.Keys -join ', '
39 | Write-Warning "Invalid service name: '$Name'. Legal service names: $serviceNames"
40 | Invoke-TerminatingException -Cmdlet $PSCmdlet -Message "Invalid service name: '$Name'. Legal service names: $serviceNames"
41 | }
42 | }
--------------------------------------------------------------------------------
/EntraAuth/internal/functions/ux/Get-ServiceCompletion.ps1:
--------------------------------------------------------------------------------
1 | function global:Get-ServiceCompletion {
2 | <#
3 | .SYNOPSIS
4 | Returns the values to complete for.service names.
5 |
6 | .DESCRIPTION
7 | Returns the values to complete for.service names.
8 | Use this command in argument completers.
9 |
10 | .PARAMETER ArgumentList
11 | The arguments an argumentcompleter receives.
12 | The third item will be the word to complete.
13 |
14 | .EXAMPLE
15 | PS C:\> Get-ServiceCompletion -ArgumentList $args
16 |
17 | Returns the values to complete for.service names.
18 | #>
19 | [OutputType([System.Management.Automation.CompletionResult])]
20 | [CmdletBinding()]
21 | param (
22 | $ArgumentList
23 | )
24 | process {
25 | $wordToComplete = $ArgumentList[2].Trim("'`"")
26 | foreach ($service in Get-EntraService) {
27 | if ($service.Name -notlike "$($wordToComplete)*") { continue }
28 |
29 | $text = if ($service.Name -notmatch '\s') { $service.Name } else { "'$($service.Name)'" }
30 | [System.Management.Automation.CompletionResult]::new(
31 | $text,
32 | $text,
33 | 'Text',
34 | $service.ServiceUrl
35 | )
36 | }
37 | }
38 | }
39 | $ExecutionContext.InvokeCommand.GetCommand("Get-ServiceCompletion","Function").Visibility = 'Private'
--------------------------------------------------------------------------------
/EntraAuth/internal/scripts/01-variables.ps1:
--------------------------------------------------------------------------------
1 | # Available Tokens
2 | $script:_EntraTokens = @{}
3 |
4 | # Endpoint Configuration for Requests
5 | $script:_EntraEndpoints = @{}
6 |
7 | # The default service to connect to
8 | $script:_DefaultService = 'Graph'
--------------------------------------------------------------------------------
/EntraAuth/internal/scripts/02-Services.ps1:
--------------------------------------------------------------------------------
1 | # Registers the default service configurations
2 | $endpointCfg = @{
3 | Name = 'Endpoint'
4 | ServiceUrl = 'https://api.securitycenter.microsoft.com/api'
5 | Resource = 'https://api.securitycenter.microsoft.com'
6 | DefaultScopes = @()
7 | Header = @{ 'Content-Type' = 'application/json' }
8 | HelpUrl = 'https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/api/apis-intro?view=o365-worldwide'
9 | }
10 | Register-EntraService @endpointCfg
11 |
12 | $securityCfg = @{
13 | Name = 'Security'
14 | ServiceUrl = 'https://api.security.microsoft.com/api'
15 | Resource = 'https://security.microsoft.com/mtp/'
16 | DefaultScopes = @('AdvancedHunting.Read')
17 | Header = @{ 'Content-Type' = 'application/json' }
18 | HelpUrl = 'https://learn.microsoft.com/en-us/microsoft-365/security/defender/api-create-app-web?view=o365-worldwide'
19 | }
20 | Register-EntraService @securityCfg
21 |
22 | $graphCfg = @{
23 | Name = 'Graph'
24 | ServiceUrl = 'https://graph.microsoft.com/v1.0'
25 | Resource = 'https://graph.microsoft.com'
26 | DefaultScopes = @()
27 | HelpUrl = 'https://developer.microsoft.com/en-us/graph/quick-start'
28 | }
29 | Register-EntraService @graphCfg
30 |
31 | $graphBetaCfg = @{
32 | Name = 'GraphBeta'
33 | ServiceUrl = 'https://graph.microsoft.com/beta'
34 | Resource = 'https://graph.microsoft.com'
35 | DefaultScopes = @()
36 | HelpUrl = 'https://developer.microsoft.com/en-us/graph/quick-start'
37 | }
38 | Register-EntraService @graphBetaCfg
39 |
40 | $azureCfg = @{
41 | Name = 'Azure'
42 | ServiceUrl = 'https://management.azure.com'
43 | Resource = 'https://management.core.windows.net/'
44 | DefaultScopes = @()
45 | HelpUrl = 'https://learn.microsoft.com/en-us/rest/api/azure/?view=rest-resources-2022-12-01'
46 | }
47 | Register-EntraService @azureCfg
48 |
49 | $azureKeyVaultCfg = @{
50 | Name = 'AzureKeyVault'
51 | ServiceUrl = 'https://%VAULTNAME%.vault.azure.net'
52 | Resource = 'https://vault.azure.net'
53 | DefaultScopes = @()
54 | HelpUrl = 'https://learn.microsoft.com/en-us/rest/api/keyvault/?view=rest-keyvault-secrets-7.4'
55 | Parameters = @{
56 | VaultName = 'Name of the Key Vault to execute against'
57 | }
58 | Query = @{
59 | 'api-version' = '7.4'
60 | }
61 | }
62 | Register-EntraService @azureKeyVaultCfg
--------------------------------------------------------------------------------
/EntraAuth/internal/scripts/readme.md:
--------------------------------------------------------------------------------
1 | # Internal > Scripts
2 |
3 | Put in all the scripts that should be run once during import
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Friedrich Weinmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/build/vsts-build.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | This script publishes the module to the gallery.
3 | It expects as input an ApiKey authorized to publish the module.
4 |
5 | Insert any build steps you may need to take before publishing it here.
6 | #>
7 | param (
8 | $ApiKey,
9 |
10 | $WorkingDirectory,
11 |
12 | $Repository = 'PSGallery',
13 |
14 | [switch]
15 | $LocalRepo,
16 |
17 | [switch]
18 | $SkipPublish,
19 |
20 | [switch]
21 | $AutoVersion
22 | )
23 |
24 | #region Handle Working Directory Defaults
25 | if (-not $WorkingDirectory)
26 | {
27 | if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS)
28 | {
29 | $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS
30 | }
31 | else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY }
32 | }
33 | if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot }
34 | #endregion Handle Working Directory Defaults
35 |
36 | # Prepare publish folder
37 | Write-Host "Creating and populating publishing directory"
38 | $publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory -Force
39 | Copy-Item -Path "$($WorkingDirectory)\EntraAuth" -Destination $publishDir.FullName -Recurse -Force
40 |
41 | #region Gather text data to compile
42 | $text = @('$script:ModuleRoot = $PSScriptRoot')
43 |
44 | # Gather Classes
45 | Get-ChildItem -Path "$($publishDir.FullName)\EntraAuth\internal\classes\" -Recurse -File -Filter "*.ps1" | ForEach-Object {
46 | $text += [System.IO.File]::ReadAllText($_.FullName)
47 | }
48 |
49 | # Gather commands
50 | Get-ChildItem -Path "$($publishDir.FullName)\EntraAuth\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object {
51 | $text += [System.IO.File]::ReadAllText($_.FullName)
52 | }
53 | Get-ChildItem -Path "$($publishDir.FullName)\EntraAuth\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object {
54 | $text += [System.IO.File]::ReadAllText($_.FullName)
55 | }
56 |
57 | # Gather scripts
58 | Get-ChildItem -Path "$($publishDir.FullName)\EntraAuth\internal\scripts\" -Recurse -File -Filter "*.ps1" | ForEach-Object {
59 | $text += [System.IO.File]::ReadAllText($_.FullName)
60 | }
61 |
62 | # Postprocess Text
63 | $text = $text -replace '#%UNCOMMENT%'
64 |
65 | #region Update the psm1 file & Cleanup
66 | [System.IO.File]::WriteAllText("$($publishDir.FullName)\EntraAuth\EntraAuth.psm1", ($text -join "`n`n"), [System.Text.Encoding]::UTF8)
67 | Remove-Item -Path "$($publishDir.FullName)\EntraAuth\internal" -Recurse -Force
68 | Remove-Item -Path "$($publishDir.FullName)\EntraAuth\functions" -Recurse -Force
69 | #endregion Update the psm1 file & Cleanup
70 |
71 | #region Updating the Module Version
72 | if ($AutoVersion)
73 | {
74 | Write-Host "Updating module version numbers."
75 | try { [version]$remoteVersion = (Find-Module 'EntraAuth' -Repository $Repository -ErrorAction Stop).Version }
76 | catch
77 | {
78 | throw "Failed to access $($Repository) : $_"
79 | }
80 | if (-not $remoteVersion)
81 | {
82 | throw "Couldn't find EntraAuth on repository $($Repository) : $_"
83 | }
84 | $newBuildNumber = $remoteVersion.Build + 1
85 | [version]$localVersion = (Import-PowerShellDataFile -Path "$($publishDir.FullName)\EntraAuth\EntraAuth.psd1").ModuleVersion
86 | Update-ModuleManifest -Path "$($publishDir.FullName)\EntraAuth\EntraAuth.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)"
87 | }
88 | #endregion Updating the Module Version
89 |
90 | #region Publish
91 | if ($SkipPublish) { return }
92 | if ($LocalRepo)
93 | {
94 | # Dependencies must go first
95 | Write-Host "Creating Nuget Package for module: PSFramework"
96 | New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath .
97 | Write-Host "Creating Nuget Package for module: EntraAuth"
98 | New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\EntraAuth" -PackagePath .
99 | }
100 | else
101 | {
102 | # Publish to Gallery
103 | Write-Host "Publishing the EntraAuth module to $($Repository)"
104 | Publish-Module -Path "$($publishDir.FullName)\EntraAuth" -NuGetApiKey $ApiKey -Force -Repository $Repository
105 | }
106 | #endregion Publish
--------------------------------------------------------------------------------
/build/vsts-prerequisites.ps1:
--------------------------------------------------------------------------------
1 | param (
2 | [string]
3 | $Repository = 'PSGallery'
4 | )
5 |
6 | $modules = @("Pester", "PSScriptAnalyzer")
7 |
8 | # Automatically add missing dependencies
9 | $data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\EntraAuth\EntraAuth.psd1"
10 | foreach ($dependency in $data.RequiredModules) {
11 | if ($dependency -is [string]) {
12 | if ($modules -contains $dependency) { continue }
13 | $modules += $dependency
14 | }
15 | else {
16 | if ($modules -contains $dependency.ModuleName) { continue }
17 | $modules += $dependency.ModuleName
18 | }
19 | }
20 |
21 | foreach ($module in $modules) {
22 | Write-Host "Installing $module" -ForegroundColor Cyan
23 | Install-Module $module -Force -SkipPublisherCheck -Repository $Repository
24 | Import-Module $module -Force -PassThru
25 | }
--------------------------------------------------------------------------------
/build/vsts-validate.ps1:
--------------------------------------------------------------------------------
1 | # Run internal pester tests
2 | & "$PSScriptRoot\..\tests\pester.ps1"
--------------------------------------------------------------------------------
/docs/api-permissions.md:
--------------------------------------------------------------------------------
1 | # API Permissions and you
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | |[Previous: Application vs. Delegate Authentication](application-vs-delegate.md)|[Next: Managing an Application & Troubleshooting logins](managing-applications.md)|
6 |
7 | So, understanding the difference between Application and Delegate authentication, you now want to get yourself some juicy permissions, right?
8 |
9 | For that we first need to navigate to the "API permissions" section in our Application's page:
10 |
11 | 
12 |
13 | As we can see, this new application only has a single permission granted by default - "User.Read", the Delegate right for a user to retrieve information about themselves.
14 |
15 | > Note on terminology: The individual API permissions are frequently called "Scopes".
16 |
17 | To request more permissions, we need to first select the "Add a permission" button above the table.
18 | This opens a new panel, where we first need to select, from _which_ service we want permissions:
19 |
20 | 
21 |
22 | If your service is listed here:
23 | Great.
24 | We will cover how to find unlisted services later.
25 |
26 | Let us assume that for the purpose of our project we need the Microsoft Graph service, as we later want to modify the groups we have access to:
27 |
28 | 
29 |
30 | Again we are faced with the choice between Delegated and Application permissions.
31 | As we are going for an interactive tool, we pick the Delegated permissions:
32 |
33 | 
34 |
35 | We can now search all the permissions the service - in this case Microsoft Graph - offers in Delegated mode.
36 | Using the search panel we can make it easier to find what is needed, then select the permissions' checkboxes and select "Add permissions":
37 |
38 | 
39 |
40 | With that selected, we return to the main table of "API permissions":
41 |
42 | 
43 |
44 | As we can see, the new permission is listed, but there is a new warning:
45 | ReadWrite for all groups appear a bit permissive and we now need the consent from a Global Administrator for the permission to apply.
46 |
47 | Fortunately, in my test tenant that is not a problem - the user already is Global Administrator.
48 | If you are not GA - most organizations try to minimize the number of accounts with that right - you will now have to ask one of them to perform the next steps.
49 |
50 | Either way, with the "Grant admin consent for %tenantname%" a GA can now grant the consent and make the permissions apply:
51 |
52 | 
53 |
54 | And with that, the consent has now been granted:
55 |
56 | 
57 |
58 | ## Service Not Found
59 |
60 | So far, so good.
61 | Some of the well-known APIs / Services are easy to find when trying to add API permissions.
62 | But ... what if our service is not?
63 | Whether it is some Defender API or maybe our own function app, not all services will be found on the main grid panel of services.
64 |
65 | Still, it can be found:
66 |
67 | 
68 |
69 | The tab "APIs my organization uses" hides all the remaining services in a tenant:
70 |
71 | 
72 |
73 | Using the search box, we can now search for any service in our tenant.
74 | Note, the search is not always convenient and a name that should match does not return anything.
75 |
76 | + If you have the Application ID of the service, searching by that will always be precise.
77 | + If you have some online guide with screenshots, the header above the individual permissions (in our last screenshot: "Microsoft Graph (2)") shows the name to search for.
78 |
79 | |[Previous: Application vs. Delegate Authentication](application-vs-delegate.md)|[Next: Managing an Application & Troubleshooting logins](managing-applications.md)|
80 |
--------------------------------------------------------------------------------
/docs/application-vs-delegate.md:
--------------------------------------------------------------------------------
1 | # Application vs. Delegate Authentication
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | |[Previous: Creating an Application](creating-applications.md)|[Next: API Permissions and you](api-permissions.md)|
6 |
7 | ## The Different Kinds of Authenticating
8 |
9 | So, you have created an application and now want to connect, right?
10 |
11 | Well, you are almost there, but first you need to declare what rights you want to use after connecting.
12 | Which requires us to talk about the two different categories of authentication:
13 |
14 | + Application
15 | + Delegate
16 |
17 | When we get to assigning permissions, this difference is critical, as the two categories have completely different sets of rights.
18 |
19 | So ... what's the difference?
20 |
21 | > Application
22 |
23 | Application authentication flows assume, that there is no human being involved in the process.
24 | This is the classic "Service Account" kind of authentication you might already know from task schedulers or cron jobs doing their thing automatically as a specified account or as the "System".
25 |
26 | Whether that reference means something to you or not, this is basic unattended authentication, so no MFA prompt, no human interaction possible.
27 |
28 | This means we usually connect using a certificate (preferred) or a Client Secret.
29 |
30 | Using this mechanism, our code will act under the application itself - it basically becomes our service account - and all permissions assigned are _right grants_.
31 | In other words, if I assign the api permission "Group.ReadWrite.All" this means we now have the permission to edit _every single group in the tenant!_
32 |
33 | This usually means an Admin must provide consent to those rights (more on that in the next chapter).
34 |
35 | > Delegate
36 |
37 | In Delegate mode, the application acts in the name of the user connecting.
38 | So if we use this to connect, we will log in as ourselves, the human user and the code will act in our own name - the application is now merely the configuration describing _how_ we connect.
39 |
40 | This logon expects there to be a human in front of the computer, ready to interact - for example servicing MFA prompts.
41 | Most flows require a browser, whether on the same machine that you are connecting from or another.
42 |
43 | In opposite to Application mode, api permissions in this mode are not _right grants_.
44 | They are a mask, a subset, of what your user account is already allowed to do.
45 | The api permission "Group.ReadWrite.All" allows you to edit all the groups you already have permission to modify.
46 | The api permission "Send.Mail" allows you to send emails as yourself only.
47 |
48 | Consequently, not all those api permissions require admin consent - for some the user may consent for themselves.
49 |
50 | ## The right tool for the job
51 |
52 | So, what authentication should I use when?
53 | The first decision is Application vs. Delegate - is this going to run unattended?
54 | If so, then Application it is.
55 | Do we have a human being in front of the console, ready to interact?
56 | Then consider using delegate mode.
57 |
58 | In some cases, even though a human is available, due to the rights situation you still need to use Application mode, but try to minimize this where possible - in most cases, Application mode is more rights than actually needed.
59 |
60 | There are different options within a given authentication mode however:
61 |
62 | > Application
63 |
64 | The two options are ...
65 |
66 | + Certificate
67 | + Client Secret (API Key)
68 |
69 | Certificate is the technically better option and should be the default choice, the Client Secret only in rare cases where the certificate logistics are too challenging.
70 |
71 | With Certificates, we sign the authentication request, while with Client Secret we send our secret over the network.
72 | Hence Certificate is the more secure choice.
73 | The Certificate can be a self-signed certificate.
74 |
75 | > Delegate
76 |
77 | The two main options are ...
78 |
79 | + Browser Logon (Authorization Code Flow)
80 | + Device Code Logon
81 |
82 | In both cases you log in via a browser window.
83 | The main difference between the two is that Browser Logon is more comfortable to use and must happen on the same computer where our code runs.
84 | Device Code Logon means your code sends the request, but the actual logon in the Browser can happen on any computer.
85 |
86 | If the computer executing the code has a user interface and a browser, then the former option (Browser Logon) is always the preferred option.
87 | Device Code - in which you execute the authentication independent of the connecting computer - requires you to loosen security requirements and is more vulnerable to token theft.
88 |
89 | On the other hand, if the computer executing the code has no user interface - just a console - Device Code is still the best option available.
90 |
91 | |[Previous: Creating an Application](creating-applications.md)|[Next: API Permissions and you](api-permissions.md)|
92 |
--------------------------------------------------------------------------------
/docs/authenticate-browser.md:
--------------------------------------------------------------------------------
1 | # Delegated: Using the Browser directly
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | ## Configure
6 |
7 | Setting up the default login via Browser is fortunately not too complex:
8 | In our App Registration configuration page, we select the "Authentication" tab:
9 |
10 | 
11 |
12 | Select "Add a platform" from this page.
13 |
14 | 
15 |
16 | Choose "Mobile and desktop applications".
17 | In the follow up menu we can now configure what adjustments we need to add:
18 |
19 | 
20 |
21 | All we need to do now, is to add "http://localhost" and select "configure":
22 |
23 | 
24 |
25 | And with that we are done!
26 |
27 | 
28 |
29 | ## Authentication & Executing Queries
30 |
31 | Using the EntraAuth PowerShell module, we can now connect using our Application, authenticating in our Browser window:
32 |
33 | ```powershell
34 | $clientID = '63a71861-498b-46ae-0000-6b5c142010e1'
35 | $tenantID = 'a948c2b3-8eb2-498a-0000-c32aeeaa0f90'
36 |
37 | Connect-EntraService -ClientID $clientID -TenantID $tenantID
38 | ```
39 |
40 | Once connected, we are now ready to use the connection to query all groups in our tenant:
41 |
42 | ```powershell
43 | Invoke-EntraRequest -Path groups
44 | ```
45 |
46 | > This example assumes, that we followed the guide on setting up App Registrations and granted the `Group.ReadWrite.All` API permission for Microsoft Graph
47 |
--------------------------------------------------------------------------------
/docs/authenticate-certificate.md:
--------------------------------------------------------------------------------
1 | # Application: Certificate
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | ## Configure
6 |
7 | The Certificate-based authentication flow requires ... well, a certificate!
8 | The important part here:
9 | Whether it is a certificate from a trusted root CA or not, matters not at all.
10 | Authentication will work either way, self-signed or formal certificate, so pick your poison.
11 |
12 | That said, for this demo we will use a self-signed certificate, as that is quite easy to do and globally applicable.
13 | To generate a certificate, all we need is a PowerShell console:
14 |
15 | ```powershell
16 | $cert = New-SelfSignedCertificate -Subject 'CN=EntraAuth demo certificate' -CertStoreLocation 'Cert:\CurrentUser\My'
17 | ```
18 |
19 | To store the certificate in the computer certificate store, rather than the user certificate store, replace `'Cert:\CurrentUser\My'` with `'Cert:\LocalMachine\My'` and execute this in an elevated ("Run as administrator") console.
20 |
21 | > This sample assumes you are using a Windows operating system.
22 | > How to obtain, store and retrieve a certificate in another OS is beyond the scope of this guide, but otherwise works just the same, once we have the certificate object.
23 |
24 | Now, in order for our Application to use this certificate for authentication, we must somehow make this certificate available to it.
25 | The easiest way to do so, now that we have the `$cert` variable is to export the public certificate into a file and upload it via the portal:
26 |
27 | ```powershell
28 | $bytes = $cert.GetRawCertData()
29 | [System.IO.File]::WriteAllBytes("C:\temp\cert.cer", $bytes)
30 | ```
31 |
32 | Now to the portal:
33 |
34 | 
35 |
36 | Go the to "Certificates & Secrets" section of the App Registration menu, select the "Certificates" tab and then the button "Upload Certificate".
37 | This leads us to the menu to upload our new certificate:
38 |
39 | 
40 |
41 | Select the blue button beside the "Select File" panel and pick the certificate file we just wrote to file in the file picker that opens.
42 |
43 | 
44 |
45 | And that's it, we are now ready to authenticate:
46 |
47 | 
48 |
49 | ## Authentication & Executing Queries
50 |
51 | > This example assumes that you have granted the "Group.Read.All" Graph API permission to the application.
52 | > The example on configuring API Permissions in this guide uses Delegated permissions instead, _which do not apply to Application Authentication flows!_
53 |
54 | Using the EntraAuth PowerShell module, we can now connect using our Application, authenticating with our certificate.
55 |
56 | ```powershell
57 | $clientID = '63a71861-498b-46ae-0000-6b5c142010e1'
58 | $tenantID = 'a948c2b3-8eb2-498a-0000-c32aeeaa0f90'
59 |
60 | # Connect using the certificate object we still have in our variable
61 | Connect-EntraService -ClientID $clientID -TenantID $tenantID -Certificate $cert
62 |
63 | # Connect via Certificate thumbprint, cert selected from cert store
64 | Connect-EntraService -ClientID $clientID -TenantID $tenantID -CertificateThumbprint 690667761F6E285B2A6AEFF098B886263433FB54
65 |
66 | # Connect via Certificate Subject, cert selected from cert store
67 | Connect-EntraService -ClientID $clientID -TenantID $tenantID -CertificateName 'CN=EntraAuth demo certificate'
68 | ```
69 |
70 | Once connected, we are now ready to use the connection to query all groups in our tenant:
71 |
72 | ```powershell
73 | Invoke-EntraRequest -Path groups
74 | ```
75 |
76 | > Certificates & Service Accounts
77 |
78 | One of the common problems when providing a certificate for a script running under a service account is, that the service account obviously has no access to our user's certificate store.
79 | So, to simplify this, we store it in the LocalMachine store, which the Service Account can access ... apparently.
80 |
81 | Still authentication keeps failing.
82 |
83 | This is usually caused by the service account not having access to the private key of the certificate.
84 | As a local admin, you can modify who can access the private key in an elevated certificate console.
85 |
--------------------------------------------------------------------------------
/docs/authenticate-clientsecret.md:
--------------------------------------------------------------------------------
1 | # Application: Client Secret
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | ## Configure
6 |
7 | To configure the Client Secret authentication, not much is needed.
8 | In the "Certificates & secrets" section of the App Registration, we already start on the Client secrets section by default.
9 |
10 | 
11 |
12 | Select "New client secret":
13 |
14 | 
15 |
16 | Adding a description is optional but recommended.
17 | The expiration option defines, how long a secret will remain valid, with a limit of two years.
18 | Configure as needed and select "Add".
19 |
20 | 
21 |
22 | We can now see the secret in the table.
23 | In the "Value" column we can now access the secret value (use the "Copy to Clipboard" button).
24 |
25 | > This is THE ONLY TIME we can access the secret.
26 | > If we do not copy it now and store it somewhere else (such as a password safe), it is gone and we can start again!
27 |
28 | That's it, we are now ready to roll.
29 |
30 | ## Authentication & Executing Queries
31 |
32 | > This example assumes that you have granted the "Group.Read.All" Graph API permission to the application.
33 | > The example on configuring API Permissions in this guide uses Delegated permissions instead, _which do not apply to Application Authentication flows!_
34 |
35 | Using the EntraAuth PowerShell module, we can now connect using our Application, authenticating with our client secret.
36 | This example assumes the secret value is in our clipboard.
37 |
38 | ```powershell
39 | $clientID = '63a71861-498b-46ae-0000-6b5c142010e1'
40 | $tenantID = 'a948c2b3-8eb2-498a-0000-c32aeeaa0f90'
41 | $secret = Get-ClipBoard | ConvertTo-SecureString -AsPlainText -Force
42 |
43 | Connect-EntraService -ClientID $clientID -TenantID $tenantID -ClientSecret $secret
44 | ```
45 |
46 | Once connected, we are now ready to use the connection to query all groups in our tenant:
47 |
48 | ```powershell
49 | Invoke-EntraRequest -Path groups
50 | ```
51 |
--------------------------------------------------------------------------------
/docs/authenticate-devicecode.md:
--------------------------------------------------------------------------------
1 | # Delegated: DeviceCode
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | > Note: It is generally recommended to use [Interactive Browser Authentication / Authorization Code](authenticate-browser.md)
6 |
7 | ## Configure
8 |
9 | Setting up the default login via DeviceCode is fortunately not too complex:
10 | In our App Registration configuration page, we select the "Authentication" tab:
11 |
12 | 
13 |
14 | Select "Add a platform" from this page.
15 |
16 | 
17 |
18 | Choose "Web".
19 | In the follow up menu we now need to configure a Redirect Uri:
20 |
21 | 
22 |
23 | All we need to do now, is to add "http://localhost" and select "configure".
24 |
25 | After adding the platform, we need to scroll to the bottom and enable "Allow public client flows":
26 |
27 | 
28 |
29 | Once enabled and saved, we are good to go.
30 |
31 | ## Authentication & Executing Queries
32 |
33 | Using the EntraAuth PowerShell module, we can now connect using our Application, authenticating in our Browser window:
34 |
35 | ```powershell
36 | $clientID = '63a71861-498b-46ae-0000-6b5c142010e1'
37 | $tenantID = 'a948c2b3-8eb2-498a-0000-c32aeeaa0f90'
38 |
39 | Connect-EntraService -ClientID $clientID -TenantID $tenantID -DeviceCode
40 | ```
41 |
42 | ```text
43 | To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code CCXBG2F8C to authenticate.
44 | ```
45 |
46 | Follow the guidance in the response and finish authentication.
47 | Once connected, we are now ready to use the connection to query all groups in our tenant:
48 |
49 | ```powershell
50 | Invoke-EntraRequest -Path groups
51 | ```
52 |
53 | > This example assumes, that we followed the guide on setting up App Registrations and granted the `Group.ReadWrite.All` API permission for Microsoft Graph
54 |
--------------------------------------------------------------------------------
/docs/building-on-entraauth.md:
--------------------------------------------------------------------------------
1 | # Building on EntraAuth
2 |
3 | So, let's assume you have been converted to this project and now want to migrate your module to use it.
4 | Or build a new module based on top of it.
5 |
6 | In either case, you will now be faced with some design considerations, but there is one major one that overshadows it all:
7 |
8 | > How do I deal with EntraAuth services?
9 |
10 | What is a fairly simple thing in a standalone script can suddenly become a lot more frustrating when a few issues start popping up...
11 |
12 | ## The problems we face
13 |
14 | It all starts with how we decide to deal with the services we use.
15 | There are a few common decision option ... and they all have their consequences:
16 |
17 | > 1.: Just use the defaults
18 |
19 | _Your module wants to do Graph requests? Just specify the "Graph" services, it works._
20 |
21 | This is basically what we do in our scripts, so why not in a module?
22 |
23 | Any other module also doing this might lead to service conflicts.
24 | Imagine a script calling three separate modules. Unless the modules are called after each other only and we reconnect inbetween, this means that we need one token that meets the scope prerequisite of them all.
25 | This can be organizationally difficult.
26 |
27 | Also, if you do not control all of those modules, it becomes simple for one of them prompt the user to reconnect to another application (or even tenant!), without the user realizing this impact.
28 | This problem becomes even more troublesome as adoption of EntraAuth increases and modules take dependencies on other modules that also use EntraAuth.
29 |
30 | > 2.: Define your own service
31 |
32 | _Conflicts are bad, so let us define our own dedicated services._
33 |
34 | Registering your own instances of services - even for services already part of the baseline - is a solid way to avoid most conflicts.
35 |
36 | The main issue with this approach is, that we now force the user to log into each service separately, which can be a bit of a bother.
37 | There are ways to "clone" delegate tokens into another service, but few users would be aware of that ... and it still needs managing.
38 |
39 | Conflict-wise, this is a fairly clean solution, but we still only have a single set of services we can use.
40 | This can be a problem if our module uses a wide range of APIs, depending on which command we call, which may use separate API permissions/scopes.
41 |
42 | A script may only want to use a limited application with fewer scopes for just what is needed, while another module using it may need a different set of scopes.
43 | Since the module can only hold a single service state / a single token per service, these will now be in conflict and force us to once again configure one large application with all scopes combined.
44 |
45 | Also, this means we do not use the default services, which is going to be unintuitive for newer users, who will not understand the entire service concept of EntraAuth (or even be aware of EntraAuth to begin with).
46 |
47 | > 3.: Define a module-wide default Service that can be changed
48 |
49 | _Why hardcode when we can give the user may chose?_
50 |
51 | We can define a module-wide variable with the service(s) we plan to use.
52 | With that, we can allow script authors or other modules to change that and either merge or split service use as needed.
53 |
54 | This would allow a script to define, which modules should use the same service and which should go separate ways.
55 | That way, we eliminate redundant logon steps, but each module can still only have a single service configured at any given time.
56 |
57 | This would not be too bad of a problem right now, but as modules depend on other modules that use EntraAuth, this might lead to conflicts.
58 |
59 | > 4.: Expose the service to use on commands of the module
60 |
61 | If we expose the choice of service on our module's functions, each caller can pick their own service to use.
62 | Our own module becomes stateless when it comes to services used.
63 |
64 | This has the great advantage of conclusively eliminating all service/token conflicts.
65 | It also is fairly well documentable, using PowerShell command-help.
66 |
67 | Which leaves one last issue - forcing us to always specify the service is a lot more verbose and raises the minimum barrier for use.
68 |
69 | ## A hybrid approach: Module-wide default & Parameters as override
70 |
71 | The probably most viable solution to these concerns is to define a module-wide default, then in our functions offer a way to override this default.
72 | Script authors can then use the `$PSDefaultParameterValue` system variable to declutter their code.
73 |
74 | Of course, that brings some overhead in implementing this in your module, which is where EntraAuth and the ServiceSelector come in:
75 |
76 | ### Example Implementation
77 |
78 | We are building the module `ContosoTools`.
79 | In it we need to interact with the Graph API (Default service: `Graph`) and the Defender for Endpoint API (Default service: `Endpoint`)
80 |
81 | > File 1: variables.ps1
82 |
83 | This is some random file we load during our module's import.
84 | After the import is over, that's it, the file will not be run again.
85 |
86 | ```powershell
87 | $script:_services = @{
88 | Graph = 'Graph'
89 | MDE = 'Endpoint'
90 | }
91 |
92 | $script:_serviceSelector = New-EntraServiceSelector -DefaultServices $script:_services
93 | ```
94 |
95 | > File 2: Set-CTServiceConnection.ps1
96 |
97 | The default services to use are now defined during import - the values of the hashtable we just defined (Here: `Graph` and `Endpoint`).
98 | Now we need a convenient way for a human user to change those defaults:
99 |
100 | ```powershell
101 | function Set-CTServiceConnection {
102 | [CmdletBinding()]
103 | param (
104 | [ArgumentCompleter({ (Get-EntraService).Name })]
105 | [string]
106 | $Graph,
107 |
108 | [ArgumentCompleter({ (Get-EntraService).Name })]
109 | [string]
110 | $Mde
111 | )
112 |
113 | if ($Graph) {
114 | $script:_services.Graph = $Graph
115 | }
116 | if ($Mde) {
117 | $script:_services.MDE = $Mde
118 | }
119 | }
120 | ```
121 |
122 | This allows a user to cleanly change the default services to use ... but it does not solve the conflict situation between other _modules_ trying to use our `ContosoTools`.
123 | We also need to actually use these services yet.
124 | Moving on to the actual implementation within our commands:
125 |
126 | > File 3: Get-CTUser.ps1
127 |
128 | This is just one of the many functions our module exposes to the public.
129 | In its simple form, it will return all users in the tenant (we probably want to add filtering in V2, but let's not overcomplicate this example).
130 |
131 | ```powershell
132 | function Get-CTUser {
133 | [CmdletBinding()]
134 | param (
135 | [hashtable]
136 | $ServiceMap = @{}
137 | )
138 |
139 | begin {
140 | $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
141 | Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Graph
142 | }
143 | process {
144 | Invoke-EntraRequest -Service $services.Graph -Path users
145 | }
146 | }
147 | ```
148 |
149 | Let's go through this a bit:
150 |
151 | ```powershell
152 | $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
153 | ```
154 |
155 | This is the line where the real magic happens:
156 |
157 | + It will pick up the default services we defined in File 1, potentially modified by the user through the command in File 2
158 | + Then it will merge that with any explicitly bound services from `$ServiceMap`
159 |
160 | Thus a user can define their default now, without affecting other module's ability to pick their own services (and without those modules interfering with the user's choice).
161 |
162 | That's it, our module is now using EntraAuth with flexible services.
163 | Let's take a look at how this would then be used ...
164 |
165 | ### Example Use
166 |
167 | > Interactive in the console
168 |
169 | ```powershell
170 | Get-CTUser
171 | ```
172 |
173 | ```text
174 | Get-CTUser: Not connected yet! Use Connect-EntraService to establish a connection to 'Graph' first.
175 | ```
176 |
177 | ```powershell
178 | Connect-EntraService -ClientID Graph
179 | Get-CTUser
180 | ```
181 |
182 | ```text
183 | < lots of results >
184 | ```
185 |
186 | > Script 1: Simple Script
187 |
188 | A simple script that only uses our module as a dependency.
189 |
190 | ```powershell
191 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -CertificateName 'CN=MyScript'
192 |
193 | foreach ($user in Get-CTUser) {
194 | # Do Something
195 | }
196 | ```
197 |
198 | > Script 2: Script that wants to use the Graph Beta
199 |
200 | Sometimes we just get more out of the Beta API for Microsoft Graph and the script wants to do some calls of its own.
201 | So, deciding to keep things simple, the script author wants _all_ Graph calls to go to the beta API:
202 |
203 | ```powershell
204 | Connect-EntraService Service 'GraphBeta' -ClientID $ClientID -TenantID $TenantID -CertificateName 'CN=MyScript2'
205 | $PSDefaultParameterValues['*-CT*:ServiceMap'] = @{ Graph = 'GraphBeta' }
206 |
207 | foreach ($user in Get-CTUser) {
208 | # Do Something
209 | }
210 | ```
211 |
212 | > Script 3: Multiple modules that use EntraAuth
213 |
214 | One of our experts is writing a complex script that needs to use not just our `ContosoTools`, but also the `NorthwindUtilities` and `FabrikamRobotics` modules.
215 | These have slightly different requirements:
216 |
217 | + ContosoTools is supposed to use the default Graph v1 api, but its requests to Defender for Endpoint must go through our homebrew proxy for the Defender API.
218 | + NorthwindUtilities requires the GraphBeta and will not interact with Defender for Endpoint at all
219 | + FabrikamRobotics needs both the GraphBeta _and_ Defender for Endpoint through our proxy.
220 |
221 | All three modules are implemented based on EntraAuth and use the setup presented above.
222 | They also use consistent command prefixes (`CT`, `NW` and `FR` respectively).
223 |
224 | ```powershell
225 | $param = @{
226 | Name = 'MDEProxy'
227 | ServiceUrl = 'https://mdeproxy.contoso.com/api'
228 | Resource = 'https://mdeproxy.contoso.com'
229 | Header = @{ 'Content-Type' = 'application/json' }
230 | }
231 | Register-EntraService @param
232 |
233 | Connect-EntraService Service 'GraphBeta', 'Graph', 'MDEProxy' -ClientID $ClientID -TenantID $TenantID -CertificateName 'CN=MyScript3'
234 | $PSDefaultParameterValues['*-CT*:ServiceMap'] = @{ MDE = 'MDEProxy' }
235 | $PSDefaultParameterValues['*-NW*:ServiceMap'] = @{ Graph = 'GraphBeta' }
236 | $PSDefaultParameterValues['*-FR*:ServiceMap'] = @{ Graph = 'GraphBeta'; MDE = 'MDEProxy' }
237 |
238 | foreach ($user in Get-CTUser) {
239 | $authDetails = Get-NWUserDetails -Id $user.id
240 | if ($authDetails.Healthy) { continue }
241 |
242 | Send-FRAuthenticationReport -Data $authDetails -Recipiemnt $user.Manager
243 | }
244 | ```
245 |
--------------------------------------------------------------------------------
/docs/creating-applications.md:
--------------------------------------------------------------------------------
1 | # Creating an Application
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | |[Back to Overview](overview.md)|[Next: Application vs. Delegate Authentication](application-vs-delegate.md)|
6 |
7 | ## What the hell?
8 |
9 | At a fundamental level, an Application - or "App Registration" as it is frequently called - is a construct in Entra that describes how you connect.
10 | On an application, you configure ...
11 |
12 | + ... just how you can connect
13 | + ... who is allowed to connect
14 | + ... what rights you want to use after connecting
15 |
16 | It also allows you to troubleshoot connection issues.
17 |
18 | > Note: This is an oversimplification, but good enough for the purpose of this guide.
19 |
20 | ## Get Creating
21 |
22 | In the [Azure Portal](https://portal.azure.com) we first must select the "App registrations" option:
23 |
24 | 
25 |
26 | If it is not on the list of already listed services (which usually shows the last services opened), you can use the search bar at the top to search for it instead.
27 |
28 | Once there, we create a new App Registration:
29 |
30 | 
31 |
32 | And finally we configure our new application's basic settings:
33 |
34 | 
35 |
36 | We need a name for this Application - and this is one of the things this guide can't really help you with:
37 | Many organizations have their own naming schemes, and you either have to figure out your organization's scheme on your own or ask a coworker in the know.
38 |
39 | Or you pick something that seems reasonable and live with the consequences - it is usually quite simple to recreate all the things we configure in an App Registration, so it is not like you will have to live for ages with a bad call here.
40 |
41 | For supported account types, leave it as it is as "Single tenant" - the other options are usually only relevant to software vendors or developers who are far more experienced than anybody reading this guide likely is and know what they do with that.
42 |
43 | Select "Register" once you are happy with your application's name.
44 |
45 | And that is it, you just created your first App Registration:
46 |
47 | 
48 |
49 | Note the highlighted "Application (client) ID" and "Directory (tenant) ID":
50 | Those will be needed when we try to connect later on.
51 |
52 | |[Back to Overview](overview.md)|[Next: Application vs. Delegate Authentication](application-vs-delegate.md)|
53 |
--------------------------------------------------------------------------------
/docs/managing-applications.md:
--------------------------------------------------------------------------------
1 | # Managing an Application & Troubleshooting logins
2 |
3 | > [Back to Overview](overview.md)
4 |
5 | |[Previous: API Permissions and you](api-permissions.md)|[Back to Overview](overview.md)|
6 |
7 | Our application is now prepared and ready to rock!
8 | Well ... almost.
9 |
10 | We have not yet configured authentication itself.
11 | If we did, then _every_ user in the tenant could use our Application.
12 |
13 | This is usually something we want to avoid, so lot us restrict it to members of a specific group!
14 |
15 | In the overview section of our App Registration, there is a link to the manageability panel ("Enterprise Application") of our application.
16 | Not going into the details here, but it is where we configure who can use it.
17 |
18 | To do so, first select the link behind the "Managed application" section:
19 |
20 | 
21 |
22 | Once in the new menu, switch to Properties:
23 |
24 | 
25 |
26 | Enable "Assignment required?" and remember to save:
27 |
28 | 
29 |
30 | Alright, that done, now nobody (other than yourself) can use the Application.
31 | To add more people to it, switch to "Users and groups":
32 |
33 | 
34 |
35 | To add a group, select "Add user/group" at the top, opening the assignment menu:
36 |
37 | 
38 |
39 | To add a group, select the "None Selected" link.
40 |
41 | > The "Select a role" option is greyed out. More complex applications could define their own permissions, allowing us to assign different permissions to different groups.
42 | > A topic for another day.
43 |
44 | 
45 |
46 | The new panel allows us to search for any user or group in the tenant.
47 | Set the checkbox for all entries we want to add and confirm with "Select" at the bottom.
48 |
49 | 
50 |
51 | Once done, select "Assign" to complete the assignment:
52 |
53 | 
54 |
55 | And that is it!
56 | We have now limited just who is allowed to use our application and the rights associated.
57 |
58 | ## Troubleshooting SignIns
59 |
60 | Not really critical for configuring our Application, if we later want to troubleshoot logon problems or check usage, there is another useful section in this menu:
61 |
62 | Sign-in logs:
63 |
64 | 
65 |
66 | Nothing to see here yet, but useful if you later fail to login and can't figure out why.
67 |
68 | |[Previous: API Permissions and you](api-permissions.md)|[Back to Overview](overview.md)|
69 |
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 | # Authentication Overview
2 |
3 | Welcome to the beginner's guide to Entra Authentication.
4 | Here we will try to get you up to speed on what you need to know to start executing PowerShell against services protected by Microsoft Entra authentication, such as the Graph API.
5 |
6 | ## Getting started
7 |
8 | Fundamentally, to connect to an API, you need to perform three steps:
9 |
10 | + Create an "Application"
11 | + Assign the permissions you want to use
12 | + Configure the Authentication process you want to use
13 |
14 | ## Assumptions
15 |
16 | This guide assumes you want to use PowerShell to connect to an API via Entra Authentication.
17 | The code examples assume further that you are using the Module [EntraAuth](https://github.com/FriedrichWeinmann/EntraAuth) for this purpose.
18 |
19 | The concepts and guidance also applies to other coding languages - whether you want to connect via PowerShell, Python, Java or C#, the different authentication options and the setup on the Entra side remain the same.
20 | Obviously, the code examples will not translate as well.
21 |
22 | If you are planning to build a web application or a desktop app for end users however, this guide is probably not ideal.
23 |
24 | ## Guide
25 |
26 | > General Topics
27 |
28 | + [Creating an Application](creating-applications.md)
29 | + [Application vs. Delegate Authentication](application-vs-delegate.md)
30 | + [API Permissions and you](api-permissions.md)
31 | + [Managing an Application & Troubleshooting logins](managing-applications.md)
32 |
33 | > Setting up Authentication
34 |
35 | + [Delegate: Using the Browser directly](authenticate-browser.md)
36 | + [Delegate: DeviceCode](authenticate-devicecode.md)
37 | + [Application: Certificate](authenticate-certificate.md)
38 | + [Application: Client Secret](authenticate-clientsecret.md)
39 |
--------------------------------------------------------------------------------
/docs/pictures/01-01-Authentication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/01-01-Authentication.png
--------------------------------------------------------------------------------
/docs/pictures/01-02-Platform.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/01-02-Platform.png
--------------------------------------------------------------------------------
/docs/pictures/01-03-RedirectUri.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/01-03-RedirectUri.png
--------------------------------------------------------------------------------
/docs/pictures/01-04-localhost.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/01-04-localhost.png
--------------------------------------------------------------------------------
/docs/pictures/01-05-Done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/01-05-Done.png
--------------------------------------------------------------------------------
/docs/pictures/02-01-Authentication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/02-01-Authentication.png
--------------------------------------------------------------------------------
/docs/pictures/02-02-Platform.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/02-02-Platform.png
--------------------------------------------------------------------------------
/docs/pictures/02-03-Localhost.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/02-03-Localhost.png
--------------------------------------------------------------------------------
/docs/pictures/02-04-AdvancedSettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/02-04-AdvancedSettings.png
--------------------------------------------------------------------------------
/docs/pictures/03-01-Certificates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/03-01-Certificates.png
--------------------------------------------------------------------------------
/docs/pictures/03-02-Selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/03-02-Selection.png
--------------------------------------------------------------------------------
/docs/pictures/03-03-Completed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/03-03-Completed.png
--------------------------------------------------------------------------------
/docs/pictures/03-04-Finished.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/03-04-Finished.png
--------------------------------------------------------------------------------
/docs/pictures/04-01-DawnOfASecret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/04-01-DawnOfASecret.png
--------------------------------------------------------------------------------
/docs/pictures/04-02-Configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/04-02-Configuration.png
--------------------------------------------------------------------------------
/docs/pictures/04-03-Secret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/04-03-Secret.png
--------------------------------------------------------------------------------
/docs/pictures/A-01-AppRegistrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/A-01-AppRegistrations.png
--------------------------------------------------------------------------------
/docs/pictures/A-02-NewRegistration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/A-02-NewRegistration.png
--------------------------------------------------------------------------------
/docs/pictures/A-03-Setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/A-03-Setup.png
--------------------------------------------------------------------------------
/docs/pictures/A-04-Portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/A-04-Portal.png
--------------------------------------------------------------------------------
/docs/pictures/C-01-ApiPermission-Portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-01-ApiPermission-Portal.png
--------------------------------------------------------------------------------
/docs/pictures/C-02-RequestPermissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-02-RequestPermissions.png
--------------------------------------------------------------------------------
/docs/pictures/C-03-ApplicationDelegate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-03-ApplicationDelegate.png
--------------------------------------------------------------------------------
/docs/pictures/C-04-ScopesFilter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-04-ScopesFilter.png
--------------------------------------------------------------------------------
/docs/pictures/C-05-ScopesAssign.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-05-ScopesAssign.png
--------------------------------------------------------------------------------
/docs/pictures/C-06-ConsentPending.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-06-ConsentPending.png
--------------------------------------------------------------------------------
/docs/pictures/C-07-ConsentGranting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-07-ConsentGranting.png
--------------------------------------------------------------------------------
/docs/pictures/C-08-ConsentGranted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-08-ConsentGranted.png
--------------------------------------------------------------------------------
/docs/pictures/C-09-UnknownService.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-09-UnknownService.png
--------------------------------------------------------------------------------
/docs/pictures/C-10-SearchingService.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/C-10-SearchingService.png
--------------------------------------------------------------------------------
/docs/pictures/D-01-Overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-01-Overview.png
--------------------------------------------------------------------------------
/docs/pictures/D-02-Properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-02-Properties.png
--------------------------------------------------------------------------------
/docs/pictures/D-03-RememberToSave.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-03-RememberToSave.png
--------------------------------------------------------------------------------
/docs/pictures/D-04-AssignUsersGroups.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-04-AssignUsersGroups.png
--------------------------------------------------------------------------------
/docs/pictures/D-05-Selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-05-Selection.png
--------------------------------------------------------------------------------
/docs/pictures/D-06-Selection2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-06-Selection2.png
--------------------------------------------------------------------------------
/docs/pictures/D-07-Assign.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-07-Assign.png
--------------------------------------------------------------------------------
/docs/pictures/D-08-Assigned.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-08-Assigned.png
--------------------------------------------------------------------------------
/docs/pictures/D-09-SigninLogs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriedrichWeinmann/EntraAuth/983ceec9fffdcfa1d9ca3f556b8a02a2110f22b7/docs/pictures/D-09-SigninLogs.png
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Entra Authentication Module
2 |
3 | Welcome to the Entra Authentication Module project.
4 | Your one stop for authenticating to any service behind Microsoft Entra Authentication.
5 |
6 | Whether you just want a token ... or for someone to deal with all of the nasty details of executing API requests.
7 |
8 | Functionally, if you liked the good old MSAL.PS and are looking for a successor, look no further.
9 |
10 | ## Installing
11 |
12 | To use this module, run the following command:
13 |
14 | ```powershell
15 | Install-Module EntraAuth -Scope CurrentUser
16 | ```
17 |
18 | Or if you have PowerShell 7.4 or later:
19 |
20 | ```powershell
21 | Install-PSResource EntraAuth
22 | ```
23 |
24 | ## Overview
25 |
26 | To profit from the module, you basically ...
27 |
28 | + Connect to a service
29 | + Then execute requests against it or retrieve its token
30 |
31 | Some common services come preconfigured (e.g. Graph, GraphBeta or the Security API), for others you might first need to register the service.
32 |
33 | > Note for module developers: There is a dedicated chapter at the bottom with important advice.
34 |
35 | ## Quickstart to Graph
36 |
37 | While this module is intended to interact with many APIs, the Graph API is by far the most common need.
38 | So, let's get started with the right away:
39 |
40 | ```powershell
41 | # Connect via default PowerShell Graph application
42 | Connect-EntraService -ClientID Graph -Scopes User.ReadBasic.All
43 |
44 | # Read info about current user
45 | Invoke-EntraRequest -Path me
46 |
47 | # List all users
48 | Invoke-EntraRequest -Path users
49 | ```
50 |
51 | There is a lot more to authenticating - especially once the default applications no longer suffice and we need to talk to more than just Graph.
52 | See below for the nitty-gritty "How the hell do I make this work?!" details.
53 |
54 | ## Preparing to Authenticate
55 |
56 | For those new to connecting to and executing against APIs that require Entra authentication, we have prepared a guide, explaining the different authentication options, which to chose when and what you need to do to prepare outside of the code.
57 |
58 | > [Guide to Authentication](docs/overview.md)
59 |
60 | ## Connect
61 |
62 | To connect you usually need a ClientID and a TenantID for the App Registration you are using for logon.
63 | Depending on how you want to authenticate, this App Registration may need some configuration:
64 |
65 | + Browser (default): In the `Authentication` tab in the portal, register a 'Mobile and Desktop Applications' Platform with the 'http://localhost' redirect uri.
66 | + DeviceCode: In the `Authentication` tab in the portal, register a 'Web' Platform with the 'http://localhost' redirect uri and enable `Allow public client flows`.
67 | + ClientSecret: In the `certificate & secrets` tab create a secret and provide it as a SecureString when connecting
68 | + Certificate: In the `certificate & secrets` tab register a certificate and provide it when connecting
69 |
70 | Example connect calls for each flow:
71 |
72 | ```powershell
73 | # Example values, fill in the appropriate ones from your App Registration
74 | $ClientID = 'd6a3ffb9-6217-40d6-bfb2-f5769b65970a'
75 | $TenantID = 'a948c2b3-8eb2-498a-9108-c32aeeaa0f97'
76 |
77 | # Browser Based
78 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID
79 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -Service Endpoint
80 |
81 | # DeviceCode authentication
82 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -DeviceCode
83 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -DeviceCode -Service Endpoint
84 |
85 | # Client Secret Based
86 | $secret = Get-ClipBoard | ConvertTo-SecureString -AsPlainText -Force # Assuming the secret is in your clipboard
87 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -ClientSecret $secret
88 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -ClientSecret $secret -Service Endpoint
89 |
90 | ## Certificate Based
91 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -Certificate $cert
92 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -CertificateThumbprint E1AE5158CA92CC9AA53D955217567B30E68647BD
93 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -CertificateName 'CN=Whatever'
94 | ```
95 |
96 | > Azure Key Vault integration
97 |
98 | It is possible to directly read a Certificate or Client Secret from an Azure Key Vault und use it for authentication.
99 | In order for this to work, an already established connection to Azure KeyVault is required:
100 |
101 | ```powershell
102 | # Option 1: Az.Accounts
103 | Connect-AzAccount
104 |
105 | # Option 2: EntraAuth integrated KeyVault service
106 | # App Registration used must have the delegate Key Vault scope "user_impersonation"
107 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -Service AzureKeyVault
108 | ```
109 |
110 | Once Key Vault access is established, this one line will retrieve the secret from Key Vault - no matter whether a certificate or a Client Secret - and complete the authentication. When connected like that, it will retrieve the secret from Key Vault again once the token expires.
111 |
112 | ```powershell
113 | # Direct Azure KeyVault integration with Certificate or Client Secret
114 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -VaultName myVault -SecretName mySecret
115 | ```
116 |
117 | > Managed Identity
118 |
119 | It is also possible to connect using a Managed Identity (for example from within an Azure Function App):
120 |
121 | ```powershell
122 | # Connect to graph using an MSI
123 | Connect-EntraService -Identity
124 |
125 | # Connect to Azure Key Vault using an MSI
126 | Connect-EntraService -Identity -Service AzureKeyVault
127 | ```
128 |
129 | It is also possible to connect via User-Assigned Managed Identity:
130 |
131 | ```powershell
132 | # Using the Client ID of the User-Assigned Managed Identity
133 | Connect-EntraService -Identity -IdentityID $miClientID
134 |
135 | # Using the Principal ID of the User-Assigned Managed Identity
136 | Connect-EntraService -Identity -IdentityID $princpalID -IdentityType PrincipalID
137 |
138 | # Using the Azure Resource ID of the User-Assigned Managed Identity
139 | Connect-EntraService -Identity -IdentityType ResourceID -IdentityID '/subscriptions//resourcegroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/'
140 | ```
141 |
142 | > Default Service
143 |
144 | By default, the service connected to is the Microsoft Graph API.
145 | The same default is also used for requests.
146 | To change the default, you can use the `-MakeDefault` parameter when connecting:
147 |
148 | ```powershell
149 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -Service Endpoint -MakeDefault
150 | ```
151 |
152 | This would make the `Endpoint` service the new default service for new connections or requests.
153 |
154 | > Multiple Services
155 |
156 | It is quite possible to be connected to multiple services in parallel.
157 | Even if you use the same app registration for both services, you need to connect for each service individually.
158 | You can however perform all connections using the same app registration in the same call:
159 |
160 | ```powershell
161 | Connect-EntraService -ClientID $ClientID -TenantID $TenantID -ClientSecret $secret -Service Endpoint, Graph
162 | ```
163 |
164 | > Graph & Graph Beta
165 |
166 | The Graph and the Graph Beta are registered as separate services.
167 | However, both can theoretically use the same token data ... but this module can't unify them properly without additional inconsistencies.
168 |
169 | However, when specifying the request (see below), rather than providing the relative api path, you can provide the full http-starting url instead.
170 | So if you mostly want to use v1.0 but have this one request that must be made in beta, you can specify the full url for that call and don't need separate connections.
171 |
172 | ## Requests
173 |
174 | Once connected to a service, executing requests against that service becomes quite simple:
175 |
176 | ```powershell
177 | # Request information from the default service
178 | Invoke-EntraRequest -Path me
179 |
180 | # Request information from the GraphBeta service
181 | Invoke-EntraRequest -Service GraphBeta -Path me
182 | ```
183 |
184 | > Query Modifiers
185 |
186 | You can modify requests by adding query parameters.
187 | Either you specify them in your path, or you use the `-Query` parameter:
188 |
189 | ```powershell
190 | Invoke-EntraRequest -Path 'users?$select=displayName,givenName,id'
191 |
192 | Invoke-EntraRequest -Path users -Query @{
193 | '$select' = 'displayName', 'givenName', 'id'
194 | }
195 | ```
196 |
197 | ## Using the Token
198 |
199 | Sometimes you want direct access to the token and just do your own thing.
200 | There are two ways to get a token:
201 |
202 | + Ask for it during the request
203 | + Retrieve it after connecting
204 |
205 | All tokens are maintained in the module for its runtime, but it will only maintain the latest iteration for a single service.
206 |
207 | ```powershell
208 | # During the Connectiong
209 | $token = Connect-EntraService -ClientID $ClientID -TenantID $TenantID -Service GraphBeta -PassThru
210 |
211 | # After already being connected
212 | $token = Get-EntraToken -Service GraphBeta
213 | ```
214 |
215 | Once obtained, a token can be used either in `Invoke-EntraRequest` or in your own code:
216 |
217 | ```powershell
218 | # Reuse token
219 | Invoke-EntraRequest -Path me -Token $token
220 |
221 | # Get Authentication header and use that
222 | Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/users' -Headers $token.GetHeader()
223 | ```
224 |
225 | > The `Getheader()` method will automatically refresh expiring tokens if needed.
226 | > Directly accessing the `.AccessToken` property is possible, but will not refresh tokens.
227 |
228 | ## Registering Services
229 |
230 | So, that whole thing is all nice and everything, but ... what if we want a token for a service not prepared in the module?
231 | What if it's an app that only exists in your own tenant?
232 | What if it's a function app only your team uses?
233 | Or some Microsoft Product that was released a year after this module and we just never updated it?
234 |
235 | Well, that is where our `*-EntraService` commands come in:
236 |
237 | + `Get-EntraService` to see all currently configured services
238 | + `Register-EntraService` to add new services
239 | + `Set-EntraService` to modify the configuration of an existing service
240 |
241 | The main one is `Register-EntraService`:
242 |
243 | ```powershell
244 | $graphCfg = @{
245 | Name = 'Graph'
246 | ServiceUrl = 'https://graph.microsoft.com/v1.0'
247 | Resource = 'https://graph.microsoft.com'
248 | DefaultScopes = @()
249 | HelpUrl = 'https://developer.microsoft.com/en-us/graph/quick-start'
250 | Header = @{ }
251 | NoRefresh = $false
252 | }
253 | Register-EntraService @graphCfg
254 | ```
255 |
256 | > Name
257 |
258 | The name the service is referenced by.
259 |
260 | > Service Url
261 |
262 | The base url all request from `Invoke-EntraRequest` using the service use, unless their requests specify the full web url.
263 | If your API calls look like this:
264 |
265 | ```text
266 | https://graph.microsoft.com/v1.0/users
267 | https://graph.microsoft.com/v1.0/me
268 | https://graph.microsoft.com/v1.0/messages
269 | ```
270 |
271 | Then `https://graph.microsoft.com/v1.0` would be the Service url.
272 | Effectively, you must provide any Url element after this value.
273 |
274 | This property only matters when you use `Invoke-EntraRequest` or directly read it off the token properties.
275 |
276 | > Resource
277 |
278 | This is the ID of the resource connecting to.
279 | To figure out the value needed here, go to the `API Permission` tab on the App Registration and click on the respective header in your list of scopes (e.g. `Microsoft Graph (##)`):
280 | The value under the display name at the top is the Resource.
281 | This could be a url such as `https://graph.microsoft.com` but can also be something like `api://` and has no fixed relationship to the requests URLs.
282 |
283 | > Default Scopes
284 |
285 | In delegate mode, we sometimes ask for what scopes we need (which may lead to users being prompted to consent them).
286 | If a service is a bit pointless to use without some minimal scopes and you want to make it more comfortable to use, you can provide the default set of scopes here.
287 | If the user does not specify any scopes during the connect, then at least these are asked for.
288 |
289 | + Not used in application authentication flow or for the ROPC flow
290 | + Can lead to failure if the application does not have these scopes configured
291 |
292 | > Help Url
293 |
294 | Pure documentation in case you want to help users figure out how to use your service.
295 |
296 | > Header
297 |
298 | Additional values to include in the header for all requests.
299 | For example, if you always must specify a specific content-type, this can be included here.
300 |
301 | > NoRefresh
302 |
303 | Tokens only last so long.
304 | With a certificate or a Client Secret, that's no problem - just silently do the authentication again and that's it.
305 | However, interactive logons using the browser would then force the user to logon again and again.
306 |
307 | To make this less painful, a refresh token can be requested on the first interactive logon, allowing the silent renewal of tokens.
308 | This is done automatically by default, so no need to meddle with that.
309 | Usually.
310 |
311 | Some services may not support this and some security policies interfere as well for administrative accounts, so refresh tokens may not be desired.
312 | Configuring a service to not refresh means interactive logons will prompt again once the token has expired.
313 |
314 | ## Module building on EntraAuth
315 |
316 | Find the latest guidance to implementing EntraAuth [in our dedicated docs page for that very topic](docs/building-on-entraauth.md).
317 |
--------------------------------------------------------------------------------
/tests/functions/New-EntraFilterBuilder.tests.ps1:
--------------------------------------------------------------------------------
1 | Describe "Testing the command New-EntraFilterBuilder" -Tag unit {
2 | It "Will create a simple condition" {
3 | $filterBuilder = New-EntraFilterBuilder
4 | $filterBuilder.Add('name', 'eq', 'Fred')
5 | $filterBuilder.Get() | Should -Be "name eq 'Fred'"
6 | }
7 | It "Will create a simple filter query" {
8 | $filterBuilder = New-EntraFilterBuilder
9 | $filterBuilder.Add('name', 'eq', 'Fred')
10 | $filterBuilder.GetHeader().'$filter' | Should -Be "name eq 'Fred'"
11 | $filterBuilder.GetHeader().Count | Should -Be 1
12 | }
13 |
14 | It "Will merge conditions with an AND condition" {
15 | $filterBuilder = New-EntraFilterBuilder
16 | $filterBuilder.Add('name', 'eq', 'Fred')
17 | $filterBuilder.Add('country', 'eq', 'Germany')
18 | $filterBuilder.Get() | Should -Be "name eq 'Fred' and country eq 'Germany'"
19 | }
20 | It "Will merge conditions with an OR condition" {
21 | $filterBuilder = New-EntraFilterBuilder -Logic OR
22 | $filterBuilder.Add('name', 'eq', 'Fred')
23 | $filterBuilder.Add('name', 'eq', 'Max')
24 | $filterBuilder.Get() | Should -Be "name eq 'Fred' or name eq 'Max'"
25 | }
26 |
27 | It "Will support wildcard filtering correctly" {
28 | $filterBuilder = New-EntraFilterBuilder
29 | $filterBuilder.Add('displayName','eq','dept-000-*')
30 | $filterBuilder.Get() | Should -Be "startswith(displayName, 'dept-000-')"
31 |
32 | $filterBuilder = New-EntraFilterBuilder
33 | $filterBuilder.Add('displayName','eq','*-admin')
34 | $filterBuilder.Get() | Should -Be "endswith(displayName, '-admin')"
35 | }
36 | It "Will support searching in a multivalue field" {
37 | $filterBuilder = New-EntraFilterBuilder
38 | $filterBuilder.Add('servicePrincipalName', 'any', 'abc@contoso.com')
39 | $filterBuilder.Get() | Should -Be "servicePrincipalName/any(x:x eq 'abc@contoso.com')"
40 | }
41 | It "Will support matching against multiple values" {
42 | $filterBuilder = New-EntraFilterBuilder
43 | $filterBuilder.Add('name','in',@('Fred','Max'))
44 | $filterBuilder.Get() | Should -Be "name in ('Fred', 'Max')"
45 | }
46 |
47 | It "Will support nested filter builders & custom filters in the correct order" {
48 | $filterBuilder = New-EntraFilterBuilder
49 | $filterBuilder.Add('enabled', 'eq', $true)
50 |
51 | $filterBuilder2 = New-EntraFilterBuilder -Logic OR
52 | $filterBuilder2.Add('name','eq','Fred')
53 | $filterBuilder2.Add('name','eq','Max')
54 | $filterBuilder.Add($filterBuilder2)
55 |
56 | $filterBuilder.Add('(abc eq 42)')
57 |
58 | $filterBuilder.Get() | Should -Be "enabled eq True and (name eq 'Fred' or name eq 'Max') and (abc eq 42)"
59 | }
60 | }
--------------------------------------------------------------------------------
/tests/functions/readme.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | This is where the function tests go.
4 |
5 | Make sure to put them in folders reflecting the actual module structure.
6 |
7 | It is not necessary to differentiate between internal and public functions here.
--------------------------------------------------------------------------------
/tests/general/FileIntegrity.Exceptions.ps1:
--------------------------------------------------------------------------------
1 | # List of forbidden commands
2 | $global:BannedCommands = @(
3 | 'Write-Output'
4 |
5 | # Use CIM instead where possible
6 | 'Get-WmiObject'
7 | 'Invoke-WmiMethod'
8 | 'Register-WmiEvent'
9 | 'Remove-WmiObject'
10 | 'Set-WmiInstance'
11 |
12 | # Use Get-WinEvent instead
13 | 'Get-EventLog'
14 |
15 | # User Preference should not be used in automation
16 | 'Clear-Host' # Console Screen belongs to the user
17 | 'Set-Location' # Console path belongs to the user. Use $PSScriptRoot instead.
18 |
19 | # Dynamic Variables are undesirable. Use hashtable instead.
20 | 'Get-Variable'
21 | 'Set-Variable'
22 | 'Clear-Variable'
23 | 'Remove-Variable'
24 | 'New-Variable'
25 |
26 | # Dynamic Code execution should not require this
27 | 'Invoke-Expression' # Consider splatting instead. Yes, you can splat parameters for external applications!
28 | )
29 |
30 | <#
31 | Contains list of exceptions for banned cmdlets.
32 | Insert the file names of files that may contain them.
33 |
34 | Example:
35 | "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1')
36 | #>
37 | $global:MayContainCommand = @{
38 | "Write-Host" = @()
39 | "Write-Verbose" = @()
40 | "Write-Warning" = @()
41 | "Write-Error" = @()
42 | "Write-Output" = @()
43 | "Write-Information" = @()
44 | "Write-Debug" = @()
45 | }
--------------------------------------------------------------------------------
/tests/general/FileIntegrity.Tests.ps1:
--------------------------------------------------------------------------------
1 | $moduleRoot = (Resolve-Path "$global:testroot\..").Path
2 |
3 | . "$global:testroot\general\FileIntegrity.Exceptions.ps1"
4 |
5 | Describe "Verifying integrity of module files" {
6 | BeforeAll {
7 | function Get-FileEncoding
8 | {
9 | <#
10 | .SYNOPSIS
11 | Tests a file for encoding.
12 |
13 | .DESCRIPTION
14 | Tests a file for encoding.
15 |
16 | .PARAMETER Path
17 | The file to test
18 | #>
19 | [CmdletBinding()]
20 | Param (
21 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)]
22 | [Alias('FullName')]
23 | [string]
24 | $Path
25 | )
26 |
27 | if ($PSVersionTable.PSVersion.Major -lt 6)
28 | {
29 | [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path
30 | }
31 | else
32 | {
33 | [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path
34 | }
35 |
36 | if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' }
37 | elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' }
38 | elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' }
39 | elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' }
40 | else { 'Unknown' }
41 | }
42 | }
43 |
44 | Context "Validating PS1 Script files" {
45 | $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*"
46 |
47 | foreach ($file in $allFiles)
48 | {
49 | $name = $file.FullName.Replace("$moduleRoot\", '')
50 |
51 | It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } {
52 | Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM'
53 | }
54 |
55 | It "[$name] Should have no trailing space" -TestCases @{ file = $file } {
56 | ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty
57 | }
58 |
59 | $tokens = $null
60 | $parseErrors = $null
61 | $null = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors)
62 |
63 | It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } {
64 | $parseErrors | Should -BeNullOrEmpty
65 | }
66 |
67 | foreach ($command in $global:BannedCommands)
68 | {
69 | if ($global:MayContainCommand["$command"] -notcontains $file.Name)
70 | {
71 | It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } {
72 | $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty
73 | }
74 | }
75 | }
76 | }
77 | }
78 |
79 | Context "Validating help.txt help files" {
80 | $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*"
81 |
82 | foreach ($file in $allFiles)
83 | {
84 | $name = $file.FullName.Replace("$moduleRoot\", '')
85 |
86 | It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } {
87 | Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM'
88 | }
89 |
90 | It "[$name] Should have no trailing space" -TestCases @{ file = $file } {
91 | ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0
92 | }
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/tests/general/Help.Exceptions.ps1:
--------------------------------------------------------------------------------
1 | # List of functions that should be ignored
2 | $global:FunctionHelpTestExceptions = @(
3 |
4 | )
5 |
6 | <#
7 | List of arrayed enumerations. These need to be treated differently. Add full name.
8 | Example:
9 |
10 | "Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]"
11 | #>
12 | $global:HelpTestEnumeratedArrays = @(
13 |
14 | )
15 |
16 | <#
17 | Some types on parameters just fail their validation no matter what.
18 | For those it becomes possible to skip them, by adding them to this hashtable.
19 | Add by following this convention: = @()
20 | Example:
21 |
22 | "Get-DbaCmObject" = @("DoNotUse")
23 | #>
24 | $global:HelpTestSkipParameterType = @{
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/tests/general/Help.Tests.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .NOTES
3 | The original test this is based upon was written by June Blender.
4 | After several rounds of modifications it stands now as it is, but the honor remains hers.
5 |
6 | Thank you June, for all you have done!
7 |
8 | .DESCRIPTION
9 | This test evaluates the help for all commands in a module.
10 |
11 | .PARAMETER SkipTest
12 | Disables this test.
13 |
14 | .PARAMETER CommandPath
15 | List of paths under which the script files are stored.
16 | This test assumes that all functions have their own file that is named after themselves.
17 | These paths are used to search for commands that should exist and be tested.
18 | Will search recursively and accepts wildcards, make sure only functions are found
19 |
20 | .PARAMETER ModuleName
21 | Name of the module to be tested.
22 | The module must already be imported
23 |
24 | .PARAMETER ExceptionsFile
25 | File in which exceptions and adjustments are configured.
26 | In it there should be two arrays and a hashtable defined:
27 | $global:FunctionHelpTestExceptions
28 | $global:HelpTestEnumeratedArrays
29 | $global:HelpTestSkipParameterType
30 | These can be used to tweak the tests slightly in cases of need.
31 | See the example file for explanations on each of these usage and effect.
32 | #>
33 | [CmdletBinding()]
34 | Param (
35 | [switch]
36 | $SkipTest,
37 |
38 | [string[]]
39 | $CommandPath = @("$global:testroot\..\EntraAuth\functions", "$global:testroot\..\EntraAuth\internal\functions"),
40 |
41 | [string]
42 | $ModuleName = "EntraAuth",
43 |
44 | [string]
45 | $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1"
46 | )
47 | if ($SkipTest) { return }
48 | . $ExceptionsFile
49 |
50 | $CommandPath = @(
51 | "$global:testroot\..\EntraAuth\functions"
52 | "$global:testroot\..\EntraAuth\internal\functions"
53 | )
54 |
55 | $includedNames = foreach ($path in $CommandPath) { (Get-ChildItem $path -Recurse -File | Where-Object Name -like "*.ps1").BaseName }
56 | $commandTypes = @('Cmdlet', 'Function')
57 | if ($PSVersionTable.PSEdition -eq 'Desktop' ) { $commandTypes += 'Workflow' }
58 | $commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTypes | Where-Object Name -In $includedNames
59 |
60 | ## When testing help, remember that help is cached at the beginning of each session.
61 | ## To test, restart session.
62 |
63 |
64 | foreach ($command in $commands) {
65 | $commandName = $command.Name
66 |
67 | # Skip all functions that are on the exclusions list
68 | if ($global:FunctionHelpTestExceptions -contains $commandName) { continue }
69 |
70 | # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets
71 | $Help = Get-Help $commandName -ErrorAction SilentlyContinue
72 |
73 | Describe "Test help for $commandName" {
74 |
75 | # If help is not found, synopsis in auto-generated help is the syntax diagram
76 | It "should not be auto-generated" -TestCases @{ Help = $Help } {
77 | $Help.Synopsis | Should -Not -BeLike '*`[``]*'
78 | }
79 |
80 | # Should be a description for every function
81 | It "gets description for $commandName" -TestCases @{ Help = $Help } {
82 | $Help.Description | Should -Not -BeNullOrEmpty
83 | }
84 |
85 | # Should be at least one example
86 | It "gets example code from $commandName" -TestCases @{ Help = $Help } {
87 | ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty
88 | }
89 |
90 | # Should be at least one example description
91 | It "gets example help from $commandName" -TestCases @{ Help = $Help } {
92 | ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty
93 | }
94 |
95 | Context "Test parameter help for $commandName" {
96 |
97 | $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'ProgressAction'
98 |
99 | $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common
100 | $parameterNames = $parameters.Name
101 | $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique
102 | foreach ($parameter in $parameters) {
103 | $parameterName = $parameter.Name
104 | $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName
105 |
106 | # Should be a description for every parameter
107 | It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } {
108 | $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty
109 | }
110 |
111 | if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue }
112 |
113 | $codeType = $parameter.ParameterType.Name
114 |
115 | if ($parameter.ParameterType.IsEnum) {
116 | # Enumerations often have issues with the typename not being reliably available
117 | $names = $parameter.ParameterType::GetNames($parameter.ParameterType)
118 | # Parameter type in Help should match code
119 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } {
120 | $parameterHelp.parameterValueGroup.parameterValue | Should -be $names
121 | }
122 | }
123 | elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) {
124 | # Enumerations often have issues with the typename not being reliably available
125 | $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType)
126 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } {
127 | $parameterHelp.parameterValueGroup.parameterValue | Should -be $names
128 | }
129 | }
130 | else {
131 | # To avoid calling Trim method on a null object.
132 | $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() }
133 | # Parameter type in Help should match code
134 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } {
135 | $helpType | Should -be $codeType
136 | }
137 | }
138 | }
139 | foreach ($helpParm in $HelpParameterNames) {
140 | # Shouldn't find extra parameters in help.
141 | It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } {
142 | $helpParm -in $parameterNames | Should -Be $true
143 | }
144 | }
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/tests/general/Manifest.Tests.ps1:
--------------------------------------------------------------------------------
1 | Describe "Validating the module manifest" {
2 | $moduleRoot = (Resolve-Path "$global:testroot\..\EntraAuth").Path
3 | $manifest = ((Get-Content "$moduleRoot\EntraAuth.psd1") -join "`n") | Invoke-Expression
4 | Context "Basic resources validation" {
5 | $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1"
6 | It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } {
7 |
8 | $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject
9 | $functions | Should -BeNullOrEmpty
10 | }
11 | It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } {
12 | $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject
13 | $functions | Should -BeNullOrEmpty
14 | }
15 |
16 | It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } {
17 | $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1"
18 | $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty
19 | }
20 | }
21 |
22 | Context "Individual file validation" {
23 | It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } {
24 | Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true
25 | }
26 |
27 | foreach ($format in $manifest.FormatsToProcess)
28 | {
29 | It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } {
30 | Test-Path "$moduleRoot\$format" | Should -Be $true
31 | }
32 | }
33 |
34 | foreach ($type in $manifest.TypesToProcess)
35 | {
36 | It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } {
37 | Test-Path "$moduleRoot\$type" | Should -Be $true
38 | }
39 | }
40 |
41 | foreach ($assembly in $manifest.RequiredAssemblies)
42 | {
43 | if ($assembly -like "*.dll") {
44 | It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } {
45 | Test-Path "$moduleRoot\$assembly" | Should -Be $true
46 | }
47 | }
48 | else {
49 | It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } {
50 | { Add-Type -AssemblyName $assembly } | Should -Not -Throw
51 | }
52 | }
53 | }
54 |
55 | foreach ($tag in $manifest.PrivateData.PSData.Tags)
56 | {
57 | It "Tags should have no spaces in name" -TestCases @{ tag = $tag } {
58 | $tag -match " " | Should -Be $false
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/tests/general/PSScriptAnalyzer.Tests.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | Param (
3 | [switch]
4 | $SkipTest,
5 |
6 | [string[]]
7 | $CommandPath = @("$global:testroot\..\EntraAuth\functions", "$global:testroot\..\EntraAuth\internal\functions")
8 | )
9 |
10 | BeforeDiscovery {
11 | if ($SkipTest) { return }
12 |
13 | $global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList
14 |
15 | # Create an array containing the path and basename of all files to test
16 | $commandFiles = $CommandPath | ForEach-Object {
17 | Get-ChildItem -Path $_ -Recurse | Where-Object Name -like "*.ps1"
18 | } | ForEach-Object {
19 | @{
20 | BaseName = $_.BaseName
21 | FullName = $_.FullName
22 | }
23 | }
24 |
25 | # Create an array contain all rules
26 | $scriptAnalyzerRules = Get-ScriptAnalyzerRule | ForEach-Object {
27 | @{
28 | RuleName = $_.RuleName
29 | }
30 | }
31 | }
32 |
33 | Describe 'Invoking PSScriptAnalyzer against commandbase' {
34 |
35 | Context "Analyzing " -ForEach $commandFiles {
36 | BeforeAll {
37 | $analysis = Invoke-ScriptAnalyzer -Path $FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess
38 | }
39 |
40 | It "Should pass " -Foreach $scriptAnalyzerRules {
41 | # Test if the rule is present and if so create a string containing more info which will be shown in the details of the test output. If it's empty the test is succesfull as there is no problem with this rule.
42 | $analysis | Where-Object RuleName -EQ $RuleName | Foreach-Object {
43 | # Create a string
44 | "$($_.Severity) at Line $($_.Line) Column $($_.Column) with '$($_.Extent)'"
45 | # Add the data (and supress the output) to the global variable for later use
46 | $null = $global:__pester_data.ScriptAnalyzer.Add($_)
47 | } | Should -BeNullOrEmpty
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/tests/pester.ps1:
--------------------------------------------------------------------------------
1 | param (
2 | $TestGeneral = $true,
3 |
4 | $TestFunctions = $true,
5 |
6 | [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')]
7 | [Alias('Show')]
8 | $Output = "None",
9 |
10 | $Include = "*",
11 |
12 | $Exclude = ""
13 | )
14 |
15 | Write-Host "Starting Tests"
16 |
17 | Write-Host "Importing Module"
18 |
19 | $global:testroot = $PSScriptRoot
20 | $global:__pester_data = @{ }
21 |
22 | Remove-Module EntraAuth -ErrorAction Ignore
23 | Import-Module "$PSScriptRoot\..\EntraAuth\EntraAuth.psd1"
24 | Import-Module "$PSScriptRoot\..\EntraAuth\EntraAuth.psm1" -Force
25 |
26 | # Need to import explicitly so we can use the configuration class
27 | Import-Module Pester
28 |
29 | Write-Host "Creating test result folder"
30 | $null = New-Item -Path "$PSScriptRoot\.." -Name TestResults -ItemType Directory -Force
31 |
32 | $totalFailed = 0
33 | $totalRun = 0
34 |
35 | $testresults = @()
36 | $config = [PesterConfiguration]::Default
37 | $config.TestResult.Enabled = $true
38 |
39 | #region Run General Tests
40 | if ($TestGeneral)
41 | {
42 | Write-Host "Modules imported, proceeding with general tests"
43 | foreach ($file in (Get-ChildItem "$PSScriptRoot\general" | Where-Object Name -like "*.Tests.ps1"))
44 | {
45 | if ($file.Name -notlike $Include) { continue }
46 | if ($file.Name -like $Exclude) { continue }
47 |
48 | Write-Host " Executing $($file.Name)"
49 | $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\TestResults" "TEST-$($file.BaseName).xml"
50 | $config.Run.Path = $file.FullName
51 | $config.Run.PassThru = $true
52 | $config.Output.Verbosity = $Output
53 | $results = Invoke-Pester -Configuration $config
54 | foreach ($result in $results)
55 | {
56 | $totalRun += $result.TotalCount
57 | $totalFailed += $result.FailedCount
58 | $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object {
59 | $testresults += [pscustomobject]@{
60 | Block = $_.Block
61 | Name = "It $($_.Name)"
62 | Result = $_.Result
63 | Message = $_.ErrorRecord.DisplayErrorMessage
64 | }
65 | }
66 | }
67 | }
68 | }
69 | #endregion Run General Tests
70 |
71 | $global:__pester_data.ScriptAnalyzer | Out-Host
72 |
73 | #region Test Commands
74 | if ($TestFunctions)
75 | {
76 | Write-Host "Proceeding with individual tests"
77 | foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1"))
78 | {
79 | if ($file.Name -notlike $Include) { continue }
80 | if ($file.Name -like $Exclude) { continue }
81 |
82 | Write-Host " Executing $($file.Name)"
83 | $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\TestResults" "TEST-$($file.BaseName).xml"
84 | $config.Run.Path = $file.FullName
85 | $config.Run.PassThru = $true
86 | $config.Output.Verbosity = $Output
87 | $results = Invoke-Pester -Configuration $config
88 | foreach ($result in $results)
89 | {
90 | $totalRun += $result.TotalCount
91 | $totalFailed += $result.FailedCount
92 | $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object {
93 | $testresults += [pscustomobject]@{
94 | Block = $_.Block
95 | Name = "It $($_.Name)"
96 | Result = $_.Result
97 | Message = $_.ErrorRecord.DisplayErrorMessage
98 | }
99 | }
100 | }
101 | }
102 | }
103 | #endregion Test Commands
104 |
105 | $testresults | Sort-Object Describe, Context, Name, Result, Message | Format-List
106 |
107 | if ($totalFailed -eq 0) { Write-Host "All $totalRun tests executed without a single failure!" }
108 | else { Write-Host "$totalFailed tests out of $totalRun tests failed!" }
109 |
110 | if ($totalFailed -gt 0)
111 | {
112 | throw "$totalFailed / $totalRun tests failed!"
113 | }
--------------------------------------------------------------------------------
/tests/readme.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | This is the folder, where all the tests go.
4 |
5 | Those are subdivided in two categories:
6 |
7 | - General
8 | - Function
9 |
10 | ## General Tests
11 |
12 | General tests are function generic and test for general policies.
13 |
14 | These test scan answer questions such as:
15 |
16 | - Is my module following my style guides?
17 | - Does any of my scripts have a syntax error?
18 | - Do my scripts use commands I do not want them to use?
19 | - Do my commands follow best practices?
20 | - Do my commands have proper help?
21 |
22 | Basically, these allow a general module health check.
23 |
24 | These tests are already provided as part of the template.
25 |
26 | ## Function Tests
27 |
28 | A healthy module should provide unit and integration tests for the commands & components it ships.
29 | Only then can be guaranteed, that they will actually perform as promised.
30 |
31 | However, as each such test must be specific to the function it tests, there cannot be much in the way of templates.
--------------------------------------------------------------------------------