Skip to content

Commit

Permalink
Add MSI for App Service / Functions support (Azure#463)
Browse files Browse the repository at this point in the history
* Add MSI for App Service / Functions support

* Add support for App Service MSI

* Make environment var names be constants

* Add tests for GetMSIEndpoint

* Switch environment var names in tests to use constants

* Allow App Service MSI to use IMDS retry logic

* cleanup
  • Loading branch information
mbrancato authored and jhendrixMSFT committed Oct 10, 2019
1 parent d9a171c commit fa2154f
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## v13.1.0

### New Features

- Added support for MSI authentication on Azure App Service and Azure Functions.

## v13.0.2

### Bug Fixes
Expand Down
46 changes: 44 additions & 2 deletions autorest/adal/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"math"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -62,6 +63,12 @@ const (

// the default number of attempts to refresh an MSI authentication token
defaultMaxMSIRefreshAttempts = 5

// asMSIEndpointEnv is the environment variable used to store the endpoint on App Service and Functions
asMSIEndpointEnv = "MSI_ENDPOINT"

// asMSISecretEnv is the environment variable used to store the request secret on App Service and Functions
asMSISecretEnv = "MSI_SECRET"
)

// OAuthTokenProvider is an interface which should be implemented by an access token retriever
Expand Down Expand Up @@ -633,6 +640,31 @@ func GetMSIVMEndpoint() (string, error) {
return msiEndpoint, nil
}

func isAppService() bool {
_, asMSIEndpointEnvExists := os.LookupEnv(asMSIEndpointEnv)
_, asMSISecretEnvExists := os.LookupEnv(asMSISecretEnv)

return asMSIEndpointEnvExists && asMSISecretEnvExists
}

// GetMSIAppServiceEndpoint get the MSI endpoint for App Service and Functions
func GetMSIAppServiceEndpoint() (string, error) {
asMSIEndpoint, asMSIEndpointEnvExists := os.LookupEnv(asMSIEndpointEnv)

if asMSIEndpointEnvExists {
return asMSIEndpoint, nil
}
return "", errors.New("MSI endpoint not found")
}

// GetMSIEndpoint get the appropriate MSI endpoint depending on the runtime environment
func GetMSIEndpoint() (string, error) {
if isAppService() {
return GetMSIAppServiceEndpoint()
}
return GetMSIVMEndpoint()
}

// NewServicePrincipalTokenFromMSI creates a ServicePrincipalToken via the MSI VM Extension.
// It will use the system assigned identity when creating the token.
func NewServicePrincipalTokenFromMSI(msiEndpoint, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
Expand Down Expand Up @@ -665,7 +697,12 @@ func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedI

v := url.Values{}
v.Set("resource", resource)
v.Set("api-version", "2018-02-01")
// App Service MSI currently only supports token API version 2017-09-01
if isAppService() {
v.Set("api-version", "2017-09-01")
} else {
v.Set("api-version", "2018-02-01")
}
if userAssignedID != nil {
v.Set("client_id", *userAssignedID)
}
Expand Down Expand Up @@ -792,7 +829,7 @@ func isIMDS(u url.URL) bool {
if err != nil {
return false
}
return u.Host == imds.Host && u.Path == imds.Path
return (u.Host == imds.Host && u.Path == imds.Path) || isAppService()
}

func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource string) error {
Expand All @@ -801,6 +838,11 @@ func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
}
req.Header.Add("User-Agent", UserAgent())
// Add header when runtime is on App Service or Functions
if isAppService() {
asMSISecret, _ := os.LookupEnv(asMSISecretEnv)
req.Header.Add("Secret", asMSISecret)
}
req = req.WithContext(ctx)
if !isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
v := url.Values{}
Expand Down
72 changes: 72 additions & 0 deletions autorest/adal/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"math/big"
"net/http"
"net/url"
"os"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -739,6 +740,77 @@ func TestGetVMEndpoint(t *testing.T) {
}
}

func TestGetAppServiceEndpoint(t *testing.T) {
const testEndpoint = "http://172.16.1.2:8081/msi/token"
if err := os.Setenv(asMSIEndpointEnv, testEndpoint); err != nil {
t.Fatalf("os.Setenv: %v", err)
}

endpoint, err := GetMSIAppServiceEndpoint()
if err != nil {
t.Fatal("Coudn't get App Service endpoint")
}

if endpoint != testEndpoint {
t.Fatal("Didn't get correct endpoint")
}

if err := os.Unsetenv(asMSIEndpointEnv); err != nil {
t.Fatalf("os.Unsetenv: %v", err)
}
}

func TestGetMSIEndpoint(t *testing.T) {
const (
testEndpoint = "http://172.16.1.2:8081/msi/token"
testSecret = "DEADBEEF-BBBB-AAAA-DDDD-DDD000000DDD"
)

// Test VM well-known endpoint is returned
if err := os.Unsetenv(asMSIEndpointEnv); err != nil {
t.Fatalf("os.Unsetenv: %v", err)
}

if err := os.Unsetenv(asMSISecretEnv); err != nil {
t.Fatalf("os.Unsetenv: %v", err)
}

vmEndpoint, err := GetMSIEndpoint()
if err != nil {
t.Fatal("Coudn't get VM endpoint")
}

if vmEndpoint != msiEndpoint {
t.Fatal("Didn't get correct endpoint")
}

// Test App Service endpoint is returned
if err := os.Setenv(asMSIEndpointEnv, testEndpoint); err != nil {
t.Fatalf("os.Setenv: %v", err)
}

if err := os.Setenv(asMSISecretEnv, testSecret); err != nil {
t.Fatalf("os.Setenv: %v", err)
}

asEndpoint, err := GetMSIEndpoint()
if err != nil {
t.Fatal("Coudn't get App Service endpoint")
}

if asEndpoint != testEndpoint {
t.Fatal("Didn't get correct endpoint")
}

if err := os.Unsetenv(asMSIEndpointEnv); err != nil {
t.Fatalf("os.Unsetenv: %v", err)
}

if err := os.Unsetenv(asMSISecretEnv); err != nil {
t.Fatalf("os.Unsetenv: %v", err)
}
}

func TestMarshalServicePrincipalNoSecret(t *testing.T) {
spt := newServicePrincipalTokenManual()
b, err := json.Marshal(spt)
Expand Down
2 changes: 1 addition & 1 deletion autorest/azure/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ type MSIConfig struct {

// Authorizer gets the authorizer from MSI.
func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) {
msiEndpoint, err := adal.GetMSIVMEndpoint()
msiEndpoint, err := adal.GetMSIEndpoint()
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion autorest/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"runtime"
)

const number = "v13.0.2"
const number = "v13.1.0"

var (
userAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",
Expand Down

0 comments on commit fa2154f

Please sign in to comment.