Skip to content
Open
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
2 changes: 1 addition & 1 deletion cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command
// If Keycloak is enabled, retrieve an OAuth token using Keycloak Client.
kc := connectors.NewKeycloakClient(keycloakURL, globalClientOpts.ClientId, globalClientOpts.ClientSecret)

oauthToken, err = kc.ConnectAndGetToken()
oauthToken, _, err = kc.ConnectAndGetToken()
if err != nil {
fmt.Printf("Got error when invoking Keycloak client: %s", err)
os.Exit(1)
Expand Down
2 changes: 1 addition & 1 deletion cmd/importURL.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func NewImportURLCommand(globalClientOpts *connectors.ClientOptions) *cobra.Comm
// If Keycloak is enabled, retrieve an OAuth token using Keycloak Client.
kc := connectors.NewKeycloakClient(keycloakURL, globalClientOpts.ClientId, globalClientOpts.ClientSecret)

oauthToken, err = kc.ConnectAndGetToken()
oauthToken, _, err = kc.ConnectAndGetToken()
if err != nil {
fmt.Printf("Got error when invoking Keycloak client: %s", err)
os.Exit(1)
Expand Down
28 changes: 26 additions & 2 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
var mc connectors.MicrocksClient
var serverAddr string

// kc and tokenExpiresAt are kept in scope so the polling loop can
// refresh the bearer token before it expires (headless mode only).
var kc connectors.KeycloakClient
var tokenExpiresAt int64
var headlessKeycloakEnabled bool

if globalClientOpts.ServerAddr != "" && globalClientOpts.ClientId != "" && globalClientOpts.ClientSecret != "" {

// create client with server address
Expand All @@ -128,13 +134,17 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
var oauthToken string = "unauthenticated-token"
if keycloakURL != "null" {
// If Keycloak is enabled, retrieve an OAuth token using Keycloak Client.
kc := connectors.NewKeycloakClient(keycloakURL, globalClientOpts.ClientId, globalClientOpts.ClientSecret)
headlessKeycloakEnabled = true
kc = connectors.NewKeycloakClient(keycloakURL, globalClientOpts.ClientId, globalClientOpts.ClientSecret)

oauthToken, err = kc.ConnectAndGetToken()
var expiresIn int
oauthToken, expiresIn, err = kc.ConnectAndGetToken()
if err != nil {
fmt.Printf("Got error when invoking Keycloak client: %s", err)
os.Exit(1)
}
// Schedule refresh 30 seconds before actual expiry to avoid races.
tokenExpiresAt = nowInMilliseconds() + int64(expiresIn-30)*1000
//fmt.Printf("Retrieve OAuthToken: %s", oauthToken)
}

Expand Down Expand Up @@ -186,6 +196,20 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {

var success = false
for nowInMilliseconds() < future {
// Proactively refresh the bearer token before it expires so that
// long-running ASYNC_API_SCHEMA tests (whose --waitFor can exceed
// the Keycloak access token lifetime) do not get spurious 401s.
if headlessKeycloakEnabled && nowInMilliseconds() >= tokenExpiresAt {
freshToken, freshExpiresIn, refreshErr := kc.ConnectAndGetToken()
if refreshErr != nil {
fmt.Printf("Got error refreshing Keycloak token: %s\n", refreshErr)
os.Exit(1)
}
mc.SetOAuthToken(freshToken)
tokenExpiresAt = nowInMilliseconds() + int64(freshExpiresIn-30)*1000
fmt.Println("MicrocksTester refreshed OAuth token for continued polling.")
}

testResultSummary, err := mc.GetTestResult(testResultID)
if err != nil {
fmt.Printf("Got error when invoking Microcks client check TestResult: %s", err)
Expand Down
32 changes: 24 additions & 8 deletions pkg/connectors/keycloak_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import (

// KeycloakClient defines methods for cinteracting with Keycloak
type KeycloakClient interface {
ConnectAndGetToken() (string, error)
// ConnectAndGetToken performs a client_credentials grant and returns the
// access token, its lifetime in seconds (expires_in), and any error.
ConnectAndGetToken() (string, int, error)
ConnectAndGetTokenAndRefreshToken(string, string) (string, string, error)
GetOIDCConfig() (*oauth2.Config, error)
}
Expand Down Expand Up @@ -69,13 +71,13 @@ func NewKeycloakClient(realmURL string, username string, password string) Keyclo
}

// ConnectAndGetToken implementation on keycloakClient structure
func (c *keycloakClient) ConnectAndGetToken() (string, error) {
func (c *keycloakClient) ConnectAndGetToken() (string, int, error) {
rel := &url.URL{Path: "protocol/openid-connect/token"}
u := c.BaseURL.ResolveReference(rel)

req, err := http.NewRequest("POST", u.String(), strings.NewReader(url.Values{"grant_type": {"client_credentials"}}.Encode()))
if err != nil {
return "", err
return "", 0, err
}

credential := base64.StdEncoding.EncodeToString([]byte(c.Username + ":" + c.Password))
Expand All @@ -88,7 +90,7 @@ func (c *keycloakClient) ConnectAndGetToken() (string, error) {

resp, err := c.httpClient.Do(req)
if err != nil {
return "", err
return "", 0, err
}
defer resp.Body.Close()

Expand All @@ -97,16 +99,30 @@ func (c *keycloakClient) ConnectAndGetToken() (string, error) {

body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err.Error())
return "", 0, err
}

if resp.StatusCode != http.StatusOK {
return "", 0, fmt.Errorf("Keycloak returned HTTP %d when requesting token: %s", resp.StatusCode, string(body))
}

var openIDResp map[string]interface{}
if err := json.Unmarshal(body, &openIDResp); err != nil {
panic(err)
return "", 0, fmt.Errorf("failed to parse Keycloak token response: %w", err)
}

accessToken, ok := openIDResp["access_token"].(string)
if !ok {
return "", 0, fmt.Errorf("missing or invalid access_token in Keycloak response")
}

// Default to 5 minutes if expires_in is absent or unparseable.
expiresIn := 300
if v, ok := openIDResp["expires_in"].(float64); ok && v > 0 {
expiresIn = int(v)
}

accessToken := openIDResp["access_token"].(string)
return accessToken, err
return accessToken, expiresIn, nil
}

func (c *keycloakClient) GetOIDCConfig() (*oauth2.Config, error) {
Expand Down
7 changes: 7 additions & 0 deletions pkg/connectors/microcks_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,13 @@ func (c *microcksClient) GetTestResult(testResultID string) (*TestResultSummary,
panic(err.Error())
}

if resp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("authentication failed (HTTP 401): bearer token has expired or is invalid — check Keycloak token lifetime vs --waitFor duration")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected HTTP %d from Microcks API while polling test result: %s", resp.StatusCode, string(body))
}

result := TestResultSummary{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse test result response: %w", err)
Expand Down