Authentication & Security
This page covers how to configure sign-in and the security model behind it, from how users authenticate to how media links are protected.
Sovereign supports two ways for users to sign in:
- Local accounts - a username and password validated directly by Sovereign. It’s always available and needs no external services.
- Social sign-in - optional “Sign in with Google” and “Sign in with Microsoft” buttons backed by external identity providers.
Both are configured through environment variables.
Local Login
Section titled “Local Login”Local login works out of the box. Sovereign creates a default Administrator account on first run from the credentials you provide, and you can add more users from the admin interface afterward.
| Environment Variable | Description | Default | Sensitive |
|---|---|---|---|
Sovereign__Authentication__AdminUsername |
Username for the default admin account seeded on first run | (random 12-char string) | ✓ |
Sovereign__Authentication__AdminPassword |
Password for the default admin account seeded on first run | (random 12-char string) | ✓ |
If you leave these unset, a random username and password are generated. Either way, the pair is printed once to the server’s console output on first start, where docker logs <container> or docker compose logs server shows them. They’re never written to the log files you can download from the admin interface. Anything that collects the container’s output, such as a log shipper, will keep a copy. It’s better to set them explicitly so you know your first-login credentials, and either way, change the password after your first sign-in. See Authorization & Roles below for the role model.
Outside the development environment, a password you set has to meet the same rules as any other account’s: at least six characters, with an upper-case letter, a lower-case letter, a digit and a symbol. A seed password that falls short stops the server on first start with the reason in the log. Five wrong passwords in a row lock an account for five minutes.
The Issuer - Required for Any Non-localhost Deployment
Section titled “The Issuer - Required for Any Non-localhost Deployment”The issuer is used both as the token issuer and as the address the browser uses to reach the sign-in endpoints, so it must be the public origin that clients actually use to reach your server.
| Environment Variable | Description | Default |
|---|---|---|
Sovereign__Authentication__Issuer |
Public origin clients use to reach the server (scheme + host) | http://localhost:5000/ |
Set it to the exact URL your users type into their browser, including https://:
Sovereign__Authentication__Issuer=https://media.example.com/Sovereign doesn’t terminate TLS itself, so put a reverse proxy in front of it and point the issuer at the proxy’s public HTTPS URL (see TLS below).
The Client ID
Section titled “The Client ID”The client ID identifies the web app to Sovereign’s sign-in. Leave it unset and Sovereign generates one on first start, writes it to the data volume as part of server.json, and reads it back on every start after. Each install gets its own id rather than a well-known one, and it survives restarts with no configuration on your part. The web app picks up the current value by itself, so nothing else needs changing when it does.
Set a fixed value when you want to choose the id yourself:
Sovereign__Authentication__ClientId=your-fixed-client-idA configured value always wins and is never written to the data volume. Remove the setting and Sovereign goes back to the stored id, generating one if it has never needed to.
Refresh tokens are tied to the client that issued them, so changing the client ID signs every browser out: the next start removes the superseded registration along with its tokens. An ordinary restart keeps the same ID, so this only happens when you change it. The Roku and Android apps authenticate as a separate device client with an id of their own, also kept in server.json, whether they were paired with a code or signed in with a password, so changing the web client ID leaves them signed in.
Session Length
Section titled “Session Length”Three settings decide how long a viewer stays signed in, and each is set twice: once for the web app (Web) and once for the Roku and Android apps (Device). The two get different defaults because they keep their sign-in in different places. A browser holds it in the page’s own storage, where a bug in the page could expose it, so a web sign-in has an end. A television keeps it in the app’s own storage on the device and is often used only every few weeks, so it stays signed in for as long as it keeps being used.
Sovereign__Authentication__Tokens__Web__AccessTokenMinutes=15Sovereign__Authentication__Tokens__Web__RefreshTokenDays=30Sovereign__Authentication__Tokens__Web__MaxSessionDays=90Sovereign__Authentication__Tokens__Device__AccessTokenMinutes=15Sovereign__Authentication__Tokens__Device__RefreshTokenDays=90Sovereign__Authentication__Tokens__Device__MaxSessionDays=0The access token is what every API call carries. It’s self-contained, so the server doesn’t look it up in the database on each request. That makes it fast, and it also means a role change (for example moving someone back to Unapproved) can take until the token expires to apply, while a deleted account stops working on its next request. Every app renews it quietly before it runs out, so the 15-minute default signs nobody out, and the longer you make it the longer a role change can take to apply.
The refresh token decides how long a device may sit unused before it asks for a sign-in. Sovereign rotates refresh tokens and slides their expiry forward each time, so the clock only runs while a device sits idle. With the defaults, a browser left alone for a month asks for a sign-in, and so does a television nobody has touched in three months.
The session cap (MaxSessionDays) is the longest one sign-in may last, however often it’s used. When it’s reached, the app asks the viewer to sign in again, which also ends any copy of that sign-in someone else may have taken. The web app’s is 90 days. 0 turns the cap off, which is the default for the Roku and Android apps.
So if viewers are complaining about being signed out of a television, raise Device__RefreshTokenDays. Raising an access token’s minutes instead buys nothing and widens the window in which a disabled account still has access.
Social Sign-In
Section titled “Social Sign-In”Social sign-in is optional and disabled by default. Each provider is enabled only when both its client ID and client secret are supplied; if they’re absent, the provider’s button is hidden and local login keeps working as normal.
Every provider needs a redirect URI registered on its side. The redirect URI is your issuer origin followed by a provider-specific callback path:
| Provider | Redirect URI to register |
|---|---|
https://<your-issuer>/callback/login/google |
|
| Microsoft Entra | https://<your-issuer>/callback/login/microsoft |
Sovereign only requests the user’s basic profile and email so it can create and identify their account.
Create an OAuth 2.0 Client ID of type Web application in the Google Cloud Console. Add the redirect URI above to the client’s Authorized redirect URIs, then supply the credentials:
| Environment Variable | Description | Sensitive |
|---|---|---|
Sovereign__Authentication__Google__ClientId |
OAuth 2.0 client ID from the Google Cloud Console | |
Sovereign__Authentication__Google__ClientSecret |
OAuth 2.0 client secret from the Google Cloud Console | ✓ |
Microsoft Entra
Section titled “Microsoft Entra”Create an App registration in the Microsoft Entra admin center. Add the redirect URI above as a Web platform redirect, create a client secret, then supply the credentials:
| Environment Variable | Description | Default | Sensitive |
|---|---|---|---|
Sovereign__Authentication__Entra__ClientId |
Application (client) ID from the app registration | ||
Sovereign__Authentication__Entra__ClientSecret |
Client secret from the app registration | ✓ | |
Sovereign__Authentication__Entra__TenantId |
Directory (tenant) ID, or common to allow sign-ins from any tenant |
common |
Example
Section titled “Example”A minimal sovereign.env for a public deployment with local login and Google sign-in enabled:
# Local admin account (first-run seed)Sovereign__Authentication__AdminUsername=adminSovereign__Authentication__AdminPassword=<a-strong-password>
# Public origin - must match the URL users browse toSovereign__Authentication__Issuer=https://media.example.com/
# Optional: enable "Sign in with Google"Sovereign__Authentication__Google__ClientId=<google-client-id>Sovereign__Authentication__Google__ClientSecret=<google-client-secret>OAuth 2.0 / OpenID Connect
Section titled “OAuth 2.0 / OpenID Connect”Sovereign uses OpenIddict to implement a full OAuth 2.0 / OpenID Connect server, backed by ASP.NET Core Identity.
It uses these OAuth flows:
| Flow | Purpose |
|---|---|
| Password (Resource Owner) | Primary flow the web app uses to sign in with a username and password |
| Device Authorization | Signs a television in from a browser: the TV shows a short code, and you approve it there |
| Refresh Token | Silently renews short-lived access tokens without user interaction |
The Roku channel and the Android apps offer the device flow at sign-in, so nobody has to type a password with a remote. The television shows an eight-character code, you enter it at /app/link on your server while signed in, and the television signs in as you. A code lasts ten minutes. Device Pairing walks through the approval page.
The sign-in endpoints under /connect accept 30 requests a minute from any one address and answer 429 beyond that. Behind a reverse proxy every client shares the proxy’s address unless you list the proxy in Sovereign__Network__TrustedProxyNetwork__0, as described in the Configuration Overview.
Token signing and encryption certificates are covered separately under Token Certificates below.
Token Certificates
Section titled “Token Certificates”Sovereign protects the OAuth tokens it issues with two certificates: one signs every token, and the other encrypts the tokens only Sovereign itself reads, such as refresh tokens. On first startup it generates a pair of self-signed RSA certificates, writes them to the certificates folder on the persistent data volume, and reuses them across restarts so existing tokens survive an upgrade or container restart. This path needs no configuration and suits a single server with persistent storage.
These certificates are internal. They protect tokens Sovereign issues to its own clients, are never presented to a browser, and never leave the server. Because nothing external validates them, they don’t need to come from a certificate authority, and there’s no certificate to buy or request. Self-signed is the right choice here.
You can supply your own certificates instead, for example to share signing keys across load-balanced replicas or to rotate them on your own schedule. Provide each as a base64-encoded PKCS#12 (.p12/.pfx) archive:
| Environment Variable | Description | Sensitive |
|---|---|---|
Sovereign__Authentication__Certificates__SigningCertificate |
Base64-encoded PKCS#12 archive holding the token-signing certificate and key | ✓ |
Sovereign__Authentication__Certificates__SigningCertificatePassword |
Password protecting the signing archive, if it has one | ✓ |
Sovereign__Authentication__Certificates__EncryptionCertificate |
Base64-encoded PKCS#12 archive holding the token-encryption certificate and key | ✓ |
Sovereign__Authentication__Certificates__EncryptionCertificatePassword |
Password protecting the encryption archive, if it has one | ✓ |
A certificate you supply is used in place of a generated one, and is written to the certificates folder on every start, replacing whatever was there. The two settings are independent. Leave one empty and that role keeps the certificate already in the folder, or gets a newly generated one if the folder has none.
Generating Self-Signed Certificates
Section titled “Generating Self-Signed Certificates”Generate two independent certificates with OpenSSL, one for signing and one for encryption. For each, create the certificate and its key, bundle them into a PKCS#12 archive, then base64-encode the archive for the environment variable:
# Signing certificate (2048-bit RSA, valid five years)openssl req -x509 -newkey rsa:2048 -sha256 -days 1825 -nodes \ -keyout signing.key -out signing.crt -subj "/CN=Sovereign Token Signing"openssl pkcs12 -export -inkey signing.key -in signing.crt \ -out signing.p12 -passout pass:<signing-password>base64 -w0 signing.p12
# Encryption certificateopenssl req -x509 -newkey rsa:2048 -sha256 -days 1825 -nodes \ -keyout encryption.key -out encryption.crt -subj "/CN=Sovereign Token Encryption"openssl pkcs12 -export -inkey encryption.key -in encryption.crt \ -out encryption.p12 -passout pass:<encryption-password>base64 -w0 encryption.p12Pass the base64 output of each .p12 as the matching *Certificate variable, and the archive password as the matching *CertificatePassword variable. To skip the password, omit -passout and leave the password variable empty.
Authorization & Roles
Section titled “Authorization & Roles”Every API endpoint is protected by default, and access is controlled by four roles:
| Role | Description |
|---|---|
Administrator |
Full access. Can manage users, configure the server, and access all content. |
ReadOnlyAdministrator |
Can view administrative dashboards and settings, but can’t make changes. |
Viewer |
Standard user. Can browse and play media but has no administrative access. |
Unapproved |
Holding role assigned to new social sign-ups pending administrator approval. Grants no access until the account is promoted to another role. |
Sovereign creates a default Administrator account on first run from the credentials in Sovereign__Authentication__AdminUsername and Sovereign__Authentication__AdminPassword (see Local Login). These credentials are printed once to the server’s console output, never to its log files, and you should change them after your first sign-in.
ID Obfuscation
Section titled “ID Obfuscation”The IDs in Sovereign’s links and in the app don’t reveal how many titles you have, and nobody can guess one from another.
Each install writes its IDs differently. The character set behind them is generated on first start, saved to the data volume as part of server.json, and read back on every start after, so a link you bookmarked or shared keeps working after a restart, an upgrade, or a database rebuild.
To choose the character set yourself, set Sovereign__Obfuscation__Alphabet to a reordering of the 62 characters 0-9a-zA-Z. A configured value always wins and is never written to the data volume. Keep it private.
Changing a configured alphabet, or deleting server.json, invalidates every ID Sovereign has already handed out. Bookmarks and deep links stop resolving until they’re followed from the library again. Nothing on disk is orphaned by it, though, and no data is lost.
Signed Asset URLs
Section titled “Signed Asset URLs”Every link to a video, image, seek preview or subtitle track is handed out only to a signed-in user, and works only for the file it names. A link that’s been changed, or has expired, is refused, so nothing can be embedded elsewhere for long. Until it expires, though, a link works for whoever has it, much like a shared download link.
Media links last about a day. Download links for exports and log files last an hour at most.
The key behind these links is Sovereign__Obfuscation__AssetSecuritySecretKey. Leave it unset and Sovereign generates one on first start, writes it to the data volume as part of server.json, and reads it back on every start after, so outstanding links survive a restart. A configured value always wins and is never written to the data volume.
Rotating the key, whether by changing a configured value or deleting server.json, immediately invalidates every outstanding link. Images and playback fail mid-session until each client asks for fresh ones, which a page refresh does.
Sovereign doesn’t terminate TLS itself. Put a reverse proxy in front of it and set Sovereign__Authentication__Issuer to the public HTTPS URL clients use to reach the server. Traefik, Caddy and Nginx Proxy Manager are popular choices.
With an HTTPS issuer, Sovereign sends Strict-Transport-Security itself, alongside a Content-Security-Policy for its web app, so the proxy doesn’t need to add either. Production Hardening explains what each covers and when to set your own at the proxy instead.
If that proxy also authenticates (Cloudflare Access, Pomerium or Authelia, say), the television clients need a credential of their own and the Roku channel needs a path carved out for artwork. If you put the server on a private network such as Tailscale instead, every device has to join it. Remote Access covers both, along with what the carve-out does and doesn’t expose.
See the Configuration Overview for the complete environment-variable reference.
Titles, artwork, people and descriptions shown in screenshots are fictional and were created for illustration. Sovereign doesn't include or supply any media: it plays the library you bring.