Repository: cloudflare/cloudflare-go
Pull request #1500 (2024-02-08 03:42:21)

AUTH-5770 Add support for OIDC SaaS Apps to Access Applications
## Description

https://developers.cloudflare.com/api/operations/access-applications-list-access-applications 

## Has your change been tested?

added unit tests

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x ] My code follows the code style of this project.
- [ x] My change requires a change to the documentation.
- [ x] I have updated the documentation accordingly.
- [ x] I have added tests to cover my changes.
- [ x] All new and existing tests passed.
- [ x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/



Patch:
diff --git a/.changelog/1500.txt b/.changelog/1500.txt
new file mode 100644
index 000000000..af3eba15b
--- /dev/null
+++ b/.changelog/1500.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+access_application: Add support for OIDC SaaS Applications 
+```
diff --git a/access_application.go b/access_application.go
index f61065991..3b5b61f54 100644
--- a/access_application.go
+++ b/access_application.go
@@ -106,17 +106,30 @@ type SAMLAttributeConfig struct {
 }
 
 type SaasApplication struct {
-	AppID              string                `json:"app_id,omitempty"`
+	// Items common to both SAML and OIDC
+	AppID     string     `json:"app_id,omitempty"`
+	UpdatedAt *time.Time `json:"updated_at,omitempty"`
+	CreatedAt *time.Time `json:"created_at,omitempty"`
+	PublicKey string     `json:"public_key,omitempty"`
+	AuthType  string     `json:"auth_type,omitempty"`
+
+	// SAML saas app
 	ConsumerServiceUrl string                `json:"consumer_service_url,omitempty"`
 	SPEntityID         string                `json:"sp_entity_id,omitempty"`
-	PublicKey          string                `json:"public_key,omitempty"`
 	IDPEntityID        string                `json:"idp_entity_id,omitempty"`
 	NameIDFormat       string                `json:"name_id_format,omitempty"`
 	SSOEndpoint        string                `json:"sso_endpoint,omitempty"`
 	DefaultRelayState  string                `json:"default_relay_state,omitempty"`
-	UpdatedAt          *time.Time            `json:"updated_at,omitempty"`
-	CreatedAt          *time.Time            `json:"created_at,omitempty"`
 	CustomAttributes   []SAMLAttributeConfig `json:"custom_attributes,omitempty"`
+
+	// OIDC saas app
+	ClientID         string   `json:"client_id,omitempty"`
+	ClientSecret     string   `json:"client_secret,omitempty"`
+	RedirectURIs     []string `json:"redirect_uris,omitempty"`
+	GrantTypes       []string `json:"grant_types,omitempty"`
+	Scopes           []string `json:"scopes,omitempty"`
+	AppLauncherURL   string   `json:"app_launcher_url,omitempty"`
+	GroupFilterRegex string   `json:"group_filter_regex,omitempty"`
 }
 
 type AccessAppLauncherCustomization struct {


Test patch:
diff --git a/access_application_test.go b/access_application_test.go
index 4bf414b2b..92cf3226f 100644
--- a/access_application_test.go
+++ b/access_application_test.go
@@ -620,7 +620,7 @@ func TestCreatePrivateAccessApplication(t *testing.T) {
 	}
 }
 
-func TestCreateSaasAccessApplications(t *testing.T) {
+func TestCreateSAMLSaasAccessApplications(t *testing.T) {
 	setup()
 	defer teardown()
 
@@ -771,6 +771,121 @@ func TestCreateSaasAccessApplications(t *testing.T) {
 	}
 }
 
+func TestCreateOIDCSaasAccessApplications(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "480f4f69-1a28-4fdd-9240-1ed29f0ac1db",
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"aud": "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+				"name": "Admin OIDC Saas App",
+				"domain": "example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+				"type": "saas",
+				"session_duration": "24h",
+				"allowed_idps": [],
+				"auto_redirect_to_identity": false,
+				"enable_binding_cookie": false,
+				"custom_deny_url": "https://www.example.com",
+				"custom_deny_message": "denied!",
+				"logo_url": "https://www.example.com/example.png",
+				"skip_interstitial": true,
+				"app_launcher_visible": true,
+				"service_auth_401_redirect": true,
+				"custom_non_identity_deny_url": "https://blocked.com",
+				"tags": ["engineers"],
+				"saas_app": {
+					"auth_type": "oidc",
+					"client_id": "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+					"client_secret": "secret",
+					"redirect_uris": ["https://saas.example.com"],
+					"grant_types": ["authorization_code"],
+					"scopes": ["openid", "email", "profile", "groups"],
+					"app_launcher_url": "https://saas.example.com",
+					"group_filter_regex": ".*"
+				}
+			}
+		}
+		`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	fullAccessApplication := AccessApplication{
+		ID:                     "480f4f69-1a28-4fdd-9240-1ed29f0ac1db",
+		Name:                   "Admin OIDC Saas App",
+		Domain:                 "example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+		Type:                   "saas",
+		SessionDuration:        "24h",
+		AUD:                    "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+		AllowedIdps:            []string{},
+		AutoRedirectToIdentity: BoolPtr(false),
+		EnableBindingCookie:    BoolPtr(false),
+		AppLauncherVisible:     BoolPtr(true),
+		ServiceAuth401Redirect: BoolPtr(true),
+		CustomDenyMessage:      "denied!",
+		CustomDenyURL:          "https://www.example.com",
+		LogoURL:                "https://www.example.com/example.png",
+		SkipInterstitial:       BoolPtr(true),
+		SaasApplication: &SaasApplication{
+			AuthType:         "oidc",
+			ClientID:         "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+			ClientSecret:     "secret",
+			RedirectURIs:     []string{"https://saas.example.com"},
+			GrantTypes:       []string{"authorization_code"},
+			Scopes:           []string{"openid", "email", "profile", "groups"},
+			AppLauncherURL:   "https://saas.example.com",
+			GroupFilterRegex: ".*",
+		},
+		CreatedAt:                &createdAt,
+		UpdatedAt:                &updatedAt,
+		CustomNonIdentityDenyURL: "https://blocked.com",
+		Tags:                     []string{"engineers"},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/apps", handler)
+
+	actual, err := client.CreateAccessApplication(context.Background(), AccountIdentifier(testAccountID), CreateAccessApplicationParams{
+		Name: "Admin Saas Site",
+		SaasApplication: &SaasApplication{
+			AuthType:         "oidc",
+			RedirectURIs:     []string{"https://saas.example.com"},
+			AppLauncherURL:   "https://saas.example.com",
+			GroupFilterRegex: ".*",
+		},
+		SessionDuration: "24h",
+	})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, fullAccessApplication, actual)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/access/apps", handler)
+
+	actual, err = client.CreateAccessApplication(context.Background(), ZoneIdentifier(testZoneID), CreateAccessApplicationParams{
+		Name: "Admin Saas Site",
+		SaasApplication: &SaasApplication{
+			AuthType:         "oidc",
+			RedirectURIs:     []string{"https://saas.example.com"},
+			AppLauncherURL:   "https://saas.example.com",
+			GroupFilterRegex: ".*",
+		},
+		SessionDuration: "24h",
+	})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, fullAccessApplication, actual)
+	}
+}
+
 func TestCreateApplicationWithAccessAppLauncherCustomization(t *testing.T) {
 	setup()
 	defer teardown()