February 22, 2025

Keycloak SSO Google Login Triggering Multiple Events for Different Clients

Introduction

In a production environment, a peculiar issue was observed where a user logging into one client application via Google SSO in Keycloak also triggered a login event for another client. This behavior was unexpected and could not be reproduced in a local development environment. This blog post explores possible causes, how to reproduce the scenario, root cause analysis, potential solutions, and additional resources for further reading.

Understanding the Issue

Observed Behavior

  • A user attempts to log in to Client A via Google SSO.
  • The login event is recorded for Client A as expected.
  • However, another login event is also recorded for Client B, even though the user did not explicitly attempt to log into it.
  • This issue does not occur consistently and is difficult to reproduce locally.

Possible Causes

  1. SSO Session Sharing Across Clients

    • If both clients (A and B) are configured within the same realm in Keycloak and have SSO enabled, logging into one client might automatically establish a session for the other.
    • Read more
  2. Misconfigured Authentication Flow

    • Certain configurations in Keycloak (e.g., implicit flow, forced re-authentication) could lead to multiple login events.
    • Keycloak Authentication Flows
  3. Redirect URIs and Post-Login Flow Issues

    • If Client B has a similar redirect URI or shares authentication flow parameters with Client A, it may also receive an authentication response.
    • OAuth Redirect URI Best Practices
  4. Cached or Persistent Sessions in the Browser

  5. Automatic Session Propagation

    • If session propagation is not explicitly disabled, Keycloak may attempt to log the user into multiple clients within the same realm automatically.
    • Disable Automatic Session Propagation
  6. Custom Login Implementation Issues

Steps to Reproduce

Prerequisites

  • Keycloak set up with two clients (Client A and Client B) within the same realm.
  • Google SSO configured as an Identity Provider.
  • Custom REST API for login via SSO.

Reproduction Steps

Step 1: Set Up Two Clients in Keycloak

  • Configure Client A and Client B to use Keycloak for authentication.
  • Ensure both clients are in the same realm.
  • Enable Standard Flow and Direct Access Grants in the client settings.

Step 2: Enable SSO for Both Clients

  • In Keycloak, navigate to Realm Settings → Login and enable SSO session sharing.
  • Set SSO Session Max Age to a high value to allow multiple logins within the same session.

Step 3: Implement Custom Login via REST API

  • Create a custom API that calls Keycloak’s token endpoint using the authorization code from Google SSO.
  • Ensure that both Client A and Client B share the same authentication flow.
  • Execute the API call for Client A.

Step 4: Simulate Concurrent Login Requests

  • Use a script or Postman to send multiple login requests to the custom API for different clients.
  • Ensure that the user session is already established for Client A.

Step 5: Check Keycloak Events

  • In Keycloak Admin Console, go to Events → Login Events.
  • Verify if an additional login event appears for Client B.

Root Cause Analysis (RCA)

  1. SSO Session Reuse Across Clients

    • Keycloak maintains a centralized session for a user across all clients in the same realm.
    • If a user logs into one client, Keycloak may automatically propagate the session to another client.
    • Keycloak Session Management
  2. Misconfigured Redirect URIs and Authentication Flows

  3. Custom REST API Handling Issues

Possible Solutions

1. Disable Automatic Session Propagation

  • Navigate to Realm Settings → SSO Session Max Age.
  • Adjust session parameters to restrict automatic logins.
  • Disable Full Scope Allowed for each client in Client Settings → Scope.
  • Set SameSite=None; Secure for authentication cookies to prevent unintended cross-client logins.
  • More on Keycloak Session Management

2. Validate Redirect URIs

  • Ensure each client has unique and correctly configured redirect URIs to prevent unintended authentication responses.
  • Navigate to Client Settings → Valid Redirect URIs.
  • Best Practices for Redirect URIs

3. Use Client-Specific Authentication Flows

4. Check Google SSO Provider Configuration

  • Ensure that Google SSO is not configured to redirect users to multiple clients inadvertently.
  • Validate that the post-login redirect URL in Google’s OAuth settings is pointing to the intended client only.
  • Configuring Google OAuth

5. Modify Custom REST API Login Implementation

  • Ensure that each login request is specific to a single client by validating client_id.
  • Modify session handling to prevent unintended reuse across clients.
  • Implement token validation before issuing new access tokens.
  • Keycloak REST API Guide

Conclusion

This issue likely arises due to session sharing, misconfigured authentication flows, or incorrect redirect URIs. By isolating login sessions per client and fine-tuning Keycloak settings, unintended login events can be prevented. If you’re facing this in production, analyze login events in Keycloak to trace the issue further and apply the solutions outlined above.


Let me know if you've encountered similar issues or found alternative solutions!

February 21, 2025

Fixing "WaitTimeSeconds is Invalid" Error in Amazon SQS

Introduction

Amazon Simple Queue Service (SQS) is like a postbox for messages in the cloud. It helps different parts of a system talk to each other by sending and receiving messages safely. However, sometimes, you might see an error like this:

"pool-12-thread-1" software.amazon.awssdk.services.sqs.model.SqsException: Value 120 for parameter WaitTimeSeconds is invalid. Reason: Must be >= 0 and <= 20, if provided. (Service: Sqs, Status Code: 400, Request ID: 6778dcd6-b3e8-59ca-a981-bce42253312d)

This error happens when the WaitTimeSeconds setting is greater than 20, which is not allowed. Let's break it down so even a child can understand and see how to fix it.


Understanding the Error

What is WaitTimeSeconds?

Think of WaitTimeSeconds like waiting at a bus stop. If you set it to 0, it's like checking for a bus and leaving immediately if there isn’t one (short polling). If you set it to a number between 1 and 20, it means you wait for some time before deciding no bus is coming (long polling). But if you try to wait more than 20 seconds, SQS says, “That’s too long!” and throws an error.

Why Does This Happen?

If the system tries to wait longer than 20 seconds, Amazon SQS stops it because that’s the maximum waiting time allowed per request.

Common reasons include:

  • Wrong settings in your application code.
  • Misunderstanding of polling rules (thinking you can wait longer than allowed).
  • Forgetting to set a valid value and using a default that is too high.

When and How to Use WaitTimeSeconds

When Should You Use It?

  • If you want to check for new messages quickly, set WaitTimeSeconds = 0 (short polling).
  • If you want to reduce unnecessary requests and save money, set WaitTimeSeconds between 1-20 (long polling).
  • If your system does not need real-time responses, use the maximum 20 seconds to lower API costs and reduce load on your application.

How to Use It Correctly

Example in AWS SDK for Java (v2):

import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest;

ReceiveMessageRequest receiveMessageRequest = ReceiveMessageRequest.builder()
    .queueUrl(queueUrl)
    .waitTimeSeconds(20) // ✅ Must be between 0 and 20
    .maxNumberOfMessages(10)
    .build();

Recommended Settings for Best Performance

Scenario Recommended WaitTimeSeconds Why?
Real-time applications 0-5 Faster response but may increase API calls.
Standard polling (normal) 10-15 Balanced approach for efficiency.
Batch processing (not urgent) 20 Reduces cost by minimizing API calls.

Fixing the Issue

1. Use a Valid WaitTimeSeconds Value

Make sure it’s between 0 and 20 to avoid errors.

2. Loop If You Need a Longer Delay

If you need more than 20 seconds, use a loop instead of setting an invalid value.

while (true) {
    ReceiveMessageRequest receiveMessageRequest = ReceiveMessageRequest.builder()
        .queueUrl(queueUrl)
        .waitTimeSeconds(20) // Maximum allowed value
        .maxNumberOfMessages(10)
        .build();

    sqsClient.receiveMessage(receiveMessageRequest);
}

3. Set Default Long Polling in Queue Settings

If you want all consumers to wait for a specific time, set the Receive Message Wait Time in the queue settings (Amazon SQS Console → Queue → Edit → Receive Message Wait Time).


Best Practices for Using Long Polling in SQS

  • Use long polling (WaitTimeSeconds > 0) to reduce API requests and costs.
  • Set WaitTimeSeconds to 20 for best efficiency and fewer requests.
  • Use batch processing (maxNumberOfMessages > 1) to optimize performance.
  • Monitor with AWS CloudWatch to track queue performance and adjust settings.
  • Implement retries with exponential backoff to handle failures properly.

Common Problems and Solutions

1. High API Costs Due to Short Polling

  • If WaitTimeSeconds=0, you’ll make too many API requests.
  • Solution: Set WaitTimeSeconds to at least 10-20 seconds.

2. Messages Take Too Long to Appear

  • If messages don’t appear, another system might be processing them due to the visibility timeout setting.
  • Solution: Check and adjust visibility timeout if necessary.

3. System Stops Receiving Messages Suddenly

  • If your app isn’t receiving messages, check if it’s not processing them fast enough.
  • Solution: Increase maxNumberOfMessages and ensure enough workers are running.

Further Reading


Conclusion

The "WaitTimeSeconds is invalid" error in Amazon SQS happens when you set an invalid value above 20. To fix it:

  1. Set WaitTimeSeconds between 0-20.
  2. Use a loop if you need longer delays.
  3. Configure queue settings for default long polling.
  4. Follow best practices for cost and performance efficiency.

By following these recommendations, you can use SQS more effectively, reduce API costs, and avoid common pitfalls. Happy coding! 🚀

February 18, 2025

Custom Annotation in Spring Boot: Restricting Age Below 18

 When making apps, sometimes we need to stop kids under 18 from signing up. Instead of writing the same rule everywhere, we can make a special tag (annotation) to check age easily. Let's learn how to do it step by step!

Why Use Custom Annotations?

Spring Boot has built-in checks like @NotNull (not empty) and @Size (length), but not for age. Instead of writing the same age-checking code again and again, we create a custom annotation that we can use anywhere in the app.

Steps to Create an Age Validator

Step 1: Create the Annotation

This is like making a new sticker that says "Check Age" which we can put on our data fields.

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = AgeValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MinAge {
    int value() default 18;
    String message() default "You must be at least {value} years old";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Understanding the Annotations Used

  • @Constraint(validatedBy = AgeValidator.class): Links the annotation to the AgeValidator class, which contains the logic to validate the age.
  • @Target({ElementType.FIELD, ElementType.PARAMETER}): Specifies where we can use this annotation. Options include:
    • ElementType.FIELD: Can be applied to fields in a class.
    • ElementType.PARAMETER: Can be used on method parameters.
    • Other options: METHOD, TYPE, ANNOTATION_TYPE, etc.
  • @Retention(RetentionPolicy.RUNTIME): Defines when the annotation is available. Options include:
    • RetentionPolicy.RUNTIME: The annotation is accessible during runtime (needed for validation).
    • RetentionPolicy.CLASS: Available in the class file but not at runtime.
    • RetentionPolicy.SOURCE: Only used in source code and discarded by the compiler.

Step 2: Write the Age Checking Logic

This part calculates the age and tells if it's 18 or more.

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.Period;

public class AgeValidator implements ConstraintValidator<MinAge, LocalDate> {
    private int minAge;

    @Override
    public void initialize(MinAge constraintAnnotation) {
        this.minAge = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(LocalDate dob, ConstraintValidatorContext context) {
        if (dob == null) {
            return false; // No date means invalid
        }
        return Period.between(dob, LocalDate.now()).getYears() >= minAge;
    }
}

Step 3: Use the Annotation in a User Data Class

Now, we use @MinAge to check age whenever someone signs up.

import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;

public class UserDTO {
    @NotNull(message = "Please enter your birthdate")
    @MinAge(18)
    private LocalDate dateOfBirth;

    // Getters and Setters
    public LocalDate getDateOfBirth() {
        return dateOfBirth;
    }

    public void setDateOfBirth(LocalDate dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }
}

Step 4: Apply Validation in a Controller

When a new user signs up, we check their age automatically.

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {
    @PostMapping("/register")
    public ResponseEntity<String> registerUser(@RequestBody @Valid UserDTO userDTO) {
        return ResponseEntity.ok("User registered successfully");
    }
}

Step 5: Test the Validation

If someone younger than 18 tries to sign up, they will see this message:

{
  "dateOfBirth": "You must be at least 18 years old"
}

Making Sure Name is Lowercase

Sometimes, we want names to be stored in lowercase automatically. There are two ways to do this:

Option 1: Use @ColumnTransformer (Hibernate)

If using Hibernate, we can transform the value before saving.

import org.hibernate.annotations.ColumnTransformer;

@Entity
public class User {
    @ColumnTransformer(write = "lower(?)")
    private String name;
}

Option 2: Custom Annotation for Lowercase

If we want to ensure lowercase format, we can create a custom annotation.

Step 1: Create the Annotation

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = LowercaseValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lowercase {
    String message() default "Must be lowercase";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Step 2: Create the Validator

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class LowercaseValidator implements ConstraintValidator<Lowercase, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && value.equals(value.toLowerCase());
    }
}

Step 3: Use the Annotation

public class UserDTO {
    @Lowercase
    private String name;
}

Recommended Approach

  • If using Hibernate, @ColumnTransformer(write = "lower(?)") is simple and works well.
  • If working with validations, a custom @Lowercase annotation ensures that input is already correct.
  • A hybrid approach: Apply both for best consistency.

Conclusion

By making a custom @MinAge annotation, we ensure kids under 18 cannot register. Similarly, using a @Lowercase annotation or Hibernate’s built-in transformation helps maintain data consistency. These techniques keep our code clean, reusable, and easy to maintain.

Hope this helps! Happy coding! 🚀