پیاده سازی JSON Web Token با ASP.NET Web API 2.x
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: بیست دقیقه

- پیشنیار بحث «معرفی JSON Web Token»

پیاده سازی‌های زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو می‌توانید پیدا کنید. اما مشکلی که تمام آن‌ها دارند، شامل این موارد هستند:
- چون توکن‌های JWT، خودشمول هستند (در پیشنیاز بحث مطرح شده‌است)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی می‌تواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمه‌ی عبور او تغییر کند و یا سطح دسترسی‌های او کاهش یابند ... مهم نیست! باز هم می‌تواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بی‌معنا است. یعنی اگر برنامه‌ای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکن‌ها جایی ذخیره نمی‌شوند، عملا این logout بی‌مفهوم است و مجددا می‌توان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشده‌است که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکان‌های مختلفی می‌توان دسترسی لازم را جهت استفاده‌ی از قسمت‌های محافظت شده‌ی برنامه یافت (در صورت دسترسی، چندین نفر می‌توانند از آن استفاده کنند).

به همین جهت راه حلی عمومی برای ذخیره سازی توکن‌های صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که می‌تواند به عنوان پایه مباحث Authentication و Authorization برنامه‌های تک صفحه‌ای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شده‌است (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمی‌کند. سرویس‌های آن برای بکارگیری انواع و اقسام روش‌های ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.


نگاهی به برنامه


در اینجا تمام قابلیت‌های این پروژه را مشاهده می‌کنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژه‌ای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیه‌ی لاگین، برطرف می‌کند.
- پیاده سازی logout


بسته‌های پیشنیاز برنامه

پروژه‌ای که در اینجا بررسی شده‌است، یک پروژه‌ی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بسته‌ی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.Jwt
PM> Install-Package structuremap
PM> Install-Package structuremap.web
بسته‌ی Microsoft.Owin.Host.SystemWeb سبب می‌شود تا کلاس OwinStartup به صورت خودکار شناسایی و بارگذاری شود. این کلاسی است که کار تنظیمات اولیه‌ی JSON Web token را انجام می‌دهد و بسته‌ی Microsoft.Owin.Security.Jwt شامل تمام امکاناتی است که برای راه اندازی توکن‌های JWT نیاز داریم.
از structuremap هم برای تنظیمات تزریق وابستگی‌های برنامه استفاده شده‌است. به این صورت قسمت تنظیمات اولیه‌ی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویس‌های برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.


دریافت کدهای کامل برنامه

کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید. در ادامه صرفا قسمت‌های مهم این کدها را بررسی خواهیم کرد.


بررسی کلاس AppJwtConfiguration

کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکن‌های برنامه در فایل web.config، ایجاد شده‌است. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration
    tokenPath="/login"
    expirationMinutes="2"
    refreshTokenExpirationMinutes="60"
    jwtKey="This is my shared key, not so secret, secret!"
    jwtIssuer="http://localhost/"
    jwtAudience="Any" />
این قسمت جدید بر اساس configSection ذیل که به کلاس AppJwtConfiguration اشاره می‌کند، قابل استفاده شده‌است (بنابراین اگر فضای نام این کلاس را تغییر دادید، باید این قسمت را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود):
 <configSections>
    <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" />
</configSections>
- در اینجا tokenPath، یک مسیر دلخواه است. برای مثال در اینجا به مسیر login تنظیم شده‌است. برنامه‌ای که با Microsoft.Owin.Security.Jwt کار می‌کند، نیازی ندارد تا یک قسمت لاگین مجزا داشته باشد (مثلا یک کنترلر User و بعد یک اکشن متد اختصاصی Login). کار لاگین، در متد GrantResourceOwnerCredentials کلاس AppOAuthProvider انجام می‌شود. اینجا است که نام کاربری و کلمه‌ی عبور کاربری که به سمت سرور ارسال می‌شوند، توسط Owin دریافت و در اختیار ما قرار می‌گیرند.
- در این تنظیمات، دو زمان منقضی شدن را مشاهده می‌کنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد این‌ها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکن‌ها در سمت سرور استفاده می‌شود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.

یک نکته
جهت سهولت کار تزریق وابستگی‌ها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شده‌است و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفاده‌ی مستقیم از کلاس AppJwtConfiguration استفاده شده‌است.


بررسی کلاس OwinStartup

شروع به کار تنظیمات JWT و ورود آن‌ها به چرخه‌ی حیات Owin از کلاس OwinStartup آغاز می‌شود. در اینجا علت استفاده‌ی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگی‌های لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوه‌ی تهیه‌ی access token و همچنین refresh token ذکر می‌شوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفاده‌ی جهت امضای توکن‌های صادر شده، ذکر می‌شوند.


حداقل‌های بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکن‌های آن‌ها

همانطور که در ابتدای بحث عنوان شد، می‌خواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار می‌کند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافه‌تری به نام SerialNumber نیز در اینجا درنظر گرفته شده‌است. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمه‌ی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).

ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکن‌های صادر شده صورت می‌گیرد. توکن‌های صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آن‌ها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال می‌کند، ابتدا token او را دریافت کرده و سپس بررسی می‌کنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شده‌است یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی می‌کنیم که تمام توکن‌های یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفاده‌ی مجدد را نخواهد داشت.


مدیریت بانک اطلاعاتی و کلاس‌های سرویس برنامه

در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService
{
    void CreateUserToken(UserToken userToken);
    bool IsValidToken(string accessToken, int userId);
    void DeleteExpiredTokens();
    UserToken FindToken(string refreshTokenIdHash);
    void DeleteToken(string refreshTokenIdHash);
    void InvalidateUserTokens(int userId);
    void UpdateUserToken(int userId, string accessTokenHash);
}
کار سرویس TokenStore، ذخیره سازی و تعیین اعتبار توکن‌های صادر شده‌است. در اینجا ثبت یک توکن، بررسی صحت وجود یک توکن رسیده، حذف توکن‌های منقضی شده، یافتن یک توکن بر اساس هش توکن، حذف یک توکن بر اساس هش توکن، غیرمعتبر کردن و حذف تمام توکن‌های یک شخص و به روز رسانی توکن یک کاربر، پیش بینی شده‌اند.
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.

یک نکته:
در سرویس ذخیره سازی توکن‌ها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken)
{
   InvalidateUserTokens(userToken.OwnerUserId);
   _tokens.Add(userToken);
}
استفاده از InvalidateUserTokens در اینجا سبب خواهد شد با لاگین شخص و یا صدور توکن جدیدی برای او، تمام توکن‌های قبلی او حذف شوند. به این ترتیب امکان لاگین چندباره و یا یافتن دسترسی به منابع محافظت شده‌ی برنامه در سرور توسط چندین نفر (که به توکن شخص دسترسی یافته‌اند یا حتی تقاضای توکن جدیدی کرده‌اند)، میسر نباشد. همینکه توکن جدیدی صادر شود، تمام لاگین‌های دیگر این شخص غیرمعتبر خواهند شد.


ب) سرویس UsersService
public interface IUsersService
{
    string GetSerialNumber(int userId);
    IEnumerable<string> GetUserRoles(int userId);
    User FindUser(string username, string password);
    User FindUser(int userId);
    void UpdateUserLastActivityDate(int userId);
}
از کلاس سرویس کاربران، برای یافتن شماره سریال یک کاربر استفاده می‌شود. در مورد شماره سریال پیشتر بحث کردیم و هدف آن مشخص سازی وضعیت تغییر این موجودیت است. اگر کاربری اطلاعاتش تغییر کرد، این فیلد باید با یک Guid جدید مقدار دهی شود.
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمه‌ی عبور او (جهت مدیریت مرحله‌ی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شده‌اند.

ج) سرویس SecurityService
public interface ISecurityService
{
   string GetSha256Hash(string input);
}
در قسمت‌های مختلف این برنامه، هش SHA256 مورد استفاده قرار گرفته‌است که با توجه به مباحث تزریق وابستگی‌ها، کاملا قابل تعویض بوده و برنامه صرفا با اینترفیس آن کار می‌کند.


پیاده سازی قسمت لاگین و صدور access token

در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شده‌است. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی می‌شود. قسمت‌های مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر می‌توانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع می‌شود. چون با یک برنامه‌ی وب در حال کار هستیم، ClientId آن‌را نال درنظر می‌گیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت می‌تواند ClientId ویژه‌ای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهم‌ترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی می‌شود. اگر به کدهای آن دقت کنید،  خود owin دارای خاصیت‌های user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت می‌کند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login می‌توانید مشاهده کنید:
function doLogin() {
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            username: "Vahid",
            password: "1234",
            grant_type: "password"
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
url آن به همان مسیری که در فایل web.config تنظیم کردیم، اشاره می‌کند. فرمت data ایی که به سرور ارسال می‌شود، دقیقا باید به همین نحو باشد و content type آن نیز مهم است و owin فقط حالت form-urlencoded را پردازش می‌کند. به این ترتیب است که user name و password توسط owin قابل شناسایی شده و grant_type آن است که سبب فراخوانی GrantResourceOwnerCredentials می‌شود و مقدار آن نیز دقیقا باید password باشد (در حین لاگین).
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمه‌ی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقش‌های او نیز به عنوان Claim جدید به توکن اضافه می‌شوند.

در اینجا یک Claim سفارشی هم اضافه شده‌است:
 identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
کار آن ذخیره سازی userId کاربر، در توکن صادر شده‌است. به این صورت هربار که توکن به سمت سرور ارسال می‌شود، نیازی نیست تا یکبار از بانک اطلاعاتی بر اساس نام او، کوئری گرفت و سپس id او را یافت. این id در توکن امضاء شده، موجود است. نمونه‌ی نحوه‌ی کار با آن‌را در کنترلرهای این API می‌توانید مشاهده کنید. برای مثال در MyProtectedAdminApiController این اطلاعات از توکن استخراج شده‌اند (جهت نمایش مفهوم).

در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شده‌ی برای کاربر، استفاده کرده‌ایم. هش این access token را در بانک اطلاعاتی ذخیره می‌کنیم (جستجوی هش‌ها سریعتر هستند از جستجوی یک رشته‌ی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هش‌ها برای مهاجم قابل استفاده نیست).

اگر بخواهیم اطلاعات ارسالی به کاربر را پس از لاگین، نمایش دهیم، به شکل زیر خواهیم رسید:


در اینجا access_token همان JSON Web Token صادر شده‌است که برنامه‌ی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.

بنابراین خلاصه‌ی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد  ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام می‌شود. برای مثال نقش‌های او به توکن صادر شده اضافه می‌شوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام می‌دهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژه‌ای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال می‌شود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی قسمت صدور Refresh token

در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده می‌کنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمه‌ی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر می‌گیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحه‌ی لاگین هدایت کنیم. می‌توانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امن‌تر است چون نیازی به ارسال کلمه‌ی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شده‌ی در زمان لاگین، به صورت زیر عمل می‌شود:
function doRefreshToken() {
    // obtaining new tokens using the refresh_token should happen only if the id_token has expired.
    // it is a bad practice to call the endpoint to get a new token every time you do an API call.
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            grant_type: "refresh_token",
            refresh_token: refreshToken
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
در اینجا نحوه‌ی عملکرد، همانند متد login است. با این تفاوت که grant_type آن اینبار بجای password مساوی refresh_token است و مقداری که به عنوان refresh_token به سمت سرور ارسال می‌کند، همان مقداری است که در عملیات لاگین از سمت سرور دریافت کرده‌است. آدرس ارسال این درخواست نیز همان tokenPath تنظیم شده‌ی در فایل web.config است. بنابراین مراحلی که در اینجا طی خواهند شد، به ترتیب زیر است:
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی می‌شود. اکنون در متد ReceiveAsync این refresh token ذخیره شده‌ی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize می‌کنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز می‌شود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافه‌تری وجود داشت، می‌توان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام می‌دهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync  بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع می‌شود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیره‌ی هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute

در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف می‌کنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی می‌شود که آیا درخواست رسیده‌ی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان می‌رسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی می‌کنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشده‌است.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آن‌را با نمونه‌ی موجود در دیتابیس مقایسه می‌کنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع می‌شود.
- همچنین در آخر بررسی می‌کنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشده‌است.

بنابراین به ازای هر درخواست به سرور، دو بار بررسی بانک اطلاعاتی را خواهیم داشت:
- یکبار بررسی جدول کاربران جهت واکشی مقدار فیلد شماره سریال مربوط به کاربر.
- یکبار هم جهت بررسی جدول توکن‌ها برای اطمینان از صدور توکن رسیده توسط برنامه‌ی ما.

و نکته‌ی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
 [JwtAuthorize(Roles = "Admin")]
public class MyProtectedAdminApiController : ApiController


نحوه‌ی ارسال درخواست‌های Ajax ایی به همراه توکن صادر شده

تا اینجا کار صدور توکن‌های برنامه به پایان می‌رسد. برای استفاده‌ی از این توکن‌ها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی می‌شوند:
var jwtToken;
var refreshToken;
 
function doLogin() {
    $.ajax({
     // same as before
    }).then(function (response) {
        jwtToken = response.access_token;
        refreshToken = response.refresh_token; 
    }
از متغیر jwtToken به ازای هربار درخواستی به یکی از کنترلرهای سایت، استفاده می‌کنیم و از متغیر refreshToken در متد doRefreshToken برای درخواست یک access token جدید. برای مثال اینبار برای اعتبارسنجی درخواست ارسالی به سمت سرور، نیاز است خاصیت headers را به نحو ذیل مقدار دهی کرد:
function doCallApi() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/MyProtectedApi",
        type: 'GET'
    }).then(function (response) {
بنابراین هر درخواست ارسالی به سرور باید دارای هدر ویژه‌ی Bearer فوق باشد؛ در غیراینصورت با پیام خطای 401، از دسترسی به منابع سرور منع می‌شود.


پیاده سازی logout سمت سرور و کلاینت

پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده می‌کنید. در اینجا در اکشن متد Logout، کار حذف توکن‌های کاربر از بانک اطلاعاتی انجام می‌شود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شده‌است یا خیر. چون هش آن دیگر در جدول توکن‌ها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شده‌است:
function doLogout() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/user/logout",
        type: 'GET'
تنها کاری که در اینجا انجام شده، فراخوانی اکشن متد logout سمت سرور است. البته ذکر jwtToken نیز در اینجا الزامی است. زیرا این توکن است که حاوی اطلاعاتی مانند userId کاربر فعلی است و بر این اساس است که می‌توان رکوردهای او را در جدول توکن‌ها یافت و حذف کرد.


بررسی تنظیمات IoC Container برنامه

تنظیمات IoC Container برنامه را در پوشه‌ی IoCConfig می‌توانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگی‌ها در کنترلرهای Web API استفاده می‌شود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگی‌ها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شده‌اند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهله‌های مورد استفاده و تنظیم طول عمر آن‌ها انجام شده‌است. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده می‌شود و Owin هربار آن‌را وهله سازی نمی‌کند. همین مساله سبب شده‌است که معرفی وابستگی‌ها در سازنده‌ی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider(
   Func<IUsersService> usersService,
   Func<ITokenStoreService> tokenStoreService,
   ISecurityService securityService,
   IAppJwtConfiguration configuration)
در کلاسی که طول عمر singleton دارد، وابستگی‌های تعریف شده‌ی در سازنده‌ی آن هم تنها یکبار در طول عمر برنامه نمونه سازی می‌شوند. برای رفع این مشکل و transient کردن آن‌ها، می‌توان از Func استفاده کرد. به این ترتیب هر بار ارجاهی به usersService، سبب وهله سازی مجدد آن می‌شود و این مساله برای کار با سرویس‌هایی که قرار است با بانک اطلاعاتی کار کنند ضروری است؛ چون باید طول عمر کوتاهی داشته باشند.
در اینجا سرویس IAppJwtConfiguration  با Func معرفی نشده‌است؛ چون طول عمر تنظیمات خوانده شده‌ی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی می‌شود، یک سازنده‌ی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
  • #
    ‫۸ سال و ۲ ماه قبل، یکشنبه ۱۳ تیر ۱۳۹۵، ساعت ۲۰:۳۰
    سلام؛ من نمونه رو با Identity نوشتم ولی در کلاس JwtAuthorizeAttribute، متد OnAuthorization  مقدار actionContext.Request.Headers.Authorization برابر با null میشه.

    اگر امکانش هست در خصوص actionContext که چی هست و کی وهله سازی میشه هم توضیح بدید.
    • #
      ‫۸ سال و ۲ ماه قبل، یکشنبه ۱۳ تیر ۱۳۹۵، ساعت ۲۳:۰۸
      قسمت «نحوه‌ی ارسال درخواست‌های Ajax ایی به همراه توکن صادر شده» را مطالعه کنید. اگر توکنی به سمت سرور ارسال نشود و هدر نهایی حاوی key/value مربوط به authorization ذکر شده نباشد:
          $.ajax({
              headers: { 'Authorization': 'Bearer ' + jwtToken },
      مقدار access token در سمت سرور قابل بازیابی نخواهد بود.
      با استفاده از developer tools مرورگرها بررسی کنید که آیا هدر authorization را به شکل زیر به سمت سرور ارسال می‌کنید یا خیر؟ (این هدر ویژه هست که actionContext.Request.Headers.Authorization را وهله سازی می‌کند)

  • #
    ‫۸ سال و ۲ ماه قبل، دوشنبه ۱۴ تیر ۱۳۹۵، ساعت ۱۷:۱۱
    زمانی که Call protected api رو بدون login با کد برنامه جاری تست می‌کنم بهم 401 و پیام Authorization has been denied for this request  نمایش داده می‌شود اما با برنامه خودم این خطا رو بهم میده. لطفا راهنمایی کنید.

    • #
      ‫۸ سال و ۲ ماه قبل، دوشنبه ۱۴ تیر ۱۳۹۵، ساعت ۱۷:۴۱
      - در مورد حالت‌هایی مانند «با برنامه خودم» این مطلب را مطالعه کنید.
      - لطفا تصویر خطا را ارسال نکنید. متن کامل آن‌را ارسال کنید.
      - در این تصویر، به سختی می‌توان مشاهده کرد که can not read string undefined را ذکر کرده‌است. علت اینجا است که چون در فایل Index.html متغیر var jwtToken بدون مقدار دهی اولیه است، مقدار undefined را به سمت سرور ارسال می‌کند.
      var bar;
      typeof bar; // "undefined"
      این بررسی را به ابتدای متدهای سمت کاربر اضافه کنید:
      if(jwtToken == null) // will check if the value is undefined or null.
      {
        alert('you need to login first');
        return;
      }
  • #
    ‫۸ سال و ۲ ماه قبل، یکشنبه ۱۰ مرداد ۱۳۹۵، ساعت ۱۶:۰۰
    سلام، نمونه پروژه‌ای که قرار دادید رو بررسی کردم، نکته‌ای که متوجه نشدم در مورد Token Path هست که در نمونه پروژه به login/ ارجاع داده شده، ممنون میشم در این مورد راهنماییم کنید و ایضا اینکه برفرض اینکه من بخوام عملیات لاگین رو در دیتابیس چک کنم، در کدام کلاس؟ البته توضیحات رو خوندم ولی هنوز کمی برام گنگه . 
    ممنون .
    • #
      ‫۸ سال و ۲ ماه قبل، یکشنبه ۱۰ مرداد ۱۳۹۵، ساعت ۱۶:۳۰
      در وب کانفیگ برنامه یک چنین تنظیمی را داریم:
      <appJwtConfiguration
          tokenPath="/login"
       />
      این مسیر، یک مسیر دلخواه است. اصلا مقدار آن مهم نیست. هرمقداری که قرار داده شود، نهایتا به متد GrantResourceOwnerCredentials کلاس AppOAuthProvider منتهی می‌شود و اینجا است که کار بررسی نام کاربری و کلمه‌ی عبور کاربر انجام خواهد شد. تمام این مراحل هم توسط Owin مدیریت می‌شود. در متد GrantResourceOwnerCredentials هم از سرویس کاربران و متد FindUser آن استفاده شده‌است. بنابراین تنها موردی را که باید بررسی و پیاده سازی کنید، این متد Find در لایه سرویس برنامه است.
      «... برای درک عملکرد این کلاس (و تمام کلاس‌های دیگر آن)، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر می‌توانید درک کنید ... »
  • #
    ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۱۸:۲۴
    با سلام
    و تشکر از مطلب خوبتون
    یه سوال، در صورتی که فردی اطلاعات توکن کپی کنه آیا می‌تونه از طریق سایت هایی مثل Postman درخواست بفرسته به WEBAPI یا امکانپذیر نیست؟
    • #
      ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۲ شهریور ۱۳۹۵، ساعت ۱۸:۴۵
      - از این لحاظ تفاوتی با کوکی‌ها وجود ندارد (بحث باگ‌های XSS و استخراج کوکی‌ها).
      + ارسال درخواست از یک دومین دیگر به Web API نیاز به فعال سازی CORS در برنامه دارد. 
    • #
      ‫۷ سال و ۵ ماه قبل، پنجشنبه ۳۱ فروردین ۱۳۹۶، ساعت ۲۲:۵۱
      Install-Package Microsoft.Owin.Cors

       public void Configuration(IAppBuilder app)
              {
                  app.UseCors(CorsOptions.AllowAll);
                  app.UseOAuthAuthorizationServer(SmObjectFactory.Container.GetInstance<AppOAuthOptions>());
                  app.UseJwtBearerAuthentication(SmObjectFactory.Container.GetInstance<AppJwtOptions>());
                  
              }
  • #
    ‫۸ سال و ۱ ماه قبل، چهارشنبه ۳ شهریور ۱۳۹۵، ساعت ۰۲:۴۳
    دو کلاس AppOAuthOptions و AppJwtOptions کجا رجیستر شده؟ در OwinStartup خطای عدم رجیستر این دو کلاس دارم
    • #
      ‫۸ سال و ۱ ماه قبل، چهارشنبه ۳ شهریور ۱۳۹۵، ساعت ۰۳:۰۹
      «... اگر فضای نام این کلاس (AppJwtConfiguration) را تغییر دادید، باید این قسمت (مقدار type قسمت appJwtConfiguration فایل وب کانفیگ) را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود ...»
      • #
        ‫۸ سال و ۱ ماه قبل، چهارشنبه ۳ شهریور ۱۳۹۵، ساعت ۱۳:۱۰
        تغییر داده شده و به صورت دستی قابل استفاده است. اما جایی که به صورت ServiceLocator بازیابی می‌شود، خطای عدم رجیستر شدن این دو کلاس وجود دارد که مجبور به تزریق دستی آن شدم.
        نکته بعدی جهت فعال سازی EnableCors چه کار خاصی باید انجام شود؟
        من فعال سازی انجام دادم و در WebApiConfig برای همه کنترلرها فعال سازی انجام دادم اما آدرس login ظاهرا جز کنترلرها نیست این تنظیمات اعمال نمی‌شود و عملا برنامه بلا استفاده خواهد شد. قصد من استفاده از توکن بین چند دامنه خاص است. اما فعلا ازمایشی جهت استفاده همه دامنه‌ها فعال سازی انجام شده ولی در عمل کار نمی‌کند. و خطای CORS در مرورگر دریافت می‌شود.
        جهت اعمال به آدرس لاگین چه باید کرد نمونه کد اعمال CORS
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                var cors = new EnableCorsAttribute("*", "*", "*");
                config.EnableCors(cors);
                // ...
            }
        }
        • #
          ‫۸ سال و ۱ ماه قبل، چهارشنبه ۳ شهریور ۱۳۹۵، ساعت ۱۳:۳۳
          - تنظیمات IoC Container آن (برنامه‌ای که آزمایش شده و کار می‌کند) در اینجا هست. کدهای خودتان را با آن مقایسه کنید.
          - آدرسی که کار می‌کند، یعنی جزو مسیریابی سیستم ثبت شده‌است.
          - برای فعال سازی CORS این مراحل باید طی شوند:
          در فایل WebApiConfig.cs، در ابتدای متد Register این کدها باید اضافه شوند:
          var cors = new EnableCorsAttribute("*", "*", "*");
          config.EnableCors(cors);
          config.MessageHandlers.Add(new PreflightRequestsHandler());
          در گردش کاری CORS، کلاینت قبل از ارسال درخواست‌های delete، put و post، ابتدا درخواستی از نوع option را ارسال می‌کند تا وضعیت دسترسی را بررسی کند:
          public class PreflightRequestsHandler : DelegatingHandler
          {
              protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
              {
                  if (request.Headers.Contains("Origin") && request.Method.Method == "OPTIONS")
                  {
                      var response = new HttpResponseMessage {StatusCode = HttpStatusCode.OK};
                      response.Headers.Add("Access-Control-Allow-Origin", "*");
                      response.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization");
                      response.Headers.Add("Access-Control-Allow-Methods", "*");
                      var tsc = new TaskCompletionSource<HttpResponseMessage>();
                      tsc.SetResult(response);
                      return tsc.Task;
                  }
                  return base.SendAsync(request, cancellationToken);
              }
          }
          به علاوه در ابتدای کدهای سمت کلاینت هم این سطر باید اضافه شود:
          jQuery.support.cors = true;
          • #
            ‫۸ سال و ۱ ماه قبل، چهارشنبه ۳ شهریور ۱۳۹۵، ساعت ۲۲:۴۳
            با تشکر از پاسخ شما
            اما این راه حل هم عمل نکرد. پس از جستجو در وب و دیدن این پست به این نتیجه رسیدم. گفتم شاید دوستان دیگر هم چنین مشکلی داشته باشند.
            در دو متد GrantRefreshToken  و  GrantResourceOwnerCredentials از فایل AppOAuthProvider کافیست خط زیر را اضافه نماییم.
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });


          • #
            ‫۸ سال و ۱ ماه قبل، چهارشنبه ۳ شهریور ۱۳۹۵، ساعت ۲۳:۱۶
            این پستم راهکار خوبی داده بد نیست
  • #
    ‫۸ سال قبل، سه‌شنبه ۲۳ شهریور ۱۳۹۵، ساعت ۲۰:۳۷
    جهت تکمیل بحث
    قسمت JwtAuthorizeAttribute  و در تکه کد زیر:  
                var claimsIdentity = actionContext.RequestContext.Principal.Identity as ClaimsIdentity;
                if (claimsIdentity == null)
                {
                    // this is not our issued token
                    this.HandleUnauthorizedRequest(actionContext);
                    return;
                }
    وقتی که توکن کاربر منقضی میشه، خط:
    var userId = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
    ایجاد Exception میکنه، بخاطر خالی بودن Claims; 
    با توجه با آموزشی بودن مطلب و اینکه خومون میتونیم متناسب با نیازمون کاستومش کنیم، بهتر است که شرط قبل رو به صورت زیر مدیریت کنیم:
    if (claimsIdentity == null || !claimsIdentity.Claims.Any())
                {
                    // ...
                }
  • #
    ‫۷ سال و ۱۲ ماه قبل، پنجشنبه ۸ مهر ۱۳۹۵، ساعت ۱۹:۰۸
    سلام و تشکر از مطلب منسجم و جامع و مانعی که نوشتید.
    در خصوص اینکه فرموده اید " آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شده‌است یا خیر؟"
    فکر میکنم برای نیل به این هدف نیازی برای ذخیره توکن‌ها نیست. همینکه مطمئن شویم امضای توکن صحیح است کفایت میکند تا اطمینان داشته باشیم اولا توکن سالم است و ثانیا صادر کننده هم خود ما بوده ایم. البته این فقط برای روش رمزنگاری نامتقارن مثل RSA صدق میکند. چرا که کاربر با داشتن Public Key نمیتواند توکنی جعل کند. ولی اگر مثلا از HMAC با بکارگیری Shared Secret برای رمزگزاری استفاده شود باید تضمین شود این کلید کاملا محافظت شده و به سمت غیرقابل اطمینان نرود.
    ثانیا برای پیاده کردن Login/out میتوان بر روی Claim‌های توکن، شماره ای (مثلا Guid) داشته باشیم که سریال توکن را نشان میدهد. این شماره‌ها را به عنوان شماره‌های Active هر کاربر در پایگاه ذخیره کنیم. هنگام رسیدن درخواست پس از اطمینان از صحت توکن و اینکه صادر کننده ما بوده ایم، چون UserId را که در توکن داریم، میتوانیم شماره سریال توکن را در لیست شماره‌های اکتیو کاربر در پایگاه جستجو کنیم. 
    درصورت Logout هم این لیستِ شماره هایِ کاربر را خالی کنیم. 
    • #
      ‫۷ سال و ۱۲ ماه قبل، پنجشنبه ۸ مهر ۱۳۹۵، ساعت ۲۲:۱۱
      در اینجا هش توکن‌ها ذخیره می‌شوند؛ در قسمت «الف) سرویس TokenStoreService » و همچنین در متد TokenEndpointResponse  که دسترسی به آن وجود دارد (توضیحات انتهای قسمت «پیاده سازی قسمت لاگین و صدور access token »). ذخیره سازی هش، هم جستجوی سریعی را به همراه خواهد داشت (چون طول آن کوتاه و ثابت است و امکان تعریف ایندکس بر روی آن وجود دارد. به علاوه موارد منقضی شده هم حذف می‌شوند از بانک اطلاعاتی) و هم نیازی به ذخیره‌ی اطلاعات اضافه‌تری در توکن نهایی ندارد و حجم آن‌را افزایش نمی‌دهد و چون در اختیار کاربر نیست، حتی در صورت افشای کلیدهای سمت سرور، قابل تولید در سمت کلاینت نیست. همچنین بحث تعیین اعتبار توکن‌ها صرفا مرتبط با تائید محل صدور آن‌ها نیست. در سرویس TokenStoreService، وضعیت فعال بودن یا نبودن کاربر، تغییر سطح دسترسی‌ها، تغییر اطلاعات پروفایل او و همچنین عملیات خروج از سیستم او و فاقد اعتبارشدن توکن آن جلسه‌ی کاری نیز بررسی می‌شوند. 
  • #
    ‫۷ سال و ۱۰ ماه قبل، دوشنبه ۱ آذر ۱۳۹۵، ساعت ۱۹:۰۳
    سلام
    - وقتی که SmWebApiFilterProvider  را معرفی می‌کنیم، در صورتی که بخواهیم فیلتری به صورت Global تعریف و  ثبت کنیم آن فیلتر را نمی‌شناسد، چطور می‌توان در این حالت فیلتر Global را ثبت کرد؟
    - وقتی که System.IdentityModel.Tokens.Jwt را آپدیت می‌کنیم، تنظیمات مربوط به SigningCredentials   در کلاس  AppJwtWriterFormat  به دلیل تغییرات ایجاد شده در پکیج‌ها دچار مشکل می‌شود

    با تشکر 
  • #
    ‫۷ سال و ۹ ماه قبل، سه‌شنبه ۲۳ آذر ۱۳۹۵، ساعت ۲۱:۰۵
    با سلام؛ در کلاس JwtAuthorizeAttribute در قسمت var userId = claimsIdentity.FindFirst(ClaimTypes.UserData).Value; با خطای Object reference not set to an instance of an object. مواجه میشم و اینکه هنگام تریس کد متوجه شدم که مقادیر UsersService() و TokenStoreService() نال(null) هستن، امکانش هست راهنماییم کنید که علت این خطا چیه؟
  • #
    ‫۷ سال و ۹ ماه قبل، جمعه ۳ دی ۱۳۹۵، ساعت ۰۵:۱۷
    با توجه به اینکه پروتکل اول برنامه‌های بلادرنگ WebSocket هست و منطقا در این پروتکل امکان ارسال هدر‌های HTTP وجود ندارد ، SignalR به عنوان fallback  به سایر پروتکل‌ها مثل لانگ پولینگ سوئیچ خواهد کرد ضمن اینکه دو خطای آزار دهنده همواره در کنسول مرورگر وجود خواهد داشت . یا اگر کاربر صفحه مرورگر خود را رفرش کند به صفحه لاگین ریدایرکت خواهد شد . برای حل این مشکل بنظر میرسد باید JWT در یک کوکی ذخیره شده و آن کوکی را مثل قبل در هر درخواست باز ارسال کرد . آیا چنین کاری درست است؟ در صورت تایید لطفا راهنمایی بفرمایید.
  • #
    ‫۷ سال و ۸ ماه قبل، پنجشنبه ۲۳ دی ۱۳۹۵، ساعت ۱۷:۴۸
    با توجه به کامنتی که در متد doRefreshToken نوشته شده:
      "گرفتن یک توکن جدید توسط refresh_token فقط زمانی باید صورت پذیرد که id_token منقضی شده است."
    بنابراین هر زمان که نیاز به token می‌باشد، باید منقضی شدن یا نشدن آن بررسی گردد و بسته به آن یا تقاضای token جدید شود یا token فعلی که معتبر است، استفاده گردد. این فرض صحیح است؟
    تصمیم گیری که token منقضی شده یا خیر چگونه باید انجام شود؟ با ارسال درخواستی به server و بررسی token ذخیره شده در دیتابیس  یا ذخیره‌ی تاریخ انقضا در مرورگر و اتخاذ تصمیم بر اساس آن؟ 
    • #
      ‫۷ سال و ۸ ماه قبل، پنجشنبه ۲۳ دی ۱۳۹۵، ساعت ۱۸:۰۰
      همان بررسی xhr.status === 401 برای تعیین معتبر بودن یا منقضی شدن توکن کافی هست.
      • #
        ‫۷ سال و ۸ ماه قبل، پنجشنبه ۲۳ دی ۱۳۹۵، ساعت ۲۰:۳۵
        در این صورت، تکلیف درخواست جاری که عملیات مد نظر کاربر را انجام نداده چه می‌شود؟ 
        • #
          ‫۷ سال و ۸ ماه قبل، پنجشنبه ۲۳ دی ۱۳۹۵، ساعت ۲۲:۴۸
          بررسی xhr.status === 401 در قسمت شکست عملیات ای‌جکسی رخ می‌دهد. دراینجا می‌توانید پیام خطایی را نمایش داده و مانند تمام سیستم‌های اعتبارسنجی، درخواست را برگشت بزنید. البته در اینجا کمی محترمانه‌تر است، چون اجبار به لاگین مجدد، تنها در صورت شکست عملیات و درخواست ای‌جکسی doRefreshToken ضروری خواهد بود. اگر doRefreshToken موفقیت آمیز بود، پیامی را در جهت «لطفا مجددا سعی کنید»، نمایش دهید (و در این حالت کاربر نیازی به لاگین مجدد ندارد؛ چون به صورت خودکار توکن او به روز شده‌است). اگر doRefreshToken موفقیت آمیز نبود، قسمت لاگین را بلافاصله نمایش دهید.
  • #
    ‫۷ سال و ۴ ماه قبل، سه‌شنبه ۲ خرداد ۱۳۹۶، ساعت ۱۶:۳۱
    چگونه از مسیر لاگین به متد GrantResourceOwnerCredentials   میرسیم؟
    حتی با گذاشتن break point نیز جایی این متد صدا نزده شده
    • #
      ‫۷ سال و ۴ ماه قبل، سه‌شنبه ۲ خرداد ۱۳۹۶، ساعت ۱۶:۳۸
      این موارد توسط OWIN به صورت خودکار مدیریت می‌شوند (قسمت «بسته‌های پیشنیاز برنامه» ابتدای بحث).

  • #
    ‫۷ سال و ۱ ماه قبل، یکشنبه ۲۲ مرداد ۱۳۹۶، ساعت ۱۹:۳۴
    با عرض سلام؛ شما یکجا فرمودین که : خود owin دارای خاصیت‌های user name و password نیز هست و برای لاگین سمت کلاینت هم باید فرمت اطلاعات به این صورت باشه
      data: {
                username: "Vahid" ,
                password: "1234" ,
                grant_type: "password" }
    می خواستم بدونم داخل این ابجکت data  آیا میشه اطلاعات دیگه ای هم قرار داد که بعدا اونور سمت سرور توسط owin  قابل شناسایی باشه ؟ مثله : mobileNumber  یا UserType.
    • #
      ‫۷ سال و ۱ ماه قبل، یکشنبه ۲۲ مرداد ۱۳۹۶، ساعت ۲۰:۳۱
      بله.  اطلاعات اضافه‌تر به این صورت قابل خواندن هستند:
      public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
      {
          // how to get additional parameters
           var form = await context.Request.ReadFormAsync();
           var key1 = form["my-very-special-key1"];
  • #
    ‫۶ سال و ۱۱ ماه قبل، یکشنبه ۱۶ مهر ۱۳۹۶، ساعت ۰۰:۲۸
    با سلام
    ممنون از مطلب کارآمدتون
    من jwt با ef code first پیاده سازی کردم ولی وقتی میخوام چند بار login کنم تا تستش کنم ارور زیرو میده
    ممنون میشم راهنمایی کنید مشکل از کجاست

  • #
    ‫۶ سال و ۱۱ ماه قبل، دوشنبه ۱۷ مهر ۱۳۹۶، ساعت ۱۷:۰۷
    با عرض سلام و خسته نباشید
    من از این روش شما در سایتم استفاده می‌کردم و در سیستم خودم با این روش هیچ مشکلی نداشتم ولی وقتی در هاست آپلود کردم سایت موقعی که دستی لاگین می‌کنم مشکلی نداره ولی وقتی مرورگر رو می‌بندم و بعد از 15 دقیقه با refresh token می‌خوام لاگین بشم، لاگین نمیشه. اینو هم بگم که بعد از 2 یا 3 دقیقه بعد از بستن سایت می‌تونم با refresh token لاگین بشم ولی بعد 15 دقیقه نه. با بررسی هایی که انجام دادم فهمیدم مشکل از متد ReceiveAsync  کلاس RefreshTokenProvider هست و مشکل اینه که با اینکه RefreshToken  درست هست و زمان انقضاش هم هنوز باقی هست. ولی بعد از اجرای متد context.DeserializeTicket(refreshToken.RefreshToken); مقدار context.Ticket نال میشه و دیگه access token جدیدی صادر نمیشه و کار همونجا خاتمه پیدا میکنه. میخواستم بدونم که مشکل کجاست  و  کار متد context.DeserializeTicket چی هستش؟
    • #
      ‫۶ سال و ۱۱ ماه قبل، سه‌شنبه ۱۸ مهر ۱۳۹۶، ساعت ۱۸:۳۵
      - اگر بر روی سرور machine key تنظیم نشده باشد، IIS هربار که ری‌استارت می‌شود، یک machine key جدید را تولید خواهد کرد و محل نگهداری آن حافظه‌ی سرور است. بنابراین اگر زیاد مشکل لاگین دارید و اطلاعات قابل رمزگشایی نیست و نال بازگشت داده می‌شود، یعنی برنامه‌ی شما بر روی سرور زیاد ری‌استارت می‌شود (متد Application_Start فایل gloal.asax را لاگ کنید تا این مساله مشخص شود).
      - امکان تنظیم machine key به صورت دستی در فایل web.config برنامه وجود دارد:
        <system.web>
          <machineKey decryptionKey="Enter decryption Key here" 
                      validation="SHA1" 
                      validationKey="Enter validation Key here" />
        </system.web>

      IIS امکان تولید این کلیدها و به روز رسانی خودکار فایل web.config برنامه را دارد:


      روش دوم تولید آن با کدنویسی است:
          protected static string GenerateKey(int length)
          {
              RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider();
              byte[] buff = new byte[length];
              rngCsp.GetBytes(buff);
              StringBuilder sb = new StringBuilder(buff.Length * 2);
              for (int i = 0; i < buff.Length; i++)
                  sb.Append(string.Format("{0:X2}", buff[i]));
              return sb.ToString();
          }
      
          string validationKey = GenerateKey(64);
          string  decryptionKey = GenerateKey(32);
  • #
    ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ آبان ۱۳۹۶، ساعت ۰۳:۳۳
    بعد از آپدیت پکیج System.IdentityModel.Tokens.Jwt،  تنظیمات مربوط به SigningCredentials  در کلاس  AppJwtWriterFormat با مشکل مواجه شد، راه حلی برای این مسئله دارید؟
    • #
      ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ آبان ۱۳۹۶، ساعت ۰۴:۵۶
      از همان نگارش 4 آن استفاده کنید. فعلا راه حل رسمی ندارد (Owin.Security v3 برای IdentityModel v5 به روز رسانی نشده (از IdentityModel v4 آن استفاده می‌کند) و تداخل اسمبلی‌ها را دارند).
      - Owin.Security v3 با IdentityModel v4 سازگار است.
      - Owin.Security v4 با IdentityModel v5 سازگار است. 
    • #
      ‫۶ سال و ۹ ماه قبل، پنجشنبه ۱۶ آذر ۱۳۹۶، ساعت ۱۶:۴۹
      به روز رسانی
      ارتقاء به Owin.Security v4 و IdentityModel v5 نیازمند این تغییرات است.  
  • #
    ‫۶ سال و ۹ ماه قبل، یکشنبه ۱۹ آذر ۱۳۹۶، ساعت ۲۰:۰۴
    با توجه به این موضوع که اپلیکیشنهای کنونی زمانی که کاربر ثبت نام میکنه باید متد لاگین به صورت خودکار انجام شده و access_token  در اختیار آپ قرار بگیره بنده این متد را نوشتم
           public async Task<IHttpActionResult> LoginUserAsync(LoginUserBindingModel model)
            {
              var requestParams = new List<KeyValuePair<string, string>>
              {
              new KeyValuePair<string, string>("grant_type", "password"),
              new KeyValuePair<string, string>("username", model.Username),
              new KeyValuePair<string, string>("password", model.Password)
              };
                var requestParamsFormUrlEncoded = new FormUrlEncodedContent(requestParams);
                using (var client = new HttpClient())
                {
                    client.BaseAddress = new Uri("http://localhost:9577/");
                    client.DefaultRequestHeaders.Accept.Clear();
                  //  client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    var response = await client.PostAsync("/login", requestParamsFormUrlEncoded);
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        var responseString = await response.Content.ReadAsStringAsync();
                        var jsSerializer = new JavaScriptSerializer();
                        var responseData = jsSerializer.Deserialize<Dictionary<string, string>>(responseString);
                        return Content(HttpStatusCode.OK, responseData);
                    }
                    else
                    {
                        return  BadRequest(response.ToString());
                    }
                }
            }
    ولی response برابر با 400 میشه ! چگونه باید به  access_token تولیدی دسترسی داشت ؟
  • #
    ‫۶ سال و ۹ ماه قبل، دوشنبه ۲۰ آذر ۱۳۹۶، ساعت ۱۲:۵۳
    چنانچه بخواهیم از این روش برای احراز هویت اپلیکیشن‌های مرتبط با api استفاده کنیم و همزمان از احراز هویت مبتنی بر کوکی برای وب اپلیکیشن در یک پروژه استفاده کنیم،  ادرس login/ با خطا ی ادرس نامعتبر مواجه میشود و زمانی که از ادرس لاگین صحیح موجود استفاده میکنم خطای زیر صادر میشود .این درحالی است که در اکشن لاگین از ValidateAntiForgeryToken  استفاده کردم.
    The required anti-forgery cookie "__RequestVerificationToken" is not present.
    1- چطور میشود این مشکل را حل کرد.
    2- آیا روش استفاده همزمان از دو روش احراز هویت را پیشنهاد می‌کنید؟
    • #
      ‫۶ سال و ۹ ماه قبل، دوشنبه ۲۰ آذر ۱۳۹۶، ساعت ۱۳:۰۰
      - در مورد آدرس ویژه‌ی login این روش (نحوه‌ی تعیین، تغییر و پردازش ویژه‌ی آن)، هم در مطلب و هم در نظرات، بحث شده‌است. واژه‌ی login را در صفحه‌ی جاری جستجو کنید.
      - مطلب و نظرات «امن سازی درخواست‌های ای‌جکسی برنامه‌های ASP.NET MVC 5.x در مقابل حملات CSRF» را در مورد نحوه‌ی ارسال RequestVerificationToken در برنامه‌های Ajax ایی مطالعه کنید.
      - روش JWT عموما برای برنامه‌های تمام SPA (تمام تک صفحه‌ای وب مانند Angular) استفاده می‌شود (پیشنیاز بحث را که در ابتدای آن عنوان شده، مطالعه کنید). اگر برنامه‌ی شما تمام MVC است و از صفحات Razor استفاده می‌کنید، بهتر است از روش «اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity» استفاده کنید.
  • #
    ‫۶ سال و ۸ ماه قبل، یکشنبه ۱۷ دی ۱۳۹۶، ساعت ۲۳:۴۸
    در کلاس  AppJwtWriterFormat  چه زمانی نیاز به پیاده سازی متد  Unprotect می باشد و نحوه پیاده سازی آن چگونه است؟
    • #
      ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۸ دی ۱۳۹۶، ساعت ۰۲:۱۷
      برای حالت‌های ترکیبی کوکی و توکن استفاده می‌شود. ربطی به بحث جاری ندارد. به همین جهت پیاده سازی نشده‌است.
  • #
    ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۸ دی ۱۳۹۶، ساعت ۱۲:۴۴
    سلام
    وقتی در متد هایی از controller از [JwtAuthorize] استفاده میکنم در بعضی از مواقع اررور زیر رو میده و سیستم به مدت چند دقیقه از کار میافته و اررور 500 میده. ممنون میشم راهنمایی کنید که باید چیکار کنم. ممنون

      • #
        ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۸ دی ۱۳۹۶، ساعت ۱۳:۲۴
        خطای فوق هنگام login اتفاق می‌افتد و پیاده سازی آن بر اساس جزئیاتی که در آموزش آمده انجام شده است:

        • #
          ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۸ دی ۱۳۹۶، ساعت ۱۳:۲۸
          احتمالا مرتبط است به قسمت حذف توکن‌های قدیمی یا ضعف مدیریت طول عمر Context در برنامه‌ی شما (بررسی این مورد، نیاز به بازبینی دقیق کدهای شخصی شما را دارد). می‌توان این مورد را به صورت مستقل و خارج از لاگین در یک وظیفه‌ی پس زمینه انجام داد تا با سایر قسمت‌های برنامه‌ی شما تداخل نکند.
  • #
    ‫۶ سال و ۶ ماه قبل، سه‌شنبه ۱۵ اسفند ۱۳۹۶، ساعت ۲۳:۰۶
    سلام. من می‌خواستم توی پروژه ام که از identity استفاده کردم این مبحث رو بگنجانم که بتونم هم از identity استفاده کنم و هم از jwt. چگونه می‌تونم از هردو با هم استفاده کنم و وقتی با jwt لاگین شد توی identity هم لاگین بشه و بالعکس.
    • #
      ‫۶ سال و ۶ ماه قبل، سه‌شنبه ۱۵ اسفند ۱۳۹۶، ساعت ۲۳:۲۰
      سیستم identity یک سیستم مبتنی بر کوکی‌ها هست و زمانیکه لاگین می‌کنید، Claims کاربر در یک کوکی رمزنگاری شده درج می‌شود تا در درخواست بعدی، پردازش شده و اطلاعات شیء this.User را مقدار دهی کند. بنابراین ترکیب آن در اینجا (یک سیستم مبتنی بر توکن‌ها) فقط از دیدگاه مدیریت کاربران و نقش‌های آن معنا پیدا می‌کند و چون در طراحی این مطلب از لایه سرویس استفاده شده و لایه سطح بالاتر از پیاده سازی‌های جزئیات آن باخبر نیست و وابستگی‌های مورد نیاز را تنها از طریق تزریق وابستگی‌ها دریافت می‌کند، فقط کافی است UsersService.cs را با توجه به سیستم identity بازنویسی کنید.
  • #
    ‫۶ سال و ۵ ماه قبل، شنبه ۱۸ فروردین ۱۳۹۷، ساعت ۱۴:۳۸
    سلام؛ میخوام وقتی بر روی call protect api کلیک کردم، همراه چند تا فیلدی که مربوط به تاریخ انقضاء و توکن‌ها میباشد، چند تا فیلد دیگه مثلا id  شخص لاگین شده یا ... برگشت بدم. ولی فیلداش طبق کلاسی نوشته شده و نمیشه اضافه کرد!
    • #
      ‫۶ سال و ۵ ماه قبل، شنبه ۱۸ فروردین ۱۳۹۷، ساعت ۱۵:۱۸
      return Ok نوشته شده محدودیتی ندارد؛ چون در پشت صحنه از کتابخانه‌ی Json.NET استفاده می‌کند و قادر هست با انواع و اقسام اشیاء پیچیده کار کند و آن‌ها را serialize کند. جهت سادگی کار در اینجا از یک anonymous object استفاده شده‌است. آن‌را با هر شیء دیگری و با هر ساختاری که علاقمندید تعویض کنید. هر نوع شیءایی را در اینجا می‌توان ذکر و یا اضافه کرد:
      public IHttpActionResult Get()
      {
        // Anonymous and Weakly-Typed Objects
         return Ok(new
                  {
                      Id = 1,
                      Title = "Hello from My Protected Controller!",
                      Username = this.User.Identity.Name,
                      Property2 = new Object1 {  id = 1, name = "V" }
                  });
         // OR ... Strongly-Typed Objects
         return Ok(model);
      }
  • #
    ‫۶ سال و ۴ ماه قبل، چهارشنبه ۱۲ اردیبهشت ۱۳۹۷، ساعت ۰۹:۰۷
    با سلام؛ در یک پروژه Angular بعد از اینکه با jwt_decode توکن رو از حالت کد شده در میارم حروف فارسی ( نام شخص لاگین کننده) به این شکل : [اکرم ماچانلو]  نمایش داده می‌شود. اگر ممکنه لطفا راهنمایی کنید. ممنون.
    • #
      ‫۶ سال و ۴ ماه قبل، چهارشنبه ۱۲ اردیبهشت ۱۳۹۷، ساعت ۱۴:۱۵
      پس از اعمال این تغییرات جهت استفاده از jwt_decode، مشکلی مشاهده نشد:


      نمونه دیگر آن در یک برنامه‌ی Angular هم در اینجا است و آن هم مشکلی ندارد.

  • #
    ‫۶ سال و ۴ ماه قبل، چهارشنبه ۱۲ اردیبهشت ۱۳۹۷، ساعت ۱۹:۱۹
    با سلام؛
    بنده از این روش استفاده کردم یه مشکلی دارم اونم اینکه وقتی refreshToken دریافت میشه کاربر شناسایی نمیشه و در بخش Claims‌ها برنامه با خطا مواجه میشه.
    به نظر شما کجای کارم ایراد داره؟
    • #
      ‫۶ سال و ۴ ماه قبل، چهارشنبه ۱۲ اردیبهشت ۱۳۹۷، ساعت ۱۹:۳۰
      - کاربر شناسایی نمی‌شود یعنی هنوز توکن قبلی منقضی شده را به سمت سرور ارسال می‌کنید. توکن جدید دریافت شده را با توکن قبلی جایگزین کنید.
      - به مثال این مطلب نمایش محتویات توکن دریافتی از سرور هم اضافه شده‌است. یکبار اجرایش کنید تا ریز محتویات توکن دریافتی از سرور را به همراه تمام Claims سفارشی آن بتوانید بررسی کنید. در نهایت این جزئیات هستند که به سمت سرور ارسال می‌شوند و دسترسی‌ها را ایجاد می‌کنند.
  • #
    ‫۶ سال و ۴ ماه قبل، پنجشنبه ۱۳ اردیبهشت ۱۳۹۷، ساعت ۰۶:۵۰
    با سلام، در پروژه Angular 5 موقع ارسال درخواست RefreshToken خطای invalid_grant میگیرم. 
    اطلاعاتی ارسالی به سرور : 
       urlSearchParams.append('grant_type', 'refresh_token');
       urlSearchParams.append('refresh_token', model.refreshToken);
    ارسال می‌شود به مسیر login/ در وب کانفیگ ست شده است. لطفا درصورت امکان راهنمایی کنید. با تشکر
    • #
      ‫۶ سال و ۴ ماه قبل، پنجشنبه ۱۳ اردیبهشت ۱۳۹۷، ساعت ۱۳:۱۸
      معادل قطعه کد jQuery Ajax زیر
       $.ajax({
                url: "/login", // web.config --> appConfiguration -> tokenPath
                data: {
                         grant_type: "refresh_token",
                         refresh_token: refreshToken
                      },
                      type: 'POST', // POST `form encoded` data
                      contentType: 'application/x-www-form-urlencoded'
                  })
      در برنامه‌های Angular و HttpClient آن به صورت زیر است:
      doRefreshToken(refreshToken: string): Observable<any> {
        const body = new HttpParams()
          .set('grant_type', "refresh_token")
          .set('refresh_token', refreshToken);
      
        return this.http.post('/login',
          body.toString(),
          {
            headers: new HttpHeaders()
              .set('Content-Type', 'application/x-www-form-urlencoded')
          }
        );
      }
      • #
        ‫۶ سال و ۴ ماه قبل، پنجشنبه ۱۳ اردیبهشت ۱۳۹۷، ساعت ۱۹:۵۶
        با سلام؛ کد رو به این شکل تغییر  دادم این دفعه خطای {"error":"unsupported_grant_type"}   میگیرم. درخواست به متد ValidateClientAuthentication در سمت سرور می‌رسه. بعد از بازگشت همچین خطایی نشون میده.
         private refreshToken(isAuthUserLoggedIn: boolean) {
              
                const headers = new HttpHeaders({ "Content-Type": "application/x-www-form-urlencoded" });
                const model = { refreshToken: this.tokenStoreService.getRawAuthToken(AuthTokenType.RefreshToken) };
                body = new HttpParams();
                body.set('grant_type', 'refresh_token');
                body.set('refresh_token', model.refreshToken);
        
                return this.http    
        (`${this.appConfig.basePath}${this.apiConfigService.configuration.loginPath}`,
                    body.toString(), { headers: headers })
                    .pipe(
                    map(response => response || {}),
                    catchError((error: HttpErrorResponse) => ErrorObservable.create(error)),
                    finalize(() => {
                        this.scheduleRefreshToken(isAuthUserLoggedIn);
                    })
                    )
                    .subscribe(result => {
                        console.log("RefreshToken Result", result);
                        this.tokenStoreService.storeLoginSession(result);
                    });
            }

        با تشکر
        • #
          ‫۶ سال و ۴ ماه قبل، جمعه ۱۴ اردیبهشت ۱۳۹۷، ساعت ۰۰:۲۰
          در همان مطلب «ارتقاء به HTTP Client در Angular 4.3» توضیح دادم؛ در قسمت «یک نکته: شیء HttpParams به صورت immutable طراحی شده‌است.»
          شما چون HttpParams را به صورت زنجیروار فراخوانی نکردید:
          body = new HttpParams();
          body.set('grant_type', 'refresh_token');
          body.set('refresh_token', model.refreshToken);
          حاصل نهایی آن خالی خواهد بود (پس از new HttpParams جائیکه ; قرار گرفته، کار تغییرات این شیء immutable تمام است و پس از آن چیزی به آن اضافه نخواهد شد). به همین جهت است که پیام unsupported_grant_type را دریافت می‌کنید. چون grant_type ایی را ارسال نکرده‌اید و ()body.toString آن یک رشته‌ی خالی است. مثالی را که پیشتر ارسال کردم، زنجیروار است.
    • #
      ‫۶ سال و ۴ ماه قبل، پنجشنبه ۱۳ اردیبهشت ۱۳۹۷، ساعت ۱۴:۲۸
      با سلام؛
      احتمالا شما توکن رو برای دریافت refreshToken  ارسال نمی‌کنید
      • #
        ‫۶ سال و ۴ ماه قبل، پنجشنبه ۱۳ اردیبهشت ۱۳۹۷، ساعت ۲۲:۱۰
        ممنون از پاسخ شما. 
        بنده RefreshToken (توکن کوتاه) را ارسال میکنم .
  • #
    ‫۶ سال و ۴ ماه قبل، پنجشنبه ۱۳ اردیبهشت ۱۳۹۷، ساعت ۱۸:۱۲
    به روز رسانی
    وابستگی‌های این پروژه به نگارش‌های زیر به روز رسانی و آزمایش شدند:
    Microsoft.AspNet.WebApi 5.2.5
    
    Microsoft.Owin 4.0.0
    Microsoft.Owin.Security 4.0.0
    
    Microsoft.Owin.Host.SystemWeb 4.0.0
    
    Microsoft.Owin.Security.Jwt 4.0.0
    System.IdentityModel.Tokens.Jwt 5.2.2
  • #
    ‫۶ سال و ۴ ماه قبل، سه‌شنبه ۱۸ اردیبهشت ۱۳۹۷، ساعت ۰۶:۳۶
    با سلام؛ درصورت امکان برای 2 مورد زیر راهنمائی کنید:
    1. در یک پروژه آنگولار 5 برای نقش (Role) کاربر یک چنین سناریویی وجود دارد: ابتدا کاربر لاگین میکند، نقش‌های متعدد کاربر نمایش داده می‌شود سپس کاربر با یکی از نقش‌ها وارد سیستم می‌شود.این نقش باید به AccessToken ضمیمه شود تا در JwtAttribute سمت سرور بتوان نقش کاربر را بررسی کرد. 
    بنده الان چنین کاری انجام داده ام: کاربر وارد سیستم می‌شود، درصورت ورود موفق به صفحه انتخاب نقش هدایت می‌شود. پس از انتخاب نقش متد RefreshToken رو به همراه نقش انتخابی فراخوانی میکنم و با استفاده از کد زیر درسمت سرور نقش را به توکن اضافه میکنم. 
    var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
    var roleID = form.Result["roleID"];
    newIdentity.AddClaim(new Claim("roleID", roleID));
    newIdentity.AddClaim(new Claim("newClaim", "refreshToken"));
    
    newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
    context.Validated(newTicket);
    2. در چنین سناریویی عنوان نقش (که طولانی هم هست) برای نمایش در بالای صفحه کجا باید ذخیره شود؟به نظرم اگر در AccessToken ذخیره شود باعث افزایش طول توکن می‌شود که در رفرش توکن‌های بعدی حجم توکن بالا میرود
    با تشکر
    • #
      ‫۶ سال و ۴ ماه قبل، سه‌شنبه ۱۸ اردیبهشت ۱۳۹۷، ساعت ۱۲:۲۹
      local storage تا 10 مگابایت اجازه‌ی ذخیره سازی اطلاعات را می‌دهد.
      • #
        ‫۵ سال و ۷ ماه قبل، سه‌شنبه ۲۳ بهمن ۱۳۹۷، ساعت ۱۷:۳۱
        من هم تقریبا با این حالت روبرو شدم . سمت کلاینت مشکل نداریم ،  local storage  می تواند تا 10مگابایت ذخیره سازی نماید. اما زمانی که از طریق header به سمت سرور ارسال می‌نماییم در سمت سرور خطای محدودیت سایز می‌دهد، در وقع ایجکس نمیتواند توکن را به سمت سرور ارسال نماید.
  • #
    ‫۵ سال و ۹ ماه قبل، سه‌شنبه ۲۰ آذر ۱۳۹۷، ساعت ۱۶:۱۹
    سلام و عرض ادب . 
    بنده این تکنولوژی رو در پروژه web api راه اندازی کردم . اما مشکلی که به آن برمیخورم این است که در لاگین بعد از اینکه نام کاربری و کلمه عبور اعتبارسنجی شد و توکن دریافت شد به صفحه ایندکس ارجاع میدم یعنی بعد از دریافت توکن در سمت کلاینت (جی کوئری) از کد زیر استفاده می‌نمایم :‌
      window.location = 'Index';
    اما بعد از وارد شدن در صفحه اصلی پروژام وقتی از طریق Rasor اطلاعات ایدنتی رو فراخوانی میکنم خالی نشان میدهد. که کد فراخوانی به شکل زیر می‌باشد در بالای صفحه. البته در کنترل هام (چه از نوع Web Api چه از نوع MVC) نیز دریافت نمیکنم.
      var claimsIdentity = this.User.Identity as System.Security.Claims.ClaimsIdentity;
    
      var userId = claimsIdentity.FindFirst(System.Security.Claims.ClaimTypes.UserData).Value;
     
    • #
      ‫۵ سال و ۹ ماه قبل، سه‌شنبه ۲۰ آذر ۱۳۹۷، ساعت ۱۶:۲۴
      مدیریت طول عمر توکن‌ها برخلاف کوکی‌ها، توسط مرورگر به صورت خودکار انجام نمی‌شود. یعنی زمانیکه redirect دارید، متغیرهای موقتی جاوا اسکریپتی را از دست خواهید داد و بنابراین دیگر توکنی را برای ارسال به سمت سرور نخواهید داشت. برای رفع این مشکل و ذخیره سازی آن‌ها در کش مرورگر، از local storage استفاده کنید.
  • #
    ‫۵ سال و ۹ ماه قبل، پنجشنبه ۲۹ آذر ۱۳۹۷، ساعت ۰۱:۰۹
    سوال. 
    ایا میتوان دو نوع توکن در پروژه داشته باشیم؟
    مثلا ما دو تالاگین داریم . یکی لاگین ادمین که کاربران اصلی سیستم لاگین میکنن . یکی هم لاگین مشتریان که پنل جدا و مخصوص خود رو دارن. 
    • #
      ‫۵ سال و ۹ ماه قبل، پنجشنبه ۲۹ آذر ۱۳۹۷، ساعت ۰۱:۲۱
      وجود نقش‌ها و claims در این پروژه، جهت جلوگیری از انجام یک چنین کارهایی است. یک کاربر پس از لاگین می‌تواند نقش ادمین را داشته باشد یا نقش یک کاربر معمولی را و یا هر نقش دیگری که صلاح است (در این حالت نیازی به چندین صفحه‌ی لاگین وجود ندارد). در سایت (یا حتی صفحه‌ی جاری) roles و claims را جستجو کنید، اطلاعات بیشتری را در مورد کاربردهای آن‌ها می‌توانید پیدا کنید.
  • #
    ‫۵ سال و ۷ ماه قبل، سه‌شنبه ۲۳ بهمن ۱۳۹۷، ساعت ۱۲:۳۱
    من قبلا اینjwt رو تو یکی از پروژه هام راه اندازی کردم ومشکلی نداشتم . اما در پروژه جدیدی که ایجادکردم و بر روی پروژه این jwt رو اوردم هنگام کلیک بر روی لاگین و فراخوانی تابع dologin خطا میده که متن خطا به شکل زیر هست. 
    <h2> <i>The resource cannot be found.</i> </h2></span>
    <b> Description: </b>HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. &nbsp;Please review the following URL and make sure that it is spelled correctly.
     <br><br>
     
     <b> Requested URL: </b>/login<br><br>
     
     [HttpException]: The controller for path &#39;/login&#39; was not found or does not implement IController.

    • #
      ‫۵ سال و ۷ ماه قبل، سه‌شنبه ۲۳ بهمن ۱۳۹۷، ساعت ۱۲:۴۴
      عنوان کرده مسیر login/ را پیدا نمی‌کند (همان tokenPath که نهایتا به متد GrantResourceOwnerCredentials کلاس AppOAuthProvider منتهی می‌شود و توسط Owin مدیریت می‌شود. در ابتدای این متد یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید...). سپس تنظیمات سیستم، تنظیمات Owin ، مسیریابی‌ها و کانفیگ برنامه را بررسی کنید.
      • #
        ‫۵ سال و ۷ ماه قبل، سه‌شنبه ۲۳ بهمن ۱۳۹۷، ساعت ۱۳:۳۸
        خیلی ممنون؛ مشکل به خاطر عدم نصب Microsoft.Owin.Host.SystemWeb بود.
  • #
    ‫۵ سال و ۵ ماه قبل، دوشنبه ۱۹ فروردین ۱۳۹۸، ساعت ۲۲:۲۸
    سلام و ضمن تشکر؛ یک سوال داشتم که بیشتر در زمینه کارایی و بهینه سازی سیستم هست. در بخش "پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute " همین مطلب یک قسمت از آن نوشتید "به ازای هر درخواست به سرور، دو بار بررسی بانک اطلاعاتی را خواهیم داشت"

    برای اینکه رفت و برگشت برای هر درخواست به بانک اطلاعاتی در پروژه‌های بزرگ رو مدیریت بهینه کنیم چکار باید کرد؟ منظورم اینه وقتی تعداد کاربران زیاد باشه و متدهای زیادی هم در پروژه باشه که در هدر همه اونها باید این توکن و دسترسی‌ها چک بشه ممکنه سربار زیادی روی بانک اطلاعاتی داشته باشه. برای مدیریت بهتر این موارد من دو راه تست کرده بودم :
    1. توی یک متغیر استاتیک اطلاعات توکن‌ها علاوه بر بانک اطلاعاتی ذخیره بشه و هردو با هم سینک باشند (موردی که خودتون هم اشاره فرموده بودید) بنابراین بیشترین درخواستها برای چک این مقادیر روی یک متغیر استاتیک هست که روی IIS فعال میشه و ممکنه رم زیادی البته بگیره و هر وقت هم IIS ریست شد دوباره اون لیست توکن‌های استاتیک از بانک اطلاعاتی فراخوانی میشه و برای درخواست‌های بعدی کاربران از اون متغیر استاتیک که لیست توکن‌ها هست رو چک می‌کنه.
    2. راه دوم استفاده از بانک اطلاعاتی هست که رفت و برگشت به بانک اطلاعاتی برای هر درخواست رو زیاد می‌کنه و ممکنه سربار زیادی داشته باشه

    البته من در پروژه از بانک اطلاعاتی مونگو استفاده کردم و این لیست توکن‌ها و کلیه بانک اطلاعاتی کاربران و غیره در اون ذخیره میشه .

    سوال اینجاست که برای زمانی که کاربران زیاد و متدهای زیادی داریم که باید چک شوند راه حل بهینه برای انجام این مورد چه راهی هست؟
    و اینکه آیا اگه از بانک اطلاعاتی Redis که بر روی رم قرار میگیره برای مدیریت توکن‌ها استفاده کنیم کارایی بهتر میشه یا باز هم همون مشکل قبلی رو داریم؟
    • #
      ‫۵ سال و ۵ ماه قبل، دوشنبه ۱۹ فروردین ۱۳۹۸، ساعت ۲۲:۵۳
      - اگر فکر می‌کنید که 2 بار چک کردن به ازای هر درخواست زیاد هست، احتمالا با ASP.NET Identity کار نکردید! در ASP.NET Identity اگر بررسی اعتبار کاربر را به ازای هر درخواست رسیده فعال کنید (بجای مقدار پیش‌فرض چند دقیقه‌ای آن، این مقدار را صفر کنید تا به ازای هر درخواست انجام شود)، همین یک مورد 5 کوئری را شامل می‌شود. برای نمونه در ASP.NET Core 2.X این بررسی‌ها شامل 5 کوئری به جداول AspNetUser, AspNetUserClaims, AspNetUserRoles, AspNetRoles, AspNetRoleClaims هستند.
      - 2 بار بررسی بانک اطلاعاتی برای بانک‌های اطلاعاتی امروزی هیچ سرباری ندارد و ضمن اینکه خودشان مباحث کش کردن اطلاعات ویژه‌ای را هم برای کوئری‌های پر استفاده دارند؛ مانند buffer cache در SQL Server که تا حد مصرف حافظه‌ی کل سرور هم می‌تواند پیش رود.
      - استفاده از متغیرهای استاتیک و حافظه‌ی سرور برای کش کردن، مقیاس پذیر نیست. در این موارد روش توصیه شده، استفاده از بانک اطلاعاتی Key/Value فوق سریع Redis هست. فقط مشکل تمام کش‌ها، هماهنگ سازی اطلاعات آن‌ها با بانک اطلاعاتی اصلی است که باید مدنظر باشند.
  • #
    ‫۵ سال و ۴ ماه قبل، شنبه ۳۱ فروردین ۱۳۹۸، ساعت ۱۵:۲۷
    درصوتیکه بخوایم سرویسها رو روی چندتا دامنه مجزا تفکیک کنیم - مثلا اطلاعات مالی روی یک بخش api1.com و اطلاعات پرسنلی روی بخش دیگه api2.com - اونوقت سناریو چطور خواهد بود ؟
  • #
    ‫۵ سال و ۴ ماه قبل، شنبه ۷ اردیبهشت ۱۳۹۸، ساعت ۲۲:۳۶
    سلام، آیا یک استانداردی برای مقادیر پیش فرض زمان منقضی شدن Access Token وRefresh Token وجود دارد؟ به عنوان مثال آیا این منطقی است که مثلا زمان منقضی شدن Access Token را یک هفته و زمان منقضی شدن Refresh Token را یک ماه در نظر بگیریم؟
    • #
      ‫۵ سال و ۴ ماه قبل، شنبه ۷ اردیبهشت ۱۳۹۸، ساعت ۲۳:۵۵
      - استانداردی وجود ندارد و  بله.
      - برای این مورد باید میزان اهمیت یک برنامه و خطرات ممکن برای آن را بررسی کرد (risk assessment). آیا یک برنامه‌ی بانکی است؟ این زمان‌ها باید حداقل ممکن باشند. آیا یک برنامه‌ی معمولی پرسنلی است؟ طولانی بودن این زمان‌ها مشکلی را ایجاد نمی‌کنند و هدف اصلی Refresh Token که پیاده سازی sliding expiration است، برآورده می‌شود و پرسنل از لاگین‌های مداوم و اعصاب خرد کن، نجات پیدا می‌کنند.
  • #
    ‫۵ سال و ۱ ماه قبل، چهارشنبه ۱۶ مرداد ۱۳۹۸، ساعت ۲۰:۵۹
    سلام؛ استفاده من از این توکن به این صورت هست که یک پروژه MVC دارم. حالا در برخی صفحات که نیاز به واکشی دیتای گریدها دارم میخوام با استفاده از WebApi دیتا رو دریافت کنم که البته در آینده اپ موبایلی هم اضافه خواد شد.
    سؤال من اینجاست که آیا در هر صفحه که نیاز به استفاده WebApi دارم آیا باید متد doLogin رو فراخوانی کنم یا نه مثلا بعد از لاگین شدن کاربر در برنامه MVC اینکار فقط یکبار انجام میشه؟
    سوال بعد اینه که آیا واقعا مقادیری مثل پسورد باید در doLogin مشخص باشه؟ مبحث امنیت پسورد چطور خواهد شد؟
    • #
      ‫۵ سال و ۱ ماه قبل، چهارشنبه ۱۶ مرداد ۱۳۹۸، ساعت ۲۳:۳۵
      - قسمت web api برنامه با mvc یکپارچه هست؟ اگر بله، برای کار با web api نیازی به این روش ندارید. همینقدر که کاربر برای مثال از طریق روش‌هایی مانند ASP.NET Identity و یا Forms authentication به سایت وارد شده باشد، کوکی حاصل از اعتبارسنجی آن‌ها، به همراه اعمال Ajax ای مختص Web API هم به صورت خودکار ارسال می‌شود و ... کاربر با موفقیت اعتبارسنجی خواهد شد. پروژه‌ی مطلب «اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity» یک قسمت Web API محافظت شده هم دارد؛ با این view که کاملا عادی به نظر می‌رسد. چون سیستم یکپارچه هست.
      - کاربرد JWT بیشتر در برنامه‌های SPA مانند Angular است: «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular»
      در اینجا کاربر پس از لاگین (از طریق صفحه‌ی لاگینی، کاملا عادی و معمولی که هیچ اطلاعاتی هم از پیش در آن ذخیره نشده)، چیزی را که سمت کلاینت ذخیره می‌کند (برای مثال در local storage مرورگر)، فقط توکن‌های دریافتی پس از اعتبارسنجی موفق است و نه کلمه‌ی عبور و نه هیچ اطلاعات دیگری. سپس در هر درخواست به منابع محافظت شده‌ی سمت سرور، صرفا این توکن‌ها را ارسال می‌کند تا توسط آن، کار اعتبارسنجی خودکار کاربر صورت گیرد و هویت و همچنین سطوح دسترسی آن مشخص شوند.
      - مثالی که در این پروژه ارائه شده، نمونه‌ی دیگر «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» است؛ جهت ارائه‌ی یک سری مفهوم.
  • #
    ‫۴ سال و ۱۰ ماه قبل، دوشنبه ۲۰ آبان ۱۳۹۸، ساعت ۲۳:۱۱
    سلام. آیا میشه اطلاعات اضافه‌تری هم در Header درخواست فرستاد یا فقط jwt با هدر درخواست فرستاده میشه و بقیه موارد باید اضافه بشه به claim و از طریق بدنه درخواست باشه؟
    • #
      ‫۴ سال و ۱۰ ماه قبل، دوشنبه ۲۰ آبان ۱۳۹۸، ساعت ۲۳:۵۴
      - اطلاعات اضافه‌تر مانند DisplayName و خواندن آن مانند UserData. مزیت؟ دارای امضای دیجیتال بودن.
      - امکان خواندن ریز اطلاعات درخواست هم وجود دارد؛ مانند خواندن اطلاعات فرم و یا کلا هر آنچه که در context.Request وجود دارد.
  • #
    ‫۴ سال و ۹ ماه قبل، یکشنبه ۳ آذر ۱۳۹۸، ساعت ۱۹:۳۹
    با اجرای مثال خود شما و قرار دادن Breakpoint و  بعد از صدا زدن اکشن Get در کنترلر MyProtectedApi با خطای زیر روبرو شدم:
    {
      "message": "Authorization has been denied for this request."
    }
    داخل این متد در OnAuthorization:
    if (!TokenStoreService().IsValidToken(accessToken, int.Parse(userId)))
    {
       // this is not our issued token
       this.HandleUnauthorizedRequest(actionContext);
       return;
    }
    عملیات با شکست مواجه می‌شود و خطای Unauthorized 401 پدیدار می‌شود.
  • #
    ‫۳ سال و ۶ ماه قبل، چهارشنبه ۶ اسفند ۱۳۹۹، ساعت ۲۱:۳۸
    با سلام و احترام؛ این ساختار تا الان کار میکرد. حالا بعد از لاگین و گرفتن توکن و ارسال به api با مشکلی مواجه میشم که مربوط به قطعه کد زیر هست:
    var claimsIdentity = actionContext.RequestContext.Principal.Identity as ClaimsIdentity;
                if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
    که Claims نال بر میگرده. مشکل از کجاست؟ ممنون میشم راهنمایی بفرمائید.
    • #
      ‫۳ سال و ۶ ماه قبل، پنجشنبه ۷ اسفند ۱۳۹۹، ساعت ۰۰:۴۵
      توکن ارسالی به سرور را در سایت jwt.io دیکود کنید و بررسی کنید که آیا به همراه Claims مدنظر هست یا خیر.