go.mod: update osbuild/images to v0.156.0

tag v0.155.0
Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com>

Changes with 0.155.0

----------------
  * Fedora 43: add shadow-utils when LockRoot is enabled, update cloud-init service name (osbuild/images#1618)
    * Author: Achilleas Koutsou, Reviewers: Gianluca Zuccarelli, Michael Vogt
  * Update osbuild dependency commit ID to latest (osbuild/images#1609)
    * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger, Tomáš Hozza
  * Update snapshots to 20250626 (osbuild/images#1623)
    * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger
  * distro/rhel9: xz compress azure-cvm image type [HMS-8587] (osbuild/images#1620)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro/rhel: introduce new image type: Azure SAP Apps [HMS-8738] (osbuild/images#1612)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro/rhel: move ansible-core to sap_extras_pkgset (osbuild/images#1624)
    * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Tomáš Hozza
  * github/create-tag: allow passing the version when run manually (osbuild/images#1621)
    * Author: Achilleas Koutsou, Reviewers: Lukáš Zapletal, Tomáš Hozza
  * rhel9: move image-config into pure YAML (HMS-8593) (osbuild/images#1616)
    * Author: Michael Vogt, Reviewers: Achilleas Koutsou, Simon de Vlieger
  * test: split manifest checksums into separate files (osbuild/images#1625)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza

— Somewhere on the Internet, 2025-06-30

---

tag v0.156.0
Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com>

Changes with 0.156.0

----------------
  * Many: delete repositories for EOL distributions (HMS-7044) (osbuild/images#1607)
    * Author: Tomáš Hozza, Reviewers: Michael Vogt, Simon de Vlieger
  * RHSM/facts: add 'image-builder CLI' API type (osbuild/images#1640)
    * Author: Tomáš Hozza, Reviewers: Brian C. Lane, Simon de Vlieger
  * Update dependencies 2025-06-29 (osbuild/images#1628)
    * Author: SchutzBot, Reviewers: Simon de Vlieger, Tomáš Hozza
  * Update osbuild dependency commit ID to latest (osbuild/images#1627)
    * Author: SchutzBot, Reviewers: Simon de Vlieger, Tomáš Hozza
  * [RFC] image: drop `InstallWeakDeps` from image.DiskImage (osbuild/images#1642)
    * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger, Tomáš Hozza
  * build(deps): bump the go-deps group across 1 directory with 3 updates (osbuild/images#1632)
    * Author: dependabot[bot], Reviewers: SchutzBot, Tomáš Hozza
  * distro/rhel10: xz compress azure-cvm image type (osbuild/images#1638)
    * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Simon de Vlieger
  * distro: cleanup/refactor distro/{defs,generic} (HMS-8744) (osbuild/images#1570)
    * Author: Michael Vogt, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro: remove some hardcoded values from generic/images.go (osbuild/images#1636)
    * Author: Michael Vogt, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro: small tweaks for the YAML based imagetypes (osbuild/images#1622)
    * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger
  * fedora/wsl: packages and locale (osbuild/images#1635)
    * Author: Simon de Vlieger, Reviewers: Michael Vogt, Tomáš Hozza
  * image/many: make compression more generic (osbuild/images#1634)
    * Author: Simon de Vlieger, Reviewers: Brian C. Lane, Michael Vogt
  * manifest: handle content template name with spaces (osbuild/images#1641)
    * Author: Bryttanie, Reviewers: Brian C. Lane, Michael Vogt, Tomáš Hozza
  * many: implement gzip (osbuild/images#1633)
    * Author: Simon de Vlieger, Reviewers: Michael Vogt, Tomáš Hozza
  * rhel/azure: set GRUB_TERMINAL based on architecture [RHEL-91383] (osbuild/images#1626)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza

— Somewhere on the Internet, 2025-07-07

---
This commit is contained in:
Achilleas Koutsou 2025-07-10 16:14:25 +02:00
parent 60c5f10af8
commit 3fd7092db5
1486 changed files with 124742 additions and 82516 deletions

View file

@ -65,6 +65,13 @@ type AuthenticationScheme = authority.AuthenticationScheme
type Account = shared.Account
type TokenSource = base.TokenSource
const (
TokenSourceIdentityProvider = base.TokenSourceIdentityProvider
TokenSourceCache = base.TokenSourceCache
)
// CertFromPEM converts a PEM file (.pem or .key) for use with [NewCredFromCert]. The file
// must contain the public certificate and the private key. If a PEM block is encrypted and
// password is not an empty string, it attempts to decrypt the PEM blocks using the password.
@ -305,7 +312,9 @@ func WithInstanceDiscovery(enabled bool) Option {
// If an invalid region name is provided, the non-regional endpoint MIGHT be used or the token request MIGHT fail.
func WithAzureRegion(val string) Option {
return func(o *clientOptions) {
o.azureRegion = val
if val != "" {
o.azureRegion = val
}
}
}
@ -429,6 +438,7 @@ func WithClaims(claims string) interface {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireByUsernamePasswordOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
@ -437,6 +447,7 @@ func WithClaims(claims string) interface {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireByUsernamePasswordOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
@ -450,6 +461,8 @@ func WithClaims(claims string) interface {
t.claims = claims
case *acquireTokenOnBehalfOfOptions:
t.claims = claims
case *acquireTokenByUsernamePasswordOptions:
t.claims = claims
case *acquireTokenSilentOptions:
t.claims = claims
case *authCodeURLOptions:
@ -496,6 +509,7 @@ func WithTenantID(tenantID string) interface {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireByUsernamePasswordOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
@ -504,6 +518,7 @@ func WithTenantID(tenantID string) interface {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireByUsernamePasswordOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
@ -517,6 +532,8 @@ func WithTenantID(tenantID string) interface {
t.tenantID = tenantID
case *acquireTokenOnBehalfOfOptions:
t.tenantID = tenantID
case *acquireTokenByUsernamePasswordOptions:
t.tenantID = tenantID
case *acquireTokenSilentOptions:
t.tenantID = tenantID
case *authCodeURLOptions:
@ -592,6 +609,46 @@ func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts
return cca.base.AcquireTokenSilent(ctx, silentParameters)
}
// acquireTokenByUsernamePasswordOptions contains optional configuration for AcquireTokenByUsernamePassword
type acquireTokenByUsernamePasswordOptions struct {
claims, tenantID string
authnScheme AuthenticationScheme
}
// AcquireByUsernamePasswordOption is implemented by options for AcquireTokenByUsernamePassword
type AcquireByUsernamePasswordOption interface {
acquireByUsernamePasswordOption()
}
// AcquireTokenByUsernamePassword acquires a security token from the authority, via Username/Password Authentication.
// NOTE: this flow is NOT recommended.
//
// Options: [WithClaims], [WithTenantID]
func (cca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username, password string, opts ...AcquireByUsernamePasswordOption) (AuthResult, error) {
o := acquireTokenByUsernamePasswordOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
authParams, err := cca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return AuthResult{}, err
}
authParams.Scopes = scopes
authParams.AuthorizationType = authority.ATUsernamePassword
authParams.Claims = o.claims
authParams.Username = username
authParams.Password = password
if o.authnScheme != nil {
authParams.AuthnScheme = o.authnScheme
}
token, err := cca.base.Token.UsernamePassword(ctx, authParams)
if err != nil {
return AuthResult{}, err
}
return cca.base.AuthResultFromToken(ctx, authParams, token)
}
// acquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
type acquireTokenByAuthCodeOptions struct {
challenge, claims, tenantID string
@ -683,7 +740,7 @@ func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string,
if err != nil {
return AuthResult{}, err
}
return cca.base.AuthResultFromToken(ctx, authParams, token, true)
return cca.base.AuthResultFromToken(ctx, authParams, token)
}
// acquireTokenOnBehalfOfOptions contains optional configuration for AcquireTokenOnBehalfOf

View file

@ -64,11 +64,20 @@ type CallErr struct {
Err error
}
type InvalidJsonErr struct {
Err error
}
// Errors implements error.Error().
func (e CallErr) Error() string {
return e.Err.Error()
}
// Errors implements error.Error().
func (e InvalidJsonErr) Error() string {
return e.Err.Error()
}
// Verbose prints a versbose error message with the request or response.
func (e CallErr) Verbose() string {
e.Resp.Request = nil // This brings in a bunch of TLS crap we don't need

View file

@ -5,16 +5,17 @@ package base
import (
"context"
"errors"
"fmt"
"net/url"
"reflect"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/internal/storage"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
@ -94,6 +95,7 @@ type AuthResult struct {
// AuthResultMetadata which contains meta data for the AuthResult
type AuthResultMetadata struct {
RefreshOn time.Time
TokenSource TokenSource
}
@ -101,9 +103,8 @@ type TokenSource int
// These are all the types of token flows.
const (
SourceUnknown TokenSource = 0
IdentityProvider TokenSource = 1
Cache TokenSource = 2
TokenSourceIdentityProvider TokenSource = 0
TokenSourceCache TokenSource = 1
)
// AuthResultFromStorage creates an AuthResult from a storage token response (which is generated from the cache).
@ -111,7 +112,6 @@ func AuthResultFromStorage(storageTokenResponse storage.TokenResponse) (AuthResu
if err := storageTokenResponse.AccessToken.Validate(); err != nil {
return AuthResult{}, fmt.Errorf("problem with access token in StorageTokenResponse: %w", err)
}
account := storageTokenResponse.Account
accessToken := storageTokenResponse.AccessToken.Secret
grantedScopes := strings.Split(storageTokenResponse.AccessToken.Scopes, scopeSeparator)
@ -132,7 +132,8 @@ func AuthResultFromStorage(storageTokenResponse storage.TokenResponse) (AuthResu
GrantedScopes: grantedScopes,
DeclinedScopes: nil,
Metadata: AuthResultMetadata{
TokenSource: Cache,
TokenSource: TokenSourceCache,
RefreshOn: storageTokenResponse.AccessToken.RefreshOn.T,
},
}, nil
}
@ -146,10 +147,11 @@ func NewAuthResult(tokenResponse accesstokens.TokenResponse, account shared.Acco
Account: account,
IDToken: tokenResponse.IDToken,
AccessToken: tokenResponse.AccessToken,
ExpiresOn: tokenResponse.ExpiresOn.T,
ExpiresOn: tokenResponse.ExpiresOn,
GrantedScopes: tokenResponse.GrantedScopes.Slice,
Metadata: AuthResultMetadata{
TokenSource: IdentityProvider,
TokenSource: TokenSourceIdentityProvider,
RefreshOn: tokenResponse.RefreshOn.T,
},
}, nil
}
@ -165,6 +167,8 @@ type Client struct {
AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
cacheAccessor cache.ExportReplace
cacheAccessorMu *sync.RWMutex
canRefresh map[string]*atomic.Value
canRefreshMu *sync.Mutex
}
// Option is an optional argument to the New constructor.
@ -241,6 +245,8 @@ func New(clientID string, authorityURI string, token *oauth.Client, options ...O
cacheAccessorMu: &sync.RWMutex{},
manager: storage.New(token),
pmanager: storage.NewPartitionedManager(token),
canRefresh: make(map[string]*atomic.Value),
canRefreshMu: &sync.Mutex{},
}
for _, o := range options {
if err = o(&client); err != nil {
@ -345,6 +351,28 @@ func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilen
if silent.Claims == "" {
ar, err = AuthResultFromStorage(storageTokenResponse)
if err == nil {
if rt := storageTokenResponse.AccessToken.RefreshOn.T; !rt.IsZero() && Now().After(rt) {
b.canRefreshMu.Lock()
refreshValue, ok := b.canRefresh[tenant]
if !ok {
refreshValue = &atomic.Value{}
refreshValue.Store(false)
b.canRefresh[tenant] = refreshValue
}
b.canRefreshMu.Unlock()
if refreshValue.CompareAndSwap(false, true) {
defer refreshValue.Store(false)
// Added a check to see if the token is still same because there is a chance
// that the token is already refreshed by another thread.
// If the token is not same, we don't need to refresh it.
// Which means it refreshed.
if str, err := m.Read(ctx, authParams); err == nil && str.AccessToken.Secret == ar.AccessToken {
if tr, er := b.Token.Credential(ctx, authParams, silent.Credential); er == nil {
return b.AuthResultFromToken(ctx, authParams, tr)
}
}
}
}
ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
return ar, err
}
@ -362,7 +390,7 @@ func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilen
if err != nil {
return ar, err
}
return b.AuthResultFromToken(ctx, authParams, token, true)
return b.AuthResultFromToken(ctx, authParams, token)
}
func (b Client) AcquireTokenByAuthCode(ctx context.Context, authCodeParams AcquireTokenAuthCodeParameters) (AuthResult, error) {
@ -391,7 +419,7 @@ func (b Client) AcquireTokenByAuthCode(ctx context.Context, authCodeParams Acqui
return AuthResult{}, err
}
return b.AuthResultFromToken(ctx, authParams, token, true)
return b.AuthResultFromToken(ctx, authParams, token)
}
// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
@ -420,15 +448,12 @@ func (b Client) AcquireTokenOnBehalfOf(ctx context.Context, onBehalfOfParams Acq
authParams.UserAssertion = onBehalfOfParams.UserAssertion
token, err := b.Token.OnBehalfOf(ctx, authParams, onBehalfOfParams.Credential)
if err == nil {
ar, err = b.AuthResultFromToken(ctx, authParams, token, true)
ar, err = b.AuthResultFromToken(ctx, authParams, token)
}
return ar, err
}
func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.AuthParams, token accesstokens.TokenResponse, cacheWrite bool) (AuthResult, error) {
if !cacheWrite {
return NewAuthResult(token, shared.Account{})
}
func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.AuthParams, token accesstokens.TokenResponse) (AuthResult, error) {
var m manager = b.manager
if authParams.AuthorizationType == authority.ATOnBehalfOf {
m = b.pmanager
@ -458,6 +483,10 @@ func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.Au
return ar, err
}
// This function wraps time.Now() and is used for refreshing the application
// was created to test the function against refreshin
var Now = time.Now
func (b Client) AllAccounts(ctx context.Context) ([]shared.Account, error) {
if b.cacheAccessor != nil {
b.cacheAccessorMu.RLock()

View file

@ -72,6 +72,7 @@ type AccessToken struct {
ClientID string `json:"client_id,omitempty"`
Secret string `json:"secret,omitempty"`
Scopes string `json:"target,omitempty"`
RefreshOn internalTime.Unix `json:"refresh_on,omitempty"`
ExpiresOn internalTime.Unix `json:"expires_on,omitempty"`
ExtendedExpiresOn internalTime.Unix `json:"extended_expires_on,omitempty"`
CachedAt internalTime.Unix `json:"cached_at,omitempty"`
@ -83,7 +84,7 @@ type AccessToken struct {
}
// NewAccessToken is the constructor for AccessToken.
func NewAccessToken(homeID, env, realm, clientID string, cachedAt, expiresOn, extendedExpiresOn time.Time, scopes, token, tokenType, authnSchemeKeyID string) AccessToken {
func NewAccessToken(homeID, env, realm, clientID string, cachedAt, refreshOn, expiresOn, extendedExpiresOn time.Time, scopes, token, tokenType, authnSchemeKeyID string) AccessToken {
return AccessToken{
HomeAccountID: homeID,
Environment: env,
@ -93,6 +94,7 @@ func NewAccessToken(homeID, env, realm, clientID string, cachedAt, expiresOn, ex
Secret: token,
Scopes: scopes,
CachedAt: internalTime.Unix{T: cachedAt.UTC()},
RefreshOn: internalTime.Unix{T: refreshOn.UTC()},
ExpiresOn: internalTime.Unix{T: expiresOn.UTC()},
ExtendedExpiresOn: internalTime.Unix{T: extendedExpiresOn.UTC()},
TokenType: tokenType,
@ -102,8 +104,9 @@ func NewAccessToken(homeID, env, realm, clientID string, cachedAt, expiresOn, ex
// Key outputs the key that can be used to uniquely look up this entry in a map.
func (a AccessToken) Key() string {
ks := []string{a.HomeAccountID, a.Environment, a.CredentialType, a.ClientID, a.Realm, a.Scopes}
key := strings.Join(
[]string{a.HomeAccountID, a.Environment, a.CredentialType, a.ClientID, a.Realm, a.Scopes},
ks,
shared.CacheKeySeparator,
)
// add token type to key for new access tokens types. skip for bearer token type to

View file

@ -114,7 +114,8 @@ func (m *PartitionedManager) Write(authParameters authority.AuthParams, tokenRes
realm,
clientID,
cachedAt,
tokenResponse.ExpiresOn.T,
tokenResponse.RefreshOn.T,
tokenResponse.ExpiresOn,
tokenResponse.ExtExpiresOn.T,
target,
tokenResponse.AccessToken,

View file

@ -173,6 +173,7 @@ func (m *Manager) Write(authParameters authority.AuthParams, tokenResponse acces
environment := authParameters.AuthorityInfo.Host
realm := authParameters.AuthorityInfo.Tenant
clientID := authParameters.ClientID
target := strings.Join(tokenResponse.GrantedScopes.Slice, scopeSeparator)
cachedAt := time.Now()
authnSchemeKeyID := authParameters.AuthnScheme.KeyID()
@ -193,7 +194,8 @@ func (m *Manager) Write(authParameters authority.AuthParams, tokenResponse acces
realm,
clientID,
cachedAt,
tokenResponse.ExpiresOn.T,
tokenResponse.RefreshOn.T,
tokenResponse.ExpiresOn,
tokenResponse.ExtExpiresOn.T,
target,
tokenResponse.AccessToken,
@ -265,6 +267,9 @@ func (m *Manager) aadMetadataFromCache(ctx context.Context, authorityInfo author
}
func (m *Manager) aadMetadata(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
if m.requests == nil {
return authority.InstanceDiscoveryMetadata{}, fmt.Errorf("httpclient in oauth instance for fetching metadata is nil")
}
m.aadCacheMu.Lock()
defer m.aadCacheMu.Unlock()
discoveryResponse, err := m.requests.AADInstanceDiscovery(ctx, authorityInfo)
@ -459,6 +464,7 @@ func (m *Manager) readAccount(homeAccountID string, envAliases []string, realm s
func (m *Manager) writeAccount(account shared.Account) error {
key := account.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract.Accounts[key] = account

View file

@ -31,4 +31,6 @@ type TokenProviderResult struct {
AccessToken string
// ExpiresInSeconds is the lifetime of the token in seconds
ExpiresInSeconds int
// RefreshInSeconds indicates the suggested time to refresh the token, if any
RefreshInSeconds int
}

View file

@ -146,7 +146,8 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
// Note: It is a little weird we handle some errors by not going to the failPage. If they all should,
// change this to s.error() and make s.error() write the failPage instead of an error code.
_, _ = w.Write([]byte(fmt.Sprintf(failPage, headerErr, desc)))
s.putResult(Result{Err: fmt.Errorf(desc)})
s.putResult(Result{Err: fmt.Errorf("%s", desc)})
return
}

View file

@ -111,7 +111,7 @@ func (t *Client) Credential(ctx context.Context, authParams authority.AuthParams
Scopes: scopes,
TenantID: authParams.AuthorityInfo.Tenant,
}
tr, err := cred.TokenProvider(ctx, params)
pr, err := cred.TokenProvider(ctx, params)
if err != nil {
if len(scopes) == 0 {
err = fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which may cause the following error: %w", err)
@ -119,14 +119,18 @@ func (t *Client) Credential(ctx context.Context, authParams authority.AuthParams
}
return accesstokens.TokenResponse{}, err
}
return accesstokens.TokenResponse{
TokenType: authParams.AuthnScheme.AccessTokenType(),
AccessToken: tr.AccessToken,
ExpiresOn: internalTime.DurationTime{
T: now.Add(time.Duration(tr.ExpiresInSeconds) * time.Second),
},
tr := accesstokens.TokenResponse{
TokenType: authParams.AuthnScheme.AccessTokenType(),
AccessToken: pr.AccessToken,
ExpiresOn: now.Add(time.Duration(pr.ExpiresInSeconds) * time.Second),
GrantedScopes: accesstokens.Scopes{Slice: authParams.Scopes},
}, nil
}
if pr.RefreshInSeconds > 0 {
tr.RefreshOn = internalTime.DurationTime{
T: now.Add(time.Duration(pr.RefreshInSeconds) * time.Second),
}
}
return tr, nil
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {

View file

@ -17,6 +17,7 @@ import (
/* #nosec */
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
@ -68,7 +69,7 @@ type DeviceCodeResponse struct {
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
VerificationURL string `json:"verification_url"`
VerificationURL string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
Message string `json:"message"`
@ -112,19 +113,31 @@ func (c *Credential) JWT(ctx context.Context, authParams authority.AuthParams) (
}
return c.AssertionCallback(ctx, options)
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
claims := jwt.MapClaims{
"aud": authParams.Endpoints.TokenEndpoint,
"exp": json.Number(strconv.FormatInt(time.Now().Add(10*time.Minute).Unix(), 10)),
"iss": authParams.ClientID,
"jti": uuid.New().String(),
"nbf": json.Number(strconv.FormatInt(time.Now().Unix(), 10)),
"sub": authParams.ClientID,
})
}
isADFSorDSTS := authParams.AuthorityInfo.AuthorityType == authority.ADFS ||
authParams.AuthorityInfo.AuthorityType == authority.DSTS
var signingMethod jwt.SigningMethod = jwt.SigningMethodPS256
thumbprintKey := "x5t#S256"
if isADFSorDSTS {
signingMethod = jwt.SigningMethodRS256
thumbprintKey = "x5t"
}
token := jwt.NewWithClaims(signingMethod, claims)
token.Header = map[string]interface{}{
"alg": "RS256",
"typ": "JWT",
"x5t": base64.StdEncoding.EncodeToString(thumbprint(c.Cert)),
"alg": signingMethod.Alg(),
"typ": "JWT",
thumbprintKey: base64.StdEncoding.EncodeToString(thumbprint(c.Cert, signingMethod.Alg())),
}
if authParams.SendX5C {
@ -133,17 +146,23 @@ func (c *Credential) JWT(ctx context.Context, authParams authority.AuthParams) (
assertion, err := token.SignedString(c.Key)
if err != nil {
return "", fmt.Errorf("unable to sign a JWT token using private key: %w", err)
return "", fmt.Errorf("unable to sign JWT token: %w", err)
}
return assertion, nil
}
// thumbprint runs the asn1.Der bytes through sha1 for use in the x5t parameter of JWT.
// https://tools.ietf.org/html/rfc7517#section-4.8
func thumbprint(cert *x509.Certificate) []byte {
/* #nosec */
a := sha1.Sum(cert.Raw)
return a[:]
func thumbprint(cert *x509.Certificate, alg string) []byte {
switch alg {
case jwt.SigningMethodRS256.Name: // identity providers like ADFS don't support SHA256 assertions, so need to support this
hash := sha1.Sum(cert.Raw) /* #nosec */
return hash[:]
default:
hash := sha256.Sum256(cert.Raw)
return hash[:]
}
}
// Client represents the REST calls to get tokens from token generator backends.
@ -262,11 +281,7 @@ func (c Client) FromClientSecret(ctx context.Context, authParameters authority.A
qv.Set(clientID, authParameters.ClientID)
addScopeQueryParam(qv, authParameters)
token, err := c.doTokenResp(ctx, authParameters, qv)
if err != nil {
return token, fmt.Errorf("FromClientSecret(): %w", err)
}
return token, nil
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) FromAssertion(ctx context.Context, authParameters authority.AuthParams, assertion string) (TokenResponse, error) {
@ -281,11 +296,7 @@ func (c Client) FromAssertion(ctx context.Context, authParameters authority.Auth
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
token, err := c.doTokenResp(ctx, authParameters, qv)
if err != nil {
return token, fmt.Errorf("FromAssertion(): %w", err)
}
return token, nil
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) FromUserAssertionClientSecret(ctx context.Context, authParameters authority.AuthParams, userAssertion string, clientSecret string) (TokenResponse, error) {

View file

@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"time"
@ -173,14 +174,75 @@ type TokenResponse struct {
FamilyID string `json:"foci"`
IDToken IDToken `json:"id_token"`
ClientInfo ClientInfo `json:"client_info"`
ExpiresOn internalTime.DurationTime `json:"expires_in"`
RefreshOn internalTime.DurationTime `json:"refresh_in,omitempty"`
ExpiresOn time.Time `json:"-"`
ExtExpiresOn internalTime.DurationTime `json:"ext_expires_in"`
GrantedScopes Scopes `json:"scope"`
DeclinedScopes []string // This is derived
AdditionalFields map[string]interface{}
scopesComputed bool
}
scopesComputed bool
func (tr *TokenResponse) UnmarshalJSON(data []byte) error {
type Alias TokenResponse
aux := &struct {
ExpiresIn internalTime.DurationTime `json:"expires_in,omitempty"`
ExpiresOn any `json:"expires_on,omitempty"`
*Alias
}{
Alias: (*Alias)(tr),
}
// Unmarshal the JSON data into the aux struct
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Function to parse different date formats
// This is a workaround for the issue described here:
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/4963
parseExpiresOn := func(expiresOn string) (time.Time, error) {
var formats = []string{
"01/02/2006 15:04:05", // MM/dd/yyyy HH:mm:ss
"2006-01-02 15:04:05", // yyyy-MM-dd HH:mm:ss
time.RFC3339Nano, // ISO 8601 (with nanosecond precision)
}
for _, format := range formats {
if t, err := time.Parse(format, expiresOn); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("invalid ExpiresOn format: %s", expiresOn)
}
if expiresOnStr, ok := aux.ExpiresOn.(string); ok {
if ts, err := strconv.ParseInt(expiresOnStr, 10, 64); err == nil {
tr.ExpiresOn = time.Unix(ts, 0)
return nil
}
if expiresOnStr != "" {
if t, err := parseExpiresOn(expiresOnStr); err != nil {
return err
} else {
tr.ExpiresOn = t
return nil
}
}
}
// Check if ExpiresOn is a number (Unix timestamp or ISO 8601)
if expiresOnNum, ok := aux.ExpiresOn.(float64); ok {
tr.ExpiresOn = time.Unix(int64(expiresOnNum), 0)
return nil
}
if !aux.ExpiresIn.T.IsZero() {
tr.ExpiresOn = aux.ExpiresIn.T
return nil
}
return errors.New("expires_in and expires_on are both missing or invalid")
}
// ComputeScope computes the final scopes based on what was granted by the server and

View file

@ -98,7 +98,7 @@ func (c *Client) JSONCall(ctx context.Context, endpoint string, headers http.Hea
if resp != nil {
if err := unmarshal(data, resp); err != nil {
return fmt.Errorf("json decode error: %w\njson message bytes were: %s", err, string(data))
return errors.InvalidJsonErr{Err: fmt.Errorf("json decode error: %w\njson message bytes were: %s", err, string(data))}
}
}
return nil
@ -221,7 +221,7 @@ func (c *Client) URLFormCall(ctx context.Context, endpoint string, qv url.Values
}
if resp != nil {
if err := unmarshal(data, resp); err != nil {
return fmt.Errorf("json decode error: %w\nraw message was: %s", err, string(data))
return errors.InvalidJsonErr{Err: fmt.Errorf("json decode error: %w\nraw message was: %s", err, string(data))}
}
}
return nil

View file

@ -5,4 +5,4 @@
package version
// Version is the version of this client package that is communicated to the server.
const Version = "1.2.0"
const Version = "1.4.2"

View file

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package managedidentity
import (
"context"
"net/http"
"os"
)
func createAzureMLAuthRequest(ctx context.Context, id ID, resource string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, os.Getenv(msiEndpointEnvVar), nil)
if err != nil {
return nil, err
}
req.Header.Set("secret", os.Getenv(msiSecretEnvVar))
q := req.URL.Query()
q.Set(apiVersionQueryParameterName, azureMLAPIVersion)
q.Set(resourceQueryParameterName, resource)
q.Set("clientid", os.Getenv("DEFAULT_IDENTITY_CLIENT_ID"))
if cid, ok := id.(UserAssignedClientID); ok {
q.Set("clientid", string(cid))
}
req.URL.RawQuery = q.Encode()
return req, nil
}

View file

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package managedidentity
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
)
func createCloudShellAuthRequest(ctx context.Context, resource string) (*http.Request, error) {
msiEndpoint := os.Getenv(msiEndpointEnvVar)
msiEndpointParsed, err := url.Parse(msiEndpoint)
if err != nil {
return nil, fmt.Errorf("couldn't parse %q: %s", msiEndpoint, err)
}
data := url.Values{}
data.Set(resourceQueryParameterName, resource)
msiDataEncoded := data.Encode()
body := io.NopCloser(strings.NewReader(msiDataEncoded))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, msiEndpointParsed.String(), body)
if err != nil {
return nil, fmt.Errorf("error creating http request %s", err)
}
req.Header.Set(metaHTTPHeaderName, "true")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}

View file

@ -0,0 +1,717 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package managedidentity provides a client for retrieval of Managed Identity applications.
The Managed Identity Client is used to acquire a token for managed identity assigned to
an azure resource such as Azure function, app service, virtual machine, etc. to acquire a token
without using credentials.
*/
package managedidentity
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
// AuthResult contains the results of one token acquisition operation.
// For details see https://aka.ms/msal-net-authenticationresult
type AuthResult = base.AuthResult
type TokenSource = base.TokenSource
const (
TokenSourceIdentityProvider = base.TokenSourceIdentityProvider
TokenSourceCache = base.TokenSourceCache
)
const (
// DefaultToIMDS indicates that the source is defaulted to IMDS when no environment variables are set.
DefaultToIMDS Source = "DefaultToIMDS"
AzureArc Source = "AzureArc"
ServiceFabric Source = "ServiceFabric"
CloudShell Source = "CloudShell"
AzureML Source = "AzureML"
AppService Source = "AppService"
// General request query parameter names
metaHTTPHeaderName = "Metadata"
apiVersionQueryParameterName = "api-version"
resourceQueryParameterName = "resource"
wwwAuthenticateHeaderName = "www-authenticate"
// UAMI query parameter name
miQueryParameterClientId = "client_id"
miQueryParameterObjectId = "object_id"
miQueryParameterPrincipalId = "principal_id"
miQueryParameterResourceIdIMDS = "msi_res_id"
miQueryParameterResourceId = "mi_res_id"
// IMDS
imdsDefaultEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
imdsAPIVersion = "2018-02-01"
systemAssignedManagedIdentity = "system_assigned_managed_identity"
// Azure Arc
azureArcEndpoint = "http://127.0.0.1:40342/metadata/identity/oauth2/token"
azureArcAPIVersion = "2020-06-01"
azureArcFileExtension = ".key"
azureArcMaxFileSizeBytes int64 = 4096
linuxTokenPath = "/var/opt/azcmagent/tokens" // #nosec G101
linuxHimdsPath = "/opt/azcmagent/bin/himds"
azureConnectedMachine = "AzureConnectedMachineAgent"
himdsExecutableName = "himds.exe"
tokenName = "Tokens"
// App Service
appServiceAPIVersion = "2019-08-01"
// AzureML
azureMLAPIVersion = "2017-09-01"
// Service Fabric
serviceFabricAPIVersion = "2019-07-01-preview"
// Environment Variables
identityEndpointEnvVar = "IDENTITY_ENDPOINT"
identityHeaderEnvVar = "IDENTITY_HEADER"
azurePodIdentityAuthorityHostEnvVar = "AZURE_POD_IDENTITY_AUTHORITY_HOST"
imdsEndVar = "IMDS_ENDPOINT"
msiEndpointEnvVar = "MSI_ENDPOINT"
msiSecretEnvVar = "MSI_SECRET"
identityServerThumbprintEnvVar = "IDENTITY_SERVER_THUMBPRINT"
defaultRetryCount = 3
)
var retryCodesForIMDS = []int{
http.StatusNotFound, // 404
http.StatusGone, // 410
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusNotImplemented, // 501
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
http.StatusHTTPVersionNotSupported, // 505
http.StatusVariantAlsoNegotiates, // 506
http.StatusInsufficientStorage, // 507
http.StatusLoopDetected, // 508
http.StatusNotExtended, // 510
http.StatusNetworkAuthenticationRequired, // 511
}
var retryStatusCodes = []int{
http.StatusRequestTimeout, // 408
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
}
var getAzureArcPlatformPath = func(platform string) string {
switch platform {
case "windows":
return filepath.Join(os.Getenv("ProgramData"), azureConnectedMachine, tokenName)
case "linux":
return linuxTokenPath
default:
return ""
}
}
var getAzureArcHimdsFilePath = func(platform string) string {
switch platform {
case "windows":
return filepath.Join(os.Getenv("ProgramData"), azureConnectedMachine, himdsExecutableName)
case "linux":
return linuxHimdsPath
default:
return ""
}
}
type Source string
type ID interface {
value() string
}
type systemAssignedValue string // its private for a reason to make the input consistent.
type UserAssignedClientID string
type UserAssignedObjectID string
type UserAssignedResourceID string
func (s systemAssignedValue) value() string { return string(s) }
func (c UserAssignedClientID) value() string { return string(c) }
func (o UserAssignedObjectID) value() string { return string(o) }
func (r UserAssignedResourceID) value() string { return string(r) }
func SystemAssigned() ID {
return systemAssignedValue(systemAssignedManagedIdentity)
}
// cache never uses the client because instance discovery is always disabled.
var cacheManager *storage.Manager = storage.New(nil)
type Client struct {
httpClient ops.HTTPClient
miType ID
source Source
authParams authority.AuthParams
retryPolicyEnabled bool
canRefresh *atomic.Value
}
type AcquireTokenOptions struct {
claims string
}
type ClientOption func(*Client)
type AcquireTokenOption func(o *AcquireTokenOptions)
// WithClaims sets additional claims to request for the token, such as those required by token revocation or conditional access policies.
// Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
func WithClaims(claims string) AcquireTokenOption {
return func(o *AcquireTokenOptions) {
o.claims = claims
}
}
// WithHTTPClient allows for a custom HTTP client to be set.
func WithHTTPClient(httpClient ops.HTTPClient) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
func WithRetryPolicyDisabled() ClientOption {
return func(c *Client) {
c.retryPolicyEnabled = false
}
}
// Client to be used to acquire tokens for managed identity.
// ID: [SystemAssigned], [UserAssignedClientID], [UserAssignedResourceID], [UserAssignedObjectID]
//
// Options: [WithHTTPClient]
func New(id ID, options ...ClientOption) (Client, error) {
source, err := GetSource()
if err != nil {
return Client{}, err
}
// Check for user-assigned restrictions based on the source
switch source {
case AzureArc:
switch id.(type) {
case UserAssignedClientID, UserAssignedResourceID, UserAssignedObjectID:
return Client{}, errors.New("Azure Arc doesn't support user-assigned managed identities")
}
case AzureML:
switch id.(type) {
case UserAssignedObjectID, UserAssignedResourceID:
return Client{}, errors.New("Azure ML supports specifying a user-assigned managed identity by client ID only")
}
case CloudShell:
switch id.(type) {
case UserAssignedClientID, UserAssignedResourceID, UserAssignedObjectID:
return Client{}, errors.New("Cloud Shell doesn't support user-assigned managed identities")
}
case ServiceFabric:
switch id.(type) {
case UserAssignedClientID, UserAssignedResourceID, UserAssignedObjectID:
return Client{}, errors.New("Service Fabric API doesn't support specifying a user-assigned identity. The identity is determined by cluster resource configuration. See https://aka.ms/servicefabricmi")
}
}
switch t := id.(type) {
case UserAssignedClientID:
if len(string(t)) == 0 {
return Client{}, fmt.Errorf("empty %T", t)
}
case UserAssignedResourceID:
if len(string(t)) == 0 {
return Client{}, fmt.Errorf("empty %T", t)
}
case UserAssignedObjectID:
if len(string(t)) == 0 {
return Client{}, fmt.Errorf("empty %T", t)
}
case systemAssignedValue:
default:
return Client{}, fmt.Errorf("unsupported type %T", id)
}
zero := atomic.Value{}
zero.Store(false)
client := Client{
miType: id,
httpClient: shared.DefaultClient,
retryPolicyEnabled: true,
source: source,
canRefresh: &zero,
}
for _, option := range options {
option(&client)
}
fakeAuthInfo, err := authority.NewInfoFromAuthorityURI("https://login.microsoftonline.com/managed_identity", false, true)
if err != nil {
return Client{}, err
}
client.authParams = authority.NewAuthParams(client.miType.value(), fakeAuthInfo)
return client, nil
}
// GetSource detects and returns the managed identity source available on the environment.
func GetSource() (Source, error) {
identityEndpoint := os.Getenv(identityEndpointEnvVar)
identityHeader := os.Getenv(identityHeaderEnvVar)
identityServerThumbprint := os.Getenv(identityServerThumbprintEnvVar)
msiEndpoint := os.Getenv(msiEndpointEnvVar)
msiSecret := os.Getenv(msiSecretEnvVar)
imdsEndpoint := os.Getenv(imdsEndVar)
if identityEndpoint != "" && identityHeader != "" {
if identityServerThumbprint != "" {
return ServiceFabric, nil
}
return AppService, nil
} else if msiEndpoint != "" {
if msiSecret != "" {
return AzureML, nil
} else {
return CloudShell, nil
}
} else if isAzureArcEnvironment(identityEndpoint, imdsEndpoint) {
return AzureArc, nil
}
return DefaultToIMDS, nil
}
// This function wraps time.Now() and is used for refreshing the application
// was created to test the function against refreshin
var now = time.Now
// Acquires tokens from the configured managed identity on an azure resource.
//
// Resource: scopes application is requesting access to
// Options: [WithClaims]
func (c Client) AcquireToken(ctx context.Context, resource string, options ...AcquireTokenOption) (AuthResult, error) {
resource = strings.TrimSuffix(resource, "/.default")
o := AcquireTokenOptions{}
for _, option := range options {
option(&o)
}
c.authParams.Scopes = []string{resource}
// ignore cached access tokens when given claims
if o.claims == "" {
stResp, err := cacheManager.Read(ctx, c.authParams)
if err != nil {
return AuthResult{}, err
}
ar, err := base.AuthResultFromStorage(stResp)
if err == nil {
if !stResp.AccessToken.RefreshOn.T.IsZero() && !stResp.AccessToken.RefreshOn.T.After(now()) && c.canRefresh.CompareAndSwap(false, true) {
defer c.canRefresh.Store(false)
if tr, er := c.getToken(ctx, resource); er == nil {
return tr, nil
}
}
ar.AccessToken, err = c.authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
return ar, err
}
}
return c.getToken(ctx, resource)
}
func (c Client) getToken(ctx context.Context, resource string) (AuthResult, error) {
switch c.source {
case AzureArc:
return c.acquireTokenForAzureArc(ctx, resource)
case AzureML:
return c.acquireTokenForAzureML(ctx, resource)
case CloudShell:
return c.acquireTokenForCloudShell(ctx, resource)
case DefaultToIMDS:
return c.acquireTokenForIMDS(ctx, resource)
case AppService:
return c.acquireTokenForAppService(ctx, resource)
case ServiceFabric:
return c.acquireTokenForServiceFabric(ctx, resource)
default:
return AuthResult{}, fmt.Errorf("unsupported source %q", c.source)
}
}
func (c Client) acquireTokenForAppService(ctx context.Context, resource string) (AuthResult, error) {
req, err := createAppServiceAuthRequest(ctx, c.miType, resource)
if err != nil {
return AuthResult{}, err
}
tokenResponse, err := c.getTokenForRequest(req, resource)
if err != nil {
return AuthResult{}, err
}
return authResultFromToken(c.authParams, tokenResponse)
}
func (c Client) acquireTokenForIMDS(ctx context.Context, resource string) (AuthResult, error) {
req, err := createIMDSAuthRequest(ctx, c.miType, resource)
if err != nil {
return AuthResult{}, err
}
tokenResponse, err := c.getTokenForRequest(req, resource)
if err != nil {
return AuthResult{}, err
}
return authResultFromToken(c.authParams, tokenResponse)
}
func (c Client) acquireTokenForCloudShell(ctx context.Context, resource string) (AuthResult, error) {
req, err := createCloudShellAuthRequest(ctx, resource)
if err != nil {
return AuthResult{}, err
}
tokenResponse, err := c.getTokenForRequest(req, resource)
if err != nil {
return AuthResult{}, err
}
return authResultFromToken(c.authParams, tokenResponse)
}
func (c Client) acquireTokenForAzureML(ctx context.Context, resource string) (AuthResult, error) {
req, err := createAzureMLAuthRequest(ctx, c.miType, resource)
if err != nil {
return AuthResult{}, err
}
tokenResponse, err := c.getTokenForRequest(req, resource)
if err != nil {
return AuthResult{}, err
}
return authResultFromToken(c.authParams, tokenResponse)
}
func (c Client) acquireTokenForServiceFabric(ctx context.Context, resource string) (AuthResult, error) {
req, err := createServiceFabricAuthRequest(ctx, resource)
if err != nil {
return AuthResult{}, err
}
tokenResponse, err := c.getTokenForRequest(req, resource)
if err != nil {
return AuthResult{}, err
}
return authResultFromToken(c.authParams, tokenResponse)
}
func (c Client) acquireTokenForAzureArc(ctx context.Context, resource string) (AuthResult, error) {
req, err := createAzureArcAuthRequest(ctx, resource, "")
if err != nil {
return AuthResult{}, err
}
response, err := c.httpClient.Do(req)
if err != nil {
return AuthResult{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusUnauthorized {
return AuthResult{}, fmt.Errorf("expected a 401 response, received %d", response.StatusCode)
}
secret, err := c.getAzureArcSecretKey(response, runtime.GOOS)
if err != nil {
return AuthResult{}, err
}
secondRequest, err := createAzureArcAuthRequest(ctx, resource, string(secret))
if err != nil {
return AuthResult{}, err
}
tokenResponse, err := c.getTokenForRequest(secondRequest, resource)
if err != nil {
return AuthResult{}, err
}
return authResultFromToken(c.authParams, tokenResponse)
}
func authResultFromToken(authParams authority.AuthParams, token accesstokens.TokenResponse) (AuthResult, error) {
if cacheManager == nil {
return AuthResult{}, errors.New("cache instance is nil")
}
account, err := cacheManager.Write(authParams, token)
if err != nil {
return AuthResult{}, err
}
// if refreshOn is not set, set it to half of the time until expiry if expiry is more than 2 hours away
if token.RefreshOn.T.IsZero() {
if lifetime := time.Until(token.ExpiresOn); lifetime > 2*time.Hour {
token.RefreshOn.T = time.Now().Add(lifetime / 2)
}
}
ar, err := base.NewAuthResult(token, account)
if err != nil {
return AuthResult{}, err
}
ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
return ar, err
}
// contains checks if the element is present in the list.
func contains[T comparable](list []T, element T) bool {
for _, v := range list {
if v == element {
return true
}
}
return false
}
// retry performs an HTTP request with retries based on the provided options.
func (c Client) retry(maxRetries int, req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for attempt := 0; attempt < maxRetries; attempt++ {
tryCtx, tryCancel := context.WithTimeout(req.Context(), time.Minute)
defer tryCancel()
if resp != nil && resp.Body != nil {
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
cloneReq := req.Clone(tryCtx)
resp, err = c.httpClient.Do(cloneReq)
retrylist := retryStatusCodes
if c.source == DefaultToIMDS {
retrylist = retryCodesForIMDS
}
if err == nil && !contains(retrylist, resp.StatusCode) {
return resp, nil
}
select {
case <-time.After(time.Second):
case <-req.Context().Done():
err = req.Context().Err()
return resp, err
}
}
return resp, err
}
func (c Client) getTokenForRequest(req *http.Request, resource string) (accesstokens.TokenResponse, error) {
r := accesstokens.TokenResponse{}
var resp *http.Response
var err error
if c.retryPolicyEnabled {
resp, err = c.retry(defaultRetryCount, req)
} else {
resp, err = c.httpClient.Do(req)
}
if err != nil {
return r, err
}
responseBytes, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return r, err
}
switch resp.StatusCode {
case http.StatusOK, http.StatusAccepted:
default:
sd := strings.TrimSpace(string(responseBytes))
if sd != "" {
return r, errors.CallErr{
Req: req,
Resp: resp,
Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s",
req.URL.String(),
req.Method,
resp.StatusCode,
sd),
}
}
return r, errors.CallErr{
Req: req,
Resp: resp,
Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d", req.URL.String(), req.Method, resp.StatusCode),
}
}
err = json.Unmarshal(responseBytes, &r)
if err != nil {
return r, errors.InvalidJsonErr{
Err: fmt.Errorf("error parsing the json error: %s", err),
}
}
r.GrantedScopes.Slice = append(r.GrantedScopes.Slice, resource)
return r, err
}
func createAppServiceAuthRequest(ctx context.Context, id ID, resource string) (*http.Request, error) {
identityEndpoint := os.Getenv(identityEndpointEnvVar)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, identityEndpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-IDENTITY-HEADER", os.Getenv(identityHeaderEnvVar))
q := req.URL.Query()
q.Set("api-version", appServiceAPIVersion)
q.Set("resource", resource)
switch t := id.(type) {
case UserAssignedClientID:
q.Set(miQueryParameterClientId, string(t))
case UserAssignedResourceID:
q.Set(miQueryParameterResourceId, string(t))
case UserAssignedObjectID:
q.Set(miQueryParameterObjectId, string(t))
case systemAssignedValue:
default:
return nil, fmt.Errorf("unsupported type %T", id)
}
req.URL.RawQuery = q.Encode()
return req, nil
}
func createIMDSAuthRequest(ctx context.Context, id ID, resource string) (*http.Request, error) {
msiEndpoint, err := url.Parse(imdsDefaultEndpoint)
if err != nil {
return nil, fmt.Errorf("couldn't parse %q: %s", imdsDefaultEndpoint, err)
}
msiParameters := msiEndpoint.Query()
msiParameters.Set(apiVersionQueryParameterName, imdsAPIVersion)
msiParameters.Set(resourceQueryParameterName, resource)
switch t := id.(type) {
case UserAssignedClientID:
msiParameters.Set(miQueryParameterClientId, string(t))
case UserAssignedResourceID:
msiParameters.Set(miQueryParameterResourceIdIMDS, string(t))
case UserAssignedObjectID:
msiParameters.Set(miQueryParameterObjectId, string(t))
case systemAssignedValue: // not adding anything
default:
return nil, fmt.Errorf("unsupported type %T", id)
}
msiEndpoint.RawQuery = msiParameters.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, msiEndpoint.String(), nil)
if err != nil {
return nil, fmt.Errorf("error creating http request %s", err)
}
req.Header.Set(metaHTTPHeaderName, "true")
return req, nil
}
func createAzureArcAuthRequest(ctx context.Context, resource string, key string) (*http.Request, error) {
identityEndpoint := os.Getenv(identityEndpointEnvVar)
if identityEndpoint == "" {
identityEndpoint = azureArcEndpoint
}
msiEndpoint, parseErr := url.Parse(identityEndpoint)
if parseErr != nil {
return nil, fmt.Errorf("couldn't parse %q: %s", identityEndpoint, parseErr)
}
msiParameters := msiEndpoint.Query()
msiParameters.Set(apiVersionQueryParameterName, azureArcAPIVersion)
msiParameters.Set(resourceQueryParameterName, resource)
msiEndpoint.RawQuery = msiParameters.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, msiEndpoint.String(), nil)
if err != nil {
return nil, fmt.Errorf("error creating http request %s", err)
}
req.Header.Set(metaHTTPHeaderName, "true")
if key != "" {
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", key))
}
return req, nil
}
func isAzureArcEnvironment(identityEndpoint, imdsEndpoint string) bool {
if identityEndpoint != "" && imdsEndpoint != "" {
return true
}
himdsFilePath := getAzureArcHimdsFilePath(runtime.GOOS)
if himdsFilePath != "" {
if _, err := os.Stat(himdsFilePath); err == nil {
return true
}
}
return false
}
func (c *Client) getAzureArcSecretKey(response *http.Response, platform string) (string, error) {
wwwAuthenticateHeader := response.Header.Get(wwwAuthenticateHeaderName)
if len(wwwAuthenticateHeader) == 0 {
return "", errors.New("response has no www-authenticate header")
}
// check if the platform is supported
expectedSecretFilePath := getAzureArcPlatformPath(platform)
if expectedSecretFilePath == "" {
return "", errors.New("platform not supported, expected linux or windows")
}
parts := strings.Split(wwwAuthenticateHeader, "Basic realm=")
if len(parts) < 2 {
return "", fmt.Errorf("basic realm= not found in the string, instead found: %s", wwwAuthenticateHeader)
}
secretFilePath := parts
// check that the file in the file path is a .key file
fileName := filepath.Base(secretFilePath[1])
if !strings.HasSuffix(fileName, azureArcFileExtension) {
return "", fmt.Errorf("invalid file extension, expected %s, got %s", azureArcFileExtension, filepath.Ext(fileName))
}
// check that file path from header matches the expected file path for the platform
if expectedSecretFilePath != filepath.Dir(secretFilePath[1]) {
return "", fmt.Errorf("invalid file path, expected %s, got %s", expectedSecretFilePath, filepath.Dir(secretFilePath[1]))
}
fileInfo, err := os.Stat(secretFilePath[1])
if err != nil {
return "", fmt.Errorf("failed to get metadata for %s due to error: %s", secretFilePath[1], err)
}
// Throw an error if the secret file's size is greater than 4096 bytes
if s := fileInfo.Size(); s > azureArcMaxFileSizeBytes {
return "", fmt.Errorf("invalid secret file size, expected %d, file size was %d", azureArcMaxFileSizeBytes, s)
}
// Attempt to read the contents of the secret file
secret, err := os.ReadFile(secretFilePath[1])
if err != nil {
return "", fmt.Errorf("failed to read %q due to error: %s", secretFilePath[1], err)
}
return string(secret), nil
}

View file

@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package managedidentity
import (
"context"
"net/http"
"os"
)
func createServiceFabricAuthRequest(ctx context.Context, resource string) (*http.Request, error) {
identityEndpoint := os.Getenv(identityEndpointEnvVar)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, identityEndpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Secret", os.Getenv(identityHeaderEnvVar))
q := req.URL.Query()
q.Set("api-version", serviceFabricAPIVersion)
q.Set("resource", resource)
req.URL.RawQuery = q.Encode()
return req, nil
}

View file

@ -51,6 +51,13 @@ type AuthenticationScheme = authority.AuthenticationScheme
type Account = shared.Account
type TokenSource = base.TokenSource
const (
TokenSourceIdentityProvider = base.TokenSourceIdentityProvider
TokenSourceCache = base.TokenSourceCache
)
var errNoAccount = errors.New("no account was specified with public.WithSilentAccount(), or the specified account is invalid")
// clientOptions configures the Client's behavior.
@ -387,7 +394,7 @@ func (pca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []s
if err != nil {
return AuthResult{}, err
}
return pca.base.AuthResultFromToken(ctx, authParams, token, true)
return pca.base.AuthResultFromToken(ctx, authParams, token)
}
type DeviceCodeResult = accesstokens.DeviceCodeResult
@ -412,7 +419,7 @@ func (d DeviceCode) AuthenticationResult(ctx context.Context) (AuthResult, error
if err != nil {
return AuthResult{}, err
}
return d.client.base.AuthResultFromToken(ctx, d.authParams, token, true)
return d.client.base.AuthResultFromToken(ctx, d.authParams, token)
}
// acquireTokenByDeviceCodeOptions contains optional configuration for AcquireTokenByDeviceCode
@ -687,7 +694,7 @@ func (pca Client) AcquireTokenInteractive(ctx context.Context, scopes []string,
return AuthResult{}, err
}
return pca.base.AuthResultFromToken(ctx, authParams, token, true)
return pca.base.AuthResultFromToken(ctx, authParams, token)
}
type interactiveAuthResult struct {