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:

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:

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:

  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.

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: