nedpals / supabase-go

Unofficial Supabase client library for Go.
https://pkg.go.dev/github.com/nedpals/supabase-go
MIT License
380 stars 73 forks source link

PKCE flow #14

Closed nzoschke closed 1 year ago

nzoschke commented 1 year ago

Fixes #13

nzoschke commented 1 year ago

This is working for me with an echo app like the following:

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    supabase := supa.CreateClient(os.Getenv("SUPABASE_PUBLIC_URL"), os.Getenv("SUPABASE_PUBLIC_ANON_KEY"))

    e := echo.New()
    e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))))

    e.GET("/auth/login/spotify", func(c echo.Context) error {
        out, err := supabase.Auth.SignInWithProvider(supa.ProviderSignInOptions{
            Provider:   "spotify",
            RedirectTo: "http://localhost:1323/api/auth/callback",
            Scopes:     []string{"user-read-email", "user-read-private"},
            FlowType:   supa.PKCE,
        })
        if err != nil {
            return xerrors.Errorf(": %w", err)
        }

        // save verifier to session to get back on callback
        sess, err := session.Get("session", c)
        if err != nil {
            return xerrors.Errorf(": %w", err)
        }

        sess.Values["code_verifier"] = out.CodeVerifier
        if err := sess.Save(c.Request(), c.Response()); err != nil {
            return xerrors.Errorf(": %w", err)
        }

        return c.Redirect(http.StatusTemporaryRedirect, out.URL)
    })

    e.GET("/auth/callback", func(c echo.Context) error {
        code := c.Request().URL.Query().Get("code")
        if code == "" {
            return xerrors.New("missing code")
        }

        // get verifier from session to exchange with code for token
        sess, err := session.Get("session", c)
        if err != nil {
            return xerrors.Errorf(": %w", err)
        }

        res, err := supabase.Auth.ExchangeCode(c.Request().Context(), supa.ExchangeCodeOpts{
            AuthCode:     code,
            CodeVerifier: fmt.Sprint(sess.Values["code_verifier"]),
        })
        if err != nil {
            return xerrors.Errorf(": %w", err)
        }

        return c.JSON(http.StatusOK, res)
    })

    e.Logger.Fatal(e.Start(":1323"))
}

This returns a token and user like:

{
  "access_token": "<REDACTED>",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "<REDACTED>",
  "user": {
    "id": "<REDACTED>",
    "aud": "authenticated",
    "role": "authenticated",
    "email": "<REDACTED>@example.com",
    "invited_at": "0001-01-01T00:00:00Z",
    "confirmed_at": "2023-05-13T14:38:47.898965Z",
    "confirmation_sent_at": "0001-01-01T00:00:00Z",
    "app_metadata": {},
    "user_metadata": {
      "avatar_url": "https://scontent-atl3-1.xx.fbcdn.net/<REDACTED>",
      "email": "<REDACTED>@example.com",
      "email_verified": true,
      "full_name": "<REDACTED>",
      "iss": "https://api.spotify.com/v1",
      "name": "<REDACTED>",
      "picture": "https://scontent-atl3-1.xx.fbcdn.net/<REDACTED>",
      "provider_id": "<REDACTED>",
      "sub": "<REDACTED>"
    },
    "created_at": "2023-05-13T14:38:47.870791Z",
    "updated_at": "2023-05-14T01:36:43.057753Z"
  }
}