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-based authentication requires a username, password, and CSRF token:
Required Parameters:
| Parameter | Type | Description |
|---|
| username | form | User’s username |
| password | form | User’s password |
| _csrf | form | CSRF token generated by client |
| XSRF-TOKEN | cookie | Cross-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.
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:
| Field | Description |
|---|
| name | PAT name (required) |
| description | PAT description (optional) |
| expiresAt | Expiration time (optional, omit for no expiration) |
| roles | Roles 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:
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
- Use HTTPS in Production: Always use HTTPS to protect credentials in transit
- Rotate PATs Regularly: Set expiration dates and rotate tokens periodically
- Principle of Least Privilege: Grant only the minimum required roles
- Enable 2FA: Use two-factor authentication for administrative accounts
- Audit Access: Monitor authentication logs and failed login attempts
- Secure PAT Storage: Store Personal Access Tokens securely, never in version control