Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
142 lines
4.2 KiB
Go
142 lines
4.2 KiB
Go
// Copyright © 2025 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"testing"
|
|
|
|
"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,
|
|
}
|
|
}
|
|
|
|
func (p *ProviderAmazon) SetProfileURL(t *testing.T, 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)
|
|
|
|
ctx, 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
|
|
}
|