ste/destReauthPolicy.go (93 lines of code) (raw):
package ste
import (
"context"
"errors"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
"github.com/Azure/azure-storage-azcopy/v10/common"
"net/http"
"sync"
"time"
)
/*
RESPONSE Status: 401 Server failed to authenticate the request. Please refer to the information in the www-authenticate header.
Www-Authenticate: Bearer authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize resource_id=https://storage.azure.com
X-Ms-Error-Code: InvalidAuthenticationInfo
<?xml version="1.0" encoding="utf-8"?> <Error><Code>InvalidAuthenticationInfo</Code><Message>Server failed to authenticate the request. Please refer to the information in the www-authenticate header. </Message><AuthenticationErrorDetail>Lifetime validation failed. The token is expired.</AuthenticationErrorDetail>
*/
type destReauthPolicy struct {
cred *common.ScopedAuthenticator
}
var reauthLock *sync.Cond = sync.NewCond(&sync.Mutex{})
func NewDestReauthPolicy(cred *common.ScopedAuthenticator) policy.Policy {
return &destReauthPolicy{cred}
}
type destReauthDebug string
var (
destReauthDebugExecuted destReauthDebug = "executed"
destReauthDebugNoPrompt destReauthDebug = "destReauthNoPrompt"
destReauthDebugCause destReauthDebug = "destReauthCause"
destReauthDebugCauseAuthenticationRequired destReauthDebug = "AuthenticationRequiredError"
destReauthDebugCauseInvalidAuthenticationInfo destReauthDebug = "InvalidAuthenticationInfoError"
)
func (d *destReauthPolicy) Do(req *policy.Request) (*http.Response, error) {
retry:
ctx := req.Raw().Context()
debugCtx := context.WithValue(ctx, destReauthDebugExecuted, true)
clone := req.Clone(ctx)
resp, err := clone.Next() // Initially attempt the request.
if err != nil || resp.StatusCode != http.StatusOK { // But, if we get back an error...
var authReq *azidentity.AuthenticationRequiredError
var respErr = &azcore.ResponseError{}
reauth := false
switch { // Is it an error we can resolve by re-authing?
case errors.As(err, &authReq):
reauth = true
debugCtx = context.WithValue(debugCtx, destReauthDebugCause, destReauthDebugCauseAuthenticationRequired)
case resp.StatusCode == http.StatusUnauthorized:
errors.As(runtime.NewResponseError(resp), &respErr)
reauth = err == nil &&
bloberror.HasCode(respErr, bloberror.InvalidAuthenticationInfo) &&
len(respErr.RawResponse.Header.Values("WWW-Authenticate")) != 0
if reauth {
debugCtx = context.WithValue(debugCtx, destReauthDebugCause, destReauthDebugCauseInvalidAuthenticationInfo)
}
}
if reauth { // If it is, pull the lock if we can, reauth
m := reauthLock.L.(*sync.Mutex)
if m.TryLock() { // Fetch the lock and try until we get auth.
for {
if ctx.Value(destReauthDebugNoPrompt) == nil {
_ = common.GetLifecycleMgr().Prompt("Authentication is required to continue the job. Reauthorize and continue?", common.PromptDetails{
PromptType: common.EPromptType.Reauth(),
ResponseOptions: []common.ResponseOption{
common.EResponseOption.Yes(),
},
})
}
_, err = d.cred.Authenticate(debugCtx, &policy.TokenRequestOptions{
Scopes: []string{},
})
// I (Adele Reed) was initially worried about every case
// Thinking about it further, the worst case is that the job ends automatically, or when the user asks it to end.
// To avoid having to handle every error, we'll catch the cancel case as a way to exit the routine, but otherwise
// we will let it happen, and just retry.
if err == nil {
break
} else {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
select {
case <-ctx.Done(): // If it was us, exit like asked.
return nil, err // If it was us, that's legitimately important.
default: // If it was them, we don't care.
}
} else {
common.GetLifecycleMgr().Info(fmt.Sprintf("Authentication failed, awaiting input to continue: %s", err))
}
time.Sleep(time.Second * 5)
}
}
m.Unlock()
reauthLock.Broadcast()
} else { // Otherwise, wait for a signal that we can try again.
reauthLock.Wait()
}
// Try the request once more
goto retry
} // If it wasn't, we won't retry, and we'll simply return the error.
}
return resp, err
}