344 lines
9.5 KiB
Markdown
344 lines
9.5 KiB
Markdown
|
|
# Google OAuth (OIDC) + AzureAD Shared SSO Service
|
||
|
|
|
||
|
|
Tai lieu nay tong hop toan bo flow dang nhap OAuth/OIDC theo muc coding cho codebase hien tai.
|
||
|
|
|
||
|
|
Pham vi:
|
||
|
|
- Giu nguyen login username/password cu.
|
||
|
|
- Dung chung 1 service cho AzureAD SSO va Google OAuth OIDC.
|
||
|
|
- Van tao/cap nhat user noi bo trong DB.
|
||
|
|
- User moi vao role Pending (khong co permission), doi admin cap quyen.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 1) Trang thai implementation hien tai
|
||
|
|
|
||
|
|
Da hoan tat trong code:
|
||
|
|
- Shared service theo provider key: SsoService.
|
||
|
|
- Controller da provider: OAuthController.
|
||
|
|
- Controller AzureAD cu (SsoController) da chay tren shared service.
|
||
|
|
- Config da provider qua OAuthProviders trong appsettings.
|
||
|
|
|
||
|
|
File chinh:
|
||
|
|
- TTMT.CompManageWeb/Program.cs
|
||
|
|
- TTMT.CompManageWeb/Interfaces/ISsoService.cs
|
||
|
|
- TTMT.CompManageWeb/Services/SsoSerivce.cs
|
||
|
|
- TTMT.CompManageWeb/Controllers/OAuthController.cs
|
||
|
|
- TTMT.CompManageWeb/Controllers/SsoController.cs
|
||
|
|
- TTMT.CompManageWeb/Dtos/Auth/OAuthProviderOptions.cs
|
||
|
|
- TTMT.CompManageWeb/Dtos/Auth/OAuthProvidersOptions.cs
|
||
|
|
- TTMT.CompManageWeb/appsettings.json
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 2) Kien truc luong dang nhap
|
||
|
|
|
||
|
|
### 2.1 Luong OAuth Google
|
||
|
|
|
||
|
|
1. FE redirect user den:
|
||
|
|
- GET /api/auth/oauth/google/login?returnUrl=<frontend-url>
|
||
|
|
2. Backend build authorize URL va redirect qua Google.
|
||
|
|
3. Google callback ve backend:
|
||
|
|
- GET /api/auth/oauth/google/callback?code=...&state=...
|
||
|
|
4. Backend:
|
||
|
|
- Exchange code -> token endpoint.
|
||
|
|
- Validate id_token.
|
||
|
|
- Lay email/name.
|
||
|
|
- Kiem tra domain.
|
||
|
|
- Upsert user noi bo.
|
||
|
|
- Issue JWT noi bo.
|
||
|
|
- Tao one-time code (2 phut).
|
||
|
|
- Redirect ve returnUrl?code=<one-time-code>.
|
||
|
|
5. FE goi API exchange:
|
||
|
|
- POST /api/auth/oauth/exchange
|
||
|
|
- Body: { "code": "..." }
|
||
|
|
6. Backend consume one-time code va tra payload login (token + role + permission).
|
||
|
|
|
||
|
|
### 2.2 Luong AzureAD cu
|
||
|
|
|
||
|
|
Van giu endpoint cu:
|
||
|
|
- GET /api/auth/sso/login
|
||
|
|
- GET /api/auth/sso/callback
|
||
|
|
- POST /api/auth/sso/exchange
|
||
|
|
|
||
|
|
Nhung ben trong da dung chung service theo provider azuread.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 3) Cau hinh bat buoc
|
||
|
|
|
||
|
|
### 3.1 appsettings.json
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"OAuthProviders": {
|
||
|
|
"DefaultProvider": "azuread",
|
||
|
|
"Providers": {
|
||
|
|
"google": {
|
||
|
|
"Authority": "https://accounts.google.com",
|
||
|
|
"AuthorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
|
||
|
|
"TokenEndpoint": "https://oauth2.googleapis.com/token",
|
||
|
|
"ClientId": "",
|
||
|
|
"ClientSecret": "",
|
||
|
|
"CallbackPath": "/api/auth/oauth/google/callback",
|
||
|
|
"AllowedDomain": "",
|
||
|
|
"PendingRoleName": "Pending",
|
||
|
|
"Scopes": "openid profile email",
|
||
|
|
"Issuer": "https://accounts.google.com",
|
||
|
|
"EmailClaim": "email",
|
||
|
|
"NameClaim": "name",
|
||
|
|
"HostedDomainClaimName": "hd",
|
||
|
|
"EnforceHostedDomainClaim": true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Giai thich nhanh:
|
||
|
|
- AllowedDomain:
|
||
|
|
- Rong: cho phep moi domain.
|
||
|
|
- Co gia tri (vd hust.edu.vn): chi cho email domain nay.
|
||
|
|
- EnforceHostedDomainClaim=true:
|
||
|
|
- Neu co claim hd trong token thi bat buoc phai khop AllowedDomain.
|
||
|
|
- Neu token khong co hd, he thong fallback check theo email domain.
|
||
|
|
- PendingRoleName: role duoc gan khi user moi dang nhap lan dau.
|
||
|
|
|
||
|
|
### 3.2 Google Cloud Console
|
||
|
|
|
||
|
|
1. Tao OAuth client (Web application).
|
||
|
|
2. Authorized redirect URIs:
|
||
|
|
- https://<your-domain>/api/auth/oauth/google/callback
|
||
|
|
- (dev) https://localhost:<port>/api/auth/oauth/google/callback
|
||
|
|
3. Lay ClientId, ClientSecret va dien vao appsettings (hoac secrets).
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 4) Dang ky DI va options
|
||
|
|
|
||
|
|
Trong Program.cs can co:
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
builder.Services.Configure<AzureAdOptions>(
|
||
|
|
builder.Configuration.GetSection(AzureAdOptions.SectionName));
|
||
|
|
|
||
|
|
builder.Services.Configure<OAuthProvidersOptions>(
|
||
|
|
builder.Configuration.GetSection(OAuthProvidersOptions.SectionName));
|
||
|
|
|
||
|
|
builder.Services.AddScoped<ISsoService, SsoService>();
|
||
|
|
```
|
||
|
|
|
||
|
|
OAuthProvidersOptions dung dictionary provider theo key (vd google, azuread, okta).
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 5) Thiet ke interface va service chung
|
||
|
|
|
||
|
|
### 5.1 Interface ISsoService
|
||
|
|
|
||
|
|
Interface da duoc mo rong thanh provider-aware:
|
||
|
|
- BuildAuthorizeUrl(provider, redirectUri, state)
|
||
|
|
- ExchangeCodeAsync(provider, code, redirectUri, ct)
|
||
|
|
- ValidateIdTokenAsync(provider, idToken, ct)
|
||
|
|
- IsAllowedDomain(provider, email, principal)
|
||
|
|
- UpsertUserAsync(provider, email, name, ct)
|
||
|
|
|
||
|
|
Va van giu overload cu de backward compatibility.
|
||
|
|
|
||
|
|
### 5.2 SsoService - logic tong
|
||
|
|
|
||
|
|
Core y tuong:
|
||
|
|
- Resolve config theo provider key.
|
||
|
|
- Provider nao khong co endpoint explicit thi suy ra tu metadata/authority.
|
||
|
|
- AzureAD duoc fallback tu AzureAdOptions de khong vo luong cu.
|
||
|
|
|
||
|
|
Phan quan trong:
|
||
|
|
|
||
|
|
1) ResolveProvider(...)
|
||
|
|
- Doc provider trong OAuthProviders.
|
||
|
|
- Neu key = azuread ma khong co trong OAuthProviders, fallback AzureAdOptions.
|
||
|
|
|
||
|
|
2) BuildAuthorizeUrl(...)
|
||
|
|
- Build URL authorize theo endpoint cua provider.
|
||
|
|
- Them scope, state, redirect_uri.
|
||
|
|
- Neu bat hosted-domain check thi them hd=<AllowedDomain>.
|
||
|
|
|
||
|
|
3) ExchangeCodeAsync(...)
|
||
|
|
- Goi token endpoint voi client_id, client_secret, code, redirect_uri.
|
||
|
|
- Parse OidcTokenResponse.
|
||
|
|
- Bat buoc co id_token (flow hien tai dang OIDC-centric).
|
||
|
|
|
||
|
|
4) ValidateIdTokenAsync(...)
|
||
|
|
- Lay metadata OIDC (.well-known/openid-configuration).
|
||
|
|
- Validate issuer, audience, signing key, lifetime.
|
||
|
|
|
||
|
|
5) IsAllowedDomain(...)
|
||
|
|
- Neu AllowedDomain rong -> cho phep.
|
||
|
|
- Neu khong rong -> check duoi email.
|
||
|
|
- Neu EnforceHostedDomainClaim bat va token co claim hd -> bat buoc hd trung AllowedDomain.
|
||
|
|
|
||
|
|
6) UpsertUserAsync(...)
|
||
|
|
- Tim user theo email (UserName).
|
||
|
|
- Neu ton tai: cap nhat ten + metadata update.
|
||
|
|
- Neu chua ton tai: tao user moi, Password = null, gan role PendingRoleName.
|
||
|
|
|
||
|
|
7) One-time code
|
||
|
|
- CreateOneTimeCodeAsync(...): tao code, luu bang SsoOneTimeCodes, het han sau 2 phut.
|
||
|
|
- ExchangeOneTimeCodeForLoginAsync(...): consume code, tra payload login final.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 6) Controller va contract API
|
||
|
|
|
||
|
|
### 6.1 OAuthController (da provider)
|
||
|
|
|
||
|
|
Route base: api/auth/oauth
|
||
|
|
|
||
|
|
1) Login
|
||
|
|
- GET /api/auth/oauth/{provider}/login?returnUrl=...
|
||
|
|
- Redirect sang provider authorize URL.
|
||
|
|
|
||
|
|
2) Callback
|
||
|
|
- GET /api/auth/oauth/{provider}/callback?code=...&state=...
|
||
|
|
- Xu ly token + domain + upsert + issue token + one-time code.
|
||
|
|
- Redirect ve FE voi query code.
|
||
|
|
|
||
|
|
3) Exchange
|
||
|
|
- POST /api/auth/oauth/exchange
|
||
|
|
|
||
|
|
Request:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"code": "one-time-code"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Response thanh cong:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"token": "<jwt-noi-bo>",
|
||
|
|
"name": "...",
|
||
|
|
"username": "email@domain",
|
||
|
|
"access": [1, 2, 3],
|
||
|
|
"role": {
|
||
|
|
"roleName": "Pending",
|
||
|
|
"priority": 99
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 SsoController (AzureAD legacy)
|
||
|
|
|
||
|
|
Van giu route cu, nhung da goi service chung voi provider azuread.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 7) Rule user va role Pending
|
||
|
|
|
||
|
|
Rule bat buoc:
|
||
|
|
- User OAuth lan dau phai duoc tao trong bang UserAccounts.
|
||
|
|
- User moi phai vao role Pending.
|
||
|
|
- Role Pending khong co permission nao (PermissionRoles.IsChecked = 0 hoac khong co row).
|
||
|
|
- Admin se cap role/permission sau.
|
||
|
|
|
||
|
|
Model lien quan:
|
||
|
|
- UserAccounts
|
||
|
|
- Roles
|
||
|
|
- PermissionRoles
|
||
|
|
- SsoOneTimeCodes
|
||
|
|
|
||
|
|
Kiem tra nhanh trong DB:
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 1) Kiem tra role Pending co ton tai
|
||
|
|
SELECT Id, RoleName, Priority
|
||
|
|
FROM "Roles"
|
||
|
|
WHERE "RoleName" = 'Pending';
|
||
|
|
|
||
|
|
-- 2) Kiem tra Pending role khong co permission active
|
||
|
|
SELECT pr.*
|
||
|
|
FROM "PermissionRoles" pr
|
||
|
|
JOIN "Roles" r ON r."Id" = pr."RoleId"
|
||
|
|
WHERE r."RoleName" = 'Pending'
|
||
|
|
AND pr."IsChecked" = 1;
|
||
|
|
|
||
|
|
-- 3) Kiem tra user tao boi OAuth
|
||
|
|
SELECT "Id", "UserName", "Password", "RoleId", "CreatedBy", "UpdatedBy"
|
||
|
|
FROM "UserAccounts"
|
||
|
|
WHERE "UserName" = '<email-user>';
|
||
|
|
```
|
||
|
|
|
||
|
|
Ky vong:
|
||
|
|
- Query #2 tra ve 0 rows.
|
||
|
|
- User moi co Password = null, CreatedBy = 'SSO'.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 8) Frontend integration (chi tiet)
|
||
|
|
|
||
|
|
1. Nguoi dung bam nut Google Login:
|
||
|
|
- Window.location -> GET /api/auth/oauth/google/login?returnUrl=<FE_CALLBACK_URL>
|
||
|
|
2. Sau callback backend, FE nhan code tu query string.
|
||
|
|
3. FE goi:
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /api/auth/oauth/exchange
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{ "code": "<code-tu-query>" }
|
||
|
|
```
|
||
|
|
|
||
|
|
4. FE luu token va xu ly permission/role giong login cu.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 9) Luu y bao mat va hardening
|
||
|
|
|
||
|
|
Nen lam tiep:
|
||
|
|
- Validate returnUrl theo allowlist de tranh open redirect.
|
||
|
|
- Luu va verify state/nonce server-side (cache/redis).
|
||
|
|
- Dung IHttpClientFactory thay new HttpClient() de kiem soat timeout/retry.
|
||
|
|
- Dua ClientSecret sang secret manager/env var.
|
||
|
|
- Bat HTTPS va secure cookie policy day du.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 10) Troubleshooting
|
||
|
|
|
||
|
|
1) Loi IdToken not found in token response
|
||
|
|
- Provider dang khong tra OIDC token.
|
||
|
|
- Kiem tra scope co openid chua.
|
||
|
|
|
||
|
|
2) Loi Email not found in token
|
||
|
|
- Kiem tra claim trong Google token (email, preferred_username).
|
||
|
|
|
||
|
|
3) Loi Email domain is not allowed
|
||
|
|
- Check AllowedDomain va claim hd.
|
||
|
|
|
||
|
|
4) Loi Role 'Pending' not found
|
||
|
|
- Tao role Pending trong bang Roles.
|
||
|
|
|
||
|
|
5) Loi exchange code het han
|
||
|
|
- One-time code chi song 2 phut va chi dung 1 lan.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 11) Checklist test E2E truoc khi merge
|
||
|
|
|
||
|
|
- Login Google thanh cong voi account hop le.
|
||
|
|
- User moi duoc tao trong DB va role = Pending.
|
||
|
|
- Pending role khong co permission active.
|
||
|
|
- User cu login lai thi khong tao duplicate user.
|
||
|
|
- One-time code chi dung 1 lan.
|
||
|
|
- AllowedDomain rong cho phep tat ca domain.
|
||
|
|
- AllowedDomain co gia tri thi chan dung domain.
|
||
|
|
|
||
|
|
----------------------------------------
|
||
|
|
|
||
|
|
## 12) Ghi chu pham vi hien tai
|
||
|
|
|
||
|
|
Hien tai flow moi dang OIDC-centric (bat buoc id_token).
|
||
|
|
Neu can support OAuth thuan (khong co id_token), can bo sung nhanh:
|
||
|
|
- UserInfo endpoint call.
|
||
|
|
- Mapping email/name tu userinfo response.
|
||
|
|
- Branch logic trong callback de fallback userinfo.
|