Skip to content

Commit

Permalink
refactor: added support for oidc (client assertion)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShocOne committed Aug 5, 2024
1 parent e6f2c84 commit 07c1c2a
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 13 deletions.
102 changes: 102 additions & 0 deletions internal/provider/cloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package provider

import (
"testing"

"github.com/deploymenttheory/terraform-provider-microsoft365/internal/constants"
"github.com/stretchr/testify/assert"
)

func TestSetCloudConstants(t *testing.T) {
testCases := []struct {
name string
cloud string
expectedAuthority string
expectedScope string
expectedServiceRoot string
expectedBetaServiceRoot string
expectedError string
}{
{
name: "Public Cloud",
cloud: "public",
expectedAuthority: constants.PUBLIC_OAUTH_AUTHORITY_URL,
expectedScope: constants.PUBLIC_GRAPH_API_SCOPE,
expectedServiceRoot: constants.PUBLIC_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.PUBLIC_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "DoD Cloud",
cloud: "dod",
expectedAuthority: constants.USDOD_OAUTH_AUTHORITY_URL,
expectedScope: constants.USDOD_GRAPH_API_SCOPE,
expectedServiceRoot: constants.USDOD_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.USDOD_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "GCC Cloud",
cloud: "gcc",
expectedAuthority: constants.USGOV_OAUTH_AUTHORITY_URL,
expectedScope: constants.USGOV_GRAPH_API_SCOPE,
expectedServiceRoot: constants.USGOV_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.USGOV_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "GCC High Cloud",
cloud: "gcchigh",
expectedAuthority: constants.USGOVHIGH_OAUTH_AUTHORITY_URL,
expectedScope: constants.USGOVHIGH_GRAPH_API_SCOPE,
expectedServiceRoot: constants.USGOVHIGH_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.USGOVHIGH_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "China Cloud",
cloud: "china",
expectedAuthority: constants.CHINA_OAUTH_AUTHORITY_URL,
expectedScope: constants.CHINA_GRAPH_API_SCOPE,
expectedServiceRoot: constants.CHINA_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.CHINA_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "EagleX Cloud",
cloud: "ex",
expectedAuthority: constants.EX_OAUTH_AUTHORITY_URL,
expectedScope: constants.EX_GRAPH_API_SCOPE,
expectedServiceRoot: constants.EX_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.EX_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "Secure Cloud (RX)",
cloud: "rx",
expectedAuthority: constants.RX_OAUTH_AUTHORITY_URL,
expectedScope: constants.RX_GRAPH_API_SCOPE,
expectedServiceRoot: constants.RX_GRAPH_API_SERVICE_ROOT,
expectedBetaServiceRoot: constants.RX_GRAPH_BETA_API_SERVICE_ROOT,
},
{
name: "Unsupported Cloud",
cloud: "unsupported",
expectedError: "unsupported microsoft cloud type 'unsupported'",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
authority, scope, serviceRoot, betaServiceRoot, err := setCloudConstants(tc.cloud)

if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
assert.Empty(t, authority)
assert.Empty(t, scope)
assert.Empty(t, serviceRoot)
assert.Empty(t, betaServiceRoot)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedAuthority, authority)
assert.Equal(t, tc.expectedScope, scope)
assert.Equal(t, tc.expectedServiceRoot, serviceRoot)
assert.Equal(t, tc.expectedBetaServiceRoot, betaServiceRoot)
}
})
}
}
28 changes: 28 additions & 0 deletions internal/provider/entra.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,34 @@ func obtainCredential(ctx context.Context, data M365ProviderModel, clientOptions
return azidentity.NewUsernamePasswordCredential(data.TenantID.ValueString(), data.ClientID.ValueString(), username, password, &azidentity.UsernamePasswordCredentialOptions{
ClientOptions: clientOptions,
})
case "client_assertion":
tflog.Debug(ctx, "Obtaining Client Assertion Credential", map[string]interface{}{
"tenant_id": data.TenantID.ValueString(),
"client_id": data.ClientID.ValueString(),
})

var assertion string
if data.ClientAssertion.ValueString() != "" {
assertion = data.ClientAssertion.ValueString()
} else if data.ClientAssertionFile.ValueString() != "" {
content, err := os.ReadFile(data.ClientAssertionFile.ValueString())
if err != nil {
return nil, fmt.Errorf("failed to read client assertion file: %v", err)
}
assertion = string(content)
} else {
return nil, fmt.Errorf("either client_assertion or client_assertion_file must be provided for client assertion authentication")
}
return azidentity.NewClientAssertionCredential(
data.TenantID.ValueString(),
data.ClientID.ValueString(),
func(context.Context) (string, error) {
return assertion, nil
},
&azidentity.ClientAssertionCredentialOptions{
ClientOptions: clientOptions,
})

default:
return nil, fmt.Errorf("unsupported authentication method '%s'", data.AuthMethod.ValueString())
}
Expand Down
74 changes: 61 additions & 13 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type M365ProviderModel struct {
ClientCertificatePassword types.String `tfsdk:"client_certificate_password"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
ClientAssertion types.String `tfsdk:"client_assertion"`
ClientAssertionFile types.String `tfsdk:"client_assertion_file"`
RedirectURL types.String `tfsdk:"redirect_url"`
UseProxy types.Bool `tfsdk:"use_proxy"`
ProxyURL types.String `tfsdk:"proxy_url"`
Expand All @@ -58,9 +60,6 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
Description: "The cloud to use for authentication and Graph / Graph Beta API requests." +
"Default is `public`. Valid values are `public`, `gcc`, `gcchigh`, `china`, `dod`, `ex`, `rx`." +
"Can also be set using the `M365_CLOUD` environment variable.",
MarkdownDescription: "The cloud to use for authentication and Graph / Graph Beta API requests." +
"Default is `public`. Valid values are `public`, `gcc`, `gcchigh`, `china`, `dod`, `ex`, `rx`." +
"Can also be set using the `M365_CLOUD` environment variable.",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("public", "gcc", "gcchigh", "china", "dod", "ex", "rx"),
Expand All @@ -70,9 +69,27 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
Required: true,
Description: "The authentication method to use for the Entra ID application to authenticate the provider. " +
"Options: 'device_code', 'client_secret', 'client_certificate', 'interactive_browser', " +
"'username_password'. Can also be set using the `M365_AUTH_METHOD` environment variable.",
"'username_password', 'client_assertion'. Each method requires different credentials to be provided. " +
"'client_assertion' is typically used with OIDC tokens for secure server-to-server authentication. " +
"Can also be set using the `M365_AUTH_METHOD` environment variable.",
MarkdownDescription: "The authentication method to use for the Entra ID application to authenticate the provider. " +
"Options:\n" +
"- `device_code`: Uses a device code flow for authentication.\n" +
"- `client_secret`: Uses a client ID and secret for authentication.\n" +
"- `client_certificate`: Uses a client certificate for authentication.\n" +
"- `interactive_browser`: Opens a browser for interactive login.\n" +
"- `username_password`: Uses username and password for authentication (not recommended for production).\n" +
"- `client_assertion`: Uses a client assertion (OIDC token) for authentication, suitable for CI/CD and server-to-server scenarios.\n\n" +
"Each method requires different credentials to be provided. Can also be set using the `M365_AUTH_METHOD` environment variable.",
Validators: []validator.String{
stringvalidator.OneOf("device_code", "client_secret", "client_certificate", "interactive_browser", "username_password"),
stringvalidator.OneOf(
"device_code",
"client_secret",
"client_certificate",
"interactive_browser",
"username_password",
"client_assertion",
),
},
},
"tenant_id": schema.StringAttribute{
Expand Down Expand Up @@ -108,7 +125,7 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
"Can also be set using the `M365_CLIENT_SECRET` environment variable.",
},
"client_certificate": schema.StringAttribute{
MarkdownDescription: "The path to the Client Certificate file associated with the Service " +
Description: "The path to the Client Certificate file associated with the Service " +
"Principal for use when authenticating as a Service Principal using a Client Certificate. " +
"Supports PKCS#12 (.pfx or .p12) file format. The file should contain the certificate, " +
"private key, and optionally a certificate chain. " +
Expand All @@ -122,7 +139,7 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
Sensitive: true,
},
"client_certificate_password": schema.StringAttribute{
MarkdownDescription: "The password to decrypt the PKCS#12 (.pfx or .p12) file specified in " +
Description: "The password to decrypt the PKCS#12 (.pfx or .p12) file specified in " +
"'client_certificate_file_path'. This is required if the PKCS#12 file is password-protected. " +
"When the certificate file is created, this password is used to encrypt the private key for " +
"security. It's not related to any Microsoft Entra ID (formerly Azure Active Directory) settings," +
Expand All @@ -143,6 +160,27 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
Description: "The password for username/password authentication. Can also be set using the" +
"`M365_PASSWORD` environment variable.",
},
"client_assertion": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "The client assertion string (OIDC token) for authentication. " +
"This is typically a JSON Web Token (JWT) that represents the identity of the client. " +
"It is used in the client credentials flow with client assertion. " +
"This method is more secure than client secret as the assertion is short-lived. " +
"Commonly used in CI/CD pipelines and server-to-server authentication scenarios. " +
"Can also be set using the `M365_CLIENT_ASSERTION` environment variable. " +
"If both this and `client_assertion_file` are specified, this takes precedence.",
},
"client_assertion_file": schema.StringAttribute{
Optional: true,
Description: "Path to a file containing the client assertion (OIDC token) for authentication. " +
"This file should contain a JSON Web Token (JWT) that represents the identity of the client. " +
"Useful when the assertion is too long to be specified directly or when it's generated externally. " +
"The provider will read this file to obtain the assertion string. " +
"Ensure the file permissions are set appropriately to protect the token. " +
"Can also be set using the `M365_CLIENT_ASSERTION_FILE` environment variable. " +
"If both this and `client_assertion` are specified, `client_assertion` takes precedence.",
},
"redirect_url": schema.StringAttribute{
Optional: true,
Description: "The redirect URL for interactive browser authentication. Can also be set using " +
Expand Down Expand Up @@ -184,15 +222,11 @@ func (p *M365Provider) Schema(ctx context.Context, req provider.SchemaRequest, r
Optional: true,
Description: "Flag to indicate whether to opt out of telemetry. Default is `false`. " +
"Can also be set using the `M365_TELEMETRY_OPTOUT` environment variable.",
MarkdownDescription: "Flag to indicate whether to opt out of telemetry. Default is `false`. " +
"Can also be set using the `M365_TELEMETRY_OPTOUT` environment variable.",
},
"debug_mode": schema.BoolAttribute{
Optional: true,
Description: "Flag to enable debug mode for the provider." +
"Can also be set using the `M365_DEBUG_MODE` environment variable.",
MarkdownDescription: "Flag to enable debug mode for the provider." +
"Can also be set using the `M365_DEBUG_MODE` environment variable.",
},
},
}
Expand Down Expand Up @@ -236,6 +270,8 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ
ClientCertificatePassword: types.StringValue(helpers.EnvDefaultFunc("M365_CLIENT_CERTIFICATE_PASSWORD", config.ClientCertificatePassword.ValueString())),
Username: types.StringValue(helpers.EnvDefaultFunc("M365_USERNAME", config.Username.ValueString())),
Password: types.StringValue(helpers.EnvDefaultFunc("M365_PASSWORD", config.Password.ValueString())),
ClientAssertion: types.StringValue(helpers.EnvDefaultFunc("M365_CLIENT_ASSERTION", config.ClientAssertion.ValueString())),
ClientAssertionFile: types.StringValue(helpers.EnvDefaultFunc("M365_CLIENT_ASSERTION_FILE", config.ClientAssertionFile.ValueString())),
RedirectURL: types.StringValue(helpers.EnvDefaultFunc("M365_REDIRECT_URL", config.RedirectURL.ValueString())),
UseProxy: types.BoolValue(helpers.EnvDefaultFuncBool("M365_USE_PROXY", config.UseProxy.ValueBool())),
ProxyURL: types.StringValue(helpers.EnvDefaultFunc("M365_PROXY_URL", config.ProxyURL.ValueString())),
Expand Down Expand Up @@ -265,6 +301,10 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ
ctx = tflog.SetField(ctx, "password", data.Password.ValueString())
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "password")

ctx = tflog.SetField(ctx, "client_assertion", data.ClientAssertion.ValueString())
ctx = tflog.SetField(ctx, "client_assertion_file", data.ClientAssertionFile.ValueString())
ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`(?i)client_assertion`))

ctx = tflog.SetField(ctx, "tenant_id", data.TenantID.ValueString())
ctx = tflog.SetField(ctx, "client_id", data.ClientID.ValueString())
ctx = tflog.SetField(ctx, "client_secret", data.ClientSecret.ValueString())
Expand Down Expand Up @@ -346,7 +386,11 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ
}

stableAdapter, err := msgraphsdk.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(
authProvider, nil, nil, httpClient)
authProvider,
nil,
nil,
httpClient,
)
if err != nil {
resp.Diagnostics.AddError(
"Failed to create Microsoft Graph Stable SDK Adapter",
Expand All @@ -356,7 +400,11 @@ func (p *M365Provider) Configure(ctx context.Context, req provider.ConfigureRequ
}

betaAdapter, err := msgraphbetasdk.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(
authProvider, nil, nil, httpClient)
authProvider,
nil,
nil,
httpClient,
)
if err != nil {
resp.Diagnostics.AddError(
"Failed to create Microsoft Graph Beta SDK Adapter",
Expand Down

0 comments on commit 07c1c2a

Please sign in to comment.