From 3946039366888e41db966fe999a119dfa5d07ab1 Mon Sep 17 00:00:00 2001 From: Kieron Woodhouse Date: Fri, 4 Nov 2022 20:44:31 +0000 Subject: [PATCH] Implement verify and start testing it --- README.md | 5 ++- api.go | 24 +++++++++++- endpoints/authorize.go | 12 ++---- endpoints/client.go | 12 ++++++ endpoints/verify.go | 67 +++++++++++++++++++++++++++++++++ integration_test/verify_test.go | 29 ++++++++++++++ types/api.go | 28 ++++++++++++++ 7 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 endpoints/verify.go create mode 100644 integration_test/verify_test.go diff --git a/README.md b/README.md index d015b14..4da75da 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Required for V1 release: - Client API - [X] GET /health - [X] GET /settings - - [ ] GET /callback - - [ ] POST /callback + - [X] GET /callback + - [X] POST /callback - [X] GET /authorize - [X] POST /invite - [X] POST /signup @@ -173,3 +173,4 @@ To interact with docker compose, you can also use `make up` and `make down`. Prior users of [`gotrue-js`](https://github.com/supabase/gotrue-js) may be familiar with its subscription mechanism and session management - in line with its ability to be used as a client-side authentication library, in addition to use on the server. As Go is typically used on the backend, this library acts purely as a convenient wrapper for interacting with a GoTrue server. It provides no session management or subscription mechanism. + diff --git a/api.go b/api.go index 0a0f530..2402095 100644 --- a/api.go +++ b/api.go @@ -6,8 +6,10 @@ import ( "github.com/kwoodhouse93/gotrue-go/types" ) -// --- API --- - +// Create a new client using gotrue.New, then you can call the methods below. +// +// Some methods require bearer token authentication. To set the bearer token, +// use the WithToken(token) method. type Client interface { // By default, the client will use the supabase project reference and assume // you are connecting to the GoTrue server as part of a supabase project. @@ -37,6 +39,8 @@ type Client interface { // copy will use the new HTTP client. WithClient(client http.Client) Client + // Endpoints: + // POST /admin/generate_link // // Returns the corresponding email action link based on the type specified. @@ -71,6 +75,13 @@ type Client interface { // redirect to. Authorize(req types.AuthorizeRequest) (*types.AuthorizeResponse, error) + // GET/POST /callback + // + // Callback endpoint for external oauth providers to redirect to. + // + // There is no meaningful implementation of this as a client method, so it is + // not included here. + // GET /health // // Check the health of the GoTrue server. @@ -162,4 +173,13 @@ type Client interface { // this method can be used to set custom user data. Changing the email will // result in a magiclink being sent out. UpdateUser(req types.UpdateUserRequest) (*types.UpdateUserResponse, error) + + // POST /verify + // + // Verify a registration or a password recovery. Type can be signup or recovery + // or magiclink or invite and the token is a token returned from either /signup + // or /recover or /magiclink. + // + // GET /verify also exists, but cannot take email or phone parameters. + Verify(req types.VerifyRequest) (*types.VerifyResponse, error) } diff --git a/endpoints/authorize.go b/endpoints/authorize.go index dbc9800..8707f96 100644 --- a/endpoints/authorize.go +++ b/endpoints/authorize.go @@ -32,21 +32,15 @@ func (c *Client) Authorize(req types.AuthorizeRequest) (*types.AuthorizeResponse r.URL.RawQuery = q.Encode() // Set up a client that will not follow the redirect. - noRedirClient := http.Client{ - Transport: c.client.Transport, - Jar: c.client.Jar, - Timeout: c.client.Timeout, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } + noRedirClient := noRedirClient(c.client) + resp, err := noRedirClient.Do(r) if err != nil { return nil, err } defer resp.Body.Close() - if resp.StatusCode != 302 { + if resp.StatusCode != http.StatusFound { fullBody, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("response status code %d", resp.StatusCode) diff --git a/endpoints/client.go b/endpoints/client.go index 748feae..8558f68 100644 --- a/endpoints/client.go +++ b/endpoints/client.go @@ -50,3 +50,15 @@ func (c Client) WithClient(client http.Client) *Client { token: c.token, } } + +// Returns a copy of a HTTP client that will not follow redirects. +func noRedirClient(client http.Client) http.Client { + return http.Client{ + Transport: client.Transport, + Jar: client.Jar, + Timeout: client.Timeout, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} diff --git a/endpoints/verify.go b/endpoints/verify.go new file mode 100644 index 0000000..5152465 --- /dev/null +++ b/endpoints/verify.go @@ -0,0 +1,67 @@ +package endpoints + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/kwoodhouse93/gotrue-go/types" +) + +const verifyPath = "/verify" + +// Get /verify +// +// Verify a registration or a password recovery. Type can be signup or recovery +// or magiclink or invite and the token is a token returned from either /signup +// or /recover or /magiclink. +func (c *Client) Verify(req types.VerifyRequest) (*types.VerifyResponse, error) { + r, err := c.newRequest(verifyPath, http.MethodGet, nil) + if err != nil { + return nil, err + } + + q := r.URL.Query() + q.Add("type", string(req.Type)) + q.Add("token", req.Token) + q.Add("redirect_to", req.RedirectTo) + r.URL.RawQuery = q.Encode() + + // Set up a client that will not follow the redirect. + noRedirClient := noRedirClient(c.client) + + resp, err := noRedirClient.Do(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusSeeOther { + fullBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("response status code %d", resp.StatusCode) + } + return nil, fmt.Errorf("response status code %d: %s", resp.StatusCode, fullBody) + } + + redirURL := resp.Header.Get("Location") + if redirURL == "" { + return nil, fmt.Errorf("no redirect URL found in response") + } + u, err := url.Parse(redirURL) + if err != nil { + return nil, err + } + values, err := url.ParseQuery(u.Fragment) + if err != nil { + return nil, err + } + + return &types.VerifyResponse{ + URL: redirURL, + Error: values.Get("error"), + ErrorCode: values.Get("error_code"), + ErrorDescription: values.Get("error_description"), + }, nil +} diff --git a/integration_test/verify_test.go b/integration_test/verify_test.go new file mode 100644 index 0000000..fe01171 --- /dev/null +++ b/integration_test/verify_test.go @@ -0,0 +1,29 @@ +package integration_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kwoodhouse93/gotrue-go/types" +) + +func TestVerify(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Unauthorized client + resp, err := autoconfirmClient.Verify(types.VerifyRequest{ + Type: types.VerificationTypeSignup, + Token: "abcde", + RedirectTo: "http://localhost:3000", + }) + require.NoError(err) + assert.NotEmpty(resp.URL) + assert.Equal("401", resp.ErrorCode) + assert.Equal("unauthorized_client", resp.Error) + + // Authorized client + // TODO: Test +} diff --git a/types/api.go b/types/api.go index 60b08a7..c0fc1fe 100644 --- a/types/api.go +++ b/types/api.go @@ -217,3 +217,31 @@ type UpdateUserRequest struct { type UpdateUserResponse struct { User } + +type VerificationType string + +const ( + VerificationTypeSignup = "signup" + VerificationTypeRecovery = "recovery" + VerificationTypeInvite = "invite" + VerificationTypeMagiclink = "magiclink" + VerificationTypeEmailChange = "email_change" + VerificationTypeSMS = "sms" + VerificationTypePhoneChange = "phone_change" +) + +type VerifyRequest struct { + Type VerificationType `json:"type"` + Token string `json:"token"` + Email string `json:"email"` + Phone string `json:"phone"` + RedirectTo string `json:"redirect_to"` +} + +type VerifyResponse struct { + URL string + + Error string + ErrorCode string + ErrorDescription string +}