Session Tags | PropelAuth BYO Documentation
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.
What are Session Tags?
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:
Node Python Go Java .NET
// 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
session, err := client.Session.Create(ctx, byo.CreateSessionCommand{
UserID: userID,
Tags: []string{"role:root", "org:acme-corp"},
})
if err != nil {
log.Println("Failed to create session:", err)
}
// 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" }
});
Getting Started
Let’s start with a common scenario: different session rules for root accounts vs regular users.
Step 1: Configure Your Tags
In your session_config.jsonc file, define the rules for each tag:
{
"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
}]
}
Step 2: Apply Tags When Creating Sessions
When a user logs in, determine which tags apply and include them when creating the session:
Node Python Go Java .NET
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}
mux.HandleFunc("POST /api/login", func(w http.ResponseWriter, r *http.Request) {
user := authenticateUser(r)
tags := []string{}
if user.IsRootAccount {
tags = []string{"role:root"}
}
session, err := client.Session.Create(r.Context(), byo.CreateSessionCommand{
UserID: user.ID,
IPAddress: byo.String(r.RemoteAddr),
UserAgent: byo.String(r.UserAgent()),
Tags: tags,
})
if err != nil {
http.Error(w, "Failed to create session", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{Name: "session", Value: session.SessionToken, HttpOnly: true, Secure: true})
json.NewEncoder(w).Encode(map[string]any{"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.
Common Patterns
Organization-Specific Requirements
Different organizations often have different security requirements. Session tags make this easy:
Node Python Go Java .NET
// 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
session, err := client.Session.Create(ctx, byo.CreateSessionCommand{
UserID: user.ID,
Tags: []string{"org:" + user.OrganizationID},
IPAddress: byo.String(r.RemoteAddr),
UserAgent: byo.String(r.UserAgent()),
})
if err != nil {
log.Println("Failed to create session:", err)
}
// 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
}
]
}
Login Type Based Sessions
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
}
]
}
Temporary or Special Access
Tags are perfect for temporary access patterns:
Node Python Go Java .NET
// 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
elevatedSession, err := client.Session.Create(ctx, byo.CreateSessionCommand{
UserID: userID,
Tags: []string{"access:elevated"},
})
if err != nil {
log.Println("Failed to create elevated session:", err)
}
// Trial user access
trialSession, err := client.Session.Create(ctx, byo.CreateSessionCommand{
UserID: guestID,
Tags: []string{"access:trial"},
})
if err != nil {
log.Println("Failed to create trial session:", err)
}
// 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" }
});
Combining Multiple Tags
Sessions can have multiple tags, and their settings merge together. This lets you compose complex behaviors from simple rules:
Node Python Go Java .NET
// 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
session, err := client.Session.Create(ctx, byo.CreateSessionCommand{
UserID: userID,
Tags: []string{"role:root", "org:acme-corp"},
})
if err != nil {
log.Println("Failed to create session:", err)
}
// 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()
);
```csharp
// 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" }
});
When tags have conflicting settings, PropelAuth BYO uses a priority system to resolve them.
### Tag Priority
When multiple tags define the same setting, the tag with higher priority wins. You control the priority order in your configuration:
```json
{
"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:
- Organization tags (like
org:acme) have highest priority - Role tags (like
role:root) come next - Login type tags (like
login_type:sso) have lowest priority - Default settings apply if no tags match
This means if Acme Corp sets a 4-hour session limit, it overrides even root account sessions.
Validating Sessions with Tags
When you validate a session, you can optionally verify it has specific tags:
Node Python Go Java .NET
// 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
cookie, _ := r.Cookie("session")
validation, err := client.Session.Validate(ctx, byo.ValidateSessionCommand{
SessionToken: byo.String(cookie.Value),
RequiredTags: []string{"role:root"},
IPAddress: byo.String(r.RemoteAddr),
UserAgent: byo.String(r.UserAgent()),
})
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// validation 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.
Advanced Features
Immutable Tags
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": ["*"]
}
Available Tag Settings
Each tag can override any of these session settings:
absolute_lifetime_secs- Maximum session durationinactivity_timeout_secs- Timeout after inactivitymax_concurrent_sessions_per_user- Concurrent session limitmax_concurrent_sessions_per_user_per_tag- A separate concurrent session limit that’s BOTH per user AND per tagon_session_limit_exceeded- What to do when limit is hit ("reject_new" or "drop_least_recently_active")disallow_ip_address_changes- Invalidate on IP changeip_allowlist- Restrict to specific IP rangesip_blocklist- Block specific IP ranges