Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 46 additions & 13 deletions docs/resources/social_provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ The `provider_type` attribute determines which OAuth2/OIDC integration to use:

~> **Note:** When using `provider_type = "generic"`, you **must** set `issuer_url` to the OIDC issuer URL. The provider uses OIDC discovery to find authorization and token endpoints automatically.

## Apple Sign-In

Apple uses a non-standard authentication flow. Instead of a static `client_secret`, Apple requires:

- **`apple_team_id`** — Your Apple Developer Team ID (e.g., `KP76DQS54M`)
- **`apple_private_key_id`** — The key ID from the Apple Developer portal (e.g., `UX56C66723`)
- **`apple_private_key`** — The private key in PEM format (the contents of your `.p8` file)

Ory uses these to automatically generate the JWT `client_secret` required by Apple's OAuth2 flow. You do **not** need to set `client_secret` when using Apple-specific fields.

Alternatively, you may provide a pre-generated `client_secret` directly if you prefer to manage the JWT yourself.

## Example Usage

```terraform
Expand Down Expand Up @@ -64,13 +76,15 @@ resource "ory_social_provider" "microsoft" {
scope = ["openid", "profile", "email"]
}

# Apple Sign-In
# Apple Sign-In (using Apple-specific credentials)
resource "ory_social_provider" "apple" {
provider_id = "apple"
provider_type = "apple"
client_id = var.apple_client_id
client_secret = var.apple_client_secret
scope = ["email", "name"]
provider_id = "apple"
provider_type = "apple"
client_id = var.apple_service_id
apple_team_id = var.apple_team_id
apple_private_key_id = var.apple_private_key_id
apple_private_key = var.apple_private_key
scope = ["email", "name"]
}

# Generic OIDC Provider with custom claims mapping
Expand Down Expand Up @@ -129,13 +143,25 @@ variable "azure_tenant_id" {
type = string
}

variable "apple_client_id" {
type = string
variable "apple_service_id" {
description = "Apple Service ID (e.g., com.example.auth)"
type = string
}

variable "apple_client_secret" {
type = string
sensitive = true
variable "apple_team_id" {
description = "Apple Developer Team ID"
type = string
}

variable "apple_private_key_id" {
description = "Apple private key ID from the Developer portal"
type = string
}

variable "apple_private_key" {
description = "Apple private key in PEM format (.p8 file contents)"
type = string
sensitive = true
}

variable "sso_client_id" {
Expand Down Expand Up @@ -173,6 +199,7 @@ If not set, the provider uses a default mapper that extracts the email claim.
- **`provider_id` and `provider_type` cannot be changed** after creation. Changing either forces a new resource.
- **`client_secret` is write-only.** The API does not return secrets on read, so Terraform cannot detect external changes to the secret.
- **`tenant` maps to `microsoft_tenant`** in the Ory API. This is only used with `provider_type = "microsoft"`.
- **Apple-specific fields** (`apple_team_id`, `apple_private_key_id`, `apple_private_key`) are only valid with `provider_type = "apple"`. The `apple_private_key` is write-only (not returned by API).
- **Deleting the last provider** resets the entire OIDC configuration to a disabled state with an empty providers array.

## Import
Expand All @@ -183,21 +210,27 @@ Import using the provider ID:
terraform import ory_social_provider.google google
```

The `provider_id` is the unique identifier you chose when creating the provider. After import, you must provide `client_secret` in your configuration since it cannot be read from the API.
The `provider_id` is the unique identifier you chose when creating the provider. After import, you must provide write-only credentials in your configuration since they cannot be read from the API:

- **Non-Apple providers:** Set `client_secret`.
- **Apple providers:** Set either `client_secret` (pre-generated JWT) or all three Apple-specific fields (`apple_team_id`, `apple_private_key_id`, and `apple_private_key`).

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `client_id` (String) OAuth2 client ID from the provider.
- `client_secret` (String, Sensitive) OAuth2 client secret from the provider.
- `provider_id` (String) Unique identifier for the provider (used in callback URLs).
- `provider_type` (String) Provider type (google, github, microsoft, apple, generic, etc.).

### Optional

- `apple_private_key` (String, Sensitive) Apple private key in PEM format (contents of the .p8 file). Required when provider_type is "apple" and client_secret is not set. Ory uses this to generate the JWT client secret automatically.
- `apple_private_key_id` (String) Apple private key ID from the Apple Developer portal (e.g., "UX56C66723"). Required when provider_type is "apple" and client_secret is not set.
- `apple_team_id` (String) Apple Developer Team ID (e.g., "KP76DQS54M"). Required when provider_type is "apple" and client_secret is not set.
- `auth_url` (String) Custom authorization URL (for non-standard providers).
- `client_secret` (String, Sensitive) OAuth2 client secret from the provider. Required for all providers except Apple (where Ory generates the secret from apple_team_id, apple_private_key_id, and apple_private_key).
- `issuer_url` (String) OIDC issuer URL (required for generic providers).
- `mapper_url` (String) Jsonnet mapper URL for claims mapping. Can be a URL or base64-encoded Jsonnet (base64://...). If not set, a default mapper that extracts email from claims will be used.
- `project_id` (String) Project ID. If not set, uses provider's project_id.
Expand Down
36 changes: 25 additions & 11 deletions examples/resources/ory_social_provider/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ resource "ory_social_provider" "microsoft" {
scope = ["openid", "profile", "email"]
}

# Apple Sign-In
# Apple Sign-In (using Apple-specific credentials)
resource "ory_social_provider" "apple" {
provider_id = "apple"
provider_type = "apple"
client_id = var.apple_client_id
client_secret = var.apple_client_secret
scope = ["email", "name"]
provider_id = "apple"
provider_type = "apple"
client_id = var.apple_service_id
apple_team_id = var.apple_team_id
apple_private_key_id = var.apple_private_key_id
apple_private_key = var.apple_private_key
scope = ["email", "name"]
}

# Generic OIDC Provider with custom claims mapping
Expand Down Expand Up @@ -91,13 +93,25 @@ variable "azure_tenant_id" {
type = string
}

variable "apple_client_id" {
type = string
variable "apple_service_id" {
description = "Apple Service ID (e.g., com.example.auth)"
type = string
}

variable "apple_client_secret" {
type = string
sensitive = true
variable "apple_team_id" {
description = "Apple Developer Team ID"
type = string
}

variable "apple_private_key_id" {
description = "Apple private key ID from the Developer portal"
type = string
}

variable "apple_private_key" {
description = "Apple private key in PEM format (.p8 file contents)"
type = string
sensitive = true
}

variable "sso_client_id" {
Expand Down
167 changes: 146 additions & 21 deletions internal/resources/socialprovider/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (
)

var (
_ resource.Resource = &SocialProviderResource{}
_ resource.ResourceWithConfigure = &SocialProviderResource{}
_ resource.ResourceWithImportState = &SocialProviderResource{}
_ resource.Resource = &SocialProviderResource{}
_ resource.ResourceWithConfigure = &SocialProviderResource{}
_ resource.ResourceWithImportState = &SocialProviderResource{}
_ resource.ResourceWithValidateConfig = &SocialProviderResource{}
)

func NewResource() resource.Resource {
Expand All @@ -32,18 +33,21 @@ type SocialProviderResource struct {
}

type SocialProviderResourceModel struct {
ID types.String `tfsdk:"id"`
ProjectID types.String `tfsdk:"project_id"`
ProviderID types.String `tfsdk:"provider_id"`
ProviderType types.String `tfsdk:"provider_type"`
ClientID types.String `tfsdk:"client_id"`
ClientSecret types.String `tfsdk:"client_secret"`
IssuerURL types.String `tfsdk:"issuer_url"`
Scope types.List `tfsdk:"scope"`
MapperURL types.String `tfsdk:"mapper_url"`
AuthURL types.String `tfsdk:"auth_url"`
TokenURL types.String `tfsdk:"token_url"`
Tenant types.String `tfsdk:"tenant"`
ID types.String `tfsdk:"id"`
ProjectID types.String `tfsdk:"project_id"`
ProviderID types.String `tfsdk:"provider_id"`
ProviderType types.String `tfsdk:"provider_type"`
ClientID types.String `tfsdk:"client_id"`
ClientSecret types.String `tfsdk:"client_secret"`
IssuerURL types.String `tfsdk:"issuer_url"`
Scope types.List `tfsdk:"scope"`
MapperURL types.String `tfsdk:"mapper_url"`
AuthURL types.String `tfsdk:"auth_url"`
TokenURL types.String `tfsdk:"token_url"`
Tenant types.String `tfsdk:"tenant"`
AppleTeamID types.String `tfsdk:"apple_team_id"`
ApplePrivateKeyID types.String `tfsdk:"apple_private_key_id"`
ApplePrivateKey types.String `tfsdk:"apple_private_key"`
}

func (r *SocialProviderResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
Expand Down Expand Up @@ -89,8 +93,8 @@ func (r *SocialProviderResource) Schema(ctx context.Context, req resource.Schema
Required: true,
},
"client_secret": schema.StringAttribute{
Description: "OAuth2 client secret from the provider.",
Required: true,
Description: "OAuth2 client secret from the provider. Required for all providers except Apple (where Ory generates the secret from apple_team_id, apple_private_key_id, and apple_private_key).",
Optional: true,
Sensitive: true,
},
"issuer_url": schema.StringAttribute{
Expand Down Expand Up @@ -118,6 +122,19 @@ func (r *SocialProviderResource) Schema(ctx context.Context, req resource.Schema
Description: "Tenant ID (for Microsoft/Azure providers).",
Optional: true,
},
"apple_team_id": schema.StringAttribute{
Description: "Apple Developer Team ID (e.g., \"KP76DQS54M\"). Required when provider_type is \"apple\" and client_secret is not set.",
Optional: true,
},
"apple_private_key_id": schema.StringAttribute{
Description: "Apple private key ID from the Apple Developer portal (e.g., \"UX56C66723\"). Required when provider_type is \"apple\" and client_secret is not set.",
Optional: true,
},
"apple_private_key": schema.StringAttribute{
Description: "Apple private key in PEM format (contents of the .p8 file). Required when provider_type is \"apple\" and client_secret is not set. Ory uses this to generate the JWT client secret automatically.",
Optional: true,
Sensitive: true,
},
},
}
}
Expand All @@ -135,19 +152,103 @@ func (r *SocialProviderResource) Configure(ctx context.Context, req resource.Con
r.client = oryClient
}

func (r *SocialProviderResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var config SocialProviderResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// Ensure provider_type is specified and known, since it is required to build the API payload.
if config.ProviderType.IsNull() {
resp.Diagnostics.AddAttributeError(
path.Root("provider_type"),
"Missing Required Attribute",
"provider_type must be specified.",
)
return
}
if config.ProviderType.IsUnknown() {
// Cannot validate attribute combinations when provider_type is unknown.
return
}

providerType := config.ProviderType.ValueString()

// Treat attributes as "configured" only when they are both non-null and non-unknown.
// This ensures validation aligns with buildProviderConfig, which drops unknown values.
hasClientSecret := !config.ClientSecret.IsNull() && !config.ClientSecret.IsUnknown()
hasAppleTeamID := !config.AppleTeamID.IsNull() && !config.AppleTeamID.IsUnknown()
hasApplePrivateKeyID := !config.ApplePrivateKeyID.IsNull() && !config.ApplePrivateKeyID.IsUnknown()
hasApplePrivateKey := !config.ApplePrivateKey.IsNull() && !config.ApplePrivateKey.IsUnknown()
hasAnyAppleField := hasAppleTeamID || hasApplePrivateKeyID || hasApplePrivateKey
hasAllAppleFields := hasAppleTeamID && hasApplePrivateKeyID && hasApplePrivateKey

// Validate that known values are not empty strings
if hasClientSecret && config.ClientSecret.ValueString() == "" {
resp.Diagnostics.AddAttributeError(path.Root("client_secret"), "Invalid Attribute Value", "client_secret must not be an empty string.")
}
if hasAppleTeamID && config.AppleTeamID.ValueString() == "" {
resp.Diagnostics.AddAttributeError(path.Root("apple_team_id"), "Invalid Attribute Value", "apple_team_id must not be an empty string.")
}
if hasApplePrivateKeyID && config.ApplePrivateKeyID.ValueString() == "" {
resp.Diagnostics.AddAttributeError(path.Root("apple_private_key_id"), "Invalid Attribute Value", "apple_private_key_id must not be an empty string.")
}
if hasApplePrivateKey && config.ApplePrivateKey.ValueString() == "" {
resp.Diagnostics.AddAttributeError(path.Root("apple_private_key"), "Invalid Attribute Value", "apple_private_key must not be an empty string.")
}

if providerType == "apple" {
// Apple: needs either client_secret OR all three Apple-specific fields
if !hasClientSecret && !hasAllAppleFields {
resp.Diagnostics.AddAttributeError(
path.Root("client_secret"),
"Missing Apple Provider Configuration",
"Apple providers require either client_secret or all three Apple-specific attributes: apple_team_id, apple_private_key_id, and apple_private_key.",
)
}
if hasAnyAppleField && !hasAllAppleFields {
resp.Diagnostics.AddAttributeError(
path.Root("apple_team_id"),
"Incomplete Apple Provider Configuration",
"When using Apple-specific attributes, all three must be set: apple_team_id, apple_private_key_id, and apple_private_key.",
)
}
} else {
// Non-Apple: client_secret is required
if !hasClientSecret {
resp.Diagnostics.AddAttributeError(
path.Root("client_secret"),
"Missing Required Attribute",
fmt.Sprintf("client_secret is required for provider_type %q.", providerType),
)
}
// Non-Apple: Apple-specific fields should not be set
if hasAnyAppleField {
resp.Diagnostics.AddAttributeError(
path.Root("provider_type"),
"Invalid Attribute Combination",
"apple_team_id, apple_private_key_id, and apple_private_key are only valid when provider_type is \"apple\".",
)
}
}
}

// defaultMapperURL returns the default Jsonnet mapper for common providers.
// This is a simple mapper that extracts email and subject from the claims.
// The base64-encoded Jsonnet maps claims to identity traits.
const defaultMapperURL = "base64://bG9jYWwgY2xhaW1zID0gc3RkLmV4dFZhcignY2xhaW1zJyk7CnsKICBpZGVudGl0eTogewogICAgdHJhaXRzOiB7CiAgICAgIGVtYWlsOiBjbGFpbXMuZW1haWwsCiAgICB9LAogIH0sCn0="

func (r *SocialProviderResource) buildProviderConfig(ctx context.Context, plan *SocialProviderResourceModel) map[string]interface{} {
config := map[string]interface{}{
"id": plan.ProviderID.ValueString(),
"provider": plan.ProviderType.ValueString(),
"client_id": plan.ClientID.ValueString(),
"client_secret": plan.ClientSecret.ValueString(),
"id": plan.ProviderID.ValueString(),
"provider": plan.ProviderType.ValueString(),
"client_id": plan.ClientID.ValueString(),
}

if !plan.ClientSecret.IsNull() && !plan.ClientSecret.IsUnknown() && plan.ClientSecret.ValueString() != "" {
config["client_secret"] = plan.ClientSecret.ValueString()
}
if !plan.IssuerURL.IsNull() && !plan.IssuerURL.IsUnknown() {
config["issuer_url"] = plan.IssuerURL.ValueString()
}
Expand All @@ -172,6 +273,17 @@ func (r *SocialProviderResource) buildProviderConfig(ctx context.Context, plan *
config["microsoft_tenant"] = plan.Tenant.ValueString()
}

// Apple-specific fields — skip empty strings to avoid sending blank credentials
if !plan.AppleTeamID.IsNull() && !plan.AppleTeamID.IsUnknown() && plan.AppleTeamID.ValueString() != "" {
config["apple_team_id"] = plan.AppleTeamID.ValueString()
}
if !plan.ApplePrivateKeyID.IsNull() && !plan.ApplePrivateKeyID.IsUnknown() && plan.ApplePrivateKeyID.ValueString() != "" {
config["apple_private_key_id"] = plan.ApplePrivateKeyID.ValueString()
}
if !plan.ApplePrivateKey.IsNull() && !plan.ApplePrivateKey.IsUnknown() && plan.ApplePrivateKey.ValueString() != "" {
config["apple_private_key"] = plan.ApplePrivateKey.ValueString()
}

return config
}

Expand Down Expand Up @@ -429,6 +541,19 @@ func (r *SocialProviderResource) Read(ctx context.Context, req resource.ReadRequ
state.Tenant = types.StringValue(tenant)
}

// Read Apple-specific fields, clearing stale state when not returned by the API
if teamID, ok := provider["apple_team_id"].(string); ok && teamID != "" {
state.AppleTeamID = types.StringValue(teamID)
} else {
state.AppleTeamID = types.StringNull()
}
if keyID, ok := provider["apple_private_key_id"].(string); ok && keyID != "" {
state.ApplePrivateKeyID = types.StringValue(keyID)
} else {
state.ApplePrivateKeyID = types.StringNull()
}
// Don't read back apple_private_key for security - it's sensitive

// Always ensure ID and ProjectID are set in state
state.ID = types.StringValue(providerID)
state.ProjectID = types.StringValue(projectID)
Expand Down
Loading