Skip to content

Commit

Permalink
Add certificate based authentication for the provider (#352)
Browse files Browse the repository at this point in the history
* adding cert based auth

* Update index.md.tmpl for typo

---------

Co-authored-by: Matt Dotson <[email protected]>
  • Loading branch information
mawasile and mattdot authored Jul 11, 2024
1 parent 54450fa commit 2b320f4
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 51 deletions.
52 changes: 51 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ provider "powerplatform" {
```

Additional Resources about OIDC:

* [OpenID Connect authentication with Microsoft Entra ID](https://learn.microsoft.com/entra/architecture/auth-oidc)
* [Configuring OpenID Connect for GitHub and Microsoft Entra ID](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure)

Expand All @@ -95,6 +96,49 @@ The Power Platform provider can use a Service Principal with Client Secret to au
1. [Register your app registration with Power Platform](https://learn.microsoft.com/power-platform/admin/powerplatform-api-create-service-principal#registering-an-admin-management-application)
1. Configure the provider to use a Service Principal with a Client Secret with either environment variables or using Terraform variables

### Authenticating to Power Platfomr using Service Principal and certificate

1. [Create an app registration for the Power Platform Terraform Provider](guides/app_registration.md)
1. [Register your app registration with Power Platform](https://learn.microsoft.com/power-platform/admin/powerplatform-api-create-service-principal#registering-an-admin-management-application)
1. Generate a certificate using openssl or other tools

```bash
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365
```

4. Merge public and private part of the certificate files together

Using linux shell

```bash
cat *.pem > cert+key.pem
```

Using Powershell

```powershell
Get-Content .\cert.pem, .\key.pem | Set-Content cert+key.pem
```

5. Generate pkcs12 file

```bash
openssl pkcs12 -export -out cert.pkcs12 -in cert+key.pem
```

6. Add public part of the certificate (`cert.pem` file) to the app registration
7. Store your key.pem and the password used to generate in a safe place
8. Configure the provider to use certificate with the following code:

```terraform
provider "powerplatform" {
client_id = var.client_id
tenant_id = var.tenant_id
client_certificate_file_path = "${path.cwd}/cert.pkcs12"
client_certificate_password = var.cert_pass
}
```

#### Using Environment Variables

We recomend using Environment Variables to pass the credentials to the provider.
Expand All @@ -104,12 +148,18 @@ We recomend using Environment Variables to pass the credentials to the provider.
| `POWER_PLATFORM_CLIENT_ID` | The service principal client id | |
| `POWER_PLATFORM_CLIENT_SECRET` | The service principal secret | |
| `POWER_PLATFORM_TENANT_ID` | The guid of the tenant | |
| `POWER_PLATFORM_CLOUD` | override for the cloud used (default is `public`) | |
| `POWER_PLATFORM_USE_OIDC` | if set to `true` then OIDC authentication will be used | |
| `POWER_PLATFORM_USE_CLI` | if set to `true` then Azure CLI authentication will be used | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE` | The Base64 format of your certificate that will be used to certificate based authentication | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE_FILE_PATH` | The path to the certificate that will be used to certificate based authentication | |
| `POWER_PLATFORM_CLIENT_CERTIFICATE_PASSWORD` | Password for the provider certificate | |

-> Variables passed into the provider will override the environment variables.

#### Using Terraform Variables

Alternatively, you can configure the provider using variables in your Terraform configuration which can be passed in via [command line parameters](https://developer.hashicorp.com/terraform/language/values/variables#variables-on-the-command-line), [a `*.tfvars` file](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files), or [environment variables](https://developer.hashicorp.com/terraform/language/values/variables#environment-variables). If you choose to use variables, please be sure to [protect sensitive input variables](https://developer.hashicorp.com/terraform/tutorials/configuration-language/sensitive-variables) so that you do not expose your credentials in your Terraform configuration.
Alternatively, you can configure the provider using variables in your Terraform configuration which can be passed in via [command line parameters](https://developer.hashicorp.com/terraform/language/values/variables#variables-on-the-command-line), [a `*.tfvars` file](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files), or [environment variables](https://developer.hashicorp.com/terraform/language/values/variables#environment-variables). If you choose to use variables, please be sure to [protect sensitive input variables](https://developer.hashicorp.com/terraform/tutorials/configuration-language/sensitive-variables) so that you do not expose your credentials in your Terraform configuration.

```terraform
provider "powerplatform" {
Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ require (
google.golang.org/protobuf v1.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
software.sslmate.com/src/go-pkcs12 v0.4.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,5 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
33 changes: 29 additions & 4 deletions internal/powerplatform/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/hashicorp/terraform-plugin-log/tflog"
config "github.com/microsoft/terraform-provider-power-platform/internal/powerplatform/config"
helpers "github.com/microsoft/terraform-provider-power-platform/internal/powerplatform/helpers"
)

type TokenExpiredError struct {
Expand Down Expand Up @@ -59,9 +60,32 @@ func NewAuthBase(config *config.ProviderConfig) *Auth {
}
}

// func (client *Auth) GetAuthority(tenantid string) string {
// return constants.OAUTH_AUTHORITY_URL + tenantid
// }
func (client *Auth) AuthenticateClientCertificate(ctx context.Context, scopes []string) (string, time.Time, error) {

cert, key, err := helpers.ConvertBase64ToCert(client.config.Credentials.ClientCertificateRaw, client.config.Credentials.ClientCertificatePassword)
if err != nil {
return "", time.Time{}, err
}

azureCertCredentials, err := azidentity.NewClientCertificateCredential(
client.config.Credentials.TenantId,
client.config.Credentials.ClientId,
cert,
key,
&azidentity.ClientCertificateCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: client.config.Cloud,
},
},
)
accessToken, err := azureCertCredentials.GetToken(ctx, policy.TokenRequestOptions{
Scopes: scopes,
})
if err != nil {
return "", time.Time{}, err
}
return accessToken.Token, accessToken.ExpiresOn, nil
}

func (client *Auth) AuthenticateUsingCli(ctx context.Context, scopes []string) (string, time.Time, error) {
azureCLICredentials, err := azidentity.NewAzureCLICredential(nil)
Expand Down Expand Up @@ -259,7 +283,8 @@ func (client *Auth) GetTokenForScopes(ctx context.Context, scopes []string) (*st
token, tokenExpiry, err = client.AuthenticateUsingCli(ctx, scopes)
case client.config.Credentials.IsOidcProvided():
token, tokenExpiry, err = client.AuthenticateOIDC(ctx, scopes)

case client.config.Credentials.IsClientCertificateCredentialsProvided():
token, tokenExpiry, err = client.AuthenticateClientCertificate(ctx, scopes)
default:
return nil, errors.New("no credentials provided")
}
Expand Down
39 changes: 23 additions & 16 deletions internal/powerplatform/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,31 @@ type ProviderCredentials struct {
ClientId string
ClientSecret string

ClientCertificatePassword string
ClientCertificateRaw string

OidcRequestToken string
OidcRequestUrl string
OidcToken string
OidcTokenFilePath string
}

func (model *ProviderCredentials) IsClientSecretCredentialsProvided() bool {
return model.ClientId != "" && model.ClientSecret != "" && model.TenantId != ""
}

func (model *ProviderCredentials) IsClientCertificateCredentialsProvided() bool {
return model.ClientCertificateRaw != ""
}

func (model *ProviderCredentials) IsCliProvided() bool {
return model.UseCli
}

func (model *ProviderCredentials) IsOidcProvided() bool {
return model.UseOidc
}

type ProviderCredentialsModel struct {
UseCli types.Bool `tfsdk:"use_cli"`
UseOidc types.Bool `tfsdk:"use_oidc"`
Expand All @@ -49,24 +68,12 @@ type ProviderCredentialsModel struct {
ClientId types.String `tfsdk:"client_id"`
ClientSecret types.String `tfsdk:"client_secret"`

ClientCertificateFilePath types.String `tfsdk:"client_certificate_file_path"`
ClientCertificate types.String `tfsdk:"client_certificate"`
ClientCertificatePassword types.String `tfsdk:"client_certificate_password"`

OidcRequestToken types.String `tfsdk:"oidc_request_token"`
OidcRequestUrl types.String `tfsdk:"oidc_request_url"`
OidcToken types.String `tfsdk:"oidc_token"`
OidcTokenFilePath types.String `tfsdk:"oidc_token_file_path"`
}

// func (model *ProviderCredentials) IsTelemetryOprout() bool {
// return model.TelemetryOptout
// }

func (model *ProviderCredentials) IsClientSecretCredentialsProvided() bool {
return model.ClientId != "" && model.ClientSecret != "" && model.TenantId != ""
}

func (model *ProviderCredentials) IsCliProvided() bool {
return model.UseCli
}

func (model *ProviderCredentials) IsOidcProvided() bool {
return model.UseOidc
}
75 changes: 75 additions & 0 deletions internal/powerplatform/helpers/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package powerplatform_helpers

import (
"crypto"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"

pkcs12 "software.sslmate.com/src/go-pkcs12"
)

func GetCertificateRawFromCertOrFilePath(certificate, certificateFilePath string) (string, error) {
if certificate != "" {
return strings.TrimSpace(certificate), nil
}
if certificateFilePath != "" {
pfx, err := os.ReadFile(certificateFilePath)
if err != nil {
return "", err
}
certAsBase64 := base64.StdEncoding.EncodeToString(pfx)
return strings.TrimSpace(certAsBase64), nil
}
return "", errors.New("either client_certificate base64 or certificate_file_path must be provided")
}

func ConvertBase64ToCert(b64, password string) ([]*x509.Certificate, crypto.PrivateKey, error) {
pfx, err := convertBase64ToByte(b64)
if err != nil {
return nil, nil, err
}

certs, key, err := convertByteToCert(pfx, password)
if err != nil {
return nil, nil, err
}

return certs, key, nil
}

func convertBase64ToByte(b64 string) ([]byte, error) {
if b64 == "" {
return nil, errors.New("got empty base64 certificate data")
}

pfx, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return pfx, fmt.Errorf("could not decode base64 certificate data: %w", err)
}

return pfx, nil
}

func convertByteToCert(certData []byte, password string) ([]*x509.Certificate, crypto.PrivateKey, error) {
var key crypto.PrivateKey

key, cert, _, err := pkcs12.DecodeChain(certData, password)
if err != nil {
return nil, nil, err
}

if cert == nil {
return nil, nil, errors.New("found no certificate")
}

certs := []*x509.Certificate{cert}

return certs, key, nil
}
Loading

0 comments on commit 2b320f4

Please sign in to comment.