Skip to content

Commit

Permalink
Implement verify and start testing it
Browse files Browse the repository at this point in the history
  • Loading branch information
kwoodhouse93 committed Nov 4, 2022
1 parent 14b8a5f commit 3946039
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 13 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

24 changes: 22 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
12 changes: 3 additions & 9 deletions endpoints/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions endpoints/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
}
67 changes: 67 additions & 0 deletions endpoints/verify.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions integration_test/verify_test.go
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 28 additions & 0 deletions types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 3946039

Please sign in to comment.