Skip to content

Commit

Permalink
Add support for PKCE flow in authorize.Authorize (#3)
Browse files Browse the repository at this point in the history
Resolves #2

* Adds PKCE flow in authorize.Authorize
* Adds new function for PKCE flow called authorize.generatePKCEParams
* Adds check for PKCE in token.Token
* Adds type Flowtype and PKCEParams
* Changes TokenRequest and AuthorizeRequest
  • Loading branch information
Qinbeans committed Aug 24, 2023
1 parent d700054 commit eb1c40f
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 5 deletions.
40 changes: 37 additions & 3 deletions endpoints/authorize.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
package endpoints

import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"io"
"net/http"

"github.com/supabase-community/gotrue-go/types"
)

const authorizePath = "/authorize"

func generatePKCEParams() (*types.PKCEParams, error) {
data := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, data); err != nil {
return nil, err
}

// RawURLEncoding since "code challenge can only contain alphanumeric characters, hyphens, periods, underscores and tildes"
verifier := base64.RawURLEncoding.EncodeToString(data)
sha := sha256.Sum256([]byte(verifier))
challenge := base64.RawURLEncoding.EncodeToString(sha[:])
return &types.PKCEParams{
Challenge: challenge,
ChallengeMethod: "S256",
Verifier: verifier,
}, nil
}

// GET /authorize
//
// Get access_token from external oauth provider.
Expand All @@ -27,8 +47,21 @@ func (c *Client) Authorize(req types.AuthorizeRequest) (*types.AuthorizeResponse
}

q := r.URL.Query()
q.Add("provider", string(req.Provider))
q.Add("scopes", req.Scopes)
q.Add("provider", string(req.Provider))

verifier := ""

if string(req.FlowType) == string(types.FlowPKCE) {
pkce, err := generatePKCEParams()
if err != nil {
return nil, err
}
q.Add("code_challenge", pkce.Challenge)
q.Add("code_challenge_method", pkce.ChallengeMethod)
verifier = pkce.Verifier
}

r.URL.RawQuery = q.Encode()

// Set up a client that will not follow the redirect.
Expand All @@ -41,7 +74,7 @@ func (c *Client) Authorize(req types.AuthorizeRequest) (*types.AuthorizeResponse
defer resp.Body.Close()

if resp.StatusCode != http.StatusFound {
fullBody, err := ioutil.ReadAll(resp.Body)
fullBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("response status code %d", resp.StatusCode)
}
Expand All @@ -54,5 +87,6 @@ func (c *Client) Authorize(req types.AuthorizeRequest) (*types.AuthorizeResponse
}
return &types.AuthorizeResponse{
AuthorizationURL: url,
Verifier: verifier,
}, nil
}
8 changes: 6 additions & 2 deletions endpoints/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func (c *Client) RefreshToken(refreshToken string) (*types.TokenResponse, error)

// POST /token
//
// This is an OAuth2 endpoint that currently implements the password and
// refresh_token grant types
// This is an OAuth2 endpoint that currently implements the password,
// refresh_token, and PKCE grant types
func (c *Client) Token(req types.TokenRequest) (*types.TokenResponse, error) {
switch req.GrantType {
case "password":
Expand All @@ -58,6 +58,10 @@ func (c *Client) Token(req types.TokenRequest) (*types.TokenResponse, error) {
if req.RefreshToken == "" || req.Email != "" || req.Phone != "" || req.Password != "" {
return nil, types.ErrInvalidTokenRequest
}
case "pkce":
if req.Code == "" || req.CodeVerifier == "" {
return nil, types.ErrInvalidTokenRequest
}
default:
return nil, types.ErrInvalidTokenRequest
}
Expand Down
9 changes: 9 additions & 0 deletions integration_test/authorize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ func TestAuthorize(t *testing.T) {
require.NoError(err)
assert.Contains(resp.AuthorizationURL, "github.com/login/oauth/authorize")

// Test login with PKCE
resp, err = autoconfirmClient.Authorize(types.AuthorizeRequest{
Provider: "github",
FlowType: "pkce",
})
require.NoError(err)
require.NotEmpty(resp.AuthorizationURL)
require.NotEmpty(resp.Verifier)

// No provider chosen
_, err = autoconfirmClient.Authorize(types.AuthorizeRequest{})
assert.Error(err)
Expand Down
3 changes: 3 additions & 0 deletions integration_test/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ func TestToken(t *testing.T) {
GrantType: "refresh_token",
Password: password,
},
"pkce/missing_code": {
GrantType: "pkce",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
Expand Down
21 changes: 21 additions & 0 deletions types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ type AdminDeleteSSOProviderResponse struct {
}

type Provider string
type FlowType string

const (
ProviderApple Provider = "apple"
Expand All @@ -277,13 +278,27 @@ const (
ProviderZoom Provider = "zoom"
)

const (
FlowImplicit FlowType = "implicit"
FlowPKCE FlowType = "pkce"
)

type AuthorizeRequest struct {
Provider Provider
FlowType FlowType
Scopes string
}

type AuthorizeResponse struct {
AuthorizationURL string
Verifier string
}

// adapted from https://go-review.googlesource.com/c/oauth2/+/463979/9/pkce.go#64
type PKCEParams struct {
Challenge string
ChallengeMethod string
Verifier string
}

type FactorType string
Expand Down Expand Up @@ -459,6 +474,12 @@ type TokenRequest struct {
// It must not be provided if GrantType is 'password'.
RefreshToken string `json:"refresh_token,omitempty"`

// Code and CodeVerifier are required if GrantType is 'pkce'.
Code string `json:"code,omitempty"`

// Code and CodeVerifier are required if GrantType is 'pkce'.
CodeVerifier string `json:"code_verifier,omitempty"`

// Provide Captcha token if enabled. Not required if GrantType is 'refresh_token'.
SecurityEmbed
}
Expand Down

0 comments on commit eb1c40f

Please sign in to comment.