From 9856c59de00bc98d048139acf8f8a42f4e9857a3 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Mon, 3 Feb 2025 12:57:15 +0200 Subject: [PATCH] prioritized user submitted OAuth2 createData.email --- CHANGELOG.md | 5 ++ apis/record_auth_with_oauth2.go | 6 +- apis/record_auth_with_oauth2_test.go | 99 ++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aec6386..1e5d0600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.26.0 (WIP) + +- ⚠️ Prioritized the user submitted non-empty `createData.email` (_it will be unverified_) when creating the PocketBase user during the first OAuth2 auth. + + ## v0.25.0 - ⚠️ Upgraded Google OAuth2 auth, token and userinfo endpoints to their latest versions. diff --git a/apis/record_auth_with_oauth2.go b/apis/record_auth_with_oauth2.go index 5a9ec48d..3b89993e 100644 --- a/apis/record_auth_with_oauth2.go +++ b/apis/record_auth_with_oauth2.go @@ -232,7 +232,11 @@ func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *cor payload = map[string]any{} } - payload[core.FieldNameEmail] = e.OAuth2User.Email + // assign the OAuth2 user email only if the user hasn't submitted one + // (ignore empty/invalid values for consistency with the OAuth2->existing user update flow) + if v, _ := payload[core.FieldNameEmail].(string); v == "" { + payload[core.FieldNameEmail] = e.OAuth2User.Email + } // map known fields (unless the field was explicitly submitted as part of CreateData) if _, ok := payload[e.Collection.OAuth2.MappedFields.Id]; !ok && e.Collection.OAuth2.MappedFields.Id != "" { diff --git a/apis/record_auth_with_oauth2_test.go b/apis/record_auth_with_oauth2_test.go index 5be9097b..149ca81b 100644 --- a/apis/record_auth_with_oauth2_test.go +++ b/apis/record_auth_with_oauth2_test.go @@ -877,7 +877,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) { `"verified":{"code":"validation_values_mismatch"`, }, NotExpectedContent: []string{ - `"email":`, // the value is always overwritten with the OAuth2 user email + `"email":`, // ignored because the record validator never ran `"rel":`, // ignored because the record validator never ran `"file":`, // ignored because the record validator never ran }, @@ -930,12 +930,10 @@ func TestRecordAuthWithOAuth2(t *testing.T) { ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, + `"email":{"code":"validation_is_email"`, `"rel":{"code":"validation_missing_rel_records"`, `"file":{"code":"validation_invalid_file"`, }, - NotExpectedContent: []string{ - `"email":`, // the value is always overwritten with the OAuth2 user email - }, ExpectedEvents: map[string]int{ "*": 0, "OnRecordAuthWithOAuth2Request": 1, @@ -949,7 +947,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) { }, }, { - Name: "creating user (valid create data)", + Name: "creating user (valid create data with empty submitted email)", Method: http.MethodPost, URL: "/api/collections/users/auth-with-oauth2", Body: strings.NewReader(`{ @@ -957,7 +955,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) { "code":"123", "redirectURL": "https://example.com", "createData": { - "email": "invalid", + "email": "", "emailVisibility": true, "password": "1234567890", "passwordConfirm": "1234567890", @@ -1041,6 +1039,95 @@ func TestRecordAuthWithOAuth2(t *testing.T) { } }, }, + { + Name: "creating user (valid create data with non-empty valid submitted email)", + Method: http.MethodPost, + URL: "/api/collections/users/auth-with-oauth2", + Body: strings.NewReader(`{ + "provider": "test", + "code":"123", + "redirectURL": "https://example.com", + "createData": { + "email": "test_create@example.com", + "emailVisibility": true, + "password": "1234567890", + "passwordConfirm": "1234567890", + "name": "test_name", + "username": "test_username", + "rel": "0yxhwia2amd8gec" + } + }`), + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + usersCol, err := app.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + // register the test provider + auth.Providers["test"] = func() auth.Provider { + return &oauth2MockProvider{ + AuthUser: &auth.AuthUser{ + Id: "test_id", + Email: "oauth2@example.com", // should be ignored because of the explicit submitted email + }, + Token: &oauth2.Token{AccessToken: "abc"}, + } + } + + // add the test provider in the collection + usersCol.MFA.Enabled = false + usersCol.OAuth2.Enabled = true + usersCol.OAuth2.Providers = []core.OAuth2ProviderConfig{{ + Name: "test", + ClientId: "123", + ClientSecret: "456", + }} + if err := app.Save(usersCol); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"email":"test_create@example.com"`, + `"emailVisibility":true`, + `"name":"test_name"`, + `"username":"test_username"`, + `"verified":false`, + `"rel":"0yxhwia2amd8gec"`, + }, + NotExpectedContent: []string{ + // hidden fields + `"tokenKey"`, + `"password"`, + }, + ExpectedEvents: map[string]int{ + "*": 0, + "OnRecordAuthWithOAuth2Request": 1, + "OnRecordAuthRequest": 1, + "OnRecordCreateRequest": 1, + "OnRecordEnrich": 2, // the auth response and from the create request + // --- + "OnModelCreate": 3, // record + authOrigins + externalAuths + "OnModelCreateExecute": 3, + "OnModelAfterCreateSuccess": 3, + "OnRecordCreate": 3, + "OnRecordCreateExecute": 3, + "OnRecordAfterCreateSuccess": 3, + // --- + "OnModelValidate": 3, + "OnRecordValidate": 3, + }, + AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) { + user, err := app.FindFirstRecordByData("users", "username", "test_username") + if err != nil { + t.Fatal(err) + } + + if !user.ValidatePassword("1234567890") { + t.Fatalf("Expected password %q to be valid", "1234567890") + } + }, + }, { Name: "creating user (with mapped OAuth2 fields and avatarURL->file field)", Method: http.MethodPost,