Authentication for Rise-Deployed Applications
Rise provides built-in authentication for your deployed applications using JWT tokens. When users authenticate to access your application, Rise issues a signed JWT token that your application can validate to identify the user.
Overview
When a user logs into a Rise-deployed application:
- Rise authenticates the user via OAuth2/OIDC (e.g., through Dex)
- Rise issues an RS256-signed JWT token with user information
- The JWT is stored in the
rise_jwtcookie - Your application can access this cookie to identify the user
The rise_jwt Cookie
The rise_jwt cookie contains a JWT token with the following structure:
JWT Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "<key-id>"
}
JWT Claims (Example)
{
"sub": "CiQwOGE4Njg0Yi1kYjg4LTRiNzMtOTBhOS0zY2QxNjYxZjU0NjYSBWxvY2Fs",
"email": "admin@example.com",
"name": "admin",
"groups": [],
"iat": 1768858875,
"exp": 1768945275,
"iss": "http://rise.local:3000",
"aud": "http://test.rise.local:8080"
}
Claim Descriptions
- sub: Unique user identifier from the identity provider (typically a base64-encoded UUID)
- email: User’s email address
- name: User’s display name (optional, included if available from IdP)
- groups: Array of Rise team names the user belongs to (empty array if user has no team memberships)
- iat: Issued at timestamp (Unix epoch seconds)
- exp: Expiration timestamp (Unix epoch seconds, default: 24 hours from issue time)
- iss: Issuer (Rise backend URL, e.g.,
http://rise.local:3000) - aud: Audience (your deployed application’s URL, e.g.,
http://test.rise.local:8080)
Note: The JWT expiration time is configurable via the jwt_expiry_seconds server setting (default: 86400 seconds = 24 hours).
Validating the JWT
Rise provides the public keys needed to validate JWTs through the standard OpenID Connect Discovery endpoint.
OpenID Connect Discovery
Applications should use the OpenID Connect Discovery 1.0 specification to discover the JWKS endpoint:
- Fetch OpenID configuration from
${RISE_ISSUER}/.well-known/openid-configuration - Extract
jwks_urifrom the configuration response - Fetch JWKS from the
jwks_uriendpoint - Cache the JWKS (recommended: 1 hour) to avoid excessive requests
- Use the JWKS to validate JWT signatures
Example discovery response:
{
"issuer": "https://rise.example.com",
"jwks_uri": "https://rise.example.com/api/v1/auth/jwks",
"id_token_signing_alg_values_supported": ["RS256", "HS256"],
"subject_types_supported": ["public"],
"claims_supported": ["sub", "email", "name", "groups", "iat", "exp", "iss", "aud"]
}
Environment Variables
Your deployed application automatically receives:
- RISE_ISSUER: Rise server URL (base URL for all Rise endpoints) and JWT issuer for validation (e.g.,
http://rise.local:3000) - RISE_APP_URL: Canonical URL where your app is accessible (primary custom domain or default project URL)
- RISE_APP_URLS: JSON array of all URLs your app is accessible at (primary ingress + custom domains), e.g.,
["http://myapp.rise.local:8080", "https://myapp.example.com"] - PORT: The HTTP port your container should listen on (default: 8080)
Example: TypeScript/Node.js
Using the jose library which handles OIDC discovery and JWKS automatically:
import { jwtVerify, createRemoteJWKSet } from 'jose';
import type { Request, Response, NextFunction } from 'express';
const RISE_ISSUER = process.env.RISE_ISSUER || 'http://rise.local:3000';
const RISE_APP_URL = process.env.RISE_APP_URL;
// Create JWKS fetcher (automatically handles caching and discovery)
const JWKS = createRemoteJWKSet(
new URL(`${RISE_ISSUER}/api/v1/auth/jwks`)
);
interface RiseClaims {
sub: string;
email: string;
name?: string;
groups?: string[];
}
// Express middleware to verify Rise JWT
async function verifyRiseJwt(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.rise_jwt;
if (!token) {
return res.status(401).send('No authentication token');
}
try {
const { payload } = await jwtVerify<RiseClaims>(token, JWKS, {
issuer: RISE_ISSUER,
audience: RISE_APP_URL, // Validates the aud claim
});
req.user = {
id: payload.sub,
email: payload.email,
name: payload.name,
groups: payload.groups || [],
};
next();
} catch (err) {
return res.status(401).send('Invalid token');
}
}
Install: npm install jose cookie-parser
Example: Python/Flask
Using joserfc which handles JWKS fetching and JWT validation:
from typing import TypedDict, Optional
from joserfc import jwt
from joserfc.jwk import JWKRegistry
import requests
from flask import request, jsonify, g
import os
class RiseClaims(TypedDict):
sub: str
email: str
name: Optional[str]
groups: list[str]
iat: int
exp: int
iss: str
aud: str
class UserInfo(TypedDict):
id: str
email: str
name: Optional[str]
groups: list[str]
RISE_ISSUER = os.environ.get('RISE_ISSUER', 'http://rise.local:3000')
RISE_APP_URL = os.environ.get('RISE_APP_URL')
# Fetch JWKS once at startup (or cache with TTL)
def get_jwks_registry():
config_url = f'{RISE_ISSUER}/.well-known/openid-configuration'
config = requests.get(config_url).json()
jwks = requests.get(config['jwks_uri']).json()
return JWKRegistry.import_key_set(jwks)
jwks_registry = get_jwks_registry()
@app.before_request
def authenticate():
"""Middleware to authenticate requests using Rise JWT"""
token = request.cookies.get('rise_jwt')
if not token:
return jsonify({'error': 'No authentication token'}), 401
try:
# Verify and decode JWT (including issuer and audience validation)
claims: RiseClaims = jwt.decode(
token,
jwks_registry,
claims_options={
"iss": {"value": RISE_ISSUER},
"aud": {"value": RISE_APP_URL},
},
)
user_info: UserInfo = {
'id': claims['sub'],
'email': claims['email'],
'name': claims.get('name'),
'groups': claims.get('groups', [])
}
g.user = user_info
except Exception as e:
return jsonify({'error': 'Invalid token'}), 401
Install: pip install joserfc requests
Authorization Based on Groups
You can use the groups claim to implement team-based authorization:
import type { Request, Response, NextFunction } from 'express';
function requireTeam(teamName: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).send('Not authenticated');
}
if (!req.user.groups.includes(teamName)) {
return res.status(403).send('Access denied - not a member of required team');
}
next();
};
}
// Protect routes by team membership
app.get('/admin', requireTeam('admin'), (req: Request, res: Response) => {
res.send('Admin panel');
});
Best Practices
- Always Validate the JWT: Don’t trust the cookie contents without verification
- Verify Audience: Always validate the
audclaim matchesRISE_APP_URL - Use Modern Libraries: Use
jose(Node.js) orauthlib(Python) - they handle OIDC discovery automatically - Use HTTPS: The
rise_jwtcookie is marked as Secure in production - Handle Missing Tokens: Users may not be authenticated - handle gracefully
- Let Libraries Cache: Modern JWT libraries automatically cache JWKS with appropriate TTLs
Troubleshooting
Token Validation Fails
- Check Algorithm: Ensure you’re using RS256, not HS256
- Verify JWKS: Ensure your library can reach
${RISE_ISSUER}/.well-known/openid-configuration - Check Audience: The
audclaim must matchRISE_APP_URL - Check Expiration: Tokens expire after 24 hours by default (configurable)
No Cookie Present
- Check Authentication: User may not be logged in
- Check Access Class: Ensure your project has authentication enabled
- Check Cookie Domain: For custom domains, cookies may not be shared
Groups Missing
- Check IdP Configuration: Groups come from your identity provider
- Check Team Sync: Ensure IdP group sync is enabled in Rise
- Check Team Membership: User must be a member of Rise teams
Security Considerations
- The
rise_jwtcookie is HttpOnly - JavaScript cannot access it (XSS protection) - The JWT is signed with RS256 - public keys fetched via OIDC discovery verify authenticity
- Tokens expire after 24 hours by default - users must re-authenticate periodically
- The
audclaim ties tokens to specific applications - always validate this claim
Additional Resources
- JWT.io - JWT debugger and documentation
- JWKS Specification - JSON Web Key Set standard
- RS256 vs HS256 - Understanding signature algorithms