Skip to main content
Halo provides a comprehensive authentication and authorization system based on Role-Based Access Control (RBAC). This guide covers the authentication mechanisms, user management, and permission system.

Authentication Methods

Halo supports multiple authentication methods to access its APIs:

Basic Authentication

The simplest authentication method using HTTP Basic Auth headers.
# Using username and password directly
curl -u "admin:P@88w0rd" -H "Accept: application/json" \
  http://localhost:8090/api/v1alpha1/users

# Using base64 encoded credentials
echo -n "admin:P@88w0rd" | base64
# Output: YWRtaW46UEA4OHcwcmQ=

curl -H "Authorization: Basic YWRtaW46UEA4OHcwcmQ=" \
  -H "Accept: application/json" \
  http://localhost:8090/api/v1alpha1/users
Basic authentication sends credentials with every request. Use it only over HTTPS in production environments.

Form Login

Form-based authentication requires a username, password, and CSRF token: Required Parameters:
ParameterTypeDescription
usernameformUser’s username
passwordformUser’s password
_csrfformCSRF token generated by client
XSRF-TOKENcookieCross-site request forgery token (same as _csrf)
Example Request:
curl 'http://localhost:8090/login' \
  -H 'Accept: application/json' \
  -H 'Cookie: XSRF-TOKEN=1ff67e0c-6f2c-4cf9-afb5-81bc1015b8e5' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-raw '_csrf=1ff67e0c-6f2c-4cf9-afb5-81bc1015b8e5&username=admin&password=P@88w0rd'
Response (HTTP 200 with Accept: application/json):
{
  "username": "admin",
  "authorities": [
    {
      "authority": "ROLE_super-role"
    }
  ],
  "accountNonExpired": true,
  "accountNonLocked": true,
  "credentialsNonExpired": true,
  "enabled": true
}
The server sets a SESSION cookie for subsequent authenticated requests.

Personal Access Token (PAT)

Personal Access Tokens provide a secure alternative to using username/password for API access.

PAT Format

PATs start with pat_ followed by a JWT token:
pat_eyJraWQiOiJabUNtcWhJX2FuaFlWQW5aRlVTS0lOckxXRFhqaEp1Nk9ZRGRtcW13Rno4...
JWT Header:
{
  "kid": "ZmCmqhI_anhYVAnZFUSKINrLWDXjhJu6OYDdmqmwFz8",
  "alg": "RS256"
}
JWT Payload:
{
  "sub": "admin",
  "roles": ["super-role"],
  "pat_name": "pat-admin-IWolQ",
  "iss": "http://localhost:8090/",
  "exp": 1694672079,
  "iat": 1694585720,
  "jti": "17ead79d-4d27-b885-6b03-38cbec412ae3"
}

Creating a PAT

Create a PAT using the API endpoint:
curl -u admin:admin -X 'POST' \
  'http://localhost:8090/apis/api.console.security.halo.run/v1alpha1/users/-/personalaccesstokens' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "spec": {
    "name": "My PAT",
    "description": "This is my first PAT.",
    "expiresAt": "2023-09-15T02:42:35.136Z",
    "roles": []
  }
}'
Request Body Fields:
FieldDescription
namePAT name (required)
descriptionPAT description (optional)
expiresAtExpiration time (optional, omit for no expiration)
rolesRoles to grant to PAT (must be subset of user’s roles, empty = anonymous only)
The PAT is only shown once in the response under metadata.annotations["security.halo.run/access-token"]. Save it securely.

Using a PAT

Include the PAT in the Authorization header:
curl http://localhost:8090/apis/api.console.halo.run/v1alpha1/users/- \
  -H "Authorization: Bearer pat_eyJraWQiOiJabUNtcWhJX2FuaFlWQW5a..."

Role-Based Access Control (RBAC)

Halo implements a Kubernetes-style RBAC system with three core extension types:

User Extension

The User extension represents user accounts in Halo. Location: run.halo.app.core.extension.User at api/src/main/java/run/halo/app/core/extension/User.java:1
apiVersion: v1alpha1
kind: User
metadata:
  name: john-doe
  annotations:
    rbac.authorization.halo.run/role-names: "[\"contributor\",\"editor\"]"
spec:
  displayName: "John Doe"
  email: "[email protected]"
  emailVerified: true
  avatar: "https://example.com/avatar.jpg"
  bio: "Content writer and editor"
  disabled: false
  twoFactorAuthEnabled: false
Key Fields:
  • spec.displayName: User’s display name
  • spec.email: User’s email address
  • spec.emailVerified: Whether email is verified
  • spec.password: Encrypted password
  • spec.disabled: Account disabled status
  • spec.twoFactorAuthEnabled: 2FA status

Role Extension

Defines a set of permissions using policy rules. Location: run.halo.app.core.extension.Role at api/src/main/java/run/halo/app/core/extension/Role.java:1
apiVersion: v1alpha1
kind: Role
metadata:
  name: post-editor
  labels:
    rbac.authorization.halo.run/aggregate-to-editor: "true"
rules:
  - apiGroups: ["content.halo.run"]
    resources: ["posts"]
    verbs: ["get", "list", "create", "update", "delete"]
  - apiGroups: ["content.halo.run"]
    resources: ["posts/publish"]
    verbs: ["create"]
PolicyRule Structure:
  • apiGroups: API groups the rule applies to (e.g., content.halo.run)
  • resources: Resource types (e.g., posts, categories)
  • resourceNames: Specific resource names (optional, empty = all)
  • nonResourceURLs: URL paths for non-resource requests
  • verbs: Allowed operations (get, list, create, update, delete, patch)
Example: Creating a Custom Role
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.Role.PolicyRule;

public Role createCustomRole() {
    var role = new Role();
    var metadata = new Metadata();
    metadata.setName("comment-moderator");
    role.setMetadata(metadata);
    
    var rules = new ArrayList<PolicyRule>();
    
    // Allow reading and updating comments
    var commentRule = new PolicyRule.Builder()
        .apiGroups("content.halo.run")
        .resources("comments")
        .verbs("get", "list", "update")
        .build();
    
    // Allow approving comments
    var approveRule = new PolicyRule.Builder()
        .apiGroups("content.halo.run")
        .resources("comments/approve")
        .verbs("create")
        .build();
    
    rules.add(commentRule);
    rules.add(approveRule);
    role.setRules(rules);
    
    return role;
}

RoleBinding Extension

Binds roles to users or groups. Location: run.halo.app.core.extension.RoleBinding at api/src/main/java/run/halo/app/core/extension/RoleBinding.java:1
apiVersion: v1alpha1
kind: RoleBinding
metadata:
  name: john-doe-post-editor-binding
subjects:
  - kind: User
    name: john-doe
    apiGroup: ""
roleRef:
  kind: Role
  name: post-editor
  apiGroup: ""
Creating RoleBindings Programmatically:
import run.halo.app.core.extension.RoleBinding;

// Simple method
var binding = RoleBinding.create("john-doe", "post-editor");

// Custom method
var customBinding = new RoleBinding();
var metadata = new Metadata();
metadata.setName("custom-binding");
customBinding.setMetadata(metadata);

// Set role reference
var roleRef = new RoleBinding.RoleRef();
roleRef.setKind("Role");
roleRef.setName("custom-role");
roleRef.setApiGroup("");
customBinding.setRoleRef(roleRef);

// Add subjects
var subject = new RoleBinding.Subject("User", "john-doe", "");
customBinding.setSubjects(List.of(subject));

Authentication Extension Points

Halo provides extension points for custom authentication logic:

FormLoginSecurityWebFilter

Extend form login authentication:
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.FormLoginSecurityWebFilter;

@Component
public class CustomFormLoginFilter implements FormLoginSecurityWebFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // Add custom logic before form login
        // e.g., rate limiting, custom validation
        
        return chain.filter(exchange);
    }
}

AuthenticationSecurityWebFilter

Extend general authentication:
import org.springframework.stereotype.Component;
import run.halo.app.security.AuthenticationSecurityWebFilter;

@Component
public class CustomAuthFilter implements AuthenticationSecurityWebFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // Implement custom authentication
        // e.g., API key authentication, OAuth2
        
        return chain.filter(exchange);
    }
}

AfterSecurityWebFilter

Post-authentication processing:
import run.halo.app.security.AfterSecurityWebFilter;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;

@Component
public class PostAuthFilter implements AfterSecurityWebFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return ReactiveSecurityContextHolder.getContext()
            .flatMap(securityContext -> {
                // Access authenticated user
                var authentication = securityContext.getAuthentication();
                // Log user activity, update last login, etc.
                
                return chain.filter(exchange);
            })
            .switchIfEmpty(chain.filter(exchange));
    }
}
Always call chain.filter(exchange) if the request doesn’t match your authentication criteria to give other filters a chance to process it.

Security Best Practices

  1. Use HTTPS in Production: Always use HTTPS to protect credentials in transit
  2. Rotate PATs Regularly: Set expiration dates and rotate tokens periodically
  3. Principle of Least Privilege: Grant only the minimum required roles
  4. Enable 2FA: Use two-factor authentication for administrative accounts
  5. Audit Access: Monitor authentication logs and failed login attempts
  6. Secure PAT Storage: Store Personal Access Tokens securely, never in version control

Build docs developers (and LLMs) love