# OIDC Identity Provider — Complete Documentation
### Asian Paints Laravel Project
---

## Table of Contents
1. What is OIDC and Why We Built It
2. Project Folder Structure
3. Database Tables
4. File-by-File Code Explanation
5. Complete API Reference with CURL
6. Full Login Flow (Step by Step)
7. Token Explanation
8. Error Reference

---

## 1. What is OIDC and Why We Built It

OIDC stands for **OpenID Connect**. It is a standard login protocol used by companies like Google, Facebook, and Apple for "Login with Google" type features.

### Why We Built It
- Shopify needs a trusted identity provider to verify who a customer is
- Instead of Shopify managing logins, our Laravel app acts as the **login server**
- When a customer logs in, we verify their identity from our `customer_details` table
- We issue a signed **JWT token (id_token)** that Shopify can trust and verify
- This allows **Single Sign-On (SSO)** — one login works across all connected apps

### Simple Analogy
```
Customer → Shopify → "Who is this user?"
                          ↓
               Our Laravel OIDC Server
                          ↓
               Checks customer_details table
                          ↓
               Issues signed JWT token
                          ↓
Shopify ← "Here is the verified user identity"
```

---

## 2. Project Folder Structure

```
asian-paints/
│
├── keys/                              ← RSA Key Files (JWT signing)
│   ├── private.pem                    ← Private key  — used to SIGN tokens
│   ├── public.pem                     ← Public key   — used to VERIFY tokens
│   └── jwks.json                      ← Public key in JWKS format for external apps
│
├── app/
│   ├── Http/Controllers/Api/
│   │   └── OidcController.php         ← Handles all 7 API endpoints
│   │
│   ├── Services/
│   │   └── OidcService.php            ← All OIDC business logic
│   │
│   └── Models/
│       ├── OidcClient.php             ← Registered apps (Shopify etc.)
│       ├── OidcAuthCode.php           ← Temporary login codes (5 min)
│       ├── OidcRefreshToken.php       ← Long-lived refresh tokens (30 days)
│       └── CustomerDetail.php         ← Customer accounts table
│
├── database/migrations/
│   ├── ..._create_oidc_clients_table.php
│   ├── ..._create_oidc_auth_codes_table.php
│   └── ..._create_oidc_refresh_tokens_table.php
│
└── routes/
    └── api.php                        ← All route definitions
```

---

## 3. Database Tables

### Table 1: `oidc_clients`
Stores registered applications that are allowed to use our login system.

| Column | Type | Description |
|--------|------|-------------|
| id | INT | Primary key |
| client_id | VARCHAR | Unique app identifier (like a username for the app) |
| client_secret | VARCHAR | Secret password for the app |
| redirect_uris | JSON | Allowed URLs to send the auth code to after login |
| post_logout_redirect_uris | JSON | Allowed URLs to redirect after logout |
| created_at | TIMESTAMP | Record creation time |
| updated_at | TIMESTAMP | Record update time |

Current registered client:
```
client_id     : 759b2c61f3db684d86f30e6de8b1965c
client_secret : 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
redirect_uris : ["https://apg-uae.myshopify.com/authorization"]
```

---

### Table 2: `oidc_auth_codes`
Stores temporary one-time codes generated after a customer logs in.

| Column | Type | Description |
|--------|------|-------------|
| id | INT | Primary key |
| code | VARCHAR | Random 32-character code |
| client_id | VARCHAR | Which app requested this code |
| user_id | INT | Which customer logged in (links to customer_details.id) |
| redirect_uri | VARCHAR | Where to send the code |
| scope | VARCHAR | What permissions were requested (openid, email) |
| nonce | VARCHAR | Security value to prevent replay attacks |
| code_challenge | VARCHAR | PKCE security hash (optional) |
| code_challenge_method | VARCHAR | PKCE method — always S256 |
| expires_at | TIMESTAMP | Code expires after 5 minutes |
| created_at | TIMESTAMP | When code was created |

---

### Table 3: `oidc_refresh_tokens`
Stores long-lived tokens used to get new access tokens without logging in again.

| Column | Type | Description |
|--------|------|-------------|
| id | INT | Primary key |
| token | VARCHAR | Random 40-character token |
| client_id | VARCHAR | Which app this token belongs to |
| user_id | INT | Which customer this token belongs to |
| expires_at | TIMESTAMP | Token expires after 30 days |
| created_at | TIMESTAMP | When token was created |

---

### Table 4: `customer_details` (existing)
The main customer table. OIDC reads from this to verify login.

| Column | Used By OIDC | Description |
|--------|-------------|-------------|
| id | ✅ | Used as `sub` (subject) in JWT token |
| customer_email | ✅ | Used as login username |
| first_name | ✅ | Included in JWT token as `name` |
| last_name | ✅ | Included in JWT token as `name` |
| customer_mobile_no | ✅ | Included in JWT token as `phone` |
| shopify_customer_id | ✅ | Included in JWT token |

---

## 4. File-by-File Code Explanation

---

### File 1: `app/Http/Controllers/Api/OidcController.php`

**Purpose:** Acts as the front door — receives HTTP requests and sends responses. Does NOT contain business logic, just calls OidcService.

#### Method: `wellKnown()`
```
Route  : GET /api/oidc/.well-known/openid-configuration
Purpose: Tells external apps (Shopify) what endpoints exist and what features are supported
Returns: JSON with all endpoint URLs
```

#### Method: `jwks()`
```
Route  : GET /api/oidc/jwks.json
Purpose: Gives external apps the public key to verify our JWT tokens
Returns: RSA public key in JWKS format
```

#### Method: `authorize()` — GET
```
Route  : GET /api/oidc/authorize
Purpose: Shows the HTML login form to the customer
Process: 1. Validates client_id and redirect_uri exist in oidc_clients table
         2. If valid → returns HTML login form
         3. If invalid → returns 400 error
Returns: HTML page with email + password form
```

#### Method: `authorize()` — POST
```
Route  : POST /api/oidc/authorize
Purpose: Processes the login form submission
Process: 1. Validates client_id and redirect_uri
         2. Looks up customer by email in customer_details table
         3. If found → generates auth code → saves to oidc_auth_codes table
         4. Returns JSON with the code
Returns: JSON { code, state, redirect_url }
```

#### Method: `token()`
```
Route  : POST /api/oidc/token
Purpose: Exchanges the auth code for actual tokens
Process: 1. Validates client_id + client_secret
         2. Validates the auth code exists and is not expired
         3. Checks PKCE if used
         4. Creates signed JWT id_token with customer details
         5. Generates access_token and refresh_token
         6. Deletes the used auth code (one-time use)
Returns: JSON { access_token, token_type, expires_in, id_token, refresh_token }
```

#### Method: `userinfo()`
```
Route  : GET /api/oidc/userinfo
Purpose: Returns customer details from a valid id_token
Process: 1. Reads Bearer token from Authorization header
         2. Verifies JWT signature using public key
         3. Checks token issuer matches our server
         4. Returns customer claims from token payload
Returns: JSON { sub, email, email_verified, name, phone, shopify_customer_id }
```

#### Method: `logout()`
```
Route  : GET /api/oidc/logout
Purpose: Logs out the customer and redirects to registered URI
Process: 1. Reads post_logout_redirect_uri from query params
         2. Checks it exists in oidc_clients.post_logout_redirect_uris
         3. If valid → redirects to that URI
Returns: 302 redirect
```

#### Private Method: `resolveClientCredentials()`
```
Purpose: Extracts client_id and client_secret from the request
         Supports two methods:
         1. Basic Auth header: Authorization: Basic base64(client_id:client_secret)
         2. Body params: client_id=xxx&client_secret=yyy
```

---

### File 2: `app/Services/OidcService.php`

**Purpose:** Contains all the actual business logic. Controller calls this, this does the real work.

#### Constructor
```
Loads 3 files when the service starts:
- keys/private.pem  → for signing JWT tokens
- keys/public.pem   → for verifying JWT tokens
- keys/jwks.json    → for exposing public key to external apps
Also reads APP_URL from .env as the issuer value
```

#### Method: `getDiscoveryDocument()`
```
Purpose: Builds the discovery JSON
Returns all endpoint URLs based on APP_URL from .env
```

#### Method: `getJwks()`
```
Purpose: Returns the contents of keys/jwks.json
Used by Shopify to verify our JWT signatures
```

#### Method: `validateClient()`
```
Purpose: Checks if client_id exists in oidc_clients table
         AND if redirect_uri is in the allowed list
Returns: true or false
```

#### Method: `buildLoginForm()`
```
Purpose: Generates HTML login form
         Embeds all OIDC params as hidden fields so they are
         submitted back when the form is posted
Returns: HTML string
```

#### Method: `issueAuthCode()`
```
Purpose: Authenticates customer and creates auth code
Process:
  1. Looks up customer by customer_email in customer_details
  2. If not found → returns error
  3. Generates random 32-char code using Str::random(32)
  4. Saves code to oidc_auth_codes table with 5 min expiry
  5. Builds redirect URL with code appended
Returns: ['redirect_url' => 'https://...?code=XXX&state=YYY']
```

#### Method: `exchangeAuthCode()`
```
Purpose: The core token issuance logic
Process:
  1. Validates client_id + client_secret against oidc_clients table
  2. Validates redirect_uri is in allowed list
  3. Finds auth code in oidc_auth_codes where expires_at > now()
  4. If PKCE was used → verifies code_verifier hash matches code_challenge
  5. Loads customer from customer_details
  6. Builds JWT payload with customer details
  7. Signs JWT with private RSA key using RS256 algorithm
  8. Generates random access_token (40 chars)
  9. Generates random refresh_token (40 chars)
  10. Saves refresh_token to oidc_refresh_tokens table (30 day expiry)
  11. Deletes the used auth code
Returns: { access_token, token_type, expires_in, id_token, refresh_token }

JWT Payload contains:
  iss   → issuer (our server URL)
  aud   → audience (client_id)
  sub   → subject (customer id)
  iat   → issued at (unix timestamp)
  exp   → expires at (iat + 300 seconds)
  email → customer_email
  name  → first_name + last_name
  phone → customer_mobile_no
  shopify_customer_id → shopify_customer_id
```

#### Method: `getUserinfo()`
```
Purpose: Verifies id_token and returns customer claims
Process:
  1. Decodes JWT using public key
  2. Verifies issuer matches our server
  3. Returns claims from token payload
Returns: { sub, email, email_verified, name, phone, shopify_customer_id }
```

#### Method: `validateLogoutUri()`
```
Purpose: Checks if post_logout_redirect_uri is registered
Loops through all oidc_clients and checks post_logout_redirect_uris arrays
Returns: true or false
```

---

### File 3: `app/Models/OidcClient.php`
```
Table  : oidc_clients
Purpose: Represents a registered application
Casts  : redirect_uris and post_logout_redirect_uris are auto-decoded from JSON to PHP array
```

### File 4: `app/Models/OidcAuthCode.php`
```
Table  : oidc_auth_codes
Purpose: Represents a temporary one-time login code
Casts  : expires_at is auto-converted to Carbon datetime object
```

### File 5: `app/Models/OidcRefreshToken.php`
```
Table  : oidc_refresh_tokens
Purpose: Represents a long-lived refresh token
Casts  : expires_at is auto-converted to Carbon datetime object
```

### File 6: `app/Models/CustomerDetail.php`
```
Table  : customer_details
Purpose: Represents a customer account
         Extends Authenticatable so it can be used with Laravel auth
         OIDC uses customer_email field as the login identifier
```

### File 7: `keys/private.pem`
```
Purpose: RSA 2048-bit private key
Used   : To SIGN the JWT id_token in OidcService::exchangeAuthCode()
Security: NEVER share this file. NEVER commit to git.
```

### File 8: `keys/public.pem`
```
Purpose: RSA 2048-bit public key (paired with private.pem)
Used   : To VERIFY JWT tokens in OidcService::getUserinfo()
Security: Safe to share — this is what external apps use to verify tokens
```

### File 9: `keys/jwks.json`
```
Purpose: Public key in JWKS (JSON Web Key Set) format
Used   : Returned by GET /api/oidc/jwks.json endpoint
         Shopify fetches this to verify our JWT signatures automatically
```

---

## 5. Complete API Reference with CURL

### API 1 — Discovery Document
```bash
curl --request GET \
  'http://127.0.0.1:8000/api/oidc/.well-known/openid-configuration'
```
Response:
```json
{
  "issuer": "http://127.0.0.1:8000",
  "authorization_endpoint": "http://127.0.0.1:8000/api/oidc/authorize",
  "token_endpoint": "http://127.0.0.1:8000/api/oidc/token",
  "jwks_uri": "http://127.0.0.1:8000/api/oidc/jwks.json",
  "userinfo_endpoint": "http://127.0.0.1:8000/api/oidc/userinfo",
  "end_session_endpoint": "http://127.0.0.1:8000/api/oidc/logout",
  "scopes_supported": ["openid", "email", "profile"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "id_token_signing_alg_values_supported": ["RS256"]
}
```

---

### API 2 — JWKS Public Keys
```bash
curl --request GET \
  'http://127.0.0.1:8000/api/oidc/jwks.json'
```
Response:
```json
{
  "keys": [{
    "kty": "RSA",
    "use": "sig",
    "alg": "RS256",
    "kid": "key-1",
    "n": "...(public key modulus)...",
    "e": "AQAB"
  }]
}
```

---

### API 3 — Show Login Form (Browser Only)
```bash
curl --request GET \
  'http://127.0.0.1:8000/api/oidc/authorize?response_type=code&client_id=759b2c61f3db684d86f30e6de8b1965c&redirect_uri=https://apg-uae.myshopify.com/authorization&state=test123&scope=openid%20email'
```
Response: HTML login form page

---

### API 4 — Login → Get Auth Code ⭐
```bash
curl --request POST \
  'http://127.0.0.1:8000/api/oidc/authorize' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'response_type=code' \
  --data-urlencode 'client_id=759b2c61f3db684d86f30e6de8b1965c' \
  --data-urlencode 'redirect_uri=https://apg-uae.myshopify.com/authorization' \
  --data-urlencode 'state=test123' \
  --data-urlencode 'scope=openid email' \
  --data-urlencode 'username=mishranimesh067@gmail.com' \
  --data-urlencode 'password='
```
Response:
```json
{
  "success": true,
  "status_code": 200,
  "message": "Authorization code generated successfully.",
  "data": {
    "code": "CbyPYm1JkIVOurMyzOjLBggVMg4H6TOG",
    "state": "test123",
    "redirect_url": "https://apg-uae.myshopify.com/authorization?code=CbyPYm1JkIVOurMyzOjLBggVMg4H6TOG&state=test123"
  }
}
```
Copy the `code` value for the next step.

---

### API 5 — Token Exchange → Get id_token ⭐
```bash
curl --request POST \
  'http://127.0.0.1:8000/api/oidc/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=PASTE_CODE_FROM_STEP_4' \
  --data-urlencode 'redirect_uri=https://apg-uae.myshopify.com/authorization' \
  --data-urlencode 'client_id=759b2c61f3db684d86f30e6de8b1965c' \
  --data-urlencode 'client_secret=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'
```
Response:
```json
{
  "access_token": "33PvglScw4SUlAzBJaYmtTnuS4XnQRqNWk9FDmiT",
  "token_type": "Bearer",
  "expires_in": 300,
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIn0...",
  "refresh_token": "87MuEl4A7864lFx8rcBHnHYqTrKQTIMiXWkL46C5"
}
```
Copy the `id_token` value for the next step.

---

### API 6 — Get User Info ⭐
```bash
curl --request GET \
  'http://127.0.0.1:8000/api/oidc/userinfo' \
  --header 'Authorization: Bearer PASTE_ID_TOKEN_FROM_STEP_5'
```
Response:
```json
{
  "sub": "3",
  "email": "mishranimesh067@gmail.com",
  "email_verified": true,
  "name": "Nimesh Mishra",
  "phone": "919099868381",
  "shopify_customer_id": "9312912310334"
}
```

---

### API 7 — Logout
```bash
curl --max-redirs 0 --request GET \
  'http://127.0.0.1:8000/api/oidc/logout?post_logout_redirect_uri=https://apg-uae.myshopify.com'
```
Response: `302 redirect` to `https://apg-uae.myshopify.com`

---

## 6. Full Login Flow (Step by Step)

```
CUSTOMER                  YOUR APP               OIDC SERVER (Laravel)        DATABASE
   |                          |                          |                        |
   |--- enters email -------->|                          |                        |
   |                          |-- POST /authorize ------>|                        |
   |                          |   (email + client_id)    |-- lookup email ------->|
   |                          |                          |<-- customer found ------|
   |                          |                          |-- save auth code ------>|
   |                          |<-- 200 { code: XXXX } ---|                        |
   |                          |                          |                        |
   |                          |-- POST /token ---------->|                        |
   |                          |   (code + client_secret) |-- validate code ------->|
   |                          |                          |-- delete code --------->|
   |                          |                          |-- save refresh_token -->|
   |                          |<-- { id_token, ... } ----|                        |
   |                          |                          |                        |
   |                          |-- GET /userinfo -------->|                        |
   |                          |   (Bearer id_token)      |-- verify JWT sig        |
   |                          |<-- { email, name, ... } -|                        |
   |<-- logged in ------------|                          |                        |
```

---

## 7. Token Explanation

### id_token (JWT)
A signed JSON Web Token containing customer identity. Looks like:
```
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIzIiwiZW1haWwiOiIuLi4ifQ.signature
```
Three parts separated by dots:
- Part 1: Header (algorithm info)
- Part 2: Payload (customer data — base64 encoded)
- Part 3: Signature (proves it was signed by our private key)

Decoded payload:
```json
{
  "iss": "https://yourdomain.com",
  "aud": "759b2c61f3db684d86f30e6de8b1965c",
  "sub": "3",
  "iat": 1776071279,
  "exp": 1776071579,
  "email": "mishranimesh067@gmail.com",
  "email_verified": true,
  "name": "Nimesh Mishra",
  "phone": "919099868381",
  "shopify_customer_id": "9312912310334"
}
```

| Field | Meaning |
|-------|---------|
| iss | Issuer — our server URL |
| aud | Audience — which app this token is for |
| sub | Subject — customer ID in our database |
| iat | Issued At — unix timestamp when created |
| exp | Expires At — unix timestamp when it expires |

### Token Lifetimes
| Token | Expires |
|-------|---------|
| auth code | 5 minutes |
| id_token | 5 minutes (300 seconds) |
| access_token | 5 minutes (300 seconds) |
| refresh_token | 30 days |

---

## 8. Error Reference

| Endpoint | Status | Error | Cause | Fix |
|----------|--------|-------|-------|-----|
| /authorize | 400 | invalid_client or redirect_uri | client_id not in oidc_clients OR redirect_uri not in allowed list | Check oidc_clients table |
| /authorize | 401 | Invalid credentials | Email not found in customer_details | Use valid customer email |
| /token | 401 | invalid_client | Wrong client_id or client_secret | Check oidc_clients table |
| /token | 400 | invalid_grant | Code expired, already used, or wrong redirect_uri | Get a fresh code from /authorize |
| /token | 400 | unsupported_grant_type | grant_type is not authorization_code | Set grant_type=authorization_code |
| /userinfo | 401 | invalid_token | JWT expired, wrong signature, or wrong issuer | Get a fresh id_token from /token |
| /logout | 400 | invalid post_logout_redirect_uri | URI not registered in oidc_clients | Check post_logout_redirect_uris column |

---

## Important Notes

1. Auth code is ONE-TIME USE — deleted after /token is called
2. Auth code expires in 5 MINUTES — call /token immediately
3. id_token expires in 5 MINUTES — call /userinfo before it expires
4. Currently NO PASSWORD CHECK — any password works, only email is verified
5. To add password → add password column to customer_details and enable Hash::check() in OidcService::issueAuthCode()
6. NEVER commit keys/private.pem to git — add keys/ to .gitignore
7. In production change APP_URL in .env to your real domain
