| title | Security Architecture |
|---|
This document describes the architecture of Signal K Server's security system, including how the various components interact.
Signal K Server uses a pluggable security strategy pattern. The security system consists of:
- Security Strategy Interface (
src/security.ts) - Defines the contract for security implementations - Dummy Security (
src/dummysecurity.ts) - No-op implementation when security is disabled - Token Security (
src/tokensecurity.ts) - Full implementation with JWT- based authentication - Authentication Providers (
src/auth/) - passport-based login methods (redirect flows) that plugins can extend - OIDC Module (
src/oidc/) - the built-in OpenID Connect provider
┌─────────────────────────────────────────────────────────────────┐
│ Signal K Server │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ security.ts │ │ tokensecurity.ts │ │
│ │ │ │ │ │
│ │ - SecurityStrategy │◄──────│ - login/logout routes │ │
│ │ interface │ │ - JWT token management │ │
│ │ - startSecurity() │ │ - Session cookie management │ │
│ │ - saveSecurityConfig│ │ - User/device management │ │
│ └─────────────────────┘ │ - ACL enforcement │ │
│ ▲ └──────────────┬──────────────┘ │
│ │ │ │
│ ┌────────┴────────┐ ▼ │
│ │ dummysecurity.ts │ ┌─────────────────────────────┐ │
│ │ │ │ src/auth/ │ │
│ │ - No-op impl │ │ │ │
│ │ - Used when │ │ - providers.ts: registry + │ │
│ │ security │ │ /signalk/v1/auth/:id/* │ │
│ │ disabled │ │ - provisioning.ts: identity │ │
│ └──────────────────┘ │ -> local user │ │
│ │ - handshake-session.ts │ │
│ └──────────────▲──────────────┘ │
│ │ registers │
│ ┌──────────────┴──────────────┐ │
│ │ src/oidc/ (built-in) │ │
│ │ plugins (any passport │ │
│ │ strategy) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The SecurityStrategy interface (src/security.ts) defines the methods that
a security implementation must provide:
tokensecurity.ts is the production security implementation. It provides:
- Local Login: Username/password via
/loginor/signalk/v1/auth/login - Provider Login: Any registered authentication provider (OIDC or a
plugin's passport strategy) via
/signalk/v1/auth/<id>/login - Device Access Requests: Devices can request access tokens
Sessions are managed via HTTP-only cookies:
JAUTHENTICATION: The JWT tokenskLoginInfo: Login status for JavaScript access (non-httpOnly)
Session cookie helpers ensure consistent security settings:
httpOnly: true(for JAUTHENTICATION)sameSite: 'strict'secure: true(when over HTTPS)
The server implements a sliding session window: when a JWT token is past the midpoint of its lifetime, the next HTTP request silently replaces the cookie with a freshly issued token. This keeps active users logged in indefinitely while inactive sessions still expire after the configured duration.
src/auth/providers.ts keeps a registry of login methods, each backed by a
passport strategy, on a private passport
instance. SecurityStrategy.registerAuthenticationProvider() (exposed to
plugins as app.registerAuthenticationProvider()) adds one at runtime.
- User picks a provider on the login page (
loginStatus.authProviders) GET /signalk/v1/auth/<id>/login?redirect=stores the return path in the handshake session and runspassport.authenticate(<id>); the strategy redirects to the identity provider- Provider redirects back to
GET|POST /signalk/v1/auth/<id>/callback - The strategy verifies the response and its verify callback returns an
ExternalIdentity(subject, issuer, username, permission, email, name) provisioning.tsfinds the local user by provider + issuer + subject, or creates one (persistingsecurity.jsonbefore the record becomes visible; calls are serialized so concurrent first logins cannot create duplicates), and refreshes permission and identity detailstokensecurityissues the regular JWT session cookies and redirects to the stored return path
Passport strategies keep their state (PKCE verifier, nonce) in req.session.
handshake-session.ts provides that as an AES-256-GCM encrypted cookie
scoped to /signalk/v1/auth, keyed from the master secret; there is no
server-side session store.
GET /signalk/v1/auth/<id>/logout clears the local session cookies and asks
the provider for a logout URL. The OIDC provider returns the identity
provider's end_session_endpoint with post_logout_redirect_uri derived
from the configured redirectUri, so the user is logged out of both.
src/oidc/ is the built-in OpenID Connect provider, registered through the
same registry as plugin providers:
| Module | Responsibility |
|---|---|
config.ts |
Parse and validate OIDC config (env + security.json) |
provider.ts |
Strategy from openid-client/passport, lazy discovery |
permission-mapping.ts |
Map groups claim to Signal K permission |
oidc-admin.ts |
Admin API: GET/PUT /security/oidc, connection test |
Discovery runs on the first login attempt (the identity provider may boot after Signal K) and is cached until the configuration changes.
Security configuration is stored in security.json:
{
"users": [...],
"devices": [...],
"secretKey": "...",
"expiration": "1h",
"allow_readonly": true,
"acls": [...],
"oidc": {
"enabled": true,
"issuer": "https://...",
"clientId": "...",
"clientSecret": "..."
}
}Environment variables can override configuration values. OIDC secrets are
recommended to be set via environment variables (SIGNALK_OIDC_*).
The security strategy is selected at startup in startSecurity():
- Check
SECURITYSTRATEGYenvironment variable - Check
config.settings.security.strategyin settings - Fall back to
dummysecurityif neither is set
The strategy is dynamically loaded and attached to app.securityStrategy.