diff --git a/endpoints/authorize.go b/endpoints/authorize.go index d3bcc24..2c7b1dc 100644 --- a/endpoints/authorize.go +++ b/endpoints/authorize.go @@ -1,8 +1,11 @@ package endpoints import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" "fmt" - "io/ioutil" + "io" "net/http" "github.com/supabase-community/gotrue-go/types" @@ -10,6 +13,23 @@ import ( 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. @@ -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. @@ -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) } @@ -54,5 +87,6 @@ func (c *Client) Authorize(req types.AuthorizeRequest) (*types.AuthorizeResponse } return &types.AuthorizeResponse{ AuthorizationURL: url, + Verifier: verifier, }, nil } diff --git a/endpoints/token.go b/endpoints/token.go index 679e138..da1e225 100644 --- a/endpoints/token.go +++ b/endpoints/token.go @@ -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": @@ -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 } diff --git a/integration_test/authorize_test.go b/integration_test/authorize_test.go index 3060d87..bbe3cd0 100644 --- a/integration_test/authorize_test.go +++ b/integration_test/authorize_test.go @@ -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) diff --git a/integration_test/token_test.go b/integration_test/token_test.go index 8dfef5b..71963a6 100644 --- a/integration_test/token_test.go +++ b/integration_test/token_test.go @@ -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) { diff --git a/types/api.go b/types/api.go index 2b6ad25..95fed2e 100644 --- a/types/api.go +++ b/types/api.go @@ -256,6 +256,7 @@ type AdminDeleteSSOProviderResponse struct { } type Provider string +type FlowType string const ( ProviderApple Provider = "apple" @@ -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 @@ -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 }