diff --git a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index e45255c659cd..82041c0f03de 100644 --- a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -333,8 +333,7 @@ export class EndpointSnippetGenerator { case "header": return values.type === "header" ? this.getConstructorHeaderAuthArg({ auth, values }) : TypeInst.nop(); case "oauth": - this.addWarning("The Go SDK doesn't support OAuth client credentials yet"); - return TypeInst.nop(); + return values.type === "oauth" ? this.getConstructorOAuthArg({ auth, values }) : TypeInst.nop(); case "inferred": this.addWarning("The Go SDK Generator does not support Inferred auth scheme yet"); return TypeInst.nop(); @@ -483,6 +482,29 @@ export class EndpointSnippetGenerator { }); } + private getConstructorOAuthArg({ + auth, + values + }: { + auth: FernIr.dynamic.OAuth; + values: FernIr.dynamic.OAuthValues; + }): go.AstNode { + return go.codeblock((writer) => { + writer.writeNode( + go.invokeFunc({ + func: go.typeReference({ + name: "WithClientCredentials", + importPath: this.context.getOptionImportPath() + }), + arguments_: [ + go.TypeInstantiation.string(values.clientId), + go.TypeInstantiation.string(values.clientSecret) + ] + }) + ); + }); + } + private getConstructorHeaderArgs({ headers, values diff --git a/generators/go/internal/generator/generator.go b/generators/go/internal/generator/generator.go index 681a30b86db0..6db9474b2541 100644 --- a/generators/go/internal/generator/generator.go +++ b/generators/go/internal/generator/generator.go @@ -580,6 +580,9 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( files = append(files, newMultipartFile(g.coordinator)) files = append(files, newMultipartTestFile(g.coordinator)) } + if needsOAuthHelpers(ir) { + files = append(files, newOAuthFile(g.coordinator)) + } clientTestFile, err := newClientTestFile(g.config.FullImportPath, rootPackageName, g.coordinator, g.config.ClientName, g.config.ClientConstructorName) if err != nil { return nil, err @@ -766,6 +769,7 @@ func (g *Generator) generateRootService( ir.Errors, g.coordinator, ) + oauthConfig := computeOAuthClientCredentialsConfig(ir, g.config.FullImportPath) generatedClient, err := writer.WriteClient( ir.Auth, irService.Endpoints, @@ -781,6 +785,7 @@ func (g *Generator) generateRootService( g.config.InlineFileProperties, g.config.ClientName, g.config.ClientConstructorName, + oauthConfig, ) if err != nil { return nil, nil, err @@ -832,6 +837,7 @@ func (g *Generator) generateService( g.config.InlineFileProperties, "", "", + nil, // OAuth config is only for root client ) if err != nil { return nil, nil, err @@ -886,6 +892,7 @@ func (g *Generator) generateServiceWithoutEndpoints( g.config.InlineFileProperties, "", "", + nil, // OAuth config is only for root client ); err != nil { return nil, err } @@ -920,6 +927,7 @@ func (g *Generator) generateRootServiceWithoutEndpoints( ir.Errors, g.coordinator, ) + oauthConfig := computeOAuthClientCredentialsConfig(ir, g.config.FullImportPath) generatedClient, err := writer.WriteClient( ir.Auth, nil, @@ -935,6 +943,7 @@ func (g *Generator) generateRootServiceWithoutEndpoints( g.config.InlineFileProperties, g.config.ClientName, g.config.ClientConstructorName, + oauthConfig, ) if err != nil { return nil, nil, err @@ -1266,6 +1275,14 @@ func newOptionalTestFile(coordinator *coordinator.Client) *File { ) } +func newOAuthFile(coordinator *coordinator.Client) *File { + return NewFile( + coordinator, + "core/oauth.go", + []byte(oauthFile), + ) +} + func newQueryFile(coordinator *coordinator.Client) *File { return NewFile( coordinator, @@ -1900,6 +1917,145 @@ func needsFileUploadHelpers(ir *fernir.IntermediateRepresentation) bool { return false } +// needsOAuthHelpers returns true if OAuth is used in the IR. +func needsOAuthHelpers(ir *fernir.IntermediateRepresentation) bool { + if ir.Auth == nil { + return false + } + for _, authScheme := range ir.Auth.Schemes { + if authScheme.Oauth != nil { + return true + } + } + return false +} + +// computeOAuthClientCredentialsConfig extracts the OAuth client credentials configuration +// from the IR and returns a config struct that can be used to generate the token refresh code. +func computeOAuthClientCredentialsConfig( + ir *fernir.IntermediateRepresentation, + baseImportPath string, +) *OAuthClientCredentialsConfig { + if ir.Auth == nil { + return nil + } + + // Find the OAuth client credentials scheme + var oauthScheme *fernir.OAuthScheme + for _, authScheme := range ir.Auth.Schemes { + if authScheme.Oauth != nil { + oauthScheme = authScheme.Oauth + break + } + } + if oauthScheme == nil || oauthScheme.Configuration == nil { + return nil + } + + // Only support client credentials for now + clientCreds := oauthScheme.Configuration.ClientCredentials + if clientCreds == nil || clientCreds.TokenEndpoint == nil { + return nil + } + + tokenEndpoint := clientCreds.TokenEndpoint + endpointRef := tokenEndpoint.EndpointReference + if endpointRef == nil { + return nil + } + + // Find the endpoint by ID to get the method name + var endpoint *fernir.HttpEndpoint + for _, service := range ir.Services { + for _, ep := range service.Endpoints { + if ep.Id == endpointRef.EndpointId { + endpoint = ep + break + } + } + if endpoint != nil { + break + } + } + if endpoint == nil { + return nil + } + + // Determine the token client import path based on the subpackage + var tokenClientImportPath string + if endpointRef.SubpackageId != nil { + subpackage := ir.Subpackages[*endpointRef.SubpackageId] + if subpackage != nil { + tokenClientImportPath = packagePathToImportPath(baseImportPath, packagePathForClient(subpackage.FernFilepath)) + } + } + if tokenClientImportPath == "" { + // Token endpoint is in the root package - use the client package + tokenClientImportPath = baseImportPath + "/client" + } + + // Get the endpoint method name + tokenEndpointMethod := endpoint.Name.PascalCase.UnsafeName + + // Get the request type info + // The request type is typically in the root package (fern) + requestImportPath := baseImportPath + var requestType string + if endpoint.SdkRequest != nil && endpoint.SdkRequest.Shape != nil { + if wrapper := endpoint.SdkRequest.Shape.Wrapper; wrapper != nil { + requestType = wrapper.WrapperName.PascalCase.UnsafeName + } + } + if requestType == "" { + // Fallback: construct from endpoint name + requestType = endpoint.Name.PascalCase.UnsafeName + "Request" + } + + // Get request property field names + reqProps := tokenEndpoint.RequestProperties + if reqProps == nil || reqProps.ClientId == nil || reqProps.ClientSecret == nil { + return nil + } + + clientIDFieldName := "ClientId" + clientSecretFieldName := "ClientSecret" + if reqProps.ClientId.Property != nil && reqProps.ClientId.Property.Body != nil { + clientIDFieldName = reqProps.ClientId.Property.Body.Name.Name.PascalCase.UnsafeName + } + if reqProps.ClientSecret.Property != nil && reqProps.ClientSecret.Property.Body != nil { + clientSecretFieldName = reqProps.ClientSecret.Property.Body.Name.Name.PascalCase.UnsafeName + } + + // Get response property field names + respProps := tokenEndpoint.ResponseProperties + if respProps == nil || respProps.AccessToken == nil { + return nil + } + + accessTokenFieldName := "AccessToken" + if respProps.AccessToken.Property != nil { + accessTokenFieldName = respProps.AccessToken.Property.Name.Name.PascalCase.UnsafeName + } + + hasExpiresIn := respProps.ExpiresIn != nil + expiresInFieldName := "ExpiresIn" + if hasExpiresIn && respProps.ExpiresIn.Property != nil { + expiresInFieldName = respProps.ExpiresIn.Property.Name.Name.PascalCase.UnsafeName + } + + return &OAuthClientCredentialsConfig{ + TokenClientImportPath: tokenClientImportPath, + TokenEndpointMethod: tokenEndpointMethod, + RequestImportPath: requestImportPath, + RequestType: requestType, + ClientIDFieldName: clientIDFieldName, + ClientSecretFieldName: clientSecretFieldName, + AccessTokenFieldName: accessTokenFieldName, + ExpiresInFieldName: expiresInFieldName, + HasExpiresIn: hasExpiresIn, + } +} + func isReservedFilename(filename string) bool { _, ok := reservedFilenames[filename] return ok diff --git a/generators/go/internal/generator/sdk.go b/generators/go/internal/generator/sdk.go index 635e0e10ea43..20aab6ee862d 100644 --- a/generators/go/internal/generator/sdk.go +++ b/generators/go/internal/generator/sdk.go @@ -57,6 +57,9 @@ var ( //go:embed sdk/core/optional_test.go optionalTestFile string + //go:embed sdk/core/oauth.go + oauthFile string + //go:embed sdk/utils/pointer.go pointerFile string @@ -311,6 +314,7 @@ func (f *fileWriter) WriteRequestOptionsDefinition( f.P("MaxAttempts uint") // Generate the exported RequestOptions type that all clients can act upon. + var hasOAuth bool for _, authScheme := range auth.Schemes { if authScheme.Bearer != nil { f.P(authScheme.Bearer.Token.PascalCase.UnsafeName, " string") @@ -329,6 +333,12 @@ func (f *fileWriter) WriteRequestOptionsDefinition( typeReferenceToGoType(authScheme.Header.ValueType, f.types, f.scope, f.baseImportPath, importPath, false), ) } + if authScheme.Oauth != nil { + hasOAuth = true + f.P("ClientID string") + f.P("ClientSecret string") + f.P("OAuthTokenProvider *OAuthTokenProvider // internal: constructed in client constructor") + } } for _, header := range headers { if !shouldGenerateHeader(header, f.types) { @@ -449,6 +459,10 @@ func (f *fileWriter) WriteRequestOptionsDefinition( f.P("}") f.P() + // Note: OAuth token refresh is handled in the client constructor, not here. + // The client will create an OAuthTokenProvider internally when ClientID and ClientSecret are set. + _ = hasOAuth // Suppress unused variable warning + if err := f.writePlatformHeaders(sdkConfig, moduleConfig, sdkVersion); err != nil { return err } @@ -557,6 +571,22 @@ func (f *fileWriter) writeRequestOptionStructs( return err } } + if authScheme.Oauth != nil { + // The client credentials option requires special care because it requires + // two parameters (similar to basic auth). + f.P("// ClientCredentialsOption implements the RequestOption interface.") + f.P("type ClientCredentialsOption struct {") + f.P("ClientID string") + f.P("ClientSecret string") + f.P("}") + f.P() + + f.P("func (c *ClientCredentialsOption) applyRequestOptions(opts *RequestOptions) {") + f.P("opts.ClientID = c.ClientID") + f.P("opts.ClientSecret = c.ClientSecret") + f.P("}") + f.P() + } } } @@ -835,6 +865,42 @@ func (f *fileWriter) WriteRequestOptions( f.P("}") f.P() } + if authScheme.Oauth != nil { + if i == 0 { + option = ast.NewCallExpr( + ast.NewImportedReference( + "WithClientCredentials", + importPath, + ), + []ast.Expr{ + ast.NewBasicLit(`""`), + ast.NewBasicLit(`""`), + }, + ) + if authScheme.Oauth.Configuration != nil && authScheme.Oauth.Configuration.ClientCredentials != nil { + cc := authScheme.Oauth.Configuration.ClientCredentials + if cc.ClientIdEnvVar != nil { + environmentVars = append(environmentVars, string(*cc.ClientIdEnvVar)) + } + if cc.ClientSecretEnvVar != nil { + environmentVars = append(environmentVars, string(*cc.ClientSecretEnvVar)) + } + } + } + f.P("// WithClientCredentials sets the client credentials for OAuth authentication.") + f.P("// The SDK will automatically handle token refresh when the token expires.") + if includeCustomAuthDocs { + f.P("//") + f.WriteDocs(auth.Docs) + } + f.P("func WithClientCredentials(clientID, clientSecret string) *core.ClientCredentialsOption {") + f.P("return &core.ClientCredentialsOption{") + f.P("ClientID: clientID,") + f.P("ClientSecret: clientSecret,") + f.P("}") + f.P("}") + f.P() + } } for _, header := range headers { @@ -883,6 +949,45 @@ type GeneratedEndpoint struct { Snippet ast.Expr } +// OAuthClientCredentialsConfig contains the configuration needed to generate +// OAuth client credentials token refresh code in the client constructor. +type OAuthClientCredentialsConfig struct { + // TokenClientImportPath is the import path for the auth client package + // e.g. "github.com/oauth-client-credentials-default/fern/auth" + TokenClientImportPath string + + // TokenEndpointMethod is the name of the method on the auth client to call + // e.g. "GetToken" + TokenEndpointMethod string + + // RequestImportPath is the import path for the request type package + // e.g. "github.com/oauth-client-credentials-default/fern" + RequestImportPath string + + // RequestType is the name of the request type + // e.g. "GetTokenRequest" + RequestType string + + // ClientIDFieldName is the field name for client ID in the request + // e.g. "ClientId" + ClientIDFieldName string + + // ClientSecretFieldName is the field name for client secret in the request + // e.g. "ClientSecret" + ClientSecretFieldName string + + // AccessTokenFieldName is the field name for access token in the response + // e.g. "AccessToken" + AccessTokenFieldName string + + // ExpiresInFieldName is the field name for expires_in in the response (optional) + // e.g. "ExpiresIn" + ExpiresInFieldName string + + // HasExpiresIn indicates whether the response has an expires_in field + HasExpiresIn bool +} + // WriteClient writes a client for interacting with the given service. func (f *fileWriter) WriteClient( irAuth *ir.ApiAuth, @@ -899,6 +1004,7 @@ func (f *fileWriter) WriteClient( inlineFileProperties bool, clientNameOverride string, clientConstructorNameOverride string, + oauthConfig *OAuthClientCredentialsConfig, ) (*GeneratedClient, error) { var errorDiscriminationByPropertyStrategy *ir.ErrorDiscriminationByPropertyStrategy if errorDiscriminationStrategy != nil && errorDiscriminationStrategy.Property != nil { @@ -987,6 +1093,53 @@ func (f *fileWriter) WriteClient( continue } } + // Generate OAuth token provider initialization if OAuth is configured + if oauthConfig != nil { + f.P(`if options.ClientID != "" && options.ClientSecret != "" && options.OAuthTokenProvider == nil {`) + // Create a no-auth copy of options for the token client to avoid circular auth + f.P("noAuthOptions := core.NewRequestOptions(opts...)") + f.P("noAuthOptions.ClientID = \"\"") + f.P("noAuthOptions.ClientSecret = \"\"") + f.P("noAuthOptions.OAuthTokenProvider = nil") + // Instantiate the auth client using noAuthOptions + tokenClientAlias := f.scope.AddImport(oauthConfig.TokenClientImportPath) + f.P("tokenClient := ", tokenClientAlias, ".NewClient(") + f.P("option.WithRequestOptions(noAuthOptions),") + f.P(")") + // Build the refresh closure + f.P("options.OAuthTokenProvider = core.NewOAuthTokenProvider(") + f.P("options.ClientID,") + f.P("options.ClientSecret,") + f.P("func(ctx ", f.scope.AddImport("context"), ".Context, clientID, clientSecret string) (*core.OAuthTokenResponse, error) {") + // Request type import + construction + reqAlias := f.scope.AddImport(oauthConfig.RequestImportPath) + f.P("resp, err := tokenClient.", oauthConfig.TokenEndpointMethod, "(ctx, &", reqAlias, ".", oauthConfig.RequestType, "{") + f.P(oauthConfig.ClientIDFieldName, ": clientID,") + f.P(oauthConfig.ClientSecretFieldName, ": clientSecret,") + f.P("})") + f.P("if err != nil {") + f.P("return nil, err") + f.P("}") + // Map response to OAuthTokenResponse + if oauthConfig.HasExpiresIn { + f.P("var expiresInPtr *int") + f.P("if resp.", oauthConfig.ExpiresInFieldName, " != 0 {") + f.P("expiresIn := int(resp.", oauthConfig.ExpiresInFieldName, ")") + f.P("expiresInPtr = &expiresIn") + f.P("}") + f.P("return &core.OAuthTokenResponse{") + f.P("AccessToken: resp.", oauthConfig.AccessTokenFieldName, ",") + f.P("ExpiresIn: expiresInPtr,") + f.P("}, nil") + } else { + f.P("return &core.OAuthTokenResponse{") + f.P("AccessToken: resp.", oauthConfig.AccessTokenFieldName, ",") + f.P("}, nil") + } + f.P("},") + f.P(")") + f.P("}") + } f.P("return &", clientName, "{") f.P(`baseURL: options.BaseURL,`) f.P("caller: internal.NewCaller(") @@ -1061,6 +1214,32 @@ func (f *fileWriter) WriteClient( f.P(receiver, ".header.Clone(),") f.P("options.ToHeader(),") f.P(")") + // Handle OAuth token refresh if OAuth is used. + if irAuth != nil { + for _, authScheme := range irAuth.Schemes { + if authScheme.Oauth != nil { + tokenPrefix := "Bearer" + tokenHeader := "Authorization" + if authScheme.Oauth.Configuration != nil && authScheme.Oauth.Configuration.ClientCredentials != nil { + cc := authScheme.Oauth.Configuration.ClientCredentials + if cc.TokenPrefix != nil { + tokenPrefix = *cc.TokenPrefix + } + if cc.TokenHeader != nil { + tokenHeader = *cc.TokenHeader + } + } + f.P("if ", receiver, ".options.OAuthTokenProvider != nil {") + f.P("oauthToken, err := ", receiver, ".options.OAuthTokenProvider.GetToken(ctx)") + f.P("if err != nil {") + f.P("return ", endpoint.ErrorReturnValues) + f.P("}") + f.P(fmt.Sprintf(`%s.Set(%q, %q + " " + oauthToken)`, headersParameter, tokenHeader, tokenPrefix)) + f.P("}") + break + } + } + } if len(endpoint.Headers) > 0 { // Add endpoint-specific headers from the request, if any. for _, header := range endpoint.Headers { diff --git a/generators/go/internal/generator/sdk/core/oauth.go b/generators/go/internal/generator/sdk/core/oauth.go new file mode 100644 index 000000000000..ad6b07eadb85 --- /dev/null +++ b/generators/go/internal/generator/sdk/core/oauth.go @@ -0,0 +1,78 @@ +package core + +import ( + "context" + "sync" + "time" +) + +const ( + // expiryBufferMinutes is the buffer time before token expiry to trigger a refresh. + expiryBufferMinutes = 2 +) + +// OAuthTokenProvider manages OAuth tokens with automatic refresh. +type OAuthTokenProvider struct { + clientID string + clientSecret string + accessToken string + expiresAt time.Time + refreshFunc func(ctx context.Context, clientID, clientSecret string) (*OAuthTokenResponse, error) + mu sync.Mutex +} + +// OAuthTokenResponse represents the response from an OAuth token endpoint. +type OAuthTokenResponse struct { + AccessToken string + ExpiresIn *int // seconds until expiry (optional) +} + +// NewOAuthTokenProvider creates a new OAuthTokenProvider. +func NewOAuthTokenProvider( + clientID string, + clientSecret string, + refreshFunc func(ctx context.Context, clientID, clientSecret string) (*OAuthTokenResponse, error), +) *OAuthTokenProvider { + return &OAuthTokenProvider{ + clientID: clientID, + clientSecret: clientSecret, + refreshFunc: refreshFunc, + } +} + +// GetToken returns a valid access token, refreshing if necessary. +func (o *OAuthTokenProvider) GetToken(ctx context.Context) (string, error) { + // Fast path: check if token is still valid without acquiring lock + if o.accessToken != "" && (o.expiresAt.IsZero() || time.Now().Before(o.expiresAt)) { + return o.accessToken, nil + } + + o.mu.Lock() + defer o.mu.Unlock() + + // Double-check after acquiring lock + if o.accessToken != "" && (o.expiresAt.IsZero() || time.Now().Before(o.expiresAt)) { + return o.accessToken, nil + } + + return o.refresh(ctx) +} + +// refresh fetches a new token from the OAuth endpoint. +func (o *OAuthTokenProvider) refresh(ctx context.Context) (string, error) { + response, err := o.refreshFunc(ctx, o.clientID, o.clientSecret) + if err != nil { + return "", err + } + + o.accessToken = response.AccessToken + if response.ExpiresIn != nil { + // Set expiry time with buffer + o.expiresAt = time.Now().Add(time.Duration(*response.ExpiresIn)*time.Second - expiryBufferMinutes*time.Minute) + } else { + // No expiry info, token is valid indefinitely + o.expiresAt = time.Time{} + } + + return o.accessToken, nil +} diff --git a/generators/go/sdk/versions.yml b/generators/go/sdk/versions.yml index a3166cb4497a..12ee426752bf 100644 --- a/generators/go/sdk/versions.yml +++ b/generators/go/sdk/versions.yml @@ -1,5 +1,15 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 1.19.0 + changelogEntry: + - summary: | + Add support for OAuth client credentials authentication with automatic token refresh. + The SDK now includes an OAuthTokenProvider that handles token management with a 2-minute + buffer before expiry to trigger refresh. Thread-safe token access is ensured using mutex locks. + type: feat + createdAt: "2025-12-08" + irVersion: 60 + - version: 1.18.4 changelogEntry: - summary: | diff --git a/seed/go-sdk/oauth-client-credentials-default/README.md b/seed/go-sdk/oauth-client-credentials-default/README.md index e2747c7ae823..a8ffbe661ae3 100644 --- a/seed/go-sdk/oauth-client-credentials-default/README.md +++ b/seed/go-sdk/oauth-client-credentials-default/README.md @@ -31,13 +31,17 @@ package example import ( client "github.com/oauth-client-credentials-default/fern/client" + option "github.com/oauth-client-credentials-default/fern/option" fern "github.com/oauth-client-credentials-default/fern" context "context" ) func do() { client := client.NewClient( - nil, + option.WithClientCredentials( + "", + "", + ), ) request := &fern.GetTokenRequest{ ClientId: "client_id", diff --git a/seed/go-sdk/oauth-client-credentials-default/core/oauth.go b/seed/go-sdk/oauth-client-credentials-default/core/oauth.go new file mode 100644 index 000000000000..ad6b07eadb85 --- /dev/null +++ b/seed/go-sdk/oauth-client-credentials-default/core/oauth.go @@ -0,0 +1,78 @@ +package core + +import ( + "context" + "sync" + "time" +) + +const ( + // expiryBufferMinutes is the buffer time before token expiry to trigger a refresh. + expiryBufferMinutes = 2 +) + +// OAuthTokenProvider manages OAuth tokens with automatic refresh. +type OAuthTokenProvider struct { + clientID string + clientSecret string + accessToken string + expiresAt time.Time + refreshFunc func(ctx context.Context, clientID, clientSecret string) (*OAuthTokenResponse, error) + mu sync.Mutex +} + +// OAuthTokenResponse represents the response from an OAuth token endpoint. +type OAuthTokenResponse struct { + AccessToken string + ExpiresIn *int // seconds until expiry (optional) +} + +// NewOAuthTokenProvider creates a new OAuthTokenProvider. +func NewOAuthTokenProvider( + clientID string, + clientSecret string, + refreshFunc func(ctx context.Context, clientID, clientSecret string) (*OAuthTokenResponse, error), +) *OAuthTokenProvider { + return &OAuthTokenProvider{ + clientID: clientID, + clientSecret: clientSecret, + refreshFunc: refreshFunc, + } +} + +// GetToken returns a valid access token, refreshing if necessary. +func (o *OAuthTokenProvider) GetToken(ctx context.Context) (string, error) { + // Fast path: check if token is still valid without acquiring lock + if o.accessToken != "" && (o.expiresAt.IsZero() || time.Now().Before(o.expiresAt)) { + return o.accessToken, nil + } + + o.mu.Lock() + defer o.mu.Unlock() + + // Double-check after acquiring lock + if o.accessToken != "" && (o.expiresAt.IsZero() || time.Now().Before(o.expiresAt)) { + return o.accessToken, nil + } + + return o.refresh(ctx) +} + +// refresh fetches a new token from the OAuth endpoint. +func (o *OAuthTokenProvider) refresh(ctx context.Context) (string, error) { + response, err := o.refreshFunc(ctx, o.clientID, o.clientSecret) + if err != nil { + return "", err + } + + o.accessToken = response.AccessToken + if response.ExpiresIn != nil { + // Set expiry time with buffer + o.expiresAt = time.Now().Add(time.Duration(*response.ExpiresIn)*time.Second - expiryBufferMinutes*time.Minute) + } else { + // No expiry info, token is valid indefinitely + o.expiresAt = time.Time{} + } + + return o.accessToken, nil +} diff --git a/seed/go-sdk/oauth-client-credentials-default/core/request_option.go b/seed/go-sdk/oauth-client-credentials-default/core/request_option.go index 7c383ae1e3ad..207f2272b84b 100644 --- a/seed/go-sdk/oauth-client-credentials-default/core/request_option.go +++ b/seed/go-sdk/oauth-client-credentials-default/core/request_option.go @@ -17,12 +17,15 @@ type RequestOption interface { // This type is primarily used by the generated code and is not meant // to be used directly; use the option package instead. type RequestOptions struct { - BaseURL string - HTTPClient HTTPClient - HTTPHeader http.Header - BodyProperties map[string]interface{} - QueryParameters url.Values - MaxAttempts uint + BaseURL string + HTTPClient HTTPClient + HTTPHeader http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + MaxAttempts uint + ClientID string + ClientSecret string + OAuthTokenProvider *OAuthTokenProvider // internal: constructed in client constructor } // NewRequestOptions returns a new *RequestOptions value. @@ -110,3 +113,14 @@ type MaxAttemptsOption struct { func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) { opts.MaxAttempts = m.MaxAttempts } + +// ClientCredentialsOption implements the RequestOption interface. +type ClientCredentialsOption struct { + ClientID string + ClientSecret string +} + +func (c *ClientCredentialsOption) applyRequestOptions(opts *RequestOptions) { + opts.ClientID = c.ClientID + opts.ClientSecret = c.ClientSecret +} diff --git a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example0/snippet.go b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example0/snippet.go index a7ed77fadc8d..d395825c0c5b 100644 --- a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example0/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example0/snippet.go @@ -12,7 +12,10 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithClientCredentials( + "", + "", + ), ) request := &fern.GetTokenRequest{ ClientId: "client_id", diff --git a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example1/snippet.go b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example1/snippet.go index 14644fb77e11..571ebf89c151 100644 --- a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example1/snippet.go @@ -11,7 +11,10 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithClientCredentials( + "", + "", + ), ) client.NestedNoAuth.Api.GetSomething( context.TODO(), diff --git a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example2/snippet.go b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example2/snippet.go index 7c6294f79007..b334eb766f3b 100644 --- a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example2/snippet.go @@ -11,7 +11,10 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithClientCredentials( + "", + "", + ), ) client.Nested.Api.GetSomething( context.TODO(), diff --git a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example3/snippet.go b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example3/snippet.go index 0201ee14be5c..0305ed090d43 100644 --- a/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example3/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-default/dynamic-snippets/example3/snippet.go @@ -11,7 +11,10 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithClientCredentials( + "", + "", + ), ) client.Simple.GetSomething( context.TODO(), diff --git a/seed/go-sdk/oauth-client-credentials-default/option/request_option.go b/seed/go-sdk/oauth-client-credentials-default/option/request_option.go index 1e077bad5099..6f19ca49a0b5 100644 --- a/seed/go-sdk/oauth-client-credentials-default/option/request_option.go +++ b/seed/go-sdk/oauth-client-credentials-default/option/request_option.go @@ -62,3 +62,12 @@ func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption { MaxAttempts: attempts, } } + +// WithClientCredentials sets the client credentials for OAuth authentication. +// The SDK will automatically handle token refresh when the token expires. +func WithClientCredentials(clientID, clientSecret string) *core.ClientCredentialsOption { + return &core.ClientCredentialsOption{ + ClientID: clientID, + ClientSecret: clientSecret, + } +} diff --git a/seed/go-sdk/oauth-client-credentials-default/snippet.json b/seed/go-sdk/oauth-client-credentials-default/snippet.json index 6441ed3a6203..cf8824c5167b 100644 --- a/seed/go-sdk/oauth-client-credentials-default/snippet.json +++ b/seed/go-sdk/oauth-client-credentials-default/snippet.json @@ -8,7 +8,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.Simple.GetSomething(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n\toption \"github.com/oauth-client-credentials-default/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithClientCredentials(\n\t\t\"\u003cYOUR_CLIENT_ID\u003e\",\n\t\t\"\u003cYOUR_CLIENT_SECRET\u003e\",\n\t),\n)\nerr := client.Simple.GetSomething(\n\tcontext.TODO(),\n)\n" } }, { @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.NestedNoAuth.Api.GetSomething(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n\toption \"github.com/oauth-client-credentials-default/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithClientCredentials(\n\t\t\"\u003cYOUR_CLIENT_ID\u003e\",\n\t\t\"\u003cYOUR_CLIENT_SECRET\u003e\",\n\t),\n)\nerr := client.NestedNoAuth.Api.GetSomething(\n\tcontext.TODO(),\n)\n" } }, { @@ -30,7 +30,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.Nested.Api.GetSomething(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n\toption \"github.com/oauth-client-credentials-default/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithClientCredentials(\n\t\t\"\u003cYOUR_CLIENT_ID\u003e\",\n\t\t\"\u003cYOUR_CLIENT_SECRET\u003e\",\n\t),\n)\nerr := client.Nested.Api.GetSomething(\n\tcontext.TODO(),\n)\n" } }, { @@ -41,7 +41,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/oauth-client-credentials-default/fern\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.Auth.GetToken(\n\tcontext.TODO(),\n\t\u0026fern.GetTokenRequest{\n\t\tClientId: \"client_id\",\n\t\tClientSecret: \"client_secret\",\n\t},\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/oauth-client-credentials-default/fern\"\n\tfernclient \"github.com/oauth-client-credentials-default/fern/client\"\n\toption \"github.com/oauth-client-credentials-default/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithClientCredentials(\n\t\t\"\u003cYOUR_CLIENT_ID\u003e\",\n\t\t\"\u003cYOUR_CLIENT_SECRET\u003e\",\n\t),\n)\nresponse, err := client.Auth.GetToken(\n\tcontext.TODO(),\n\t\u0026fern.GetTokenRequest{\n\t\tClientId: \"client_id\",\n\t\tClientSecret: \"client_secret\",\n\t},\n)\n" } } ] diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/README.md b/seed/go-sdk/oauth-client-credentials-environment-variables/README.md index 398573a58ef3..2e22f3abc923 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/README.md +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/README.md @@ -31,13 +31,21 @@ package example import ( client "github.com/oauth-client-credentials-environment-variables/fern/client" + option "github.com/oauth-client-credentials-environment-variables/fern/option" + core "github.com/oauth-client-credentials-environment-variables/fern/core" fern "github.com/oauth-client-credentials-environment-variables/fern" context "context" ) func do() { client := client.NewClient( - nil, + option.WithOAuthTokenProvider( + core.NewOAuthTokenProvider( + "", + "", + nil, + ), + ), ) request := &fern.GetTokenRequest{ ClientId: "client_id", diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/core/oauth.go b/seed/go-sdk/oauth-client-credentials-environment-variables/core/oauth.go new file mode 100644 index 000000000000..ad6b07eadb85 --- /dev/null +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/core/oauth.go @@ -0,0 +1,78 @@ +package core + +import ( + "context" + "sync" + "time" +) + +const ( + // expiryBufferMinutes is the buffer time before token expiry to trigger a refresh. + expiryBufferMinutes = 2 +) + +// OAuthTokenProvider manages OAuth tokens with automatic refresh. +type OAuthTokenProvider struct { + clientID string + clientSecret string + accessToken string + expiresAt time.Time + refreshFunc func(ctx context.Context, clientID, clientSecret string) (*OAuthTokenResponse, error) + mu sync.Mutex +} + +// OAuthTokenResponse represents the response from an OAuth token endpoint. +type OAuthTokenResponse struct { + AccessToken string + ExpiresIn *int // seconds until expiry (optional) +} + +// NewOAuthTokenProvider creates a new OAuthTokenProvider. +func NewOAuthTokenProvider( + clientID string, + clientSecret string, + refreshFunc func(ctx context.Context, clientID, clientSecret string) (*OAuthTokenResponse, error), +) *OAuthTokenProvider { + return &OAuthTokenProvider{ + clientID: clientID, + clientSecret: clientSecret, + refreshFunc: refreshFunc, + } +} + +// GetToken returns a valid access token, refreshing if necessary. +func (o *OAuthTokenProvider) GetToken(ctx context.Context) (string, error) { + // Fast path: check if token is still valid without acquiring lock + if o.accessToken != "" && (o.expiresAt.IsZero() || time.Now().Before(o.expiresAt)) { + return o.accessToken, nil + } + + o.mu.Lock() + defer o.mu.Unlock() + + // Double-check after acquiring lock + if o.accessToken != "" && (o.expiresAt.IsZero() || time.Now().Before(o.expiresAt)) { + return o.accessToken, nil + } + + return o.refresh(ctx) +} + +// refresh fetches a new token from the OAuth endpoint. +func (o *OAuthTokenProvider) refresh(ctx context.Context) (string, error) { + response, err := o.refreshFunc(ctx, o.clientID, o.clientSecret) + if err != nil { + return "", err + } + + o.accessToken = response.AccessToken + if response.ExpiresIn != nil { + // Set expiry time with buffer + o.expiresAt = time.Now().Add(time.Duration(*response.ExpiresIn)*time.Second - expiryBufferMinutes*time.Minute) + } else { + // No expiry info, token is valid indefinitely + o.expiresAt = time.Time{} + } + + return o.accessToken, nil +} diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/core/request_option.go b/seed/go-sdk/oauth-client-credentials-environment-variables/core/request_option.go index 2748549b3270..428ae3700601 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/core/request_option.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/core/request_option.go @@ -3,6 +3,7 @@ package core import ( + context "context" http "net/http" url "net/url" ) @@ -17,12 +18,13 @@ type RequestOption interface { // This type is primarily used by the generated code and is not meant // to be used directly; use the option package instead. type RequestOptions struct { - BaseURL string - HTTPClient HTTPClient - HTTPHeader http.Header - BodyProperties map[string]interface{} - QueryParameters url.Values - MaxAttempts uint + BaseURL string + HTTPClient HTTPClient + HTTPHeader http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + MaxAttempts uint + OAuthTokenProvider *OAuthTokenProvider } // NewRequestOptions returns a new *RequestOptions value. @@ -48,6 +50,20 @@ func (r *RequestOptions) ToHeader() http.Header { return header } +// ToHeaderWithContext maps the configured request options into a http.Header used +// for the request(s). It handles OAuth token refresh if an OAuthTokenProvider is set. +func (r *RequestOptions) ToHeaderWithContext(ctx context.Context) (http.Header, error) { + header := r.ToHeader() + if r.OAuthTokenProvider != nil { + token, err := r.OAuthTokenProvider.GetToken(ctx) + if err != nil { + return nil, err + } + header.Set("Authorization", "Bearer "+token) + } + return header, nil +} + func (r *RequestOptions) cloneHeader() http.Header { headers := r.HTTPHeader.Clone() headers.Set("X-Fern-Language", "Go") @@ -110,3 +126,12 @@ type MaxAttemptsOption struct { func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) { opts.MaxAttempts = m.MaxAttempts } + +// OAuthTokenProviderOption implements the RequestOption interface. +type OAuthTokenProviderOption struct { + OAuthTokenProvider *OAuthTokenProvider +} + +func (o *OAuthTokenProviderOption) applyRequestOptions(opts *RequestOptions) { + opts.OAuthTokenProvider = o.OAuthTokenProvider +} diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example0/snippet.go b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example0/snippet.go index f43b8026cb2f..2817754357da 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example0/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example0/snippet.go @@ -3,6 +3,7 @@ package example import ( client "github.com/oauth-client-credentials-environment-variables/fern/client" option "github.com/oauth-client-credentials-environment-variables/fern/option" + core "github.com/oauth-client-credentials-environment-variables/fern/core" fern "github.com/oauth-client-credentials-environment-variables/fern" context "context" ) @@ -12,7 +13,13 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithOAuthTokenProvider( + core.NewOAuthTokenProvider( + "", + "", + nil, + ), + ), ) request := &fern.GetTokenRequest{ ClientId: "client_id", diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example1/snippet.go b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example1/snippet.go index 602f9b658310..e03c433607c5 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example1/snippet.go @@ -3,6 +3,7 @@ package example import ( client "github.com/oauth-client-credentials-environment-variables/fern/client" option "github.com/oauth-client-credentials-environment-variables/fern/option" + core "github.com/oauth-client-credentials-environment-variables/fern/core" fern "github.com/oauth-client-credentials-environment-variables/fern" context "context" ) @@ -12,7 +13,13 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithOAuthTokenProvider( + core.NewOAuthTokenProvider( + "", + "", + nil, + ), + ), ) request := &fern.RefreshTokenRequest{ ClientId: "client_id", diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example2/snippet.go b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example2/snippet.go index 37645fd21bc5..5d75af8ae2b4 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example2/snippet.go @@ -3,6 +3,7 @@ package example import ( client "github.com/oauth-client-credentials-environment-variables/fern/client" option "github.com/oauth-client-credentials-environment-variables/fern/option" + core "github.com/oauth-client-credentials-environment-variables/fern/core" context "context" ) @@ -11,7 +12,13 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithOAuthTokenProvider( + core.NewOAuthTokenProvider( + "", + "", + nil, + ), + ), ) client.NestedNoAuth.Api.GetSomething( context.TODO(), diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example3/snippet.go b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example3/snippet.go index d5b69dd01f8e..316d7d6026c6 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example3/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example3/snippet.go @@ -3,6 +3,7 @@ package example import ( client "github.com/oauth-client-credentials-environment-variables/fern/client" option "github.com/oauth-client-credentials-environment-variables/fern/option" + core "github.com/oauth-client-credentials-environment-variables/fern/core" context "context" ) @@ -11,7 +12,13 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithOAuthTokenProvider( + core.NewOAuthTokenProvider( + "", + "", + nil, + ), + ), ) client.Nested.Api.GetSomething( context.TODO(), diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example4/snippet.go b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example4/snippet.go index 9d9c07c5a67c..83d0ba9b1864 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example4/snippet.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/dynamic-snippets/example4/snippet.go @@ -3,6 +3,7 @@ package example import ( client "github.com/oauth-client-credentials-environment-variables/fern/client" option "github.com/oauth-client-credentials-environment-variables/fern/option" + core "github.com/oauth-client-credentials-environment-variables/fern/core" context "context" ) @@ -11,7 +12,13 @@ func do() { option.WithBaseURL( "https://api.fern.com", ), - nil, + option.WithOAuthTokenProvider( + core.NewOAuthTokenProvider( + "", + "", + nil, + ), + ), ) client.Simple.GetSomething( context.TODO(), diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/option/request_option.go b/seed/go-sdk/oauth-client-credentials-environment-variables/option/request_option.go index d1e5341cfbe8..858ab5bef3db 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/option/request_option.go +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/option/request_option.go @@ -62,3 +62,10 @@ func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption { MaxAttempts: attempts, } } + +// WithOAuthTokenProvider sets the OAuth token provider for automatic token refresh. +func WithOAuthTokenProvider(tokenProvider *core.OAuthTokenProvider) *core.OAuthTokenProviderOption { + return &core.OAuthTokenProviderOption{ + OAuthTokenProvider: tokenProvider, + } +} diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/snippet.json b/seed/go-sdk/oauth-client-credentials-environment-variables/snippet.json index bff6fde1048b..d2ca259f890f 100644 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/snippet.json +++ b/seed/go-sdk/oauth-client-credentials-environment-variables/snippet.json @@ -8,7 +8,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.Simple.GetSomething(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n\toption \"github.com/oauth-client-credentials-environment-variables/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithOAuthTokenProvider(\n\t\toauthTokenProvider,\n\t),\n)\nerr := client.Simple.GetSomething(\n\tcontext.TODO(),\n)\n" } }, { @@ -19,7 +19,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.NestedNoAuth.Api.GetSomething(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n\toption \"github.com/oauth-client-credentials-environment-variables/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithOAuthTokenProvider(\n\t\toauthTokenProvider,\n\t),\n)\nerr := client.NestedNoAuth.Api.GetSomething(\n\tcontext.TODO(),\n)\n" } }, { @@ -30,7 +30,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.Nested.Api.GetSomething(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n\toption \"github.com/oauth-client-credentials-environment-variables/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithOAuthTokenProvider(\n\t\toauthTokenProvider,\n\t),\n)\nerr := client.Nested.Api.GetSomething(\n\tcontext.TODO(),\n)\n" } }, { @@ -41,7 +41,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/oauth-client-credentials-environment-variables/fern\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.Auth.GetTokenWithClientCredentials(\n\tcontext.TODO(),\n\t\u0026fern.GetTokenRequest{\n\t\tClientId: \"client_id\",\n\t\tClientSecret: \"client_secret\",\n\t},\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/oauth-client-credentials-environment-variables/fern\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n\toption \"github.com/oauth-client-credentials-environment-variables/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithOAuthTokenProvider(\n\t\toauthTokenProvider,\n\t),\n)\nresponse, err := client.Auth.GetTokenWithClientCredentials(\n\tcontext.TODO(),\n\t\u0026fern.GetTokenRequest{\n\t\tClientId: \"client_id\",\n\t\tClientSecret: \"client_secret\",\n\t},\n)\n" } }, { @@ -52,7 +52,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/oauth-client-credentials-environment-variables/fern\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n)\n\nclient := fernclient.NewClient()\nresponse, err := client.Auth.RefreshToken(\n\tcontext.TODO(),\n\t\u0026fern.RefreshTokenRequest{\n\t\tClientId: \"client_id\",\n\t\tClientSecret: \"client_secret\",\n\t\tRefreshToken: \"refresh_token\",\n\t},\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/oauth-client-credentials-environment-variables/fern\"\n\tfernclient \"github.com/oauth-client-credentials-environment-variables/fern/client\"\n\toption \"github.com/oauth-client-credentials-environment-variables/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithOAuthTokenProvider(\n\t\toauthTokenProvider,\n\t),\n)\nresponse, err := client.Auth.RefreshToken(\n\tcontext.TODO(),\n\t\u0026fern.RefreshTokenRequest{\n\t\tClientId: \"client_id\",\n\t\tClientSecret: \"client_secret\",\n\t\tRefreshToken: \"refresh_token\",\n\t},\n)\n" } } ]