- هدرهای سفارشی را نمیتوان با فرمهای معمولی ارسال کرد. این روش برای کار با فرمهای معمولی POST غیر Ajax ای طراحی نشده. برای آنها (فرمهای post back ای) از روش کار با کوکیها استفاده کنید که به صورت خودکار توسط مرورگر ارسال میشوند و نیاز به تنظیم خاصی ندارند: «اعتبارسنجی مبتنی بر کوکیها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- البته OnMessageReceived را در صفحهی جاری جستجو کنید. امکان ارسال توکنها به صورت فیلدهای سفارشی هم وجود دارد که در سمت سرور باید آنها را پردازش کنید.
در قسمت سوم، کار ورود به سیستم و امکان مشاهدهی صفحهی محافظت شدهی پس از لاگین را پیاده سازی کردیم. در این قسمت میخواهیم امکان دسترسی به مسیر http://localhost:4200/protectedPage را کنترل کنیم. تا اینجا اگر کاربر بدون لاگین کردن نیز این مسیر را درخواست کند، میتواند حداقل ساختار ابتدایی آنرا مشاهده کند که باید مدیریت شود و این مدیریت دسترسی میتواند بر اساس وضعیت لاگین کاربر و همچنین نقشهای او در سیستم باشد:
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
اما با توجه به اینکه در زمان لاگین، نقشهای او (و سایر Claims دلخواه) نیز به توکن دسترسی نهایی اضافه میشوند، میتوان این کوئری گرفتن مدام از سرور را تبدیل به روش بسیار سریعتر استخراج آنها از توکنی که هم اکنون در کش مرورگر ذخیره شدهاست، کرد.
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
به همین جهت متدی را برای تبدیل این اطلاعات به شیء کاربر، ایجاد خواهیم کرد و در سراسر برنامه از این اطلاعات آماده، برای تعیین دسترسی او به قسمتهای مختلف برنامهی سمت کلاینت، استفاده میکنیم.
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
پس از آن متد getAuthUser را جهت استخراج خواص ویژهی توکن دسترسی decode شدهی فوق، به صورت ذیل به سرویس Auth اضافه میکنیم:
کار با این متد بسیار سریع است و نیازی به رفت و برگشتی به سمت سرور ندارد؛ چون تمام اطلاعات استخراجی توسط آن هم اکنون در کش سریع مرورگر کلاینت موجود هستند. استفادهی از متد Object.freeze هم سبب read-only شدن این خروجی میشود.
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
که در اینجا ساختار اینترفیس AuthGuardPermission، در فایل جدید app\core\models\auth-guard-permission.ts به صورت ذیل تعریف شدهاست:
به این ترتیب هر قسمتی از برنامه که نیاز به اطلاعات سطوح دسترسی مسیری را داشت، ابتدا به دنبال route.data["permission"] خواهد گشت (کلیدی به نام permission در خاصیت data یک مسیر) و سپس اطلاعات آنرا بر اساس ساختار اینترفیس AuthGuardPermission، تحلیل میکند.
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
با این خروجی که سبب درج خودکار آن در قسمت declaration فایل authentication.module نیز میشود:
سپس به فایل authentication-routing.module.ts مراجعه کرده و مسیریابی آنرا نیز اضافه میکنیم:
قالب access-denied.component.html را نیز به صورت ذیل تکمیل میکنیم:
که دکمهی Back آن به کمک سرویس Location، صورت ذیل پیاده سازی شدهاست:
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
متد some در جاوا اسکریپت شبیه به متد Any در C# LINQ عمل میکند. همچنین در مقایسهی صورت گرفته، با توجه به اینکه user.roles را پیشتر به LowerCase تبدیل کردیم، حساسیتی بین نقش Admin و admin و کلا کوچکی و بزرگی نام نقشها وجود ندارد.
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
در اینجا در ابتدا وضعیت لاگین کاربر بررسی میگردد. این وضعیت نیز از طریق سرویس Auth که به سازندهی کلاس تزریق شدهاست، تامین میشود. اگر کاربر هنوز لاگین نکرده باشد، به صفحهی عدم دسترسی هدایت خواهد شد.
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
و پس از لاگین موفق، در صورت وجود این کوئری استرینگ، هدایت خودکار کاربر، به مسیر returnUrl پیشین صورت خواهد گرفت:
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
در آخر برای اعمال این Guard جدید، به فایل dashboard-routing.module.ts مراجعه کرده و خاصیت canActivate را مقدار دهی میکنیم:
به این ترتیب با درخواست این مسیر، پیش از فعالسازی و نمایش آن، توسط AuthGuard معرفی شدهی به آن، کار بررسی وضعیت کاربر و نقشهای او که از خاصیت permission خاصیت data دریافت میشوند، صورت گرفته و اگر عملیات تعیین اعتبار اطلاعات با موفقیت به پایان رسید، آنگاه کاربر مجوز دسترسی به این قسمت از برنامه را خواهد یافت.
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت سوم - بررسی مفاهیم OpenID Connect
پیش از نصب و راه اندازی IdentityServer، نیاز است با یک سری از مفاهیم اساسی پروتکل OpenID Connect، مانند Identity token ،Client types ،Flow و Endpoints، آشنا شویم تا بتوانیم از امکانات این IDP ویژه استفاده و آنها را تنظیم کنیم. بدون آشنایی با این مفاهیم، اتصال برنامهای که در قسمت قبل تدارک دیدیم به IdentityServer میسر نیست.
پروتکل OpenID Connect چگونه کار میکند؟
در انتهای قسمت اول این سری، پروتکل OpenID Connect معرفی شد. در ادامه جزئیات بیشتری از این پروتکل را بررسی میکنیم.
هر برنامهی کلاینت متصل به WebAPI مثال قسمت قبل، نیاز به دانستن هویت کاربر وارد شدهی به آنرا دارد. در اینجا به این برنامهی کلاینت، اصطلاحا relying party هم گفته میشود؛ از این جهت که این برنامهی کلاینت، به برنامهی Identity provider و یا به اختصار IDP، جهت دریافت نتیجهی اعتبارسنجی کاربر، وابستهاست. برنامهی کلاینت یک درخواست Authentication را به سمت IDP ارسال میکند. به این ترتیب کاربر به صورت موقت از برنامهی جاری خارج شده و به برنامهی IDP منتقل میشود. در برنامهی IDP است که کاربر مشخص میکند کیست؛ برای مثال با ارائهی نام کاربری و کلمهی عبور. پس از این مرحله، در صورت تائید هویت کاربر، برنامهی IDP یک Identity Token را تولید و امضاء میکند. سپس برنامهی IDP کاربر را مجددا به برنامهی کلاینت اصلی هدایت میکند و Identity Token را در اختیار آن کلاینت قرار میدهد. در اینجا برنامهی کلاینت، این توکن هویت را دریافت و اعتبارسنجی میکند. اگر این اعتبارسنجی با موفقیت انجام شود، اکنون کاربر تعیین اعتبار شده و هویت او جهت استفادهی از قسمتهای مختلف برنامه مشخص میشود. در برنامههای ASP.NET Core، این توکن هویت، پردازش و بر اساس آن یکسری Claims تولید میشوند که در اغلب موارد به صورت یک کوکی رمزنگاری شده در سمت کلاینت ذخیره میشوند.
به این ترتیب مرورگر با هر درخواستی از سمت کاربر، این کوکی را به صورت خودکار به سمت برنامهی کلاینت ارسال میکند و از طریق آن، هویت کاربر بدون نیاز به مراجعهی مجدد به IDP، استخراج و استفاده میشود.
مراحل انتقال کاربر به IDP، صدور توکن هویت، بازگشت مجدد به برنامهی کلاینت، اعتبارسنجی، استخراج Claims و ذخیرهی آن به صورت یک کوکی رمزنگاری شده را در تصویر فوق ملاحظه میکنید. بنابراین در حین کار با یک IDP، مرحلهی لاگین به سیستم، دیگر در برنامه یا برنامههای کلاینت قرار ندارد. در اینجا دو فلش به سمت IDP و سپس به سمت کلاینت را بخاطر بسپارید. در ادامه از آنها برای توضیح Flow و Endpoints استفاده خواهیم کرد.
البته OpenID Connect برای کار همزمان با انواع و اقسام برنامههای کلاینت طراحی شدهاست؛ مانند برنامهی سمت سرور MVC، برنامههای سمت کلاینت جاوا اسکریپتی مانند Angular و برنامههای موبایل. برای این منظور باید در IDP نوع کلاینت و یکسری از تنظیمات مرتبط با آنرا مشخص کرد.
کلاینتهای عمومی و محرمانه
زمانیکه قرار است با یک IDP کار کنیم، این IDP باید بتواند بین یک برنامهی حسابداری و یک برنامهی پرسنلی که از آن برای احراز هویت استفاده میکنند، تفاوت قائل شود و آنها را شناسایی کند.
- کلاینت محرمانه (Confidential Client)
هر کلاینت با یک client-id و یک client-secret شناخته میشود. کلاینتی که بتواند محرمانگی این اطلاعات را حفظ کند، کلاینت محرمانه نامیده میشود.
در اینجا هر کاربر، اطلاعات هویت خود را در IDP وارد میکند. اما اطلاعات تعیین هویت کلاینتها در سمت کلاینتها ذخیره میشوند. برای مثال برنامههای وب ASP.NET Core میتوانند هویت کلاینت خود را به نحو امنی در سمت سرور ذخیره کنند و این اطلاعات، قابل دسترسی توسط کاربران آن برنامه نیستند.
- کلاینت عمومی (Public Client)
این نوع کلاینتها نمیتوانند محرمانگی هویت خود را حفظ و تضمین کنند؛ مانند برنامههای جاوا اسکریپتی Angular و یا برنامههای موبایل که بر روی وسایل الکترونیکی کاربران اجرا میشوند. در این حالت هرچقدر هم سعی کنیم، چون کاربران به اصل این برنامهها دسترسی دارند، نمیتوان محرمانگی اطلاعات ذخیره شدهی در آنها را تضمین کرد.
مفهوم OpenID Connect Endpoints
در تصویر ابتدای بحث، دو فلش را مشاهده میکنید؛ برای مثال چگونه میتوان به Identity token دسترسی یافت (Authentication) و همچنین زمانیکه صحبت از Authorization میشود، چگونه میتوان Access tokens را دریافت کرد. اینکه این مراحله چگونه کار میکنند، توسط Flow مشخص میشود. Flow مشخص میکند که چگونه باید توکنها از سمت IDP به سمت کلاینت بازگشت داده شوند. بسته به نوع کلاینتها که در مورد آنها بحث شد و نیازمندیهای برنامه، باید از Flow مناسبی استفاده کرد.
هر Flow با Endpoint متفاوتی ارتباط برقرار میکند. این Endpointها در حقیقت جایگزین راهحلهای خانگی تولید برنامههای IDP هستند.
- در ابتدا یک Authorization Endpoint وجود دارد که در سطح IDP عمل میکند. این مورد همان انتهای فلش اول ارسال درخواست به سمت IDP است؛ در تصویر ابتدای بحث. کار این Endpoint، بازگشت Identity token جهت انجام عملیات Authentication و بازگشت Access token برای تکمیل عملیات Authorization است. این عملیات نیز توسط Redirection کلاینت انجام میشود (هدایت کاربر به سمت برنامهی IDP، دریافت توکنها و سپس هدایت مجدد به سمت برنامهی کلاینت اصلی).
نکتهی مهم: استفادهی از TLS و یا همان پروتکل HTTPS برای کار با OpenID Connect Endpoints اجباری است و بدون آن نمیتوانید با این سیستم کار کنید. به عبارتی در اینجا ترافیک بین کلاینت و IDP، همواره باید رمزنگاری شده باشد.
البته مزیت کار با ASP.NET Core 2.1، یکپارچگی بهتر و پیشفرض آن با پروتکل HTTPS است؛ تا حدی که مثال پیشفرض local آن به همراه یک مجوز موقتی SSL نصب شدهی توسط SDK آن کار میکند.
- پس از Authorization Endpoint که در مورد آن توضیح داده شد، یک Redirection Endpoint وجود دارد. در ابتدای کار، کلاینت با یک Redirect به سمت IDP هدایت میشود و پس از احراز هویت، مجددا کاربر به سمت کلاینت Redirect خواهد شد. به آدرسی که IDP کاربر را به سمت کلاینت Redirect میکند، Redirection Endpoint میگویند و در سطح کلاینت تعریف میشود. برنامهی IDP، اطلاعات تولیدی خود را مانند انواع توکنها، به سمت این Endpoint که در سمت کلاینت قرار دارد، ارسال میکند.
- پس از آن یک Token Endpoint نیز وجود دارد که در سطح IDP تعریف میشود. این Endpoint، آدرسی است در سمت IDP، که برنامهی کلاینت میتواند با برنامه نویسی، توکنهایی را از آن درخواست کند. این درخواست عموما از نوع HTTP Post بدون Redirection است.
مفهوم OpenID Connect Flow
- اولین Flow موجود، Authorization Code Flow است. کار آن بازگشت کدهای Authorization از Authorization Endpoint و همچنین توکنها از طریق Token Endpoint میباشد. در ایجا منظور از «کدهای Authorization»، اطلاعات دسترسی با طول عمر کوتاه است. هدف Authorization Code این است که مشخص کند، کاربری که به IDP لاگین کردهاست، همانی است که Flow را از طریق برنامهی وب کلاینت، شروع کردهاست. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد.
- Implicit Flow، تنها توکنهای تولیدی را توسط Authorization Endpoint بازگشت میدهد و در اینجا خبری از بازگشت «کدهای Authorization» نیست. بنابراین از Token Endpoint استفاده نمیکند. این نوع Flow، برای کلاینتهای عمومی مناسب است. در اینجا کار client authentication انجام نمیشود؛ از این جهت که کلاینتهای عمومی، مناسب ذخیره سازی client-secret نیستند. به همین جهت در اینجا امکان دسترسی به Refresh token و توکنهایی با طول عمر بالا میسر نیست. این نوع از Flow، ممکن است توسط کلاینتهای محرمانه نیز استفاده شود.
- Hybrid Flow، تعدادی از توکنها را توسط Authorization Endpoint و تعدادی دیگر را توسط Token Endpoint بازگشت میدهد؛ بنابراین ترکیبی از دو Flow قبلی است. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد. از این نوع Flow ممکن است برای native mobile apps نیز استفاده شود.
آگاهی از انواع Flowها، انتخاب نوع صحیح آنها را میسر میکند که در نتیجه منتهی به مشکلات امنیتی نخواهند شد. برای مثال Hybrid Flow توسط پشتیبانی از Refresh token امکان تمدید توکن جاری و بالا بردن طول عمر آنرا دارد و این طول عمر بالا بهتر است به کلاینتهای اعتبارسنجی شده ارائه شود. برای اعتبارسنجی یک کلاینت، نیاز به client-secret داریم و برای مثال برنامههای جاوا اسکریپتی نمیتوانند محل مناسبی برای ذخیره سازی client-secret باشند؛ چون از نوع کلاینتهای عمومی محسوب میشوند. بنابراین نباید از Hybrid Flow برای برنامههای Angular استفاده کرد. هرچند انتخاب این مساله صرفا به شما بر میگردد و چیزی نمیتواند مانع آن شود. برای مثال میتوان Hybrid Flow را با برنامههای Angular هم بکار برد؛ هرچند ایدهی خوبی نیست.
انتخاب OpenID Connect Flow مناسب برای یک برنامهی کلاینت از نوع ASP.NET Core
برنامههای ASP.NET Core، از نوع کلاینتهای محرمانه بهشمار میروند. بنابراین در اینجا میتوان تمام Flowهای یاد شده را انتخاب کرد. در برنامههای سمت سرور وب، به ویژگی به روز رسانی توکن نیاز است. بنابراین باید دسترسی به Refresh token را نیز داشت که توسط Implicit Flow پشتیبانی نمیشود. به همین جهت از Implicit Flow در اینجا استفاده نمیکنیم. پیش از ارائهی OpenID Connect، تنها Flow مورد استفادهی در برنامههای سمت سرور وب، همان Authorization Code Flow بود. در این Flow تمام توکنها توسط Token Endpoint بازگشت داده میشوند. اما Hybrid Flow نسبت به آن این مزیتها را دارد:
- ابتدا اجازه میدهد تا Identity token را از IDP دریافت کنیم. سپس میتوان آنرا بدون دریافت توکن دسترسی، تعیین اعتبار کرد.
- در ادامه OpenID Connect این Identity token را به یک توکن دسترسی، متصل میکند.
به همین جهت OpenID Connect نسبت به OAuth 2 ارجحیت بیشتری پیدا میکند.
پس از آشنایی با این مقدمات، در قسمت بعدی، کار نصب و راه اندازی IdentityServer را انجام خواهیم داد.
پروتکل OpenID Connect چگونه کار میکند؟
در انتهای قسمت اول این سری، پروتکل OpenID Connect معرفی شد. در ادامه جزئیات بیشتری از این پروتکل را بررسی میکنیم.
هر برنامهی کلاینت متصل به WebAPI مثال قسمت قبل، نیاز به دانستن هویت کاربر وارد شدهی به آنرا دارد. در اینجا به این برنامهی کلاینت، اصطلاحا relying party هم گفته میشود؛ از این جهت که این برنامهی کلاینت، به برنامهی Identity provider و یا به اختصار IDP، جهت دریافت نتیجهی اعتبارسنجی کاربر، وابستهاست. برنامهی کلاینت یک درخواست Authentication را به سمت IDP ارسال میکند. به این ترتیب کاربر به صورت موقت از برنامهی جاری خارج شده و به برنامهی IDP منتقل میشود. در برنامهی IDP است که کاربر مشخص میکند کیست؛ برای مثال با ارائهی نام کاربری و کلمهی عبور. پس از این مرحله، در صورت تائید هویت کاربر، برنامهی IDP یک Identity Token را تولید و امضاء میکند. سپس برنامهی IDP کاربر را مجددا به برنامهی کلاینت اصلی هدایت میکند و Identity Token را در اختیار آن کلاینت قرار میدهد. در اینجا برنامهی کلاینت، این توکن هویت را دریافت و اعتبارسنجی میکند. اگر این اعتبارسنجی با موفقیت انجام شود، اکنون کاربر تعیین اعتبار شده و هویت او جهت استفادهی از قسمتهای مختلف برنامه مشخص میشود. در برنامههای ASP.NET Core، این توکن هویت، پردازش و بر اساس آن یکسری Claims تولید میشوند که در اغلب موارد به صورت یک کوکی رمزنگاری شده در سمت کلاینت ذخیره میشوند.
به این ترتیب مرورگر با هر درخواستی از سمت کاربر، این کوکی را به صورت خودکار به سمت برنامهی کلاینت ارسال میکند و از طریق آن، هویت کاربر بدون نیاز به مراجعهی مجدد به IDP، استخراج و استفاده میشود.
مراحل انتقال کاربر به IDP، صدور توکن هویت، بازگشت مجدد به برنامهی کلاینت، اعتبارسنجی، استخراج Claims و ذخیرهی آن به صورت یک کوکی رمزنگاری شده را در تصویر فوق ملاحظه میکنید. بنابراین در حین کار با یک IDP، مرحلهی لاگین به سیستم، دیگر در برنامه یا برنامههای کلاینت قرار ندارد. در اینجا دو فلش به سمت IDP و سپس به سمت کلاینت را بخاطر بسپارید. در ادامه از آنها برای توضیح Flow و Endpoints استفاده خواهیم کرد.
البته OpenID Connect برای کار همزمان با انواع و اقسام برنامههای کلاینت طراحی شدهاست؛ مانند برنامهی سمت سرور MVC، برنامههای سمت کلاینت جاوا اسکریپتی مانند Angular و برنامههای موبایل. برای این منظور باید در IDP نوع کلاینت و یکسری از تنظیمات مرتبط با آنرا مشخص کرد.
کلاینتهای عمومی و محرمانه
زمانیکه قرار است با یک IDP کار کنیم، این IDP باید بتواند بین یک برنامهی حسابداری و یک برنامهی پرسنلی که از آن برای احراز هویت استفاده میکنند، تفاوت قائل شود و آنها را شناسایی کند.
- کلاینت محرمانه (Confidential Client)
هر کلاینت با یک client-id و یک client-secret شناخته میشود. کلاینتی که بتواند محرمانگی این اطلاعات را حفظ کند، کلاینت محرمانه نامیده میشود.
در اینجا هر کاربر، اطلاعات هویت خود را در IDP وارد میکند. اما اطلاعات تعیین هویت کلاینتها در سمت کلاینتها ذخیره میشوند. برای مثال برنامههای وب ASP.NET Core میتوانند هویت کلاینت خود را به نحو امنی در سمت سرور ذخیره کنند و این اطلاعات، قابل دسترسی توسط کاربران آن برنامه نیستند.
- کلاینت عمومی (Public Client)
این نوع کلاینتها نمیتوانند محرمانگی هویت خود را حفظ و تضمین کنند؛ مانند برنامههای جاوا اسکریپتی Angular و یا برنامههای موبایل که بر روی وسایل الکترونیکی کاربران اجرا میشوند. در این حالت هرچقدر هم سعی کنیم، چون کاربران به اصل این برنامهها دسترسی دارند، نمیتوان محرمانگی اطلاعات ذخیره شدهی در آنها را تضمین کرد.
مفهوم OpenID Connect Endpoints
در تصویر ابتدای بحث، دو فلش را مشاهده میکنید؛ برای مثال چگونه میتوان به Identity token دسترسی یافت (Authentication) و همچنین زمانیکه صحبت از Authorization میشود، چگونه میتوان Access tokens را دریافت کرد. اینکه این مراحله چگونه کار میکنند، توسط Flow مشخص میشود. Flow مشخص میکند که چگونه باید توکنها از سمت IDP به سمت کلاینت بازگشت داده شوند. بسته به نوع کلاینتها که در مورد آنها بحث شد و نیازمندیهای برنامه، باید از Flow مناسبی استفاده کرد.
هر Flow با Endpoint متفاوتی ارتباط برقرار میکند. این Endpointها در حقیقت جایگزین راهحلهای خانگی تولید برنامههای IDP هستند.
- در ابتدا یک Authorization Endpoint وجود دارد که در سطح IDP عمل میکند. این مورد همان انتهای فلش اول ارسال درخواست به سمت IDP است؛ در تصویر ابتدای بحث. کار این Endpoint، بازگشت Identity token جهت انجام عملیات Authentication و بازگشت Access token برای تکمیل عملیات Authorization است. این عملیات نیز توسط Redirection کلاینت انجام میشود (هدایت کاربر به سمت برنامهی IDP، دریافت توکنها و سپس هدایت مجدد به سمت برنامهی کلاینت اصلی).
نکتهی مهم: استفادهی از TLS و یا همان پروتکل HTTPS برای کار با OpenID Connect Endpoints اجباری است و بدون آن نمیتوانید با این سیستم کار کنید. به عبارتی در اینجا ترافیک بین کلاینت و IDP، همواره باید رمزنگاری شده باشد.
البته مزیت کار با ASP.NET Core 2.1، یکپارچگی بهتر و پیشفرض آن با پروتکل HTTPS است؛ تا حدی که مثال پیشفرض local آن به همراه یک مجوز موقتی SSL نصب شدهی توسط SDK آن کار میکند.
- پس از Authorization Endpoint که در مورد آن توضیح داده شد، یک Redirection Endpoint وجود دارد. در ابتدای کار، کلاینت با یک Redirect به سمت IDP هدایت میشود و پس از احراز هویت، مجددا کاربر به سمت کلاینت Redirect خواهد شد. به آدرسی که IDP کاربر را به سمت کلاینت Redirect میکند، Redirection Endpoint میگویند و در سطح کلاینت تعریف میشود. برنامهی IDP، اطلاعات تولیدی خود را مانند انواع توکنها، به سمت این Endpoint که در سمت کلاینت قرار دارد، ارسال میکند.
- پس از آن یک Token Endpoint نیز وجود دارد که در سطح IDP تعریف میشود. این Endpoint، آدرسی است در سمت IDP، که برنامهی کلاینت میتواند با برنامه نویسی، توکنهایی را از آن درخواست کند. این درخواست عموما از نوع HTTP Post بدون Redirection است.
مفهوم OpenID Connect Flow
- اولین Flow موجود، Authorization Code Flow است. کار آن بازگشت کدهای Authorization از Authorization Endpoint و همچنین توکنها از طریق Token Endpoint میباشد. در ایجا منظور از «کدهای Authorization»، اطلاعات دسترسی با طول عمر کوتاه است. هدف Authorization Code این است که مشخص کند، کاربری که به IDP لاگین کردهاست، همانی است که Flow را از طریق برنامهی وب کلاینت، شروع کردهاست. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد.
- Implicit Flow، تنها توکنهای تولیدی را توسط Authorization Endpoint بازگشت میدهد و در اینجا خبری از بازگشت «کدهای Authorization» نیست. بنابراین از Token Endpoint استفاده نمیکند. این نوع Flow، برای کلاینتهای عمومی مناسب است. در اینجا کار client authentication انجام نمیشود؛ از این جهت که کلاینتهای عمومی، مناسب ذخیره سازی client-secret نیستند. به همین جهت در اینجا امکان دسترسی به Refresh token و توکنهایی با طول عمر بالا میسر نیست. این نوع از Flow، ممکن است توسط کلاینتهای محرمانه نیز استفاده شود.
- Hybrid Flow، تعدادی از توکنها را توسط Authorization Endpoint و تعدادی دیگر را توسط Token Endpoint بازگشت میدهد؛ بنابراین ترکیبی از دو Flow قبلی است. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد. از این نوع Flow ممکن است برای native mobile apps نیز استفاده شود.
آگاهی از انواع Flowها، انتخاب نوع صحیح آنها را میسر میکند که در نتیجه منتهی به مشکلات امنیتی نخواهند شد. برای مثال Hybrid Flow توسط پشتیبانی از Refresh token امکان تمدید توکن جاری و بالا بردن طول عمر آنرا دارد و این طول عمر بالا بهتر است به کلاینتهای اعتبارسنجی شده ارائه شود. برای اعتبارسنجی یک کلاینت، نیاز به client-secret داریم و برای مثال برنامههای جاوا اسکریپتی نمیتوانند محل مناسبی برای ذخیره سازی client-secret باشند؛ چون از نوع کلاینتهای عمومی محسوب میشوند. بنابراین نباید از Hybrid Flow برای برنامههای Angular استفاده کرد. هرچند انتخاب این مساله صرفا به شما بر میگردد و چیزی نمیتواند مانع آن شود. برای مثال میتوان Hybrid Flow را با برنامههای Angular هم بکار برد؛ هرچند ایدهی خوبی نیست.
انتخاب OpenID Connect Flow مناسب برای یک برنامهی کلاینت از نوع ASP.NET Core
برنامههای ASP.NET Core، از نوع کلاینتهای محرمانه بهشمار میروند. بنابراین در اینجا میتوان تمام Flowهای یاد شده را انتخاب کرد. در برنامههای سمت سرور وب، به ویژگی به روز رسانی توکن نیاز است. بنابراین باید دسترسی به Refresh token را نیز داشت که توسط Implicit Flow پشتیبانی نمیشود. به همین جهت از Implicit Flow در اینجا استفاده نمیکنیم. پیش از ارائهی OpenID Connect، تنها Flow مورد استفادهی در برنامههای سمت سرور وب، همان Authorization Code Flow بود. در این Flow تمام توکنها توسط Token Endpoint بازگشت داده میشوند. اما Hybrid Flow نسبت به آن این مزیتها را دارد:
- ابتدا اجازه میدهد تا Identity token را از IDP دریافت کنیم. سپس میتوان آنرا بدون دریافت توکن دسترسی، تعیین اعتبار کرد.
- در ادامه OpenID Connect این Identity token را به یک توکن دسترسی، متصل میکند.
به همین جهت OpenID Connect نسبت به OAuth 2 ارجحیت بیشتری پیدا میکند.
پس از آشنایی با این مقدمات، در قسمت بعدی، کار نصب و راه اندازی IdentityServer را انجام خواهیم داد.
Warp is a modern, Rust-based terminal with AI built in so you and your team can build great software, faster.
Warp is the terminal reimagined with AI and collaborative tools for better productivity.
اشتراکها
انتشار Visual Studio 2022 Preview 2
اشتراکها
معرفی ویژگی Hot Reload در .NET 6.0.
اشتراکها