Skip to content

Commit

Permalink
fix: support both postgres and http hooks
Browse files Browse the repository at this point in the history
Combination of multiple commits. More context below:

Fixes #1533

Attempting to signInWithOAuth with linkedin_iodc provider results in
error 500

Attempting to signInWithOAuth with linkedin_iodc results in a successful
login

Error from Supabase Auth Logs:
`oidc: id token issued by a different provider, expected
\"https://www.linkedin.com\" got \"https://www.linkedin.com/oauth\"`

fix: revert patch for linkedin_oidc provider error (#1535)

Reverts #1534

Doesn't seem to work as expected. Directly testing against the API by
calling `https://localhost:9999/?provider=linkedin_oidc will return a
404 error.

fix: update linkedin issuer url (#1536)

* Linkedin introduced a breaking change by changing the issuer url in
their
discover document from `https://linkedin.com` to
`https://linkedin.com/oauth`

* Fixes #1533, #1534,
[#22711](https://github.com/orgs/supabase/discussions/22711),
[#22708](https://github.com/orgs/supabase/discussions/22708)

Please link any relevant issues here.

Feel free to include screenshots if it includes visual changes.

Add any other context or screenshots.

chore(master): release 2.149.0 (#1532)

:robot: I have created a release *beep* *boop*
---

[2.149.0](v2.148.0...v2.149.0)
(2024-04-15)

* refactor generate accesss token to take in request
([#1531](#1531))
([e4f2b59](e4f2b59))

* linkedin_oidc provider error
([#1534](#1534))
([4f5e8e5](4f5e8e5))
* revert patch for linkedin_oidc provider error
([#1535](#1535))
([58ef4af](58ef4af))
* update linkedin issuer url
([#1536](#1536))
([10d6d8b](10d6d8b))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

Revert "Merge branch 'master' into j0/allow_postgres_and_http_on_extensibility_point"

This reverts commit 4311d7e, reversing
changes made to 32fd777.
  • Loading branch information
J0 committed Apr 15, 2024
1 parent ecbc323 commit 991ffa2
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 8 deletions.
3 changes: 1 addition & 2 deletions internal/api/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,6 @@ func (a *API) invokeHook(conn *storage.Connection, r *http.Request, input, outpu
}

return nil

case *hooks.CustomAccessTokenInput:
hookOutput, ok := output.(*hooks.CustomAccessTokenOutput)
if !ok {
Expand Down Expand Up @@ -347,7 +346,7 @@ func (a *API) runHook(r *http.Request, conn *storage.Connection, hookConfig conf
case "pg-functions":
response, err = a.runPostgresHook(ctx, conn, hookConfig, input, output)
default:
return nil, fmt.Errorf("only postgres hooks and HTTPS functions are supported at the moment")
return nil, fmt.Errorf("unsupported protocol: %v only postgres hooks and HTTPS functions are supported at the moment", scheme)
}
if err != nil {
return nil, internalServerError("Error running hook URI: %v", hookConfig.URI).WithInternalError(err)
Expand Down
97 changes: 97 additions & 0 deletions internal/api/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import (
"net/http"
"testing"

"errors"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/hooks"
"github.com/supabase/auth/internal/storage"
"net/http/httptest"

"gopkg.in/h2non/gock.v1"
)
Expand Down Expand Up @@ -151,3 +154,97 @@ func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
// Ensure that all expected HTTP interactions (mocks) have been called
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
}

func (ts *HooksTestSuite) TestInvokeHookIntegration() {
// We use the Send Email Hook as illustration
defer gock.OffAll()
hookFunctionSQL := `
create or replace function invoke_test(input jsonb)
returns json as $$
begin
return input;
end; $$ language plpgsql;`
require.NoError(ts.T(), ts.API.db.RawQuery(hookFunctionSQL).Exec())

testHTTPUri := "http://myauthservice.com/signup"
testHTTPSUri := "https://myauthservice.com/signup"
testPGUri := "pg-functions://postgres/auth/invoke_test"
mockContentLength := "20"
successOutput := map[string]interface{}{}
authEndpoint := "https://app.myapp.com/otp"
gock.New(testHTTPUri).
Post("/").
MatchType("json").
Reply(http.StatusOK).
JSON(successOutput).SetHeader("content-length", mockContentLength)

gock.New(testHTTPSUri).
Post("/").
MatchType("json").
Reply(http.StatusOK).
JSON(successOutput).SetHeader("content-length", mockContentLength)

tests := []struct {
description string
conn *storage.Connection
request *http.Request
input any
output any
uri string
expectedError error
}{
{
description: "HTTP endpoint success",
conn: nil,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: testHTTPUri,
},
{
description: "HTTPS endpoint success",
conn: nil,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: testHTTPSUri,
},
{
description: "PostgreSQL function success",
conn: ts.API.db,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: testPGUri,
},
{
description: "Unsupported protocol error",
conn: nil,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: "ftp://example.com/path",
expectedError: errors.New("unsupported protocol: ftp only postgres hooks and HTTPS functions are supported at the moment"),
},
}

var err error
for _, tc := range tests {
// Set up hook config
ts.Config.Hook.SendEmail.Enabled = true
ts.Config.Hook.SendEmail.URI = tc.uri
require.NoError(ts.T(), ts.Config.Hook.SendEmail.PopulateExtensibilityPoint())

ts.Run(tc.description, func() {
err = ts.API.invokeHook(tc.conn, tc.request, tc.input, tc.output, tc.uri)
if tc.expectedError != nil {
require.EqualError(ts.T(), err, tc.expectedError.Error())
} else {
require.NoError(ts.T(), err)
}
})

}
// Ensure that all expected HTTP interactions (mocks) have been called
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
}
185 changes: 185 additions & 0 deletions internal/api/phone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ func doTestSendPhoneConfirmation(ts *PhoneTestSuite, useTestOTP bool) {
}
})
}
// Reset at end of test
ts.API.config.Sms.TestOTP = nil

}

func (ts *PhoneTestSuite) TestSendPhoneConfirmation() {
Expand Down Expand Up @@ -261,3 +264,185 @@ func (ts *PhoneTestSuite) TestMissingSmsProviderConfig() {
}
}
}
func (ts *PhoneTestSuite) TestSendSMSHook() {
u, err := models.FindUserByPhoneAndAudience(ts.API.db, "123456789", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
now := time.Now()
u.PhoneConfirmedAt = &now
require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")

s, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(s))

req := httptest.NewRequest(http.MethodPost, "/token?grant_type=password", nil)
token, _, err := ts.API.generateAccessToken(req, ts.API.db, u, &s.ID, models.PasswordGrant)
require.NoError(ts.T(), err)

// We setup a job table to enqueue SMS requests to send. Similar in spirit to the pg_boss postgres extension
createJobsTableSQL := `CREATE TABLE job_queue (
id serial PRIMARY KEY,
job_type text,
payload jsonb,
status text DEFAULT 'pending', -- Possible values: 'pending', 'processing', 'completed', 'failed'
created_at timestamp without time zone DEFAULT NOW()
);`
require.NoError(ts.T(), ts.API.db.RawQuery(createJobsTableSQL).Exec())

type sendSMSHookTestCase struct {
desc string
uri string
endpoint string
method string
header string
body map[string]string
hookFunctionSQL string
expectedCode int
expectToken bool
hookFunctionIdentifier string
}
cases := []sendSMSHookTestCase{
{
desc: "Phone signup using Hook",
endpoint: "/signup",
method: http.MethodPost,
uri: "pg-functions://postgres/auth/send_sms_signup",
hookFunctionSQL: `
create or replace function send_sms_signup(input jsonb)
returns json as $$
begin
insert into job_queue(job_type, payload)
values ('sms_signup', input);
return input;
end; $$ language plpgsql;`,
header: "",
body: map[string]string{
"phone": "1234567890",
"password": "testpassword",
},
expectedCode: http.StatusOK,
hookFunctionIdentifier: "send_sms_signup(input jsonb)",
},
{
desc: "SMS OTP sign in using hook",
endpoint: "/otp",
method: http.MethodPost,
uri: "pg-functions://postgres/auth/send_sms_otp",
hookFunctionSQL: `
create or replace function send_sms_otp(input jsonb)
returns json as $$
begin
insert into job_queue(job_type, payload)
values ('sms_signup', input);
return input;
end; $$ language plpgsql;`,
header: "",
body: map[string]string{
"phone": "123456789",
},
expectToken: false,
expectedCode: http.StatusOK,
hookFunctionIdentifier: "send_sms_otp(input jsonb)",
},
{
desc: "Phone Change",
endpoint: "/user",
method: http.MethodPut,
uri: "pg-functions://postgres/auth/send_sms_phone_change",
hookFunctionSQL: `
create or replace function send_sms_phone_change(input jsonb)
returns json as $$
begin
insert into job_queue(job_type, payload)
values ('phone_change', input);
return input;
end; $$ language plpgsql;`,
header: token,
body: map[string]string{
"phone": "111111111",
},
expectToken: true,
expectedCode: http.StatusOK,
hookFunctionIdentifier: "send_sms_phone_change(input jsonb)",
},
{
desc: "Reauthenticate",
endpoint: "/reauthenticate",
method: http.MethodGet,
uri: "pg-functions://postgres/auth/reauthenticate",
hookFunctionSQL: `
create or replace function reauthenticate(input jsonb)
returns json as $$
begin
return input;
end; $$ language plpgsql;`,
header: "",
body: nil,
expectToken: true,
expectedCode: http.StatusOK,
hookFunctionIdentifier: "reauthenticate(input jsonb)",
},
{
desc: "SMS OTP Hook (Error)",
endpoint: "/otp",
method: http.MethodPost,
uri: "pg-functions://postgres/auth/send_sms_otp_failure",
hookFunctionSQL: `
create or replace function send_sms_otp(input jsonb)
returns json as $$
begin
RAISE EXCEPTION 'Intentional Error for Testing';
end; $$ language plpgsql;`,
header: "",
body: map[string]string{
"phone": "123456789",
},
expectToken: false,
expectedCode: http.StatusBadRequest,
hookFunctionIdentifier: "send_sms_otp_failure(input jsonb)",
},
}

for _, c := range cases {
ts.T().Run(c.desc, func(t *testing.T) {

ts.Config.External.Phone.Enabled = true
ts.Config.Hook.SendSMS.Enabled = true
ts.Config.Hook.SendSMS.URI = c.uri
// Disable FrequencyLimit to allow back to back sending
ts.Config.Sms.MaxFrequency = 0 * time.Second
// We still need a mock provider for hooks to work right now for backward compatibility
ts.Config.Sms.Provider = "twilio"
ts.Config.Sms.Twilio = conf.TwilioProviderConfiguration{
AccountSid: "test_account_sid",
AuthToken: "test_auth_token",
MessageServiceSid: "test_message_service_id",
}
require.NoError(ts.T(), ts.Config.Hook.SendSMS.PopulateExtensibilityPoint())

require.NoError(t, ts.API.db.RawQuery(c.hookFunctionSQL).Exec())

var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
req := httptest.NewRequest(c.method, "http://localhost"+c.endpoint, &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)

require.Equal(t, c.expectedCode, w.Code, "Unexpected HTTP status code")

// Delete the function and reset env
cleanupHookSQL := fmt.Sprintf("drop function if exists %s", ts.Config.Hook.SendSMS.HookName)
require.NoError(t, ts.API.db.RawQuery(cleanupHookSQL).Exec())
ts.Config.Hook.SendSMS.Enabled = false
ts.Config.Sms.MaxFrequency = 1 * time.Second
})
}

// Cleanup
deleteJobsTableSQL := `drop table if exists job_queue`
require.NoError(ts.T(), ts.API.db.RawQuery(deleteJobsTableSQL).Exec())

}
6 changes: 2 additions & 4 deletions internal/api/provider/linkedin_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@ func NewLinkedinOIDCProvider(ext conf.OAuthProviderConfiguration, scopes string)
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

// Linkedin uses a different issuer from it's oidc discovery url
// https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#validating-id-tokens
ctx := oidc.InsecureIssuerURLContext(context.Background(), IssuerLinkedin)
oidcProvider, err := oidc.NewProvider(ctx, IssuerLinkedin+"/oauth")

oidcProvider, err := oidc.NewProvider(context.Background(), IssuerLinkedin)
if err != nil {
return nil, err
}
Expand Down
6 changes: 4 additions & 2 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,10 @@ func (e *ExtensibilityPointConfiguration) PopulateExtensibilityPoint() error {
if err != nil {
return err
}
pathParts := strings.Split(u.Path, "/")
e.HookName = fmt.Sprintf("%q.%q", pathParts[1], pathParts[2])
if u.Scheme == "pg-functions" {
pathParts := strings.Split(u.Path, "/")
e.HookName = fmt.Sprintf("%q.%q", pathParts[1], pathParts[2])
}
return nil
}

Expand Down

0 comments on commit 991ffa2

Please sign in to comment.