diff --git a/.gitignore b/.gitignore index 5b3d88e..c9f1c32 100644 --- a/.gitignore +++ b/.gitignore @@ -217,6 +217,7 @@ release/ go/**/assets go/**/websites go/**/bootstrap.sh +!go/bootstrap.sh go/**/bootstrap.bat go/**/bootstrap.ps1 nodejs/**/assets diff --git a/go/bootstrap.sh b/go/bootstrap.sh new file mode 100755 index 0000000..8785469 --- /dev/null +++ b/go/bootstrap.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Check if Go is installed +if ! command -v go &> /dev/null; then + echo "Error: Go is not installed on your system." + echo "To install Go, please visit: https://golang.org/doc/install" + echo "After installation, make sure 'go' command is available in your PATH" + exit 1 +fi + +# Print Go version +echo "Using Go version: $(go version)" + +# Change to src directory and build +# Build the binary +echo "Building application..." +go build -o build/main main.go || { + echo "Error: Failed to build the application" + exit 1 +} + +# Run the binary +echo "Starting the application..." +./build/main diff --git a/go/device-oauth/go.mod b/go/device-oauth/go.mod new file mode 100644 index 0000000..acb8a66 --- /dev/null +++ b/go/device-oauth/go.mod @@ -0,0 +1,7 @@ +module github.com/coze-dev/coze-oauth-quickstart/go/device-oauth + +go 1.18 + +require github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561 + +require github.com/golang-jwt/jwt v3.2.2+incompatible // indirect diff --git a/go/device-oauth/go.sum b/go/device-oauth/go.sum new file mode 100644 index 0000000..0de8723 --- /dev/null +++ b/go/device-oauth/go.sum @@ -0,0 +1,8 @@ +github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561 h1:LRZX2wQJEZ8VWuZipbhfiPJdirtKv7t840rIUVnLoSA= +github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561/go.mod h1:zc7+Lrzh5Lm7BYmqbtjMnV3eoRJpjE05LSqtT+iH+LE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/device-oauth/main.go b/go/device-oauth/main.go new file mode 100644 index 0000000..e65eb81 --- /dev/null +++ b/go/device-oauth/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "time" + + "github.com/coze-dev/coze-go" +) + +const CozeOAuthConfigPath = "coze_oauth_config.json" + +type Config struct { + ClientType string `json:"client_type"` + ClientID string `json:"client_id"` + CozeDomain string `json:"coze_www_base"` + CozeAPIBase string `json:"coze_api_base"` +} + +func loadConfig() (*Config, error) { + configFile, err := os.ReadFile(CozeOAuthConfigPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("coze_oauth_config.json not found in current directory") + } + return nil, fmt.Errorf("failed to read config file: %v", err) + } + + var config Config + if err := json.Unmarshal(configFile, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + + if config.ClientType != "device" { + return nil, fmt.Errorf("invalid client type: %s. expected: device", config.ClientType) + } + + return &config, nil +} + +func timestampToDateTime(timestamp int64) string { + t := time.Unix(timestamp, 0) + return t.Format("2006-01-02 15:04:05") +} + +func main() { + log.SetFlags(0) + + config, err := loadConfig() + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + oauth, err := coze.NewDeviceOAuthClient( + config.ClientID, + coze.WithAuthBaseURL(config.CozeAPIBase), + ) + if err != nil { + log.Fatalf("Error creating device OAuth client: %v\n", err) + } + + ctx := context.Background() + deviceCode, err := oauth.GetDeviceCode(ctx, &coze.GetDeviceOAuthCodeReq{}) + if err != nil { + log.Fatalf("Error getting device code: %v\n", err) + } + + fmt.Println("Please visit the following url to authorize the app:") + fmt.Printf(" URL: %s\n\n", deviceCode.VerificationURL) + + resp, err := oauth.GetAccessToken(ctx, &coze.GetDeviceOAuthAccessTokenReq{ + DeviceCode: deviceCode.DeviceCode, + Poll: true, + }) + if err != nil { + log.Fatalf("Error getting access token: %v\n", err) + } + + fmt.Printf("[device-oauth] access_token: %s\n", resp.AccessToken) + fmt.Printf("[device-oauth] refresh_token: %s\n", resp.RefreshToken) + expiresStr := timestampToDateTime(resp.ExpiresIn) + fmt.Printf("[device-oauth] expires_in: %d (%s)\n", resp.ExpiresIn, expiresStr) +} diff --git a/go/device-oauth/quickstart.md b/go/device-oauth/quickstart.md new file mode 100644 index 0000000..03098da --- /dev/null +++ b/go/device-oauth/quickstart.md @@ -0,0 +1,4 @@ +Run this command for linux/macos users: +```bash +./bootstrap.sh +``` diff --git a/go/jwt-oauth/bootstrap.sh b/go/jwt-oauth/bootstrap.sh deleted file mode 100755 index 80e5e86..0000000 --- a/go/jwt-oauth/bootstrap.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -# Check if Python is installed -if ! command -v python3 &> /dev/null; then - echo "Error: python3 is not installed" - echo "Please visit https://www.python.org/downloads/ to install Python" - echo "After installation, make sure 'python3' command is available in your PATH" - exit 1 -fi - -# Check Python version is >= 3.8 -python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))') -if [ "$(printf '%s\n' "3.8" "$python_version" | sort -V | head -n1)" != "3.8" ]; then - echo "Error: Python version must be >= 3.8" - echo "Current version: $python_version" - echo "Please upgrade Python to 3.8 or higher" - exit 1 -else - echo "Using Python version: $python_version" -fi - -# Check if .venv directory exists -if [ -d ".venv" ]; then - echo "Virtual environment already exists" - # Check if already in virtual environment - if [ -z "$VIRTUAL_ENV" ]; then - source .venv/bin/activate - echo "Activated virtual environment" - fi -else - echo "Creating new virtual environment and activating it..." - python3 -m venv .venv - source .venv/bin/activate -fi - -# Check dependencies installed -echo "Checking dependencies..." -installed_deps=$(pip freeze) -while IFS= read -r line || [[ -n "$line" ]]; do - [ -z "$line" ] && continue # skip empty line - pkg_name=$(echo "$line" | cut -d'=' -f1) - pkg_version=$(echo "$line" | cut -d'=' -f3) - if echo "$installed_deps" | grep -q "^$pkg_name==$pkg_version$"; then - echo "✓ $pkg_name==$pkg_version installed" - else - echo "Installing $pkg_name==$pkg_version ..." - pip install -q "$pkg_name==$pkg_version" - fi -done < requirements.txt - -# Run the application -python3 main.py diff --git a/go/jwt-oauth/src/go.mod b/go/jwt-oauth/go.mod similarity index 100% rename from go/jwt-oauth/src/go.mod rename to go/jwt-oauth/go.mod diff --git a/go/jwt-oauth/src/go.sum b/go/jwt-oauth/go.sum similarity index 100% rename from go/jwt-oauth/src/go.sum rename to go/jwt-oauth/go.sum diff --git a/go/jwt-oauth/main.go b/go/jwt-oauth/main.go new file mode 100644 index 0000000..0e36db7 --- /dev/null +++ b/go/jwt-oauth/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/coze-dev/coze-go" +) + +const CozeOAuthConfigPath = "coze_oauth_config.json" + +type Config struct { + ClientType string `json:"client_type"` + ClientID string `json:"client_id"` + PrivateKey string `json:"private_key"` + PublicKeyID string `json:"public_key_id"` + CozeDomain string `json:"coze_www_base"` + CozeAPIBase string `json:"coze_api_base"` +} + +type TokenResponse struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn string `json:"expires_in"` +} + +func loadConfig() (*Config, error) { + configFile, err := os.ReadFile(CozeOAuthConfigPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("coze_oauth_config.json not found in current directory") + } + return nil, fmt.Errorf("failed to read config file: %v", err) + } + + var config Config + if err := json.Unmarshal(configFile, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + + if config.ClientType != "jwt" { + return nil, fmt.Errorf("invalid client type: %s. expected: jwt", config.ClientType) + } + + return &config, nil +} + +func timestampToDateTime(timestamp int64) string { + t := time.Unix(timestamp, 0) + return t.Format("2006-01-02 15:04:05") +} + +func readHTMLTemplate(filePath string) (string, error) { + content, err := ioutil.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("failed to read template file: %v", err) + } + return string(content), nil +} + +func renderTemplate(template string, data map[string]interface{}) string { + result := template + for key, value := range data { + placeholder := "{{" + key + "}}" + result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) + } + return result +} + +func main() { + log.SetFlags(0) + + config, err := loadConfig() + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + oauth, err := coze.NewJWTOAuthClient(coze.NewJWTOAuthClientParam{ + ClientID: config.ClientID, + PublicKey: config.PublicKeyID, + PrivateKeyPEM: config.PrivateKey, + }, coze.WithAuthBaseURL(config.CozeAPIBase)) + if err != nil { + log.Fatalf("Error creating JWT OAuth client: %v\n", err) + } + + listener, err := net.Listen("tcp", "127.0.0.1:8080") + if err != nil { + log.Fatalf("Port 8080 is already in use by another application. Please free up the port and try again") + } + listener.Close() + + fs := http.FileServer(http.Dir("assets")) + http.Handle("/assets/", http.StripPrefix("/assets/", fs)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + template, err := readHTMLTemplate("websites/index.html") + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "client_type": config.ClientType, + "client_id": config.ClientID, + "coze_www_base": config.CozeDomain, + } + + result := renderTemplate(template, data) + w.Write([]byte(result)) + }) + + http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/callback", http.StatusFound) + }) + + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + resp, err := oauth.GetAccessToken(ctx, nil) + if err != nil { + template, parseErr := readHTMLTemplate("websites/error.html") + if parseErr != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "error": fmt.Sprintf("Failed to get access token: %v", err), + "coze_www_base": config.CozeDomain, + } + + w.WriteHeader(http.StatusInternalServerError) + result := renderTemplate(template, data) + w.Write([]byte(result)) + return + } + + expiresStr := fmt.Sprintf("%d (%s)", resp.ExpiresIn, timestampToDateTime(resp.ExpiresIn)) + tokenResp := TokenResponse{ + TokenType: "Bearer", + AccessToken: resp.AccessToken, + RefreshToken: "", + ExpiresIn: expiresStr, + } + + // Check if it's an AJAX request + if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(tokenResp) + return + } + + // Otherwise render the callback template + template, err := readHTMLTemplate("websites/callback.html") + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "token_type": tokenResp.TokenType, + "access_token": tokenResp.AccessToken, + "refresh_token": tokenResp.RefreshToken, + "expires_in": tokenResp.ExpiresIn, + } + + result := renderTemplate(template, data) + w.Write([]byte(result)) + }) + + log.Printf("\nServer starting on http://127.0.0.1:8080 (API Base: %s, Client Type: %s, Client ID: %s)\n", + config.CozeAPIBase, config.ClientType, config.ClientID) + if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/go/jwt-oauth/quickstart.md b/go/jwt-oauth/quickstart.md index e69de29..03098da 100644 --- a/go/jwt-oauth/quickstart.md +++ b/go/jwt-oauth/quickstart.md @@ -0,0 +1,4 @@ +Run this command for linux/macos users: +```bash +./bootstrap.sh +``` diff --git a/go/jwt-oauth/src/main.go b/go/jwt-oauth/src/main.go deleted file mode 100644 index 06de17f..0000000 --- a/go/jwt-oauth/src/main.go +++ /dev/null @@ -1,136 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net" - "net/http" - "os" - "time" - - "github.com/coze-dev/coze-go" -) - -const CozeOAuthConfigPath = "coze_oauth_config.json" - -type Config struct { - ClientType string `json:"client_type"` - ClientID string `json:"client_id"` - PrivateKey string `json:"private_key"` - PublicKeyID string `json:"public_key_id"` - CozeDomain string `json:"coze_domain"` - CozeAPIBase string `json:"coze_api_base"` -} - -type TokenResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int64 `json:"expires_in"` -} - -func loadConfig() (*Config, error) { - // Read config file from current directory - configFile, err := os.ReadFile(CozeOAuthConfigPath) - if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("coze_oauth_config.json not found in current directory") - } - return nil, fmt.Errorf("failed to read config file: %v", err) - } - - var config Config - if err := json.Unmarshal(configFile, &config); err != nil { - return nil, fmt.Errorf("failed to parse config file: %v", err) - } - - return &config, nil -} - -func main() { - // Set log flags to remove timestamp - log.SetFlags(0) - - // Load configuration from coze_oauth_config.json - config, err := loadConfig() - if err != nil { - log.Fatalf("Error loading config: %v", err) - } - - // Initialize the JWT OAuth client - oauth, err := coze.NewJWTOAuthClient(coze.NewJWTOAuthClientParam{ - ClientID: config.ClientID, - PublicKey: config.PublicKeyID, - PrivateKeyPEM: config.PrivateKey, - }, coze.WithAuthBaseURL(config.CozeAPIBase)) - if err != nil { - log.Fatalf("Error creating JWT OAuth client: %v\n", err) - } - - // Check if port 8080 is available - listener, err := net.Listen("tcp", "127.0.0.1:8080") - if err != nil { - log.Fatalf("Port 8080 is already in use by another application. Please free up the port and try again") - } - listener.Close() - - // Define HTTP handler - http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - ctx := context.Background() - resp, err := oauth.GetAccessToken(ctx, nil) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting access token: %v", err), http.StatusInternalServerError) - return - } - - tokenResp := TokenResponse{ - AccessToken: resp.AccessToken, - ExpiresIn: resp.ExpiresIn, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(tokenResp) - }) - - // Start HTTP server in a separate goroutine - go func() { - log.Printf("\nServer starting on 127.0.0.1:8080... (API Base: %s, Client Type: %s, Client ID: %s)\n", - config.CozeAPIBase, config.ClientType, config.ClientID) - if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil { - log.Fatalf("Server failed to start: %v", err) - } - }() - - // Wait a moment for the server to start - time.Sleep(time.Second) - - // Make a POST request to the local /token endpoint - log.Println("\nMaking request to /token endpoint to get access token...") - resp, err := http.Get("http://127.0.0.1:8080/token") - if err != nil { - log.Fatalf("Failed to request token: %v", err) - } - defer resp.Body.Close() - - // Parse the response - var tokenResp TokenResponse - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - log.Fatalf("Failed to decode token response: %v", err) - } - - // Print the access token information - log.Printf("Successfully obtained access token:") - log.Printf("Access Token: %s", tokenResp.AccessToken) - expiresAt := time.Unix(tokenResp.ExpiresIn, 0) - log.Printf("Token will expire at: %s", expiresAt.Format("2006-01-02 15:04:05")) - - log.Printf("\nServer is still running. You can get a new access token anytime using: curl http://127.0.0.1:8080/token") - - // Keep the main goroutine running - select {} -} diff --git a/go/pkce-oauth/go.mod b/go/pkce-oauth/go.mod new file mode 100644 index 0000000..960ac5b --- /dev/null +++ b/go/pkce-oauth/go.mod @@ -0,0 +1,13 @@ +module github.com/coze-dev/coze-oauth-quickstart/go/pkce-oauth + +go 1.18 + +require ( + github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561 + github.com/gorilla/sessions v1.2.2 +) + +require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/securecookie v1.1.2 // indirect +) diff --git a/go/pkce-oauth/go.sum b/go/pkce-oauth/go.sum new file mode 100644 index 0000000..75075a0 --- /dev/null +++ b/go/pkce-oauth/go.sum @@ -0,0 +1,18 @@ +github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561 h1:LRZX2wQJEZ8VWuZipbhfiPJdirtKv7t840rIUVnLoSA= +github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561/go.mod h1:zc7+Lrzh5Lm7BYmqbtjMnV3eoRJpjE05LSqtT+iH+LE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/pkce-oauth/main.go b/go/pkce-oauth/main.go new file mode 100644 index 0000000..cb678f9 --- /dev/null +++ b/go/pkce-oauth/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/coze-dev/coze-go" + "github.com/gorilla/sessions" +) + +const ( + CozeOAuthConfigPath = "coze_oauth_config.json" + RedirectURI = "http://127.0.0.1:8080/callback" +) + +type Config struct { + ClientType string `json:"client_type"` + ClientID string `json:"client_id"` + CozeDomain string `json:"coze_www_base"` + CozeAPIBase string `json:"coze_api_base"` +} + +type TokenResponse struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn string `json:"expires_in"` +} + +var ( + store = sessions.NewCookieStore([]byte("secret-key")) +) + +func loadConfig() (*Config, error) { + configFile, err := os.ReadFile(CozeOAuthConfigPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("coze_oauth_config.json not found in current directory") + } + return nil, fmt.Errorf("failed to read config file: %v", err) + } + + var config Config + if err := json.Unmarshal(configFile, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + + if config.ClientType != "pkce" { + return nil, fmt.Errorf("invalid client type: %s, expect pkce", config.ClientType) + } + + return &config, nil +} + +func timestampToDateTime(timestamp int64) string { + t := time.Unix(timestamp, 0) + return t.Format("2006-01-02 15:04:05") +} + +func readHTMLTemplate(filePath string) (string, error) { + content, err := ioutil.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("failed to read template file: %v", err) + } + return string(content), nil +} + +func renderTemplate(template string, data map[string]interface{}) string { + result := template + for key, value := range data { + placeholder := "{{" + key + "}}" + result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) + } + return result +} + +func handleError(w http.ResponseWriter, config *Config, err error) { + template, parseErr := readHTMLTemplate("websites/error.html") + if parseErr != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "error": err.Error(), + "coze_www_base": config.CozeDomain, + } + + w.WriteHeader(http.StatusInternalServerError) + result := renderTemplate(template, data) + w.Write([]byte(result)) +} + +func main() { + log.SetFlags(0) + + config, err := loadConfig() + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + oauth, err := coze.NewPKCEOAuthClient(config.ClientID, coze.WithAuthBaseURL(config.CozeAPIBase)) + if err != nil { + log.Fatalf("Error creating OAuth client: %v", err) + } + + fs := http.FileServer(http.Dir("assets")) + http.Handle("/assets/", http.StripPrefix("/assets/", fs)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + template, err := readHTMLTemplate("websites/index.html") + if err != nil { + handleError(w, config, fmt.Errorf("failed to read template: %v", err)) + return + } + + data := map[string]interface{}{ + "client_type": config.ClientType, + "client_id": config.ClientID, + } + + result := renderTemplate(template, data) + w.Write([]byte(result)) + }) + + http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + resp, err := oauth.GetOAuthURL(ctx, &coze.GetPKCEOAuthURLReq{ + RedirectURI: RedirectURI, + State: "random", + }) + if err != nil { + handleError(w, config, fmt.Errorf("failed to get OAuth URL: %v", err)) + return + } + + session, _ := store.Get(r, "pkce-session") + session.Values["code_verifier"] = resp.CodeVerifier + session.Save(r, w) + + http.Redirect(w, r, resp.AuthorizationURL, http.StatusFound) + }) + + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code == "" { + handleError(w, config, fmt.Errorf("authorization failed: no authorization code received")) + return + } + + session, _ := store.Get(r, "pkce-session") + codeVerifier, ok := session.Values["code_verifier"].(string) + if !ok { + handleError(w, config, fmt.Errorf("authorization failed: no code verifier found")) + return + } + + ctx := context.Background() + resp, err := oauth.GetAccessToken(ctx, &coze.GetPKCEAccessTokenReq{ + Code: code, + RedirectURI: RedirectURI, + CodeVerifier: codeVerifier, + }) + if err != nil { + handleError(w, config, fmt.Errorf("failed to get access token: %v", err)) + return + } + + // Store token in session + tokenSession, _ := store.Get(r, fmt.Sprintf("oauth_token_%s", config.ClientID)) + tokenSession.Values["token_type"] = "Bearer" + tokenSession.Values["access_token"] = resp.AccessToken + tokenSession.Values["refresh_token"] = resp.RefreshToken + tokenSession.Values["expires_in"] = resp.ExpiresIn + tokenSession.Save(r, w) + + expiresStr := fmt.Sprintf("%d (%s)", resp.ExpiresIn, timestampToDateTime(resp.ExpiresIn)) + + template, err := readHTMLTemplate("websites/callback.html") + if err != nil { + handleError(w, config, fmt.Errorf("failed to read template: %v", err)) + return + } + + data := map[string]interface{}{ + "token_type": "Bearer", + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "expires_in": expiresStr, + "coze_www_base": config.CozeDomain, + } + + result := renderTemplate(template, data) + w.Write([]byte(result)) + }) + + http.HandleFunc("/refresh_token", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var requestData struct { + RefreshToken string `json:"refresh_token"` + } + + if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if requestData.RefreshToken == "" { + http.Error(w, "No refresh token provided", http.StatusBadRequest) + return + } + + ctx := context.Background() + resp, err := oauth.RefreshToken(ctx, requestData.RefreshToken) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to refresh token: %v", err), http.StatusInternalServerError) + return + } + + // Update session + session, _ := store.Get(r, fmt.Sprintf("oauth_token_%s", config.ClientID)) + session.Values["token_type"] = "Bearer" + session.Values["access_token"] = resp.AccessToken + session.Values["refresh_token"] = resp.RefreshToken + session.Values["expires_in"] = resp.ExpiresIn + session.Save(r, w) + + expiresStr := fmt.Sprintf("%d (%s)", resp.ExpiresIn, timestampToDateTime(resp.ExpiresIn)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "token_type": "Bearer", + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "expires_in": expiresStr, + }) + }) + + log.Printf("Server starting on http://127.0.0.1:8080 (API Base: %s, Client Type: %s, Client ID: %s)\n", + config.CozeAPIBase, config.ClientType, config.ClientID) + if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/go/pkce-oauth/quickstart.md b/go/pkce-oauth/quickstart.md new file mode 100644 index 0000000..03098da --- /dev/null +++ b/go/pkce-oauth/quickstart.md @@ -0,0 +1,4 @@ +Run this command for linux/macos users: +```bash +./bootstrap.sh +``` diff --git a/go/web-oauth/go.mod b/go/web-oauth/go.mod new file mode 100644 index 0000000..4687582 --- /dev/null +++ b/go/web-oauth/go.mod @@ -0,0 +1,13 @@ +module github.com/coze-dev/coze-oauth-quickstart/go/web-oauth + +go 1.18 + +require ( + github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561 + github.com/gorilla/sessions v1.3.0 +) + +require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/securecookie v1.1.2 // indirect +) diff --git a/go/web-oauth/go.sum b/go/web-oauth/go.sum new file mode 100644 index 0000000..3834a70 --- /dev/null +++ b/go/web-oauth/go.sum @@ -0,0 +1,18 @@ +github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561 h1:LRZX2wQJEZ8VWuZipbhfiPJdirtKv7t840rIUVnLoSA= +github.com/coze-dev/coze-go v0.0.0-20250115094839-23bd27bd6561/go.mod h1:zc7+Lrzh5Lm7BYmqbtjMnV3eoRJpjE05LSqtT+iH+LE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= +github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/web-oauth/main.go b/go/web-oauth/main.go new file mode 100644 index 0000000..6bb5e95 --- /dev/null +++ b/go/web-oauth/main.go @@ -0,0 +1,244 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/coze-dev/coze-go" + "github.com/gorilla/sessions" +) + +const ( + CozeOAuthConfigPath = "coze_oauth_config.json" + RedirectURI = "http://127.0.0.1:8080/callback" +) + +type Config struct { + ClientType string `json:"client_type"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + CozeDomain string `json:"coze_www_base"` + CozeAPIBase string `json:"coze_api_base"` +} + +type TokenResponse struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn string `json:"expires_in"` +} + +var ( + store = sessions.NewCookieStore([]byte("secret-key")) +) + +func loadConfig() (*Config, error) { + configFile, err := os.ReadFile(CozeOAuthConfigPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("coze_oauth_config.json not found in current directory") + } + return nil, fmt.Errorf("failed to read config file: %v", err) + } + + var config Config + if err := json.Unmarshal(configFile, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + + if config.ClientType != "web" { + return nil, fmt.Errorf("invalid client type: %s, expect web", config.ClientType) + } + + return &config, nil +} + +func timestampToDateTime(timestamp int64) string { + t := time.Unix(timestamp, 0) + return t.Format("2006-01-02 15:04:05") +} + +func readHTMLTemplate(filePath string) (string, error) { + content, err := ioutil.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("failed to read template file: %v", err) + } + return string(content), nil +} + +func renderTemplate(template string, data map[string]interface{}) string { + result := template + for key, value := range data { + placeholder := "{{" + key + "}}" + result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) + } + return result +} + +func handleError(w http.ResponseWriter, config *Config, err error) { + template, parseErr := readHTMLTemplate("websites/error.html") + if parseErr != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "error": err.Error(), + "coze_www_base": config.CozeDomain, + } + + w.WriteHeader(http.StatusInternalServerError) + result := renderTemplate(template, data) + w.Write([]byte(result)) +} + +func main() { + log.SetFlags(0) + + config, err := loadConfig() + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + oauth, err := coze.NewWebOAuthClient(config.ClientID, config.ClientSecret, coze.WithAuthBaseURL(config.CozeAPIBase)) + if err != nil { + log.Fatalf("Error creating OAuth client: %v", err) + } + + fs := http.FileServer(http.Dir("assets")) + http.Handle("/assets/", http.StripPrefix("/assets/", fs)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + template, err := readHTMLTemplate("websites/index.html") + if err != nil { + handleError(w, config, fmt.Errorf("failed to read template: %v", err)) + return + } + + data := map[string]interface{}{ + "client_type": config.ClientType, + "client_id": config.ClientID, + } + + result := renderTemplate(template, data) + w.Write([]byte(result)) + }) + + http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + authURL := oauth.GetOAuthURL(ctx, &coze.GetWebOAuthURLReq{ + RedirectURI: RedirectURI, + State: "random", + }) + http.Redirect(w, r, authURL, http.StatusFound) + }) + + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code == "" { + handleError(w, config, fmt.Errorf("authorization failed: no authorization code received")) + return + } + + ctx := context.Background() + resp, err := oauth.GetAccessToken(ctx, &coze.GetWebOAuthAccessTokenReq{ + Code: code, + RedirectURI: RedirectURI, + }) + if err != nil { + handleError(w, config, fmt.Errorf("failed to get access token: %v", err)) + return + } + + // Store token in session + session, _ := store.Get(r, fmt.Sprintf("oauth_token_%s", config.ClientID)) + session.Values["token_type"] = "Bearer" + session.Values["access_token"] = resp.AccessToken + session.Values["refresh_token"] = resp.RefreshToken + session.Values["expires_in"] = resp.ExpiresIn + session.Save(r, w) + + expiresStr := fmt.Sprintf("%d (%s)", resp.ExpiresIn, timestampToDateTime(resp.ExpiresIn)) + + template, err := readHTMLTemplate("websites/callback.html") + if err != nil { + handleError(w, config, fmt.Errorf("failed to read template: %v", err)) + return + } + + data := map[string]interface{}{ + "token_type": "Bearer", + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "expires_in": expiresStr, + "coze_www_base": config.CozeDomain, + } + + result := renderTemplate(template, data) + w.Write([]byte(result)) + }) + + http.HandleFunc("/refresh_token", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var requestData struct { + RefreshToken string `json:"refresh_token"` + } + + if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if requestData.RefreshToken == "" { + http.Error(w, "No refresh token provided", http.StatusBadRequest) + return + } + + ctx := context.Background() + resp, err := oauth.RefreshToken(ctx, requestData.RefreshToken) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to refresh token: %v", err), http.StatusInternalServerError) + return + } + + // Update session + session, _ := store.Get(r, fmt.Sprintf("oauth_token_%s", config.ClientID)) + session.Values["token_type"] = "Bearer" + session.Values["access_token"] = resp.AccessToken + session.Values["refresh_token"] = resp.RefreshToken + session.Values["expires_in"] = resp.ExpiresIn + session.Save(r, w) + + expiresStr := fmt.Sprintf("%d (%s)", resp.ExpiresIn, timestampToDateTime(resp.ExpiresIn)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "token_type": "Bearer", + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "expires_in": expiresStr, + }) + }) + + log.Printf("Server starting on http://127.0.0.1:8080 (API Base: %s, Client Type: %s, Client ID: %s)\n", + config.CozeAPIBase, config.ClientType, config.ClientID) + if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/go/web-oauth/quickstart.md b/go/web-oauth/quickstart.md new file mode 100644 index 0000000..03098da --- /dev/null +++ b/go/web-oauth/quickstart.md @@ -0,0 +1,4 @@ +Run this command for linux/macos users: +```bash +./bootstrap.sh +```