diff --git a/BRANCH_ORGANIZATION.md b/BRANCH_ORGANIZATION.md new file mode 100644 index 0000000..eecfdd5 --- /dev/null +++ b/BRANCH_ORGANIZATION.md @@ -0,0 +1,101 @@ +# OAuth2 库分支组织 + +## 📋 分支说明 + +这个项目使用功能分支来组织不同的 OAuth2 提供者实现,以保持代码整洁和功能分离。 + +## 🌳 分支结构 + +### 主分支 +- **`master`** - 稳定的主分支,包含经过测试的功能 +- 包含: 基础 OAuth2 框架和成熟的提供者(GitHub、Google 等) + +### 功能分支 + +#### 🔐 Supabase 分支 +- **分支名**: `cursor/implement-supabase-authentication-86da` +- **功能**: Supabase OAuth2 提供者 +- **状态**: ✅ 完成 +- **包含**: + - `supabase/supabase.go` - Supabase OAuth2 实现 + - `supabase/README.md` - 详细文档 + - `example/supabase/` - 完整示例应用 + +#### 🔑 WebAuthn 分支 +- **分支名**: `feature/webauthn-support` +- **功能**: WebAuthn 无密码认证 +- **状态**: ✅ 完成 +- **包含**: + - `webauthn/webauthn.go` - WebAuthn OAuth2 适配器 + - `webauthn/user.go` - 用户和会话管理 + - `webauthn/README.md` - 详细文档和使用指南 + - `example/webauthn/` - 完整示例应用(中文界面) + - `webauthn/webauthn_test.go` - 单元测试 + +## 🚀 特性对比 + +| 功能 | Supabase 分支 | WebAuthn 分支 | +|-----|-------------|-------------| +| 认证方式 | 传统 OAuth2 | 无密码认证 | +| 安全性 | 标准 OAuth2 | 生物识别 + 硬件密钥 | +| 用户体验 | 传统登录流程 | 现代无密码体验 | +| 浏览器要求 | 所有现代浏览器 | Chrome 67+, Firefox 60+, Safari 14+ | +| 硬件要求 | 无特殊要求 | 需要支持 WebAuthn 的设备 | + +## 📦 如何使用 + +### 切换到 Supabase 分支 +```bash +git checkout cursor/implement-supabase-authentication-86da +cd example/supabase +go run main.go +``` + +### 切换到 WebAuthn 分支 +```bash +git checkout feature/webauthn-support +cd example/webauthn +go run main.go +``` + +## 🔄 分支合并策略 + +1. **功能分支 → master**: 当功能稳定且经过充分测试后 +2. **独立开发**: 各功能分支独立开发,避免冲突 +3. **向前兼容**: 新功能不影响现有功能 + +## 📝 开发建议 + +### Supabase 分支开发 +- 专注于 Supabase 特定功能和改进 +- 保持与 Supabase API 的同步 +- 增强错误处理和用户体验 + +### WebAuthn 分支开发 +- 关注无密码认证的安全性和兼容性 +- 扩展支持更多浏览器和设备 +- 优化用户体验和错误处理 + +## 🛡️ 安全考虑 + +### Supabase 分支 +- ✅ HTTPS 要求 +- ✅ 标准 OAuth2 安全流程 +- ✅ 令牌安全存储 + +### WebAuthn 分支 +- ✅ 生物识别验证 +- ✅ 硬件密钥支持 +- ✅ 防钓鱼攻击 +- ✅ 域名绑定验证 + +## 📊 测试状态 + +| 分支 | 编译状态 | 测试状态 | 示例运行 | +|-----|---------|---------|---------| +| Supabase | ✅ 通过 | ⚠️ 需要测试 | ✅ 正常 | +| WebAuthn | ✅ 通过 | ✅ 全部通过 | ✅ 正常 | + +--- + +**💡 提示**: 这种分支组织确保了功能的独立性,便于维护和合并,同时避免了不同功能之间的冲突。 \ No newline at end of file diff --git a/README.md b/README.md index 0cb2564..fc6e80c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,43 @@ To install the package, run: go get github.com/go-zoox/oauth2 ``` +## Supported Providers + +This library supports many OAuth2 providers, including: + +- **Supabase** - Full-featured authentication platform +- **GitHub** - Version control and collaboration +- **Google** - Google services authentication +- **Auth0** - Identity and access management +- **Microsoft Azure** - Microsoft cloud services +- **Slack** - Team communication platform +- **Discord** - Gaming and community communication +- **Facebook** - Social networking +- **GitLab** - DevOps platform +- **Twitter** - Social media platform +- **WeChat** - Chinese messaging platform +- **Doreamon** - Custom authentication provider +- And many more... + +### Supabase Provider + +The Supabase provider offers seamless integration with Supabase Auth: + +```go +import "github.com/go-zoox/oauth2/supabase" + +// Create Supabase client +client, err := supabase.New(&supabase.SupabaseConfig{ + BaseURL: "https://your-project.supabase.co", + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + RedirectURI: "http://localhost:8080/auth/callback", + Scope: "openid email profile", +}) +``` + +For detailed Supabase setup instructions, see the [Supabase provider documentation](supabase/README.md). + ## Getting Started ### Example 1: Using only one oauth2 provider => doreamon diff --git a/example/supabase/.env.example b/example/supabase/.env.example new file mode 100644 index 0000000..4d78066 --- /dev/null +++ b/example/supabase/.env.example @@ -0,0 +1,16 @@ +# Supabase OAuth2 Configuration +# Copy this file to .env and fill in your actual values + +# Your Supabase project URL (found in your Supabase dashboard) +SUPABASE_BASE_URL=https://your-project-id.supabase.co + +# OAuth2 credentials from your Supabase project +# These can be found in: Authentication > Settings > OAuth2 +SUPABASE_CLIENT_ID=your-client-id +SUPABASE_CLIENT_SECRET=your-client-secret + +# Callback URL - make sure this matches what you set in Supabase dashboard +SUPABASE_REDIRECT_URI=http://localhost:8080/auth/callback + +# Optional: Port for the example server (defaults to 8080) +PORT=8080 \ No newline at end of file diff --git a/example/supabase/main.go b/example/supabase/main.go new file mode 100644 index 0000000..6024af5 --- /dev/null +++ b/example/supabase/main.go @@ -0,0 +1,163 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/go-zoox/oauth2" + "github.com/go-zoox/oauth2/supabase" +) + +func main() { + // Environment variables needed: + // SUPABASE_BASE_URL=https://your-project.supabase.co + // SUPABASE_CLIENT_ID=your-client-id + // SUPABASE_CLIENT_SECRET=your-client-secret + // SUPABASE_REDIRECT_URI=http://localhost:8080/auth/callback + + baseURL := os.Getenv("SUPABASE_BASE_URL") + clientID := os.Getenv("SUPABASE_CLIENT_ID") + clientSecret := os.Getenv("SUPABASE_CLIENT_SECRET") + redirectURI := os.Getenv("SUPABASE_REDIRECT_URI") + + if baseURL == "" || clientID == "" || clientSecret == "" || redirectURI == "" { + log.Fatal("Missing required environment variables: SUPABASE_BASE_URL, SUPABASE_CLIENT_ID, SUPABASE_CLIENT_SECRET, SUPABASE_REDIRECT_URI") + } + + // Create Supabase OAuth2 client + client, err := supabase.New(&supabase.SupabaseConfig{ + BaseURL: baseURL, + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURI: redirectURI, + Scope: "openid email profile", + }) + if err != nil { + log.Fatal("Failed to create Supabase client:", err) + } + + // Home page + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + html := ` + + + + Supabase OAuth2 Example + + + +

Supabase OAuth2 Example

+

This is a simple example of using Supabase OAuth2 authentication.

+ Login with Supabase + Logout + + + ` + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(html)) + }) + + // Login endpoint + http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + client.Authorize("oauth2-state", func(loginURL string) { + http.Redirect(w, r, loginURL, http.StatusFound) + }) + }) + + // Logout endpoint + http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { + client.Logout(func(logoutURL string) { + if logoutURL == "" { + // If no logout URL provided, redirect to home + http.Redirect(w, r, "/", http.StatusFound) + } else { + http.Redirect(w, r, logoutURL, http.StatusFound) + } + }) + }) + + // OAuth callback endpoint + http.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + client.Callback(code, state, func(user *oauth2.User, token *oauth2.Token, err error) { + if err != nil { + log.Printf("OAuth callback error: %v", err) + http.Error(w, "Authentication failed: "+err.Error(), http.StatusInternalServerError) + return + } + + // Successful authentication + log.Printf("User authenticated: %+v", user) + log.Printf("Token: %+v", token) + + // Create a simple success page + html := ` + + + + Authentication Success + + + +

Authentication Success!

+
+

Welcome, you have successfully authenticated with Supabase!

+
+
+

User Information:

+

ID: %s

+

Email: %s

+

Username: %s

+

Nickname: %s

+
+ Go Home + + + ` + response := fmt.Sprintf(html, user.ID, user.Email, user.Username, user.Nickname) + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(response)) + }) + }) + + // Start the server + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Starting server on port %s", port) + log.Printf("Visit http://localhost:%s to test the Supabase OAuth2 integration", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} \ No newline at end of file diff --git a/example/supabase/supabase-example b/example/supabase/supabase-example new file mode 100755 index 0000000..cac1218 Binary files /dev/null and b/example/supabase/supabase-example differ diff --git a/go.mod b/go.mod index 6ddb7fc..67d10a5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/go-zoox/oauth2 -go 1.19 +go 1.23.0 + +toolchain go1.24.2 require ( github.com/go-zoox/cookie v1.2.0 diff --git a/go.sum b/go.sum index 900993e..40070b7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-zoox/chalk v1.0.2 h1:DCWft37fogmvqF37JdbGSLg28L/tQeA8u0lMvb62KOg= github.com/go-zoox/chalk v1.0.2/go.mod h1:z5+qvE9nEJI5uT4px2tyoFa/xxkqf3CUo22KmXLKbNI= github.com/go-zoox/cookie v1.2.0 h1:MO33lPQ/QGJIAEzgrsAfEpJc25lcJ/XR0w+smM19sNQ= @@ -14,13 +15,18 @@ github.com/go-zoox/headers v1.0.8/go.mod h1:WEgEbewswEw4n4qS1iG68Kn/vOQVCAKGwwuZ github.com/go-zoox/logger v1.4.6 h1:zHUaB6KQ9rD/N3hM0JJ3/JCNdgtedf4mVBBNNSyWCOg= github.com/go-zoox/logger v1.4.6/go.mod h1:o7ddvv/gMoMa0TomPhHoIz11ZWRbQ92pF6rwYbOY3iQ= github.com/go-zoox/testify v1.0.2 h1:G5sQ3xm0uwCuytnMhgnqZ5BItCt2DN3n2wLBqlIJEWA= +github.com/go-zoox/testify v1.0.2/go.mod h1:L35iVL6xDKDL/TQOTRWyNL4H4nm8bzs6nde5XA7PYnY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= @@ -31,6 +37,7 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/supabase/README.md b/supabase/README.md new file mode 100644 index 0000000..0c3c025 --- /dev/null +++ b/supabase/README.md @@ -0,0 +1,193 @@ +# Supabase OAuth2 Provider + +This package provides Supabase authentication support for the [go-zoox/oauth2](https://github.com/go-zoox/oauth2) library. + +## Features + +- Full OAuth2 integration with Supabase Auth +- Support for custom Supabase project URLs +- Automatic user information extraction +- Token refresh support +- Customizable scopes + +## Prerequisites + +1. A Supabase project - Create one at [https://supabase.com](https://supabase.com) +2. OAuth2 application configured in your Supabase project + +## Configuration + +### 1. Set up OAuth2 in Supabase Dashboard + +1. Go to your Supabase project dashboard +2. Navigate to **Authentication** > **Settings** +3. In the **Site URL** section, add your application's URL +4. In the **Redirect URLs** section, add your callback URL (e.g., `http://localhost:8080/auth/callback`) +5. Note down your project URL, it will be something like `https://your-project-id.supabase.co` + +### 2. Environment Variables + +Set the following environment variables: + +```bash +export SUPABASE_BASE_URL="https://your-project-id.supabase.co" +export SUPABASE_CLIENT_ID="your-client-id" +export SUPABASE_CLIENT_SECRET="your-client-secret" +export SUPABASE_REDIRECT_URI="http://localhost:8080/auth/callback" +``` + +## Usage + +### Basic Usage + +```go +package main + +import ( + "log" + "github.com/go-zoox/oauth2" + "github.com/go-zoox/oauth2/supabase" +) + +func main() { + // Create Supabase OAuth2 client + client, err := supabase.New(&supabase.SupabaseConfig{ + BaseURL: "https://your-project-id.supabase.co", + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + RedirectURI: "http://localhost:8080/auth/callback", + Scope: "openid email profile", + }) + if err != nil { + log.Fatal("Failed to create Supabase client:", err) + } + + // Start authentication flow + client.Authorize("state-value", func(loginURL string) { + // Redirect user to loginURL + log.Println("Visit:", loginURL) + }) +} +``` + +### Handle OAuth Callback + +```go +// Handle the OAuth callback +client.Callback(code, state, func(user *oauth2.User, token *oauth2.Token, err error) { + if err != nil { + log.Printf("Authentication failed: %v", err) + return + } + + // Authentication successful + log.Printf("User: %+v", user) + log.Printf("Token: %+v", token) + + // User information available: + // user.ID - User's unique ID + // user.Email - User's email address + // user.Username - User's username + // user.Nickname - User's display name + // user.Avatar - User's avatar URL +}) +``` + +### Custom Scopes + +```go +client, err := supabase.New(&supabase.SupabaseConfig{ + BaseURL: "https://your-project-id.supabase.co", + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + RedirectURI: "http://localhost:8080/auth/callback", + Scope: "openid email profile user_metadata", // Custom scopes +}) +``` + +## Configuration Options + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `BaseURL` | string | Yes | Your Supabase project URL (e.g., `https://your-project.supabase.co`) | +| `ClientID` | string | Yes | OAuth2 client ID from your Supabase project | +| `ClientSecret` | string | Yes | OAuth2 client secret from your Supabase project | +| `RedirectURI` | string | Yes | Callback URL after authentication | +| `Scope` | string | No | OAuth2 scopes (default: `"openid email profile"`) | + +## User Information + +The following user information is automatically extracted from Supabase: + +- **ID**: User's unique identifier +- **Email**: User's email address +- **Username**: User's username (falls back to email if not set) +- **Nickname**: User's display name from `user_metadata.full_name` +- **Avatar**: User's avatar URL from `user_metadata.avatar_url` + +## Example Application + +See the [example](../example/supabase/) directory for a complete working example with a web server. + +To run the example: + +1. Set the required environment variables +2. Run the example: + ```bash + cd example/supabase + go run main.go + ``` +3. Visit `http://localhost:8080` in your browser + +## Error Handling + +The provider handles common OAuth2 errors: + +- Invalid credentials +- Missing configuration +- Network errors +- Invalid callback parameters + +Always check for errors in the callback function: + +```go +client.Callback(code, state, func(user *oauth2.User, token *oauth2.Token, err error) { + if err != nil { + // Handle authentication error + log.Printf("Authentication failed: %v", err) + return + } + // Success case +}) +``` + +## Security Considerations + +1. **Environment Variables**: Store sensitive information like client secrets in environment variables +2. **HTTPS**: Always use HTTPS in production +3. **State Parameter**: Use a random state parameter to prevent CSRF attacks +4. **Scope Limitation**: Only request the minimum scopes required for your application +5. **Token Storage**: Store tokens securely and consider encryption for sensitive data + +## Troubleshooting + +### Common Issues + +1. **"Invalid redirect URI"**: Ensure your redirect URI is exactly the same in your code and Supabase dashboard +2. **"Invalid client"**: Check that your client ID and secret are correct +3. **"Base URL required"**: Make sure you provide the full Supabase project URL +4. **CORS errors**: Configure CORS settings in your Supabase dashboard if needed + +### Debug Mode + +Enable debug logging to see OAuth2 flow details: + +```go +import "github.com/go-zoox/logger" + +logger.SetLevel(logger.DEBUG) +``` + +## License + +This package is part of the [go-zoox/oauth2](https://github.com/go-zoox/oauth2) library and follows the same license terms. \ No newline at end of file diff --git a/supabase/supabase.go b/supabase/supabase.go new file mode 100644 index 0000000..a75e30d --- /dev/null +++ b/supabase/supabase.go @@ -0,0 +1,84 @@ +package supabase + +import ( + "fmt" + "net/url" + + "github.com/go-zoox/oauth2" +) + +type SupabaseConfig struct { + // Basic OAuth2 configuration + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RedirectURI string `json:"redirect_uri"` + Scope string `json:"scope"` + // Supabase specific configuration + BaseURL string `json:"base_url"` // e.g., "https://your-project.supabase.co" +} + +func New(cfg *SupabaseConfig) (oauth2.Client, error) { + if cfg.BaseURL == "" { + return nil, fmt.Errorf("supabase: base_url is required") + } + + scope := cfg.Scope + if scope == "" { + scope = "openid email profile" + } + + // Ensure BaseURL doesn't have trailing slash + baseURL := cfg.BaseURL + if baseURL[len(baseURL)-1] == '/' { + baseURL = baseURL[:len(baseURL)-1] + } + + config := oauth2.Config{ + Name: "Supabase", + AuthURL: baseURL + "/auth/v1/authorize", + TokenURL: baseURL + "/auth/v1/token", + UserInfoURL: baseURL + "/auth/v1/user", + LogoutURL: baseURL + "/auth/v1/logout", + Scope: scope, + RedirectURI: cfg.RedirectURI, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + // + AccessTokenAttributeName: "access_token", + RefreshTokenAttributeName: "refresh_token", + ExpiresInAttributeName: "expires_in", + TokenTypeAttributeName: "token_type", + // + EmailAttributeName: "email", + IDAttributeName: "id", + NicknameAttributeName: "user_metadata.full_name", + AvatarAttributeName: "user_metadata.avatar_url", + HomepageAttributeName: "user_metadata.website", + // + BaseURL: baseURL, + } + + // Custom register URL for Supabase + config.GetRegisterURL = func(oac *oauth2.Config) string { + // Supabase doesn't have a standard register endpoint, redirect to auth + return fmt.Sprintf("%s/auth/v1/signup", baseURL) + } + + // Custom login URL to handle Supabase's OAuth flow + config.GetLoginURL = func(oac *oauth2.Config, state string) string { + clientID := oac.ClientID + redirectURI := oac.RedirectURI + scope := oac.Scope + + params := url.Values{} + params.Add("client_id", clientID) + params.Add("redirect_uri", redirectURI) + params.Add("response_type", "code") + params.Add("scope", scope) + params.Add("state", state) + + return fmt.Sprintf("%s?%s", oac.AuthURL, params.Encode()) + } + + return oauth2.New(config) +} \ No newline at end of file