From 8a3324a25625b99a7d1d41ced275e5414ac20f84 Mon Sep 17 00:00:00 2001 From: Tanner Oakes Date: Thu, 7 May 2026 11:32:43 -0600 Subject: [PATCH] add support for credentials v3 --- README.md | 31 ++++++++++++-------- auth.go | 35 +++++++++++++++++------ client.go | 3 +- client_test.go | 69 ++++++++++++++++++++++++++++++++++++++++----- example_test.go | 4 +-- marketplace.go | 67 +++++++++++++++++++++++++------------------ marketplace_test.go | 47 +++++++++++++++--------------- server.go | 23 ++++++++++++--- server_test.go | 16 +++++------ 9 files changed, 202 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 36f1ce8..642d8af 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,18 @@ Query constructor marketplace arguments are retained only for compatibility and Creators API credentials are region-group scoped. Use credentials that match the marketplace group you call. -| Credential Version | Region group | Example marketplaces | -|---|---|---| -| `2.1` | North America | `www.amazon.com`, `www.amazon.ca`, `www.amazon.com.mx`, `www.amazon.com.br` | -| `2.2` | Europe / Middle East / India | `www.amazon.co.uk`, `www.amazon.de`, `www.amazon.fr`, `www.amazon.in`, `www.amazon.sa`, `www.amazon.ae` | -| `2.3` | Far East | `www.amazon.co.jp`, `www.amazon.sg`, `www.amazon.com.au` | +New applications typically receive **v3.x** credentials (Login with Amazon token endpoints). **v2.x** credentials use regional Cognito endpoints and a different OAuth scope and catalog `Authorization` header shape; if Associates Central still shows `2.1`/`2.2`/`2.3`, pass `creatorsapi.WithCredentialVersion("2.1")` (etc.) on `CreateClient`. + +| Credential Version | Region group | Token endpoint (default) | Catalog `Authorization` | +|---|---|---|---| +| `3.1` | North America | `https://api.amazon.com/auth/o2/token` | `Bearer ` | +| `3.2` | Europe / Middle East / India | `https://api.amazon.co.uk/auth/o2/token` | `Bearer ` | +| `3.3` | Far East | `https://api.amazon.co.jp/auth/o2/token` | `Bearer ` | +| `2.1` | North America | `https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token` | `Bearer , Version 2.1` | +| `2.2` | Europe / Middle East / India | `https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token` | `Bearer , Version 2.2` | +| `2.3` | Far East | `https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token` | `Bearer , Version 2.3` | + +Example marketplace groups (same as before): NA — `www.amazon.com`, `www.amazon.ca`, `www.amazon.com.mx`, `www.amazon.com.br`; EU — `www.amazon.co.uk`, `www.amazon.de`, `www.amazon.fr`, `www.amazon.in`, `www.amazon.sa`, `www.amazon.ae`; FE — `www.amazon.co.jp`, `www.amazon.sg`, `www.amazon.com.au`. If token acquisition fails with an auth error, verify that your credential version and marketplace group match. @@ -83,7 +90,7 @@ Recommended client behavior: ### Getting Creators API credentials -Only the primary Amazon Associates account owner can mint credentials. From [Associates Central][creatorsapi-portal] go to **Tools** → **Creators API** → **Create Application**, then **Add New Credential**. Copy the Credential Secret immediately — it is only shown once. Note the **Credential Version** (`2.1` for North America, `2.2` for Europe, `2.3` for Far East) — you'll need it if you call multiple regions from the same process. +Only the primary Amazon Associates account owner can mint credentials. From [Associates Central][creatorsapi-portal] go to **Tools** → **Creators API** → **Create Application**, then **Add New Credential**. Copy the Credential Secret immediately — it is only shown once. Note the **Credential Version** shown for your credential (`3.1`/`3.2`/`3.3` for Login with Amazon, or legacy `2.1`/`2.2`/`2.3` for Cognito). You'll need it if you call multiple regions from the same process or if your credential region group does not match the configured marketplace. ### Quick migration checklist @@ -94,7 +101,7 @@ Use this path when migrating existing PA-API v5 call sites: 3. Configure marketplace on `Server`/`Client` (`WithMarketplace`) and do not rely on per-query marketplace arguments. 4. Replace V1 offers usage with OffersV2 (`EnableOffersV2`; `EnableOffers` remains as a compatibility alias). 5. Remove expectations around `Merchant`, `OfferCount`, and `PartnerType` request effects; these are ignored. -6. Confirm Credential Version (`2.1`/`2.2`/`2.3`) matches the marketplace group you call. +6. Confirm Credential Version matches the marketplace group you call (`3.1`/`3.2`/`3.3` by default per marketplace, or `2.1`/`2.2`/`2.3` for legacy Cognito credentials via `WithCredentialVersion`). 7. Add retry and rate-limit control for `429` and transient `5xx` responses. 8. Run local verification with your project's standard test/lint workflow before opening a PR. @@ -103,28 +110,28 @@ Use this path when migrating existing PA-API v5 call sites: ### Create a server configuration ```go -sv := creatorsapi.New() // default: US marketplace, NA credential version 2.1 +sv := creatorsapi.New() // default: US marketplace, NA credential version 3.1 (Login with Amazon) fmt.Println("Marketplace:", sv.Marketplace()) fmt.Println("CredentialVersion:", sv.CredentialVersion()) fmt.Println("URL:", sv.URL(creatorsapi.GetItems.Path())) // Output: // Marketplace: www.amazon.com -// CredentialVersion: 2.1 +// CredentialVersion: 3.1 // URL: https://creatorsapi.amazon/catalog/v1/getItems ``` For another marketplace: ```go -sv := creatorsapi.New(creatorsapi.WithMarketplace(creatorsapi.LocaleJapan)) // Japan -> credential version 2.3 +sv := creatorsapi.New(creatorsapi.WithMarketplace(creatorsapi.LocaleJapan)) // Japan -> credential version 3.3 fmt.Println("Marketplace:", sv.Marketplace()) fmt.Println("CredentialVersion:", sv.CredentialVersion()) // Output: // Marketplace: www.amazon.co.jp -// CredentialVersion: 2.3 +// CredentialVersion: 3.3 ``` -The credential version is auto-derived from the configured marketplace's region group. Override it explicitly with `creatorsapi.WithCredentialVersion("2.2")` when needed. +The credential version is auto-derived from the configured marketplace's region group (`3.1`/`3.2`/`3.3`). Override with `creatorsapi.WithCredentialVersion("2.2")` when using legacy Cognito credentials (`2.1`/`2.2`/`2.3`), or when your credential group differs from the marketplace default. ### Create a client diff --git a/auth.go b/auth.go index 776254a..9874c40 100644 --- a/auth.go +++ b/auth.go @@ -2,6 +2,7 @@ package paapi5 import ( "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -15,8 +16,10 @@ import ( ) const ( - // oauthScope is the scope requested when fetching an access token. - oauthScope = "creatorsapi/default" + // oauthScopeCognito is the scope for v2.x (Cognito /oauth2/token). + oauthScopeCognito = "creatorsapi/default" + // oauthScopeLWA is the scope for v3.x (Login with Amazon /auth/o2/token). + oauthScopeLWA = "creatorsapi::default" // oauthGrantType is the OAuth2 grant type used for the client_credentials flow. oauthGrantType = "client_credentials" // oauthTokenLeewaySeconds keeps a small buffer before the announced expiration @@ -38,6 +41,7 @@ type tokenManager struct { endpoint string clientID string clientSecret string + lwa bool mu sync.Mutex accessToken string @@ -46,7 +50,7 @@ type tokenManager struct { // newTokenManager constructs a tokenManager. httpClient may be nil, in which // case http.DefaultClient is used. -func newTokenManager(httpClient *http.Client, endpoint, clientID, clientSecret string) *tokenManager { +func newTokenManager(httpClient *http.Client, endpoint, clientID, clientSecret string, lwa bool) *tokenManager { if httpClient == nil { httpClient = http.DefaultClient } @@ -55,6 +59,7 @@ func newTokenManager(httpClient *http.Client, endpoint, clientID, clientSecret s endpoint: endpoint, clientID: clientID, clientSecret: clientSecret, + lwa: lwa, } } @@ -96,15 +101,23 @@ func (t *tokenManager) refreshLocked(ctx context.Context) error { } form := url.Values{} form.Set("grant_type", oauthGrantType) - form.Set("client_id", t.clientID) - form.Set("client_secret", t.clientSecret) - form.Set("scope", oauthScope) + if t.lwa { + form.Set("scope", oauthScopeLWA) + } else { + form.Set("client_id", t.clientID) + form.Set("client_secret", t.clientSecret) + form.Set("scope", oauthScopeCognito) + } req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.endpoint, strings.NewReader(form.Encode())) if err != nil { return errs.Wrap(err, errs.WithContext("endpoint", t.endpoint)) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") + if t.lwa { + basic := base64.StdEncoding.EncodeToString([]byte(t.clientID + ":" + t.clientSecret)) + req.Header.Set("Authorization", "Basic "+basic) + } resp, err := t.httpClient.Do(req) if err != nil { return errs.Wrap(err, errs.WithContext("endpoint", t.endpoint)) @@ -163,9 +176,13 @@ func truncateForLog(b []byte, max int) string { return string(b[:max]) + "...(truncated)" } -// authorizationHeader returns the value of the Authorization header expected -// by the Creators API: `Bearer , Version `. -func authorizationHeader(token, version string) string { +// authorizationHeader returns the Creators API catalog Authorization header. +// v3.x (Login with Amazon) tokens use `Bearer ` only; v2.x Cognito +// tokens append `, Version `. +func authorizationHeader(token, version string, lwa bool) string { + if lwa { + return "Bearer " + token + } b := strings.Builder{} b.WriteString("Bearer ") b.WriteString(token) diff --git a/client.go b/client.go index 59a01c4..ed73d03 100644 --- a/client.go +++ b/client.go @@ -41,6 +41,7 @@ type client struct { credentialSecret string version string authEndpoint string + lwaFlow bool auth *tokenManager } @@ -98,7 +99,7 @@ func (c *client) post(ctx context.Context, cmd Operation, payload []byte) ([]byt fetch.WithRequestHeaderSet("Accept", c.server.Accept()), fetch.WithRequestHeaderSet("Content-Type", c.server.ContentType()), fetch.WithRequestHeaderSet(marketplaceHeader, c.server.Marketplace()), - fetch.WithRequestHeaderSet("Authorization", authorizationHeader(token, c.version)), + fetch.WithRequestHeaderSet("Authorization", authorizationHeader(token, c.version, c.lwaFlow)), ) if err != nil { return nil, errs.Wrap(err, errs.WithContext("url", u.String()), errs.WithContext("payload", string(payload))) diff --git a/client_test.go b/client_test.go index 2ee5cd6..3774188 100644 --- a/client_test.go +++ b/client_test.go @@ -2,6 +2,7 @@ package paapi5 import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -58,7 +59,7 @@ func TestClientBasics(t *testing.T) { if !ok { t.Fatalf("Client is not *client: %T", c) } - if got, want := cc.version, CredentialVersionNA; got != want { + if got, want := cc.version, CredentialVersionNAv3; got != want { t.Errorf("client.version = %q, want %q", got, want) } } @@ -73,18 +74,30 @@ func TestClientRequestSendsExpectedHeadersAndBody(t *testing.T) { if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { t.Errorf("token Content-Type = %q, want %q", got, want) } + authz := r.Header.Get("Authorization") + if !strings.HasPrefix(authz, "Basic ") { + t.Fatalf("token Authorization = %q, want Basic …", authz) + } + raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authz, "Basic ")) + if err != nil { + t.Fatalf("decode Basic auth: %v", err) + } + if got, want := string(raw), "credID:credSecret"; got != want { + t.Errorf("Basic credentials = %q, want %q", got, want) + } body, _ := io.ReadAll(r.Body) form := string(body) for _, want := range []string{ "grant_type=client_credentials", - "client_id=credID", - "client_secret=credSecret", - "scope=creatorsapi%2Fdefault", + "scope=creatorsapi%3A%3Adefault", } { if !strings.Contains(form, want) { t.Errorf("token request body %q missing %q", form, want) } } + if strings.Contains(form, "client_id=") || strings.Contains(form, "client_secret=") { + t.Errorf("LwA token request body must not embed client_id/client_secret: %q", form) + } _ = json.NewEncoder(w).Encode(map[string]any{ "access_token": "tok-abc", "expires_in": 3600, @@ -101,7 +114,7 @@ func TestClientRequestSendsExpectedHeadersAndBody(t *testing.T) { if got, want := r.URL.Path, "/catalog/v1/getItems"; got != want { t.Errorf("api path = %q, want %q", got, want) } - if got, want := r.Header.Get("Authorization"), "Bearer tok-abc, Version 2.1"; got != want { + if got, want := r.Header.Get("Authorization"), "Bearer tok-abc"; got != want { t.Errorf("Authorization header = %q, want %q", got, want) } if got, want := r.Header.Get(marketplaceHeader), "www.amazon.com"; got != want { @@ -151,6 +164,45 @@ func TestClientRequestSendsExpectedHeadersAndBody(t *testing.T) { } } +// TestClientLegacyCognitoCredentialVersion verifies v2.x Cognito credentials: +// client id/secret in the POST body, slash scope, and Version on catalog calls. +func TestClientLegacyCognitoCredentialVersion(t *testing.T) { + tokenHandler := func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Authorization"), ""; got != want { + t.Errorf("Cognito token request should not send Authorization header, got %q", got) + } + body, _ := io.ReadAll(r.Body) + form := string(body) + for _, want := range []string{ + "grant_type=client_credentials", + "client_id=id", + "client_secret=secret", + "scope=creatorsapi%2Fdefault", + } { + if !strings.Contains(form, want) { + t.Errorf("token body %q missing %q", form, want) + } + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "access_token": "tok-v2", + "expires_in": 3600, + }) + } + apiHandler := func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Authorization"), "Bearer tok-v2, Version 2.2"; got != want { + t.Errorf("Authorization = %q, want %q", got, want) + } + _, _ = w.Write([]byte("{}")) + } + _, _, sv := newServers(t, tokenHandler, apiHandler) + c := sv.CreateClient("tag", "id", "secret", WithCredentialVersion(CredentialVersionEU)) + + q := stubQuery{op: GetItems, payload: []byte("{}")} + if _, err := c.RequestContext(context.Background(), q); err != nil { + t.Fatalf("RequestContext: %v", err) + } +} + func TestClientTokenRefreshAfterExpiry(t *testing.T) { var tokenCalls int32 tokenHandler := func(w http.ResponseWriter, r *http.Request) { @@ -300,12 +352,15 @@ func TestClientNilQueryError(t *testing.T) { } func TestAuthorizationHeader(t *testing.T) { - if got, want := authorizationHeader("abc", "2.1"), "Bearer abc, Version 2.1"; got != want { + if got, want := authorizationHeader("abc", "2.1", false), "Bearer abc, Version 2.1"; got != want { t.Errorf("authorizationHeader = %q, want %q", got, want) } - if got, want := authorizationHeader("abc", ""), "Bearer abc"; got != want { + if got, want := authorizationHeader("abc", "", false), "Bearer abc"; got != want { t.Errorf("authorizationHeader (no version) = %q, want %q", got, want) } + if got, want := authorizationHeader("abc", "3.1", true), "Bearer abc"; got != want { + t.Errorf("authorizationHeader (LwA) = %q, want %q", got, want) + } } /* Copyright 2019,2020 Spiegel diff --git a/example_test.go b/example_test.go index b672309..7c470d5 100644 --- a/example_test.go +++ b/example_test.go @@ -19,7 +19,7 @@ func ExampleServer() { // Marketplace: www.amazon.com // Region: us-east-1 // AcceptLanguage: en_US - // CredentialVersion: 2.1 + // CredentialVersion: 3.1 // URL: https://creatorsapi.amazon/catalog/v1/getItems } @@ -34,7 +34,7 @@ func ExampleNew() { // Marketplace: www.amazon.co.jp // Region: us-west-2 // AcceptLanguage: ja_JP - // CredentialVersion: 2.3 + // CredentialVersion: 3.3 // URL: https://creatorsapi.amazon/catalog/v1/getItems } diff --git a/marketplace.go b/marketplace.go index 56ba0f2..e7f9a3f 100644 --- a/marketplace.go +++ b/marketplace.go @@ -1,12 +1,23 @@ package paapi5 -// Credential version codes for the Amazon Creators API. The version selects -// which regional Cognito token endpoint a credential pair authenticates -// against, and is also echoed back to the API in the Authorization header. +// Credential version codes for the Amazon Creators API. +// +// v2.x values select the regional Cognito (`…amazoncognito.com/oauth2/token`) +// token endpoint and require `Authorization: Bearer , Version ` on +// catalog requests. +// +// v3.x values select the regional Login with Amazon (`api.amazon…/auth/o2/token`) +// endpoint; tokens are obtained with HTTP Basic auth and scope +// `creatorsapi::default`, and catalog requests use `Authorization: Bearer +// ` without a Version suffix. const ( - CredentialVersionNA = "2.1" // North America (US, CA, MX, BR) - CredentialVersionEU = "2.2" // Europe / Middle East / India - CredentialVersionFE = "2.3" // Far East (JP, SG, AU) + CredentialVersionNA = "2.1" // North America — Cognito pool (legacy) + CredentialVersionEU = "2.2" // Europe / Middle East / India — Cognito + CredentialVersionFE = "2.3" // Far East — Cognito + + CredentialVersionNAv3 = "3.1" // North America — Login with Amazon + CredentialVersionEUv3 = "3.2" // Europe / Middle East / India — LwA + CredentialVersionFEv3 = "3.3" // Far East — LwA ) // Marketplace is the interface implemented by locale providers. @@ -25,7 +36,7 @@ type Marketplace interface { // credentialVersioner is an optional, internal interface satisfied by // Marketplace implementations that can report their Creators API credential -// version (the region group code: 2.1, 2.2, or 2.3). MarketplaceEnum +// version (the region group code: 2.1–2.3 or 3.1–3.3). MarketplaceEnum // satisfies it; external implementations may opt in. type credentialVersioner interface { CredentialVersion() string @@ -150,27 +161,27 @@ var languageMap = map[MarketplaceEnum]string{ // region-scoped, not marketplace-scoped, so several marketplaces share a // single version code. var versionMap = map[MarketplaceEnum]string{ - LocaleUnitedStates: CredentialVersionNA, - LocaleCanada: CredentialVersionNA, - LocaleMexico: CredentialVersionNA, - LocaleBrazil: CredentialVersionNA, - LocaleUnitedKingdom: CredentialVersionEU, - LocaleGermany: CredentialVersionEU, - LocaleFrance: CredentialVersionEU, - LocaleItaly: CredentialVersionEU, - LocaleSpain: CredentialVersionEU, - LocaleNetherlands: CredentialVersionEU, - LocaleEgypt: CredentialVersionEU, - LocaleIndia: CredentialVersionEU, - LocaleIreland: CredentialVersionEU, - LocalePoland: CredentialVersionEU, - LocaleSaudiArabia: CredentialVersionEU, - LocaleSweden: CredentialVersionEU, - LocaleTurkey: CredentialVersionEU, - LocaleUnitedArabEmirates: CredentialVersionEU, - LocaleJapan: CredentialVersionFE, - LocaleSingapore: CredentialVersionFE, - LocaleAustralia: CredentialVersionFE, + LocaleUnitedStates: CredentialVersionNAv3, + LocaleCanada: CredentialVersionNAv3, + LocaleMexico: CredentialVersionNAv3, + LocaleBrazil: CredentialVersionNAv3, + LocaleUnitedKingdom: CredentialVersionEUv3, + LocaleGermany: CredentialVersionEUv3, + LocaleFrance: CredentialVersionEUv3, + LocaleItaly: CredentialVersionEUv3, + LocaleSpain: CredentialVersionEUv3, + LocaleNetherlands: CredentialVersionEUv3, + LocaleEgypt: CredentialVersionEUv3, + LocaleIndia: CredentialVersionEUv3, + LocaleIreland: CredentialVersionEUv3, + LocalePoland: CredentialVersionEUv3, + LocaleSaudiArabia: CredentialVersionEUv3, + LocaleSweden: CredentialVersionEUv3, + LocaleTurkey: CredentialVersionEUv3, + LocaleUnitedArabEmirates: CredentialVersionEUv3, + LocaleJapan: CredentialVersionFEv3, + LocaleSingapore: CredentialVersionFEv3, + LocaleAustralia: CredentialVersionFEv3, } // MarketplaceOf function returns Marketplace instance from service domain. diff --git a/marketplace_test.go b/marketplace_test.go index bebce15..0ce5924 100644 --- a/marketplace_test.go +++ b/marketplace_test.go @@ -12,28 +12,28 @@ func TestMarketplace(t *testing.T) { language string version string }{ - {name: "www.amazon.com.au", marketplace: LocaleAustralia, str: "www.amazon.com.au", hostName: defaultHost, region: "us-west-2", language: "en_AU", version: CredentialVersionFE}, - {name: "www.amazon.com.br", marketplace: LocaleBrazil, str: "www.amazon.com.br", hostName: defaultHost, region: "us-east-1", language: "pt_BR", version: CredentialVersionNA}, - {name: "www.amazon.ca", marketplace: LocaleCanada, str: "www.amazon.ca", hostName: defaultHost, region: "us-east-1", language: "en_CA", version: CredentialVersionNA}, - {name: "www.amazon.eg", marketplace: LocaleEgypt, str: "www.amazon.eg", hostName: defaultHost, region: "us-west-1", language: "ar_EG", version: CredentialVersionEU}, - {name: "www.amazon.fr", marketplace: LocaleFrance, str: "www.amazon.fr", hostName: defaultHost, region: "eu-west-1", language: "fr_FR", version: CredentialVersionEU}, - {name: "www.amazon.de", marketplace: LocaleGermany, str: "www.amazon.de", hostName: defaultHost, region: "eu-west-1", language: "de_DE", version: CredentialVersionEU}, - {name: "www.amazon.in", marketplace: LocaleIndia, str: "www.amazon.in", hostName: defaultHost, region: "eu-west-1", language: "en_IN", version: CredentialVersionEU}, - {name: "www.amazon.ie", marketplace: LocaleIreland, str: "www.amazon.ie", hostName: defaultHost, region: "eu-west-1", language: "en_IE", version: CredentialVersionEU}, - {name: "www.amazon.it", marketplace: LocaleItaly, str: "www.amazon.it", hostName: defaultHost, region: "eu-west-1", language: "it_IT", version: CredentialVersionEU}, - {name: "www.amazon.co.jp", marketplace: LocaleJapan, str: "www.amazon.co.jp", hostName: defaultHost, region: "us-west-2", language: "ja_JP", version: CredentialVersionFE}, - {name: "www.amazon.com.mx", marketplace: LocaleMexico, str: "www.amazon.com.mx", hostName: defaultHost, region: "us-east-1", language: "es_MX", version: CredentialVersionNA}, - {name: "www.amazon.nl", marketplace: LocaleNetherlands, str: "www.amazon.nl", hostName: defaultHost, region: "eu-west-1", language: "nl_NL", version: CredentialVersionEU}, - {name: "www.amazon.pl", marketplace: LocalePoland, str: "www.amazon.pl", hostName: defaultHost, region: "eu-west-1", language: "pl_PL", version: CredentialVersionEU}, - {name: "www.amazon.sg", marketplace: LocaleSingapore, str: "www.amazon.sg", hostName: defaultHost, region: "us-west-2", language: "en_SG", version: CredentialVersionFE}, - {name: "www.amazon.sa", marketplace: LocaleSaudiArabia, str: "www.amazon.sa", hostName: defaultHost, region: "eu-west-1", language: "en_AE", version: CredentialVersionEU}, - {name: "www.amazon.es", marketplace: LocaleSpain, str: "www.amazon.es", hostName: defaultHost, region: "eu-west-1", language: "es_ES", version: CredentialVersionEU}, - {name: "www.amazon.se", marketplace: LocaleSweden, str: "www.amazon.se", hostName: defaultHost, region: "eu-west-1", language: "sv_SE", version: CredentialVersionEU}, - {name: "www.amazon.com.tr", marketplace: LocaleTurkey, str: "www.amazon.com.tr", hostName: defaultHost, region: "eu-west-1", language: "tr_TR", version: CredentialVersionEU}, - {name: "www.amazon.ae", marketplace: LocaleUnitedArabEmirates, str: "www.amazon.ae", hostName: defaultHost, region: "eu-west-1", language: "en_AE", version: CredentialVersionEU}, - {name: "www.amazon.co.uk", marketplace: LocaleUnitedKingdom, str: "www.amazon.co.uk", hostName: defaultHost, region: "eu-west-1", language: "en_GB", version: CredentialVersionEU}, - {name: "www.amazon.com", marketplace: LocaleUnitedStates, str: "www.amazon.com", hostName: defaultHost, region: "us-east-1", language: "en_US", version: CredentialVersionNA}, - {name: "foo.bar", marketplace: LocaleUnknown, str: "www.amazon.com", hostName: defaultHost, region: "us-east-1", language: "en_US", version: CredentialVersionNA}, + {name: "www.amazon.com.au", marketplace: LocaleAustralia, str: "www.amazon.com.au", hostName: defaultHost, region: "us-west-2", language: "en_AU", version: CredentialVersionFEv3}, + {name: "www.amazon.com.br", marketplace: LocaleBrazil, str: "www.amazon.com.br", hostName: defaultHost, region: "us-east-1", language: "pt_BR", version: CredentialVersionNAv3}, + {name: "www.amazon.ca", marketplace: LocaleCanada, str: "www.amazon.ca", hostName: defaultHost, region: "us-east-1", language: "en_CA", version: CredentialVersionNAv3}, + {name: "www.amazon.eg", marketplace: LocaleEgypt, str: "www.amazon.eg", hostName: defaultHost, region: "us-west-1", language: "ar_EG", version: CredentialVersionEUv3}, + {name: "www.amazon.fr", marketplace: LocaleFrance, str: "www.amazon.fr", hostName: defaultHost, region: "eu-west-1", language: "fr_FR", version: CredentialVersionEUv3}, + {name: "www.amazon.de", marketplace: LocaleGermany, str: "www.amazon.de", hostName: defaultHost, region: "eu-west-1", language: "de_DE", version: CredentialVersionEUv3}, + {name: "www.amazon.in", marketplace: LocaleIndia, str: "www.amazon.in", hostName: defaultHost, region: "eu-west-1", language: "en_IN", version: CredentialVersionEUv3}, + {name: "www.amazon.ie", marketplace: LocaleIreland, str: "www.amazon.ie", hostName: defaultHost, region: "eu-west-1", language: "en_IE", version: CredentialVersionEUv3}, + {name: "www.amazon.it", marketplace: LocaleItaly, str: "www.amazon.it", hostName: defaultHost, region: "eu-west-1", language: "it_IT", version: CredentialVersionEUv3}, + {name: "www.amazon.co.jp", marketplace: LocaleJapan, str: "www.amazon.co.jp", hostName: defaultHost, region: "us-west-2", language: "ja_JP", version: CredentialVersionFEv3}, + {name: "www.amazon.com.mx", marketplace: LocaleMexico, str: "www.amazon.com.mx", hostName: defaultHost, region: "us-east-1", language: "es_MX", version: CredentialVersionNAv3}, + {name: "www.amazon.nl", marketplace: LocaleNetherlands, str: "www.amazon.nl", hostName: defaultHost, region: "eu-west-1", language: "nl_NL", version: CredentialVersionEUv3}, + {name: "www.amazon.pl", marketplace: LocalePoland, str: "www.amazon.pl", hostName: defaultHost, region: "eu-west-1", language: "pl_PL", version: CredentialVersionEUv3}, + {name: "www.amazon.sg", marketplace: LocaleSingapore, str: "www.amazon.sg", hostName: defaultHost, region: "us-west-2", language: "en_SG", version: CredentialVersionFEv3}, + {name: "www.amazon.sa", marketplace: LocaleSaudiArabia, str: "www.amazon.sa", hostName: defaultHost, region: "eu-west-1", language: "en_AE", version: CredentialVersionEUv3}, + {name: "www.amazon.es", marketplace: LocaleSpain, str: "www.amazon.es", hostName: defaultHost, region: "eu-west-1", language: "es_ES", version: CredentialVersionEUv3}, + {name: "www.amazon.se", marketplace: LocaleSweden, str: "www.amazon.se", hostName: defaultHost, region: "eu-west-1", language: "sv_SE", version: CredentialVersionEUv3}, + {name: "www.amazon.com.tr", marketplace: LocaleTurkey, str: "www.amazon.com.tr", hostName: defaultHost, region: "eu-west-1", language: "tr_TR", version: CredentialVersionEUv3}, + {name: "www.amazon.ae", marketplace: LocaleUnitedArabEmirates, str: "www.amazon.ae", hostName: defaultHost, region: "eu-west-1", language: "en_AE", version: CredentialVersionEUv3}, + {name: "www.amazon.co.uk", marketplace: LocaleUnitedKingdom, str: "www.amazon.co.uk", hostName: defaultHost, region: "eu-west-1", language: "en_GB", version: CredentialVersionEUv3}, + {name: "www.amazon.com", marketplace: LocaleUnitedStates, str: "www.amazon.com", hostName: defaultHost, region: "us-east-1", language: "en_US", version: CredentialVersionNAv3}, + {name: "foo.bar", marketplace: LocaleUnknown, str: "www.amazon.com", hostName: defaultHost, region: "us-east-1", language: "en_US", version: CredentialVersionNAv3}, } for _, tc := range testCases { m := MarketplaceOf(tc.name) @@ -85,6 +85,9 @@ func TestAuthEndpointFor(t *testing.T) { {version: CredentialVersionNA, endpoint: "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token"}, {version: CredentialVersionEU, endpoint: "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token"}, {version: CredentialVersionFE, endpoint: "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token"}, + {version: CredentialVersionNAv3, endpoint: "https://api.amazon.com/auth/o2/token"}, + {version: CredentialVersionEUv3, endpoint: "https://api.amazon.co.uk/auth/o2/token"}, + {version: CredentialVersionFEv3, endpoint: "https://api.amazon.co.jp/auth/o2/token"}, {version: "9.9", endpoint: ""}, } for _, tc := range testCases { diff --git a/server.go b/server.go index 0d5edad..014f05b 100644 --- a/server.go +++ b/server.go @@ -18,8 +18,8 @@ const ( defaultHost = "creatorsapi.amazon" ) -// authEndpointMap maps a Creators API credential version to the OAuth2 -// (Cognito) token endpoint URL that issues tokens for that version. These +// authEndpointMap maps a Creators API credential version to the OAuth2 token +// endpoint URL (regional Cognito for v2.x, Login with Amazon for v3.x). These // are public Amazon endpoint URLs, not credentials — the gosec G101 // hardcoded-credentials check fires on the `oauth2/token` substring. // @@ -28,6 +28,10 @@ var authEndpointMap = map[string]string{ CredentialVersionNA: "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token", CredentialVersionEU: "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token", CredentialVersionFE: "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token", + + CredentialVersionNAv3: "https://api.amazon.com/auth/o2/token", + CredentialVersionEUv3: "https://api.amazon.co.uk/auth/o2/token", + CredentialVersionFEv3: "https://api.amazon.co.jp/auth/o2/token", } // AuthEndpointFor returns the default OAuth2 token endpoint URL for the @@ -41,6 +45,15 @@ func isSupportedCredentialVersion(version string) bool { return ok } +func isLWACredentialVersion(version string) bool { + switch version { + case CredentialVersionNAv3, CredentialVersionEUv3, CredentialVersionFEv3: + return true + default: + return false + } +} + // Server type is a configuration of the Amazon Creators API service. type Server struct { scheme string @@ -253,6 +266,7 @@ func (s *Server) CreateClient(associateTag, credentialID, credentialSecret strin credentialID: credentialID, credentialSecret: credentialSecret, version: s.CredentialVersion(), + lwaFlow: false, // Carry only an explicit server-level override; leaving the // auto-derived endpoint out lets WithCredentialVersion (applied // below) re-resolve through AuthEndpointFor with the new version. @@ -270,7 +284,8 @@ func (s *Server) CreateClient(associateTag, credentialID, credentialSecret strin if len(cli.authEndpoint) == 0 { cli.authEndpoint = AuthEndpointFor(cli.version) } - cli.auth = newTokenManager(cli.tokenHTTPClient, cli.authEndpoint, cli.credentialID, cli.credentialSecret) + cli.lwaFlow = isLWACredentialVersion(cli.version) + cli.auth = newTokenManager(cli.tokenHTTPClient, cli.authEndpoint, cli.credentialID, cli.credentialSecret, cli.lwaFlow) return cli } @@ -305,7 +320,7 @@ func WithCredentialVersion(version string) ClientOptFunc { } // WithAuthEndpoint overrides the OAuth2 token endpoint. Defaults to the -// Cognito endpoint resolved from the credential version. +// token endpoint resolved from the credential version (or LWA for v3.x). func WithAuthEndpoint(endpoint string) ClientOptFunc { return func(c *client) { if c != nil && len(endpoint) > 0 { diff --git a/server_test.go b/server_test.go index e614c4e..259ab30 100644 --- a/server_test.go +++ b/server_test.go @@ -23,8 +23,8 @@ func TestServer(t *testing.T) { accept: defaultAccept, acceptLanguage: "en_US", contentType: defaultContentType, - version: CredentialVersionNA, - authEndpoint: "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token", + version: CredentialVersionNAv3, + authEndpoint: "https://api.amazon.com/auth/o2/token", url: "https://creatorsapi.amazon/catalog/v1/getItems", }, { @@ -35,8 +35,8 @@ func TestServer(t *testing.T) { accept: defaultAccept, acceptLanguage: "ja_JP", contentType: defaultContentType, - version: CredentialVersionFE, - authEndpoint: "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token", + version: CredentialVersionFEv3, + authEndpoint: "https://api.amazon.co.jp/auth/o2/token", url: "https://creatorsapi.amazon/catalog/v1/getItems", }, { @@ -47,8 +47,8 @@ func TestServer(t *testing.T) { accept: defaultAccept, acceptLanguage: "de_DE", contentType: defaultContentType, - version: CredentialVersionEU, - authEndpoint: "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token", + version: CredentialVersionEUv3, + authEndpoint: "https://api.amazon.co.uk/auth/o2/token", url: "https://creatorsapi.amazon/catalog/v1/getItems", }, } @@ -111,10 +111,10 @@ func TestWithCredentialVersionIgnoresUnsupportedValue(t *testing.T) { if !ok { t.Fatalf("Client is not *client: %T", c) } - if got, want := cc.version, CredentialVersionNA; got != want { + if got, want := cc.version, CredentialVersionNAv3; got != want { t.Errorf("client.version = %q, want %q", got, want) } - if got, want := cc.authEndpoint, AuthEndpointFor(CredentialVersionNA); got != want { + if got, want := cc.authEndpoint, AuthEndpointFor(CredentialVersionNAv3); got != want { t.Errorf("client.authEndpoint = %q, want %q", got, want) } }