Configuration
ShadowLogin
The door of your server, and everyone who walks through it.
Where the files are
plugins/ShadowLogin/ The plugin creates them on first start and updates them by itself on new versions, keeping your changes.
Applying changes
/shadowlogin reload Reloads the files without restarting the server.
No setting found
Try another word, or search every plugin.
Settings
The files that decide how the plugin behaves. Start here.
config.yml
General settings 111 settings
config.ymlplugins/ShadowLogin/config.yml File notes
Every value here already holds the right number for an ordinary server: you can drop in the jar and never open this file. When you do need to change something, the comment above each key says WHAT it does and, more importantly, WHY it is set that way.
A wrong value never stops the plugin: the default is used instead and the console prints the path and what was expected.
On update your files are moved to the new version automatically: every value you changed is carried over, new options arrive with their comments, the old files are kept in backup/<date>/, and if something needs your attention you will find PORTING-<version>.txt in the plugin folder.
Durations are written in plain text: 500ms, 20t (ticks), 30s, 5m, 2h30m, 7d. A bare number means seconds.
This file is English on purpose - keys, sections and comments alike - so that a server anywhere in the world reads the same configuration. Only what a player sees is translated, and that lives in lang/.
config-version6Licence
ShadowLogin is a Premium plugin: there is no free edition. Without a valid licence at start-up it does NOT run - no database, no login, nothing but the licence commands below - and says so in the console. Activate the licence, then restart the server.
Keep reading
While it is not running, a server that does not verify players itself (offline mode, with no online-mode proxy forwarding to it) REFUSES EVERY CONNECTION: without its login plugin anybody could join under any name, an operator's included. In online mode, or behind an online-mode proxy, players join normally.
A licence that stops applying while the server is running never stops the plugin mid-session: that would lock everybody out or let everybody in. The session carries on, and the console says ShadowLogin will not start at the next restart.
You normally touch nothing here: a key is activated in game with
/shadowlogin license claim <purchase-code> /shadowlogin license activate <key>
and is written to license.yml, which sits outside this file on purpose: a config gets sent to support, a licence does not.
license4 keysTHE KEY LIVES IN license.yml (same folder as this file), or in the central plugins/ShadowControl/licenses.yml for the whole suite. The 'key' below is only a fallback for deploy systems that generate config.yml: leave it empty.
The key does NOT live here: it lives in license.yml, in this folder, written at first start (or in plugins/ShadowControl/licenses.yml for the whole suite). Only if you would rather keep it here than in license.yml. The SHADOWLOGIN_LICENSE environment variable and the -Dshadowlogin.license=... system property work too.
grace-days7Days of grace, running normally, before the licence stops applying when a subscription expires or when the server address does not match the licence. A renewal has almost always already been started, and an address that stops matching is far more often a NAT than a thief.
server-ip""The address this server is known by, if you know it. Behind a proxy the machine never sees its own domain, and this line is the only way to tell it without calling anybody.
resolve-public-iptrueOne call at start-up to the licence service (activation-url + /ip) to find out this server's public address: behind NAT it is the only way to recognise it. Only when a licence is installed: a server without a key asks nobody. false = never ask.
activation-url'https://shadow-licences.riccardobotins.workers.dev'Activation service. This is the BASE address: the plugin appends /claim and /check itself. Empty means fully offline verification.
Security
The part you do not change lightly. The defaults are right for ninety per cent of servers; every time you lower one you are trading security for convenience, and the comment tells you what it costs.
security7 keyssecurity.hash2 keyscost12BCrypt's cost factor. Every +1 DOUBLES the time it takes.
Keep reading
10 -> about 60 ms a little thin in 2026 12 -> about 250 ms the recommended value 14 -> about 1 second for few players and a lot of worry
The time is paid by whoever logs in (once) and by whoever attacks (for every single attempt, multiplied by millions). That is the whole reason SHA-256 is not used here: it is fast, and "fast" is exactly the gift you do not want to hand somebody who steals a database.
CAREFUL on a server with hundreds of connections a minute: hashing runs off the game thread, so it never costs you ticks, but it does hold a thread for as long as it runs. Above 13 on a very busy server, measure.
rehash-on-logintrueWhen you raise the cost above, existing accounts still carry the old hash. With this option each password is recomputed at the new cost on its owner's next login: nobody has to do anything, and within a few days the whole database is up to date.
It never works the other way round: if you lower the cost, stronger hashes are left alone.
security.password5 keysmin-length6Minimum length. Six is the honest compromise for a Minecraft server: below that it gets guessed, above it people write it on a sticky note or reuse one from another site.
max-length64The maximum is NOT strictness: BCrypt only looks at the first 72 bytes, so beyond that characters stop counting. The plugin says so rather than letting you find out the day you change the end of a very long phrase and the old one still works.
require-letter-and-digitfalseRequire a letter AND a digit. Off by default, and that is not an oversight: complicated rules produce worse passwords ("Marco1", "Password1") and stop nobody. If you turn it on, know what you bought.
forbid-name-as-passwordtrueForbid a password identical to the player's own name. This one yes, because it is the first thing anybody tries.
forbidden[password, 123456, 12345678, qwerty, minecraft, abc123, 111111, letmein, iloveyou]+6
Passwords that may not be used. These are the first entries in any dictionary: blocking them costs nothing and removes half of the account thefts on a Minecraft server.
Add your own server name: it is always on an attacker's list.
security.attempts7 keysmax5How many attempts before being kicked. Five is generous for somebody who really did mistype and tight for somebody guessing.
window"10m"How long before a mistake is forgotten. Somebody who got it wrong once last month should not be dragging it around.
backoff-base"5s"The wait after the first mistake, which then DOUBLES with every further one: 5s, 10s, 20s, 40s... up to the ceiling below.
Keep reading
This is the defence that actually matters. A hard limit of five attempts is worked around by waiting for the reset; a doubling wait turns a dictionary from three attempts a second into three an hour, which is an attack that never finishes.
Attempts are counted per ADDRESS and per NAME, both. Counting only the name lets through somebody trying "123456" on a thousand different names; counting only the address lets through a botnet on one account.
backoff-max"10m"lock-after10After how many mistakes the account is locked outright, and for how long.
lock-duration"30m"kick-on-maxtrueWhether to kick somebody who has run out of attempts. On: whoever is guessing passwords has to reconnect every time, and reconnecting goes through the antibot limits.
securitylogin-timeout"60s"How long there is to log in before being kicked. Mostly this is against people who open connections and leave them sitting on a slot.
reminder-every"5s"How often to remind somebody waiting what they are supposed to do. Five seconds is enough to be noticed without becoming annoying.
clear-chat-after-authtrueAfter logging in, push what was typed off the screen.
HONESTLY: no plugin can clear the client's local history - the up arrow will still bring back the typed command, and that is a limit of the game, not an oversight. This takes the line out of sight immediately, so it is not left sitting there while somebody watches the screen.
If this genuinely worries you, the answer is login.input: GUI: with the on-screen keypad the password never goes through the chat box at all.
clear-chat-lines30Official accounts (Mojang / Microsoft)
The feature this plugin exists for. Read at least the first two paragraphs before changing anything below.
Keep reading
THE RIGHT QUESTION. It is not "does this name belong to a bought account?" - that says nothing about who is knocking right now, and letting anybody who types an official name in without a password would be handing over every bought account on the server. The right question is: "has this session ALREADY been authenticated by Mojang?"
Which can be answered with facts, without touching a single packet:
1. the server is in online-mode -> yes, the server did it; 2. the session's UUID is Mojang's real one for that name -> then it comes from upstream, that is from the proxy that authenticated the person, because a client cannot choose it for itself.
CASE 2 DEPENDS ON HOW YOUR NETWORK IS SET UP. If identity forwarding from the proxy is not protected (server port open to the world, or the old BungeeCord forwarding without a shared secret), anybody can connect directly and declare whatever UUID they like. That is not a flaw in this plugin and no plugin can repair it from the inside: it is a server that can be walked around. Set trust-proxy-authentication: false if you are not sure about your network.
Identity - read this chapter in full
On a server that also accepts unofficial accounts, THE SAME NAME CAN ARRIVE WITH TWO DIFFERENT UUIDS. Somebody who bought the game brings Mojang's UUID with them; somebody who did not gets one computed from the name, and the two never coincide.
Keep reading
Minecraft keeps inventory, progress, position and everything else PER UUID. Getting this setting wrong means handing one player's inventory to another. This is not a textbook case: it is exactly what happens to somebody who one day puts a proxy in front of their server.
UUID (default)
One account per UUID. The official "Marco" and the unofficial "Marco" are two separate accounts, with two passwords and two inventories - which is also what the server is going to do anyway, so the plugin is not telling a different story from the true one. On its own it is not enough: it is premium.protect-names that stops the second of the two from being born.
NAME
One account per name, like AuthMe and like almost every existing server. The UUID seen at registration is still stored, and if another one turns up one day with the same name, on-uuid-mismatch below applies.
If you are importing from AuthMe, NAME is what you expect. If you are starting from scratch, leave UUID.
identity3 keysmodeUUIDOne person, one identity - read this if your server is mixed
identityunify-uuid"OFF"THE PROBLEM, with the example that actually happens. Marco logs in with the original game, gets Mojang's UUID, and builds an island. The next day he logs in from an unofficial client with the same name: that UUID is a different one, so to the server he is A DIFFERENT PERSON. New island, empty inventory, no money. Marco does not understand, and he is right not to: he is still Marco.
Keep reading
The important thing to understand is that this is not a login problem. The island, the inventory, the money and the permissions are kept by OTHER plugins, and every one of them keys on the UUID. So it is not enough for this plugin to keep its own house in order - the whole server has to see one single UUID. That is what this setting does, and it is the only place where ShadowLogin changes something that is not its own.
OFF = change nothing (default). Official and unofficial stay two
different people, with two accounts, two islands and two
inventories - which is also what the server would do by
itself. Right for a server where the two never mix.OFFLINE = everybody gets the UUID computed from the name, official
players included. RECOMMENDED on a mixed server, and the
reason is that it depends on nobody: that UUID is worked
out from the name with a calculation, always the same,
without asking any service anything. A UUID that changes
because Mojang was down for five minutes is a lost island.PREMIUM = everybody gets Mojang's real UUID, when the name has one.
For servers that already have data created in online-mode
and do not want to lose it. It costs one dependency: if
Mojang does not answer for a name never seen before, the
plugin does NOT guess - it refuses the connection with
"try again in a minute". Letting somebody in under the
wrong UUID is not an inconvenience like retyping a
password: it is landing on nobody's island and building
on it.CAREFUL if the server already has data. Turning this on means people get a new UUID, and everything saved under the old one stays where it is and stops being reachable. On a new server there is nothing to worry about. The console says so loudly at start-up.
AND ONE THING THAT GOES WITH IT: with the UUIDs unified, an account born from an official login still has no password, and without a password there is no way in from an unofficial client. That is on purpose - it is what stops a stranger from claiming the name. The owner logs in with the original game once and sets a password with /changepassword; from then on both clients open the same account, with the same island. The plugin tells them so by itself the first time.
Quoted on purpose. In YAML, OFF and ON are not words: they are booleans. The plugin understands both spellings either way - it translates a boolean back into the word you wrote - but quoting says what you meant, and keeps the file readable by anything else that parses it.
on-uuid-mismatchDENYWhat to do when the name is right but the UUID is not.
DENY = refuse the connection and write it in the console
(recommended: this is exactly the dangerous case)
ASK_PASSWORD = let them try with the password, with no shortcuts at all
(useful while you are changing your network setup and you
know what you are doing)
ALLOW = adopt the new UUID as the account's own
(only for a migration, and only for as long as it takes)Session
Somebody reconnecting from the same address within N minutes does not retype their password. This is the feature that decides whether your login plugin is loved or hated: without it, anybody on a flaky connection types their password ten times an evening.
session3 keysenabledtrueduration"30m"How long it lasts. Half an hour covers a disconnect, a server restart and a hop between servers on a network; several hours starts to be a real window for somebody sitting down at another person's computer.
require-same-iptrueTie the session to the address. Leave this on: without it a session would be valid from anywhere in the world, and at that point the password would be needed exactly once, ever.
Keep reading
CAREFUL - and this is the real limit of any address-bound session: if two people share the same public address (the same house, the same school, or a mobile network, where thousands of users come out of one address) then inside the session window one of them can come in under the other's name without typing anything.
That is not a flaw in this plugin: it is what "same address" means. If your server has a lot of players on mobile networks, the defence is to shorten the duration above (five minutes still covers a disconnect) or to turn sessions off entirely with enabled: false.
Two-factor authentication
Optional, and switched on by whoever wants it, on their own account. It is for the few accounts that are worth something: staff, people who bought ranks, people with a base full of things. Forcing it on everybody gets it disabled by players in the simplest way available: by going to play somewhere else.
two-factor4 keysenabledtrueissuer"ShadowLogin"The name shown in the authenticator app. Put your server's name here: somebody with twenty entries in their app needs to work out which one is you.
window1How many codes to accept before and after the current one (one = a minute and a half in total). This is for clocks that are not in sync. Raising it multiplies the codes valid at any instant: if you need more than 1, the problem is the server's clock.
backup-codes8How many backup codes to hand out. They are for the day the phone breaks: without them, that day the account is lost.
Recovery
recovery2 keysrecovery.question1 keyenabledtrueThe security question, set by the player.
recovery.staff-code2 keyslength8The code an operator generates with /shadowlogin resetpassword. This is the honest route: an administrator recognises the person and hands them a temporary token, instead of walking into the account in their place.
valid-for"15m"Limbo - where somebody sits before they have logged in
An empty sky and a message. Somebody who has not typed their password sees nothing and does nothing: no chat, no commands other than the login ones, no movement beyond a radius, no inventory, no real position. Everything you let them see is information given away to somebody trying to get into another person's account.
limbo8 keysenabledtrueuse-spawntrueUse the player's world spawn, raised to the height below. Turn it off to use the fixed position in location.
limbo.location6 keysworld""x0.5y320Three hundred and twenty blocks: there is nothing to look at above the clouds, and from up there you cannot even tell which part of the map you are on.
z0.5yaw0pitch0limboradius3How many blocks one may move. Not zero: blocking movement entirely makes the client "rubber-band" and the screen shakes. Three blocks let client and server stay in agreement.
hide-playerstrueHide those waiting from everybody else, and everybody else from them. That way the login screen cannot be used to spy on who is online.
blindnesstrueBlindness: the empty sky mentioned above.
hide-inventorytruePut the inventory aside and give it back after login.
The snapshot is written to the database BEFORE emptying anything, and if that write fails the inventory is not touched at all. So a sudden server crash costs nobody anything: on the first login after the restart everything comes back where it was.
freeze-hungertrueNo hunger while waiting.
Antibot
A bot attack does not try passwords: it opens connections. Three hundred fake clients a second, each with an invented name, each taking a slot and a database row. The server does not fall over from gameplay load: it falls over because every connection costs a read and a write.
antibot7 keysenabledtruemax-connections-per-ip3How many simultaneous connections from one address. Three lets a family with three brothers through and stops a botnet on a single address.
IF YOU ARE BEHIND A PROXY WITHOUT REAL-ADDRESS FORWARDING, this number applies to THE WHOLE SERVER AT ONCE: read the "proxy" chapter below. The plugin notices by itself and tells you in the console.
max-registrations-per-ip2How many registrations from one address inside the window. This is the limit that matters most: creating accounts is the only thing a bot can do that REMAINS after it has gone.
registrations-window"24h"antibot.join-rate2 keysconnections8Above this many connections inside the window, the plugin works out that something is happening. Eight in five seconds is far more than a normal server sees, even a full one.
window"5s"antibot.adaptive3 keysenabledtrueStrict mode switches itself ON above the threshold above, and for a few minutes only people who already have an account get in.
Keep reading
This is the defence worth more than all the others put together, because a bot wave is one hundred per cent made of names never seen before - and your players do not even notice it happening.
It switches itself on because an attack arrives at four in the morning, and a switch you have to flip by hand gets flipped twenty minutes later.
(Part of the Premium edition. The fixed thresholds above apply to everybody, licence or not.)
duration"5m"block-registrationstrueDuring strict mode, block new registrations as well.
antibotname-pattern"^[A-Za-z0-9_]{3,16}$"The shape a name has to have. The default is exactly what Minecraft allows: 3 to 16 letters, digits and underscores.
CAREFUL if you have Bedrock players through Geyser: their names carry a prefix (usually a dot) and are longer. In that case use:
"^[.A-Za-z0-9_ ]{3,20}$"Proxy - read this if you have a BungeeCord or a Velocity in front
Behind a proxy, the address the server sees is the PROXY's, not the player's. Without forwarding configured, all your players look like they are connected from the same address, and therefore:
- the antibot's per-address limits block real players; - per-address sessions become one single session for everybody.
Keep reading
The fix is NOT in this file: it is in the proxy.
Velocity -> modern forwarding, plus paper-global.yml with
proxies.velocity.enabled: true and the right secret
BungeeCord -> ip_forward: true, and the server reachable ONLY from the
proxy (firewall!), because that forwarding is not
authenticatedThe plugin checks by itself whether many different names are arriving from one address and, if so, tells you in the console once.
proxy1 keyuse-forwarded-addresstrueUse the address the proxy declares (the player's real one). Set it to false only if you do NOT trust your forwarding: in that case everybody will look like they come from the same address. With forwarding on and this set to false the plugin knows it, and switches off everything keyed on the address: the per-address limits (they would apply to the whole server), address-bound sessions and devices remembered by address (they would let in anybody typing the name).
How people log in
login3 keysinputBOTHCHAT = /login and /register only GUI = the on-screen keypad only BOTH = both, and the player chooses
Keep reading
The keypad exists because a command typed in chat is readable by anybody looking at the screen, and a plugin cannot clear the client's history. With the GUI you click and dots appear.
A PIN is weaker than a long password and there is no point pretending otherwise: six digits is a million combinations. It holds up because attempts are counted and slowed down - at the tenth mistake the account is frozen for half an hour, and a million attempts at that rate is centuries.
login.gui3 keysopen-on-joinfalseOpen the keypad by itself on join. Off by default: somebody used to typing /login would get a window in their face.
pin-length6How many digits the PIN has. Do not go below six.
click-delay"120ms"Minimum wait between two clicks, against click bots.
loginhide-join-message-until-logintrueDo not announce "Marco joined" before Marco has typed his password: whoever joined is somebody who typed the name Marco. The message is sent after login, and if the login never happens it is not sent at all.
Storage
storage5 keystable-prefix"sl_"The table prefix. Change it if two servers share one database but must NOT share accounts.
storage.mysql7 keysenabledfalseAn external MySQL or MariaDB. It buys one thing, and it is worth it only for that: on a network, a registration made on one server counts on all the others.
(Part of the Premium edition. Without a licence the plugin stays on the local database, with all of its data, and says so in the console.)
host"127.0.0.1"port3306database"shadowlogin"username"root"password""parameters"useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf8"storagefallback-to-localtrueIf MySQL does not answer, start on the local database instead of not starting. On: a login plugin that does not start is a closed server.
storage.h21 keyauto-serverfalseLets another process open the same H2 file. Only useful if you know why you need it.
storage.pool5 keysmaximum-pool-size10minimum-idle2connection-timeout-ms10000idle-timeout-ms600000max-lifetime-ms1800000Messages
messages3 keysdefault-locale"en_US"The default language. The files live in lang/: the plugin ships en_US and it_IT, and a file you add there is picked up too.
follow-client-localetrueFollow the language each player has set in their client.
auto-updatetrueMove the language files to the new version when the plugin is updated: new texts arrive, the texts you changed stay yours, and the previous file is kept in backup/<date>/. With false the files in lang/ are never rewritten, and a text missing from them is read from the copy inside the jar.
compatibility1 keysection-color-codestrueAccept the section character as well as & in colour codes.
Integrations
(all optional, all switched off by themselves if the other piece is missing)
integrations4 keysrefuse-other-login-pluginstrueDo not start if another login plugin is installed.
Two plugins handling entry do not add up: they get in each other's way, and the result is not "more secure" but UNDEFINED. Leave this on.
integrations.shadowcontrol2 keysreportstrueSuspicious registrations and repeated attempts become a report in the panel. No password and no secret ever ends up in there: only what happened and to whom.
report-seconds60integrations.shadowessentials1 keyspawn-after-logintrueTeleport to spawn after login, and not before: before would mean showing spawn to somebody who has not yet proved they are who they say.
integrations.shadowpermissions1 keyhold-until-logintruePermissions are recomputed after login. Before that none of them apply, and not because we take them away: because there is nothing that could use them.
Diagnostics
logging2 keysconsole-attemptstrueWrite every wrong password to the console (name and address, never the password). Useful for noticing a targeted attack.
log-sqlfalseWrite every SQL statement. Only for working out a problem: it is slow and it fills the log.
metrics1 keyenabledtruebStats (https://bStats.org): how many servers use this plugin and which version they are on. What is sent, every 30 minutes: a random server id (serverUuid, not derived from anything), the plugin version, the Minecraft/Paper and Java versions, the operating system and core count, the online mode, how many players are online (a number only) and the licence tier (free/premium/creator). No player data of any kind: no names, no UUIDs, no addresses, no passwords. Nothing is sent while the plugin waits for a licence.
Keep reading
false switches off this plugin only. enabled: false in plugins/bStats/config.yml switches off every bStats plugin at once.
Languages
The texts the plugin shows to players, one file per language.
lang/
Texts and messages 2 files
lang/plugins/ShadowLogin/lang/ File notes
This file holds the TEXT. Everything else in the plugin - keys, sections, comments, commands - is English on purpose, so that a server anywhere in the world reads the same configuration. Only what a player sees is translated, and this is where it lives.
Every message the plugin shows to players, one file per language. The keys are not listed one by one: here are the files, how many texts they hold and the placeholders they use.
| File | Language | Texts |
|---|---|---|
lang/en_US.yml reference | English | 303 |
lang/it_IT.yml | Italiano | 303 |
Most used placeholders
The plugin replaces them with the right value: leave them written as they are.
<prefix><label><name><value><count><time><mode><max><ip><duration><file><date><reason><min>
Message groups
prefix1common13command4login34register5password13device19twofactor23recovery7limbo4antibot5gui14admin117license38locked6