Skip to content

Commit a697910

Browse files
authored
PKCE flow (nedpals#14)
* PKCE flow * RawURLEncoding to avoid validation error; exchage error; provider tokens
1 parent d939aa9 commit a697910

File tree

1 file changed

+97
-7
lines changed

1 file changed

+97
-7
lines changed

auth.go

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ package supabase
33
import (
44
"bytes"
55
"context"
6+
"crypto/rand"
7+
"crypto/sha256"
8+
"encoding/base64"
69
"encoding/json"
710
"errors"
811
"fmt"
12+
"io"
913
"net/http"
1014
"time"
1115

@@ -59,18 +63,24 @@ func (a *Auth) SignUp(ctx context.Context, credentials UserCredentials) (*User,
5963
}
6064

6165
type AuthenticatedDetails struct {
62-
AccessToken string `json:"access_token"`
63-
TokenType string `json:"token_type"`
64-
ExpiresIn int `json:"expires_in"`
65-
RefreshToken string `json:"refresh_token"`
66-
User User `json:"user"`
66+
AccessToken string `json:"access_token"`
67+
TokenType string `json:"token_type"`
68+
ExpiresIn int `json:"expires_in"`
69+
RefreshToken string `json:"refresh_token"`
70+
User User `json:"user"`
71+
ProviderToken string `json:"provider_token"`
72+
ProviderRefreshToken string `json:"provider_refresh_token"`
6773
}
6874

6975
type authenticationError struct {
7076
Error string `json:"error"`
7177
ErrorDescription string `json:"error_description"`
7278
}
7379

80+
type exchangeError struct {
81+
Message string `json:"msg"`
82+
}
83+
7484
// SignIn enters the user credentials and returns the current user if succeeded.
7585
func (a *Auth) SignIn(ctx context.Context, credentials UserCredentials) (*AuthenticatedDetails, error) {
7686
reqBody, _ := json.Marshal(credentials)
@@ -116,6 +126,33 @@ func (a *Auth) RefreshUser(ctx context.Context, userToken string, refreshToken s
116126
return &res, nil
117127
}
118128

129+
type ExchangeCodeOpts struct {
130+
AuthCode string `json:"auth_code"`
131+
CodeVerifier string `json:"code_verifier"`
132+
}
133+
134+
// ExchangeCode takes an auth code and PCKE verifier and returns the current user if succeeded.
135+
func (a *Auth) ExchangeCode(ctx context.Context, opts ExchangeCodeOpts) (*AuthenticatedDetails, error) {
136+
reqBody, _ := json.Marshal(opts)
137+
reqURL := fmt.Sprintf("%s/%s/token?grant_type=pkce", a.client.BaseURL, AuthEndpoint)
138+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewBuffer(reqBody))
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
req.Header.Set("Content-Type", "application/json")
144+
res := AuthenticatedDetails{}
145+
errRes := exchangeError{}
146+
hasCustomError, err := a.client.sendCustomRequest(req, &res, &errRes)
147+
if err != nil {
148+
return nil, err
149+
} else if hasCustomError {
150+
return nil, errors.New(errRes.Message)
151+
}
152+
153+
return &res, err
154+
}
155+
119156
// SendMagicLink sends a link to a specific e-mail address for passwordless auth.
120157
func (a *Auth) SendMagicLink(ctx context.Context, email string) error {
121158
reqBody, _ := json.Marshal(map[string]string{"email": email})
@@ -140,11 +177,20 @@ type ProviderSignInOptions struct {
140177
Provider string `url:"provider"`
141178
RedirectTo string `url:"redirect_to"`
142179
Scopes []string `url:"scopes"`
180+
FlowType FlowType
143181
}
144182

183+
type FlowType string
184+
185+
const (
186+
Implicit FlowType = "implicit"
187+
PKCE FlowType = "pkce"
188+
)
189+
145190
type ProviderSignInDetails struct {
146-
URL string `json:"url"`
147-
Provider string `json:"provider"`
191+
URL string `json:"url"`
192+
Provider string `json:"provider"`
193+
CodeVerifier string `json:"code_verifier"`
148194
}
149195

150196
// SignInWithProvider returns a URL for signing in via OAuth
@@ -154,10 +200,30 @@ func (a *Auth) SignInWithProvider(opts ProviderSignInOptions) (*ProviderSignInDe
154200
return nil, err
155201
}
156202

203+
if opts.FlowType == PKCE {
204+
p, err := generatePKCEParams()
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
params.Add("code_challenge", p.Challenge)
210+
params.Add("code_challenge_method", p.ChallengeMethod)
211+
212+
details := ProviderSignInDetails{
213+
URL: fmt.Sprintf("%s/%s/authorize?%s", a.client.BaseURL, AuthEndpoint, params.Encode()),
214+
Provider: opts.Provider,
215+
CodeVerifier: p.Verifier,
216+
}
217+
218+
return &details, nil
219+
}
220+
221+
// Implicit flow
157222
details := ProviderSignInDetails{
158223
URL: fmt.Sprintf("%s/%s/authorize?%s", a.client.BaseURL, AuthEndpoint, params.Encode()),
159224
Provider: opts.Provider,
160225
}
226+
161227
return &details, nil
162228
}
163229

@@ -257,3 +323,27 @@ func (a *Auth) InviteUserByEmail(ctx context.Context, email string) (*User, erro
257323

258324
return &res, nil
259325
}
326+
327+
// adapted from https://go-review.googlesource.com/c/oauth2/+/463979/9/pkce.go#64
328+
type PKCEParams struct {
329+
Challenge string
330+
ChallengeMethod string
331+
Verifier string
332+
}
333+
334+
func generatePKCEParams() (*PKCEParams, error) {
335+
data := make([]byte, 32)
336+
if _, err := io.ReadFull(rand.Reader, data); err != nil {
337+
return nil, err
338+
}
339+
340+
// RawURLEncoding since "code challenge can only contain alphanumeric characters, hyphens, periods, underscores and tildes"
341+
verifier := base64.RawURLEncoding.EncodeToString(data)
342+
sha := sha256.Sum256([]byte(verifier))
343+
challenge := base64.RawURLEncoding.EncodeToString(sha[:])
344+
return &PKCEParams{
345+
Challenge: challenge,
346+
ChallengeMethod: "S256",
347+
Verifier: verifier,
348+
}, nil
349+
}

0 commit comments

Comments
 (0)