Refactor OAuth handling and enhance error management in Signin API

- Removed unused OAuth response structures to streamline the codebase.
- Improved the authback function to handle redirect URIs and provider retrieval more effectively, enhancing error responses for invalid requests.
- Updated the getOAuthAuthorizationURL function to include better handling of provider configurations and session state management.
- Introduced a new function to normalize expiration formats for client secrets, ensuring consistent duration handling across providers.
This commit is contained in:
Max 2025-08-01 11:34:05 +08:00
parent 1a2b366056
commit 39b834c929
4 changed files with 686 additions and 147 deletions

View file

@ -18,27 +18,6 @@ import (
"github.com/yaoapp/yao/openapi/utils"
)
// OAuthAuthorizationURLResponse represents the response for OAuth authorization URL
type OAuthAuthorizationURLResponse struct {
AuthorizationURL string `json:"authorization_url"`
State string `json:"state"`
}
// OAuthCallbackResponse represents the response for OAuth callback
type OAuthCallbackResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
// OAuthAuthbackRequest represents the request for OAuth callback
type OAuthAuthbackRequest struct {
Code string `json:"code" form:"code"`
State string `json:"state" form:"state"`
Provider string `json:"provider" form:"provider"`
Scope string `json:"scope,omitempty" form:"scope,omitempty"`
}
// Attach attaches the signin handlers to the router
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
group.GET("/signin", getConfig)
@ -95,12 +74,6 @@ func authbackPrepare(c *gin.Context) {
return
}
// Remove the redirect URI from the session
err = removeRedirectURI(providerID, state)
if err != nil {
log.Warn("Failed to remove redirect URI: %v", err)
}
params := url.Values{}
params.Add("code", code)
params.Add("state", state)
@ -140,8 +113,47 @@ func authback(c *gin.Context) {
return
}
// Remove the state from the session
err := removeState(providerID, sid)
// Get redirect URI
redirectURI, err := getRedirectURI(providerID, params.State)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Failed to get redirect URI",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Get provider
provider, err := GetProvider(params.Locale, providerID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: fmt.Sprintf("Failed to get provider: %v", err),
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// if response mode is form_post
if provider.ResponseMode == "form_post" {
// Replace the redirectURI to
pathname := strings.Replace(c.Request.URL.Path, "/signin/authback/", "/signin/oauth/", 1) + "/authorize/prepare"
fmt.Println(pathname)
newRedirectURI, err := reconstructRedirectURI(redirectURI, pathname, c)
if err != nil {
log.Error("Failed to reconstruct redirectURI: %v", err)
response.RespondWithError(c, response.StatusBadRequest, &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid redirect URI format",
})
return
}
redirectURI = newRedirectURI
}
// Remove the state from the session and cache
err = removeState(providerID, sid)
if err != nil {
log.With(log.F{"sid": sid, "providerID": providerID}).Error("Failed to remove state")
errorResp := &response.ErrorResponse{
@ -152,8 +164,23 @@ func authback(c *gin.Context) {
return
}
// Get UserInfo
tokenResponse, err := provider.AccessToken(params.Code, redirectURI)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: fmt.Sprintf("Failed to get user info: %v", err),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Respond with success
response.RespondWithSuccess(c, response.StatusOK, params)
response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{
"params": params,
"token": tokenResponse,
"provider": provider,
})
}
// getOAuthAuthorizationURL generates OAuth authorization URL for a provider
@ -173,28 +200,16 @@ func getOAuthAuthorizationURL(c *gin.Context) {
state := c.Query("state")
locale := c.Query("locale")
// Get full configuration
config := GetFullConfig(locale)
if config == nil {
provider, err := GetProvider(locale, providerID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "No signin configuration found",
ErrorDescription: "Failed to get provider",
}
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,
@ -260,7 +275,7 @@ func getOAuthAuthorizationURL(c *gin.Context) {
}
// Save the state to the session for 20 minutes
err := saveState(providerID, sid, state)
err = saveState(providerID, sid, state)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
@ -270,18 +285,8 @@ func getOAuthAuthorizationURL(c *gin.Context) {
return
}
// if response mode is form_post, save the redirect URI to the session
// if response mode is form_post
if provider.ResponseMode == "form_post" {
err := saveRedirectURI(providerID, state, redirectURI)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Failed to save OAuth redirect URI",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Replace the redirectURI to
pathname := c.Request.URL.Path + "/prepare"
newRedirectURI, err := reconstructRedirectURI(redirectURI, pathname, c)
@ -297,6 +302,17 @@ func getOAuthAuthorizationURL(c *gin.Context) {
params.Set("redirect_uri", newRedirectURI)
}
// Save the redirect URI to the cache
err = saveRedirectURI(providerID, state, redirectURI)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Failed to save OAuth redirect URI",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Build the authorization URL
authorizationURL := fmt.Sprintf("%s?%s", provider.Endpoints.Authorization, params.Encode())
@ -385,6 +401,14 @@ func removeRedirectURI(providerID, state string) error {
}
func removeState(providerID, sid string) error {
// Get the state from the session
state, err := session.Global().ID(sid).Get(fmt.Sprintf("oauth_state_%s", providerID))
if err != nil {
return err
}
// Remove the redirect URI from the cache
removeRedirectURI(providerID, state.(string))
return session.Global().ID(sid).Del(fmt.Sprintf("oauth_state_%s", providerID))
}

394
openapi/signin/provider.go Normal file
View file

@ -0,0 +1,394 @@
package signin
import (
"crypto/ecdsa"
"crypto/hmac"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/http"
"github.com/yaoapp/kun/log"
)
// GetClientSecret gets the client secret for the provider
func (p *Provider) GetClientSecret() (string, error) {
if p.ClientSecret != "" {
return p.ClientSecret, nil
}
if p.ClientSecretGenerator == nil {
return "", fmt.Errorf("client secret generator not found, set client_secret or client_secret_generator at least one")
}
// Generate the client secret using the configured generator
return p.GenerateClientSecret()
}
// GetUserInfo gets the user information from the provider
func (p *Provider) GetUserInfo(accessToken string) (*OAuthUserInfoResponse, error) {
if p.Endpoints == nil {
return nil, fmt.Errorf("endpoints not found, set endpoints at least one")
}
return nil, nil
}
// GenerateClientSecret generates client secret based on the configured generator type
func (p *Provider) GenerateClientSecret() (string, error) {
if p.ClientSecretGenerator == nil {
return "", fmt.Errorf("client secret generator not configured")
}
switch p.ClientSecretGenerator.Type {
case "JWT_ES256", "JWT_APPLE": // Apple JWT is the same as JWT_ES256
return p.generateJWTES256()
case "BASIC_CONCAT":
return p.generateBasicConcat()
case "HMAC_SHA256":
return p.generateHMACSignature()
default:
return "", fmt.Errorf("unsupported client secret generator type: %s", p.ClientSecretGenerator.Type)
}
}
// generateJWTES256 generates JWT client secret using ES256 algorithm
func (p *Provider) generateJWTES256() (string, error) {
gen := p.ClientSecretGenerator
// Validate required fields
if gen.PrivateKey == "" {
return "", fmt.Errorf("private_key is required for JWT ES256 generation")
}
if gen.Header == nil {
return "", fmt.Errorf("header is required for JWT ES256 generation")
}
if gen.Payload == nil {
return "", fmt.Errorf("payload is required for JWT ES256 generation")
}
// Read private key
privateKey, err := p.loadPrivateKey(gen.PrivateKey)
if err != nil {
return "", fmt.Errorf("failed to load private key: %w", err)
}
// Parse expiration time (already normalized during config loading)
expiresIn := time.Hour * 24 * 90 // Default 90 days
if gen.ExpiresIn != "" {
duration, err := time.ParseDuration(gen.ExpiresIn)
if err != nil {
// This should not happen since it's normalized during config loading
log.Error("Failed to parse normalized expires_in '%s': %v", gen.ExpiresIn, err)
// Use default duration
} else {
expiresIn = duration
}
}
// Create JWT token
now := time.Now()
token := jwt.New(jwt.SigningMethodES256)
// Set header claims
for key, value := range gen.Header {
token.Header[key] = value
}
// Set payload claims
claims := token.Claims.(jwt.MapClaims)
for key, value := range gen.Payload {
claims[key] = value
}
// Set standard claims
claims["iat"] = now.Unix()
claims["exp"] = now.Add(expiresIn).Unix()
// Sign the token
tokenString, err := token.SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sign JWT: %w", err)
}
return tokenString, nil
}
// loadPrivateKey loads and parses the ES256 private key
func (p *Provider) loadPrivateKey(keyPath string) (*ecdsa.PrivateKey, error) {
var keyData []byte
var err error
// Check if keyPath is absolute or relative to openapi/certs
if filepath.IsAbs(keyPath) {
keyData, err = os.ReadFile(keyPath)
} else {
// Try relative to openapi/certs directory
certPath := filepath.Join("openapi", "certs", keyPath)
keyData, err = application.App.Read(certPath)
}
if err != nil {
log.Error("failed to read private key file: %v", err)
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
// Parse PEM block
block, _ := pem.Decode(keyData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
// Parse private key
switch block.Type {
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "PRIVATE KEY":
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
ecKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an ECDSA private key")
}
return ecKey, nil
default:
return nil, fmt.Errorf("unsupported private key type: %s", block.Type)
}
}
// generateBasicConcat generates client secret by concatenating client_id and other values
func (p *Provider) generateBasicConcat() (string, error) {
gen := p.ClientSecretGenerator
// Default pattern: client_id:timestamp
parts := []string{p.ClientID}
// Add custom parts from payload
if gen.Payload != nil {
for key, value := range gen.Payload {
if key == "separator" {
continue // Skip separator key
}
parts = append(parts, fmt.Sprintf("%v", value))
}
} else {
// Add timestamp if no custom payload
parts = append(parts, fmt.Sprintf("%d", time.Now().Unix()))
}
// Get separator from payload, default to ":"
separator := ":"
if gen.Payload != nil {
if sep, ok := gen.Payload["separator"].(string); ok {
separator = sep
}
}
return strings.Join(parts, separator), nil
}
// generateHMACSignature generates client secret using HMAC-SHA256 signature
func (p *Provider) generateHMACSignature() (string, error) {
gen := p.ClientSecretGenerator
// Get the secret key for HMAC
secretKey := ""
if gen.PrivateKey != "" {
secretKey = gen.PrivateKey
} else if gen.Payload != nil {
if key, ok := gen.Payload["secret_key"].(string); ok {
secretKey = key
}
}
if secretKey == "" {
return "", fmt.Errorf("secret_key is required for HMAC_SHA256 generation")
}
// Build message to sign
message := p.ClientID
if gen.Payload != nil {
if msg, ok := gen.Payload["message"].(string); ok {
message = msg
} else if msg, ok := gen.Payload["data"].(string); ok {
message = msg
}
}
// Add timestamp if configured
if gen.Payload != nil {
if addTimestamp, ok := gen.Payload["add_timestamp"].(bool); ok && addTimestamp {
message += fmt.Sprintf(":%d", time.Now().Unix())
}
}
// Create HMAC signature
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(message))
signature := h.Sum(nil)
// Return as hex or base64 based on configuration
encoding := "hex" // default
if gen.Payload != nil {
if enc, ok := gen.Payload["encoding"].(string); ok {
encoding = enc
}
}
switch encoding {
case "base64":
return base64.StdEncoding.EncodeToString(signature), nil
case "hex":
return hex.EncodeToString(signature), nil
default:
return hex.EncodeToString(signature), nil
}
}
// AccessToken gets the access token for the provider using OAuth 2.0 authorization code flow
func (p *Provider) AccessToken(code, redirectURI string) (*OAuthTokenResponse, error) {
if code == "" {
return nil, fmt.Errorf("authorization code is required")
}
// Get the access token endpoint
if p.Endpoints == nil {
return nil, fmt.Errorf("endpoints not found, set endpoints at least one")
}
if p.Endpoints.Token == "" {
return nil, fmt.Errorf("token endpoint not found, set token endpoint at least one")
}
// Get client secret (handles both ClientSecret and ClientSecretGenerator cases)
secret, err := p.GetClientSecret()
if err != nil {
return nil, fmt.Errorf("failed to get client secret: %w", err)
}
// Prepare the request parameters according to OAuth 2.0 spec
params := map[string]string{
"grant_type": "authorization_code",
"code": code,
"client_id": p.ClientID,
"client_secret": secret,
"redirect_uri": redirectURI,
}
// Create HTTP request using gou/http package (with DNS optimization)
req := http.New(p.Endpoints.Token).
SetHeader("Content-Type", "application/x-www-form-urlencoded").
SetHeader("Accept", "application/json").
SetHeader("User-Agent", "Yao-OAuth-Client/1.0")
// Make the POST request
resp := req.Post(params)
if resp == nil {
return nil, fmt.Errorf("failed to make token request: no response")
}
// Check for HTTP errors
if resp.Code != 200 {
if resp.Data != nil {
if data, ok := resp.Data.(map[string]interface{}); ok {
if err, ok := data["error_description"]; ok {
return nil, fmt.Errorf("%v", err)
}
if err, ok := data["error"]; ok {
return nil, fmt.Errorf("%v", err)
}
}
}
if resp.Message != "" {
return nil, fmt.Errorf("token request failed with status %d: %s", resp.Code, resp.Message)
}
return nil, fmt.Errorf("token request failed with status %d", resp.Code)
}
// Parse the JSON response
var tokenResponse OAuthTokenResponse
// Handle the response data - it could be already parsed JSON or raw bytes
switch data := resp.Data.(type) {
case map[string]interface{}:
// Already parsed JSON, convert to our struct
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
if err := json.Unmarshal(jsonBytes, &tokenResponse); err != nil {
return nil, fmt.Errorf("failed to parse token response from parsed JSON: %w", err)
}
case []byte:
// Raw bytes, parse as JSON
if err := json.Unmarshal(data, &tokenResponse); err != nil {
return nil, fmt.Errorf("failed to parse token response from bytes: %w", err)
}
case string:
// String response, parse as JSON
if err := json.Unmarshal([]byte(data), &tokenResponse); err != nil {
return nil, fmt.Errorf("failed to parse token response from string: %w", err)
}
default:
return nil, fmt.Errorf("unexpected response data type: %T", data)
}
// Check for OAuth error response
if tokenResponse.Error != "" {
errorMsg := tokenResponse.Error
if tokenResponse.ErrorDesc != "" {
errorMsg += ": " + tokenResponse.ErrorDesc
}
return nil, fmt.Errorf("OAuth error: %s", errorMsg)
}
// Validate that we got an access token
if tokenResponse.AccessToken == "" {
return nil, fmt.Errorf("no access token in response")
}
return &tokenResponse, nil
}
// GetProvider gets the provider by ID
func GetProvider(locale, providerID string) (*Provider, error) {
// Get the signin configuration
config := GetFullConfig(locale)
if config == nil {
return nil, fmt.Errorf("no signin configuration found")
}
// 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 {
return nil, fmt.Errorf("OAuth provider '%s' not found", providerID)
}
return provider, nil
}

View file

@ -6,8 +6,10 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/yao/config"
@ -25,96 +27,6 @@ var (
configMutex sync.RWMutex
)
// Config represents the signin page configuration
type Config struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
SuccessURL string `json:"success_url,omitempty"`
FailureURL string `json:"failure_url,omitempty"`
Form *FormConfig `json:"form,omitempty"`
Token *TokenConfig `json:"token,omitempty"`
ThirdParty *ThirdParty `json:"third_party,omitempty"`
}
// FormConfig represents the form configuration
type FormConfig struct {
Username *UsernameConfig `json:"username,omitempty"`
Password *PasswordConfig `json:"password,omitempty"`
Captcha *CaptchaConfig `json:"captcha,omitempty"`
ForgotPasswordLink bool `json:"forgot_password_link,omitempty"`
RememberMe bool `json:"remember_me,omitempty"`
RegisterLink string `json:"register_link,omitempty"`
TermsOfServiceLink string `json:"terms_of_service_link,omitempty"`
PrivacyPolicyLink string `json:"privacy_policy_link,omitempty"`
}
// UsernameConfig represents the username field configuration
type UsernameConfig struct {
Placeholder string `json:"placeholder,omitempty"`
Fields []string `json:"fields,omitempty"`
}
// PasswordConfig represents the password field configuration
type PasswordConfig struct {
Placeholder string `json:"placeholder,omitempty"`
}
// CaptchaConfig represents the captcha configuration
type CaptchaConfig struct {
Type string `json:"type,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
}
// TokenConfig represents the token configuration
type TokenConfig struct {
ExpiresIn string `json:"expires_in,omitempty"`
RememberMeExpiresIn string `json:"remember_me_expires_in,omitempty"`
}
// ThirdParty represents the third party login configuration
type ThirdParty struct {
Register *RegisterConfig `json:"register,omitempty"`
Providers []*Provider `json:"providers,omitempty"`
}
// RegisterConfig represents the auto register configuration
type RegisterConfig struct {
Auto bool `json:"auto,omitempty"`
Role string `json:"role,omitempty"`
}
// Provider represents a third party login provider
type Provider struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Logo string `json:"logo,omitempty"`
Color string `json:"color,omitempty"`
TextColor string `json:"text_color,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
ClientSecretGenerator *SecretGenerator `json:"client_secret_generator,omitempty"`
Scopes []string `json:"scopes,omitempty"`
ResponseMode string `json:"response_mode,omitempty"`
Endpoints *Endpoints `json:"endpoints,omitempty"`
Mapping map[string]string `json:"mapping,omitempty"`
}
// SecretGenerator represents the client secret generator configuration
type SecretGenerator struct {
Type string `json:"type,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
Header map[string]interface{} `json:"header,omitempty"`
Payload map[string]interface{} `json:"payload,omitempty"`
}
// Endpoints represents the OAuth endpoints
type Endpoints struct {
Authorization string `json:"authorization,omitempty"`
Token string `json:"token,omitempty"`
UserInfo string `json:"user_info,omitempty"`
}
// Load loads all signin configurations from the openapi directory
func Load(appConfig config.Config) error {
configMutex.Lock()
@ -275,6 +187,19 @@ func processENVVariables(config *Config, rootPath string) {
provider.ClientSecretGenerator.PrivateKey = filepath.Join(rootPath, "openapi", "certs", provider.ClientSecretGenerator.PrivateKey)
}
// Process and normalize expires_in format
if provider.ClientSecretGenerator.ExpiresIn != "" {
normalizedDuration, err := normalizeExpiresIn(provider.ClientSecretGenerator.ExpiresIn)
if err != nil {
log.Printf("Warning: Invalid expires_in format '%s' for provider '%s': %v",
provider.ClientSecretGenerator.ExpiresIn, provider.ID, err)
// Set default to 90 days
provider.ClientSecretGenerator.ExpiresIn = "2160h" // 90 * 24 hours
} else {
provider.ClientSecretGenerator.ExpiresIn = normalizedDuration
}
}
// Process header values
if provider.ClientSecretGenerator.Header != nil {
for key, value := range provider.ClientSecretGenerator.Header {
@ -526,3 +451,55 @@ func GetDefaultLanguage() string {
}
return defaultLang
}
// normalizeExpiresIn converts custom time units to Go standard duration format
func normalizeExpiresIn(expiresIn string) (string, error) {
if expiresIn == "" {
return "", nil
}
// Try parsing as standard Go duration first
if _, err := time.ParseDuration(expiresIn); err == nil {
return expiresIn, nil
}
// Custom unit conversion patterns
patterns := map[string]func(int) string{
"d": func(n int) string { return fmt.Sprintf("%dh", n*24) }, // days to hours
"w": func(n int) string { return fmt.Sprintf("%dh", n*24*7) }, // weeks to hours
"M": func(n int) string { return fmt.Sprintf("%dh", n*24*30) }, // months to hours (approximate)
"y": func(n int) string { return fmt.Sprintf("%dh", n*24*365) }, // years to hours (approximate)
"ms": func(n int) string { return fmt.Sprintf("%dms", n) }, // milliseconds
"s": func(n int) string { return fmt.Sprintf("%ds", n) }, // seconds
"m": func(n int) string { return fmt.Sprintf("%dm", n) }, // minutes
"h": func(n int) string { return fmt.Sprintf("%dh", n) }, // hours
}
// Extract number and unit using regex
re := regexp.MustCompile(`^(\d+)(\w+)$`)
matches := re.FindStringSubmatch(expiresIn)
if len(matches) != 3 {
return "", fmt.Errorf("invalid duration format: %s", expiresIn)
}
number, err := strconv.Atoi(matches[1])
if err != nil {
return "", fmt.Errorf("invalid number in duration: %s", matches[1])
}
unit := matches[2]
converter, exists := patterns[unit]
if !exists {
return "", fmt.Errorf("unsupported time unit: %s", unit)
}
normalized := converter(number)
// Validate the normalized duration
if _, err := time.ParseDuration(normalized); err != nil {
return "", fmt.Errorf("failed to create valid duration: %v", err)
}
return normalized, nil
}

144
openapi/signin/types.go Normal file
View file

@ -0,0 +1,144 @@
package signin
// Config represents the signin page configuration
type Config struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
SuccessURL string `json:"success_url,omitempty"`
FailureURL string `json:"failure_url,omitempty"`
Form *FormConfig `json:"form,omitempty"`
Token *TokenConfig `json:"token,omitempty"`
ThirdParty *ThirdParty `json:"third_party,omitempty"`
}
// FormConfig represents the form configuration
type FormConfig struct {
Username *UsernameConfig `json:"username,omitempty"`
Password *PasswordConfig `json:"password,omitempty"`
Captcha *CaptchaConfig `json:"captcha,omitempty"`
ForgotPasswordLink bool `json:"forgot_password_link,omitempty"`
RememberMe bool `json:"remember_me,omitempty"`
RegisterLink string `json:"register_link,omitempty"`
TermsOfServiceLink string `json:"terms_of_service_link,omitempty"`
PrivacyPolicyLink string `json:"privacy_policy_link,omitempty"`
}
// UsernameConfig represents the username field configuration
type UsernameConfig struct {
Placeholder string `json:"placeholder,omitempty"`
Fields []string `json:"fields,omitempty"`
}
// PasswordConfig represents the password field configuration
type PasswordConfig struct {
Placeholder string `json:"placeholder,omitempty"`
}
// CaptchaConfig represents the captcha configuration
type CaptchaConfig struct {
Type string `json:"type,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
}
// TokenConfig represents the token configuration
type TokenConfig struct {
ExpiresIn string `json:"expires_in,omitempty"`
RememberMeExpiresIn string `json:"remember_me_expires_in,omitempty"`
}
// ThirdParty represents the third party login configuration
type ThirdParty struct {
Register *RegisterConfig `json:"register,omitempty"`
Providers []*Provider `json:"providers,omitempty"`
}
// RegisterConfig represents the auto register configuration
type RegisterConfig struct {
Auto bool `json:"auto,omitempty"`
Role string `json:"role,omitempty"`
}
// Provider represents a third party login provider
type Provider struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Logo string `json:"logo,omitempty"`
Color string `json:"color,omitempty"`
TextColor string `json:"text_color,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
ClientSecretGenerator *SecretGenerator `json:"client_secret_generator,omitempty"`
Scopes []string `json:"scopes,omitempty"`
ResponseMode string `json:"response_mode,omitempty"`
Endpoints *Endpoints `json:"endpoints,omitempty"`
Mapping map[string]string `json:"mapping,omitempty"`
}
// SecretGenerator represents the client secret generator configuration
type SecretGenerator struct {
Type string `json:"type,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
Header map[string]interface{} `json:"header,omitempty"`
Payload map[string]interface{} `json:"payload,omitempty"`
}
// Endpoints represents the OAuth endpoints
type Endpoints struct {
Authorization string `json:"authorization,omitempty"`
Token string `json:"token,omitempty"`
UserInfo string `json:"user_info,omitempty"`
}
// ==== API Types ====
// OAuthAuthorizationURLResponse represents the response for OAuth authorization URL
type OAuthAuthorizationURLResponse struct {
AuthorizationURL string `json:"authorization_url"`
State string `json:"state"`
}
// OAuthCallbackResponse represents the response for OAuth callback
type OAuthCallbackResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
// OAuthAuthbackRequest represents the request for OAuth callback
type OAuthAuthbackRequest struct {
Locale string `json:"locale" form:"locale"`
Code string `json:"code" form:"code"`
State string `json:"state" form:"state"`
Provider string `json:"provider" form:"provider"`
Scope string `json:"scope,omitempty" form:"scope,omitempty"`
}
// OAuthTokenResponse represents the response from OAuth token endpoint
type OAuthTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
Error string `json:"error"`
ErrorDesc string `json:"error_description"`
}
// OAuthTokenRequest represents the request to OAuth token endpoint
type OAuthTokenRequest struct {
GrantType string `json:"grant_type" form:"grant_type"`
Code string `json:"code" form:"code"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
RedirectURI string `json:"redirect_uri,omitempty" form:"redirect_uri,omitempty"`
}
// OAuthUserInfoResponse represents the user information from OAuth provider
type OAuthUserInfoResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Username string `json:"username"`
}