forked from baron/baron-sso
Merge pull request 'dev/passwd-tab-signup' (#71) from dev/passwd-tab-signup into main
Reviewed-on: ai-team/baron-sso#71
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"general": {
|
|
||||||
"previewFeatures": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -749,44 +749,6 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
ale.Log(slog.LevelInfo, "Attempting to login")
|
ale.Log(slog.LevelInfo, "Attempting to login")
|
||||||
|
|
||||||
// Validate password complexity before sending to Descope
|
|
||||||
password := req.Password
|
|
||||||
if len(password) < 8 {
|
|
||||||
ale.Status = fiber.StatusBadRequest
|
|
||||||
ale.LatencyMs = time.Since(startTime)
|
|
||||||
ale.DescopeError = "Password must be at least 8 characters long"
|
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: password too short")
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must be at least 8 characters long"})
|
|
||||||
}
|
|
||||||
if ok, _ := regexp.MatchString(`[a-z]`, password); !ok {
|
|
||||||
ale.Status = fiber.StatusBadRequest
|
|
||||||
ale.LatencyMs = time.Since(startTime)
|
|
||||||
ale.DescopeError = "Password must contain at least one lowercase letter"
|
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no lowercase letter")
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one lowercase letter"})
|
|
||||||
}
|
|
||||||
if ok, _ := regexp.MatchString(`[A-Z]`, password); !ok {
|
|
||||||
ale.Status = fiber.StatusBadRequest
|
|
||||||
ale.LatencyMs = time.Since(startTime)
|
|
||||||
ale.DescopeError = "Password must contain at least one uppercase letter"
|
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no uppercase letter")
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one uppercase letter"})
|
|
||||||
}
|
|
||||||
if ok, _ := regexp.MatchString(`[0-9]`, password); !ok {
|
|
||||||
ale.Status = fiber.StatusBadRequest
|
|
||||||
ale.LatencyMs = time.Since(startTime)
|
|
||||||
ale.DescopeError = "Password must contain at least one number"
|
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no number")
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one number"})
|
|
||||||
}
|
|
||||||
if ok, _ := regexp.MatchString(`[\W_]`, password); !ok {
|
|
||||||
ale.Status = fiber.StatusBadRequest
|
|
||||||
ale.LatencyMs = time.Since(startTime)
|
|
||||||
ale.DescopeError = "Password must contain at least one special character"
|
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no special character")
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one special character"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.DescopeClient == nil {
|
if h.DescopeClient == nil {
|
||||||
ale.Status = fiber.StatusInternalServerError
|
ale.Status = fiber.StatusInternalServerError
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.LatencyMs = time.Since(startTime)
|
||||||
@@ -1076,41 +1038,55 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
|||||||
// 디버깅을 위해 요청된 새 비밀번호를 로그로 출력
|
// 디버깅을 위해 요청된 새 비밀번호를 로그로 출력
|
||||||
ale.Log(slog.LevelInfo, "Received new password for reset")
|
ale.Log(slog.LevelInfo, "Received new password for reset")
|
||||||
|
|
||||||
// Validate password complexity
|
// Validate password complexity dynamically based on Descope policy
|
||||||
if len(req.NewPassword) < 8 {
|
policy, err := h.DescopeClient.Auth.Password().GetPasswordPolicy(context.Background())
|
||||||
ale.Status = fiber.StatusBadRequest
|
if err != nil {
|
||||||
ale.LatencyMs = time.Since(startTime)
|
// If policy fetch fails, log warning and proceed (or fallback to basic check)
|
||||||
ale.DescopeError = "Password must be at least 8 characters long"
|
ale.Log(slog.LevelWarn, "Failed to fetch password policy, skipping dynamic validation: "+err.Error())
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: password too short")
|
} else {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must be at least 8 characters long"})
|
if len(req.NewPassword) < int(policy.MinLength) {
|
||||||
}
|
ale.Status = fiber.StatusBadRequest
|
||||||
if ok, _ := regexp.MatchString(`[a-z]`, req.NewPassword); !ok {
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.DescopeError = fmt.Sprintf("Password must be at least %d characters long", policy.MinLength)
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.Log(slog.LevelWarn, "Validation failed: password too short")
|
||||||
ale.DescopeError = "Password must contain at least one lowercase letter"
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": ale.DescopeError})
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no lowercase letter")
|
}
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one lowercase letter"})
|
if policy.Lowercase {
|
||||||
}
|
if ok, _ := regexp.MatchString(`[a-z]`, req.NewPassword); !ok {
|
||||||
if ok, _ := regexp.MatchString(`[A-Z]`, req.NewPassword); !ok {
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.Status = fiber.StatusBadRequest
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.DescopeError = "Password must contain at least one lowercase letter"
|
||||||
ale.DescopeError = "Password must contain at least one uppercase letter"
|
ale.Log(slog.LevelWarn, "Validation failed: no lowercase letter")
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no uppercase letter")
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one lowercase letter"})
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one uppercase letter"})
|
}
|
||||||
}
|
}
|
||||||
if ok, _ := regexp.MatchString(`[0-9]`, req.NewPassword); !ok {
|
if policy.Uppercase {
|
||||||
ale.Status = fiber.StatusBadRequest
|
if ok, _ := regexp.MatchString(`[A-Z]`, req.NewPassword); !ok {
|
||||||
ale.LatencyMs = time.Since(startTime)
|
ale.Status = fiber.StatusBadRequest
|
||||||
ale.DescopeError = "Password must contain at least one number"
|
ale.LatencyMs = time.Since(startTime)
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no number")
|
ale.DescopeError = "Password must contain at least one uppercase letter"
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one number"})
|
ale.Log(slog.LevelWarn, "Validation failed: no uppercase letter")
|
||||||
}
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one uppercase letter"})
|
||||||
if ok, _ := regexp.MatchString(`[\W_]`, req.NewPassword); !ok {
|
}
|
||||||
ale.Status = fiber.StatusBadRequest
|
}
|
||||||
ale.LatencyMs = time.Since(startTime)
|
if policy.Number {
|
||||||
ale.DescopeError = "Password must contain at least one special character"
|
if ok, _ := regexp.MatchString(`[0-9]`, req.NewPassword); !ok {
|
||||||
ale.Log(slog.LevelWarn, "Validation failed: no special character")
|
ale.Status = fiber.StatusBadRequest
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one special character"})
|
ale.LatencyMs = time.Since(startTime)
|
||||||
|
ale.DescopeError = "Password must contain at least one number"
|
||||||
|
ale.Log(slog.LevelWarn, "Validation failed: no number")
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one number"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if policy.NonAlphanumeric {
|
||||||
|
if ok, _ := regexp.MatchString(`[\W_]`, req.NewPassword); !ok {
|
||||||
|
ale.Status = fiber.StatusBadRequest
|
||||||
|
ale.LatencyMs = time.Since(startTime)
|
||||||
|
ale.DescopeError = "Password must contain at least one special character"
|
||||||
|
ale.Log(slog.LevelWarn, "Validation failed: no special character")
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one special character"})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ale.Log(slog.LevelInfo, "Attempting to update password via Descope Auth API")
|
ale.Log(slog.LevelInfo, "Attempting to update password via Descope Auth API")
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
bool _termsAccepted = false;
|
bool _termsAccepted = false;
|
||||||
bool _privacyAccepted = false;
|
bool _privacyAccepted = false;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
Map<String, dynamic>? _policy;
|
||||||
|
bool _isPolicyLoading = false;
|
||||||
|
|
||||||
// Inline Errors
|
// Inline Errors
|
||||||
String? _emailError;
|
String? _emailError;
|
||||||
@@ -58,6 +60,24 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
'baroncs.co.kr': 'BARON',
|
'baroncs.co.kr': 'BARON',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPolicy() async {
|
||||||
|
setState(() => _isPolicyLoading = true);
|
||||||
|
try {
|
||||||
|
final policy = await AuthProxyService.fetchPasswordPolicy();
|
||||||
|
if (mounted) setState(() => _policy = policy);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore errors, will use defaults
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isPolicyLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_emailTimer?.cancel();
|
_emailTimer?.cancel();
|
||||||
@@ -757,13 +777,40 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _buildPolicyDescription() {
|
||||||
|
if (_isPolicyLoading) {
|
||||||
|
return "비밀번호 정책을 불러오는 중입니다...";
|
||||||
|
}
|
||||||
|
final minLength = (_policy?['minLength'] as int?) ?? 8;
|
||||||
|
final requiresLower = _policy?['lowercase'] ?? true;
|
||||||
|
final requiresUpper = _policy?['uppercase'] ?? true;
|
||||||
|
final requiresNumber = _policy?['number'] ?? true;
|
||||||
|
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||||
|
|
||||||
|
final parts = <String>["최소 $minLength자 이상"];
|
||||||
|
if (requiresUpper) parts.add("대문자");
|
||||||
|
if (requiresLower) parts.add("소문자");
|
||||||
|
if (requiresNumber) parts.add("숫자");
|
||||||
|
if (requiresSymbol) parts.add("특수문자");
|
||||||
|
|
||||||
|
return "보안 정책: ${parts.join(', ')}를 각각 최소 1자 이상 포함해야 합니다.";
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStepPassword() {
|
Widget _buildStepPassword() {
|
||||||
String p = _passwordController.text;
|
String p = _passwordController.text;
|
||||||
bool hasUpper = p.contains(RegExp(r'[A-Z]'));
|
|
||||||
bool hasLower = p.contains(RegExp(r'[a-z]'));
|
// Default Policy Fallback
|
||||||
bool hasDigit = p.contains(RegExp(r'[0-9]'));
|
final minLength = (_policy?['minLength'] as int?) ?? 8;
|
||||||
bool hasSpecial = p.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
|
final requiresLower = _policy?['lowercase'] ?? true;
|
||||||
bool hasLength = p.length >= 12;
|
final requiresUpper = _policy?['uppercase'] ?? true;
|
||||||
|
final requiresNumber = _policy?['number'] ?? true;
|
||||||
|
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||||
|
|
||||||
|
bool hasLength = p.length >= minLength;
|
||||||
|
bool hasUpper = !requiresUpper || p.contains(RegExp(r'[A-Z]'));
|
||||||
|
bool hasLower = !requiresLower || p.contains(RegExp(r'[a-z]'));
|
||||||
|
bool hasDigit = !requiresNumber || p.contains(RegExp(r'[0-9]'));
|
||||||
|
bool hasSpecial = !requiresSymbol || p.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -780,7 +827,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'보안 정책: 12자 이상, 대문자/소문자/숫자/특수문자를 각각 최소 1자 이상 포함해야 합니다.',
|
_buildPolicyDescription(),
|
||||||
style: TextStyle(fontSize: 12, color: Colors.blue[800], fontWeight: FontWeight.w500),
|
style: TextStyle(fontSize: 12, color: Colors.blue[800], fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -802,11 +849,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
children: [
|
children: [
|
||||||
_cryptoCheck('12자 이상', hasLength),
|
_cryptoCheck('$minLength자 이상', hasLength),
|
||||||
_cryptoCheck('대문자', hasUpper),
|
if (requiresUpper) _cryptoCheck('대문자', hasUpper),
|
||||||
_cryptoCheck('소문자', hasLower),
|
if (requiresLower) _cryptoCheck('소문자', hasLower),
|
||||||
_cryptoCheck('숫자', hasDigit),
|
if (requiresNumber) _cryptoCheck('숫자', hasDigit),
|
||||||
_cryptoCheck('특수문자', hasSpecial),
|
if (requiresSymbol) _cryptoCheck('특수문자', hasSpecial),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
Reference in New Issue
Block a user