tokenutil/token_util.go (93 lines of code) (raw):
package tokenutil
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/pkg/errors"
)
const (
// environment variable to override the default msi endpoint
envMsiEndpoint = "MSI_ENDPOINT"
)
// RegistryRefreshToken is the response body from ACR exchange API
type RegistryRefreshToken struct {
RefreshToken string `json:"refresh_token"`
}
// GetRegistryRefreshToken return a Registry token.
// It first gets an ARM token, and makes a POST request to registry/v2 endpoint to
// exchange it for a Registry token. Steps mentioned in detail below
// Authentication: https://github.com/Azure/acr/blob/master/docs/AAD-OAuth.md#authenticating-to-a-registry-with-azure-cli
// Exchange: https://github.com/Azure/acr/blob/master/docs/AAD-OAuth.md#calling-post-oauth2exchange-to-get-an-acr-refresh-token
// Note, we don't need to do token challenge part.
func GetRegistryRefreshToken(registry, resourceID, clientID string) (string, error) {
armToken, err := GetRefreshAuthToken(resourceID, clientID)
if err != nil {
return "", errors.Wrap(err, "unable to get ARM token")
}
client := autorest.NewClientWithUserAgent("azure/acr/tasks")
exchangeURL := fmt.Sprintf("https://%s/oauth2/exchange", registry)
v := url.Values{}
v.Set("grant_type", "access_token")
v.Set("service", registry)
v.Set("access_token", armToken.AccessToken)
req, err := http.NewRequest("POST", exchangeURL, strings.NewReader(v.Encode()))
if err != nil {
return "", errors.Wrap(err, "unable to create the request to get ACR refresh token")
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := client.Do(req)
if err != nil {
return "", errors.Wrap(err, "unable to send the request to get ACR refresh token")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get ACR refresh token, exchange API response code: %s", response.Status)
}
var token RegistryRefreshToken
jsonResponse, err := io.ReadAll(response.Body)
if err != nil {
return "", errors.Wrap(err, "unable to read the response from ACR exchange API")
}
err = json.Unmarshal(jsonResponse, &token)
if err != nil {
return "", errors.Wrapf(err, "unable to parse the response from ACR exchange API: %s", jsonResponse)
}
return token.RefreshToken, nil
}
// GetRefreshAuthToken gets and refreshes an Auth token for the resourceID
func GetRefreshAuthToken(resourceID, clientID string) (*adal.Token, error) {
spToken, err := GetServicePrincipalToken(resourceID, clientID)
if err != nil {
return nil, err
}
// try refresh
if err := spToken.EnsureFresh(); err != nil {
return nil, err
}
token := spToken.Token()
return &token, nil
}
// GetServicePrincipalToken gets ServicePrincipal token
// it is based on github.com/Azure/acr-builder/vendor/github.com/Azure/go-autorest/autorest/azure/auth/auth.go and allows overriding the msi endpont using environment variable
func GetServicePrincipalToken(resourceID, clientID string) (*adal.ServicePrincipalToken, error) {
mc := GetMSIConfig(resourceID, clientID)
// default to the well known endpoint for getting MSI authentications tokens
msiEndpoint := "http://169.254.169.254/metadata/identity/oauth2/token"
// override the default from environment variable
if endpoint := os.Getenv(envMsiEndpoint); endpoint != "" {
msiEndpoint = endpoint
}
var spToken *adal.ServicePrincipalToken
var err error
if mc.ClientID == "" {
spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
}
} else {
spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, mc.Resource, mc.ClientID)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err)
}
}
return spToken, nil
}
// GetMSIConfig gets the MSI Config given resourceID and MSI clientID
func GetMSIConfig(resourceID, clientID string) *auth.MSIConfig {
return &auth.MSIConfig{
Resource: resourceID,
ClientID: clientID,
}
}