- Added Cloudflare Turnstile site key and secret to the environment variables in both `pr-test.yml` and `unit-test.yml` workflows. - Introduced a new endpoint in the Signin API for generating OAuth authorization URLs, improving support for third-party authentication providers. - Enhanced the handling of OAuth provider configurations, including validation and error responses for missing or incomplete settings. - Updated tests to cover the new OAuth authorization URL functionality and ensure sensitive data is not exposed in public configurations.
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
package signin
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
"github.com/yaoapp/yao/openapi/response"
|
|
)
|
|
|
|
// Attach attaches the signin handlers to the router
|
|
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|
group.GET("/signin", getConfig)
|
|
group.POST("/signin", signin)
|
|
group.GET("/signin/authback/:id", authback)
|
|
group.GET("/signin/oauth/:provider/authorize", getOAuthAuthorizationURL)
|
|
}
|
|
|
|
// getConfig is the handler for get signin configuration
|
|
func getConfig(c *gin.Context) {
|
|
// Get locale from query parameter (optional)
|
|
locale := c.Query("locale")
|
|
|
|
// Get public configuration for the specified locale
|
|
config := GetPublicConfig(locale)
|
|
|
|
// If no configuration found, return error
|
|
if config == nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "No signin configuration found for the requested locale",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
// Return the public configuration
|
|
response.RespondWithSuccess(c, response.StatusOK, config)
|
|
}
|
|
|
|
// signin is the handler for signin (password login)
|
|
func signin(c *gin.Context) {}
|
|
|
|
// authback is the handler for authback
|
|
func authback(c *gin.Context) {}
|
|
|
|
// OAuthAuthorizationURLResponse represents the response for OAuth authorization URL
|
|
type OAuthAuthorizationURLResponse struct {
|
|
AuthorizationURL string `json:"authorization_url"`
|
|
State string `json:"state"`
|
|
}
|
|
|
|
// getOAuthAuthorizationURL generates OAuth authorization URL for a provider
|
|
func getOAuthAuthorizationURL(c *gin.Context) {
|
|
providerID := c.Param("provider")
|
|
if providerID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Provider ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Get optional parameters
|
|
redirectURI := c.Query("redirect_uri")
|
|
state := c.Query("state")
|
|
locale := c.Query("locale")
|
|
|
|
// Get full configuration
|
|
config := GetFullConfig(locale)
|
|
if config == nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "No signin configuration found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
// Find the provider
|
|
var provider *Provider
|
|
if config.ThirdParty != nil && config.ThirdParty.Providers != nil {
|
|
for _, p := range config.ThirdParty.Providers {
|
|
if p.ID == providerID {
|
|
provider = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if provider == nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: fmt.Sprintf("OAuth provider '%s' not found", providerID),
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
// Validate required provider configuration
|
|
if provider.ClientID == "" || provider.Endpoints == nil || provider.Endpoints.Authorization == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Provider configuration is incomplete",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Generate state if not provided
|
|
if state == "" {
|
|
var err error
|
|
state, err = generateRandomState()
|
|
if err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Failed to generate OAuth state",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Set default redirect URI if not provided
|
|
if redirectURI == "" {
|
|
redirectURI = fmt.Sprintf("%s://%s/auth/callback", getScheme(c), c.Request.Host)
|
|
}
|
|
|
|
// Build authorization URL
|
|
params := url.Values{}
|
|
params.Add("client_id", provider.ClientID)
|
|
params.Add("response_type", "code")
|
|
params.Add("redirect_uri", redirectURI)
|
|
params.Add("state", state)
|
|
|
|
// Add scopes
|
|
if len(provider.Scopes) > 0 {
|
|
params.Add("scope", strings.Join(provider.Scopes, " "))
|
|
} else {
|
|
params.Add("scope", "openid profile email")
|
|
}
|
|
|
|
authorizationURL := fmt.Sprintf("%s?%s", provider.Endpoints.Authorization, params.Encode())
|
|
|
|
// Return the authorization URL and state
|
|
response.RespondWithSuccess(c, response.StatusOK, &OAuthAuthorizationURLResponse{
|
|
AuthorizationURL: authorizationURL,
|
|
State: state,
|
|
})
|
|
}
|
|
|
|
// generateRandomState generates a cryptographically secure random state parameter
|
|
func generateRandomState() (string, error) {
|
|
bytes := make([]byte, 16)
|
|
_, err := rand.Read(bytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
// getScheme returns the request scheme (http or https)
|
|
func getScheme(c *gin.Context) string {
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
return "https"
|
|
}
|
|
return "http"
|
|
}
|