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
5 changes: 3 additions & 2 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ func (c *ApiClient) getAuthHeader(method, url string, authType string, username
}

// If no auth type is specified, try to use the first OAuth2 token
token := c.auth.TokenStore.GetFirstOAuth2Token()
// Use ForApp variants so the active app name (set via --app) is respected.
token := c.auth.TokenStore.GetFirstOAuth2TokenForApp(c.auth.AppName())
if token != nil {
accessToken, err := c.auth.GetOAuth2Header(username)
if err == nil {
Expand All @@ -355,7 +356,7 @@ func (c *ApiClient) getAuthHeader(method, url string, authType string, username
}

// If no OAuth2 token is available, try to use the first OAuth1 token
token = c.auth.TokenStore.GetOAuth1Tokens()
token = c.auth.TokenStore.GetOAuth1TokensForApp(c.auth.AppName())
if token != nil {
authHeader, err := c.auth.GetOAuth1Header(method, url, nil)
if err == nil {
Expand Down
79 changes: 79 additions & 0 deletions api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"

"github.com/xdevplatform/xurl/auth"
"github.com/xdevplatform/xurl/config"
Expand Down Expand Up @@ -356,3 +357,81 @@ func TestStreamRequest(t *testing.T) {
assert.True(t, xurlErrors.IsAPIError(err), "Expected API error")
})
}

// futureExpiry returns a unix timestamp 1 hour in the future.
func futureExpiry() uint64 {
return uint64(time.Now().Add(time.Hour).Unix())
}

// TC 5.3: ApiClient with multi-app Auth; app-b only has Bearer → BuildRequest uses app-b's Bearer
func TestTC5_3_ApiClientUsesAppBBearerNotDefaultOAuth2(t *testing.T) {
tempDir, err := os.MkdirTemp("", "xurl_api_multiapp_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

tempFile := filepath.Join(tempDir, ".xurl")
ts := &store.TokenStore{
Apps: make(map[string]*store.App),
DefaultApp: "app-a",
FilePath: tempFile,
}

// app-a: has OAuth2 (default/active for auto-selection cascade)
ts.Apps["app-a"] = &store.App{
ClientID: "id-a",
ClientSecret: "secret-a",
DefaultUser: "alice-a",
OAuth2Tokens: map[string]store.Token{
"alice-a": {
Type: store.OAuth2TokenType,
OAuth2: &store.OAuth2Token{
AccessToken: "oauth2-token-alice-a",
RefreshToken: "refresh-alice-a",
ExpirationTime: futureExpiry(),
},
},
},
BearerToken: &store.Token{
Type: store.BearerTokenType,
Bearer: "bearer-a",
},
}

// app-b: has ONLY Bearer token, no OAuth2
ts.Apps["app-b"] = &store.App{
ClientID: "id-b",
ClientSecret: "secret-b",
OAuth2Tokens: make(map[string]store.Token),
BearerToken: &store.Token{
Type: store.BearerTokenType,
Bearer: "bearer-b-only",
},
}

// Build Auth starting with app-a credentials
a := auth.NewAuth(&config.Config{
ClientID: "id-a",
ClientSecret: "secret-a",
APIBaseURL: "https://api.x.com",
AuthURL: "https://x.com/i/oauth2/authorize",
TokenURL: "https://api.x.com/2/oauth2/token",
RedirectURI: "http://localhost:8080/callback",
InfoURL: "https://api.x.com/2/users/me",
}).WithTokenStore(ts)

// Switch to app-b
a.WithAppName("app-b")

cfg := &config.Config{APIBaseURL: "https://api.x.com"}
client := NewApiClient(cfg, a)

req, err := client.BuildRequest(RequestOptions{
Method: "GET",
Endpoint: "/2/users/me",
})
require.NoError(t, err)

authHeader := req.Header.Get("Authorization")
assert.Equal(t, "Bearer bearer-b-only", authHeader,
"Authorization header must use app-b's Bearer token, not app-a's OAuth2")
}
31 changes: 16 additions & 15 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,20 @@ func (a *Auth) WithAppName(appName string) *Auth {
a.appName = appName
app := a.TokenStore.ResolveApp(appName)
if app != nil {
if a.clientID == "" {
a.clientID = app.ClientID
}
if a.clientSecret == "" {
a.clientSecret = app.ClientSecret
}
a.clientID = app.ClientID // unconditional override
a.clientSecret = app.ClientSecret // unconditional override
}
return a
}

// AppName returns the current explicit app name override.
func (a *Auth) AppName() string {
return a.appName
}

// GetOAuth1Header gets the OAuth1 header for a request
func (a *Auth) GetOAuth1Header(method, urlStr string, additionalParams map[string]string) (string, error) {
token := a.TokenStore.GetOAuth1Tokens()
token := a.TokenStore.GetOAuth1TokensForApp(a.appName)
if token == nil || token.OAuth1 == nil {
return "", xurlErrors.NewAuthError("TokenNotFound", errors.New("OAuth1 token not found"))
}
Expand Down Expand Up @@ -141,14 +142,14 @@ func (a *Auth) GetOAuth1Header(method, urlStr string, additionalParams map[strin
return "OAuth " + strings.Join(oauthParams, ", "), nil
}

// GetOAuth2Token gets or refreshes an OAuth2 token
// GetOAuth2Header gets or refreshes an OAuth2 token
func (a *Auth) GetOAuth2Header(username string) (string, error) {
var token *store.Token

if username != "" {
token = a.TokenStore.GetOAuth2Token(username)
token = a.TokenStore.GetOAuth2TokenForApp(a.appName, username)
} else {
token = a.TokenStore.GetFirstOAuth2Token()
token = a.TokenStore.GetFirstOAuth2TokenForApp(a.appName)
}

if token == nil {
Expand Down Expand Up @@ -253,7 +254,7 @@ func (a *Auth) OAuth2Flow(username string) (string, error) {

expirationTime := uint64(time.Now().Add(time.Duration(token.Expiry.Unix()-time.Now().Unix()) * time.Second).Unix())

err = a.TokenStore.SaveOAuth2Token(usernameStr, token.AccessToken, token.RefreshToken, expirationTime)
err = a.TokenStore.SaveOAuth2TokenForApp(a.appName, usernameStr, token.AccessToken, token.RefreshToken, expirationTime)
if err != nil {
return "", xurlErrors.NewAuthError("TokenStorageError", err)
}
Expand All @@ -266,9 +267,9 @@ func (a *Auth) RefreshOAuth2Token(username string) (string, error) {
var token *store.Token

if username != "" {
token = a.TokenStore.GetOAuth2Token(username)
token = a.TokenStore.GetOAuth2TokenForApp(a.appName, username)
} else {
token = a.TokenStore.GetFirstOAuth2Token()
token = a.TokenStore.GetFirstOAuth2TokenForApp(a.appName)
}

if token == nil || token.OAuth2 == nil {
Expand Down Expand Up @@ -310,7 +311,7 @@ func (a *Auth) RefreshOAuth2Token(username string) (string, error) {

expirationTime := uint64(time.Now().Add(time.Duration(newToken.Expiry.Unix()-time.Now().Unix()) * time.Second).Unix())

err = a.TokenStore.SaveOAuth2Token(usernameStr, newToken.AccessToken, newToken.RefreshToken, expirationTime)
err = a.TokenStore.SaveOAuth2TokenForApp(a.appName, usernameStr, newToken.AccessToken, newToken.RefreshToken, expirationTime)
if err != nil {
return "", xurlErrors.NewAuthError("RefreshTokenError", err)
}
Expand All @@ -320,7 +321,7 @@ func (a *Auth) RefreshOAuth2Token(username string) (string, error) {

// GetBearerTokenHeader gets the bearer token from the token store
func (a *Auth) GetBearerTokenHeader() (string, error) {
token := a.TokenStore.GetBearerToken()
token := a.TokenStore.GetBearerTokenForApp(a.appName)
if token == nil {
return "", xurlErrors.NewAuthError("TokenNotFound", errors.New("bearer token not found"))
}
Expand Down
Loading