diff --git a/.changelog/3324.txt b/.changelog/3324.txt
new file mode 100644
index 0000000000..504226a770
--- /dev/null
+++ b/.changelog/3324.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+resource/cloudflare_access_application: Add Hybrid and Implicit flow support to OIDC SaaS Apps
+```
diff --git a/docs/resources/access_application.md b/docs/resources/access_application.md
index 9d1a613452..ab968e65ff 100644
--- a/docs/resources/access_application.md
+++ b/docs/resources/access_application.md
@@ -147,6 +147,7 @@ Optional:
- `default_relay_state` (String) The relay state used if not provided by the identity provider.
- `grant_types` (Set of String) The OIDC flows supported by this application.
- `group_filter_regex` (String) A regex to filter Cloudflare groups returned in ID token and userinfo endpoint.
+- `hybrid_and_implicit_options` (Block List, Max: 1) Hybrid and Implicit Flow options. (see [below for nested schema](#nestedblock--saas_app--hybrid_and_implicit_options))
- `name_id_format` (String) The format of the name identifier sent to the SaaS application.
- `name_id_transform_jsonata` (String) A [JSONata](https://jsonata.org/) expression that transforms an application's user identities into a NameID value for its SAML assertion. This expression should evaluate to a singular string. The output of this expression can override the `name_id_format` setting.
- `redirect_uris` (Set of String) The permitted URL's for Cloudflare to return Authorization codes and Access/ID tokens.
@@ -216,6 +217,15 @@ Optional:
+
+### Nested Schema for `saas_app.hybrid_and_implicit_options`
+
+Optional:
+
+- `return_access_token_from_authorization_endpoint` (Boolean) If true, the authorization endpoint will return an access token.
+- `return_id_token_from_authorization_endpoint` (Boolean) If true, the authorization endpoint will return an id token.
+
+
### Nested Schema for `saas_app.refresh_token_options`
diff --git a/internal/sdkv2provider/resource_cloudflare_access_application_test.go b/internal/sdkv2provider/resource_cloudflare_access_application_test.go
index 4f6d1c7a45..1fac6ba551 100644
--- a/internal/sdkv2provider/resource_cloudflare_access_application_test.go
+++ b/internal/sdkv2provider/resource_cloudflare_access_application_test.go
@@ -542,8 +542,9 @@ func TestAccCloudflareAccessApplication_WithOIDCSaas(t *testing.T) {
resource.TestCheckResourceAttr(name, "saas_app.0.auth_type", "oidc"),
resource.TestCheckResourceAttr(name, "saas_app.0.redirect_uris.#", "1"),
resource.TestCheckResourceAttr(name, "saas_app.0.redirect_uris.0", "https://saas-app.example/sso/oauth2/callback"),
- resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.#", "1"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.#", "2"),
resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.0", "authorization_code"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.1", "hybrid"),
resource.TestCheckResourceAttr(name, "saas_app.0.scopes.#", "4"),
resource.TestCheckResourceAttr(name, "saas_app.0.scopes.0", "email"),
resource.TestCheckResourceAttr(name, "saas_app.0.scopes.1", "groups"),
@@ -558,6 +559,9 @@ func TestAccCloudflareAccessApplication_WithOIDCSaas(t *testing.T) {
resource.TestCheckResourceAttr(name, "saas_app.0.custom_claim.0.scope", "profile"),
resource.TestCheckResourceAttr(name, "saas_app.0.custom_claim.0.required", "true"),
resource.TestCheckResourceAttr(name, "saas_app.0.custom_claim.0.source.0.name", "rank"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.hybrid_and_implicit_options.#", "1"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.hybrid_and_implicit_options.0.return_access_token_from_authorization_endpoint", "true"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.hybrid_and_implicit_options.0.return_id_token_from_authorization_endpoint", "true"),
resource.TestCheckResourceAttrSet(name, "saas_app.0.client_secret"),
resource.TestCheckResourceAttrSet(name, "saas_app.0.public_key"),
),
@@ -581,8 +585,9 @@ func TestAccCloudflareAccessApplication_WithOIDCSaas_Import(t *testing.T) {
resource.TestCheckResourceAttr(name, "saas_app.0.auth_type", "oidc"),
resource.TestCheckResourceAttr(name, "saas_app.0.redirect_uris.#", "1"),
resource.TestCheckResourceAttr(name, "saas_app.0.redirect_uris.0", "https://saas-app.example/sso/oauth2/callback"),
- resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.#", "1"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.#", "2"),
resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.0", "authorization_code"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.grant_types.1", "hybrid"),
resource.TestCheckResourceAttr(name, "saas_app.0.scopes.#", "4"),
resource.TestCheckResourceAttr(name, "saas_app.0.scopes.0", "email"),
resource.TestCheckResourceAttr(name, "saas_app.0.scopes.1", "groups"),
@@ -598,6 +603,9 @@ func TestAccCloudflareAccessApplication_WithOIDCSaas_Import(t *testing.T) {
resource.TestCheckResourceAttr(name, "saas_app.0.custom_claim.0.scope", "profile"),
resource.TestCheckResourceAttr(name, "saas_app.0.custom_claim.0.required", "true"),
resource.TestCheckResourceAttr(name, "saas_app.0.custom_claim.0.source.0.name", "rank"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.hybrid_and_implicit_options.#", "1"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.hybrid_and_implicit_options.0.return_access_token_from_authorization_endpoint", "true"),
+ resource.TestCheckResourceAttr(name, "saas_app.0.hybrid_and_implicit_options.0.return_id_token_from_authorization_endpoint", "true"),
)
resource.Test(t, resource.TestCase{
@@ -1133,7 +1141,7 @@ resource "cloudflare_access_application" "%[1]s" {
saas_app {
auth_type = "oidc"
redirect_uris = ["https://saas-app.example/sso/oauth2/callback"]
- grant_types = ["authorization_code"]
+ grant_types = ["authorization_code", "hybrid"]
scopes = ["openid", "email", "profile", "groups"]
app_launcher_url = "https://saas-app.example/sso/login"
group_filter_regex = ".*"
@@ -1149,6 +1157,11 @@ resource "cloudflare_access_application" "%[1]s" {
name = "rank"
}
}
+
+ hybrid_and_implicit_options {
+ return_id_token_from_authorization_endpoint = true
+ return_access_token_from_authorization_endpoint = true
+ }
}
auto_redirect_to_identity = false
}
diff --git a/internal/sdkv2provider/schema_cloudflare_access_application.go b/internal/sdkv2provider/schema_cloudflare_access_application.go
index 996a4766b5..188694102e 100644
--- a/internal/sdkv2provider/schema_cloudflare_access_application.go
+++ b/internal/sdkv2provider/schema_cloudflare_access_application.go
@@ -286,6 +286,26 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
},
},
},
+ "hybrid_and_implicit_options": {
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Hybrid and Implicit Flow options",
+ MaxItems: 1,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "return_access_token_from_authorization_endpoint": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Description: "If true, the authorization endpoint will return an access token",
+ },
+ "return_id_token_from_authorization_endpoint": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Description: "If true, the authorization endpoint will return an id token",
+ },
+ },
+ },
+ },
// SAML options
"sp_entity_id": {
@@ -867,6 +887,13 @@ func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplicati
claimsAsMap := customClaims.(map[string]interface{})
SaasConfig.CustomClaims = append(SaasConfig.CustomClaims, convertOIDCClaimSchemaToStruct(claimsAsMap))
}
+
+ if _, ok := d.GetOk("saas_app.0.hybrid_and_implicit_options"); ok {
+ SaasConfig.HybridAndImplicitOptions = &cloudflare.AccessApplicationHybridAndImplicitOptions{
+ ReturnAccessTokenFromAuthorizationEndpoint: cloudflare.BoolPtr(d.Get("saas_app.0.hybrid_and_implicit_options.0.return_access_token_from_authorization_endpoint").(bool)),
+ ReturnIDTokenFromAuthorizationEndpoint: cloudflare.BoolPtr(d.Get("saas_app.0.hybrid_and_implicit_options.0.return_id_token_from_authorization_endpoint").(bool)),
+ }
+ }
} else {
SaasConfig.SPEntityID = d.Get("saas_app.0.sp_entity_id").(string)
SaasConfig.ConsumerServiceUrl = d.Get("saas_app.0.consumer_service_url").(string)
@@ -1106,6 +1133,18 @@ func convertOIDCClaimStructToSchema(attr cloudflare.OIDCClaimConfig) map[string]
return m
}
+func convertHybridAndImplicitOptionsStructToSchema(hybridAndImplicitOptions *cloudflare.AccessApplicationHybridAndImplicitOptions) []interface{} {
+ if hybridAndImplicitOptions == nil {
+ return []interface{}{}
+ }
+
+ m := map[string]interface{}{
+ "return_access_token_from_authorization_endpoint": hybridAndImplicitOptions.ReturnAccessTokenFromAuthorizationEndpoint,
+ "return_id_token_from_authorization_endpoint": hybridAndImplicitOptions.ReturnIDTokenFromAuthorizationEndpoint,
+ }
+ return []interface{}{m}
+}
+
func convertSaasStructToSchema(d *schema.ResourceData, app *cloudflare.SaasApplication) []interface{} {
if app == nil {
return []interface{}{}
@@ -1135,6 +1174,10 @@ func convertSaasStructToSchema(d *schema.ResourceData, app *cloudflare.SaasAppli
m["custom_claim"] = customClaims
}
+ if app.HybridAndImplicitOptions != nil {
+ m["hybrid_and_implicit_options"] = convertHybridAndImplicitOptionsStructToSchema(app.HybridAndImplicitOptions)
+ }
+
// client secret is only returned on create, if it is present in the state, preserve it
if client_secret, ok := d.GetOk("saas_app.0.client_secret"); ok {
m["client_secret"] = client_secret.(string)