From 995fa3c1e418a22e102413a1f46eff4530b1eff2 Mon Sep 17 00:00:00 2001 From: Andrew O Hart Date: Wed, 21 Aug 2024 08:30:44 +0100 Subject: [PATCH 1/4] Changes for running --- apps/managedidentity/managedidentity.go | 137 +++++++++++++++++++ apps/managedidentity/managedidentity_test.go | 46 +++++++ apps/tests/devapps/main.go | 4 +- apps/tests/devapps/managedidentity_sample.go | 38 +++++ 4 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 apps/managedidentity/managedidentity.go create mode 100644 apps/managedidentity/managedidentity_test.go create mode 100644 apps/tests/devapps/managedidentity_sample.go diff --git a/apps/managedidentity/managedidentity.go b/apps/managedidentity/managedidentity.go new file mode 100644 index 00000000..3789c47b --- /dev/null +++ b/apps/managedidentity/managedidentity.go @@ -0,0 +1,137 @@ +// 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" + "fmt" + "sync" + + "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base" + "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops" + "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" +) + +const ( + // AzureArc represents the source to acquire token for managed identity is Azure Arc. + AzureArc = 0 + + // DefaultToIMDS indicates that the source is defaulted to IMDS since no environment variables are set. + DefaultToIMDS = 1 +) + +// Client is a client that provides access to Managed Identity token calls. +type Client struct { + AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New(). also may remove from here + cacheAccessorMu *sync.RWMutex + // base ops.HTTPClient + // managedIdentityType Type + // Token *oauth.Client + // pmanager manager // todo : expose the manager from base. + // cacheAccessor cache.ExportReplace +} + +// clientOptions are optional settings for New(). These options are set using various functions +// returning Option calls. +type clientOptions struct { + claims string // bypasses cache, does nothing else + httpClient ops.HTTPClient + // disableInstanceDiscovery bool // always false + // clientId string +} + +type withClaimsOption struct{ Claims string } +type withHTTPClientOption struct{ HttpClient ops.HTTPClient } + +// Option is an optional argument to New(). +type Option interface{ apply(*clientOptions) } +type ClientOption interface{ ClientOption() } +type AcquireTokenOption interface{ AcquireTokenOption() } + +// Source represents the managed identity sources supported. +type Source int + +type systemAssignedValue string + +type ID interface { + value() string +} + +func SystemAssigned() ID { + return systemAssignedValue("") +} + +type ClientID string +type ObjectID string +type ResourceID string + +func (s systemAssignedValue) value() string { return string(s) } +func (c ClientID) value() string { return string(c) } +func (o ObjectID) value() string { return string(o) } +func (r ResourceID) value() string { return string(r) } + +func (w withClaimsOption) AcquireTokenOption() {} +func (w withHTTPClientOption) AcquireTokenOption() {} +func (w withHTTPClientOption) apply(opts *clientOptions) { opts.httpClient = w.HttpClient } + +// WithClaims sets additional claims to request for the token, such as those required by 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 withClaimsOption{Claims: claims} +} + +// WithHTTPClient allows for a custom HTTP client to be set. +func WithHTTPClient(httpClient ops.HTTPClient) Option { + return withHTTPClientOption{HttpClient: httpClient} +} + +// Client to be used to acquire tokens for managed identity. +// ID: [SystemAssigned()], [ClientID("clientID")], [ResourceID("resourceID")], [ObjectID("objectID")] +// +// Options: [WithHTTPClient] +func New(id ID, options ...Option) (Client, error) { + fmt.Println("idType: ", id.value()) + + opts := clientOptions{ + claims: "claims", + } + + for _, option := range options { + option.apply(&opts) + } + + authInfo, err := authority.NewInfoFromAuthorityURI("authorityURI", true, false) + if err != nil { + return Client{}, err + } + + authParams := authority.NewAuthParams(id.value(), authInfo) + client := Client{ // Note: Hey, don't even THINK about making Base into *Base. See "design notes" in public.go and confidential.go + AuthParams: authParams, + cacheAccessorMu: &sync.RWMutex{}, + // manager: storage.New(token), + // pmanager: storage.NewPartitionedManager(token), + } + + return client, err +} + +// Acquires tokens from the configured managed identity on an azure resource. +// +// Resource: scopes application is requesting access to +// Options: [WithClaims] +func (client Client) AcquireToken(context context.Context, resource string, options ...AcquireTokenOption) (base.AuthResult, error) { + return base.AuthResult{}, nil +} + +// Detects and returns the managed identity source available on the environment. +func GetSource() Source { + return DefaultToIMDS +} diff --git a/apps/managedidentity/managedidentity_test.go b/apps/managedidentity/managedidentity_test.go new file mode 100644 index 00000000..4bf34540 --- /dev/null +++ b/apps/managedidentity/managedidentity_test.go @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +package managedidentity + +import ( + "context" + "testing" +) + +func fakeClient(mangedIdentityId ID, options ...Option) (Client, error) { + client, err := New(mangedIdentityId, options...) + + if err != nil { + return Client{}, err + } + + return client, nil +} + +func TestManagedIdentity(t *testing.T) { + client, err := fakeClient(SystemAssigned()) + + if err != nil { + t.Fatal(err) + } + + _, err = client.AcquireToken(context.Background(), "scope", WithClaims("claim")) + + if err == nil { + t.Errorf("TestManagedIdentity: unexpected nil error from TestManagedIdentity") + } +} + +func TestManagedIdentityWithClaims(t *testing.T) { + client, err := fakeClient(ClientID("123")) + + if err != nil { + t.Fatal(err) + } + + _, err = client.AcquireToken(context.Background(), "scope", WithClaims("claim")) + + if err == nil { + t.Errorf("TestManagedIdentityWithClaims: unexpected nil error from TestManagedIdentityWithClaims") + } +} diff --git a/apps/tests/devapps/main.go b/apps/tests/devapps/main.go index 59672fab..027bd6e4 100644 --- a/apps/tests/devapps/main.go +++ b/apps/tests/devapps/main.go @@ -39,6 +39,8 @@ func main() { acquireTokenClientCertificate() // this time the token comes from the cache! - acquireTokenClientCertificate() + // acquireTokenClientCertificate() + } else if exampleType == "7" { + RunManagedIdentity() } } diff --git a/apps/tests/devapps/managedidentity_sample.go b/apps/tests/devapps/managedidentity_sample.go new file mode 100644 index 00000000..337b1128 --- /dev/null +++ b/apps/tests/devapps/managedidentity_sample.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "fmt" + "net/http" + + mi "github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity" +) + +func RunManagedIdentity() { + customHttpClient := &http.Client{} + + miSystemAssigned, error := mi.New(mi.SystemAssigned()) + if error != nil { + fmt.Println(error) + } + + miClientIdAssigned, error := mi.New(mi.ClientID("client id 123"), mi.WithHTTPClient(customHttpClient)) + if error != nil { + fmt.Println(error) + } + + miResourceIdAssigned, error := mi.New(mi.ResourceID("resource id 123")) + if error != nil { + fmt.Println(error) + } + + miObjectIdAssigned, error := mi.New(mi.ObjectID("object id 123")) + if error != nil { + fmt.Println(error) + } + + miSystemAssigned.AcquireToken(context.Background(), "resource", mi.WithClaims("claim")) + miClientIdAssigned.AcquireToken(context.Background(), "resource") + miResourceIdAssigned.AcquireToken(context.Background(), "resource", mi.WithClaims("claim")) + miObjectIdAssigned.AcquireToken(context.Background(), "resource") +} From 0993a3f96efb2b520f4b1cdaae999f2257e9bb68 Mon Sep 17 00:00:00 2001 From: Andrew O Hart Date: Wed, 21 Aug 2024 11:37:26 +0100 Subject: [PATCH 2/4] * Adds .md file for managed identitys public api --- docs/managedidentity_public_api.md | 206 +++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/managedidentity_public_api.md diff --git a/docs/managedidentity_public_api.md b/docs/managedidentity_public_api.md new file mode 100644 index 00000000..b91f8acd --- /dev/null +++ b/docs/managedidentity_public_api.md @@ -0,0 +1,206 @@ + +# Managed Identity Public API Design Specification + +The purpose of this file is to go over the changes required for adding the Managed Identity feature to MSAL GO + +## Public API + +The public API will be quite small. Based on the Java and .NET implementations, there is only 1 exposed method, **acquireTokenForManagedIdentity()** + +```go +// Acquires tokens from the configured managed identity on an azure resource. +// +// Resource: scopes application is requesting access to +// Options: [WithClaims] +func (client Client) AcquireToken(context context.Context, resource string, options ...AcquireTokenOption) (base.AuthResult, error) { + return base.AuthResult{}, nil +} + +// Source represents the managed identity sources supported. +type Source int + +const ( + // AzureArc represents the source to acquire token for managed identity is Azure Arc. + AzureArc = 0 + + // DefaultToIMDS indicates that the source is defaulted to IMDS since no environment variables are set. + DefaultToIMDS = 1 +) + +// Detects and returns the managed identity source available on the environment. +func GetSource() Source { + return DefaultToIMDS +} +``` + +The end user simply needs to create their own instance of Managed Identity Client, i.e **managedIdentity.Client()**, passing in the **ManagedIdentityType** they want to use, and then call the public API. The example below shows creation of different clients for each of the different Managed Identity Types + +```go +import ( + "context" + "fmt" + "net/http" + + mi "github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity" +) + +func RunManagedIdentity() { + customHttpClient := &http.Client{} + + miSystemAssigned, error := mi.New(mi.SystemAssigned()) + if error != nil { + fmt.Println(error) + } + + miClientIdAssigned, error := mi.New(mi.ClientID("client id 123"), mi.WithHTTPClient(customHttpClient)) + if error != nil { + fmt.Println(error) + } + + miResourceIdAssigned, error := mi.New(mi.ResourceID("resource id 123")) + if error != nil { + fmt.Println(error) + } + + miObjectIdAssigned, error := mi.New(mi.ObjectID("object id 123")) + if error != nil { + fmt.Println(error) + } + + miSystemAssigned.AcquireToken(context.Background(), "resource", mi.WithClaims("claim")) + + miClientIdAssigned.AcquireToken(context.Background(), "resource") + + miResourceIdAssigned.AcquireToken(context.Background(), "resource", mi.WithClaims("claim")) + + miObjectIdAssigned.AcquireToken(context.Background(), "resource") +} +``` + +To create a new **ManagedIdentityClient** + +```go +// Client to be used to acquire tokens for managed identity. +// ID: [SystemAssigned()], [ClientID("clientID")], [ResourceID("resourceID")], [ObjectID("objectID")] +// +// Options: [WithHTTPClient] +func New(id ID, options ...Option) (Client, error) { + // implementation details +} +``` + +The options available for passing to the client are + +```go +// WithHTTPClient allows for a custom HTTP client to be set. +func WithHTTPClient(httpClient ops.HTTPClient) Option { + // implementation details +} +``` + +The options available for the request are + +```go +// WithClaims sets additional claims to request for the token, such as those required by 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 { + // implementation details +} +``` + +## Error Handling + +Error handling in GO is different to what we used to in languages like Java or Swift. +There is no concept of ‘exceptions’, instead we just return errors and immediately check if an error was returned and handle it there and then. +Basically, we will return client-side errors like so: + +```go +if err != nil { + return errors.New("Some Managed Identity Error here”) +} +``` + +This will be inside of any client methods that throw errors, using descriptive errors based on the .NET and Java Implementation. These errors will be propagated down the chain and handled when they are received + +For service side errors it works a little differently + +```go +switch reply.StatusCode { + case 200, 201: + default: + sd := strings.TrimSpace(string(data)) + + if sd != "" { + // We probably have the error in the body. + return nil, errors.CallErr { + Req: req, + Resp: reply, + Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s",req.URL.String(), req.Method, reply.StatusCode, sd) + } + } + + return nil, errors.CallErr{ + Req: req, + Resp: reply, + Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d", req.URL.String(), req.Method, reply.StatusCode), + } +} +``` + +In this example, you can see we are returning **errors.CallErr(Req: httpRequest, Resp: httpResponse, Err: error)** + +For the service side errors we have a struct object like this: + +```go +type CallErr struct { + Req *http.Request + // Resp contains response body + Resp *http.Response + Err error +} +``` + +This structure should be followed for future service calls. More information on this implementation can be found [here](https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/ae2db6b72c7010958355f448e99209bd28e76e67/apps/errors/error_design.md#L1) + +## Caching + +Other MSALs have an Enum called **TokenSource** that lets us differentiate between **IdentityProvider**, **Cache** and **Broker**. + +Since GO does not have Brokers, we have created a PR [here](https://github.com/AzureAD/microsoft-authentication-library-for-go/pull/498) that adds a **AuthenticationResultMetadata** class to the **_base.go_** instance of **AuthResult** + +This **AuthenticationResultMetadata** contains the **TokenSource** and **RefreshOn** values, like .NET and Java implementations. The **TokenSource** here does not contain the broker field as it is not something that is planned currently + +```go +type TokenSource int + +const ( + IdentityProvider TokenSource = 0 + Cache = 1 +) + +type AuthResultMetadata struct { + TokenSource TokenSource + RefreshOn time.Time +} +``` + +## FIC Support + +You can review information on FIC [here](https://review.learn.microsoft.com/en-us/identity/microsoft-identity-platform/federated-identity-credentials?branch=main&tabs=dotnet) + +Managed Identity abstracts the complexity of certificates away, by virtue of being hosted on an Azure VM you get access to the services you need i.e. key vault + +Managed Identity is a single tenant. This is an issue as Microsoft has many multi tenanted apps. +FIC solves this by allowing you to declare a trust relationship with an identity provider and application i.e. ‘I trust this GitHub token, if I see this Git Hub token, give me a token for something I want access to i.e. Key Vault’ +So, if you can get a token for Managed Identity you can use it to access the key vault in all tenants + +Right now, we shouldn’t have to do anything. +Currently FIC would be the token for the certificate in **acquireTokenByCredential()**, we would just provide the token for ManagedIdentity instead of using the certificate + +This is a 2-step process: + +1. Get token for Managed Identity. Would be a special token for a specific scope. + +2. Create a confidential client and get a token. Will get an API certificate for the assertion, and use the Managed Identity token instead of the certificate + +All we need to do for now is test FIC with Managed Identity, and update any documentation to go along with it \ No newline at end of file From 1053165379da292bfa6d0e3a7ae9f896b3f4d482 Mon Sep 17 00:00:00 2001 From: Andrew O Hart Date: Wed, 21 Aug 2024 11:45:52 +0100 Subject: [PATCH 3/4] Update docs/managedidentity_public_api.md * Readme suggestion update Co-authored-by: Bogdan Gavril --- docs/managedidentity_public_api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managedidentity_public_api.md b/docs/managedidentity_public_api.md index b91f8acd..5733b6a7 100644 --- a/docs/managedidentity_public_api.md +++ b/docs/managedidentity_public_api.md @@ -112,7 +112,7 @@ func WithClaims(claims string) AcquireTokenOption { Error handling in GO is different to what we used to in languages like Java or Swift. There is no concept of ‘exceptions’, instead we just return errors and immediately check if an error was returned and handle it there and then. -Basically, we will return client-side errors like so: +The SDK will return client-side errors like so: ```go if err != nil { From aadf13d43d5f2aa5367fcb57e7e532770620e9fa Mon Sep 17 00:00:00 2001 From: Andrew O Hart Date: Thu, 22 Aug 2024 11:22:48 +0100 Subject: [PATCH 4/4] Makes DefaultToIMDS the default 0 value --- apps/managedidentity/managedidentity.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/managedidentity/managedidentity.go b/apps/managedidentity/managedidentity.go index 3789c47b..ddbe71b0 100644 --- a/apps/managedidentity/managedidentity.go +++ b/apps/managedidentity/managedidentity.go @@ -20,11 +20,11 @@ import ( ) const ( - // AzureArc represents the source to acquire token for managed identity is Azure Arc. - AzureArc = 0 - // DefaultToIMDS indicates that the source is defaulted to IMDS since no environment variables are set. - DefaultToIMDS = 1 + DefaultToIMDS = 0 + + // AzureArc represents the source to acquire token for managed identity is Azure Arc. + AzureArc = 1 ) // Client is a client that provides access to Managed Identity token calls.