From 2fcc202444908278ccd3bfb08f1f2153a7e8d37d Mon Sep 17 00:00:00 2001 From: Alexandros Ntitoras Date: Thu, 14 Nov 2024 14:04:30 +0200 Subject: [PATCH] feat: Add support for OAuth applications In this commit we are adding the CRUD methods and the rotate client secret for for OAuth Applications, the documentation for those API can be found at https://clerk.com/docs/reference/backend-api/tag/OAuth-Applications --- CHANGELOG.md | 4 + oauth_application.go | 25 ++++ oauthapplication/api.go | 45 +++++++ oauthapplication/client.go | 115 ++++++++++++++++ oauthapplication/client_test.go | 226 ++++++++++++++++++++++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 oauth_application.go create mode 100644 oauthapplication/api.go create mode 100644 oauthapplication/client.go create mode 100644 oauthapplication/client_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 584dcda..05fe7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.3.0 + +- Add support for the OAuth Applications API. Added the oauthapplication package for API operations and a clerk.OAuthApplication type. + ## 2.2.0 - Add support for bulk invitation creation with the `invitation.BulkCreate` method. diff --git a/oauth_application.go b/oauth_application.go new file mode 100644 index 0000000..c343216 --- /dev/null +++ b/oauth_application.go @@ -0,0 +1,25 @@ +package clerk + +type OAuthApplication struct { + APIResource + Object string `json:"object"` + ID string `json:"id"` + InstanceID string `json:"instance_id"` + Name string `json:"name"` + ClientID string `json:"client_id"` + Public bool `json:"public"` + Scopes string `json:"scopes"` + CallbackURL string `json:"callback_url"` + AuthorizeURL string `json:"authorize_url"` + TokenFetchURL string `json:"token_fetch_url"` + UserInfoURL string `json:"user_info_url"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + ClientSecret *string `json:"client_secret,omitempty"` +} + +type OAuthApplicationList struct { + APIResource + OAuthApplications []*OAuthApplication `json:"data"` + TotalCount int64 `json:"total_count"` +} diff --git a/oauthapplication/api.go b/oauthapplication/api.go new file mode 100644 index 0000000..bb8bfc5 --- /dev/null +++ b/oauthapplication/api.go @@ -0,0 +1,45 @@ +// Code generated by "gen"; DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. +package oauthapplication + +import ( + "context" + + "github.com/clerk/clerk-sdk-go/v2" +) + +// Get fetches a single OAuth application by its ID. +func Get(ctx context.Context, id string) (*clerk.OAuthApplication, error) { + return getClient().Get(ctx, id) +} + +// List retrieves all OAuth applications. +func List(ctx context.Context, params *ListParams) (*clerk.OAuthApplicationList, error) { + return getClient().List(ctx, params) +} + +// Create creates a new OAuth application with the given parameters. +func Create(ctx context.Context, params *CreateParams) (*clerk.OAuthApplication, error) { + return getClient().Create(ctx, params) +} + +// Update updates an existing OAuth application. +func Update(ctx context.Context, id string, params *UpdateParams) (*clerk.OAuthApplication, error) { + return getClient().Update(ctx, id, params) +} + +// Delete deletes the given OAuth application +func DeleteOAuthApplication(ctx context.Context, id string) (*clerk.DeletedResource, error) { + return getClient().DeleteOAuthApplication(ctx, id) +} + +// RotateClientSecret rotates the OAuth application's client secret +func RotateClientSecret(ctx context.Context, id string) (*clerk.OAuthApplication, error) { + return getClient().RotateClientSecret(ctx, id) +} + +func getClient() *Client { + return &Client{ + Backend: clerk.GetBackend(), + } +} diff --git a/oauthapplication/client.go b/oauthapplication/client.go new file mode 100644 index 0000000..7d4c41b --- /dev/null +++ b/oauthapplication/client.go @@ -0,0 +1,115 @@ +// Package oauthapplication provides the OAuth applications API. +package oauthapplication + +import ( + "context" + "net/http" + "net/url" + + "github.com/clerk/clerk-sdk-go/v2" +) + +//go:generate go run ../cmd/gen/main.go + +const path = "/oauth_applications" + +type Client struct { + Backend clerk.Backend +} + +func NewClient(config *clerk.ClientConfig) *Client { + return &Client{ + Backend: clerk.NewBackend(&config.BackendConfig), + } +} + +// Get fetches a single OAuth application by its ID. +func (c *Client) Get(ctx context.Context, id string) (*clerk.OAuthApplication, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + resource := &clerk.OAuthApplication{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +type ListParams struct { + clerk.APIParams + clerk.ListParams +} + +func (params *ListParams) ToQuery() url.Values { + return params.ListParams.ToQuery() +} + +// List retrieves all OAuth applications. +func (c *Client) List(ctx context.Context, params *ListParams) (*clerk.OAuthApplicationList, error) { + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + list := &clerk.OAuthApplicationList{} + err := c.Backend.Call(ctx, req, list) + return list, err +} + +type CreateParams struct { + clerk.APIParams + Name string `json:"name"` + CallbackURL string `json:"callback_url"` + Scopes string `json:"scopes"` + Public bool `json:"public"` +} + +// Create creates a new OAuth application with the given parameters. +func (c *Client) Create(ctx context.Context, params *CreateParams) (*clerk.OAuthApplication, error) { + req := clerk.NewAPIRequest(http.MethodPost, path) + req.SetParams(params) + authApplication := &clerk.OAuthApplication{} + err := c.Backend.Call(ctx, req, authApplication) + return authApplication, err +} + +type UpdateParams struct { + clerk.APIParams + Name *string `json:"name,omitempty"` + CallbackURL *string `json:"callback_url,omitempty"` + Scopes *string `json:"scopes,omitempty"` +} + +// Update updates an existing OAuth application. +func (c *Client) Update(ctx context.Context, id string, params *UpdateParams) (*clerk.OAuthApplication, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + authApplication := &clerk.OAuthApplication{} + err = c.Backend.Call(ctx, req, authApplication) + return authApplication, err +} + +// Delete deletes the given OAuth application +func (c *Client) DeleteOAuthApplication(ctx context.Context, id string) (*clerk.DeletedResource, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + authApplication := &clerk.DeletedResource{} + err = c.Backend.Call(ctx, req, authApplication) + return authApplication, err +} + +// RotateClientSecret rotates the OAuth application's client secret +func (c *Client) RotateClientSecret(ctx context.Context, id string) (*clerk.OAuthApplication, error) { + path, err := clerk.JoinPath(path, id, "rotate_secret") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + authApplication := &clerk.OAuthApplication{} + err = c.Backend.Call(ctx, req, authApplication) + return authApplication, err +} diff --git a/oauthapplication/client_test.go b/oauthapplication/client_test.go new file mode 100644 index 0000000..a3b576c --- /dev/null +++ b/oauthapplication/client_test.go @@ -0,0 +1,226 @@ +package oauthapplication + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/clerk/clerk-sdk-go/v2" + "github.com/clerk/clerk-sdk-go/v2/clerktest" + "github.com/stretchr/testify/require" +) + +func TestOAuthApplicationClientCreate(t *testing.T) { + t.Parallel() + id := "oauth_app_123" + name := "Test Application" + callbackURL := "https://callback.url" + scopes := "read,write" + public := true + clientSecret := "secret_123" + + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"name":"%s","callback_url":"https://callback.url","scopes":"read,write","public":true}`, name)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s","callback_url":"%s","scopes":"%s","public":%t,"client_secret":"%s"}`, id, name, callbackURL, scopes, public, clientSecret)), + Method: http.MethodPost, + Path: "/v1/oauth_applications", + }, + } + client := NewClient(config) + + params := &CreateParams{ + Name: name, + CallbackURL: callbackURL, + Scopes: scopes, + Public: public, + } + oauthApp, err := client.Create(context.Background(), params) + require.NoError(t, err) + require.Equal(t, id, oauthApp.ID) + require.Equal(t, name, oauthApp.Name) + require.Equal(t, callbackURL, oauthApp.CallbackURL) + require.Equal(t, scopes, oauthApp.Scopes) + require.Equal(t, public, oauthApp.Public) + require.Equal(t, clientSecret, *oauthApp.ClientSecret) +} + +func TestOrganizationClientCreate_Error(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"create-error-code" + }], + "clerk_trace_id":"create-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.Create(context.Background(), &CreateParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "create-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "create-error-code", apiErr.Errors[0].Code) +} + +func TestOAuthApplicationClientGet(t *testing.T) { + t.Parallel() + id := "app_123" + name := "Test Application" + callbackURL := "https://callback.url" + scopes := "read,write" + public := true + + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s","callback_url":"%s","scopes":"%s","public":%t}`, id, name, callbackURL, scopes, public)), + Method: http.MethodGet, + Path: fmt.Sprintf("/v1/oauth_applications/%s", id), + }, + } + + client := NewClient(config) + oauthApp, err := client.Get(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, oauthApp.ID) + require.Equal(t, name, oauthApp.Name) + require.Equal(t, callbackURL, oauthApp.CallbackURL) + require.Equal(t, scopes, oauthApp.Scopes) + require.Equal(t, public, oauthApp.Public) +} + +func TestOAuthApplicationClientUpdate(t *testing.T) { + t.Parallel() + id := "app_123" + updatedName := "Updated Application" + callbackURL := "https://updated.callback.url" + + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"name":"%s","callback_url":"%s"}`, updatedName, callbackURL)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s","callback_url":"%s"}`, id, updatedName, callbackURL)), + Method: http.MethodPatch, + Path: fmt.Sprintf("/v1/oauth_applications/%s", id), + }, + } + + client := NewClient(config) + params := &UpdateParams{ + Name: clerk.String(updatedName), + CallbackURL: clerk.String(callbackURL), + } + oauthApp, err := client.Update(context.Background(), id, params) + require.NoError(t, err) + require.Equal(t, id, oauthApp.ID) + require.Equal(t, updatedName, oauthApp.Name) + require.Equal(t, callbackURL, oauthApp.CallbackURL) +} + +func TestOrganizationClientUpdate_Error(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"update-error-code" + }], + "clerk_trace_id":"update-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.Update(context.Background(), "oauth_123", &UpdateParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "update-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "update-error-code", apiErr.Errors[0].Code) +} + +func TestOAuthApplicationClientDelete(t *testing.T) { + t.Parallel() + id := "oauth_app_123" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","deleted":true}`, id)), + Method: http.MethodDelete, + Path: "/v1/oauth_applications/" + id, + }, + } + client := NewClient(config) + deletedApp, err := client.DeleteOAuthApplication(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, deletedApp.ID) + require.True(t, deletedApp.Deleted) +} + +func TestOAuthApplicationClientList(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{ + "data": [{"id":"oauth_app_123","name":"OAuth App 1"}], + "total_count": 1 + }`), + Method: http.MethodGet, + Path: "/v1/oauth_applications", + Query: &url.Values{ + "limit": []string{"10"}, + "offset": []string{"0"}, + }, + }, + } + client := NewClient(config) + params := &ListParams{} + params.Limit = clerk.Int64(10) + params.Offset = clerk.Int64(0) + appList, err := client.List(context.Background(), params) + require.NoError(t, err) + require.Equal(t, int64(1), appList.TotalCount) + require.Equal(t, "oauth_app_123", appList.OAuthApplications[0].ID) + require.Equal(t, "OAuth App 1", appList.OAuthApplications[0].Name) +} + +func TestOAuthApplicationClientRotateClientSecret(t *testing.T) { + t.Parallel() + id := "oauth_app_123" + newSecret := "new_client_secret" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","client_secret":"%s"}`, id, newSecret)), + Method: http.MethodPost, + Path: "/v1/oauth_applications/" + id + "/rotate_secret", + }, + } + client := NewClient(config) + updatedApp, err := client.RotateClientSecret(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, updatedApp.ID) + require.Equal(t, newSecret, *updatedApp.ClientSecret) +}