Skip to content

Commit

Permalink
Add support for multi-tenant authentication (Azure#412)
Browse files Browse the repository at this point in the history
* Add support for multi-tenant authentication

Support for multi-tenant via x-ms-authorization-auxiliary header has
been added for client credentials with secret scenario; this basically
bundles multiple OAuthConfig and ServicePrincipalToken types into
corresponding MultiTenant* types along with a new authorizer that adds
the primary and auxiliary token headers to the reqest.
The authenticaion helpers have been updated to support this scenario; if
environment var AZURE_AUXILIARY_TENANT_IDS is set with a semicolon
delimited list of tenants the multi-tenant codepath will kick in to
create the appropriate authorizer.

* feedback
  • Loading branch information
jhendrixMSFT committed Jun 25, 2019
1 parent bb605b3 commit ef48668
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 27 deletions.
62 changes: 61 additions & 1 deletion autorest/adal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ package adal
// limitations under the License.

import (
"errors"
"fmt"
"net/url"
)

const (
activeDirectoryEndpointTemplate = "%s/oauth2/%s%s"
)

// OAuthConfig represents the endpoints needed
// in OAuth operations
type OAuthConfig struct {
Expand Down Expand Up @@ -60,7 +65,6 @@ func NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, tenantID string, apiV
}
api = fmt.Sprintf("?api-version=%s", *apiVersion)
}
const activeDirectoryEndpointTemplate = "%s/oauth2/%s%s"
u, err := url.Parse(activeDirectoryEndpoint)
if err != nil {
return nil, err
Expand Down Expand Up @@ -89,3 +93,59 @@ func NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, tenantID string, apiV
DeviceCodeEndpoint: *deviceCodeURL,
}, nil
}

// MultiTenantOAuthConfig provides endpoints for primary and aulixiary tenant IDs.
type MultiTenantOAuthConfig interface {
PrimaryTenant() *OAuthConfig
AuxiliaryTenants() []*OAuthConfig
}

// Options contains optional OAuthConfig creation arguments.
type Options struct {
APIVersion string
}

func (c Options) apiVersion() string {
if c.APIVersion != "" {
return fmt.Sprintf("?api-version=%s", c.APIVersion)
}
return "1.0"
}

// NewMultiTenantOAuthConfig creates an object that support multitenant OAuth configuration.
// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/authenticate-multi-tenant for more information.
func NewMultiTenantOAuthConfig(activeDirectoryEndpoint, primaryTenantID string, auxiliaryTenantIDs []string, options Options) (MultiTenantOAuthConfig, error) {
if len(auxiliaryTenantIDs) == 0 || len(auxiliaryTenantIDs) > 3 {
return nil, errors.New("must specify one to three auxiliary tenants")
}
mtCfg := multiTenantOAuthConfig{
cfgs: make([]*OAuthConfig, len(auxiliaryTenantIDs)+1),
}
apiVer := options.apiVersion()
pri, err := NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, primaryTenantID, &apiVer)
if err != nil {
return nil, fmt.Errorf("failed to create OAuthConfig for primary tenant: %v", err)
}
mtCfg.cfgs[0] = pri
for i := range auxiliaryTenantIDs {
aux, err := NewOAuthConfig(activeDirectoryEndpoint, auxiliaryTenantIDs[i])
if err != nil {
return nil, fmt.Errorf("failed to create OAuthConfig for tenant '%s': %v", auxiliaryTenantIDs[i], err)
}
mtCfg.cfgs[i+1] = aux
}
return mtCfg, nil
}

type multiTenantOAuthConfig struct {
// first config in the slice is the primary tenant
cfgs []*OAuthConfig
}

func (m multiTenantOAuthConfig) PrimaryTenant() *OAuthConfig {
return m.cfgs[0]
}

func (m multiTenantOAuthConfig) AuxiliaryTenants() []*OAuthConfig {
return m.cfgs[1:]
}
71 changes: 51 additions & 20 deletions autorest/adal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,81 +15,112 @@ package adal
// limitations under the License.

import (
"fmt"
"testing"
)

func TestNewOAuthConfig(t *testing.T) {
const testActiveDirectoryEndpoint = "https://login.test.com"
const testTenantID = "tenant-id-test"
const TestAuxTenantPrefix = "aux-tenant-test-"

var (
TestAuxTenantIDs = []string{TestAuxTenantPrefix + "0", TestAuxTenantPrefix + "1", TestAuxTenantPrefix + "2"}
)

config, err := NewOAuthConfig(testActiveDirectoryEndpoint, testTenantID)
func TestNewOAuthConfig(t *testing.T) {
config, err := NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID)
if err != nil {
t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err)
}

expected := "https://login.test.com/tenant-id-test/oauth2/authorize?api-version=1.0"
expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize?api-version=1.0", TestTenantID)
if config.AuthorizeEndpoint.String() != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint)
}

expected = "https://login.test.com/tenant-id-test/oauth2/token?api-version=1.0"
expected = fmt.Sprintf("https://login.test.com/%s/oauth2/token?api-version=1.0", TestTenantID)
if config.TokenEndpoint.String() != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint)
}

expected = "https://login.test.com/tenant-id-test/oauth2/devicecode?api-version=1.0"
expected = fmt.Sprintf("https://login.test.com/%s/oauth2/devicecode?api-version=1.0", TestTenantID)
if config.DeviceCodeEndpoint.String() != expected {
t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint)
}
}

func TestNewOAuthConfigWithAPIVersionNil(t *testing.T) {
const testActiveDirectoryEndpoint = "https://login.test.com"
const testTenantID = "tenant-id-test"

config, err := NewOAuthConfigWithAPIVersion(testActiveDirectoryEndpoint, testTenantID, nil)
config, err := NewOAuthConfigWithAPIVersion(TestActiveDirectoryEndpoint, TestTenantID, nil)
if err != nil {
t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err)
}

expected := "https://login.test.com/tenant-id-test/oauth2/authorize"
expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize", TestTenantID)
if config.AuthorizeEndpoint.String() != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint)
}

expected = "https://login.test.com/tenant-id-test/oauth2/token"
expected = fmt.Sprintf("https://login.test.com/%s/oauth2/token", TestTenantID)
if config.TokenEndpoint.String() != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint)
}

expected = "https://login.test.com/tenant-id-test/oauth2/devicecode"
expected = fmt.Sprintf("https://login.test.com/%s/oauth2/devicecode", TestTenantID)
if config.DeviceCodeEndpoint.String() != expected {
t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint)
}
}

func TestNewOAuthConfigWithAPIVersionNotNil(t *testing.T) {
const testActiveDirectoryEndpoint = "https://login.test.com"
const testTenantID = "tenant-id-test"
apiVersion := "2.0"

config, err := NewOAuthConfigWithAPIVersion(testActiveDirectoryEndpoint, testTenantID, &apiVersion)
config, err := NewOAuthConfigWithAPIVersion(TestActiveDirectoryEndpoint, TestTenantID, &apiVersion)
if err != nil {
t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err)
}

expected := "https://login.test.com/tenant-id-test/oauth2/authorize?api-version=2.0"
expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize?api-version=2.0", TestTenantID)
if config.AuthorizeEndpoint.String() != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint)
}

expected = "https://login.test.com/tenant-id-test/oauth2/token?api-version=2.0"
expected = fmt.Sprintf("https://login.test.com/%s/oauth2/token?api-version=2.0", TestTenantID)
if config.TokenEndpoint.String() != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint)
}

expected = "https://login.test.com/tenant-id-test/oauth2/devicecode?api-version=2.0"
expected = fmt.Sprintf("https://login.test.com/%s/oauth2/devicecode?api-version=2.0", TestTenantID)
if config.DeviceCodeEndpoint.String() != expected {
t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint)
}
}

func TestNewMultiTenantOAuthConfig(t *testing.T) {
cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, Options{})
if err != nil {
t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err)
}
expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize?api-version=1.0", TestTenantID)
if ep := cfg.PrimaryTenant().AuthorizeEndpoint.String(); ep != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, ep)
}
aux := cfg.AuxiliaryTenants()
if len(aux) == 0 {
t.Fatal("autorest/adal: unexpected zero-length auxiliary tenants")
}
for i := range aux {
expected := fmt.Sprintf("https://login.test.com/aux-tenant-test-%d/oauth2/authorize?api-version=1.0", i)
if ep := aux[i].AuthorizeEndpoint.String(); ep != expected {
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, ep)
}
}
}

func TestNewMultiTenantOAuthConfigFail(t *testing.T) {
_, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, nil, Options{})
if err == nil {
t.Fatal("autorest/adal: expected non-nil error")
}
_, err = NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, []string{"one", "two", "three", "four"}, Options{})
if err == nil {
t.Fatal("autorest/adal: expected non-nil error")
}
}
70 changes: 70 additions & 0 deletions autorest/adal/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ type OAuthTokenProvider interface {
OAuthToken() string
}

// MultitenantOAuthTokenProvider provides tokens used for multi-tenant authorization.
type MultitenantOAuthTokenProvider interface {
PrimaryOAuthToken() string
AuxiliaryOAuthTokens() []string
}

// TokenRefreshError is an interface used by errors returned during token refresh.
type TokenRefreshError interface {
error
Expand Down Expand Up @@ -983,3 +989,67 @@ func (spt *ServicePrincipalToken) Token() Token {
defer spt.refreshLock.RUnlock()
return spt.inner.Token
}

// MultiTenantServicePrincipalToken contains tokens for multi-tenant authorization.
type MultiTenantServicePrincipalToken struct {
PrimaryToken *ServicePrincipalToken
AuxiliaryTokens []*ServicePrincipalToken
}

// PrimaryOAuthToken returns the primary authorization token.
func (mt *MultiTenantServicePrincipalToken) PrimaryOAuthToken() string {
return mt.PrimaryToken.OAuthToken()
}

// AuxiliaryOAuthTokens returns one to three auxiliary authorization tokens.
func (mt *MultiTenantServicePrincipalToken) AuxiliaryOAuthTokens() []string {
tokens := make([]string, len(mt.AuxiliaryTokens))
for i := range mt.AuxiliaryTokens {
tokens[i] = mt.AuxiliaryTokens[i].OAuthToken()
}
return tokens
}

// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (mt *MultiTenantServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh primary token: %v", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %v", err)
}
}
return nil
}

// NewMultiTenantServicePrincipalToken creates a new MultiTenantServicePrincipalToken with the specified credentials and resource.
func NewMultiTenantServicePrincipalToken(multiTenantCfg MultiTenantOAuthConfig, clientID string, secret string, resource string) (*MultiTenantServicePrincipalToken, error) {
if err := validateStringParam(clientID, "clientID"); err != nil {
return nil, err
}
if err := validateStringParam(secret, "secret"); err != nil {
return nil, err
}
if err := validateStringParam(resource, "resource"); err != nil {
return nil, err
}
auxTenants := multiTenantCfg.AuxiliaryTenants()
m := MultiTenantServicePrincipalToken{
AuxiliaryTokens: make([]*ServicePrincipalToken, len(auxTenants)),
}
primary, err := NewServicePrincipalToken(*multiTenantCfg.PrimaryTenant(), clientID, secret, resource)
if err != nil {
return nil, fmt.Errorf("failed to create SPT for primary tenant: %v", err)
}
m.PrimaryToken = primary
for i := range auxTenants {
aux, err := NewServicePrincipalToken(*auxTenants[i], clientID, secret, resource)
if err != nil {
return nil, fmt.Errorf("failed to create SPT for auxiliary tenant: %v", err)
}
m.AuxiliaryTokens[i] = aux
}
return &m, nil
}
16 changes: 16 additions & 0 deletions autorest/adal/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,22 @@ func TestMarshalInnerToken(t *testing.T) {
}
}

func TestNewMultiTenantServicePrincipalToken(t *testing.T) {
cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, Options{})
if err != nil {
t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err)
}
mt, err := NewMultiTenantServicePrincipalToken(cfg, "clientID", "superSecret", "resource")
if !strings.Contains(mt.PrimaryToken.inner.OauthConfig.AuthorizeEndpoint.String(), TestTenantID) {
t.Fatal("didn't find primary tenant ID in primary SPT")
}
for i := range mt.AuxiliaryTokens {
if ep := mt.AuxiliaryTokens[i].inner.OauthConfig.AuthorizeEndpoint.String(); !strings.Contains(ep, fmt.Sprintf("%s%d", TestAuxTenantPrefix, i)) {
t.Fatalf("didn't find auxiliary tenant ID in token %s", ep)
}
}
}

func newTokenJSON(expiresOn string, resource string) string {
return fmt.Sprintf(`{
"access_token" : "accessToken",
Expand Down
49 changes: 49 additions & 0 deletions autorest/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,52 @@ func (ba *BasicAuthorizer) WithAuthorization() PrepareDecorator {

return NewAPIKeyAuthorizerWithHeaders(headers).WithAuthorization()
}

// MultiTenantServicePrincipalTokenAuthorizer provides authentication across tenants.
type MultiTenantServicePrincipalTokenAuthorizer interface {
WithAuthorization() PrepareDecorator
}

// NewMultiTenantServicePrincipalTokenAuthorizer crates a BearerAuthorizer using the given token provider
func NewMultiTenantServicePrincipalTokenAuthorizer(tp adal.MultitenantOAuthTokenProvider) MultiTenantServicePrincipalTokenAuthorizer {
return &multiTenantSPTAuthorizer{tp: tp}
}

type multiTenantSPTAuthorizer struct {
tp adal.MultitenantOAuthTokenProvider
}

// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header using the
// primary token along with the auxiliary authorization header using the auxiliary tokens.
//
// By default, the token will be automatically refreshed through the Refresher interface.
func (mt multiTenantSPTAuthorizer) WithAuthorization() PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err != nil {
return r, err
}
if refresher, ok := mt.tp.(adal.RefresherWithContext); ok {
err = refresher.EnsureFreshWithContext(r.Context())
if err != nil {
var resp *http.Response
if tokError, ok := err.(adal.TokenRefreshError); ok {
resp = tokError.Response()
}
return r, NewErrorWithError(err, "azure.multiTenantSPTAuthorizer", "WithAuthorization", resp,
"Failed to refresh one or more Tokens for request to %s", r.URL)
}
}
r, err = Prepare(r, WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", mt.tp.PrimaryOAuthToken())))
if err != nil {
return r, err
}
auxTokens := mt.tp.AuxiliaryOAuthTokens()
for i := range auxTokens {
auxTokens[i] = fmt.Sprintf("Bearer %s", auxTokens[i])
}
return Prepare(r, WithHeader(headerAuxAuthorization, strings.Join(auxTokens, "; ")))
})
}
}
Loading

0 comments on commit ef48668

Please sign in to comment.