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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
go-version: "1.25"
- run: go list -json > go.list
- name: Run nancy
uses: sonatype-nexus-community/nancy-github-action@v1.0.2
uses: sonatype-nexus-community/nancy-github-action@v1.0.3
with:
nancyVersion: v1.0.42
- run: |
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/pm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: ory-corp/planning-automation-action@v0.1
- uses: ory-corp/planning-automation-action@v0.2
with:
project: 5
organization: ory-corp
token: ${{ secrets.ORY_BOT_PAT }}
todoLabel: "Needs Triage"
statusName: Status
statusValue: "Needs Triage"
prStatusValue: "Needs Triage"
issueStatusValue: "Needs Triage"
includeEffort: "false"
monthlyMilestoneName: Roadmap Monthly
quarterlyMilestoneName: Roadmap
5 changes: 3 additions & 2 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@
},
"provider": {
"title": "Provider",
"description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.",
"description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, amazon.",
"type": "string",
"enum": [
"github",
Expand All @@ -462,7 +462,8 @@
"linkedin_v2",
"lark",
"x",
"fedcm-test"
"fedcm-test",
"amazon"
],
"examples": ["google"]
},
Expand Down
141 changes: 141 additions & 0 deletions selfservice/strategy/oidc/provider_amazon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package oidc

import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"slices"

"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"golang.org/x/oauth2/amazon"

"github.com/ory/herodot"
"github.com/ory/x/httpx"
"github.com/ory/x/otelx"
)

var _ OAuth2Provider = (*ProviderAmazon)(nil)

var amazonSupportedScopes = []string{"profile", "profile:user_id", "postal_code"}

type ProviderAmazon struct {
*ProviderGenericOIDC
amazonProfileURL string // Only overriden in tests.
}

type amazonProfileResponse struct {
UserId string `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
PostalCode string `json:"postal_code"`
}

func NewProviderAmazon(
config *Configuration,
reg Dependencies,
) Provider {
config.IssuerURL = amazon.Endpoint.AuthURL
const amazonProfileURL string = "https://api.amazon.com/user/profile"

return &ProviderAmazon{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
amazonProfileURL: amazonProfileURL,
}
}

// Only to be used in tests.
func (p *ProviderAmazon) SetProfileURL(url string) {
p.amazonProfileURL = url
}

func (p *ProviderAmazon) Config() *Configuration {
return p.config
}

func (p *ProviderAmazon) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: p.config.ClientID,
ClientSecret: p.config.ClientSecret,
Endpoint: amazon.Endpoint,
Scopes: p.config.Scope,
RedirectURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)),
}
}

func (p *ProviderAmazon) validateConfiguration() error {
for _, s := range p.config.Scope {
if !slices.Contains(amazonSupportedScopes, s) {
return errors.WithStack(
herodot.ErrMisconfiguration.WithReasonf("scope %s not supported. Supported: %+v", s, amazonSupportedScopes))
}
}
if p.config.PKCE == "auto" {
return errors.WithStack(herodot.ErrMisconfiguration.WithReason("pkce:auto is not supported because Amazon does not support PKCE discovery"))
}

return nil
}

func (p *ProviderAmazon) OAuth2(ctx context.Context) (*oauth2.Config, error) {
// This is as good a place as any to validate the configuration.
if err := p.validateConfiguration(); err != nil {
return nil, err
}

return p.oauth2(ctx), nil
}

func (p *ProviderAmazon) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{}
}

func (p *ProviderAmazon) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) {
ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderAmazon.Claims")
defer otelx.End(span, &err)

_, client := httpx.SetOAuth2(ctx, p.reg.HTTPClient(ctx), p.oauth2(ctx), exchange)

req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, p.amazonProfileURL, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("failed to create HTTP request").WithDetail("url", p.amazonProfileURL).WithError(err.Error()))
}
req.Header.Set("x-amz-access-token", exchange.AccessToken)
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError.WithReason("failed to make HTTP request").WithDetail("url", p.amazonProfileURL).WithError(err.Error()))
}
defer func() { _ = resp.Body.Close() }()
body := io.LimitReader(resp.Body, 64*1024) // 64 KiB

if resp.StatusCode != http.StatusOK {
rawResponse, _ := io.ReadAll(body)
return nil, errors.WithStack(herodot.ErrUpstreamError.WithReason("non 200 response").WithDetail("url", p.amazonProfileURL).WithDetail("external_error", string(rawResponse)).
WithDetail("external_status_code", resp.StatusCode))
}

profile := amazonProfileResponse{}
if err := json.NewDecoder(body).Decode(&profile); err != nil {
rawResponse, _ := io.ReadAll(body)
return nil, errors.WithStack(herodot.ErrUpstreamError.WithDetail("url", p.amazonProfileURL).WithDetail("raw_response", rawResponse).WithError(err.Error()))
}

claims := &Claims{
Subject: profile.UserId,
Issuer: amazon.Endpoint.TokenURL,
Name: profile.Name,
Email: profile.Email,
Zoneinfo: profile.PostalCode,
}

return claims, nil
}
59 changes: 59 additions & 0 deletions selfservice/strategy/oidc/provider_amazon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package oidc_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"golang.org/x/oauth2/amazon"

"github.com/ory/kratos/internal"
"github.com/ory/kratos/selfservice/strategy/oidc"
)

func TestAmazonOidcClaims(t *testing.T) {
t.Parallel()

handler := http.NewServeMux()
expectedAccessToken := "my-access-token"
handler.HandleFunc("GET /user/profile", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("x-amz-access-token")
if token != expectedAccessToken {
w.WriteHeader(http.StatusForbidden)
return
}
// From the official docs: https://developer.amazon.com/docs/login-with-amazon/customer-profile.html .
userProfile := `
{
"user_id" : "amzn1.account.K2LI23KL2LK2",
"email" : "johndoe@gmail.com",
"name" : "John Doe",
"postal_code": "98101"
}
`

_, err := w.Write([]byte(userProfile))
require.NoError(t, err)
})
amazonApi := httptest.NewServer(handler)
t.Cleanup(amazonApi.Close)

_, reg := internal.NewFastRegistryWithMocks(t)
p := oidc.NewProviderAmazon(&oidc.Configuration{}, reg).(*oidc.ProviderAmazon)
p.SetProfileURL(amazonApi.URL + "/user/profile")

claims, err := p.Claims(t.Context(), &oauth2.Token{AccessToken: expectedAccessToken}, nil)
require.NoError(t, err)
require.NotNil(t, claims)

require.Equal(t, claims.Subject, "amzn1.account.K2LI23KL2LK2")
require.Equal(t, claims.Issuer, amazon.Endpoint.TokenURL)
require.Equal(t, claims.Name, "John Doe")
require.Equal(t, claims.Email, "johndoe@gmail.com")
require.Equal(t, claims.Zoneinfo, "98101")
}
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Configuration struct {
// - dingtalk
// - linkedin
// - patreon
// - amazon
Provider string `json:"provider"`

// Label represents an optional label which can be used in the UI generation.
Expand Down Expand Up @@ -189,6 +190,7 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies
"line": NewProviderLineV21,
"jackson": NewProviderJackson,
"fedcm-test": NewProviderTestFedcm,
"amazon": NewProviderAmazon,
}

func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) {
Expand Down