diff --git a/go.mod b/go.mod index db50ded5817e..1669ac017013 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/imdario/mergo v0.3.12 github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743 + github.com/jarcoal/httpmock v1.0.5 github.com/jteeuwen/go-bindata v3.0.7+incompatible github.com/julienschmidt/httprouter v1.3.0 github.com/knadh/koanf v1.4.0 diff --git a/go.sum b/go.sum index e2ba2e6c1705..57e1e72adf6c 100644 --- a/go.sum +++ b/go.sum @@ -1142,6 +1142,7 @@ github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jandelgado/gcov2lcov v1.0.4/go.mod h1:NnSxK6TMlg1oGDBfGelGbjgorT5/L3cchlbtgFYZSss= github.com/jandelgado/gcov2lcov v1.0.5 h1:rkBt40h0CVK4oCb8Dps950gvfd1rYvQ8+cWa346lVU0= github.com/jandelgado/gcov2lcov v1.0.5/go.mod h1:NnSxK6TMlg1oGDBfGelGbjgorT5/L3cchlbtgFYZSss= +github.com/jarcoal/httpmock v1.0.5 h1:cHtVEcTxRSX4J0je7mWPfc9BpDpqzXSJ5HbymZmyHck= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcchavezs/porto v0.1.0/go.mod h1:fESH0gzDHiutHRdX2hv27ojnOVFco37hg1W6E9EZF4A= diff --git a/selfservice/strategy/oidc/provider_facebook.go b/selfservice/strategy/oidc/provider_facebook.go index 15e45cc78555..7dd133087348 100644 --- a/selfservice/strategy/oidc/provider_facebook.go +++ b/selfservice/strategy/oidc/provider_facebook.go @@ -2,7 +2,11 @@ package oidc import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" + "fmt" "net/url" "github.com/hashicorp/go-retryablehttp" @@ -34,6 +38,15 @@ func NewProviderFacebook( } } +func (g *ProviderFacebook) generateAppSecretProof(ctx context.Context, exchange *oauth2.Token) string { + secret := g.config.ClientSecret + data := exchange.AccessToken + + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + func (g *ProviderFacebook) OAuth2(ctx context.Context) (*oauth2.Config, error) { p, err := g.provider(ctx) if err != nil { @@ -52,8 +65,9 @@ func (g *ProviderFacebook) Claims(ctx context.Context, exchange *oauth2.Token, q return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } + appSecretProof := g.generateAppSecretProof(ctx, exchange) client := g.reg.HTTPClient(ctx, httpx.ResilientClientWithClient(o.Client(ctx, exchange))) - u, err := url.Parse("https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender") + u, err := url.Parse(fmt.Sprintf("https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=%s", appSecretProof)) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } diff --git a/selfservice/strategy/oidc/provider_facebook_test.go b/selfservice/strategy/oidc/provider_facebook_test.go new file mode 100644 index 000000000000..99062edcaa60 --- /dev/null +++ b/selfservice/strategy/oidc/provider_facebook_test.go @@ -0,0 +1,89 @@ +package oidc_test + +import ( + "context" + "net/http" + "net/url" + "testing" + "time" + + "github.com/jarcoal/httpmock" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" +) + +const fakeIDToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTksImF1ZCI6ImFiY2QiLCJpc3MiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYWVuZWFzci9wcml2YXRlLW9pZGMvbWFzdGVyL3Rva2VuIn0.G9v8pJXJrEOgdJ5ecE6sIIcTH_p-RKkBaImfZY5DDVCl7h5GEis1n3GKKYbL_O3fj8Fu-WzI2mquI8S8BOVCQ6wN0XtrqJv22iX_nzeVHc4V_JWV1q7hg2gPpoFFcnF3KKtxZLvDOA8ujsDbAXmoBu0fEBdwCN56xLOOKQDzULyfijuAa8hrCwespZ9HaqcHzD3iHf_Utd4nHqlTM-6upWpKIMkplS_NGcxrfIRIWusZ0wob6ryy8jECD9QeZpdTGUozq-YM64lZfMOZzuLuqichH_PCMKFyB_tOZb6lDIiiSX4Irz7_YF-DP-LmfxgIW4934RqTCeFGGIP64h4xAA" + +func TestProviderFacebook_Claims(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "https://graph.facebook.com/me", + func(req *http.Request) (*http.Response, error) { + if _, ok := req.URL.Query()["appsecret_proof"]; !ok { + resp, err := httpmock.NewJsonResponse(400, map[string]interface{}{ + "error": map[string]interface{}{ + "message": "API calls from the server require an appsecret_proof argument", + "type": "GraphMethodException", + "code": 100, + "fbtrace_id": "Ay8LR3n5BsHm809VYpJ3eDM", + }, + }) + return resp, err + } + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "id": "123456789012345", + "name": "John Doe", + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "birthday": "01/01/1990", + }) + return resp, err + }, + ) + + httpmock.RegisterResponder("GET", "https://www.facebook.com/.well-known/openid-configuration", + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{ + "issuer": "https://www.facebook.com", + }) + return resp, err + }, + ) + + _, reg := internal.NewFastRegistryWithMocks(t) + c := &oidc.Configuration{ + ID: "facebook", + Provider: "facebook", + ClientID: "abcd", + ClientSecret: "secret", + Mapper: "file://./stub/oidc.facebook.jsonnet", + Scope: []string{"email"}, + } + facebook := oidc.NewProviderFacebook(c, reg) + + actual, err := facebook.Claims( + context.Background(), + (&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeIDToken}), + url.Values{}, + ) + require.NoError(t, err) + + assert.Equal(t, &oidc.Claims{ + Issuer: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=773ba44693c7553d6ee20f61ea5d2757a9a4f4a44d2841ae4e95b52e4cd62db4", + Subject: "123456789012345", + Name: "John Doe", + GivenName: "John", + FamilyName: "Doe", + Nickname: "John Doe", + PreferredUsername: "John Doe", + Email: "john.doe@example.com", + EmailVerified: true, + Birthdate: "01/01/1990", + }, actual) +} diff --git a/selfservice/strategy/oidc/stub/oidc.facebook.jsonnet b/selfservice/strategy/oidc/stub/oidc.facebook.jsonnet new file mode 100644 index 000000000000..4a1c10265aa9 --- /dev/null +++ b/selfservice/strategy/oidc/stub/oidc.facebook.jsonnet @@ -0,0 +1,13 @@ +local claims = std.extVar('claims'); + +if std.length(claims.sub) == 0 then + error 'claim sub not set' +else + { + identity: { + traits: { + subject: claims.sub, + [if "email" in claims then "email" else null]: claims.email, + }, + }, + }