Skip to content

Session Tags

Not all users need the same session settings. Your root users need stricter security than regular users. Enterprise customers might require shorter session timeouts for compliance. Some organizations want to restrict access to their office IP addresses.

Without session tags, you’d need complex conditional logic scattered throughout your codebase. With session tags, you define the rules once and PropelAuth BYO handles the rest automatically.

Session tags are labels you attach to sessions when they’re created. Each tag automatically applies specific session settings, overriding your defaults. You define the rules once in your configuration, then apply them by adding tags when creating sessions.

A tag has two parts: type and value, separated by a colon:

  • role:root - Tag type is “role”, value is “root”
  • org:acme-corp - Tag type is “org”, value is “acme-corp”
  • login_type:sso - Tag type is “login_type”, value is “sso”

When you create a session, you can add any combination of tags:

// Root user from Acme Corp gets both root rules AND org-specific rules
const session = await client.session.create({
userId: userId,
tags: ["role:root", "org:acme-corp"],
});
# Root user from Acme Corp gets both root rules AND org-specific rules
session = await client.session.create(
user_id=user_id,
tags=["role:root", "org:acme-corp"]
)
// Root user from Acme Corp gets both root rules AND org-specific rules
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.tags(List.of("role:root", "org:acme-corp"))
.build()
);
// Root user from Acme Corp gets both root rules AND org-specific rules
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId,
Tags = new List<string> { "role:root", "org:acme-corp" }
});

Let’s start with a common scenario: different session rules for root accounts vs regular users.

In your session_config.jsonc file, define the rules for each tag:

/configs/session_config.jsonc
{
"defaults": {
"absolute_lifetime_secs": 1209600, // 14 days
"max_concurrent_sessions_per_user": 10
},
"tags": [{
"tag": "role:root",
"absolute_lifetime_secs": 14400, // 4 hours
"inactivity_timeout_secs": 900, // 15 minutes
"disallow_ip_address_changes": true
}]
}

When a user logs in, determine which tags apply and include them when creating the session:

app.post("/api/login", async (req, res) => {
const user = await authenticateUser(req.body);
// Determine if this is a root account
const tags = user.isRootAccount ? ["role:root"] : [];
// Create session with appropriate tags
const session = await auth.session.create({
userId: user.id,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
tags: tags,
});
if (!session.ok) {
return res.status(500).json({ error: "Failed to create session" });
}
res.cookie("session", session.data.sessionToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
});
res.json({ success: true });
});
@app.post("/api/login")
async def login(request: LoginRequest, response: Response):
user = await authenticate_user(request)
# Determine if this is a root account
tags = ["role:root"] if user.is_root_account else []
# Create session with appropriate tags
session = await client.session.create(
user_id=user.id,
ip_address=request.client.host,
user_agent=request.headers.get("user-agent"),
tags=tags
)
if is_err(session):
raise HTTPException(status_code=500, detail="Failed to create session")
response.set_cookie(
key="session",
value=session.data.session_token,
httponly=True,
secure=True,
samesite="lax"
)
return {"success": True}
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest, HttpServletResponse response) {
User user = authenticateUser(request);
// Determine if this is a root account
List<String> tags = user.isRootAccount() ? List.of("role:root") : List.of();
// Create session with appropriate tags
CreateSessionResponse session;
try {
session = client.session.create(
CreateSessionCommand.builder()
.userId(user.getId())
.ipAddress(httpRequest.getRemoteAddr())
.userAgent(httpRequest.getHeader("User-Agent"))
.tags(tags)
.build()
);
} catch (CreateSessionException e) {
return ResponseEntity.status(500).body(Map.of("error", "Failed to create session"));
}
Cookie cookie = new Cookie("session", session.getSessionToken());
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setSameSite("Lax");
response.addCookie(cookie);
return ResponseEntity.ok(Map.of("success", true));
}
[HttpPost("/api/login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await AuthenticateUser(request);
// Determine if this is a root account
var tags = user.IsRootAccount ? new List<string> { "role:root" } : new List<string>();
// Create session with appropriate tags
CreateSessionResponse session;
try
{
session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = user.Id,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = Request.Headers["User-Agent"].ToString(),
Tags = tags
});
}
catch (CreateSessionException)
{
return StatusCode(500, new { Error = "Failed to create session" });
}
Response.Cookies.Append("session", session.SessionToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax
});
return Ok(new { Success = true });
}

That’s it! Root account sessions will now automatically:

  • Expire after 4 hours (instead of 14 days)
  • Time out after 15 minutes of inactivity
  • Get invalidated if the IP address changes

Regular users get the default settings - no extra code needed.

Different organizations often have different security requirements. Session tags make this easy:

// Tag sessions based on the user's organization
const session = await client.session.create({
userId: user.id,
tags: [`org:${user.organizationId}`],
ipAddress: req.ip,
userAgent: req.headers["user-agent"]
});
# Tag sessions based on the user's organization
session = await client.session.create(
user_id=user.id,
tags=[f"org:{user.organization_id}"],
ip_address=request.client.host,
user_agent=request.headers.get("user-agent")
)
// Tag sessions based on the user's organization
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(user.getId())
.tags(List.of("org:" + user.getOrganizationId()))
.ipAddress(request.getRemoteAddr())
.userAgent(request.getHeader("User-Agent"))
.build()
);
// Tag sessions based on the user's organization
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = user.Id,
Tags = new List<string> { $"org:{user.OrganizationId}" },
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = Request.Headers["User-Agent"].ToString()
});

Your configuration might include specific rules for each organization:

{
"tags": [
{
"tag": "org:acme-corp",
"absolute_lifetime_secs": 28800, // 8 hours
"ip_allowlist": ["203.0.113.0/24"], // Their office network
"max_concurrent_sessions_per_user": 1
},
{
"tag": "org:globex-inc",
"absolute_lifetime_secs": 86400, // 24 hours
"max_concurrent_sessions_per_user": 3
}
]
}

Different authentication methods might need different session behaviors:

{
"tags": [
{
"tag": "login_type:password",
"absolute_lifetime_secs": 86400, // 1 day
"inactivity_timeout_secs": 3600 // 1 hour
},
{
"tag": "login_type:sso",
"absolute_lifetime_secs": 43200, // 12 hours
"inactivity_timeout_secs": 1800 // 30 minutes
},
{
"tag": "login_type:passkey",
"absolute_lifetime_secs": 2592000 // 30 days - more secure method
}
]
}

Tags are perfect for temporary access patterns:

// Grant temporary elevated access
const session = await client.session.create({
userId: userId,
tags: ["access:elevated"],
});
// Trial user access
const session = await client.session.create({
userId: guestId,
tags: ["access:trial"],
});
# Grant temporary elevated access
session = await client.session.create(
user_id=user_id,
tags=["access:elevated"]
)
# Trial user access
session = await client.session.create(
user_id=guest_id,
tags=["access:trial"]
)
// Grant temporary elevated access
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.tags(List.of("access:elevated"))
.build()
);
// Trial user access
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(guestId)
.tags(List.of("access:trial"))
.build()
);
// Grant temporary elevated access
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId,
Tags = new List<string> { "access:elevated" }
});
// Trial user access
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = guestId,
Tags = new List<string> { "access:trial" }
});

Sessions can have multiple tags, and their settings merge together. This lets you compose complex behaviors from simple rules:

// A root user from Acme Corp gets both root rules AND org-specific rules
const session = await auth.session.create({
userId: userId,
tags: ["role:root", "org:acme-corp"]
});
// This session will have:
// - 4 hour expiration (from role:root)
// - IP restrictions if Acme Corp configured them
// - Both tags' settings merged together
# A root user from Acme Corp gets both root rules AND org-specific rules
session = await client.session.create(
user_id=user_id,
tags=["role:root", "org:acme-corp"]
)
# This session will have:
# - 4 hour expiration (from role:root)
# - IP restrictions if Acme Corp configured them
# - Both tags' settings merged together
// A root user from Acme Corp gets both root rules AND org-specific rules
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.tags(List.of("role:root", "org:acme-corp"))
.build()
);
// This session will have:
// - 4 hour expiration (from role:root)
// - IP restrictions if Acme Corp configured them
// - Both tags' settings merged together
// A root user from Acme Corp gets both root rules AND org-specific rules
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId,
Tags = new List<string> { "role:root", "org:acme-corp" }
});
// This session will have:
// - 4 hour expiration (from role:root)
// - IP restrictions if Acme Corp configured them
// - Both tags' settings merged together

When tags have conflicting settings, PropelAuth BYO uses a priority system to resolve them.

When multiple tags define the same setting, the tag with higher priority wins. You control the priority order in your configuration:

Tag priority configuration
{
"defaults": {
"absolute_lifetime_secs": 1209600 // 14 days
},
"tags": [{
"tag": "org:acme-corp",
"absolute_lifetime_secs": 28800, // 8 hours
"ip_allowlist": ["203.0.113.0/24"] // Their office network
}, {
"tag": "org:globex-inc",
"absolute_lifetime_secs": 86400, // 24 hours
"max_concurrent_sessions_per_user": 3
}, {
"tag": "role:root",
"absolute_lifetime_secs": 14400, // 4 hours
"inactivity_timeout_secs": 900, // 15 minutes
"disallow_ip_address_changes": true
}, {
"tag": "login_type:sso",
"absolute_lifetime_secs": 43200, // 12 hours
"inactivity_timeout_secs": 3600 // 1 hour
}, {
"tag": "login_type:passkey",
"absolute_lifetime_secs": 2592000 // 30 days
}],
// Organization settings override role settings
"tag_priority": ["org", "role", "login_type"]
}

In this example:

  1. Organization tags (like org:acme) have highest priority
  2. Role tags (like role:root) come next
  3. Login type tags (like login_type:sso) have lowest priority
  4. Default settings apply if no tags match

This means if Acme Corp sets a 4-hour session limit, it overrides even root account sessions.

When you validate a session, you can optionally verify it has specific tags:

// Validate that a session exists and has specific tags
const validation = await client.session.validate({
sessionToken: req.cookies.session,
requiredTags: ["role:root"],
ipAddress: req.ip,
userAgent: req.headers["user-agent"]
});
if (!validation.ok) {
return res.status(401).json({ error: "Unauthorized" });
}
// validation.data contains user info and session metadata
# Validate that a session exists and has specific tags
validation = await client.session.validate(
session_token=request.cookies.get("session"),
required_tags=["role:root"],
ip_address=request.client.host,
user_agent=request.headers.get("user-agent")
)
if is_err(validation):
raise HTTPException(status_code=401, detail="Unauthorized")
# validation.data contains user info and session metadata
// Validate that a session exists and has specific tags
try {
ValidateSessionResponse validation = client.session.validate(
ValidateSessionCommand.builder()
.sessionToken(sessionToken)
.requiredTags(List.of("role:root"))
.ipAddress(request.getRemoteAddr())
.userAgent(request.getHeader("User-Agent"))
.build()
);
// validation contains user info and session metadata
} catch (ValidateSessionException e) {
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
}
// Validate that a session exists and has specific tags
try
{
var validation = await client.Session.ValidateAsync(new ValidateSessionCommand
{
SessionToken = Request.Cookies["session"],
RequiredTags = new List<string> { "role:root" },
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = Request.Headers["User-Agent"].ToString()
});
// validation contains user info and session metadata
}
catch (ValidateSessionException)
{
return StatusCode(401, new { Error = "Unauthorized" });
}

This is useful for endpoints that require specific permissions or access levels.

Some tags shouldn’t change after a session is created. For example, if a session starts as a root session, it should stay that way. To make some tags immutable, in your session_config.jsonc, use the on_create_only_tags setting:

{
"on_create_only_tags": ["role", "access"]
}

Now role:root or access:impersonation tags can’t be added or removed after creation.

You can also make all tags immutable:

{
"on_create_only_tags": ["*"]
}

Each tag can override any of these session settings:

  • absolute_lifetime_secs - Maximum session duration
  • inactivity_timeout_secs - Timeout after inactivity
  • max_concurrent_sessions_per_user - Concurrent session limit
  • max_concurrent_sessions_per_user_per_tag - A separate concurrent session limit that’s BOTH per user AND per tag
  • on_session_limit_exceeded - What to do when limit is hit (“reject_new” or “drop_least_recently_active”)
  • disallow_ip_address_changes - Invalidate on IP change
  • ip_allowlist - Restrict to specific IP ranges
  • ip_blocklist - Block specific IP ranges