--- title: Users and permissions description: Sign administrators in with JWT and cookie auth, declare permissions and navigation, and manage administrators from the console. section: backend order: 50 --- # Users and permissions The admin keeps WinterCMS's backend user model: the `backend_users` and `backend_user_roles` tables, roles with permission grants, and superusers who pass every check. [cabana](../../modules/cabana/README.md) signs administrators in and checks their permissions; the tables are created by the framework migrations that `migrate` runs. ## Signing in `POST /api/v1/auth/login` checks the login and password against `backend_users` and issues a JWT for the admin audience, signed with `admin.jwt.secret`. Login attempts are throttled per `admin.login.max_attempts` and `admin.login.decay_minutes`. `POST .../auth/refresh` reissues a token inside the refresh window, and `POST .../auth/logout` revokes the current token by blacklisting its ID in `backend_jwt_blacklist`. The admin API accepts the token two ways: - API clients send `Authorization: Bearer `. - The admin SPA sends `X-Requested-With: XMLHttpRequest` and receives the token in an HttpOnly, SameSite=Strict cookie. A cookie-authenticated request that changes state must carry that header, which blocks cross-site request forgery. The guard is registered in [bouncer](../../modules/bouncer/README.md) under the name `backend` and is the middleware of every admin route except login, refresh and the language bundle. See [Authentication](../services/authentication.md) for tokens and guards in general. The admin keys go in `config/admin.yaml`, with the secret in the environment (`SUMMER_ADMIN__JWT__SECRET`): ```yaml jwt: ttl: 60 refresh_ttl: 20160 password: bcrypt_cost: 12 login: max_attempts: 5 decay_minutes: 1 ``` The cookie carries the Secure attribute. `backend.cookie_secure: false` drops it for plain-HTTP development and is refused in the `production` environment. ## Permissions A plugin declares its permissions with `pact.HasPermissions`, the Go form of `registerPermissions`, and its menu entries with `pact.HasNavigation`: ```go src=modules/cabana/example_controller_test.go#BlogPlugin.Permissions // Permissions replaces registerPermissions(). func (p *BlogPlugin) Permissions() []pact.Permission { return []pact.Permission{ {Code: "acme.blog.access_posts", Tab: "acme.blog::lang.plugin.name", Label: "acme.blog::lang.permissions.posts"}, {Code: "acme.blog.access_settings", Tab: "acme.blog::lang.plugin.name", Label: "acme.blog::lang.permissions.settings"}, } } ``` ```go src=modules/cabana/example_controller_test.go#BlogPlugin.Navigation // Navigation replaces registerNavigation(). func (p *BlogPlugin) Navigation() []pact.NavigationItem { return []pact.NavigationItem{{ Code: "blog", Label: "acme.blog::lang.plugin.name", Icon: "icon-pencil", Permissions: []string{"acme.blog.access_posts"}, Controller: "acme.blog.posts", SideMenu: []pact.NavigationItem{ {Code: "posts", Label: "acme.blog::lang.posts.title", Controller: "acme.blog.posts"}, }, }} } ``` A controller's `pact.AdminPermissioned.RequiredPermissions` are checked before any schema is served or query runs, and navigation and settings entries are filtered by the permissions they name, so an administrator sees only what they may open. `cabana.Allows` is the check: superusers pass, a grant ending in `.*` matches every code with that prefix, and an empty requirement list allows any signed-in administrator. The last lines of the activation example on [Admin controllers](admin-controllers.md) show it. Actions registered through `pact.HasAdminActions` may name extra permissions, checked on top of the controller's. ## Managing administrators The application binary has two commands for operators: ```sh ./bin/acme admin:create --email admin@example.com --password '' --superuser ./bin/acme admin:reset-password admin@example.com --password '' ``` `admin:create` creates an activated administrator; `--login` defaults to the lower-cased email and `--role ` assigns a role. `admin:reset-password` takes a login or an email, sets the password and revokes every token issued before the reset. Passwords are hashed with bcrypt at `admin.password.bcrypt_cost`, so hashes copied from a WinterCMS database keep working. > [!TIP] > Pass the password through an environment variable or a prompt of your shell rather than typing it on the command line, where it stays in the shell history.