diff --git a/openapi/signin/api.go b/openapi/signin/api.go index c7de9b5b..f5d828bd 100644 --- a/openapi/signin/api.go +++ b/openapi/signin/api.go @@ -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)) } diff --git a/openapi/signin/provider.go b/openapi/signin/provider.go new file mode 100644 index 00000000..f7d4a35e --- /dev/null +++ b/openapi/signin/provider.go @@ -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 +} diff --git a/openapi/signin/signin.go b/openapi/signin/signin.go index 617f5e82..504847b3 100644 --- a/openapi/signin/signin.go +++ b/openapi/signin/signin.go @@ -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 +} diff --git a/openapi/signin/types.go b/openapi/signin/types.go new file mode 100644 index 00000000..3c6ff4ab --- /dev/null +++ b/openapi/signin/types.go @@ -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"` +}