Lesson 7: Refresh Tokens & Role-Based Access Control (RBAC) in .NET with Firebase

0
49

Lesson 7: Refresh Tokens & Role-Based Access Control (RBAC) in .NET with Firebase

Meta Description

Learn how to implement refresh tokens and role-based access control (RBAC) in a .NET backend using Firebase authentication. This tutorial covers secure token handling, refreshing JWTs, and enforcing user roles.


What Are Refresh Tokens?

A refresh token allows users to obtain a new access token without requiring them to log in again. Access tokens usually have a short lifespan for security reasons, while refresh tokens provide a way to maintain authenticated sessions.

How Refresh Tokens Work

  1. User logs in β†’ Receives an access token and a refresh token.
  2. When the access token expires, the client sends the refresh token to request a new access token.
  3. The server validates the refresh token and issues a new access token.

Step 1: Update FirebaseRequest.cs to Support Refresh Tokens

Modify FirebaseRequest.cs to store the refresh token.

namespace CGZAPI.Models
{
    public class FirebaseRequest
    {
        public object FirebaseJson { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
        public string Token { get; set; }
        public bool IsEncrypted { get; set; }
        public string FirebaseProjectId { get; set; }
        public string ApiKey { get; set; }
        public string RefreshToken { get; set; } // πŸ”Ή New field for refresh token
    }
}

Explanation:

  • RefreshToken: Stores the refresh token for requesting new access tokens.

Step 2: Implement Refresh Token Handling in AuthController.cs

Modify AuthController.cs to Include a Refresh Token Endpoint

[HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken([FromBody] FirebaseRequest request)
{
    if (string.IsNullOrEmpty(request.RefreshToken) || string.IsNullOrEmpty(request.ApiKey))
    {
        return BadRequest("Refresh Token and API Key are required.");
    }

    using var httpClient = new HttpClient();
    var refreshUrl = $"https://securetoken.googleapis.com/v1/token?key={request.ApiKey}";
    
    var refreshData = new
    {
        grant_type = "refresh_token",
        refresh_token = request.RefreshToken
    };
    
    var response = await httpClient.PostAsJsonAsync(refreshUrl, refreshData);
    if (!response.IsSuccessStatusCode)
    {
        return BadRequest("Invalid refresh token.");
    }

    var jsonResponse = await response.Content.ReadAsStringAsync();
    var result = System.Text.Json.JsonDocument.Parse(jsonResponse).RootElement;

    if (!result.TryGetProperty("id_token", out var newToken) || !result.TryGetProperty("refresh_token", out var newRefreshToken))
    {
        return BadRequest("Invalid Firebase response.");
    }

    return Ok(new
    {
        message = "Token refreshed successfully",
        accessToken = newToken.GetString(),
        refreshToken = newRefreshToken.GetString()
    });
}

Explanation:

  • Receives a refresh token from the client.
  • Requests a new access token from Firebase.
  • Returns the new access and refresh tokens to the client.

Step 3: How to Store & Use Refresh Tokens for Persistent Login

Where to Store Refresh Tokens?

βœ… Web Apps (React, Vue, etc.)

  • Best Option: Secure HTTP-Only Cookies (Prevents XSS attacks)
  • Alternative: Local Storage (Less secure)
document.cookie = `refreshToken=${refreshToken}; Secure; HttpOnly; path=/`;

βœ… Mobile Apps (React Native, Unity, Android, iOS)

  • Best Option: Encrypted Storage
    • iOS β†’ Keychain
    • Android β†’ Encrypted Shared Preferences
    • React Native β†’ AsyncStorage with encryption
    • Unity β†’ Secure PlayerPrefs or local file storage with AES encryption
import * as SecureStore from 'expo-secure-store';
async function saveToken(refreshToken) {
  await SecureStore.setItemAsync("refreshToken", refreshToken);
}
async function getToken() {
  return await SecureStore.getItemAsync("refreshToken");
}

Auto Login Flow

  1. On App Startup: Check if the refresh token exists.
  2. If the refresh token is found: Send it to the backend (/api/auth/refresh-token) to get a new access token.
  3. If refresh token fails (e.g., expired), ask the user to log in again.
async function autoLogin() {
  const refreshToken = await getToken();
  if (!refreshToken) {
    console.log("No refresh token found. Redirecting to login.");
    return;
  }

  const response = await fetch("https://your-api.com/api/auth/refresh-token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken, apiKey: "YOUR_FIREBASE_API_KEY" }),
  });

  const data = await response.json();
  if (response.ok) {
    console.log("Auto-login successful!");
    saveAccessToken(data.accessToken);
  } else {
    console.log("Refresh token expired, user needs to log in again.");
  }
}

Step 4: Role-Based Access Control (RBAC) with Firebase Custom Claims

What Is RBAC?

Role-Based Access Control (RBAC) allows restricting API access based on user roles like admin, moderator, and user. Firebase custom claims help define roles at the authentication level.

Assigning Roles to Users

To assign roles, we use Firebase Admin SDK to set custom claims.

Modify FirebaseManager.cs to Add Role Management

public static async Task AssignUserRole(string uid, string role)
{
    var claims = new Dictionary<string, object>
    {
        { "role", role }
    };
    await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(uid, claims);
}

Create an Endpoint to Assign Roles in AuthController.cs

[HttpPost("assign-role")]
public async Task<IActionResult> AssignRole([FromBody] RoleAssignmentRequest request)
{
    try
    {
        await FirebaseManager.AssignUserRole(request.UserId, request.Role);
        return Ok(new { message = $"Role {request.Role} assigned successfully." });
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { error = "Failed to assign role", details = ex.Message });
    }
}

RoleAssignmentRequest.cs Model

public class RoleAssignmentRequest
{
    public string UserId { get; set; }
    public string Role { get; set; }
}

Protecting Routes Based on Roles

Now that we assign roles, let’s protect endpoints based on them.

Modify AuthController.cs to Add a Protected Route

[Authorize]
[HttpGet("admin-only")]
public IActionResult AdminOnly()
{
    var roleClaim = User.FindFirst("role")?.Value;
    if (roleClaim != "admin")
    {
        return Forbid();
    }
    return Ok(new { message = "Welcome Admin!" });
}

Explanation:

  • Only users with an admin role can access this endpoint.
  • If the role is missing or invalid, it returns 403 Forbidden.

Step 5: Testing in Postman

1️⃣ Register a User & Get a Refresh Token

πŸ“Œ Endpoint: POST /api/auth/register

{
  "email": "testuser@example.com",
  "password": "Test@1234",
  "firebaseJson": "{ \"type\": \"service_account\" }",
  "apiKey": "YOUR_FIREBASE_API_KEY"
}

βœ… Response:

{
  "message": "User registered successfully",
  "userId": "some-user-id"
}

2️⃣ Request a New Access Token Using Refresh Token

πŸ“Œ Endpoint: POST /api/auth/refresh-token

{
  "refreshToken": "YOUR_REFRESH_TOKEN",
  "apiKey": "YOUR_FIREBASE_API_KEY"
}

βœ… Response:

{
  "accessToken": "NEW_ACCESS_TOKEN",
  "refreshToken": "NEW_REFRESH_TOKEN"
}

3️⃣ Assign an Admin Role to a User

πŸ“Œ Endpoint: POST /api/auth/assign-role

{
  "userId": "some-user-id",
  "role": "admin"
}

4️⃣ Access a Protected Admin Route

πŸ“Œ Endpoint: GET /api/auth/admin-only Headers:

Authorization: Bearer YOUR_ACCESS_TOKEN

βœ… If the role is admin, response is:

{
  "message": "Welcome Admin!"
}

Next Steps

πŸš€ Lesson 8 β†’ Adding Google, Facebook, Apple, and AWS Authentication

Let me know if you have any questions! βœ