Admin Role Assignment Guide
This guide explains how to assign and revoke admin privileges in Archery Apprentice. Admin roles are managed through Firebase Authentication custom claims.
Overview
Admin status is stored as a Firebase Auth custom claim:
{
"admin": true
}When a user has this claim, the app grants them access to the Admin Panel and all moderation capabilities.
Assignment Methods
Method 1: Firebase Console (Recommended)
Best for: One-off assignments, small admin team
- Open Firebase Console
- Select your project
- Navigate to Authentication → Users
- Find the user by email or UID
- Click the ⋮ menu → Edit user
- Scroll to Custom claims
- Enter:
{"admin": true} - Click Save
┌─────────────────────────────────────────────────────────────────┐
│ Firebase Console │
│ │
│ Authentication > Users │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Email │ UID │ Actions │ │
│ ├────────────────────┼──────────────────┼─────────────────────┤ │
│ │ admin@example.com │ abc123... │ ⋮ → Edit user │ │
│ └────────────────────┴──────────────────┴─────────────────────┘ │
│ │
│ Edit User │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Custom claims: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ {"admin": true} │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ [Save] │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Method 2: Firebase Admin SDK (Programmatic)
Best for: Automated assignment, CI/CD workflows
// Node.js with Firebase Admin SDK
const admin = require('firebase-admin');
admin.initializeApp();
async function setAdminClaim(uid) {
await admin.auth().setCustomUserClaims(uid, { admin: true });
console.log(`Admin claim set for user ${uid}`);
}
// Usage
setAdminClaim('firebase-uid-of-user');Method 3: Cloud Function (Self-Service with Approval)
Best for: Larger organizations with approval workflows
// Cloud Function for admin role requests
exports.requestAdminRole = functions.https.onCall(async (data, context) => {
// Log request for manual approval
await admin.firestore().collection('admin_requests').add({
requesterId: context.auth.uid,
requestedAt: admin.firestore.FieldValue.serverTimestamp(),
reason: data.reason,
status: 'pending'
});
return { message: 'Request submitted for approval' };
});
// Separate function for approval (manual trigger)
exports.approveAdminRequest = functions.https.onCall(async (data, context) => {
// Verify caller is existing admin
if (!context.auth.token.admin) {
throw new functions.https.HttpsError('permission-denied', 'Not an admin');
}
await admin.auth().setCustomUserClaims(data.targetUid, { admin: true });
// Update request status
await admin.firestore()
.collection('admin_requests')
.doc(data.requestId)
.update({ status: 'approved', approvedBy: context.auth.uid });
return { message: 'Admin role granted' };
});Revocation
Via Firebase Console
- Navigate to Authentication → Users
- Find the user
- Click ⋮ → Edit user
- Change custom claims to:
{"admin": false}or clear the field - Click Save
Via Admin SDK
async function revokeAdminClaim(uid) {
await admin.auth().setCustomUserClaims(uid, { admin: false });
console.log(`Admin claim revoked for user ${uid}`);
}Claim Propagation
Token Refresh Required
Custom claims are embedded in the ID token. After changing claims:
- User must sign out and sign in again, OR
- App must force token refresh:
user.getIdToken(forceRefresh: true)
App-Side Refresh
Android:
val tokenResult = auth.currentUser?.getIdToken(true)?.await()
val isAdmin = tokenResult?.claims?.get("admin") == trueiOS:
let tokenResult = try await user.getIDTokenResult(forcingRefresh: true)
let isAdmin = tokenResult.claims["admin"] as? Bool ?? falseVerification
Check Current Claims (Console)
- Go to Authentication → Users
- Find user and click ⋮ → Edit user
- View Custom claims field
Check via Admin SDK
async function checkAdminStatus(uid) {
const user = await admin.auth().getUser(uid);
const isAdmin = user.customClaims?.admin === true;
console.log(`User ${uid} is admin: ${isAdmin}`);
return isAdmin;
}Check via REST API
curl -X POST \
"https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"idToken": "USER_ID_TOKEN"}'Security Considerations
Never Trust Client-Side Only
Always verify admin status server-side (Firestore security rules):
function isAdmin() {
return request.auth != null
&& request.auth.token.admin == true;
}
match /admin_audit_logs/{logId} {
allow read, create: if isAdmin();
}Limit Admin Count
Keep the admin team small to:
- Reduce attack surface
- Simplify access control
- Maintain accountability
Audit Admin Changes
Log all admin role changes:
exports.onAdminClaimChange = functions.auth.user().beforeCreate(async (user) => {
// This doesn't catch claim updates, but logs new users
});
// Better: Create an admin when setting claim
async function setAdminClaimWithAudit(uid, grantedBy) {
await admin.auth().setCustomUserClaims(uid, { admin: true });
await admin.firestore().collection('admin_role_changes').add({
targetUid: uid,
grantedBy: grantedBy,
action: 'grant',
timestamp: admin.firestore.FieldValue.serverTimestamp()
});
}Troubleshooting
”Access Denied” After Setting Claim
Cause: Token not refreshed after claim change
Solution:
- User signs out and signs back in, OR
- Force token refresh in app:
auth.currentUser?.getIdToken(true)
Claim Not Showing in Console
Cause: Console may cache user data
Solution:
- Refresh the Firebase Console page
- Wait a few seconds and check again
Admin Status Lost After Sign Out
Cause: Claims are stored in Firebase Auth, not locally
Solution: This is expected behavior. Claims persist in Firebase and are reloaded on sign in.
Best Practices
| Practice | Reason |
|---|---|
| Use Firebase Console for small teams | Simple, no code required |
| Document all admin assignments | Accountability |
| Review admin list periodically | Remove stale accounts |
| Use strong passwords + 2FA | Protect admin accounts |
| Test in development first | Verify claim propagation |
Related Documentation
- global-admin-system - Admin system architecture
- admin-audit-trail - Audit trail for admin actions
- 2025-12-30-phase5-admin-system - Phase 5 session notes