Repository navigation
OCI Authentication Hardcoded for Azure #1946
Description
Activity
This also brings into question around what the credential type is expected to be in a SecretManagement store. Right now it expects a credential in the form of
tenantIdand the Az AccessToken and will do theoauth2/exchangeflow to get the ACR refresh token from that. This isn't ideal as- It's ACR specific so any other OCI registries cannot store a token in the repository registration
- The access token is short lived which defeats the purpose of persisting such a value over time
It seems like this logic should change so that the credential stored here is used directly with the bearer token exchange like
# The user/pass is dependent on the registry but it's typically a user + secret # GHCR is username:path_token # ACR is client_id:client_secret $secretCred = Get-Secret ... $registry = "ghcr.io" $scope = "repository:jborean93/publishtest:pull" $registryUri = "https://$Registry/v2/" $resp = Invoke-WebRequest -Uri $registryUri -SkipHttpErrorCheck $bearer = $resp.Headers['WWW-Authenticate'] | Select-Object -First 1 if (-not $bearer) { throw "No WWW-Authenticate found in response headers for '$registryUri'" } if ($bearer -match 'realm="([^"]+)"') { $realm = $matches[1] } else { throw "Could not extract realm from WWW-Authenticate header '$bearer'" } if ($bearer -match 'service="([^"]+)"') { $service = $matches[1] } else { throw "Could not extract service from WWW-Authenticate header '$bearer'" } $tokenUri = "${realm}?service=${service}&client_id=testclient&scope=${scope}" $tokenResponse = Invoke-WebRequest -Uri $tokenUri -Method Get -Authentication Basic -Credential $secretCred $tokenResponse.Content | ConvertFrom-Json
Maybe a credential can have additional metadata to indicate that it's an AzAccess token so it does that extra
oauth2/exchangestep if that's needed for testing but that should definitely not be the default.I've opened #1949 and created a summary on the changes/risks/reasons behind it all. I'm happy to talk about it further as needed but hopefully this helps explain what is happening and why I made the changes I did.
Note: This has been generated by AI but I've reviewed/modified the changes as necessary.
OCI Registry Authentication: Current Issues and Proposed Changes
Summary
PSResourceGet's current authentication implementation is tightly coupled to Azure Container Registry (ACR) and doesn't work with other OCI-compliant registries like GitHub Container Registry (GHCR). This document outlines:
- The Problem: Current implementation only works with ACR and unauthenticated registries (e.g., MCR)
- Root Causes: ACR-specific authentication workflows, overly broad token scopes, and incompatible HTTP methods
- The Solution: Implement standard OCI authentication with ACR-specific handling only when needed
- Breaking Change: Explicit credentials will use standard OCI auth by default; ACR-specific auth requires username prefix
- Risk: Reliance on service name pattern
*.azurecr.ioto detect ACR endpoints for automatic Azure login
Glossary
Registries
- OCI (Open Container Initiative) - Open standard that defines container registry APIs
- ACR (Azure Container Registry) - Azure's OCI registry implementation (e.g.,
myregistry.azurecr.io) - GHCR (GitHub Container Registry) - GitHub's OCI registry implementation (
ghcr.io) - MAR/MCR (Microsoft Artifact Registry/Microsoft Container Registry) - Public OCI registry for Microsoft artifacts (
mcr.microsoft.com)
Token Types (Authentication Flow)
Understanding the token types used and their names is critical to understanding the problems:
-
Azure Access Token - Short-lived token from Azure login (
az login,Connect-AzAccount,.NET DefaultAzureCredential)- Used with ACR only
- Not an OCI standard concept
-
ACR Refresh Token - Short-lived token ACR issues when given an Azure Access Token
- ACR-specific OAuth2 exchange (
/oauth2/exchange) - Acts as a password for the next step
- ACR-specific OAuth2 exchange (
-
Registry Bearer Token - The actual token used for OCI API requests
- Standard across all OCI registries
- Has specific scopes defining permissions
-
Token Scope - Defines what the bearer token can do
- Format:
{resourcetype}:{packagename}:{action} - Examples:
repository:mymodule:pull,registry:catalog:* - Some OCI endpoints support wildcards for sections, others do not
- Format:
Current Authentication Workflow
The current implementation attempts authentication in the following order:
Step 1: Discover Authentication Requirements
Send
HEAD /v2/to the registry and check the response:- 200 OK → Registry is unauthenticated (e.g., MCR), no bearer token needed
- 401 Unauthorized → Extract
realmandservicefromWWW-Authenticateheader - Other → Treated as error
Example
WWW-Authenticateheaders:# ACR (Invoke-WebRequest https://psresourcegettest.azurecr.io/v2/ -SkipHttpErrorCheck).Headers['WWW-Authenticate'] # Bearer realm="https://psresourcegettest.azurecr.io/oauth2/token",service="psresourcegettest.azurecr.io" # GHCR (Invoke-WebRequest https://ghcr.io/v2/ -SkipHttpErrorCheck).Headers['WWW-Authenticate'] # Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull" # MCR - No authentication (Invoke-WebRequest https://mcr.microsoft.com/v2/ -SkipHttpErrorCheck).StatusCode # 200
Step 2: Try Anonymous Access
Request a bearer token anonymously from
{realm}?service={service}&scope={scope}with noAuthorizationheader.Scope used:
repository:*:*for package operationsregistry:catalog:*for catalog/search operations
If this succeeds, use the returned bearer token from the
access_tokenkey for API requests.Step 3: Try Authenticated Access (ACR-Specific)
If anonymous access fails:
- Check for credential from
Register-PSResourceRepositoryOR attempt to get Azure Access Token viaDefaultAzureCredential(environment/browser login) - If an Azure Access Token is available, perform ACR-specific OAuth2 exchange:
- Exchange Azure Access Token → ACR Refresh Token
- Exchange ACR Refresh Token → Registry Bearer Token
- Use the registry bearer token for API requests
Key Limitation: This workflow assumes all explicit credentials are Azure Access Tokens and always uses the ACR OAuth2 flow.
Problems with Current Implementation
The current implementation works for ACR and unauthenticated registries (MCR) but fails with other OCI registries (GHCR, Docker Hub, etc.). Issues fall into three categories:
1. OCI Spec Compatibility Issues
Problem: Using
HEAD /v2/instead ofGET /v2/The OCI spec recommends
GET /v2/for the version check endpoint. Some registries don't supportHEAD:# GHCR rejects HEAD requests Invoke-WebRequest -Method Head -Uri https://ghcr.io/v2/ # Invoke-WebRequest: Response status code does not indicate success: 405 (Method Not Allowed) # GET works as expected Invoke-WebRequest -Method Get -Uri https://ghcr.io/v2/ # Returns 401 with WWW-Authenticate header (expected behavior)
Impact: PSResourceGet fails immediately when trying to use GHCR.
Problem: Overly Broad Token Scopes
Current implementation requests wildcard scopes (
repository:*:*) which many registries reject:# GHCR rejects wildcard scopes Invoke-WebRequest -Method Get -Uri 'https://ghcr.io/token?service=ghcr.io&scope=repository:*:*' # ERROR: "requested access to the resource is denied" # GHCR accepts package-specific scopes Invoke-WebRequest -Method Get -Uri 'https://ghcr.io/token?service=ghcr.io&scope=repository:jborean93/publishtest:pull' # SUCCESS: Returns anonymous bearer token
Impact: Anonymous and authenticated access fails even for public packages on GHCR.
Problem: Anonymous token is only checked under access_token
Current implementation only uses the
access_tokenkey in theGET /v2/response but OCI registries can return it on eithertokenoraccess_tokenhttps://distribution.github.io/distribution/spec/auth/token/#token-response-fields.For compatibility with OAuth 2.0, we will also accept token under the name access_token. At least one of these fields must be specified
(Invoke-WebRequest -Method Get -Uri 'https://ghcr.io/token?service=ghcr.io&scope=repository:jborean93/publishtest:pull').Content # {"token":"...."} # This has been configured to allow anonymous access # I'll be removing this registry once all this has been sorted so # this link may not work in the future. (Invoke-WebRequest -Method Get -Uri 'https://jboreancrtest.azurecr.io/oauth2/token?service=jboreancrtest.azurecr.io&scope=repository:publishtest:pull').Content # {"access_token":"..."}
2. ACR-Specific Assumptions
Problem: All Explicit Credentials Use ACR OAuth2 Flow
The current code assumes any provided credential is an Azure Access Token and always performs the ACR-specific OAuth2 exchange. This makes it impossible to use:
- Entra Service Principal credentials (client ID + secret) for long-lived ACR authentication
- Standard username/password credentials for non-ACR registries (GHCR tokens, Docker Hub, etc.)
Impact: Cannot authenticate using longer lived Entra client secret or to non-ACR registries with credentials.
Problem: Azure Login Attempted for All Registries
When no explicit credential is provided, the code attempts to retrieve an Azure Access Token via
DefaultAzureCredential(environment variables or browser login) regardless of registry type. This is made worse when there is no credential present and the browser cannot be displayed (VM, SSH logon, etc) as an error will be displayed.Impact: Users get unnecessary Azure login prompts when installing from GHCR or other non-ACR registries.
3. ACR Credential Limitations
Problem: Cannot Use Service Principals Directly
ACR supports authentication with Entra Service Principals (client ID + secret), which are suitable for automation and long-lived scenarios. However, the current implementation can only use short-lived Azure Access Tokens.
Impact: Users cannot create stable, long-lived credentials for ACR in automation scenarios.
Proposed Changes
This PR implements standard OCI authentication with ACR-specific handling only when needed. Here's how each change addresses the problems:
1. Fix OCI Compatibility Issues
Change Addresses Details Use GET /v2/instead ofHEAD /v2/Method Not Allowed on GHCR Follows OCI spec recommendation Use package-specific scopes Wildcard rejection on GHCR repository:{packagename}:pullfor installrepository:{packagename}:push,pullfor publishregistry:catalog:*only for catalog operationsSimplify anonymous token requests Unnecessary complexity Use query parameters only (no form body needed) Use tokenoraccess_tokenresponseDifferent return JSON structures Some OCI registries return tokenoraccess_tokenin the bearer token response exchange2. Implement Standard OCI Authentication
New Authentication Flow:
- Discover authentication requirements -
GET /v2/(changed fromHEAD) - Try anonymous access with package-specific scope (changed from wildcard)
- If credentials provided (new:
-Credentialparameter support onInstall-PSResource/Publish-PSResource):- Default: Use standard OCI basic auth (username/password → bearer token)
- ACR special case: If username in the format
AzureAccessToken: GUID, use ACR OAuth2 flow - Currently this credential is set as
HttpClientHandler.Credentialwhich should work but - The various issues above means it's never really used and for ACR it only works for a
clientId:clientSecretcombo (no access token)
- If no credentials and registry service ends with
.azurecr.io:- Try
DefaultAzureCredential(environment/browser login) - Use ACR OAuth2 flow with the Azure Access Token
- Try
3. Key Behavioral Changes
Now Supported:
- GHCR, Docker Hub, and other OCI registries
- Entra Service Principal credentials for ACR (client ID + secret)
- Standard username/password for any OCI registry
- Per-operation credentials via
-Credentialparameter
Breaking Change:
- Explicit credentials use standard OCI auth by default breaking a credential with a short lived Azure Access Token
- ACR-specific OAuth2 flow now requires username prefix:
AzureAccessToken: GUID
ACR Auto-Login:
- Only triggered when
servicefromWWW-Authenticateends with.azurecr.io - Prevents unwanted Azure login prompts for non-ACR registries
Breaking Changes
Change: Credential Handling for ACR
Before (PSResourceGet 1.1.1+):
# Any credential automatically used ACR OAuth2 flow Register-PSResourceRepository -Name MyACR -Uri https://myregistry.azurecr.io -CredentialInfo $cred # $cred assumed to be tenantId:Azure Access Token, always uses ACR OAuth2
After (This PR):
# Standard credentials use OCI Basic token exchange Set-Secret -Name 'clientId' -Value (ConvertTo-SecureString 'client-secret' -AsPlainText) $cred = [PSCredentialInfo]@{ VaultName = "SecretStore" SecretName = 'clientId' } Register-PSResourceRepository -Name MyACR -Uri https://myregistry.azurecr.io -CredentialInfo $cred # Azure Access Tokens need explicit prefix $azt = Get-AzureAccessToken Set-Secret -Name "AzureAccessToken: $tenantId" -Secret $azt.Token -Verbose $cred = [PSCredentialInfo]@{ VaultName = "SecretStore" SecretName = "AzureAccessToken: $tenantId" } Register-PSResourceRepository -Name MyACR -Uri https://myregistry.azurecr.io -CredentialInfo $cred
Impact Assessment
Who is affected:
- Users storing credentials with
Register-PSResourceRepositoryfor ACR
Likelihood of impact:
- Low - This behavior was never documented and only existed since v1.1.1
- Azure Access Tokens are short-lived (hours), so storing them in repository registration doesn't make practical sense
- Most users likely rely on
DefaultAzureCredential(auto-login) rather than explicit tokens
Migration path:
- If you were storing Azure Access Tokens: Add
AzureAccessToken: GUIDusername prefix whereGUIDis thetenantId, or - Use Service Principal credentials (client ID + secret) for automation scenarios
Why This Change is Beneficial
- Enables Service Principal support - Long-lived credentials for automation (previously impossible)
- Enables non-ACR registries - GHCR, Docker Hub, etc. can now use credentials
- Follows OCI standards - Default behavior matches standard OCI authentication
- Still supports Azure tokens - Just requires explicit opt-in via username prefix
Potential Risks and Concerns
Primary Risk: ACR Detection via Service Name Pattern
The Issue:
The PR uses the
servicevalue from theWWW-Authenticateheader to detect ACR registries:# ACR returns service ending with .azurecr.io Bearer realm="https://myregistry.azurecr.io/oauth2/token",service="myregistry.azurecr.io"
Code checks:
if ($service -like "*.azurecr.io")→ trigger Azure auto-loginWhy This Matters:
This pattern-matching is critical because we cannot trigger
DefaultAzureCredential(browser login prompts) for non-ACR registries. The code must distinguish between ACR and other OCI registries.The Risk:
If Azure changes the
servicevalue format in the future (unlikely but possible), the detection would break and:- ACR auto-login would stop working
- Users would need to provide explicit credentials
Likelihood: Low - Service names are typically tied to DNS and ACR architecture
Mitigation: The behavior degrades gracefully - explicit credentials still work
Alternative Approaches Considered
Option 1: Explicit ACR Flag (More Conservative)
Add a switch to
Register-PSResourceRepositoryto explicitly mark ACR registries:Register-PSResourceRepository -Name MyACR -Uri https://myregistry.azurecr.io -IsAzureContainerRegistry
Pros:
- No reliance on service name pattern
- Explicit user intent
- Future-proof against Azure changes
Cons:
- Extra parameter users must know about
- Breaking change for ACR auto-login (requires re-registration)
- More complex UX
Option 2: New ApiVersion for OCI Standards
Add
-ApiVersion OciRegistrywhile keepingContainerRegistryfor legacy ACR behavior:# Legacy ACR-only behavior (default, backwards compatible) Register-PSResourceRepository -Name MyACR -Uri ... -ApiVersion ContainerRegistry # New OCI-standard behavior Register-PSResourceRepository -Name MyGHCR -Uri ... -ApiVersion OciRegistry
Pros:
- Full backwards compatibility
- Clear distinction between legacy and standard behavior
Cons:
- Maintains technical debt
- Users must know which ApiVersion to use
- ACR users wouldn't benefit from improvements without migration
Option 3: Metadata-Based Credential Typing
Use
SecretManagementmetadata to indicate credential type:Set-Secret -Name ACRToken -Secret $cred -Metadata @{ Type = "AzureAccessToken" }
Pros:
- No username prefix needed
- Explicit credential type
Cons:
- Requires SecretManagement module
- More complex credential management
- Doesn't solve the broader OCI compatibility issues
- Doesn't solve the implicit Azure env/browser lookup problem
Recommended Approach (Current PR)
Why the pattern-matching approach is best:
- Minimal breaking change - Only affects undocumented behavior since v1.1.1
- Best user experience - ACR auto-login continues to "just work"
- Enables new scenarios - GHCR, Service Principals, etc.
- Graceful degradation - If detection fails, explicit credentials still work
- Follows standards - Default behavior matches OCI spec
The service name pattern is stable (tied to Azure's DNS infrastructure) and the risk is acceptable given the benefits.
Summary
This PR transforms PSResourceGet from an ACR-centric implementation to a proper OCI-compliant registry client while maintaining ACR-specific features where needed.
What works now that didn't before:
- GitHub Container Registry (GHCR)
- Docker Hub and other OCI registries
- Entra Service Principal credentials for ACR automation
- Per-operation credentials via
-Credentialparameter - Package-specific token scopes (security best practice)
What continues to work:
- ACR with Azure auto-login (environment/browser)
- ACR with explicit Azure Access Tokens (requires username prefix)
- Unauthenticated registries (MCR)
The tradeoff:
- Breaking change for undocumented Azure Access Token storage (fixable with username prefix)
- Relies on service name pattern to detect ACR (low risk, graceful degradation)
Reacted by Janne KujanpääSydneyhSmith commented
on Aug 13, 2026 CollaboratorMore actionsWe are working on getting the ORAS .net library merged in which hopefully should resolve some of these issue
Reacted by Rui Lopes
Prerequisites
Steps to reproduce
The authentication mechanisms used on OCI/Container Registries is essentially hardcoded to only work for ACR based registries. This makes it impossible to use PSResourceGet with other OCI registries that don't follow the ACR specific auth flow.
For example it is not possible to install from GitHub's Container/OCI repository. I've got a test nupkg published at https://github.com/jborean93/PowerShell-PublishTest/pkgs/container/publishtest and this is what happens when you try to install it with and without a credential with PSResourceGet 1.2.0-rc3
The same error occurs when you use a
-Credentialwith a GitHub username/PAT. The error may vary depending on whether there is an Az context available as PSResourceGet falls back on this for all registries.The reason is because GetContainerRegistryAccessToken is coded to do the following
DefaultAzureCredentialThe 3 problems I see here
oauth2/exchangeworkflow that ultimately gets the ACR bearer token from an Az AccessToken needs to be selectively done (some flag on the registered repository maybe?)oauth2/exchangeflow should not be done unlessGet-AzAccessTokenI'm hoping to send through a PR to try and fix this at least for some of the scenarios but wanted to raise the issue to at least summarise what is happening. I've written Get-OciBearerToken to show the workflow that I've found works for ACR, MAR, GHCR, and hopefully any other OCI compliant registry. I've used this function to
write:packagesset or through the GHAGITHUB_TOKENvalueGet-AzAccessTokenfor any valid connection using theoauth2/exchangeto get the ACR refresh tokenThe biggest hiccup is figuring out whether we have an Az AccessToken to do the
oauth2/exchangestep.Expected behavior
Module can be installed with no credential when the registry allows it or with a credential when it is private.Actual behavior
See above (fails)Error details
See aboveEnvironment data
Visuals
No response