Add Deployment
This commit is contained in:
@@ -0,0 +1,785 @@
|
||||
|
||||
# AUTHENTICATION METHODS FOR ASP.NET CORE WEB API
|
||||
|
||||
### 1. **JWT (JSON Web Token) Authentication**
|
||||
|
||||
**Most common for SPAs (like your React frontend)**
|
||||
|
||||
- ✅ **Stateless**: No session stored on the server.
|
||||
|
||||
- 🔐 **Token structure**: Header + Payload (including roles) + Signature.
|
||||
|
||||
- 🎯 Good for: SPAs, mobile apps, APIs.
|
||||
|
||||
- 📦 Libraries: `Microsoft.AspNetCore.Authentication.JwtBearer`.
|
||||
|
||||
|
||||
**Setup Highlights:**
|
||||
|
||||
- Client sends credentials → API generates token with user roles → Client stores token → Client sends token in `Authorization` header (`Bearer <token>`).
|
||||
|
||||
- Server verifies signature and extracts user identity and roles from the token.
|
||||
|
||||
|
||||
**Role support**:
|
||||
|
||||
- You embed roles inside the JWT (`"roles": ["Admin", "User"]`).
|
||||
|
||||
- Use `[Authorize(Roles = "Admin")]`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 2. **Cookie-Based Authentication**
|
||||
|
||||
**Traditional approach for server-rendered apps (not ideal for APIs)**
|
||||
|
||||
- 🛑 **Not recommended for APIs** due to CSRF vulnerability and session overhead.
|
||||
|
||||
- 🗂 Stores session identifier in browser cookie.
|
||||
|
||||
- Useful when using ASP.NET Core MVC or Razor Pages (not Web API).
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3. **OAuth2 + OpenID Connect (OIDC)**
|
||||
|
||||
**Best for federated login, single sign-on (SSO), or external providers**
|
||||
|
||||
- 🔗 Integrates with Identity Providers (IDPs) like:
|
||||
|
||||
- Azure AD
|
||||
|
||||
- Google, Facebook, GitHub
|
||||
|
||||
- Auth0, Okta, Duende IdentityServer
|
||||
|
||||
- 📦 Library: `Microsoft.AspNetCore.Authentication.OpenIdConnect`
|
||||
|
||||
- Uses **access tokens** (JWT) issued by an authority.
|
||||
|
||||
|
||||
**Role support**:
|
||||
|
||||
- Roles/claims provided by the Identity Provider.
|
||||
|
||||
- You map these claims to roles in the API.
|
||||
|
||||
- `[Authorize(Roles = "Admin")]` still works.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 4. **API Key Authentication**
|
||||
|
||||
**Lightweight alternative (not ideal for user-based roles)**
|
||||
|
||||
- Client includes a static API key in headers or query string.
|
||||
|
||||
- 🔐 No user context → ❌ no role-based support unless you map API keys to roles in a custom way.
|
||||
|
||||
- 🔧 Implemented manually in middleware or filters.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 5. **Basic Authentication**
|
||||
|
||||
- User provides `username:password` in Base64 via `Authorization` header.
|
||||
|
||||
- ❌ Insecure unless used with HTTPS.
|
||||
|
||||
- ⚠️ Rarely used anymore — not good for role-based systems or production apps.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 6. **ASP.NET Core Identity**
|
||||
|
||||
**Full-featured user management system (often combined with JWT)**
|
||||
|
||||
- ✅ Provides login, registration, role management, password hashing, etc.
|
||||
|
||||
- 🎯 Good choice if you want to **own the user system** and **manage roles** yourself.
|
||||
|
||||
- Can be used with:
|
||||
|
||||
- JWT tokens (custom token generation)
|
||||
|
||||
- Cookie auth (not for APIs)
|
||||
|
||||
- 🔧 Use `UserManager`, `RoleManager`.
|
||||
|
||||
|
||||
**Example**: Use Identity for creating users and roles, then issue JWTs on login.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Summary Table
|
||||
|
||||
|Method|Stateless|Token-Based|Role Support|Ideal For|
|
||||
|---|---|---|---|---|
|
||||
|JWT Authentication|✅|✅|✅|APIs, SPAs (React, etc.)|
|
||||
|Cookie Authentication|❌|❌|✅|Server-side apps only|
|
||||
|OAuth2 + OpenID Connect|✅|✅|✅|External login, SSO, enterprise|
|
||||
|API Key|✅|❌|❌ (manual)|Simple apps, service-to-service|
|
||||
|Basic Auth|✅|❌|❌ (manual)|Very basic use, not recommended|
|
||||
|ASP.NET Core Identity|❌|Optional|✅|User/Role management|
|
||||
|
||||
---
|
||||
|
||||
# Web Security and JWT Terms
|
||||
|
||||
|
||||
### 🧨 **CSP (Content Security Policy)**
|
||||
|
||||
**Definition:**
|
||||
A **browser security mechanism** that helps prevent **XSS attacks** by controlling which sources the browser can load content from.
|
||||
|
||||
**Example Usage:**
|
||||
It can prevent JavaScript from running unless it's from a trusted source.
|
||||
|
||||
**Example CSP header:**
|
||||
|
||||
```http
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' https://trustedscripts.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💥 **XSS (Cross-Site Scripting)**
|
||||
|
||||
**Definition:**
|
||||
A vulnerability where an attacker **injects malicious JavaScript** into a web page that gets executed in a user’s browser.
|
||||
|
||||
**Why It Matters for JWT:**
|
||||
If you store JWTs in `localStorage`, and your app is vulnerable to XSS, the attacker can steal the token and impersonate the user.
|
||||
|
||||
**Prevention:**
|
||||
|
||||
- Never trust user input (sanitize!)
|
||||
|
||||
- Use **CSP headers**
|
||||
|
||||
- Avoid inline scripts
|
||||
|
||||
- Use frameworks that auto-escape HTML (like React, Razor)
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 📦 **Payload (in JWT)**
|
||||
|
||||
**Definition:**
|
||||
The **middle part** of a JWT. It contains the **claims** (user data, permissions, roles, etc.) in a **Base64-encoded JSON** format.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"role": "Admin",
|
||||
"exp": 1716850984
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **Note:** Payload is **not encrypted**, just encoded — anyone can read it, but not modify it without invalidating the signature.
|
||||
|
||||
---
|
||||
|
||||
### ✍️ **Signature (in JWT)**
|
||||
|
||||
**Definition:**
|
||||
The **third part** of the token. It's a cryptographic hash (HMAC or RSA/ECDSA) of the header and payload, signed with a **secret** or private key.
|
||||
|
||||
**Purpose:**
|
||||
To verify that the token **hasn’t been tampered with**.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
JWT = base64(header) + '.' + base64(payload) + '.' + signature
|
||||
```
|
||||
|
||||
Only the server (with the secret key) can validate the signature.
|
||||
|
||||
---
|
||||
|
||||
### 🦠 **CSRF (Cross-Site Request Forgery)**
|
||||
|
||||
**Definition:**
|
||||
A vulnerability where an attacker tricks a user’s **browser** (with a valid session/cookie) into making an unwanted request to your site **without the user’s knowledge**.
|
||||
|
||||
**Example:**
|
||||
If you're logged in and visit a malicious site, that site may submit a POST request using your cookies to perform actions on your behalf.
|
||||
|
||||
**Why It Matters:**
|
||||
|
||||
- If you store JWTs in **HttpOnly cookies**, you must guard against CSRF.
|
||||
|
||||
|
||||
**Defense:**
|
||||
|
||||
- Use `SameSite=Strict` cookies
|
||||
|
||||
- Use anti-CSRF tokens
|
||||
|
||||
- Prefer `Authorization` header with tokens (doesn’t auto-send like cookies)
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🧠 **Session Overhead**
|
||||
|
||||
**Definition:**
|
||||
The **memory and server resource cost** of maintaining a session for each logged-in user on the server.
|
||||
|
||||
**Why It Matters:**
|
||||
|
||||
- Traditional **cookie-based auth** stores session info on the server.
|
||||
|
||||
- With JWT, the session is **stateless** (no server memory), reducing overhead and scaling better.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔑 **Authentication Ecosystem Concepts**
|
||||
|
||||
---
|
||||
|
||||
### 🌐 **Federated Login**
|
||||
|
||||
**Definition:**
|
||||
Users log in to your application using **another trusted identity provider** (IdP), such as:
|
||||
|
||||
- Google
|
||||
|
||||
- Facebook
|
||||
|
||||
- Microsoft
|
||||
|
||||
- GitHub
|
||||
|
||||
|
||||
You delegate authentication to the third party and just receive user info (often as a JWT or OpenID Connect token).
|
||||
|
||||
**Protocol Examples:**
|
||||
|
||||
- OAuth2
|
||||
|
||||
- OpenID Connect
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🔁 **SSO (Single Sign-On)**
|
||||
|
||||
**Definition:**
|
||||
A user logs in **once** and gains access to **multiple systems or applications** without logging in again.
|
||||
|
||||
**Common in Enterprises**:
|
||||
|
||||
- Logging into one dashboard gives you access to HR system, email, file storage, etc.
|
||||
|
||||
|
||||
**How It Works:**
|
||||
|
||||
- A **central identity provider (IdP)** issues tokens
|
||||
|
||||
- Applications **trust the token** and skip login
|
||||
|
||||
- Often uses **OAuth2**, **OpenID Connect**, or **SAML**
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Summary Table
|
||||
|
||||
|Term|Meaning|
|
||||
|---|---|
|
||||
|**CSP**|Restricts sources of scripts/styles to prevent XSS|
|
||||
|**XSS**|Injected JavaScript that steals data like JWT tokens|
|
||||
|**JWT Payload**|JSON with user claims (readable but not secure on its own)|
|
||||
|**JWT Signature**|Proves the token is untampered (signed by server)|
|
||||
|**CSRF**|Unauthorized actions using authenticated user's browser/session|
|
||||
|**Session Overhead**|Cost of storing user sessions in memory on the server|
|
||||
|**Federated Login**|Login using a third-party identity provider (e.g., Google)|
|
||||
|**SSO**|One login grants access to multiple trusted systems|
|
||||
|
||||
|
||||
# ASP.NET Identity vs JWT Authentication
|
||||
|
||||
---
|
||||
|
||||
### ✅ **What is ASP.NET Identity?**
|
||||
|
||||
**ASP.NET Identity** is a full **membership system** that:
|
||||
|
||||
- Manages users, roles, passwords, claims, tokens, and external logins.
|
||||
|
||||
- Stores user data in a **database** (usually using Entity Framework).
|
||||
|
||||
- Works well with **cookie-based authentication** by default.
|
||||
|
||||
- Handles login, logout, registration, password hashing, email confirmation, two-factor auth, etc.
|
||||
|
||||
|
||||
**Default storage:** SQL Server (via Entity Framework)
|
||||
|
||||
---
|
||||
|
||||
### 🔑 **How Authentication Works in ASP.NET Identity (Cookie-Based)**
|
||||
|
||||
1. **Login request**: The user submits a form with username/password.
|
||||
|
||||
2. **Server validates** the credentials using ASP.NET Identity.
|
||||
|
||||
3. If valid, the server issues a **cookie** (with a session token).
|
||||
|
||||
4. The browser stores this **authentication cookie**.
|
||||
|
||||
5. For future requests, the browser **automatically sends the cookie**.
|
||||
|
||||
6. The server validates the cookie (and reads the user session from memory or database).
|
||||
|
||||
|
||||
✅ **Stateful**
|
||||
❌ Doesn't scale well for large APIs unless you add external session storage (like Redis).
|
||||
|
||||
---
|
||||
|
||||
### 🔐 What is JWT Authentication?
|
||||
|
||||
JWT Authentication is:
|
||||
|
||||
- **Stateless**.
|
||||
|
||||
- Based on tokens — not sessions.
|
||||
|
||||
- Works well for APIs and SPAs/mobile apps.
|
||||
|
||||
|
||||
#### Flow:
|
||||
|
||||
1. **User logs in**, and if credentials are valid...
|
||||
|
||||
2. Server **creates a JWT** containing user info (e.g., roles).
|
||||
|
||||
3. Server **signs the token** and sends it to the client.
|
||||
|
||||
4. Client stores it (e.g., in localStorage or cookies).
|
||||
|
||||
5. On every request, the client sends the JWT in the `Authorization` header.
|
||||
|
||||
6. Server **verifies the JWT signature** using a secret or key.
|
||||
|
||||
7. If valid → allow access (no server-side session needed).
|
||||
|
||||
|
||||
✅ **Stateless**
|
||||
✅ Scales easily
|
||||
✅ Good for distributed APIs
|
||||
|
||||
---
|
||||
|
||||
### 📊 Comparison Table
|
||||
|
||||
|Feature|ASP.NET Identity (Cookie)|JWT Authentication (Token-Based)|
|
||||
|---|---|---|
|
||||
|**Stateful/Stateless**|Stateful|Stateless|
|
||||
|**Storage**|Cookie on client, session on server|Token on client only|
|
||||
|**Default Transport**|Cookie (auto-sent by browser)|Authorization header (manual send)|
|
||||
|**Built-in Support**|ASP.NET Identity (UI + EF Core)|ASP.NET Core + Manual JWT setup|
|
||||
|**Scalability**|Limited (server stores session)|High (no session to manage)|
|
||||
|**Security**|Cookie CSRF risk|XSS risk if stored in JS-accessible storage|
|
||||
|**Use Case**|Web apps with UI (MVC, Razor)|APIs, SPAs, mobile apps|
|
||||
|**External login support**|Built-in|Needs integration|
|
||||
|**Token expiration**|Server-controlled session|Token has expiration embedded|
|
||||
|
||||
---
|
||||
|
||||
### 🚨 Statelessness — What Does It Mean?
|
||||
|
||||
- **Stateful Authentication**: Server **stores a session** (usually in memory or a database) for each user. The client just stores a cookie with a session ID.
|
||||
|
||||
- When user logs in, server keeps a record of that.
|
||||
|
||||
- Logout → delete session.
|
||||
|
||||
- **Stateless Authentication** (JWT):
|
||||
|
||||
- Server **does not remember anything**.
|
||||
|
||||
- JWT has all info about the user in itself (claims, roles, expiry).
|
||||
|
||||
- Logout = just delete token on client side (server has no "memory").
|
||||
|
||||
|
||||
⚠️ **You can't "force logout" someone server-side in pure JWT unless you blacklist tokens manually** (or rotate secrets).
|
||||
|
||||
---
|
||||
|
||||
### 💡 Can You Use ASP.NET Identity with JWT?
|
||||
|
||||
Yes — ASP.NET Identity can be configured to:
|
||||
|
||||
- Authenticate user credentials
|
||||
|
||||
- Then issue a **JWT** instead of using a cookie
|
||||
|
||||
- This way, you get:
|
||||
|
||||
- ASP.NET Identity’s user management
|
||||
|
||||
- JWT’s **stateless** API authentication
|
||||
|
||||
|
||||
This is often used in **hybrid apps**:
|
||||
|
||||
- Use Identity for registration/login
|
||||
|
||||
- Use JWT for frontend and mobile API calls
|
||||
|
||||
|
||||
|
||||
# What does `null!` mean?
|
||||
|
||||
It's a **null-forgiving operator**.
|
||||
|
||||
When you write:
|
||||
|
||||
```csharp
|
||||
string? maybeNull = null;
|
||||
string definitelyNotNull = null!; // ← This tells the compiler: “Trust me, it’s not null.”
|
||||
```
|
||||
|
||||
You're telling the compiler:
|
||||
|
||||
> _“I know this is technically null, but treat it as non-null and don’t warn me.”_
|
||||
|
||||
---
|
||||
|
||||
### 🧩 Why is this needed?
|
||||
|
||||
C# 8 introduced **nullable reference types**, where the compiler gives warnings if you assign or use `null` where it's not safe.
|
||||
|
||||
For example:
|
||||
|
||||
```csharp
|
||||
string name = null; // Warning: assigning null to non-nullable reference
|
||||
```
|
||||
|
||||
To silence the warning, you can use `null!`:
|
||||
|
||||
```csharp
|
||||
string name = null!; // No warning
|
||||
```
|
||||
|
||||
But this is **your responsibility**: the compiler **assumes you're guaranteeing** that it won’t cause a `NullReferenceException`.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ When to use `null!`?
|
||||
|
||||
Use it **sparingly and carefully**, only when:
|
||||
|
||||
- You know an object **will be initialized later**.
|
||||
|
||||
- You’re working with **ORMs like EF Core** where properties are set **after construction**.
|
||||
|
||||
- You're dealing with **dependency injection** and the value is assigned **automatically later**.
|
||||
|
||||
|
||||
#### Example:
|
||||
|
||||
```csharp
|
||||
public class Account
|
||||
{
|
||||
public string PhoneNumber { get; set; } = null!; // EF will populate it
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ Summary
|
||||
|
||||
|Expression|Meaning|
|
||||
|---|---|
|
||||
|`null!`|"This is null, but treat it as if it's non-null (trust me)."|
|
||||
|Use case|Avoiding compiler warnings for nullable reference types|
|
||||
|Risk|Can lead to runtime `NullReferenceException` if used incorrectly|
|
||||
|
||||
---
|
||||
|
||||
Let me know if you want help turning on/off nullable reference types or dealing with EF Core model warnings.
|
||||
|
||||
|
||||
# What is a JWT Generator?
|
||||
|
||||
A **JWT Generator** is a class or service in your backend that:
|
||||
|
||||
1. **Generates** a signed JWT (JSON Web Token) when a user logs in.
|
||||
|
||||
2. **Encodes** the user's identity, roles, and other claims.
|
||||
|
||||
3. **Signs** the token with a secret or private key so it can be validated later.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Structure of a JWT
|
||||
|
||||
A JWT has 3 parts:
|
||||
|
||||
```plaintext
|
||||
xxxxx.yyyyy.zzzzz
|
||||
```
|
||||
|
||||
1. **Header** (Base64-encoded JSON):
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Payload** (Base64-encoded JSON):
|
||||
Contains user data and claims (e.g., user ID, role, expiry).
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "userId123",
|
||||
"phone": "0930xxx",
|
||||
"role": "Admin",
|
||||
"exp": 1717502800
|
||||
}
|
||||
```
|
||||
|
||||
3. **Signature**:
|
||||
HMACSHA256(header + "." + payload, secret key)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ✅ What Goes into the Payload?
|
||||
|
||||
Include things you want to check _without querying the DB every time_:
|
||||
|
||||
- `sub` (Subject – usually user ID)
|
||||
|
||||
- `phone` or username
|
||||
|
||||
- `role` (e.g., "Admin", "User")
|
||||
|
||||
- `exp` (expiration timestamp)
|
||||
|
||||
- Any custom claim, like `companyId`, `verified`, etc.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Signature
|
||||
|
||||
The **signature ensures** that the token hasn't been tampered with. If the signature doesn't match (due to modification or incorrect secret), the token is invalid.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Example: JWT Generator in ASP.NET Core
|
||||
|
||||
```csharp
|
||||
public interface IJwtGenerator
|
||||
{
|
||||
string GenerateToken(Account account);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public class JwtGenerator : IJwtGenerator
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public JwtGenerator(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string GenerateToken(Account account)
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, account.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.PhoneNumber, account.PhoneNumber),
|
||||
new Claim(ClaimTypes.Role, account.Role),
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _config["Jwt:Issuer"],
|
||||
audience: _config["Jwt:Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(2),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Configuration in `appsettings.json`
|
||||
|
||||
```json
|
||||
"Jwt": {
|
||||
"Key": "YourSuperSecureSecretKey123!",
|
||||
"Issuer": "YourAppName",
|
||||
"Audience": "YourFrontendApp"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Token Validation
|
||||
|
||||
When the frontend sends this token on requests (via `Authorization: Bearer <token>` header), ASP.NET Core automatically validates:
|
||||
|
||||
- Signature (using the same secret)
|
||||
|
||||
- Expiry (`exp`)
|
||||
|
||||
- Audience, Issuer
|
||||
|
||||
- Claims (like role)
|
||||
|
||||
|
||||
This is done via `JwtBearer` middleware.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Roles in the Token?
|
||||
|
||||
Yes, you can (and should) include user roles. The backend will **automatically enforce** `[Authorize(Roles = "Admin")]` using that claim.
|
||||
|
||||
But:
|
||||
|
||||
- Don't rely **only** on frontend logic — always **protect endpoints** on the backend too.
|
||||
|
||||
- Frontend can use roles to **hide UI elements**, but not for enforcing access.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🤔 Should You Encrypt the Token?
|
||||
|
||||
No, standard practice is:
|
||||
|
||||
- Don't encrypt JWTs — they’re just base64-encoded.
|
||||
|
||||
- **Don’t put sensitive data** inside.
|
||||
|
||||
- Sign them to prevent tampering.
|
||||
|
||||
- Secure the token in frontend (e.g., HttpOnly cookies or `localStorage` with care).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary of Responsibilities
|
||||
|
||||
|Responsibility|Backend (JWT Generator)|Frontend|
|
||||
|---|---|---|
|
||||
|Token generation|✅|❌|
|
||||
|Storing token|❌|✅ (localStorage / cookie)|
|
||||
|Sending token|❌|✅ (Authorization header)|
|
||||
|Token validation|✅ (`JwtBearer`)|❌|
|
||||
|Role enforcement|✅ (`[Authorize]`)|✅ (UI-level only)|
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
# How multiple roles in JWT claims work
|
||||
|
||||
- The JWT claims are basically a list of key-value pairs.
|
||||
|
||||
- For roles, the key is usually `"role"` or `ClaimTypes.Role`.
|
||||
|
||||
- You add **multiple claims with the same key** — one for each role.
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"phone_number": "123456789",
|
||||
"role": "Admin",
|
||||
"role": "Editor",
|
||||
"role": "User",
|
||||
"exp": 1711600000
|
||||
}
|
||||
```
|
||||
|
||||
- When the token is created, it contains multiple `"role"` entries.
|
||||
|
||||
- On the backend, ASP.NET Core `ClaimsPrincipal` reads all of them, so `[Authorize(Roles="Admin,Editor")]` works by checking if **any** of the roles match.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### How frontend handles multiple roles in the JWT
|
||||
|
||||
1. The frontend receives the JWT (usually after login).
|
||||
|
||||
2. It **decodes** the JWT payload (using a library like `jwt-decode`).
|
||||
|
||||
3. It extracts the roles as an **array of strings**.
|
||||
|
||||
|
||||
Example in React using `jwt-decode`:
|
||||
|
||||
```js
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const decoded = jwtDecode(token);
|
||||
const roles = decoded.role; // roles is usually an array if multiple roles exist
|
||||
|
||||
console.log(roles); // ["Admin", "Editor", "User"]
|
||||
```
|
||||
|
||||
4. The frontend can then use these roles to:
|
||||
|
||||
- Conditionally render UI components or routes.
|
||||
|
||||
- Show/hide buttons, pages, or features.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Important security note for frontend roles
|
||||
|
||||
- The frontend can **only do UI-level checks** based on roles.
|
||||
|
||||
- **Never trust frontend role checks to secure data or APIs.**
|
||||
|
||||
- The backend must always verify the token and roles via `[Authorize]` attributes or middleware.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
|
||||
| Step | How it works |
|
||||
| --------------------- | ----------------------------------------------------- |
|
||||
| JWT creation | Multiple `"role"` claims added to token |
|
||||
| Backend authorization | Validates token and checks if user has required roles |
|
||||
| Frontend decoding | Extracts `role` claim(s) as array of strings |
|
||||
| Frontend UI control | Shows or hides content based on roles |
|
||||
@@ -0,0 +1,543 @@
|
||||
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
## 🚧 Branching
|
||||
|
||||
- [ ] Create the `feature/[name]` branch from `develop`
|
||||
|
||||
## Task
|
||||
- [ ] Task
|
||||
📂 Suggested Folder: `Domain/Framework/Interfaces/Respositories`
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
This file
|
||||
---
|
||||
## Add a field in Database
|
||||
- [ ] Add `User` field in `Roles` table using SSMS or Seed data in DbContext file
|
||||
- [ ] Make sure the property `PersonId` is nullable in `Account`, so you can add fields related to "Person" later after registration
|
||||
|
||||
## Branching
|
||||
- [ ] Create the feature/authentication branch based on develop
|
||||
|
||||
## Adjusting Account and Configurations
|
||||
|
||||
- [ ] Add navigation property for `AccountRoles` in Account
|
||||
```c#
|
||||
public virtual ICollection<AccountRole> AccountRoles { get; set; }
|
||||
```
|
||||
|
||||
- [ ] Add navigation properties for Account and Role in `AccountRole`
|
||||
```C#
|
||||
public virtual Role Role { get; set; }
|
||||
public virtual Account Account{ get; set; }
|
||||
```
|
||||
|
||||
- [ ] Update the entity configuration to reflect relationship mappings:
|
||||
```c#
|
||||
builder.HasOne<Account>(ar => ar.Account)
|
||||
.WithMany(a => a.AccountRoles)
|
||||
.HasForeignKey(ar => ar.AccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne<Role>(ar => ar.Role)
|
||||
.WithMany()
|
||||
.HasForeignKey(ar => ar.RoleId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
```
|
||||
|
||||
## Creating DTOs
|
||||
📂 Suggested Folder: ApplicationLayer/DTOs/[RelatedFolder]
|
||||
### AccountDto
|
||||
- [ ] Create `AccountDto` to expose relevant account information:
|
||||
```c#
|
||||
public class AccountDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public required string PhoneNumber { get; set; }
|
||||
public required string Password { set; get; }
|
||||
public string? Email { get; set; }
|
||||
public long? PersonId { get; set; }
|
||||
public List<string> Roles { get; set; }
|
||||
}
|
||||
```
|
||||
### AuthResponseDto
|
||||
- [ ] Define a DTO for authentication responses:
|
||||
```c#
|
||||
public class AuthResponseDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Token { get; set; } = null!;
|
||||
public string PhoneNumber { get; set; } = null!;
|
||||
public List<string> Roles { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Define a DTO for login requests:
|
||||
```c#
|
||||
public class LoginRequestDto
|
||||
{
|
||||
public string PhoneNumber { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
||||
```
|
||||
### RegisterRequestDto
|
||||
- [ ] Define a DTO for registration with validation attributes:
|
||||
```c#
|
||||
public class RegisterRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "Phone number is required.")]
|
||||
[Phone(ErrorMessage = "Phone number format is invalid.")]
|
||||
public required string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Password is required.")]
|
||||
[MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")]
|
||||
public required string Password { get; set; }
|
||||
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public required string ConfirmPassword { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 🔹 **1. What do the annotations like `[Required]`, `[Phone]`, `[MinLength]`, `[Compare]` on the DTO do?**
|
||||
|
||||
These are **Data Annotations** from `System.ComponentModel.DataAnnotations`.
|
||||
|
||||
They’re used by:
|
||||
|
||||
- The ASP.NET Core `[ApiController]` attribute
|
||||
|
||||
- **Model binding & automatic validation**
|
||||
|
||||
|
||||
**What happens:**
|
||||
If your controller is marked with `[ApiController]`, ASP.NET Core will **automatically validate** the DTO against these annotations **before entering your action method**.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
public class AuthController : ControllerBase
|
||||
```
|
||||
|
||||
Then this:
|
||||
|
||||
```csharp
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register(RegisterRequestDto dto)
|
||||
```
|
||||
|
||||
If `dto.PhoneNumber` is missing, it **won’t even run your logic**, and will return a `400 Bad Request` with validation errors.
|
||||
|
||||
|
||||
> “Why are these here if my frontend is separate?”
|
||||
|
||||
✅ **Answer**: They're still useful:
|
||||
|
||||
- For **security and safety**: you _must_ validate on the backend — never trust the frontend.
|
||||
|
||||
- For **auto validation** before hitting your logic — saving you boilerplate checks.
|
||||
|
||||
- You can use them for **Swagger/OpenAPI documentation** as well.
|
||||
|
||||
|
||||
Frontend validation is for **user experience**, not security.
|
||||
|
||||
---
|
||||
|
||||
### 🔹 **2. Where should password requirements be checked? Frontend or backend?**
|
||||
|
||||
✅ **Both.**
|
||||
|
||||
- **Frontend**: show real-time UX feedback (“Password must be 6+ characters”).
|
||||
- **Backend**: enforce security.
|
||||
|
||||
**Backend is the source of truth.**
|
||||
Frontend can be bypassed (e.g., Postman).
|
||||
|
||||
In the backend, you can either:
|
||||
|
||||
- Use annotations like `[MinLength(6)]`
|
||||
- Or do manual checks:
|
||||
|
||||
```csharp
|
||||
if (dto.Password.Length < 6)
|
||||
return BadRequest("Password must be at least 6 characters long.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔹 **3. Should confirm password be in the backend?**
|
||||
|
||||
✅ **Yes — if you're doing password comparison in backend.**
|
||||
|
||||
- `[Compare("Password")]` will validate if `ConfirmPassword` matches.
|
||||
|
||||
- Otherwise, you’ll need to check manually.
|
||||
|
||||
|
||||
You **can skip sending ConfirmPassword to backend** and just validate in frontend if you’re confident your frontend handles it.
|
||||
|
||||
But again: if someone sends malformed input manually (e.g., via Postman), backend should defend.
|
||||
|
||||
💡 **Best practice:**
|
||||
|
||||
- Validate `ConfirmPassword` in frontend (UX)
|
||||
- Do one last check in backend, or use `[Compare]` for auto-validation
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🔹 **4. Is it OK to send plain password in request? Or should we hash it on frontend?**
|
||||
|
||||
**✅ YES — it is OK and standard to send raw password in the login/signup request.**
|
||||
|
||||
Why?
|
||||
|
||||
- Passwords are sent over **HTTPS**, which encrypts the entire request.
|
||||
|
||||
- Hashing on frontend is _not_ secure, because:
|
||||
|
||||
- Your algorithm/salt would be exposed
|
||||
|
||||
- It defeats the purpose of salting and hashing correctly
|
||||
|
||||
- You lose control over security management
|
||||
|
||||
|
||||
### 🔹 **5. Error Response from Automatic Model Validation**
|
||||
|
||||
If your DTO looks like this:
|
||||
|
||||
```csharp
|
||||
public class RegisterRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "Phone number is required.")]
|
||||
[Phone(ErrorMessage = "Phone number format is invalid.")]
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Password is required.")]
|
||||
[MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
And the frontend sends this:
|
||||
|
||||
```json
|
||||
{
|
||||
"phoneNumber": "",
|
||||
"password": "123",
|
||||
"confirmPassword": "abc"
|
||||
}
|
||||
```
|
||||
|
||||
#### The backend will automatically return:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
"title": "One or more validation errors occurred.",
|
||||
"status": 400,
|
||||
"errors": {
|
||||
"PhoneNumber": [
|
||||
"Phone number is required."
|
||||
],
|
||||
"Password": [
|
||||
"Password must be at least 6 characters long."
|
||||
],
|
||||
"ConfirmPassword": [
|
||||
"Passwords do not match."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is thanks to `[ApiController]` on your controller class. The framework uses the **ModelState** and returns errors in a structured way.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Adding Mappings
|
||||
- [ ] Update `MappingProfile` with the following mappings:
|
||||
|
||||
```c#
|
||||
CreateMap<Account, AccountDto>()
|
||||
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.AccountRoles.Select(x=>x.Role.Title)));
|
||||
CreateMap<AccountDto, Account>()
|
||||
.ForMember(dest => dest.AccountRoles, opt => opt.Ignore());
|
||||
```
|
||||
|
||||
# Add Password Hasher Utility
|
||||
- [ ] Create a password hashing utility class
|
||||
📂 Suggested Folder: ApplicationLayer/Utils/`PasswordHasher.cs`
|
||||
```c#
|
||||
public static class PasswordHasher
|
||||
{
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256);
|
||||
byte[] hash = pbkdf2.GetBytes(32);
|
||||
|
||||
byte[] hashBytes = new byte[48];
|
||||
Array.Copy(salt, 0, hashBytes, 0, 16);
|
||||
Array.Copy(hash, 0, hashBytes, 16, 32);
|
||||
|
||||
return Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string hashedPassword)
|
||||
{
|
||||
byte[] hashBytes = Convert.FromBase64String(hashedPassword);
|
||||
|
||||
byte[] salt = new byte[16];
|
||||
Array.Copy(hashBytes, 0, salt, 0, 16);
|
||||
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256);
|
||||
byte[] hash = pbkdf2.GetBytes(32);
|
||||
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
if (hashBytes[i + 16] != hash[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Modifying Account Repository
|
||||
|
||||
- [ ] Add the following methods in **`IAccountRepository`**
|
||||
|
||||
```c#
|
||||
Task<Account> GetByPhoneNumberAsync(string phoneNumber);
|
||||
Task AddAccountRoleAsync(AccountRole accountRole);
|
||||
```
|
||||
|
||||
- [ ] Implement the methods in **`AccountRepository`**
|
||||
```c#
|
||||
public async Task AddAccountRoleAsync(AccountRole accountRole)
|
||||
{
|
||||
await DbContext.AccountRoles.AddAsync(accountRole);
|
||||
}
|
||||
|
||||
public async Task<Account> GetByPhoneNumberAsync(string phoneNumber)
|
||||
{
|
||||
var user = await DbContext.Accounts.Include(x => x.AccountRoles).ThenInclude(x => x.Role).FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
# Creating Service
|
||||
## Fix `Result.cs` Error Method
|
||||
- [ ] Update the `Error` method to include error messages:
|
||||
```c#
|
||||
public static Result<T> Error(T data, string errorMessage) => new() { Status = ResultStatus.Error, Data = data, ErrorMessage = errorMessage };
|
||||
```
|
||||
|
||||
## Creating `IAuthService.cs` and `AuthService.cs`
|
||||
- [ ] Define the `IAuthService` interface
|
||||
```c#
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<Result<AuthResponseDto>> RegisterAsync(RegisterRequestDto request);
|
||||
Task<Result<AuthResponseDto>> LoginAsync(LoginRequestDto request);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Implement the interface in `AuthService.cs`
|
||||
|
||||
Use this project as a reference:
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/Services/AuthService.cs
|
||||
|
||||
## Register `IAuthService` in Service in `Program.cs`
|
||||
|
||||
- [ ] Add to `Program.cs`
|
||||
```c#
|
||||
//...
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
//...
|
||||
```
|
||||
|
||||
|
||||
# Adding JWT
|
||||
|
||||
## Installing Required NuGet Packages
|
||||
Install these packages in the **`WebApi` (Presentation Layer)** project:
|
||||
```
|
||||
Microsoft.AspNetCore.Authentication.JwtBearer
|
||||
Microsoft.IdentityModel.Tokens
|
||||
System.IdentityModel.Tokens.Jwt
|
||||
```
|
||||
|
||||
## Create JWT Configuration Classes
|
||||
|
||||
- [ ] Add JWT section to `appsettings.json`
|
||||
```xaml
|
||||
"Jwt": {
|
||||
"Key": "[supersecretkeyyoustoresecurely]",
|
||||
"Issuer": "[Issuer]",
|
||||
"Audience": "MyAppUsers",
|
||||
"ExpiryMinutes": 60
|
||||
}
|
||||
```
|
||||
Note that you should fill the values as you wish - these are just samples
|
||||
|
||||
- [ ] Create `JwtSettings` and add the following method
|
||||
📂 Suggested Folder: WebAPI/Authentication
|
||||
```c#
|
||||
public class JwtSettings
|
||||
{
|
||||
public string Key { get; set; } = null!;
|
||||
public string Issuer { get; set; } = null!;
|
||||
public string Audience { get; set; } = null!;
|
||||
public int ExpiryMinutes { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Create `IJwtGenerator` and add the following method
|
||||
📂 Suggested Folder: WebAPI/Authentication
|
||||
```c#
|
||||
string GenerateToken(AuthResponseDto authResponseDto);
|
||||
```
|
||||
|
||||
|
||||
- [ ] Create `JwtGenerator`, implementing `IJwtGenerator`
|
||||
📂 Suggested Folder: WebAPI/Authentication
|
||||
|
||||
use this project as a reference
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.WebAPI/Authentication/JwtGenerator.cs
|
||||
|
||||
|
||||
## Configuring Jwt in Program.cs
|
||||
- [ ] Register `JwtGenerator` service
|
||||
```c#
|
||||
builder.Services.AddScoped<IJwtGenerator, JwtGenerator>();
|
||||
```
|
||||
- [ ] Bind `JwtSettings` from configuration
|
||||
```c#
|
||||
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
|
||||
```
|
||||
- [ ] Configure JWT authentication
|
||||
```c#
|
||||
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>();
|
||||
builder.Services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer("Bearer", options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtSettings.Issuer,
|
||||
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)),
|
||||
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
```
|
||||
|
||||
- [ ] Add `app.UseAuthentication` before `app.UseAuthorization`
|
||||
```c#
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
```
|
||||
# Add ApiControllers
|
||||
|
||||
- [ ] Create `AuthController`
|
||||
📂 Suggested Folder: WebApi/Controllers/`AuthController.cs`
|
||||
|
||||
- [ ] Add Class and Constructor
|
||||
```c#
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IJwtGenerator _jwtGenerator;
|
||||
|
||||
public AuthController(IAuthService authService, IJwtGenerator jwtGenerator)
|
||||
{
|
||||
_authService = authService;
|
||||
_jwtGenerator = jwtGenerator;
|
||||
}
|
||||
```
|
||||
- [ ] Add Register Method
|
||||
```c#
|
||||
public async Task<IActionResult> Register(RegisterRequestDto request)
|
||||
{
|
||||
var result = await _authService.RegisterAsync(request);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
return BadRequest(result.ErrorMessage);
|
||||
|
||||
var token = _jwtGenerator.GenerateToken(result.Data);
|
||||
var response = new AuthResponseDto
|
||||
{
|
||||
PhoneNumber = result.Data.PhoneNumber,
|
||||
Roles = result.Data.Roles,
|
||||
Token = token
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
```
|
||||
- [ ] Add Login Method
|
||||
|
||||
```c#
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login(LoginRequestDto request)
|
||||
{
|
||||
var result = await _authService.LoginAsync(request);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
return Unauthorized(result.ErrorMessage);
|
||||
|
||||
var token = _jwtGenerator.GenerateToken(result.Data);
|
||||
var response = new AuthResponseDto
|
||||
{
|
||||
PhoneNumber = result.Data.PhoneNumber,
|
||||
Roles = result.Data.Roles,
|
||||
Token = token
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Create AccountController
|
||||
📂 Suggested Folder: WebApi/Controllers/AccountController.cs
|
||||
```c#
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpGet("profile")]
|
||||
public IActionResult GetProfile()
|
||||
{
|
||||
return Ok("Hi there, hello");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,803 @@
|
||||
# Branching
|
||||
- [ ] Create the feature/authentication branch based on develop
|
||||
|
||||
# Adding Models
|
||||
Create three files inside this folder:
|
||||
📂 Suggested Folder: shared/models/authentication
|
||||
|
||||
- [ ] `AuthResponseDto.ts`
|
||||
```ts
|
||||
export interface AuthResponseDto {
|
||||
token: string;
|
||||
phoneNumber: string;
|
||||
roles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `LoginRequestDto.ts`
|
||||
```ts
|
||||
export interface LoginRequestDto {
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `RegisterRequestDto.ts`
|
||||
```ts
|
||||
export interface RegisterRequestDto {
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
```
|
||||
|
||||
# Adding Authentication API Calls in `agent.ts`
|
||||
- [ ] Add the `Auth` object to `agent.ts`:
|
||||
```ts
|
||||
const Auth = {
|
||||
register: (data: RegisterRequestDto) =>
|
||||
request.post<{ token: string }>('/auth/register', data),
|
||||
login: (data: LoginRequestDto) =>
|
||||
request.post<{ token: string }>('/auth/login', data),
|
||||
};
|
||||
```
|
||||
- [ ] Ensure `agent.ts` ends like this:
|
||||
```tsx
|
||||
const agent = {
|
||||
TransportationSearch,
|
||||
Cities,
|
||||
Auth
|
||||
}
|
||||
```
|
||||
|
||||
# Creating `authStore.ts`
|
||||
Suggested Folder
|
||||
📂 Suggested Folder: shared/store/
|
||||
- [ ] Create authStore.ts
|
||||
```tsx
|
||||
import { AuthResponseDto } from '@/shared/models/authentication/AuthResponseDto';
|
||||
import {create} from 'zustand';
|
||||
|
||||
interface User {
|
||||
phoneNumber: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (response: AuthResponseDto) => void;
|
||||
logout: () => void;
|
||||
setToken: (token: string) => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
token: null,
|
||||
|
||||
|
||||
login: (response) =>
|
||||
set(() => ({
|
||||
token: response.token,
|
||||
user: {
|
||||
phoneNumber: response.phoneNumber,
|
||||
roles: response.roles
|
||||
},
|
||||
|
||||
isLoggedIn: true,
|
||||
})),
|
||||
|
||||
logout: () =>
|
||||
set(() => ({
|
||||
token: null,
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
})),
|
||||
|
||||
|
||||
|
||||
setToken: (token) =>
|
||||
set((state) => ({
|
||||
token,
|
||||
isLoggedIn: !!token,
|
||||
user: state.user,
|
||||
})),
|
||||
}));
|
||||
```
|
||||
|
||||
## About `authStore.ts`
|
||||
|
||||
This file defines a centralized **authentication state store** using [`Zustand`](https://github.com/pmndrs/zustand), a minimal and scalable state management library for React. It helps manage login state, user data, and authentication token across your application.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `User` Interface
|
||||
|
||||
```ts
|
||||
interface User {
|
||||
phoneNumber: string;
|
||||
roles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
This interface defines the shape of the `user` object stored in the auth state. It currently includes:
|
||||
- `phoneNumber`: A string representing the user's phone number.
|
||||
- `roles`: An array representing user roles
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `AuthState` Interface
|
||||
|
||||
```ts
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (response: AuthResponseDto) => void;
|
||||
logout: () => void;
|
||||
setToken: (token: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
This defines the overall structure of the authentication store:
|
||||
|
||||
- `isLoggedIn`: Indicates whether a user is logged in.
|
||||
- `user`: Stores user-specific data if authenticated; otherwise `null`.
|
||||
- `token`: JWT or access token from the server.
|
||||
- `login()`: Accepts an `AuthResponseDto` and updates the state.
|
||||
- `logout()`: Clears all authentication-related data.
|
||||
- `setToken()`: Sets the token and toggles login status accordingly.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Zustand Store Definition
|
||||
|
||||
```ts
|
||||
export const useAuthStore = create<AuthState>((set) => ({ ... });
|
||||
```
|
||||
|
||||
Creates a global auth store using Zustand. `create()` accepts a function that receives `set` (used to update state) and returns the initial store state and methods.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Initial State
|
||||
|
||||
```ts
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
token: null,
|
||||
```
|
||||
|
||||
These lines define the initial, default state for an unauthenticated user.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `login()` Method
|
||||
|
||||
```ts
|
||||
login: (response) =>
|
||||
set(() => ({
|
||||
token: response.token,
|
||||
user: {
|
||||
phoneNumber: response.phoneNumber,
|
||||
roles: response.roles
|
||||
},
|
||||
isLoggedIn: true,
|
||||
})),
|
||||
```
|
||||
|
||||
- Accepts an `AuthResponseDto` object after a successful login.
|
||||
|
||||
- Extracts the `token`, `phoneNumber`, and `roles`, and sets them in state.
|
||||
|
||||
- Marks the user as `isLoggedIn: true`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `logout()` Method
|
||||
|
||||
```ts
|
||||
logout: () =>
|
||||
set(() => ({
|
||||
token: null,
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
})),
|
||||
```
|
||||
|
||||
- Clears all auth-related data (token and user).
|
||||
|
||||
- Effectively logs the user out by setting `isLoggedIn` to `false`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `setToken()` Method
|
||||
|
||||
```ts
|
||||
setToken: (token) =>
|
||||
set((state) => ({
|
||||
token,
|
||||
isLoggedIn: !!token,
|
||||
user: state.user,
|
||||
})),
|
||||
```
|
||||
|
||||
- Updates the token in the store.
|
||||
|
||||
- Sets `isLoggedIn` based on whether a non-empty token exists.
|
||||
|
||||
- Retains the current `user` object.
|
||||
|
||||
|
||||
---
|
||||
# Add LoginModal
|
||||
📂 Suggested Folder: shared/features/authentication/modals
|
||||
## Example
|
||||
```tsx
|
||||
import React, { useState } from "react";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import agent from "@/shared/api/agent";
|
||||
import { LoginRequestDto } from "@/shared/models/authentication/LoginRequestDto";
|
||||
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => {
|
||||
|
||||
const login = useAuthStore((state) => state.login);
|
||||
|
||||
const [form, setForm] = useState<LoginRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validate = () => {
|
||||
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
|
||||
if (!phoneRegex.test(form.phoneNumber)) {
|
||||
return "Invalid phone number format";
|
||||
}
|
||||
|
||||
if (!form.password || form.password.length < 8) {
|
||||
return "Password must be at least 8 characters";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const validationError = validate();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await agent.Auth.login(form);
|
||||
login(response);
|
||||
setError(null);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Login failed");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
<h2 style={{ marginBottom: "1rem" }}>Login</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Phone Number"
|
||||
value={form.phoneNumber}
|
||||
onChange={(e) => setForm({ ...form, phoneNumber: e.target.value })}
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
style={styles.input}
|
||||
/>
|
||||
<button onClick={handleSubmit} style={styles.button}>
|
||||
Login
|
||||
</button>
|
||||
{error && (
|
||||
<p style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
...styles.button,
|
||||
marginTop: "0.5rem",
|
||||
backgroundColor: "#ccc",
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
//ADD STYLES
|
||||
};
|
||||
|
||||
export default LoginModal;
|
||||
```
|
||||
|
||||
## About LoginModal
|
||||
|
||||
This component provides a modal UI that allows users to log in using their **phone number and password**. It integrates with the authentication store and API to perform login logic and handle errors.
|
||||
|
||||
---
|
||||
## 🔹 Props Interface
|
||||
|
||||
```tsx
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
- `onClose`: A callback to be called when the modal should be closed (e.g., user clicks "Cancel" or logs in successfully).
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Component Setup
|
||||
|
||||
```tsx
|
||||
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => { ... };
|
||||
```
|
||||
|
||||
Defines a functional React component with the `onClose` prop destructured.
|
||||
|
||||
### 🔸 Accessing Auth Store
|
||||
|
||||
```tsx
|
||||
const login = useAuthStore((state) => state.login);
|
||||
```
|
||||
|
||||
Retrieves the `login` method from Zustand’s `authStore` so that the global auth state can be updated after successful login.
|
||||
|
||||
### 🔸 Local Form State
|
||||
|
||||
```tsx
|
||||
const [form, setForm] = useState<LoginRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
});
|
||||
```
|
||||
|
||||
Initializes `form` state with empty values for the phone number and password.
|
||||
|
||||
### 🔸 Error Handling State
|
||||
|
||||
```tsx
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
```
|
||||
|
||||
Stores any error messages resulting from validation or login attempt.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Validation Logic
|
||||
|
||||
```tsx
|
||||
const validate = () => {
|
||||
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
|
||||
if (!phoneRegex.test(form.phoneNumber)) {
|
||||
return "Invalid phone number format";
|
||||
}
|
||||
|
||||
if (!form.password || form.password.length < 8) {
|
||||
return "Password must be at least 8 characters";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
- Validates the phone number format (Iranian phone format in this case).
|
||||
- Ensures password is at least 8 characters long.
|
||||
- Returns a string error message or `null` if validation passes.
|
||||
|
||||
---
|
||||
## 🔹 Submit Handler
|
||||
|
||||
```tsx
|
||||
const handleSubmit = async () => {
|
||||
const validationError = validate();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await agent.Auth.login(form);
|
||||
login(response);
|
||||
setError(null);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Login failed");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- Calls `validate()` and prevents submission if there's an error.
|
||||
- Calls the backend API using `agent.Auth.login()`.
|
||||
- On success: updates auth state, clears error, closes modal.
|
||||
- On failure: shows error message.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 UI Layout
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
<h2>Login</h2>
|
||||
<input ... />
|
||||
<input ... />
|
||||
<button onClick={handleSubmit}>Login</button>
|
||||
{error && <p>{error}</p>}
|
||||
<button onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Elements:
|
||||
|
||||
- **Phone Number Input**
|
||||
|
||||
- **Password Input**
|
||||
|
||||
- **Login Button**: Triggers `handleSubmit`.
|
||||
|
||||
- **Error Message**: Shown only if there's an error.
|
||||
|
||||
- **Cancel Button**: Triggers `onClose` callback.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Styles Placeholder
|
||||
|
||||
```tsx
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
// Add modal styles here
|
||||
};
|
||||
```
|
||||
|
||||
This placeholder defines inline CSS styles for the modal. Each style (e.g., `overlay`, `modal`, `input`, `button`) should be defined here.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
This modal:
|
||||
|
||||
- Provides a simple, reusable login form.
|
||||
|
||||
- Validates input before calling the API.
|
||||
|
||||
- Updates global auth state via Zustand.
|
||||
|
||||
- Handles success/failure states.
|
||||
|
||||
- Uses modal-friendly inline styles (with room for improvement).
|
||||
|
||||
|
||||
---
|
||||
# Add RegisterModal
|
||||
- [ ] Create RegisterModal
|
||||
📂 Suggested Folder: shared/features/authentication/modals
|
||||
```tsx
|
||||
import agent from "@/shared/api/agent";
|
||||
import { RegisterRequestDto } from "@/shared/models/authentication/RegisterRequestDto";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import React, { useState } from "react";
|
||||
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
const RegisterModal: React.FC<Props> = ({ onClose }) => {
|
||||
|
||||
const [form, setForm] = useState<RegisterRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const login = useAuthStore((state) => state.login);
|
||||
|
||||
const validate = () => {
|
||||
|
||||
const { phoneNumber, password, confirmPassword } = form;
|
||||
if (!phoneNumber || !password || !confirmPassword) {
|
||||
return "All fields are required.";
|
||||
}
|
||||
|
||||
if (!/^\d{11}$/.test(phoneNumber)) {
|
||||
return "Phone number must be 11 digits.";
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return "Password must be at least 6 characters.";
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return "Passwords do not match.";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
const validationError = validate();
|
||||
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// Use the form as RegisterRequestDto explicitly
|
||||
|
||||
const requestData: RegisterRequestDto = {
|
||||
phoneNumber: form.phoneNumber,
|
||||
password: form.password,
|
||||
confirmPassword: form.confirmPassword,
|
||||
};
|
||||
|
||||
|
||||
|
||||
const response = await agent.Auth.register(requestData);
|
||||
login(response);
|
||||
setForm({ phoneNumber: "", password: "", confirmPassword: "" });
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Registration failed.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
<h2 style={{ marginBottom: "1rem" }}>Register</h2>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
value={form.phoneNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="Phone Number"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
placeholder="Password"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={form.confirmPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="Confirm Password"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
|
||||
<p
|
||||
style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button type="submit" style={styles.button}>
|
||||
Register
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<button
|
||||
|
||||
onClick={onClose}
|
||||
|
||||
style={{
|
||||
...styles.button,
|
||||
marginTop: "0.5rem",
|
||||
backgroundColor: "#ccc",
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
//ADD STYLES
|
||||
};
|
||||
|
||||
export default RegisterModal;
|
||||
```
|
||||
|
||||
## About RegisterModal
|
||||
|
||||
|
||||
### **Component Structure**
|
||||
|
||||
### 1. **Props**
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
- The modal only expects one prop: `onClose`, a function to close the modal (e.g., hide it from the screen).
|
||||
|
||||
---
|
||||
|
||||
### 2. **State Management**
|
||||
|
||||
```tsx
|
||||
const [form, setForm] = useState<RegisterRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
```
|
||||
|
||||
- Initializes the form state for inputs, based on the `RegisterRequestDto` shape.
|
||||
|
||||
```tsx
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
```
|
||||
|
||||
- Stores any validation or server error message to display in the UI.
|
||||
|
||||
```tsx
|
||||
const login = useAuthStore((state) => state.login);
|
||||
```
|
||||
|
||||
- Accesses the `login` method from your global auth store, to automatically log in the user after successful registration.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3. **Validation Logic**
|
||||
|
||||
```tsx
|
||||
const validate = () => {
|
||||
// Checks for empty fields
|
||||
// Validates phone number format (must be 11 digits)
|
||||
// Ensures password length is sufficient
|
||||
// Confirms password and confirmation match
|
||||
};
|
||||
```
|
||||
|
||||
- Ensures client-side validation before making a request to the server.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Input Handling**
|
||||
|
||||
```tsx
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
```
|
||||
|
||||
- Updates the correct field in the `form` object dynamically based on the input `name`.
|
||||
|
||||
---
|
||||
|
||||
### 5. **Form Submission**
|
||||
|
||||
```tsx
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const validationError = validate();
|
||||
// If validation passes, submit the data to the backend
|
||||
// If backend response is successful, log in and close modal
|
||||
// If it fails, show error message
|
||||
};
|
||||
```
|
||||
|
||||
- Prevents default form submission
|
||||
- Validates inputs
|
||||
- Sends the data to `agent.Auth.register`
|
||||
- On success: logs in user and clears form
|
||||
- On failure: shows error from server
|
||||
|
||||
---
|
||||
|
||||
### 6. **JSX Render**
|
||||
|
||||
```tsx
|
||||
<div style={styles.overlay}>...</div>
|
||||
```
|
||||
|
||||
- **Modal Overlay**: darkened background behind the modal
|
||||
- **Modal Box**: contains title, form, and buttons
|
||||
|
||||
### Inside `<form>`:
|
||||
|
||||
- Inputs for:
|
||||
- `phoneNumber`
|
||||
- `password`
|
||||
- `confirmPassword`
|
||||
|
||||
- Submit button for Register
|
||||
|
||||
- Error message display (if any)
|
||||
|
||||
- Cancel button that calls `onClose`
|
||||
|
||||
---
|
||||
|
||||
# Handle login/logout and register buttons in navbar
|
||||
- [ ] implement a way for showing login and register buttons in navbar when user is not signed in
|
||||
- [ ] when clicked, the button should show the related modal, for the user to sign in or register
|
||||
# Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
Reference in New Issue
Block a user