Repository: amp-labs/connectors
Pull request #668 (2024-07-08 23:04:07)

feat: Infer credentials for Provider from JSON/ENV
# Goal

When constructing connector field readers should be automatically infered from the provider information. Catalog has all information that we need.

# Changes

Any credential registry files moved under `credsregistry` package.
Select provider from the catalog that matches provider name from ENV or JSON file. Then create list of fields that are required. Example: oauth2 then for sure clientID+clientSecret. If basic auth then username+password.

The field names in JSON(or ENV vars) will be consistent and hence meet expactations when dealing with any connector. (as of now only DynamicsCRM is adapted).

# Example DynamicsCRM

```
	filePath := credsregistry.LoadPath(providers.DynamicsCRM)
	reader := utils.MustCreateProvCredJSON(filePath, true) // all field readers are automatically added

	conn, err := dynamicscrm.NewConnector(
                // construction of oauth token is no longer repetative
		dynamicscrm.WithClient(ctx, http.DefaultClient, getConfig(reader), reader.GetOauthToken()),
		dynamicscrm.WithWorkspace(reader.Get(credsregistry.Fields.Workspace)),
	)
```


jlimatampersand: @Cobalt0s  I was waiting on upstream PR https://github.com/amp-labs/openapi/pull/15/files which seems to be closed after @laurenzlong  reviewed this. Can you modify this PR to reflect that as well? Feel free to ping me on slack after you modify the PR. 
jlimatampersand: I will remove myself from this reviewer list. You can add me back after modification. 
Cobalt0s: @jlimatampersand, I replaced a `todo` with a comment. See commit: https://github.com/amp-labs/connectors/pull/668/commits/c6ef634660eff80533dda819ee77bc59feb6b245
I think when this change will happen a person removing `ExplicitWorkspaceRequired` will end up reading of what is needed from that part of code.
Cobalt0s: * **#668** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/668?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈
* `main`

This stack of pull requests is managed by Graphite. <a href="https://stacking.dev/?utm_source=stack-comment">Learn more about stacking.</a>
<h2></h2>

 Join @Cobalt0s and the rest of your teammates on <a href="https://graphite.dev?utm-source=stack-comment"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="11px" height="11px"/> <b>Graphite</b></a>
  <!-- Current dependencies on/for this PR: -->
Cobalt0s: Running lint locally and clearing cache by running `golangci-lint cache clean` still succeeds :man_shrugging: 
laurenzlong: @jlimatampersand can you please re-review?
jlimatampersand: Feel free to merge after fixing the line errors. 

Patch:
diff --git a/common/handy/lists.go b/common/handy/lists.go
new file mode 100644
index 00000000..fb2bcda5
--- /dev/null
+++ b/common/handy/lists.go
@@ -0,0 +1,11 @@
+package handy
+
+type Lists[T any] map[string][]T
+
+func (l Lists[T]) Add(kind string, objects ...T) {
+	if _, ok := l[kind]; !ok {
+		l[kind] = make([]T, 0)
+	}
+
+	l[kind] = append(l[kind], objects...)
+}
diff --git a/common/scanning/credscanning/fields.go b/common/scanning/credscanning/fields.go
new file mode 100644
index 00000000..7a915143
--- /dev/null
+++ b/common/scanning/credscanning/fields.go
@@ -0,0 +1,161 @@
+package credscanning
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/common/scanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/iancoleman/strcase"
+)
+
+// Fields is a grouping of constant values that dictate the keys that
+// can be present inside *creds.json file.
+var Fields = struct { // nolint:gochecknoglobals
+	Provider Field
+	// Tokens
+	AccessToken  Field
+	RefreshToken Field
+	Expiry       Field
+	ExpiryFormat Field
+	// Client ID, Secret
+	ClientId     Field
+	ClientSecret Field
+	// Basic Authentication
+	Username Field
+	Password Field
+	// Key
+	ApiKey Field
+	// Catalog variables
+	Workspace Field
+}{
+	Provider: Field{
+		Name:      "provider",
+		PathJSON:  "provider",
+		SuffixENV: "PROVIDER",
+	},
+	AccessToken: Field{
+		Name:      "accessToken",
+		PathJSON:  "accessToken",
+		SuffixENV: "ACCESS_TOKEN",
+	},
+	RefreshToken: Field{
+		Name:      "refreshToken",
+		PathJSON:  "refreshToken",
+		SuffixENV: "REFRESH_TOKEN",
+	},
+	Expiry: Field{
+		Name:      "expiry",
+		PathJSON:  "expiry",
+		SuffixENV: "EXPIRY",
+	},
+	ExpiryFormat: Field{
+		Name:      "expiryFormat",
+		PathJSON:  "expiryFormat",
+		SuffixENV: "EXPIRY_FORMAT",
+	},
+	ClientId: Field{
+		Name:      "clientId",
+		PathJSON:  "clientId",
+		SuffixENV: "CLIENT_ID",
+	},
+	ClientSecret: Field{
+		Name:      "clientSecret",
+		PathJSON:  "clientSecret",
+		SuffixENV: "CLIENT_SECRET",
+	},
+	Username: Field{
+		Name:      "username",
+		PathJSON:  "username",
+		SuffixENV: "USERNAME",
+	},
+	Password: Field{
+		Name:      "password",
+		PathJSON:  "password",
+		SuffixENV: "PASSWORD",
+	},
+	ApiKey: Field{
+		Name:      "apiKey",
+		PathJSON:  "apiKey",
+		SuffixENV: "API_KEY",
+	},
+	Workspace: Field{
+		Name:      "workspace",
+		PathJSON:  "substitutions.workspace",
+		SuffixENV: "WORKSPACE",
+	},
+}
+
+type Field struct {
+	Name      string
+	PathJSON  string
+	SuffixENV string
+}
+
+func (f Field) GetJSONReader(filepath string) *scanning.JSONReader {
+	return &scanning.JSONReader{
+		FilePath: filepath,
+		JSONPath: jsonPathTo(f.PathJSON),
+		KeyName:  f.Name,
+	}
+}
+
+func (f Field) GetENVReader(providerName string) *scanning.EnvReader {
+	return &scanning.EnvReader{
+		EnvName: envNameFor(providerName, f.SuffixENV),
+		KeyName: f.Name,
+	}
+}
+
+func getFields(info providers.ProviderInfo, withAccessToken bool) (handy.Lists[Field], error) {
+	lists := handy.Lists[Field]{}
+	requiredType := "required"
+	optionalType := "optional"
+
+	lists.Add(requiredType, Fields.Provider)
+
+	if withAccessToken {
+		lists.Add(requiredType, Fields.AccessToken)
+		lists.Add(optionalType, Fields.RefreshToken, Fields.Expiry, Fields.ExpiryFormat)
+	}
+
+	switch info.AuthType {
+	case providers.ApiKey:
+		lists.Add(requiredType, Fields.ApiKey)
+	case providers.Basic:
+		lists.Add(requiredType, Fields.Username, Fields.Password)
+	case providers.None:
+	case providers.Oauth2:
+		lists.Add(requiredType, Fields.ClientId, Fields.ClientSecret)
+	default:
+		return nil, ErrProviderInfo
+	}
+
+	options := info.Oauth2Opts
+	if options != nil {
+		// As of now Workspace can only be implied for connectors supporting Oauth2.
+		// The changes extending to all connectors will happen
+		// at later time as indicated by https://github.com/amp-labs/openapi/pull/15.
+		// This field should be a general/universal workspace flag in which ever place it will be under ProviderInfo.
+		if options.ExplicitWorkspaceRequired {
+			lists.Add(requiredType, Fields.Workspace)
+		}
+	}
+
+	return lists, nil
+}
+
+func envNameFor(providerName string, suffix string) string {
+	return fmt.Sprintf("%v_%v", envNameFormat(providerName), suffix)
+}
+
+func jsonPathTo(path string) string {
+	return fmt.Sprintf("$.%v", path)
+}
+
+func envNameFormat(name string) string {
+	name = strcase.ToSnake(name)
+
+	return strings.ToUpper(name)
+}
diff --git a/common/scanning/credscanning/path.go b/common/scanning/credscanning/path.go
new file mode 100644
index 00000000..318aee6f
--- /dev/null
+++ b/common/scanning/credscanning/path.go
@@ -0,0 +1,24 @@
+package credscanning
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/iancoleman/strcase"
+)
+
+// LoadPath will give path to creds.json.
+// For provider called `dynamicsCRM` the file location will either be
+// * value of DYNAMICS_CRM_CRED_FILE env var, or
+// * ./dynamics-crm-creds.json.
+func LoadPath(providerName string) string {
+	filePath := os.Getenv(
+		fmt.Sprintf("%v_CRED_FILE", envNameFormat(providerName)),
+	)
+	if len(filePath) == 0 {
+		filePath = strcase.ToKebab(providerName)
+		filePath = fmt.Sprintf("./%v-creds.json", filePath)
+	}
+
+	return filePath
+}
diff --git a/common/scanning/credscanning/provider.go b/common/scanning/credscanning/provider.go
new file mode 100644
index 00000000..562cb567
--- /dev/null
+++ b/common/scanning/credscanning/provider.go
@@ -0,0 +1,149 @@
+// Package credscanning is a wrapper for scanning package.
+// Its focus is on scanning Credentials required for catalog provider.
+package credscanning
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/amp-labs/connectors/common/handy"
+	"github.com/amp-labs/connectors/common/scanning"
+	"github.com/amp-labs/connectors/providers"
+)
+
+var (
+	ErrProviderNotFound = errors.New("provider not found")
+	ErrProviderInfo     = errors.New("provider info is not understood")
+)
+
+// ProviderCredentials is a collection of values for a provider that come either from JSON or ENV.
+type ProviderCredentials struct {
+	Registry       scanning.Registry
+	ProviderValues map[string]string
+}
+
+// NewJSONProviderCredentials reads JSON fields that must be present for a provider.
+// It performs validation and will tell you fields that are expected for this provider in JSON file.
+//
+// Note: As of right now there is no way to infer if access token must be provided.
+// Therefore, explicitly state via arguments.
+func NewJSONProviderCredentials(
+	filePath string,
+	withAccessToken bool,
+) (*ProviderCredentials, error) {
+	return createProviderCreds(getProviderName(filePath), withAccessToken, filePath)
+}
+
+// NewENVProviderCredentials reads ENV variables associated with a provider.
+func NewENVProviderCredentials(
+	providerName string,
+	withAccessToken bool,
+) (*ProviderCredentials, error) {
+	return createProviderCreds(providerName, withAccessToken, "")
+}
+
+func createProviderCreds(
+	providerName string, withAccessToken bool, filePath string,
+) (*ProviderCredentials, error) {
+	// load provider from catalog to imply fields in JSON or ENV vars
+	catalog, err := providers.ReadCatalog()
+	if err != nil {
+		return nil, errors.Join(err, ErrProviderNotFound)
+	}
+
+	info, ok := catalog[providerName]
+	if !ok {
+		return nil, ErrProviderNotFound
+	}
+
+	fields, err := getFields(info, withAccessToken)
+	if err != nil {
+		return nil, err
+	}
+
+	registry := scanning.NewRegistry()
+	readers := handy.Lists[scanning.Reader]{}
+
+	// add readers for every field
+	for kind, fieldList := range fields {
+		for _, field := range fieldList {
+			reader := selectReader(field, filePath, providerName)
+			readers.Add(kind, reader)
+
+			if err = registry.AddReader(reader); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	r := &ProviderCredentials{
+		Registry:       registry,
+		ProviderValues: make(map[string]string),
+	}
+
+	return r, r.loadValues(readers)
+}
+
+func selectReader(field Field, filePath string, providerName string) scanning.Reader { // nolint:ireturn
+	if len(filePath) != 0 {
+		return field.GetJSONReader(filePath)
+	}
+
+	return field.GetENVReader(providerName)
+}
+
+func (r ProviderCredentials) loadValues(readers handy.Lists[scanning.Reader]) error {
+	// validate JSON file or ENV has all Required variables
+	missingKeys := make([]string, 0)
+
+	for _, reader := range readers["required"] {
+		key, err := reader.Key()
+		if err != nil {
+			return err
+		}
+
+		if value, err := r.Registry.GetString(key); err != nil {
+			missingKeys = append(missingKeys, key)
+		} else {
+			r.ProviderValues[key] = value
+		}
+	}
+
+	if len(missingKeys) != 0 {
+		return fmt.Errorf("%w: %s", scanning.ErrKeyNotFound, strings.Join(missingKeys, ","))
+	}
+
+	// Optional values should be saved too
+	for _, reader := range readers["optional"] {
+		key, err := reader.Key()
+		if err != nil {
+			return err
+		}
+
+		if value, err := r.Registry.GetString(key); err == nil {
+			r.ProviderValues[key] = value
+		}
+	}
+
+	return nil
+}
+
+func (r ProviderCredentials) Get(field Field) string {
+	return r.ProviderValues[field.Name]
+}
+
+func getProviderName(filePath string) string {
+	registry := scanning.NewRegistry()
+
+	reader := Fields.Provider.GetJSONReader(filePath)
+
+	err := registry.AddReader(reader)
+	if err != nil {
+		return ""
+	}
+
+	name, _ := registry.GetString(Fields.Provider.Name)
+
+	return name
+}
diff --git a/common/scanning/credscanning/tokens.go b/common/scanning/credscanning/tokens.go
new file mode 100644
index 00000000..83569e73
--- /dev/null
+++ b/common/scanning/credscanning/tokens.go
@@ -0,0 +1,73 @@
+package credscanning
+
+import (
+	"time"
+
+	"golang.org/x/oauth2"
+)
+
+// GetOauthToken constructs Token group from creds file.
+// Some connectors may implement Refresh tokens, when it happens expiry must be provided alongside.
+// Library shouldn't attempt to refresh tokens if API doesn't support `refresh_token` grant type.
+func (r ProviderCredentials) GetOauthToken() *oauth2.Token {
+	accessToken := r.Get(Fields.AccessToken)
+	refreshToken := r.Get(Fields.RefreshToken)
+
+	if len(refreshToken) == 0 {
+		// we are working without refresh token
+		return &oauth2.Token{
+			TokenType:   "bearer",
+			AccessToken: accessToken,
+		}
+	}
+
+	atExpiry := r.Get(Fields.Expiry)
+	atExpiryTimeFormat := r.Get(Fields.ExpiryFormat)
+
+	expiry := time.Now().Add(-1 * time.Hour) // just pretend it's expired already, whatever, it'll fetch a new one.
+
+	if len(atExpiry) != 0 && len(atExpiryTimeFormat) != 0 {
+		// refresh token was given with expiry directive, if the value is valid use it
+		if expiryFromFile, err := parseAccessTokenExpiry(atExpiry, atExpiryTimeFormat); err == nil {
+			expiry = expiryFromFile
+		}
+	}
+
+	return &oauth2.Token{
+		TokenType:    "bearer",
+		AccessToken:  accessToken,
+		RefreshToken: refreshToken,
+		Expiry:       expiry, // required: will trigger reuse of refresh token
+	}
+}
+
+func parseAccessTokenExpiry(expiryStr, timeFormat string) (time.Time, error) {
+	formatEnums := map[string]string{
+		"Layout":      time.Layout,
+		"ANSIC":       time.ANSIC,
+		"UnixDate":    time.UnixDate,
+		"RubyDate":    time.RubyDate,
+		"RFC822":      time.RFC822,
+		"RFC822Z":     time.RFC822Z,
+		"RFC850":      time.RFC850,
+		"RFC1123":     time.RFC1123,
+		"RFC1123Z":    time.RFC1123Z,
+		"RFC3339":     time.RFC3339,
+		"RFC3339Nano": time.RFC3339Nano,
+		"Kitchen":     time.Kitchen,
+		"DateOnly":    time.DateOnly,
+	}
+
+	format, found := formatEnums[timeFormat]
+	if !found {
+		// specific format is specified instead of enum
+		format = timeFormat
+	}
+
+	expiry, err := time.Parse(format, expiryStr)
+	if err != nil {
+		return time.Time{}, err
+	}
+
+	return expiry, nil
+}
diff --git a/common/scanning/readers.go b/common/scanning/readers.go
new file mode 100644
index 00000000..42391c11
--- /dev/null
+++ b/common/scanning/readers.go
@@ -0,0 +1,97 @@
+package scanning
+
+import (
+	"fmt"
+	"log/slog"
+	"os"
+
+	"github.com/spyzhov/ajson"
+)
+
+// Reader gets data from source specific to implementation as a key-value pair.
+type Reader interface {
+	Value() (any, error)
+	Key() (string, error)
+}
+
+type EnvReader struct {
+	KeyName string `json:"string" validate:"required"`
+	EnvName string `json:"params" validate:"required"`
+}
+
+func (r *EnvReader) Value() (any, error) {
+	value := os.Getenv(r.EnvName)
+	if value == "" {
+		return "", fmt.Errorf("%w: %s", ErrEnvVarNotSet, r.EnvName)
+	}
+
+	return value, nil
+}
+
+func (r *EnvReader) Key() (string, error) {
+	if r.KeyName == "" {
+		return "", fmt.Errorf("%w: %s", ErrKeyNotFound, r.EnvName)
+	}
+
+	return r.KeyName, nil
+}
+
+type ValueReader struct {
+	KeyName string `json:"string" validate:"required"`
+	Val     any    `json:"val"    validate:"required"`
+}
+
+func (r *ValueReader) Value() (any, error) {
+	if r.Val == nil {
+		return "", fmt.Errorf("%w: %s", ErrValueNotFound, r.KeyName)
+	}
+
+	return r.Val, nil
+}
+
+func (r *ValueReader) Key() (string, error) {
+	if r.KeyName == "" {
+		return "", fmt.Errorf("%w: %s", ErrKeyNotFound, r.Val)
+	}
+
+	return r.KeyName, nil
+}
+
+type JSONReader struct {
+	KeyName  string `json:"string"   validate:"required"`
+	FilePath string `json:"filePath" validate:"required"`
+	JSONPath string `json:"jsonPath" validate:"required"`
+}
+
+func (r *JSONReader) Key() (string, error) {
+	if r.KeyName == "" {
+		return "", fmt.Errorf("%w: %s", ErrKeyNotFound, r.FilePath)
+	}
+
+	return r.KeyName, nil
+}
+
+func (r *JSONReader) Value() (any, error) {
+	data, err := os.ReadFile(r.FilePath)
+	if err != nil {
+		slog.Error("Error opening creds.json", "error", err)
+
+		return nil, err
+	}
+
+	credsMap, err := ajson.Unmarshal(data)
+	if err != nil {
+		return nil, err
+	}
+
+	list, err := credsMap.JSONPath(r.JSONPath)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(list) == 0 || list[0] == nil {
+		return nil, fmt.Errorf("%w: %s", ErrJSONPathNotFound, r.JSONPath)
+	}
+
+	return list[0].Value()
+}
diff --git a/common/scanning/registry.go b/common/scanning/registry.go
new file mode 100644
index 00000000..f8b7db57
--- /dev/null
+++ b/common/scanning/registry.go
@@ -0,0 +1,350 @@
+// nolint: ireturn
+package scanning
+
+import (
+	"errors"
+	"fmt"
+	"log"
+
+	"github.com/go-playground/validator"
+	"github.com/spyzhov/ajson"
+)
+
+var (
+	validate = validator.New() //nolint:gochecknoglobals
+
+	ErrKeyNotFound      = errors.New("key not found")
+	ErrValueNotFound    = errors.New("value not found")
+	ErrWrongType        = errors.New("wrong type")
+	ErrEnvVarNotSet     = errors.New("environment variable not set")
+	ErrJSONPathNotFound = errors.New("empty value at json path")
+	ErrReaderNotFound   = errors.New("reader not found")
+)
+
+// Registry of readers capable of scanning data of different type from file, env sources.
+type Registry map[string]Reader
+
+func NewRegistry() Registry {
+	return make(Registry)
+}
+
+func (c Registry) AddReader(reader Reader) error {
+	if err := validate.Struct(reader); err != nil {
+		return err
+	}
+
+	key, err := reader.Key()
+	if err != nil {
+		return err
+	}
+
+	c[key] = reader
+
+	return nil
+}
+
+func (c Registry) AddReaders(readers ...Reader) error {
+	for _, reader := range readers {
+		if err := validate.Struct(reader); err != nil {
+			return fmt.Errorf("%w: %v", err, reader)
+		}
+
+		if err := c.AddReader(reader); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c Registry) Get(key string) (any, error) {
+	reader, ok := c[key]
+	if !ok {
+		return nil, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return reader.Value()
+}
+
+func (c Registry) GetString(key string) (string, error) {
+	reader, ok := c[key]
+	if !ok {
+		return "", fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[string](reader)
+}
+
+func (c Registry) GetBool(key string) (bool, error) {
+	reader, ok := c[key]
+	if !ok {
+		return false, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[bool](reader)
+}
+
+func (c Registry) GetInt(key string) (int, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int](reader)
+}
+
+func (c Registry) GetFloat64(key string) (float64, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[float64](reader)
+}
+
+func (c Registry) GetFloat32(key string) (float32, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[float32](reader)
+}
+
+func (c Registry) GetInt64(key string) (int64, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int64](reader)
+}
+
+func (c Registry) GetInt32(key string) (int32, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int32](reader)
+}
+
+func (c Registry) GetInt16(key string) (int16, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int16](reader)
+}
+
+func (c Registry) GetInt8(key string) (int8, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int8](reader)
+}
+
+func (c Registry) GetUint64(key string) (uint64, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint64](reader)
+}
+
+func (c Registry) GetUint32(key string) (uint32, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint32](reader)
+}
+
+func (c Registry) GetUint16(key string) (uint16, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint16](reader)
+}
+
+func (c Registry) GetUint8(key string) (uint8, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint8](reader)
+}
+
+func (c Registry) GetUint(key string) (uint, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint](reader)
+}
+
+func (c Registry) GetMap(key string) (map[string]*ajson.Node, error) {
+	reader, ok := c[key]
+	if !ok {
+		return nil, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[map[string]*ajson.Node](reader)
+}
+
+func (c Registry) MustString(credKey string) string {
+	str, err := c.GetString(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return str
+}
+
+func (c Registry) MustBool(credKey string) bool {
+	b, err := c.GetBool(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return b
+}
+
+func (c Registry) MustInt(credKey string) int {
+	i, err := c.GetInt(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustFloat64(credKey string) float64 {
+	f, err := c.GetFloat64(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return f
+}
+
+func (c Registry) MustFloat32(credKey string) float32 {
+	f, err := c.GetFloat32(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return f
+}
+
+func (c Registry) MustInt64(credKey string) int64 {
+	i, err := c.GetInt64(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustInt32(credKey string) int32 {
+	i, err := c.GetInt32(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustInt16(credKey string) int16 {
+	i, err := c.GetInt16(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustInt8(credKey string) int8 {
+	i, err := c.GetInt8(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustUint64(credKey string) uint64 {
+	i, err := c.GetUint64(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustUint32(credKey string) uint32 {
+	i, err := c.GetUint32(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustUint16(credKey string) uint16 {
+	i, err := c.GetUint16(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustUint8(credKey string) uint8 {
+	i, err := c.GetUint8(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c Registry) MustUint(credKey string) uint {
+	i, err := c.GetUint(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func getFromReader[T any](reader Reader) (T, error) {
+	var of T
+
+	val, err := reader.Value()
+	if err != nil {
+		return of, err
+	}
+
+	return get[T](val)
+}
+
+func get[T any](val any) (T, error) {
+	var of T
+
+	v, ok := (val).(T)
+	if !ok {
+		return of, fmt.Errorf("%w. expected %T, got %T", ErrWrongType, of, val)
+	}
+
+	return v, nil
+}
diff --git a/utils/testcred1.json b/common/scanning/test/cred1.json
similarity index 100%
rename from utils/testcred1.json
rename to common/scanning/test/cred1.json
diff --git a/utils/testcred2.json b/common/scanning/test/cred2.json
similarity index 100%
rename from utils/testcred2.json
rename to common/scanning/test/cred2.json
diff --git a/providers/types.gen.go b/providers/types.gen.go
index 2ef21f93..a9ff746f 100644
--- a/providers/types.gen.go
+++ b/providers/types.gen.go
@@ -102,7 +102,7 @@ type Provider = string
 type ProviderInfo struct {
 	// ApiKeyOpts Configuration for API key. Must be provided if authType is apiKey.
 	ApiKeyOpts *ApiKeyOpts `json:"apiKeyOpts,omitempty"`
-	AuthType   AuthType    `json:"authType" validate:"required"`
+	AuthType   AuthType    `json:"authType"             validate:"required"`
 
 	// BaseURL The base URL for making API requests.
 	BaseURL string `json:"baseURL" validate:"required"`
@@ -117,7 +117,7 @@ type ProviderInfo struct {
 	// PostAuthInfoNeeded If true, we require additional information after auth to start making requests.
 	PostAuthInfoNeeded bool         `json:"postAuthInfoNeeded,omitempty"`
 	ProviderOpts       ProviderOpts `json:"providerOpts"`
-	Support            Support      `json:"support" validate:"required"`
+	Support            Support      `json:"support"                      validate:"required"`
 }
 
 // ProviderOpts defines model for ProviderOpts.
diff --git a/scripts/oauth/token.go b/scripts/oauth/token.go
index 03ece862..40eb415f 100644
--- a/scripts/oauth/token.go
+++ b/scripts/oauth/token.go
@@ -19,8 +19,8 @@ import (
 	"time"
 
 	"github.com/amp-labs/connectors/common/paramsbuilder"
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/providers"
-	"github.com/amp-labs/connectors/utils"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/clientcredentials"
 )
@@ -63,48 +63,48 @@ const (
 // No changes required below
 // ================================
 
-var registry = utils.NewCredentialsRegistry()
+var registry = scanning.NewRegistry()
 
-var readers = []utils.Reader{
-	&utils.JSONReader{
+var readers = []scanning.Reader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['clientId']",
-		CredKey:  "ClientId",
+		KeyName:  "ClientId",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['clientSecret']",
-		CredKey:  "ClientSecret",
+		KeyName:  "ClientSecret",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['scopes']",
-		CredKey:  "Scopes",
+		KeyName:  "Scopes",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['provider']",
-		CredKey:  "Provider",
+		KeyName:  "Provider",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['substitutions']",
-		CredKey:  "Substitutions",
+		KeyName:  "Substitutions",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['state']",
-		CredKey:  "State",
+		KeyName:  "State",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['userName']",
-		CredKey:  "UserName",
+		KeyName:  "UserName",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['password']",
-		CredKey:  "Password",
+		KeyName:  "Password",
 	},
 }
 
diff --git a/scripts/proxy/proxy.go b/scripts/proxy/proxy.go
index 1493a484..6743fd6f 100644
--- a/scripts/proxy/proxy.go
+++ b/scripts/proxy/proxy.go
@@ -15,13 +15,13 @@ import (
 	"os"
 	"os/signal"
 	"strings"
-	"time"
 
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/paramsbuilder"
+	"github.com/amp-labs/connectors/common/scanning"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
 	"github.com/amp-labs/connectors/connector"
 	"github.com/amp-labs/connectors/providers"
-	"github.com/amp-labs/connectors/utils"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/clientcredentials"
 )
@@ -56,68 +56,68 @@ var (
 // Main (no changes needed)
 // ==============================
 
-var registry = utils.NewCredentialsRegistry()
+var registry = scanning.NewRegistry()
 
-var readers = []utils.Reader{
-	&utils.JSONReader{
+var readers = []scanning.Reader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['clientId']",
-		CredKey:  "ClientId",
+		KeyName:  "ClientId",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['clientSecret']",
-		CredKey:  "ClientSecret",
+		KeyName:  "ClientSecret",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['scopes']",
-		CredKey:  "Scopes",
+		KeyName:  "Scopes",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['provider']",
-		CredKey:  "Provider",
+		KeyName:  "Provider",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['substitutions']",
-		CredKey:  "Substitutions",
+		KeyName:  "Substitutions",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['accessToken']",
-		CredKey:  "AccessToken",
+		KeyName:  "AccessToken",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['refreshToken']",
-		CredKey:  "RefreshToken",
+		KeyName:  "RefreshToken",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['expiry']",
-		CredKey:  "Expiry",
+		KeyName:  "Expiry",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['expiryFormat']",
-		CredKey:  "ExpiryFormat",
+		KeyName:  "ExpiryFormat",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['apiKey']",
-		CredKey:  "ApiKey",
+		KeyName:  "ApiKey",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['userName']",
-		CredKey:  "UserName",
+		KeyName:  "UserName",
 	},
-	&utils.JSONReader{
+	&scanning.JSONReader{
 		FilePath: DefaultCredsFile,
 		JSONPath: "$['password']",
-		CredKey:  "Password",
+		KeyName:  "Password",
 	},
 }
 
@@ -252,60 +252,13 @@ func createClientAuthParams(provider string) *ClientAuthParams {
 	}
 }
 
-// Some connectors may implement Refresh tokens, when it happens expiry must be provided alongside.
-// Library shouldn't attempt to refresh tokens if API doesn't support `refresh_token` grant type.
 func getTokensFromRegistry() *oauth2.Token {
-	accessToken := registry.MustString("AccessToken")
-	refreshToken, err := registry.GetString("RefreshToken")
-
-	if err != nil {
-		// we are working without refresh token
-		return &oauth2.Token{
-			AccessToken: accessToken,
-		}
-	}
-
-	// refresh token should be specified with expiry
-	atExpiry := registry.MustString("Expiry")
-	atExpiryTimeFormat := registry.MustString("ExpiryFormat")
-	expiry := parseAccessTokenExpiry(atExpiry, atExpiryTimeFormat)
-
-	return &oauth2.Token{
-		AccessToken:  accessToken,
-		RefreshToken: refreshToken,
-		Expiry:       expiry, // required: will trigger reuse of refresh token
-	}
-}
-
-func parseAccessTokenExpiry(expiryStr, timeFormat string) time.Time {
-	formatEnums := map[string]string{
-		"Layout":      time.Layout,
-		"ANSIC":       time.ANSIC,
-		"UnixDate":    time.UnixDate,
-		"RubyDate":    time.RubyDate,
-		"RFC822":      time.RFC822,
-		"RFC822Z":     time.RFC822Z,
-		"RFC850":      time.RFC850,
-		"RFC1123":     time.RFC1123,
-		"RFC1123Z":    time.RFC1123Z,
-		"RFC3339":     time.RFC3339,
-		"RFC3339Nano": time.RFC3339Nano,
-		"Kitchen":     time.Kitchen,
-		"DateOnly":    time.DateOnly,
-	}
-
-	format, found := formatEnums[timeFormat]
-	if !found {
-		// specific format is specified instead of enum
-		format = timeFormat
-	}
-
-	expiry, err := time.Parse(format, expiryStr)
+	reader, err := credscanning.NewJSONProviderCredentials(DefaultCredsFile, true)
 	if err != nil {
 		panic(err)
 	}
 
-	return expiry
+	return reader.GetOauthToken()
 }
 
 func validateRequiredOAuth2Flags(provider, clientId, clientSecret string) {
diff --git a/utils/credentialRegistry.go b/utils/credentialRegistry.go
deleted file mode 100644
index cd133d22..00000000
--- a/utils/credentialRegistry.go
+++ /dev/null
@@ -1,442 +0,0 @@
-// nolint: ireturn
-package utils
-
-import (
-	"errors"
-	"fmt"
-	"log"
-	"log/slog"
-	"os"
-
-	"github.com/go-playground/validator"
-	"github.com/spyzhov/ajson"
-)
-
-var (
-	validate                = validator.New() //nolint:gochecknoglobals
-	ErrKeyNotFound          = errors.New("key not found")
-	ErrWrongType            = errors.New("wrong type")
-	ErrEnvVarNotSet         = errors.New("environment variable not set")
-	ErrEmptyFilePathPathVar = errors.New("empty value for file path")
-	ErrJSONPathNotFound     = errors.New("empty value at json path")
-	ErrReaderNotFound       = errors.New("Reader not found")
-	ErrCredentialNotFound   = errors.New("credential not found")
-)
-
-type CredentialsRegistry map[string]Reader
-
-type Reader interface {
-	Value() (any, error)
-	Key() (string, error)
-}
-
-type EnvReaderParam struct {
-	EnvName string `json:"envVar" validate:"required"`
-}
-
-type EnvReader struct {
-	EnvName string `json:"params" validate:"required"`
-	CredKey string `json:"string" validate:"required"`
-}
-
-func (r *EnvReader) Value() (any, error) {
-	value := os.Getenv(r.EnvName)
-	if value == "" {
-		return "", fmt.Errorf("%w: %s", ErrEnvVarNotSet, r.EnvName)
-	}
-
-	return value, nil
-}
-
-func (r *EnvReader) Key() (string, error) {
-	if r.CredKey == "" {
-		return "", fmt.Errorf("%w: %s", ErrKeyNotFound, r.EnvName)
-	}
-
-	return r.CredKey, nil
-}
-
-type ValueReader struct {
-	Val     any    `json:"val"    validate:"required"`
-	CredKey string `json:"string" validate:"required"`
-}
-
-func (r *ValueReader) Value() (any, error) {
-	if r.Val == nil {
-		return "", fmt.Errorf("%w: %s", ErrCredentialNotFound, r.CredKey)
-	}
-
-	return r.Val, nil
-}
-
-func (r *ValueReader) Key() (string, error) {
-	if r.CredKey == "" {
-		return "", fmt.Errorf("%w: %s", ErrKeyNotFound, r.Val)
-	}
-
-	return r.CredKey, nil
-}
-
-type JSONReader struct {
-	FilePath string `json:"filePath" validate:"required"`
-	JSONPath string `json:"jsonPath" validate:"required"`
-	CredKey  string `json:"string"   validate:"required"`
-}
-
-func (r *JSONReader) Key() (string, error) {
-	if r.CredKey == "" {
-		return "", fmt.Errorf("%w: %s", ErrKeyNotFound, r.FilePath)
-	}
-
-	return r.CredKey, nil
-}
-
-func (r *JSONReader) Value() (any, error) {
-	data, err := os.ReadFile(r.FilePath)
-	if err != nil {
-		slog.Error("Error opening creds.json", "error", err)
-
-		return nil, err
-	}
-
-	credsMap, err := ajson.Unmarshal(data)
-	if err != nil {
-		return nil, err
-	}
-
-	list, err := credsMap.JSONPath(r.JSONPath)
-	if err != nil {
-		return nil, err
-	}
-
-	if len(list) == 0 || list[0] == nil {
-		return nil, fmt.Errorf("%w: %s", ErrJSONPathNotFound, r.JSONPath)
-	}
-
-	return list[0].Value()
-}
-
-func (c CredentialsRegistry) AddReader(reader Reader) error {
-	if err := validate.Struct(reader); err != nil {
-		return err
-	}
-
-	key, err := reader.Key()
-	if err != nil {
-		return err
-	}
-
-	c[key] = reader
-
-	return nil
-}
-
-func (c CredentialsRegistry) AddReaders(readers ...Reader) error {
-	for _, reader := range readers {
-		if err := validate.Struct(reader); err != nil {
-			return fmt.Errorf("%w: %v", err, reader)
-		}
-
-		if err := c.AddReader(reader); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (c CredentialsRegistry) Get(key string) (any, error) {
-	reader, ok := c[key]
-	if !ok {
-		return nil, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return reader.Value()
-}
-
-func (c CredentialsRegistry) GetString(key string) (string, error) {
-	reader, ok := c[key]
-	if !ok {
-		return "", fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[string](reader)
-}
-
-func (c CredentialsRegistry) GetBool(key string) (bool, error) {
-	reader, ok := c[key]
-	if !ok {
-		return false, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[bool](reader)
-}
-
-func (c CredentialsRegistry) GetInt(key string) (int, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[int](reader)
-}
-
-func (c CredentialsRegistry) GetFloat64(key string) (float64, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[float64](reader)
-}
-
-func (c CredentialsRegistry) GetFloat32(key string) (float32, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[float32](reader)
-}
-
-func (c CredentialsRegistry) GetInt64(key string) (int64, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[int64](reader)
-}
-
-func (c CredentialsRegistry) GetInt32(key string) (int32, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[int32](reader)
-}
-
-func (c CredentialsRegistry) GetInt16(key string) (int16, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[int16](reader)
-}
-
-func (c CredentialsRegistry) GetInt8(key string) (int8, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[int8](reader)
-}
-
-func (c CredentialsRegistry) GetUint64(key string) (uint64, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[uint64](reader)
-}
-
-func (c CredentialsRegistry) GetUint32(key string) (uint32, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[uint32](reader)
-}
-
-func (c CredentialsRegistry) GetUint16(key string) (uint16, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[uint16](reader)
-}
-
-func (c CredentialsRegistry) GetUint8(key string) (uint8, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[uint8](reader)
-}
-
-func (c CredentialsRegistry) GetUint(key string) (uint, error) {
-	reader, ok := c[key]
-	if !ok {
-		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[uint](reader)
-}
-
-func (c CredentialsRegistry) GetMap(key string) (map[string]*ajson.Node, error) {
-	reader, ok := c[key]
-	if !ok {
-		return nil, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
-	}
-
-	return getFromReader[map[string]*ajson.Node](reader)
-}
-
-func (c CredentialsRegistry) MustString(credKey string) string {
-	str, err := c.GetString(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return str
-}
-
-func (c CredentialsRegistry) MustBool(credKey string) bool {
-	b, err := c.GetBool(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return b
-}
-
-func (c CredentialsRegistry) MustInt(credKey string) int {
-	i, err := c.GetInt(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustFloat64(credKey string) float64 {
-	f, err := c.GetFloat64(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return f
-}
-
-func (c CredentialsRegistry) MustFloat32(credKey string) float32 {
-	f, err := c.GetFloat32(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return f
-}
-
-func (c CredentialsRegistry) MustInt64(credKey string) int64 {
-	i, err := c.GetInt64(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustInt32(credKey string) int32 {
-	i, err := c.GetInt32(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustInt16(credKey string) int16 {
-	i, err := c.GetInt16(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustInt8(credKey string) int8 {
-	i, err := c.GetInt8(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustUint64(credKey string) uint64 {
-	i, err := c.GetUint64(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustUint32(credKey string) uint32 {
-	i, err := c.GetUint32(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustUint16(credKey string) uint16 {
-	i, err := c.GetUint16(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustUint8(credKey string) uint8 {
-	i, err := c.GetUint8(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func (c CredentialsRegistry) MustUint(credKey string) uint {
-	i, err := c.GetUint(credKey)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return i
-}
-
-func NewCredentialsRegistry() CredentialsRegistry {
-	return make(CredentialsRegistry)
-}
-
-func getFromReader[T any](reader Reader) (T, error) {
-	var of T
-
-	val, err := reader.Value()
-	if err != nil {
-		return of, err
-	}
-
-	return get[T](val)
-}
-
-func get[T any](val any) (T, error) {
-	var of T
-
-	v, ok := (val).(T)
-	if !ok {
-		return of, fmt.Errorf("%w. expected %T, got %T", ErrWrongType, of, val)
-	}
-
-	return v, nil
-}
diff --git a/utils/credentials.go b/utils/credentials.go
index 3a4b0853..634fcfd2 100644
--- a/utils/credentials.go
+++ b/utils/credentials.go
@@ -4,11 +4,14 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"golang.org/x/oauth2"
 )
 
 //nolint:gochecknoglobals
 var (
+	// TODO replace this with values from credsregistry/fields.go.
+
 	AccessToken  = "accessToken"
 	RefreshToken = "refreshToken"
 	ClientId     = "clientId"
@@ -17,7 +20,7 @@ var (
 	Provider     = "provider"
 )
 
-func SalesforceOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
+func SalesforceOAuthConfigFromRegistry(registry scanning.Registry) *oauth2.Config {
 	clientId := registry.MustString(ClientId)
 	clientSecret := registry.MustString(ClientSecret)
 	salesforceWorkspace := registry.MustString(WorkspaceRef)
@@ -35,7 +38,7 @@ func SalesforceOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Con
 	return cfg
 }
 
-func SalesforceOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
+func SalesforceOauthTokenFromRegistry(registry scanning.Registry) *oauth2.Token {
 	accessToken := registry.MustString(AccessToken)
 	refreshToken := registry.MustString(RefreshToken)
 
@@ -49,7 +52,7 @@ func SalesforceOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Toke
 	return tok
 }
 
-func HubspotOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
+func HubspotOauthTokenFromRegistry(registry scanning.Registry) *oauth2.Token {
 	accessToken := registry.MustString(AccessToken)
 	refreshToken := registry.MustString(RefreshToken)
 
@@ -63,7 +66,7 @@ func HubspotOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
 	return tok
 }
 
-func HubspotOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
+func HubspotOAuthConfigFromRegistry(registry scanning.Registry) *oauth2.Config {
 	clientId := registry.MustString(ClientId)
 	clientSecret := registry.MustString(ClientSecret)
 
@@ -90,44 +93,7 @@ func HubspotOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config
 	return cfg
 }
 
-func MSDynamics365CRMConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
-	clientId := registry.MustString(ClientId)
-	clientSecret := registry.MustString(ClientSecret)
-	workspace := registry.MustString(WorkspaceRef)
-
-	cfg := &oauth2.Config{
-		ClientID:     clientId,
-		ClientSecret: clientSecret,
-		RedirectURL:  "http://localhost:8080/callbacks/v1/oauth",
-		Endpoint: oauth2.Endpoint{
-			AuthURL:   "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
-			TokenURL:  "https://login.microsoftonline.com/common/oauth2/v2.0/token",
-			AuthStyle: oauth2.AuthStyleInParams,
-		},
-		Scopes: []string{
-			fmt.Sprintf("https://%v.crm.dynamics.com/user_impersonation", workspace),
-			"offline_access",
-		},
-	}
-
-	return cfg
-}
-
-func MSDynamics365CRMTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
-	accessToken := registry.MustString(AccessToken)
-	refreshToken := registry.MustString(RefreshToken)
-
-	tok := &oauth2.Token{
-		AccessToken:  accessToken,
-		RefreshToken: refreshToken,
-		TokenType:    "bearer",
-		Expiry:       time.Now().Add(-1 * time.Hour), // just pretend it's expired already, whatever, it'll fetch a new one.
-	}
-
-	return tok
-}
-
-func SalesloftConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
+func SalesloftConfigFromRegistry(registry scanning.Registry) *oauth2.Config {
 	clientId := registry.MustString(ClientId)
 	clientSecret := registry.MustString(ClientSecret)
 
@@ -146,7 +112,7 @@ func SalesloftConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
 	return cfg
 }
 
-func SalesloftTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
+func SalesloftTokenFromRegistry(registry scanning.Registry) *oauth2.Token {
 	accessToken := registry.MustString(AccessToken)
 	refreshToken := registry.MustString(RefreshToken)
 
@@ -160,7 +126,7 @@ func SalesloftTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
 	return tok
 }
 
-func OutreachOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
+func OutreachOAuthConfigFromRegistry(registry scanning.Registry) *oauth2.Config {
 	clientId := registry.MustString(ClientId)
 	clientSecret := registry.MustString(ClientSecret)
 
@@ -185,7 +151,7 @@ func OutreachOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Confi
 	return cfg
 }
 
-func OutreachOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
+func OutreachOauthTokenFromRegistry(registry scanning.Registry) *oauth2.Token {
 	accessToken := registry.MustString(AccessToken)
 	refreshToken := registry.MustString(RefreshToken)
 
@@ -199,7 +165,7 @@ func OutreachOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token
 	return tok
 }
 
-func IntercomConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
+func IntercomConfigFromRegistry(registry scanning.Registry) *oauth2.Config {
 	clientId := registry.MustString(ClientId)
 	clientSecret := registry.MustString(ClientSecret)
 
@@ -218,7 +184,7 @@ func IntercomConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
 	return cfg
 }
 
-func IntercomTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
+func IntercomTokenFromRegistry(registry scanning.Registry) *oauth2.Token {
 	accessToken := registry.MustString(AccessToken)
 
 	tok := &oauth2.Token{
@@ -229,7 +195,7 @@ func IntercomTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
 	return tok
 }
 
-func GongOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
+func GongOAuthConfigFromRegistry(registry scanning.Registry) *oauth2.Config {
 	clientId := registry.MustString(ClientId)
 	clientSecret := registry.MustString(ClientSecret)
 
@@ -258,7 +224,7 @@ func GongOAuthConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
 	return cfg
 }
 
-func GongOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
+func GongOauthTokenFromRegistry(registry scanning.Registry) *oauth2.Token {
 	accessToken := registry.MustString(AccessToken)
 	refreshToken := registry.MustString(RefreshToken)
 
@@ -273,37 +239,3 @@ func GongOauthTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
 
 	return tok
 }
-
-func ZendeskSupportConfigFromRegistry(registry CredentialsRegistry) *oauth2.Config {
-	clientId := registry.MustString(ClientId)
-	clientSecret := registry.MustString(ClientSecret)
-	workspace := registry.MustString(WorkspaceRef)
-
-	cfg := &oauth2.Config{
-		ClientID:     clientId,
-		ClientSecret: clientSecret,
-		RedirectURL:  fmt.Sprintf("https://%v.zendesk.com", workspace),
-		Endpoint: oauth2.Endpoint{
-			AuthURL:   fmt.Sprintf("https://%v.zendesk.com/oauth/authorizations/new", workspace),
-			TokenURL:  fmt.Sprintf("https://%v.zendesk.com/oauth/tokens", workspace),
-			AuthStyle: oauth2.AuthStyleInParams,
-		},
-		Scopes: []string{
-			"read",
-			"write",
-		},
-	}
-
-	return cfg
-}
-
-func ZendeskSupportTokenFromRegistry(registry CredentialsRegistry) *oauth2.Token {
-	accessToken := registry.MustString(AccessToken)
-
-	tok := &oauth2.Token{
-		AccessToken: accessToken,
-		TokenType:   "bearer",
-	}
-
-	return tok
-}


Test patch:
diff --git a/utils/credentialRegistry_test.go b/common/scanning/registry_test.go
similarity index 82%
rename from utils/credentialRegistry_test.go
rename to common/scanning/registry_test.go
index 10d7ba4e..5ba281df 100644
--- a/utils/credentialRegistry_test.go
+++ b/common/scanning/registry_test.go
@@ -1,5 +1,5 @@
 //nolint:gochecknoglobals
-package utils
+package scanning
 
 import (
 	"os"
@@ -9,39 +9,39 @@ import (
 )
 
 var testAccessTokenReader = &JSONReader{
-	FilePath: "testcred1.json",
+	FilePath: "test/cred1.json",
 	JSONPath: "$['accessToken']",
-	CredKey:  "AccessToken",
+	KeyName:  "AccessToken",
 }
 
 var testPreset = []Reader{
 	&EnvReader{
 		EnvName: "TEST_ENV_CLIENT_ID",
-		CredKey: "ClientId",
+		KeyName: "ClientId",
 	},
 	&JSONReader{
-		FilePath: "testcred1.json",
+		FilePath: "test/cred1.json",
 		JSONPath: "$['useToken']",
-		CredKey:  "UseToken",
+		KeyName:  "UseToken",
 	},
 	&JSONReader{
-		FilePath: "testcred2.json",
+		FilePath: "test/cred2.json",
 		JSONPath: "$['refreshToken']",
-		CredKey:  "RefreshToken",
+		KeyName:  "RefreshToken",
 	},
 	&JSONReader{
-		FilePath: "testcred2.json",
+		FilePath: "test/cred2.json",
 		JSONPath: "$['providers'][0]['name']",
-		CredKey:  "Provider",
+		KeyName:  "Provider",
 	},
 	&JSONReader{
-		FilePath: "testcred2.json",
+		FilePath: "test/cred2.json",
 		JSONPath: "$['providers'][0]['number']",
-		CredKey:  "ProviderNumber",
+		KeyName:  "ProviderNumber",
 	},
 	&ValueReader{
 		Val:     "myValue",
-		CredKey: "MyValue",
+		KeyName: "MyValue",
 	},
 }
 
@@ -52,7 +52,7 @@ func TestCredentialOptions(t *testing.T) {
 		t.Fatal("Error setting environment variable")
 	}
 
-	opts := NewCredentialsRegistry()
+	opts := NewRegistry()
 
 	require.NoError(t, opts.AddReader(testAccessTokenReader))
 
diff --git a/test/dynamicscrm/connector.go b/test/dynamicscrm/connector.go
index 2cbfb131..23fbe9b4 100644
--- a/test/dynamicscrm/connector.go
+++ b/test/dynamicscrm/connector.go
@@ -2,61 +2,46 @@ package dynamicscrm
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
 	"github.com/amp-labs/connectors/dynamicscrm"
-	testUtils "github.com/amp-labs/connectors/test/utils"
-	"github.com/amp-labs/connectors/utils"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/test/utils"
+	"golang.org/x/oauth2"
 )
 
-func GetMSDynamics365CRMConnector(ctx context.Context, filePath string) *dynamicscrm.Connector {
-	registry := utils.NewCredentialsRegistry()
-
-	readers := []utils.Reader{
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.CLIENT_ID",
-			CredKey:  utils.ClientId,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.CLIENT_SECRET",
-			CredKey:  utils.ClientSecret,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.ACCESS_TOKEN",
-			CredKey:  utils.AccessToken,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.REFRESH_TOKEN",
-			CredKey:  utils.RefreshToken,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.PROVIDER",
-			CredKey:  utils.Provider,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.WORKSPACE",
-			CredKey:  utils.WorkspaceRef,
-		},
-	}
-	_ = registry.AddReaders(readers...)
-
-	cfg := utils.MSDynamics365CRMConfigFromRegistry(registry)
-	tok := utils.MSDynamics365CRMTokenFromRegistry(registry)
-	workspace := registry.MustString(utils.WorkspaceRef)
+func GetMSDynamics365CRMConnector(ctx context.Context) *dynamicscrm.Connector {
+	filePath := credscanning.LoadPath(providers.DynamicsCRM)
+	reader := utils.MustCreateProvCredJSON(filePath, true)
 
 	conn, err := dynamicscrm.NewConnector(
-		dynamicscrm.WithClient(ctx, http.DefaultClient, cfg, tok),
-		dynamicscrm.WithWorkspace(workspace),
+		dynamicscrm.WithClient(ctx, http.DefaultClient, getConfig(reader), reader.GetOauthToken()),
+		dynamicscrm.WithWorkspace(reader.Get(credscanning.Fields.Workspace)),
 	)
+
 	if err != nil {
-		testUtils.Fail("error creating microsoft CRM connector", "error", err)
+		utils.Fail("error creating microsoft CRM connector", "error", err)
 	}
 
 	return conn
 }
+
+func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
+	return &oauth2.Config{
+		ClientID:     reader.Get(credscanning.Fields.ClientId),
+		ClientSecret: reader.Get(credscanning.Fields.ClientSecret),
+		RedirectURL:  "http://localhost:8080/callbacks/v1/oauth",
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+			TokenURL:  "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+			AuthStyle: oauth2.AuthStyleInParams,
+		},
+		Scopes: []string{
+			fmt.Sprintf("https://%v.crm.dynamics.com/user_impersonation",
+				reader.Get(credscanning.Fields.Workspace)),
+			"offline_access",
+		},
+	}
+}
diff --git a/test/dynamicscrm/metadata/main.go b/test/dynamicscrm/metadata/main.go
index 2e7cb47d..bd482d84 100644
--- a/test/dynamicscrm/metadata/main.go
+++ b/test/dynamicscrm/metadata/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
 	"os/signal"
 	"strings"
 	"syscall"
@@ -29,12 +28,7 @@ func main() {
 	// Set up slog logging.
 	utils.SetupLogging()
 
-	filePath := os.Getenv("MS_CRM_CRED_FILE")
-	if filePath == "" {
-		filePath = "./ms-crm-creds.json"
-	}
-
-	conn := connTest.GetMSDynamics365CRMConnector(ctx, filePath)
+	conn := connTest.GetMSDynamics365CRMConnector(ctx)
 	defer utils.Close(conn)
 
 	response, err := conn.Read(ctx, common.ReadParams{
diff --git a/test/dynamicscrm/read/main.go b/test/dynamicscrm/read/main.go
index 740d7fa1..aeb7cd17 100644
--- a/test/dynamicscrm/read/main.go
+++ b/test/dynamicscrm/read/main.go
@@ -25,12 +25,7 @@ func main() {
 	// Set up slog logging.
 	utils.SetupLogging()
 
-	filePath := os.Getenv("MS_CRM_CRED_FILE")
-	if filePath == "" {
-		filePath = "./ms-crm-creds.json"
-	}
-
-	conn := connTest.GetMSDynamics365CRMConnector(ctx, filePath)
+	conn := connTest.GetMSDynamics365CRMConnector(ctx)
 	defer utils.Close(conn)
 
 	res, err := conn.Read(ctx, common.ReadParams{
diff --git a/test/dynamicscrm/write-delete/main.go b/test/dynamicscrm/write-delete/main.go
index 27252f77..03ac1df1 100644
--- a/test/dynamicscrm/write-delete/main.go
+++ b/test/dynamicscrm/write-delete/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
 	"os/signal"
 	"syscall"
 
@@ -40,12 +39,7 @@ func main() {
 	// Set up slog logging.
 	utils.SetupLogging()
 
-	filePath := os.Getenv("MS_CRM_CRED_FILE")
-	if filePath == "" {
-		filePath = "./ms-crm-creds.json"
-	}
-
-	conn := connTest.GetMSDynamics365CRMConnector(ctx, filePath)
+	conn := connTest.GetMSDynamics365CRMConnector(ctx)
 	defer utils.Close(conn)
 
 	fmt.Println("> TEST Create/Update/Delete lead")
diff --git a/test/gong/connector.go b/test/gong/connector.go
index fdf19a83..4ab6de28 100644
--- a/test/gong/connector.go
+++ b/test/gong/connector.go
@@ -4,39 +4,40 @@ import (
 	"context"
 	"net/http"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/gong"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
 )
 
 func GetGongConnector(ctx context.Context, filePath string) *gong.Connector {
-	registry := utils.NewCredentialsRegistry()
+	registry := scanning.NewRegistry()
 
-	readers := []utils.Reader{
-		&utils.JSONReader{
+	readers := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.clientId",
-			CredKey:  utils.ClientId,
+			KeyName:  utils.ClientId,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.clientSecret",
-			CredKey:  utils.ClientSecret,
+			KeyName:  utils.ClientSecret,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.accessToken",
-			CredKey:  utils.AccessToken,
+			KeyName:  utils.AccessToken,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.refreshToken",
-			CredKey:  utils.RefreshToken,
+			KeyName:  utils.RefreshToken,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.provider",
-			CredKey:  utils.Provider,
+			KeyName:  utils.Provider,
 		},
 	}
 	_ = registry.AddReaders(readers...)
diff --git a/test/hubspot/write.go b/test/hubspot/write.go
index a526c6a6..edd46a00 100644
--- a/test/hubspot/write.go
+++ b/test/hubspot/write.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/http"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/hubspot"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
@@ -11,33 +12,33 @@ import (
 
 // GetHubspotConnector returns a Hubspot connector.
 func GetHubspotConnector(ctx context.Context, filePath string) *hubspot.Connector {
-	registry := utils.NewCredentialsRegistry()
+	registry := scanning.NewRegistry()
 
-	readers := []utils.Reader{
-		&utils.JSONReader{
+	readers := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.CLIENT_ID",
-			CredKey:  "clientId",
+			KeyName:  "clientId",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.CLIENT_SECRET",
-			CredKey:  "clientSecret",
+			KeyName:  "clientSecret",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.REFRESH_TOKEN",
-			CredKey:  "refreshToken",
+			KeyName:  "refreshToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.ACCESS_TOKEN",
-			CredKey:  "accessToken",
+			KeyName:  "accessToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.PROVIDER",
-			CredKey:  "provider",
+			KeyName:  "provider",
 		},
 	}
 	registry.AddReaders(readers...)
diff --git a/test/intercom/connector.go b/test/intercom/connector.go
index 93c4d16f..857e5ba1 100644
--- a/test/intercom/connector.go
+++ b/test/intercom/connector.go
@@ -4,29 +4,30 @@ import (
 	"context"
 	"net/http"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/intercom"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
 )
 
 func GetIntercomConnector(ctx context.Context, filePath string) *intercom.Connector {
-	registry := utils.NewCredentialsRegistry()
+	registry := scanning.NewRegistry()
 
-	readers := []utils.Reader{
-		&utils.JSONReader{
+	readers := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.CLIENT_ID",
-			CredKey:  "clientId",
+			KeyName:  "clientId",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.CLIENT_SECRET",
-			CredKey:  "clientSecret",
+			KeyName:  "clientSecret",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.ACCESS_TOKEN",
-			CredKey:  "accessToken",
+			KeyName:  "accessToken",
 		},
 	}
 	_ = registry.AddReaders(readers...)
diff --git a/test/outreach/metadata/metadata.go b/test/outreach/metadata/metadata.go
index 6af4867d..d6948476 100644
--- a/test/outreach/metadata/metadata.go
+++ b/test/outreach/metadata/metadata.go
@@ -6,6 +6,7 @@ import (
 	"log"
 	"net/http"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/outreach"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
@@ -16,33 +17,33 @@ const (
 )
 
 func GetOutreachConnector(ctx context.Context, filePath string) *outreach.Connector {
-	registry := utils.NewCredentialsRegistry()
+	registry := scanning.NewRegistry()
 
-	readers := []utils.Reader{
-		&utils.JSONReader{
+	readers := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['clientId']",
-			CredKey:  "clientId",
+			KeyName:  "clientId",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['clientSecret']",
-			CredKey:  "clientSecret",
+			KeyName:  "clientSecret",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['refreshToken']",
-			CredKey:  "refreshToken",
+			KeyName:  "refreshToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['accessToken']",
-			CredKey:  "accessToken",
+			KeyName:  "accessToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['provider']",
-			CredKey:  "provider",
+			KeyName:  "provider",
 		},
 	}
 	registry.AddReaders(readers...)
diff --git a/test/outreach/read-write/read-write.go b/test/outreach/read-write/read-write.go
index 8da8f201..03dc3c6b 100644
--- a/test/outreach/read-write/read-write.go
+++ b/test/outreach/read-write/read-write.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/amp-labs/connectors"
 	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/outreach"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
@@ -38,33 +39,33 @@ type EmailAddressUpdate struct {
 }
 
 func GetOutreachConnector(ctx context.Context, filePath string) *outreach.Connector {
-	registry := utils.NewCredentialsRegistry()
+	registry := scanning.NewRegistry()
 
-	readers := []utils.Reader{
-		&utils.JSONReader{
+	readers := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['clientId']",
-			CredKey:  "clientId",
+			KeyName:  "clientId",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['clientSecret']",
-			CredKey:  "clientSecret",
+			KeyName:  "clientSecret",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['refreshToken']",
-			CredKey:  "refreshToken",
+			KeyName:  "refreshToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['accessToken']",
-			CredKey:  "accessToken",
+			KeyName:  "accessToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$['provider']",
-			CredKey:  "provider",
+			KeyName:  "provider",
 		},
 	}
 	registry.AddReaders(readers...)
diff --git a/test/salesforce/connector.go b/test/salesforce/connector.go
index c89d6044..11cb4cab 100644
--- a/test/salesforce/connector.go
+++ b/test/salesforce/connector.go
@@ -2,57 +2,23 @@ package salesforce
 
 import (
 	"context"
+	"fmt"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/test/utils"
+	"golang.org/x/oauth2"
 	"net/http"
 
 	"github.com/amp-labs/connectors/salesforce"
 	testUtils "github.com/amp-labs/connectors/test/utils"
-	"github.com/amp-labs/connectors/utils"
 )
 
-func GetSalesforceConnector(ctx context.Context, filePath string) *salesforce.Connector {
-	registry := utils.NewCredentialsRegistry()
-
-	readers := []utils.Reader{
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.clientId",
-			CredKey:  utils.ClientId,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.clientSecret",
-			CredKey:  utils.ClientSecret,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.accessToken",
-			CredKey:  utils.AccessToken,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.refreshToken",
-			CredKey:  utils.RefreshToken,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.provider",
-			CredKey:  utils.Provider,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.substitutions.workspace",
-			CredKey:  utils.WorkspaceRef,
-		},
-	}
-	_ = registry.AddReaders(readers...)
-
-	cfg := utils.SalesforceOAuthConfigFromRegistry(registry)
-	tok := utils.SalesforceOauthTokenFromRegistry(registry)
-	workspace := registry.MustString(utils.WorkspaceRef)
+func GetSalesforceConnector(ctx context.Context) *salesforce.Connector {
+	reader := getSalesforceJSONReader()
 
 	conn, err := salesforce.NewConnector(
-		salesforce.WithClient(ctx, http.DefaultClient, cfg, tok),
-		salesforce.WithWorkspace(workspace),
+		salesforce.WithClient(ctx, http.DefaultClient, getConfig(reader), reader.GetOauthToken()),
+		salesforce.WithWorkspace(reader.Get(credscanning.Fields.Workspace)),
 	)
 	if err != nil {
 		testUtils.Fail("error creating connector", "error", err)
@@ -61,14 +27,29 @@ func GetSalesforceConnector(ctx context.Context, filePath string) *salesforce.Co
 	return conn
 }
 
-func GetSalesforceAccessToken(filePath string) string {
-	registry := utils.NewCredentialsRegistry()
+func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
+	workspace := reader.Get(credscanning.Fields.Workspace)
+
+	return &oauth2.Config{
+		ClientID:     reader.Get(credscanning.Fields.ClientId),
+		ClientSecret: reader.Get(credscanning.Fields.ClientSecret),
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   fmt.Sprintf("https://%s.my.salesforce.com/services/oauth2/authorize", workspace),
+			TokenURL:  fmt.Sprintf("https://%s.my.salesforce.com/services/oauth2/token", workspace),
+			AuthStyle: oauth2.AuthStyleInParams,
+		},
+	}
+}
+
+func GetSalesforceAccessToken() string {
+	reader := getSalesforceJSONReader()
+
+	return reader.Get(credscanning.Fields.AccessToken)
+}
 
-	_ = registry.AddReaders(&utils.JSONReader{
-		FilePath: filePath,
-		JSONPath: "$.accessToken",
-		CredKey:  utils.AccessToken,
-	})
+func getSalesforceJSONReader() *credscanning.ProviderCredentials {
+	filePath := credscanning.LoadPath(providers.Salesforce)
+	reader := utils.MustCreateProvCredJSON(filePath, true)
 
-	return registry.MustString(utils.AccessToken)
+	return reader
 }
diff --git a/test/salesforce/event-flow/main.go b/test/salesforce/event-flow/main.go
index c8e20d54..6db7c85c 100644
--- a/test/salesforce/event-flow/main.go
+++ b/test/salesforce/event-flow/main.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/salesforce"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
@@ -31,7 +32,7 @@ func main() {
 	}
 
 	ampConnectionSchemaReader := testUtils.JSONFileReaders(filePath)
-	credentialsRegistry := utils.NewCredentialsRegistry()
+	credentialsRegistry := scanning.NewRegistry()
 	credentialsRegistry.AddReaders(ampConnectionSchemaReader...)
 	salesforceWorkspace := credentialsRegistry.MustString(utils.WorkspaceRef)
 
diff --git a/test/salesforce/metadata-read/main.go b/test/salesforce/metadata-read/main.go
index c691d774..c3710d54 100644
--- a/test/salesforce/metadata-read/main.go
+++ b/test/salesforce/metadata-read/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
 	"os/signal"
 	"syscall"
 
@@ -27,12 +26,7 @@ func main() {
 	// Set up slog logging.
 	utils.SetupLogging()
 
-	filePath := os.Getenv("SALESFORCE_CRED_FILE")
-	if filePath == "" {
-		filePath = "./salesforce-creds.json"
-	}
-
-	conn := connTest.GetSalesforceConnector(ctx, filePath)
+	conn := connTest.GetSalesforceConnector(ctx)
 	defer utils.Close(conn)
 
 	response, err := conn.Read(ctx, common.ReadParams{
diff --git a/test/salesforce/metadata-write/main.go b/test/salesforce/metadata-write/main.go
index ea216467..eda3f56b 100644
--- a/test/salesforce/metadata-write/main.go
+++ b/test/salesforce/metadata-write/main.go
@@ -24,14 +24,7 @@ func main() {
 	logger := slog.New(handler)
 	slog.SetDefault(logger)
 
-	// assumes that this code is being run from the root of the project
-	// go run test/salesforce/bulkwrite/main.go
-	filePath := os.Getenv("SALESFORCE_CRED_FILE_PATH")
-	if filePath == "" {
-		filePath = "./salesforce-creds.json"
-	}
-
-	conn := connTest.GetSalesforceConnector(ctx, filePath)
+	conn := connTest.GetSalesforceConnector(ctx)
 	defer utils.Close(conn)
 
 	input := []byte(`
@@ -61,7 +54,7 @@ func main() {
     </metadata>
 </createMetadata>
 `)
-	accessToken := connTest.GetSalesforceAccessToken(filePath)
+	accessToken := connTest.GetSalesforceAccessToken()
 
 	res, err := conn.CreateMetadata(ctx, input, accessToken)
 	if err != nil {
diff --git a/test/salesforce/read-write/read-write.go b/test/salesforce/read-write/read-write.go
index f5ee5cd8..b0383da2 100644
--- a/test/salesforce/read-write/read-write.go
+++ b/test/salesforce/read-write/read-write.go
@@ -5,20 +5,16 @@ import (
 	"encoding/json"
 	"fmt"
 	"log/slog"
-	"net/http"
 	"os"
+	"os/signal"
+	"syscall"
 	"time"
 
 	"github.com/amp-labs/connectors"
-	"github.com/amp-labs/connectors/salesforce"
-	testUtils "github.com/amp-labs/connectors/test/utils"
-	"github.com/amp-labs/connectors/utils"
-	"github.com/joho/godotenv"
+	connTest "github.com/amp-labs/connectors/test/salesforce"
+	"github.com/amp-labs/connectors/test/utils"
 )
 
-// Set the appropriate environment variables in a .env file, then run:
-// go run test/salesforce.go
-
 const TimeoutSeconds = 30
 
 func main() {
@@ -26,54 +22,24 @@ func main() {
 }
 
 func mainFn() int { //nolint:funlen
-	handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
-		Level: slog.LevelDebug,
-	})
-
-	logger := slog.New(handler)
-	slog.SetDefault(logger)
-
-	if err := godotenv.Load(); err != nil {
-		slog.Error("Error loading .env file", "error", err)
-
-		return 1
-	}
-
-	salesforceEnvVarPrefix := "SALESFORCE_"
-	credentialsRegistry := utils.NewCredentialsRegistry()
-
-	envSchema := testUtils.EnvVarsReaders(salesforceEnvVarPrefix)
-	credentialsRegistry.AddReaders(
-		envSchema...,
-	)
-
-	salesforceWorkspace := credentialsRegistry.MustString(utils.WorkspaceRef)
-
-	cfg := utils.SalesforceOAuthConfigFromRegistry(credentialsRegistry)
-	tok := utils.SalesforceOauthTokenFromRegistry(credentialsRegistry)
-	ctx := context.Background()
-
-	sfc, err := salesforce.NewConnector(
-		salesforce.WithClient(ctx, http.DefaultClient, cfg, tok),
-		salesforce.WithWorkspace(salesforceWorkspace))
-	if err != nil {
-		slog.Error("Error creating Salesforce connector", "error", err)
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
 
-		return 1
-	}
+	// Set up slog logging.
+	utils.SetupLogging()
 
-	defer func() {
-		_ = sfc.Close()
-	}()
+	conn := connTest.GetSalesforceConnector(ctx)
+	defer utils.Close(conn)
 
-	if err := testReadConnector(ctx, sfc); err != nil {
-		slog.Error("Error testing", "connector", sfc, "error", err)
+	if err := testReadConnector(ctx, conn); err != nil {
+		slog.Error("Error testing", "connector", conn, "error", err)
 
 		return 1
 	}
 
-	if err := testWriteConnector(ctx, sfc); err != nil {
-		slog.Error("Error testing", "connector", sfc, "error", err)
+	if err := testWriteConnector(ctx, conn); err != nil {
+		slog.Error("Error testing", "connector", conn, "error", err)
 
 		return 1
 	}
diff --git a/test/salesloft/connector.go b/test/salesloft/connector.go
index 86117251..a1f17518 100644
--- a/test/salesloft/connector.go
+++ b/test/salesloft/connector.go
@@ -4,39 +4,40 @@ import (
 	"context"
 	"net/http"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/salesloft"
 	testUtils "github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/utils"
 )
 
 func GetSalesloftConnector(ctx context.Context, filePath string) *salesloft.Connector {
-	registry := utils.NewCredentialsRegistry()
+	registry := scanning.NewRegistry()
 
-	readers := []utils.Reader{
-		&utils.JSONReader{
+	readers := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.CLIENT_ID",
-			CredKey:  "clientId",
+			KeyName:  "clientId",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.CLIENT_SECRET",
-			CredKey:  "clientSecret",
+			KeyName:  "clientSecret",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.ACCESS_TOKEN",
-			CredKey:  "accessToken",
+			KeyName:  "accessToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.REFRESH_TOKEN",
-			CredKey:  "refreshToken",
+			KeyName:  "refreshToken",
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.PROVIDER",
-			CredKey:  "provider",
+			KeyName:  "provider",
 		},
 	}
 	_ = registry.AddReaders(readers...)
diff --git a/test/utils/connector.go b/test/utils/connector.go
index 07d69a05..e68238cd 100644
--- a/test/utils/connector.go
+++ b/test/utils/connector.go
@@ -5,6 +5,7 @@ import (
 	"net/http"
 	"os"
 
+	"github.com/amp-labs/connectors/common/scanning"
 	"github.com/amp-labs/connectors/salesforce"
 	"github.com/amp-labs/connectors/utils"
 )
@@ -18,7 +19,7 @@ func Connector(ctx context.Context) (*salesforce.Connector, error) {
 	}
 
 	ampConnectionSchemaReader := JSONFileReaders(filePath)
-	credentialsRegistry := utils.NewCredentialsRegistry()
+	credentialsRegistry := scanning.NewRegistry()
 	credentialsRegistry.AddReaders(ampConnectionSchemaReader...)
 	salesforceWorkspace := credentialsRegistry.MustString(utils.WorkspaceRef)
 
diff --git a/test/utils/creds.go b/test/utils/creds.go
index ade8ee9a..b19c8419 100644
--- a/test/utils/creds.go
+++ b/test/utils/creds.go
@@ -3,68 +3,88 @@ package utils
 import (
 	"fmt"
 
+	"github.com/amp-labs/connectors/common/scanning"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
 	"github.com/amp-labs/connectors/utils"
 )
 
 // preset values from JSON file from schema equal to response from reference: https://docs.withampersand.com/reference/getconnection
-func JSONFileReaders(filePath string) []utils.Reader {
-	schema := []utils.Reader{
-		&utils.JSONReader{
+func JSONFileReaders(filePath string) []scanning.Reader {
+	schema := []scanning.Reader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.AccessToken.Token",
-			CredKey:  utils.AccessToken,
+			KeyName:  utils.AccessToken,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.RefreshToken.Token",
-			CredKey:  utils.RefreshToken,
+			KeyName:  utils.RefreshToken,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.providerApp.clientId",
-			CredKey:  utils.ClientId,
+			KeyName:  utils.ClientId,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.providerApp.clientSecret",
-			CredKey:  utils.ClientSecret,
+			KeyName:  utils.ClientSecret,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.providerWorkspaceRef",
-			CredKey:  utils.WorkspaceRef,
+			KeyName:  utils.WorkspaceRef,
 		},
-		&utils.JSONReader{
+		&scanning.JSONReader{
 			FilePath: filePath,
 			JSONPath: "$.providerApp.provider",
-			CredKey:  utils.Provider,
+			KeyName:  utils.Provider,
 		},
 	}
 
 	return schema
 }
 
-func EnvVarsReaders(prefix string) []utils.Reader {
-	return []utils.Reader{
-		&utils.EnvReader{
+func EnvVarsReaders(prefix string) []scanning.Reader {
+	return []scanning.Reader{
+		&scanning.EnvReader{
 			EnvName: fmt.Sprintf("%sACCESS_TOKEN", prefix),
-			CredKey: utils.AccessToken,
+			KeyName: utils.AccessToken,
 		},
-		&utils.EnvReader{
+		&scanning.EnvReader{
 			EnvName: fmt.Sprintf("%sREFRESH_TOKEN", prefix),
-			CredKey: utils.RefreshToken,
+			KeyName: utils.RefreshToken,
 		},
-		&utils.EnvReader{
+		&scanning.EnvReader{
 			EnvName: fmt.Sprintf("%sCLIENT_ID", prefix),
-			CredKey: utils.ClientId,
+			KeyName: utils.ClientId,
 		},
-		&utils.EnvReader{
+		&scanning.EnvReader{
 			EnvName: fmt.Sprintf("%sCLIENT_SECRET", prefix),
-			CredKey: utils.ClientSecret,
+			KeyName: utils.ClientSecret,
 		},
-		&utils.EnvReader{
+		&scanning.EnvReader{
 			EnvName: fmt.Sprintf("%sWORKSPACE_REF", prefix),
-			CredKey: utils.WorkspaceRef,
+			KeyName: utils.WorkspaceRef,
 		},
 	}
 }
+
+func MustCreateProvCredJSON(filePath string, withAccessToken bool) *credscanning.ProviderCredentials {
+	reader, err := credscanning.NewJSONProviderCredentials(filePath, withAccessToken)
+	if err != nil {
+		Fail("json creds file error", "error", err)
+	}
+
+	return reader
+}
+
+func MustCreateProvCredENV(providerName string, withAccessToken bool) *credscanning.ProviderCredentials {
+	reader, err := credscanning.NewENVProviderCredentials(providerName, withAccessToken)
+	if err != nil {
+		Fail("environment error", "error", err)
+	}
+
+	return reader
+}
diff --git a/test/zendesksupport/connector.go b/test/zendesksupport/connector.go
index 4a179d7a..26044375 100644
--- a/test/zendesksupport/connector.go
+++ b/test/zendesksupport/connector.go
@@ -2,56 +2,46 @@ package zendesksupport
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 
-	testUtils "github.com/amp-labs/connectors/test/utils"
-	"github.com/amp-labs/connectors/utils"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/test/utils"
 	"github.com/amp-labs/connectors/zendesksupport"
+	"golang.org/x/oauth2"
 )
 
-func GetZendeskSupportConnector(ctx context.Context, filePath string) *zendesksupport.Connector {
-	registry := utils.NewCredentialsRegistry()
-
-	readers := []utils.Reader{
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.clientId",
-			CredKey:  utils.ClientId,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.clientSecret",
-			CredKey:  utils.ClientSecret,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.accessToken",
-			CredKey:  utils.AccessToken,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.provider",
-			CredKey:  utils.Provider,
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$.substitutions.workspace",
-			CredKey:  utils.WorkspaceRef,
-		},
-	}
-	_ = registry.AddReaders(readers...)
-
-	cfg := utils.ZendeskSupportConfigFromRegistry(registry)
-	tok := utils.ZendeskSupportTokenFromRegistry(registry)
-	workspace := registry.MustString(utils.WorkspaceRef)
+func GetZendeskSupportConnector(ctx context.Context) *zendesksupport.Connector {
+	filePath := credscanning.LoadPath(providers.ZendeskSupport)
+	reader := utils.MustCreateProvCredJSON(filePath, true)
 
 	conn, err := zendesksupport.NewConnector(
-		zendesksupport.WithClient(ctx, http.DefaultClient, cfg, tok),
-		zendesksupport.WithWorkspace(workspace),
+		zendesksupport.WithClient(ctx, http.DefaultClient, getConfig(reader), reader.GetOauthToken()),
+		zendesksupport.WithWorkspace(reader.Get(credscanning.Fields.Workspace)),
 	)
 	if err != nil {
-		testUtils.Fail("error creating connector", "error", err)
+		utils.Fail("error creating connector", "error", err)
 	}
 
 	return conn
 }
+
+func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
+	workspace := reader.Get(credscanning.Fields.Workspace)
+
+	return &oauth2.Config{
+		ClientID:     reader.Get(credscanning.Fields.ClientId),
+		ClientSecret: reader.Get(credscanning.Fields.ClientSecret),
+		RedirectURL:  fmt.Sprintf("https://%v.zendesk.com", workspace),
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   fmt.Sprintf("https://%v.zendesk.com/oauth/authorizations/new", workspace),
+			TokenURL:  fmt.Sprintf("https://%v.zendesk.com/oauth/tokens", workspace),
+			AuthStyle: oauth2.AuthStyleInParams,
+		},
+		Scopes: []string{
+			"read",
+			"write",
+		},
+	}
+}
diff --git a/test/zendesksupport/read/main.go b/test/zendesksupport/read/main.go
index c79b89c6..f4d45978 100644
--- a/test/zendesksupport/read/main.go
+++ b/test/zendesksupport/read/main.go
@@ -25,12 +25,7 @@ func main() {
 	// Set up slog logging.
 	utils.SetupLogging()
 
-	filePath := os.Getenv("ZENDESK_SUPPORT_CRED_FILE")
-	if filePath == "" {
-		filePath = "./zendesk-support-creds.json"
-	}
-
-	conn := connTest.GetZendeskSupportConnector(ctx, filePath)
+	conn := connTest.GetZendeskSupportConnector(ctx)
 	defer utils.Close(conn)
 
 	res, err := conn.Read(ctx, common.ReadParams{
diff --git a/test/zendesksupport/write-delete/main.go b/test/zendesksupport/write-delete/main.go
index 8d9b21c4..399c74bf 100644
--- a/test/zendesksupport/write-delete/main.go
+++ b/test/zendesksupport/write-delete/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
 	"os/signal"
 	"strings"
 	"syscall"
@@ -37,12 +36,7 @@ func main() {
 	// Set up slog logging.
 	utils.SetupLogging()
 
-	filePath := os.Getenv("ZENDESK_SUPPORT_CRED_FILE")
-	if filePath == "" {
-		filePath = "./zendesk-support-creds.json"
-	}
-
-	conn := connTest.GetZendeskSupportConnector(ctx, filePath)
+	conn := connTest.GetZendeskSupportConnector(ctx)
 	defer utils.Close(conn)
 
 	fmt.Println("> TEST Create/Update/Delete brands")