Skip to content

Commit f59eb17

Browse files
committed
Authenticate via Auth0-CLI
1 parent 273f18e commit f59eb17

File tree

6 files changed

+175
-17
lines changed

6 files changed

+175
-17
lines changed

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ better alternative.
3737

3838
- `api_token` (String) Your Auth0 [management api access token](https://auth0.com/docs/security/tokens/access-tokens/management-api-access-tokens). It can also be sourced from the `AUTH0_API_TOKEN` environment variable. It can be used instead of `client_id` + `client_secret`. If both are specified, `api_token` will be used over `client_id` + `client_secret` fields.
3939
- `audience` (String) Your Auth0 audience when using a custom domain. It can also be sourced from the `AUTH0_AUDIENCE` environment variable.
40+
- `cli_login` (Boolean) While toggled on, the API token gets fetched from the keyring for the given domain
4041
- `client_id` (String) Your Auth0 client ID. It can also be sourced from the `AUTH0_CLIENT_ID` environment variable.
4142
- `client_secret` (String) Your Auth0 client secret. It can also be sourced from the `AUTH0_CLIENT_SECRET` environment variable.
4243
- `debug` (Boolean) Enables HTTP request and response logging when TF_LOG=DEBUG is set. It can also be sourced from the `AUTH0_DEBUG` environment variable.

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ require (
1414
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1
1515
github.com/hashicorp/terraform-plugin-testing v1.12.0
1616
github.com/stretchr/testify v1.10.0
17+
github.com/zalando/go-keyring v0.2.6
1718
gopkg.in/dnaeon/go-vcr.v3 v3.2.0
1819
)
1920

2021
require (
22+
al.essio.dev/pkg/shellescape v1.5.1 // indirect
2123
github.com/BurntSushi/toml v1.4.0 // indirect
2224
github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
2325
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -30,10 +32,12 @@ require (
3032
github.com/bgentry/speakeasy v0.1.0 // indirect
3133
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
3234
github.com/cloudflare/circl v1.3.8 // indirect
35+
github.com/danieljoos/wincred v1.2.2 // indirect
3336
github.com/davecgh/go-spew v1.1.1 // indirect
3437
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
3538
github.com/fatih/color v1.17.0 // indirect
3639
github.com/goccy/go-json v0.10.5 // indirect
40+
github.com/godbus/dbus/v5 v5.1.0 // indirect
3741
github.com/golang/protobuf v1.5.4 // indirect
3842
github.com/google/uuid v1.6.0 // indirect
3943
github.com/hashicorp/cli v1.1.7 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
2+
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
13
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
24
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
35
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
@@ -40,6 +42,8 @@ github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqw
4042
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
4143
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
4244
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
45+
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
46+
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
4347
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4448
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4549
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -67,6 +71,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
6771
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
6872
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
6973
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
74+
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
75+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7076
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
7177
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
7278
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -78,6 +84,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
7884
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7985
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
8086
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
87+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
88+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
8189
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
8290
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8391
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -214,6 +222,8 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
214222
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
215223
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
216224
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
225+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
226+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
217227
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
218228
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
219229
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -237,6 +247,8 @@ github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
237247
github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
238248
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
239249
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
250+
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
251+
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
240252
github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70=
241253
github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
242254
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=

internal/config/config.go

Lines changed: 141 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,33 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7+
"encoding/base64"
8+
"encoding/json"
9+
"errors"
710
"fmt"
811
"net/http"
912
"net/url"
1013
"regexp"
1114
"strconv"
15+
"strings"
1216
"time"
1317

1418
"github.com/PuerkitoBio/rehttp"
1519
"github.com/auth0/go-auth0"
1620
"github.com/auth0/go-auth0/management"
21+
"github.com/auth0/terraform-provider-auth0/internal/mutex"
1722
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1823
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1924
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
20-
21-
"github.com/auth0/terraform-provider-auth0/internal/mutex"
25+
"github.com/zalando/go-keyring"
2226
)
2327

24-
const providerName = "Terraform-Provider-Auth0" // #nosec G101
28+
const providerName = "Terraform-Provider-Auth0" // #nosec G101
29+
const secretAccessToken = "Auth0 CLI Access Token" // #nosec G101
30+
31+
// Access tokens have no size limit, but should be smaller than (50*2048) bytes.
32+
// The max number of loops safeguards against infinite loops, however unlikely.
33+
const secretAccessTokenMaxChunks = 50
2534

2635
var version = "dev"
2736

@@ -55,23 +64,92 @@ func (c *Config) GetMutex() *mutex.KeyValue {
5564
// and passed into the subsequent resources as the meta parameter.
5665
func ConfigureProvider(terraformVersion *string) schema.ConfigureContextFunc {
5766
return func(_ context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) {
67+
var apiToken string
5868
domain := data.Get("domain").(string)
5969
clientID := data.Get("client_id").(string)
6070
clientSecret := data.Get("client_secret").(string)
61-
apiToken := data.Get("api_token").(string)
71+
apiToken = data.Get("api_token").(string)
6272
audience := data.Get("audience").(string)
6373
debug := data.Get("debug").(bool)
6474
dynamicCredentials := data.Get("dynamic_credentials").(bool)
75+
cliLogin := data.Get("cli_login").(bool)
76+
77+
switch {
78+
case dynamicCredentials:
79+
if domain == "" {
80+
return nil, diag.Diagnostics{{
81+
Severity: diag.Error,
82+
Summary: "Missing required configuration",
83+
Detail: "The 'AUTH0_DOMAIN' must be specified along with the 'AUTH0_DYNAMIC_CREDENTIALS'.",
84+
}}
85+
}
86+
case cliLogin:
87+
// Ensure domain is present.
88+
if domain == "" {
89+
return nil, diag.Diagnostics{{
90+
Severity: diag.Error,
91+
Summary: "Missing required configuration",
92+
Detail: "The 'AUTH0_DOMAIN' must be specified along with the 'AUTH0_CLI_LOGIN'.",
93+
}}
94+
}
6595

66-
if !dynamicCredentials && apiToken == "" && (clientID == "" || clientSecret == "" || domain == "") {
67-
return nil, diag.Diagnostics{
68-
{
96+
// Check for tempToken when cliLogin is enabled.
97+
var tempToken string
98+
for i := 0; i < secretAccessTokenMaxChunks; i++ {
99+
a, err := keyring.Get(fmt.Sprintf("%s %d", secretAccessToken, i), domain)
100+
if errors.Is(err, keyring.ErrNotFound) || err != nil {
101+
break
102+
}
103+
tempToken += a
104+
}
105+
106+
if tempToken == "" {
107+
return nil, diag.Diagnostics{{
69108
Severity: diag.Error,
70-
Summary: "Missing environment variables",
71-
Detail: fmt.Sprintf("Either AUTH0_API_TOKEN or AUTH0_DOMAIN:AUTH0_CLIENT_ID:AUTH0_CLIENT_SECRET must be configured. " +
72-
"Ref: https://registry.terraform.io/providers/auth0/auth0/latest/docs"),
73-
},
109+
Summary: "Authentication required",
110+
Detail: "No CLI token found. Please log in using 'auth0 login' via auth0-cli or disable 'AUTH0_CLI_LOGIN'.",
111+
}}
74112
}
113+
114+
// Check if the token is expired.
115+
if err := validateTokenExpiry(tempToken); err != nil {
116+
return nil, diag.Diagnostics{{
117+
Severity: diag.Error,
118+
Summary: "Token validation failed",
119+
Detail: err.Error(),
120+
}}
121+
}
122+
123+
// Set the apiToken to the tempToken.
124+
apiToken = tempToken
125+
126+
case apiToken != "":
127+
// Ensure domain is present.
128+
if domain == "" {
129+
return nil, diag.Diagnostics{{
130+
Severity: diag.Error,
131+
Summary: "Missing required configuration",
132+
Detail: "The 'AUTH0_DOMAIN' must be specified along with the 'AUTH0_API_TOKEN'.",
133+
}}
134+
}
135+
136+
case clientID != "" && clientSecret != "":
137+
// Ensure domain is present.
138+
if domain == "" {
139+
return nil, diag.Diagnostics{{
140+
Severity: diag.Error,
141+
Summary: "Missing required configuration",
142+
Detail: "The 'AUTH0_DOMAIN' must be specified.",
143+
}}
144+
}
145+
146+
default:
147+
return nil, diag.Diagnostics{{
148+
Severity: diag.Error,
149+
Summary: "Missing environment variables",
150+
Detail: "AUTH0_DOMAIN is required. Then, configure either AUTH0_API_TOKEN, " +
151+
"or both AUTH0_CLIENT_ID and AUTH0_CLIENT_SECRET. Or enable CLI login with AUTH0_CLI_LOGIN=true",
152+
}}
75153
}
76154

77155
apiClient, err := management.New(domain,
@@ -90,6 +168,58 @@ func ConfigureProvider(terraformVersion *string) schema.ConfigureContextFunc {
90168
}
91169
}
92170

171+
func decodeSegment(seg string) ([]byte, error) {
172+
// Add padding if necessary.
173+
if l := len(seg) % 4; l > 0 {
174+
seg += strings.Repeat("=", 4-l)
175+
}
176+
return base64.URLEncoding.DecodeString(seg)
177+
}
178+
179+
func decodeJWT(token string) (map[string]interface{}, map[string]interface{}, error) {
180+
parts := strings.Split(token, ".")
181+
if len(parts) != 3 {
182+
return nil, nil, fmt.Errorf("invalid JWT format")
183+
}
184+
185+
headerBytes, err := decodeSegment(parts[0])
186+
if err != nil {
187+
return nil, nil, fmt.Errorf("error decoding header: %w", err)
188+
}
189+
payloadBytes, err := decodeSegment(parts[1])
190+
if err != nil {
191+
return nil, nil, fmt.Errorf("error decoding payload: %w", err)
192+
}
193+
194+
var header, payload map[string]interface{}
195+
if err := json.Unmarshal(headerBytes, &header); err != nil {
196+
return nil, nil, fmt.Errorf("error unmarshalling header: %w", err)
197+
}
198+
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
199+
return nil, nil, fmt.Errorf("error unmarshalling payload: %w", err)
200+
}
201+
202+
return header, payload, nil
203+
}
204+
205+
func validateTokenExpiry(tokenString string) error {
206+
_, payload, err := decodeJWT(tokenString)
207+
if err != nil {
208+
return err
209+
}
210+
211+
exp, ok := payload["exp"]
212+
if !ok {
213+
return fmt.Errorf("missing expiration: the token does not contain an expiration claim")
214+
}
215+
216+
if time.Now().Unix() > exp.(int64) {
217+
return fmt.Errorf("expired token: the stored auth0-cli token has expired. Please log in again")
218+
}
219+
220+
return nil
221+
}
222+
93223
// userAgent computes the desired User-Agent header for the *management.Management client.
94224
func userAgent(terraformVersion *string) string {
95225
sdkVersion := auth0.Version

internal/config/config_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,14 @@ func TestConfigureProvider(t *testing.T) {
4949
expectedDiagnostics: nil,
5050
},
5151
{
52-
name: "it returns an error when it can't initialize the api client",
53-
givenTerraformConfig: map[string]interface{}{
54-
"domain": "example.com:path",
55-
},
52+
name: "it returns an error when it can't initialize the api client",
53+
givenTerraformConfig: map[string]interface{}{},
5654
expectedDiagnostics: diag.Diagnostics{
5755
diag.Diagnostic{
5856
Severity: diag.Error,
5957
Summary: "Missing environment variables",
60-
Detail: "Either AUTH0_API_TOKEN or AUTH0_DOMAIN:AUTH0_CLIENT_ID:AUTH0_CLIENT_SECRET must be configured. Ref: https://registry.terraform.io/providers/auth0/auth0/latest/docs",
58+
Detail: "AUTH0_DOMAIN is required. Then, configure either AUTH0_API_TOKEN, " +
59+
"or both AUTH0_CLIENT_ID and AUTH0_CLIENT_SECRET. Or enable CLI login with AUTH0_CLI_LOGIN=true",
6160
},
6261
},
6362
},

internal/provider/provider.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
"github.com/auth0/terraform-provider-auth0/internal/auth0/form"
1313

14-
selfserviceprofile "github.com/auth0/terraform-provider-auth0/internal/auth0/selfserviceprofile"
14+
"github.com/auth0/terraform-provider-auth0/internal/auth0/selfserviceprofile"
1515

1616
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1717

@@ -110,6 +110,18 @@ func New() *schema.Provider {
110110
Description: "Indicates whether credentials will be dynamically passed to the provider from" +
111111
"other terraform resources.",
112112
},
113+
"cli_login": {
114+
Type: schema.TypeBool,
115+
Optional: true,
116+
DefaultFunc: func() (interface{}, error) {
117+
v := os.Getenv("AUTH0_CLI_LOGIN")
118+
if v == "" {
119+
return false, nil
120+
}
121+
return v == "1" || v == "true" || v == "on", nil
122+
},
123+
Description: "While toggled on, the API token gets fetched from the keyring for the given domain",
124+
},
113125
},
114126
ResourcesMap: map[string]*schema.Resource{
115127
"auth0_action": action.NewResource(),

0 commit comments

Comments
 (0)