diff --git a/embedx/config.schema.json b/embedx/config.schema.json index a42568389fe7..7cf79092f009 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -436,7 +436,8 @@ "dingtalk", "patreon", "linkedin", - "lark" + "lark", + "epic-games" ], "examples": ["google"] }, diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index c6fc84f762de..b1a17260d6c4 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -39,6 +39,7 @@ type Configuration struct { // - dingtalk // - linkedin // - patreon + // - epic-games Provider string `json:"provider"` // Label represents an optional label which can be used in the UI generation. @@ -154,6 +155,7 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies "linkedin": NewProviderLinkedIn, "patreon": NewProviderPatreon, "lark": NewProviderLark, + "epic-games": NewProviderEpicGames, } func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) { diff --git a/selfservice/strategy/oidc/provider_epic_games.go b/selfservice/strategy/oidc/provider_epic_games.go new file mode 100644 index 000000000000..3909cc07524d --- /dev/null +++ b/selfservice/strategy/oidc/provider_epic_games.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/ory/herodot" +) + +type ProviderEpicGames struct { + config *Configuration + reg Dependencies +} + +type EpicGamesIdentityResponse struct { + Data struct { + Attributes struct { + Email string `json:"email"` + FirstName string `json:"first_name"` + FullName string `json:"full_name"` + ImageUrl string `json:"image_url"` + LastName string `json:"last_name"` + } `json:"attributes"` + Id string `json:"id"` + Type string `json:"type"` + } `json:"data"` +} + +func NewProviderEpicGames( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderEpicGames{ + config: config, + reg: reg, + } +} + +func (e *ProviderEpicGames) Config() *Configuration { + return e.config +} + +func (e *ProviderEpicGames) oauth2(ctx context.Context) *oauth2.Config { + return &oauth2.Config{ + ClientID: e.config.ClientID, + ClientSecret: e.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://www.epicgames.com/id/authorize", + TokenURL: "https://api.epicgames.dev/epic/oauth/v1/token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + RedirectURL: e.config.Redir(e.reg.Config().OIDCRedirectURIBase(ctx)), + Scopes: e.config.Scope, + } +} + +func (e *ProviderEpicGames) OAuth2(ctx context.Context) (*oauth2.Config, error) { + e.reg.Logger().WithField("provider", "epic-games").Trace("ProviderCreating new oauth2 configuration in OAuth2 method.") + return e.oauth2(ctx), nil +} + +func (e *ProviderEpicGames) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + identityUrl := "https://api.epicgames.dev/epic/oauth/v1/userInfo" + + o := e.oauth2(ctx) + ctx, client := httpx.SetOAuth2(ctx, e.reg.HTTPClient(ctx), o, exchange) + + req, err := retryablehttp.NewRequest("GET", identityUrl, nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+exchange.AccessToken) + + res, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + defer res.Body.Close() + + if err := logUpstreamError(e.reg.Logger(), res); err != nil { + return nil, err + } + + data := EpicGamesIdentityResponse{} + jsonErr := json.NewDecoder(res.Body).Decode(&data) + if jsonErr != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", jsonErr)) + } + + claims := &Claims{ + Issuer: "https://api.epicgames.dev/epic/oauth/v1", + Subject: data.Data.Id, + Name: data.Data.Attributes.FullName, + Email: data.Data.Attributes.Email, + GivenName: data.Data.Attributes.FirstName, + FamilyName: data.Data.Attributes.LastName, + LastName: data.Data.Attributes.LastName, + Picture: data.Data.Attributes.ImageUrl, + } + + return claims, nil +} + +func (e *ProviderEpicGames) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go index 1e1d71458ee9..570eac240269 100644 --- a/selfservice/strategy/oidc/provider_private_net_test.go +++ b/selfservice/strategy/oidc/provider_private_net_test.go @@ -79,6 +79,7 @@ func TestProviderPrivateIP(t *testing.T) { // VK uses a fixed token URL and does not use the issuer. // Yandex uses a fixed token URL and does not use the issuer. // NetID uses a fixed token URL and does not use the issuer. + // Epic Games uses a fixed token URL and does not use the issuer. } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { p := tc.p(tc.c) diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/cypress/support/config.d.ts index 060cb12822bf..36cab1bbd089 100644 --- a/test/e2e/cypress/support/config.d.ts +++ b/test/e2e/cypress/support/config.d.ts @@ -207,7 +207,7 @@ export type SelfServiceOIDCProvider1 = { [k: string]: unknown | undefined } /** - * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon. + * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, epic-games. */ export type Provider = | "github" @@ -229,6 +229,7 @@ export type Provider = | "patreon" | "linkedin" | "lark" + | "epic-games" export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons = string /**