Skip to content

Commit e8a0794

Browse files
committed
Add support for GraphQL API OAuth 2.0 authentication using a login query
or mutation
1 parent 37f8088 commit e8a0794

File tree

4 files changed

+120
-3
lines changed

4 files changed

+120
-3
lines changed

docsite/docs/provider.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ The latest release can be downloaded from the graphql provider [release page](ht
1313

1414
## Provider Configuration
1515

16-
This provider has only two configuration inputs: `url` & `headers`
16+
This provider has several configuration inputs.
17+
18+
In most cases, only `url` & `headers` are used, e.g.:
1719

1820
```hcl
1921
provider "graphql" {
@@ -26,12 +28,43 @@ provider "graphql" {
2628
}
2729
```
2830

31+
In some advanced cases where the GraphQL server exposes an endpoint to perform OAuth 2.0 authentication, instead of using `headers`, you can use `oauth2_login_query`, `oauth2_login_query_variables` and `oauth2_login_query_value_attribute`, e.g.:
32+
33+
```hcl
34+
provider "graphql" {
35+
url = "https://your-graphql-server-url"
36+
37+
oauth2_login_query = "mutation loginAPI($apiKey: String!) {loginAPI(apiKey: $apiKey) {accessToken}}"
38+
oauth2_login_query_variables = {
39+
"apiKey" = "5555-44-33-99"
40+
}
41+
oauth2_login_query_value_attribute = "data.loginAPI.accessToken"
42+
}
43+
```
44+
2945
## Inputs
3046

3147
### url
48+
- **Required**: `true`
3249
- **Type**: `string`
33-
- **Description**: `The GraphQL API url that the provider will use to make requests`
50+
- **Description**: The GraphQL API url that the provider will use to make requests.
3451

3552
### headers
53+
- **Required**: `false`
54+
- **Type**: `map(string)`
55+
- **Desciption**: Any http headers that the GraphQL API server requires (e.g. `Authentication`, `x-api-key`, etc.).
56+
57+
### oauth2_login_query
58+
- **Required**: `false`
59+
- **Type**: `string`
60+
- **Description**: The GraphQL query or mutation used to retrieve an OAuth 2.0 access token from the GraphQL server. It will be executed once during provider initialization. Be aware that renewal is not implemented, which may cause issue with short-lived access tokens. Note: you must also define `oauth2_login_query_variables` and `oauth2_login_query_value_attribute` when using `oauth2_login_query`.
61+
62+
### oauth2_login_query_variables
63+
- **Required**: `false`
3664
- **Type**: `map(string)`
37-
- **Desciption**: `Any http headers that the GraphQL API requires. (eg; Authentication; x-api-key; etc)`
65+
- **Desciption**: A map of any variables that will be used in your OAuth 2.0 login query. Each variable's value is interpreted as JSON when possible. Note: you must also define `oauth2_login_query` and `oauth2_login_query_value_attribute` when using `oauth2_login_query_variables`.
66+
67+
### oauth2_login_query_value_attribute
68+
- **Required**: `false`
69+
- **Type**: `string`
70+
- **Description**: The dot-separated path to the attribute containing the access token value that will be extracted from the OAuth 2.0 login query or mutation response (it must start with `data.`, e.g. `data.loginAPI.accessToken`). Note: you must also define `oauth2_login_query` and `oauth2_login_query_variables` when using `oauth2_login_query_value_attribute`.

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ require (
66
cloud.google.com/go/storage v1.12.0 // indirect
77
github.com/aws/aws-sdk-go v1.34.33 // indirect
88
github.com/fatih/color v1.9.0 // indirect
9+
github.com/hashicorp/hcl/v2 v2.3.0
910
github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0
1011
github.com/jarcoal/httpmock v1.0.6
1112
github.com/mattn/go-colorable v0.1.7 // indirect
1213
github.com/mitchellh/mapstructure v1.3.3 // indirect
1314
github.com/stretchr/testify v1.7.0
15+
github.com/zclconf/go-cty v1.8.4
1416
golang.org/x/tools v0.0.0-20200929173036-5272f303b6eb // indirect
1517
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe // indirect
1618
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect

graphql/provider.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ package graphql
22

33
import (
44
"context"
5+
"fmt"
56

7+
"github.com/hashicorp/hcl/v2"
8+
"github.com/hashicorp/hcl/v2/hclsyntax"
69
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
710
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/zclconf/go-cty/cty"
12+
"github.com/zclconf/go-cty/cty/function"
13+
"github.com/zclconf/go-cty/cty/function/stdlib"
14+
"github.com/zclconf/go-cty/cty/gocty"
815
)
916

1017
func Provider() *schema.Provider {
@@ -24,6 +31,21 @@ func Provider() *schema.Provider {
2431
Optional: true,
2532
ForceNew: true,
2633
},
34+
"oauth2_login_query": {
35+
Type: schema.TypeString,
36+
Optional: true,
37+
},
38+
"oauth2_login_query_variables": {
39+
Type: schema.TypeMap,
40+
Elem: &schema.Schema{
41+
Type: schema.TypeString,
42+
},
43+
Optional: true,
44+
},
45+
"oauth2_login_query_value_attribute": {
46+
Type: schema.TypeString,
47+
Optional: true,
48+
},
2749
},
2850
ResourcesMap: map[string]*schema.Resource{
2951
"graphql_mutation": resourceGraphqlMutation(),
@@ -40,10 +62,66 @@ func graphqlConfigure(ctx context.Context, d *schema.ResourceData) (interface{},
4062
GQLServerUrl: d.Get("url").(string),
4163
RequestHeaders: d.Get("headers").(map[string]interface{}),
4264
}
65+
66+
if oauth2LoginQueryValueAttribute := d.Get("oauth2_login_query_value_attribute"); d.Get("oauth2_login_query") != "" && oauth2LoginQueryValueAttribute != "" {
67+
queryResponse, resBytes, err := queryExecute(ctx, d, config, "oauth2_login_query", "oauth2_login_query_variables")
68+
if err != nil {
69+
return nil, diag.FromErr(fmt.Errorf("unable to execute oauth2_login_query: %w", err))
70+
}
71+
72+
if queryErrors := queryResponse.ProcessErrors(); queryErrors.HasError() {
73+
return nil, *queryErrors
74+
}
75+
76+
var queryResponseCty cty.Value
77+
if queryResponseCty, err = gocty.ToCtyValue(string(resBytes), cty.String); err != nil {
78+
return nil, diag.FromErr(err)
79+
}
80+
81+
evalCtx := &hcl.EvalContext{
82+
Variables: map[string]cty.Value{
83+
"oauth2_login_query_response": queryResponseCty,
84+
},
85+
Functions: map[string]function.Function{
86+
"jsondecode": stdlib.JSONDecodeFunc,
87+
},
88+
}
89+
90+
var expr hcl.Expression
91+
var hclDiags hcl.Diagnostics
92+
valueTemplate := fmt.Sprintf("${jsondecode(oauth2_login_query_response).%s}", oauth2LoginQueryValueAttribute.(string))
93+
if expr, hclDiags = hclsyntax.ParseTemplate([]byte(valueTemplate), "", hcl.InitialPos); len(hclDiags) > 0 {
94+
return nil, convertDiagnosticsFromHCLToTerraformSDK(hclDiags)
95+
}
96+
97+
var interpolatedValue cty.Value
98+
if interpolatedValue, hclDiags = expr.Value(evalCtx); len(hclDiags) > 0 {
99+
return nil, convertDiagnosticsFromHCLToTerraformSDK(hclDiags)
100+
}
101+
102+
config.RequestAuthorizationHeaders = map[string]interface{}{
103+
"Authorization": fmt.Sprintf("Bearer %s", interpolatedValue.AsString()),
104+
}
105+
}
106+
43107
return config, diag.Diagnostics{}
44108
}
45109

46110
type graphqlProviderConfig struct {
47111
GQLServerUrl string
48112
RequestHeaders map[string]interface{}
113+
114+
RequestAuthorizationHeaders map[string]interface{}
115+
}
116+
117+
func convertDiagnosticsFromHCLToTerraformSDK(hclDiags hcl.Diagnostics) diag.Diagnostics {
118+
diags := make(diag.Diagnostics, len(hclDiags))
119+
for i, hclDiag := range hclDiags {
120+
// HCL severity enum is: 0 (invalid), 1 (error), 2 (warning)
121+
// terraform SDK severity enum is: 0 (error), 1 (warning)
122+
diags[i].Severity = diag.Severity(hclDiag.Severity - 1)
123+
diags[i].Summary = hclDiag.Summary
124+
diags[i].Detail = hclDiag.Detail
125+
}
126+
return diags
49127
}

graphql/query_executor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func queryExecute(ctx context.Context, d *schema.ResourceData, m interface{}, qu
1818
inputVariables := d.Get(variableSource).(map[string]interface{})
1919
apiURL := m.(*graphqlProviderConfig).GQLServerUrl
2020
headers := m.(*graphqlProviderConfig).RequestHeaders
21+
authorizationHeaders := m.(*graphqlProviderConfig).RequestAuthorizationHeaders
2122

2223
var queryBodyBuffer bytes.Buffer
2324

@@ -50,6 +51,9 @@ func queryExecute(ctx context.Context, d *schema.ResourceData, m interface{}, qu
5051

5152
req.Header.Set("Content-Type", "application/json; charset=utf-8")
5253
req.Header.Set("Accept", "application/json; charset=utf-8")
54+
for key, value := range authorizationHeaders {
55+
req.Header.Set(key, value.(string))
56+
}
5357
for key, value := range headers {
5458
req.Header.Set(key, value.(string))
5559
}

0 commit comments

Comments
 (0)