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)