Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions internal/account-api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"net/http"
"os"

"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"

"github.com/shopware/shopware-cli/logging"
)

Expand All @@ -19,6 +22,17 @@ func NewApi(ctx context.Context) (*Client, error) {
return client, nil
}

// Try OAuth2 client credentials from environment variables (for CI/CD)
clientID := os.Getenv("SHOPWARE_CLI_ACCOUNT_CLIENT_ID")
clientSecret := os.Getenv("SHOPWARE_CLI_ACCOUNT_CLIENT_SECRET")

if clientID != "" || clientSecret != "" {
if clientID == "" || clientSecret == "" {
return nil, fmt.Errorf("both SHOPWARE_CLI_ACCOUNT_CLIENT_ID and SHOPWARE_CLI_ACCOUNT_CLIENT_SECRET must be set")
}
return loginWithClientCredentials(ctx, clientID, clientSecret)
}

// Try legacy username/password auth from environment variables
email := os.Getenv("SHOPWARE_CLI_ACCOUNT_EMAIL")
password := os.Getenv("SHOPWARE_CLI_ACCOUNT_PASSWORD")
Expand All @@ -42,6 +56,29 @@ func NewApi(ctx context.Context) (*Client, error) {
return client, nil
}

func loginWithClientCredentials(ctx context.Context, clientID, clientSecret string) (*Client, error) {
conf := &clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: fmt.Sprintf("%s/oauth2/token", getOIDCEndpoint()),
Scopes: []string{ClientCredentialsScopes},
AuthStyle: oauth2.AuthStyleInParams,
}
Comment thread
shyim marked this conversation as resolved.

token, err := conf.Token(ctx)
if err != nil {
return nil, fmt.Errorf("client credentials login: %w", err)
}

client := &Client{Token: token}

if err := saveApiTokenToTokenCache(client); err != nil {
logging.FromContext(ctx).Errorf("Cannot save token cache: %v", err)
}

return client, nil
}

func loginWithCredentials(ctx context.Context, email, password string) (*Client, error) {
s, err := json.Marshal(loginRequest{Email: email, Password: password})
if err != nil {
Expand Down
55 changes: 55 additions & 0 deletions internal/account-api/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package account_api

import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewApiUsesClientCredentialsFromEnv(t *testing.T) {
var tokenRequested atomic.Bool

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/oauth2/token" {
tokenRequested.Store(true)

assert.Equal(t, "client_credentials", r.FormValue("grant_type"))
assert.Equal(t, "test-client-id", r.FormValue("client_id"))
assert.Equal(t, "test-client-secret", r.FormValue("client_secret"))

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"access_token": "test-token",
"token_type": "Bearer",
"expires_in": 3600,
})
return
}
http.NotFound(w, r)
}))
defer srv.Close()

t.Setenv("SHOPWARE_CLI_CACHE_DIR", t.TempDir())
t.Setenv("SHOPWARE_CLI_ACCOUNT_CLIENT_ID", "test-client-id")
t.Setenv("SHOPWARE_CLI_ACCOUNT_CLIENT_SECRET", "test-client-secret")
t.Setenv("SHOPWARE_CLI_OIDC_ENDPOINT", srv.URL)

client, err := NewApi(t.Context())
assert.NoError(t, err)
assert.NotNil(t, client)
assert.True(t, tokenRequested.Load(), "expected token endpoint to be called")
assert.NotNil(t, client.Token)
assert.Equal(t, "test-token", client.Token.AccessToken)
}

func TestNewApiFailsWithIncompleteClientCredentials(t *testing.T) {
t.Setenv("SHOPWARE_CLI_CACHE_DIR", t.TempDir())
t.Setenv("SHOPWARE_CLI_ACCOUNT_CLIENT_ID", "test-client-id")

_, err := NewApi(t.Context())
assert.ErrorContains(t, err, "both SHOPWARE_CLI_ACCOUNT_CLIENT_ID and SHOPWARE_CLI_ACCOUNT_CLIENT_SECRET must be set")
}
6 changes: 6 additions & 0 deletions internal/account-api/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import "os"

const OIDCScopes = "openid offline_access email profile extension_management_read_write"

// ClientCredentialsScopes excludes openid as it is not allowed for client credentials grant.
const ClientCredentialsScopes = "extension_management_read_write"

func isStaging() bool {
return os.Getenv("SHOPWARE_CLI_ACCOUNT_STAGING") != ""
}

func getOIDCEndpoint() string {
if v := os.Getenv("SHOPWARE_CLI_OIDC_ENDPOINT"); v != "" {
return v
}
Comment thread
shyim marked this conversation as resolved.
Comment thread
shyim marked this conversation as resolved.
if isStaging() {
return "https://auth-api.shopware.in"
}
Expand Down