برنامه نویسی

Bluesky OAuth2 Client، با Vanilla JavaScript

سلام، وجود دارد! ;^)

مقدمه

این پست در مورد ادغام صحبت می کند احراز هویت Bluesky (OAuth + DPoP) در یک “بدون سرور“برنامه مشتری، فقط با حکمرانی”جاوا اسکریپت وانیلی“.
برای شما خوبه؟ باشه… بریم!

سلب مسئولیت

این پست صرفا برای نشان دادن است”چگونه به“. این یک” نیستنمونه کار“؛ عمدتاً به دلیل منقضی شدن توکن ها! ;^)

در صورت مشاهده هرگونه خطایی، لطفاً در تماس با من برای اصلاح آن شک نکنید!

برنامه ما

کلمه “OAuth”.

فرض کنید می‌خواهیم یک برنامه بدون سرور توسعه دهیم، برای هر کسی که می‌خواهد دسترسی داشته باشد، اما ما نیاز داریم که کاربران خود را احراز هویت کنند تا به آن دسترسی پیدا کنند.

یک گزینه این است که از کاربر ورودی درخواست کنید تا مستقیماً در برنامه ثبت نام خودکار کند (ایجاد یک حساب کاربری و ایجاد یک “اعتبارنامه”؛ معمولا)، ترکیبی از ورود به سیستم با رمز عبور برای آن حساب. اما راه دیگری برای احراز هویت آنها این است که “اعتماد“یک مقام شخص ثالث.

درست مثل گوگل در این مورد انجام می دهد بلواسکی همچنین راهی برای کاربران bluesky برای احراز هویت در جایی که لازم است فراهم می کند.

این توسط هدایت می شود OAuth2 Protocol.

اطلاعات بیشتر در مورد نحوه OAuth در داخل کار می کند بلواسکی را می توان در اینجا یافت: OAuth – پروتکل AT.

فراداده مشتری… این چیست؟

به منظور اینکه برنامه وب جدید ما (فرض کنید این یک برنامه مبتنی بر جاوا اسکریپت است) می تواند از این مکانیسم احراز هویت استفاده کند بلواسکی، خدمات/سرورهای احراز هویت Bluesky باید برنامه ما را بشناسید چگونه می توانیم آن را انجام دهیم؟
آسان! تولید الف فراداده مشتری“فایل، که تمام اطلاعاتی را که سرویس ها/سرورهای احراز هویت Bluesky برای ارائه اطلاعات کاربر به برنامه نیاز دارند، در خود نگه می دارد.

با این فایل، همانطور که در بالا در صفحه “پروتکل OAuth” بیان شد، “ثبت خودکار مشتری با استفاده از ابرداده مشتری“رویکرد دنبال می شود. این بدان معنی است که نیازی به” نخواهد بودثبت نام کنید“برنامه جدید ما در هر سرور احراز هویت، کافی است یک “فراداده“فایل به صورت خودکار یک”مشتری OAuth Bluesky“.

بنابراین برای اینکه این کار عمل کند، تنها چیزی که در سیستم خود نیاز داریم یک “client-metadata.json” را فایل کنید و آن را در زیر در دسترس قرار دهید https:// پروتکل

توجه: می توانیم فایل را با هر نامی که بخواهیم نامگذاری کنیم. حتی بیشتر، ما می توانیم آن را در هر جایی قرار دهیم. این فقط یک توصیف کننده است.

بنابراین، ما باید یک را ایجاد کنیم JSON فایلی که سرورهای احراز هویت Bluesky برای شناسایی برنامه ما درخواست خواهند کرد. برای آنها، برنامه های ما یک “درخواست مشتریتوضیح داده شده توسط آن فایل JSON.

فایل فراداده“، برای ما”برنامه مشتری“، باید از هر کجای اینترنت قابل دسترسی باشد.

توجه: به عنوان یک مثال، ما یک فایل را در این url مستقر کرده ایم: https://madrilenyer.neocities.org/bsky/oauth/client-metadata.json. اگر روی آن لینک کلیک کنید، محتویات آن را مشاهده خواهید کرد. مستقیما

بنابراین، برای نشان دادن یک مثال، و پیروی از دستورالعمل های Bluesky در اینجا و اینجا، ما یک ” را تنظیم کرده ایم.فایل فراداده” که شبیه این است:

client-metadata.json:

{
  "client_id":"https://madrilenyer.neocities.org/bsky/oauth/client-metadata.json",
  "application_type":"web",
  "grant_types":[
    "authorization_code",
    "refresh_token"
  ],
  "scope":"atproto transition:generic transition:chat.bsky",
  "response_types":[
    "code id_token",
    "code"
  ],
  "redirect_uris":[
    "https://madrilenyer.neocities.org/bsky/oauth/callback/"
  ],
  "dpop_bound_access_tokens":true,
  "token_endpoint_auth_method":"none",
  "client_name":"Madrilenyer Example Browser App",
  "client_uri":"https://madrilenyer.neocities.org/bsky/"
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این JSON فایل “توصیف می کند“برنامه ای که”می خواهد به عنوان یک «برنامه مشتری» Bluesky OAuth شناسایی شود“.

اکنون، ما آماده ایم تا از کاربران درخواست احراز هویت کنیم بلواسکی.

بیایید از … یک زبان برنامه نویسی استفاده کنیم

مشکل اینجاست که چندین پیاده سازی از “نحوه انجام“این ادغام اما با چارچوب های مدرن بچه ها از @atproto.com یک بسته فوق العاده را در TypeScript و برخی از بچه ها راه حل هایی با آن دارند NodeJS.

اما شخصا ترجیح می دهم “جاوا اسکریپت وانیلی“، اول، فقط برای درک اصول اولیه مکانیسم؛ فقط برای یادگیری آن، قبل از شروع استفاده از “کتابخانه” که تقریبا همه را پنهان می کند. مشکل این است که وجود دارد هیچ چیز اون بیرون…

بنابراین این دلیلی است که من این پست را می نویسم.

مبانی: زمینه

باشه بنابراین ما اینجا هستیم.
ما سعی می کنیم به یک کاربر ورودی بگوییم که برای دسترسی به برنامه ما خود را شناسایی کند.
و ما به او پیشنهاد می کنیم که “با Bluesky وارد شوید«، اما… چه کنیم واقعا نیاز؟
حداقل داده ای از کاربر که برای انجام یک فرآیند اعتبار سنجی با یک کاربر احراز هویت شده نیاز داریم چقدر است؟

دسته کاربر

اول از همه، ما به یک “دسته“.
تنها چیزی که نیاز داریم از کاربر اوست دسته.

توجه: «دسته بلوسکی» تمام متنی است که به دنبال «نشانی اینترنتی نمایه بلوسکی» شما می‌آید. شخصیت های بعد از: “https://bsky.app/profile/_______________________“.

این ما “Bluesky *دسته“، ما”Bluesky *حساب**”؛ به عنوان مثال، مال من این است: madrilenyer.bsky.social.

توجه: جی (مدیرعامل Bluesky) مدتی پیش پستی در این باره نوشت. در صورت نیاز، می توانید برای اطلاعات بیشتر در مورد دسته ها، PDS ها، حساب ها، پروتکل AT به Bluesky Docs شیرجه بزنید.

DID کاربر

بنابراین، زمانی که کاربر را بشناسیم “دسته“، اولین قدم این است که کاربر را بازیابی کنید did: کاربر “شناسه غیرمتمرکز“.

توجه: جهنم چیهانجام داد“و چگونه به نظر می رسد؟ خب… اینجا را کلیک کنید یا اینجا

برای جمع آوری کاربر “انجام داد“، ما باید یک API را فراخوانی کنیم (همانطور که گفتم با استفاده از جاوا اسکریپت): DID را بازیابی کنید
فقط این لینک را در مرورگر باز کنید و منتظر بمانید.

اگر به URL نگاه کنید، چیزی شبیه به: [https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=**madrilenyer.bsky.social**].
این بدان معنی است که با فراخوانی این URL اما تغییر “دسته“، شما دریافت خواهید کرد”انجام داد“از موارد مربوطه”دسته“.

توجه: میخوای مال خودت رو امتحان کنی؟ ;^)

بنابراین، این تماس ما را به ما نشان خواهد داد did:

{
  "did": "did:plc:tjc27aje4uwxtw5ab6wwm4km"
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه: این کار برای دسته Bluesky است: [madrilenyer.bsky.social]

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------
    const USER_HANDLE = "madrilenyer.bsky.social";
    const APP_CLIENT_ID = "https://madrilenyer.neocities.org/bsky/oauth/client-metadata.json";
    const APP_CALLBACK_URL = "https://madrilenyer.neocities.org/bsky/oauth/callback/";

    let userDid = null;

    let url = "https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=" + USER_HANDLE;
    fetch( url ).then( response => {
        // Process the HTTP Response
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        // Here, we gather the "did" item in the received json.
        userDid = data.did;
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سند DID کاربر

یک بار با did، و با کمک PLC API، گام بعدی ما بازیابی است user/handle's DID Document.
ما این کار را با فراخوانی یک EndPoint API خاص (https://plc.directory/) به دنبال کاربر انجام داد (“did:plc:tjc27aje4uwxtw5ab6wwm4km“)؛ مال کاربر نیست دسته (“madrilenyer.bsky.social“).

توجه: اطلاعات کلی در مورد DID، PLC و غیره را می توان در اینجا به دست آورد. اطلاعات دقیق در مورد DID PLC اینجا
توجه: این نیز می تواند کمک کند: did:plc Directory Server API (0.1)

بنابراین، با فراخوانی آن URL، ما DID Document (توسط Bluesky تولید یا ذخیره شده است) چیزی شبیه این است (فرمت فایل JSON):

{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/multikey/v1",
    "https://w3id.org/security/suites/secp256k1-2019/v1"
  ],
  "id": "did:plc:tjc27aje4uwxtw5ab6wwm4km",
  "alsoKnownAs": [
    "at://madrilenyer.bsky.social"
  ],
  "verificationMethod": [
    {
      "id": "did:plc:tjc27aje4uwxtw5ab6wwm4km#atproto",
      "type": "Multikey",
      "controller": "did:plc:tjc27aje4uwxtw5ab6wwm4km",
      "publicKeyMultibase": "zQ3shQzL5vznqAdHiD6wvKRfH5xEaDXWpP3JTGQYAfhQo6Dz5"
    }
  ],
  "service": [
    {
      "id": "#atproto_pds",
      "type": "AtprotoPersonalDataServer",
      "serviceEndpoint": "https://velvetfoot.us-east.host.bsky.network"
    }
  ]
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کدنویسی آن در Vanilla JavaScript:

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------
    let userDidDocument = null;
    let userPDSURL = null;

    let url = "https://plc.directory/" + USER_HANDLE;
    fetch( url ).then( response => {
        // Process the HTTP Response
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        userDidDocument = data;
        userPDSURL = userDidDocument.service[0].serviceEndpoint;
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آدرس PDS

همانطور که ممکن است در آن پاسخ متوجه شوید، در “سند انجام دادیک کلید ویژه در زیر وجود دارد: .service.serviceEndpoint، که به یک URL اشاره می کند: https://velvetfoot.us-east.host.bsky.network

این آدرس آدرس ماست سرور PDS.

توجه: باز هم … “PDS Server” چیست؟ خوب… کلیک کنید اینجا. برای کسانی که استفاده می کنند ماستودون درست مثل یک “نمونه“.

اگر آن URL را در مرورگر باز کنید (آدرس سرور PDS؛ دوباره، برای عموم قابل دسترس است، تنها چیزی که می بینید باید چیزی شبیه به این باشد:

This is an AT Protocol Personal Data Server (PDS): https://github.com/bluesky-social/atproto

Most API routes are under /xrpc/
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین … این بدان معنی است که هر زمان که ما نیاز به درخواست چیزی از خود داشته باشیم سرور PDS، باید یک URL بسازیم که با چیزی شبیه به:

https://velvetfoot.us-east.host.bsky.network/xrpc/[whatever_follows]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

فراداده PDS

چیز دیگری که باید بازیابی شود این است فراداده سرور PDS. این اطلاعات اولیه ای است که سرور در اختیار همه قرار می دهد و در این URL قابل دسترسی است: https://velvetfoot.us-east.host.bsky.network/.well-known/oauth-protected-resource.

پاسخ چیزی شبیه به این است:

{
  "resource": "https://velvetfoot.us-east.host.bsky.network",
  "authorization_servers": [
    "https://bsky.social"
  ],
  "scopes_supported": [],
  "bearer_methods_supported": [
    "header"
  ],
  "resource_documentation": "https://atproto.com"
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه: تغییر “نام میزبان” بخشی از URL (https://velvetfoot.us-east.host.bsky.network) توسط یک جهت میزبان دیگر سرور PDS، نتایج مشابهی را ایجاد خواهد کرد.

درست مانند قبل، در این JSON نیز یک ورودی ویژه در زیر وجود دارد: .authorization_servers، معمولاً تنها با یک ورودی (در قالب JSON، یک آرایه است) و در این مورد، این ورودی نشان دهنده سرور مجوز این PDS استفاده می کند. در این مورد، PDS ما به آن اشاره می کند این سرور مجوز: https://bsky.social.

این URL، یکی از سرور مجوز، مورد نیاز است زیرا هر محافظت شده است درخواست به سرور PDS ما، محافظت شده با OAuth2، به یک نیاز دارد توکن کاربر، که فقط می توان به دست آورد از سرور مجوز زمانی که در برابر آن احراز هویت شدیم.

این بدان معنی است که، اول از همه، ما باید خودمان را در برابر آن سرور احراز هویت/مجوز شناسایی کنیم تا به برنامه شخص ثالث اجازه دهیم تا “ما” را بازیابی کند.توکن کاربر” برای اجرا هر اقدامی کاربر می خواهد.

بنابراین، در صورت وجود، نگاهی به “فرداده های سرور احراز هویت/مجازات” بیندازیم.

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------
    let userPDSMetadata = null;
    let userAuthServerURL = null;

    let url = userPDSURL + "/.well-known/oauth-protected-resource";
    fetch( url ).then( response => {
        // Process the HTTP Response
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        userPDSMetadata = data;
        userAuthServerURL = userPDSMetadata.authorization_servers[0];
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کشف سرور مجوز

مرحله بعدی جمع آوری ابرداده سرور مجوز است. این نیز بخشی از پروتکل OAuth2 است (معروف به: “کشف”) و در این مورد، می توانید با: https://bsky.social/.well-known/oauth-authorization-server دسترسی داشته باشید

بار دیگر، برای عموم قابل دسترسی است، بنابراین اگر روی آن URL کلیک کنیم، این اطلاعات را دریافت خواهیم کرد. “کشفاطلاعات سرور مجوز ما:

{
  "issuer":"https://bsky.social",
  "scopes_supported":[
    "atproto",
    "transition:generic",
    "transition:chat.bsky"
  ],
  "subject_types_supported":[
    "public"
  ],
  "response_types_supported":[
    "code"
  ],
  "response_modes_supported":[
    "query",
    "fragment",
    "form_post"
  ],
  "grant_types_supported":[
    "authorization_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported":[
    "S256"
  ],
  "ui_locales_supported":[
    "en-US"
  ],
  "display_values_supported":[
    "page",
    "popup",
    "touch"
  ],
  "authorization_response_iss_parameter_supported":true,
  "request_object_signing_alg_values_supported":[
    "RS256",
    "RS384",
    "RS512",
    "PS256",
    "PS384",
    "PS512",
    "ES256",
    "ES256K",
    "ES384",
    "ES512",
    "none"
  ],
  "request_object_encryption_alg_values_supported":[

  ],
  "request_object_encryption_enc_values_supported":[

  ],
  "request_parameter_supported":true,
  "request_uri_parameter_supported":true,
  "require_request_uri_registration":true,
  "jwks_uri":"https://bsky.social/oauth/jwks",
  "authorization_endpoint":"https://bsky.social/oauth/authorize",
  "token_endpoint":"https://bsky.social/oauth/token",
  "token_endpoint_auth_methods_supported":[
    "none",
    "private_key_jwt"
  ],
  "token_endpoint_auth_signing_alg_values_supported":[
    "RS256",
    "RS384",
    "RS512",
    "PS256",
    "PS384",
    "PS512",
    "ES256",
    "ES256K",
    "ES384",
    "ES512"
  ],
  "revocation_endpoint":"https://bsky.social/oauth/revoke",
  "introspection_endpoint":"https://bsky.social/oauth/introspect",
  "pushed_authorization_request_endpoint":"https://bsky.social/oauth/par",
  "require_pushed_authorization_requests":true,
  "dpop_signing_alg_values_supported":[
    "RS256",
    "RS384",
    "RS512",
    "PS256",
    "PS384",
    "PS512",
    "ES256",
    "ES256K",
    "ES384",
    "ES512"
  ],
  "client_id_metadata_document_supported":true
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برخی از ورودی‌ها در اینجا وجود دارد که می‌توان از آنها برای بازیابی ما استفاده کردتوکن کاربر«… شروع کنیم:

  • authorization_endpoint: برای درخواست مجوز دسترسی به توکن کاربر به این URL نیاز داریم. در این مورد، این ورودی این است: https://bsky.social/oauth/authorize
  • token_endpoint: این نشانی اینترنتی برای درخواست رمز دسترسی کاربر است در این مورد، این ورودی است: https://bsky.social/oauth/token
  • pushed_authorization_request_endpoint (توسط EndPoint): الف”پیش نیازتمام تماس‌هایی که به سرور مجوز برای به دست آوردن توکن کاربر انجام می‌شود، باید اعتبارسنجی شوند. RFC 9126
    در این مورد، این ورودی: https://bsky.social/oauth/par

و اینجا به پایان می رسد “در دسترس عموم” مراحل احراز هویت کاربران در برابر برنامه ما.
از این پس باید به توسعه اپلیکیشن جاوا اسکریپت ادامه دهیم. “به دلیل دیگر”انواع درخواست ها“نیاز هستند، نه به آسانی”اینجا را کلیک کنید“.

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------
    let userAuthServerDiscovery = null;
    let userAuthorizationEndPoint = null;
    let userTokenEndPoint = null;
    let userPAREndPoint = null;

    let url = userAuthServerURL + "/.well-known/oauth-authorization-server";
    fetch( url ).then( response => {
        // Process the HTTP Response
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        userAuthServerDiscovery   = data;
        userAuthorizationEndPoint = userAuthServerDiscovery.authorization_endpoint;
        userTokenEndPoint         = userAuthServerDiscovery.token_endpoint;
        userPAREndPoint           = userAuthServerDiscovery.pushed_authorization_request_endpoint;
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

احراز هویت

به عنوان کمک، Bluesky یک ورودی دارد که توضیح می دهد “چگونه به“همه این مراحل را انجام دهید… به جز آخرین مورد. بعدا خواهیم دید.

درخواست PAR

هنگامی که در این مرحله، ما باید درخواست PAR Authorization; این را می توان با استفاده از PKCE.

خلاصه: برای بازیابی توکن کاربر، باید:

  1. تماس بگیرید “token_endpoint“.
  2. اما، قبل از آن، باید مجوز دریافت کنیم، با تماس با “authorization_endpoint“.
  3. و همچنین، قبل از مراحل بالا، باید به سرور بگوییم که قرار است آن عملیات را با استفاده از ” انجام دهیم.pushed_authorization_request_endpoint“، توسط EndPoint.

توجه: درخواست‌های مجوز تحت فشار OAuth 2.0 مشخصات اینجاست

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

  • دولت: اول، ما به یک “دولت” نیاز داریم. یک رشته با 28 شخصیت های تصادفی برای اهداف ما، این مقدار باید: 2e94cf77e8b0ba2209dc6dcb90018c8d044ac31cb526fc4823278585
  • code_verifier: بعداً یک “code_verifier” مورد نیاز است؛ درست مانند قبل. برای اهداف ما، این مقدار باید: 46148ae0fd74b698a5f78efc44a8f76f1fd778602b14b46a2318a814
  • کد_چالش: در نهایت، از “code_verifier“ما باید یک” تولید کنیمcode_challengeاساساً این: base64urlencode( sha256( code_verifier ) );
    برای اهداف ما، این مقدار باید: URQ-2arwHpJzNwcFPng-_IE3gRGGBN0SVoFMN7wEiWI

توجه: ما به پارامترهای بیشتری نیاز خواهیم داشت، اما همه آنها در این مرحله به خوبی شناخته شده اند:
+ چند ثابت ثابت (code_challenge_method، محدوده)
+ برخی از داده ها از ما client-metadata.json فایل (client_id، redirect_uri، login_hint)
+ و برخی از داده های تازه تولید شده (کد_چالش، حالت)

اکنون، با تمام آن اطلاعات، یک را آماده می کنیم POST درخواست در برابر URL نشان داده شده در pushed_authorization_request_endpoint کلید (در این مورد: https://bsky.social/oauth/par) با اینها content-type: application/x-www-form-urlencodedو این “بدن“:

response_type=code&code_challenge_method=S256&scope=atproto+transition%3Ageneric&client_id=https%3A%2F%2Fmadrilenyer.neocities.org%2Fbsky%2Foauth%2Fclient-metadata.json&redirect_uri=https%3A%2F%2Fmadrilenyer.neocities.org%2Fbsky%2Foauth%2Fcallback%2F&code_challenge=URQ-2arwHpJzNwcFPng-_IE3gRGGBN0SVoFMN7wEiWI&state=2e94cf77e8b0ba2209dc6dcb90018c8d044ac31cb526fc4823278585&login_hint=madrilenyer.bsky.social
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تقسیم شده:

  response_type=code
    &code_challenge_method=S256
    &scope=atproto+transition%3Ageneric
    &client_id=https%3A%2F%2Fmadrilenyer.neocities.org%2Fbsky%2Foauth%2Fclient-metadata.json
    &redirect_uri=https%3A%2F%2Fmadrilenyer.neocities.org%2Fbsky%2Foauth%2Fcallback%2F
    &code_challenge=URQ-2arwHpJzNwcFPng-_IE3gRGGBN0SVoFMN7wEiWI
    &state=2e94cf77e8b0ba2209dc6dcb90018c8d044ac31cb526fc4823278585
    &login_hint=madrilenyer.bsky.social
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که ما ارسال می کنیم state و code_challenge; نه code_challenge; ما از این آخرین مقدار برای بررسی چیزها بعدا استفاده خواهیم کرد.

این یک نمونه از پاسخ است (201 (Created)):

{
  "request_uri": "urn:ietf:params:oauth:request_uri:req-df74117722b7f1e7d807d4244a8dae0a",
  "expires_in": 299
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما نیاز داریم request_uri مورد برای مرحله بعدی
و همچنین در سرفصل های پاسخ می توان به موارد زیر پی برد: [DPoP-Nonce] سربرگ؛ معروف به “nonceما بعداً به ارزش آن نیاز خواهیم داشت.

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------
    let dpopNonce = null;
    let userAuthServerRequestURI = null;

    // The AuthServer Discovery Information
    // ------------------------------------------
    let url = userAuthServerURL + "/.well-known/oauth-authorization-server";
    fetch( url ).then( response => {
        // Process the HTTP Response
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        userAuthServerDiscovery   = data;
        userAuthorizationEndPoint = userAuthServerDiscovery.authorization_endpoint;
        userTokenEndPoint         = userAuthServerDiscovery.token_endpoint;
        userPAREndPoint           = userAuthServerDiscovery.pushed_authorization_request_endpoint;
    });

    // The state
    // ------------------------------------------
    let stateArray = new Uint32Array(28);
    window.crypto.getRandomValues(stateArray);
    let state = Array.from(stateArray, dec => ('0' + dec.toString(16)).substr(-2)).join('');

    // The code verifier
    // ------------------------------------------
    let codeVerifierArray = new Uint32Array(28);
    window.crypto.getRandomValues(codeVerifierArray);
    let codeVerifier = Array.from(codeVerifierArray, dec => ('0' + dec.toString(16)).substr(-2)).join('');

    // The code verifier challenge
    // ------------------------------------------
    let hashedCodeVerifier = await sha256(codeVerifier);
    let codeChallenge = base64urlencode(hashedCodeVerifier);

    // Build up the URL.
    // Just, to make it simple! I know there are better ways to do this, BUT...
    // ------------------------------------------
    let url = userPAREndPoint;
    let body = "response_type=code";
    body += "&code_challenge_method=S256";
    body += "&scope=" + encodeURIComponent( "atproto transition:generic" ); // MUST match the scopes in the client-metadata.json
    body += "&client_id=" + encodeURIComponent( APP_CLIENT_ID );
    body += "&redirect_uri=" + encodeURIComponent( APP_CALLBACK_URL );
    body += "&code_challenge=" + codeChallenge;
    body += "&state=" + state;
    body += "login_hint=" + USER_HANDLE;

    // TuneUp and perform the call
    // ------------------------------------------
    let fetchOptions = {
        method: 'POST',
        headers: {
            'Content-Type': "application/x-www-form-urlencoded"
        },
        body: body
    }
    fetch( url, fetchOptions ).then( response => {
        // Process the HTTP Response
        dpopNonce = response.headers.get( "dpop-nonce" );
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        userAuthServerRequestURI = data.request_uri;
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

احراز هویت کاربر

ما به اندازه کافی برای درخواست از کاربر برای احراز هویت در برابر سرور Bluesky … چگونه؟

خوب، ما باید “ساختنیک URL جدید برای هدایت کاربر به آن.
چیزی شبیه این است:

[`authorization_endpoint`]?client_id=[client_id]&request_uri=[`request_uri`]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه: پارامترهای URL باید کدگذاری شوند. شما می توانید از این رمزگذار استفاده کنید، فکر می کنم جاوا اسکریپت استفاده می کند encodeURIComponent

در مورد ما، URL ما به این شکل است (ارزش ها ممکن است یکسان نباشند):

https://bsky.social/oauth/authorize?client_id=https%3A%2F%2Fmadrilenyer.neocities.org%2Fbsky%2Foauth%2Fclient-metadata.json&request_uri%3Durn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Areq-df74117722b7f1e7d807d4244a8dae0a
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کدگذاری شده در Vanilla Javascript ما:

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------

    // Buld up the URL.
    // ------------------------------------------
    let url = userAuthorizationEndPoint;
    url += "?client_id=" + encodeURIComponent( APP_CLIENT_ID );
    url += "&request_uri=" + encodeURIComponent( userAuthServerRequestURI );

    // Redirect the user to the Bluesky Auth Page
    // ------------------------------------------
    window.location = url;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

صفحه احراز هویت Bluesky OAuth

این URL (خوب … “مشابه”; از آنجایی که اعتبار حدود چند دقیقه است، اگر کلیک کنید، ممکن است صفحه خطایی را مشاهده کنید) کاربر را به صفحه احراز هویت Bluesky هدایت می کند.

در این صفحه، سرور از کاربر درخواست می‌کند تا احراز هویت و در این صورت، به برنامه اجازه استفاده از شما را بدهد.acess_token“اجرا کردن”things“به نام تو

درست مثل گوگل، اینطور نیست؟ ;^)

صفحه تغییر مسیر داد

اگر در آن صفحه (به یاد داشته باشید، “صفحه مجوز Bluesky”) کاربر موافق است و می پذیرد برای دادن مجوز به برنامه برای استفاده از “توکن کاربر“، سپس سرور مرورگر کاربر را به “صفحه تغییر مسیر/بازخوانی” هدایت می کند.

یادت باشه که “redirect_uri“پارامتر در حالی که”PAR Requestبالا”؟ بله، آن پارامتر; یکی از مواردی که در “client-metadata.json” فایل، در زیر آرایه (بله، می توانید چندین URL برای پاسخ به تماس اعلام کنید) با کلید مشخص می شود: redirect_uris.

اکنون، جریان به کنترل ما باز می گردد. هنگامی که کاربر در “صفحه برگشت به تماس“، ما چیزی شبیه به آن را دریافت خواهیم کرد این:

https://madrilenyer.neocities.org/bsky/oauth/callback/?iss=https%3A%2F%2Fbsky.social&state=4e47aaac8cbd35ed1a2afff53ce6f4511898d7c2ef0e47b37d77110f&code=cod-b17f75f356b83f35e99c4d7664ed30442a9c79c5c37ecf88261d77db799d0c0f
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تقسیم شده:

https://madrilenyer.neocities.org/bsky/oauth/callback/
    ?iss=https%3A%2F%2Fbsky.social
    &state=4e47aaac8cbd35ed1a2afff53ce6f4511898d7c2ef0e47b37d77110f
    &code=cod-b17f75f356b83f35e99c4d7664ed30442a9c79c5c37ecf88261d77db799d0c0f
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سه پارامتر:

  • iss: “اختیار“؛ در این مورد، URL سرور مجوز Bluesky
  • state: “state“پارامتری که قبلا در قسمت ارسال می کنیم PAR Request، و
  • code: الف (یکبار استفاده) کدی که برنامه برای بازیابی رمز دسترسی کاربر از سرور نیاز دارد.
    // ------------------------------------------
    //   Javascript
    // ------------------------------------------
    let receivedIss = null;
    let receivedState = null;
    let receivedCode = null;

    // Let's retrieve the values from the URL.
    // ------------------------------------------
    // Retrieve the URL.
    let thisURL = new URL(window.location);

    // Retrieve the "search" part from the url
    let parsedSearch = new URLSearchParams(thisURL.search);

    // Retrieve the data.
    let receivedIss = parsedSearch.get("iss");
    let receivedState = parsedSearch.get("state");
    let receivedCode = parsedSearch.get("code");

    // We should include here some checks (the 'iss', the 'state'...), BUT...
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به توکن ها و امنیت دسترسی داشته باشید

اما هنوز یک مشکل وجود دارد؛ برای بازیابی رمز دسترسی کاربر، سرور از ما می خواهد که اعتبار سنجی که ما کسی هستیم که توکن را اداره خواهیم کرد. اپلیکیشن چگونه این کار را انجام می دهد؟

وجود دارد “جدیدمشخصات، تحت پروتکل OAuth، به نام:اثبات مالکیت (DPoP)“.

توجه: مشخصات DPoP اینجاست
در اینجا می توانید توضیحی در مورد DPoP پیدا کنید
Bluesky docs نیز اطلاعاتی در این مورد دارد… در اینجا.

DPoP

ایده از DPoP داده ها به “مقید کردن“برنامه مشتری به نشانه دسترسی کاربر؛ بیایید بگوییم،”این نشانه توسط این برنامه استفاده خواهد شد“، و هیچ کس دیگری. این فقط یکی دیگر از موارد اضافی است سطح امنیت، برای جلوگیری از اینکه کسی رمز را بگیرد و در برنامه دیگری استفاده کند.

مشکل این است که برای پیوند دادن هر دو داده (توکن و “برنامه مشتری”، باید از یک کلید رمزنگاری استفاده کنیم. جاوا اسکریپت می تواند چنین کلیدی را تولید کند و ما می توانیم از آن استفاده کنیم. مؤلفه کلیدی در این مرحله … به یاد داشته باشید “هیچاینجا می آید!

توجه: یک DPoP-Proof مورد نیاز است هر بار ما باید یک EndPoint محافظت شده با OAuth را فراخوانی کنیم. هر DPoP-Proof شامل خواهد شد URL فراخوانی، بنابراین باید برای هر درخواست، DPoP-Proofs جدید (دوباره) ایجاد کنیم.

کاربر access_token

اولین چیزی که ما نیاز داریم این است چند-گام-پیش-دریافت شده است dpop_nonce داده ها در طول تماس با هدر آمد PAR EndPoint، و ما از آن استفاده خواهیم کرد.

چیز دیگری که ما نیاز داریم این است که به سرور بگوییم “ما کی هستیم“، به”پیوند“کاربر access_token به درخواست های آینده ما برای این کار باید یک DPoP-Prook ایجاد کنیم. فقط برای اینکه “عبور کندکلیدهای رمزنگاری ما در سرور.

برای اولین آزمایش، ما هنوز توکن را نداریم، اما می‌توانیم یک DPoP-Proof راه‌اندازی کنیم، چیزی که ما را شناسایی می‌کند، چگونه؟
خوب، بیایید از این سه مورد استفاده کنیم:

  • userTokenEndPoint(**): نقطه پایانی رمز سرور
  • client_id: بیایید بگوییم، “APP_CLIENT_ID“، و
  • dpopNonce: برای ایجاد DPoP-Proof با یک کلید رمزنگاری

ما دوباره یک URL ایجاد خواهیم کرد

    // ------------------------------------------
    //   Javascript
    //
    //   (maybe some steps are wrong 'typed')...
    // ------------------------------------------
    let userAccessToken = null;

    // Build up the URL.
    // ------------------------------------------
    let url = userTokenEndPoint;

    // The body of the call
    // ------------------------------------------
    let body = new URLSearchParams({
        // Fixed values
        'grant_type': 'authorization_code',
        // Constant values
        'client_id': encodeURIComponent( APP_CLIENT_ID ),
        'redirect_uri': encodeURIComponent( APP_CALLBACK_URL ),
        // Variable values
        'code': receivedCode,
        'code_verifier': codeVerifier
    });

    // Create the crypto key.
    // Must save it, 'cause we'll reuse it later.
    // ------------------------------------------
    let keyOptions = {
        name: "ECDSA",
        namedCurve: "P-256"
    };
    let keyPurposes = ["sign", "verify"];
    let key = await crypto.subtle.generateKey(keyOptions, false, keyPurposes).then(function(eckey) {
        return eckey;
    });
    let jwk = await crypto.subtle.exportKey("jwk", key.publicKey).then(function(keydata) {
        return keydata;
    });
    delete jwk.ext;
    delete jwk.key_ops;

    // Create the DPoP-Proof 'body' for this request.
    // ------------------------------------------
    let uuid = self.crypto.randomUUID();
    let dpop_proof_header = {
        typ: "dpop+jwt",
        alg: "ES256",
        jwk: jwk
    };
    let dpop_proof_payload = {
        iss: APP_CLIENT_ID, // Added
        jti: uuid,
        htm: "POST",
        htu: url,
        iat: Math.floor(Date.now() / 1000),
        nonce: dpopNonce
    };

    // Crypt and sign the DPoP-Proof header+body
    // ------------------------------------------
    const h = JSON.stringify(dpop_proof_header);
    const p = JSON.stringify(dpop_proof_payload);
    const partialToken = [
        Base64.ToBase64Url(Base64.utf8ToUint8Array(h)),
        Base64.ToBase64Url(Base64.utf8ToUint8Array(p)),
    ].join(".");
    const messageAsUint8Array = Base64.utf8ToUint8Array(partialToken);

    let signOptions = {
        name: "ECDSA",
        hash: { name: "SHA-256" },
    };
    let signatureAsBase64 = await crypto.subtle.sign(signOptions, key.privateKey, dpop_proof_payload)
    .then(function(signature) {
        return Base64.ToBase64Url(new Uint8Array(signature));
    });

    // The DPoP-Proof
    // ------------------------------------------
    let dpopProof = `${partialToken}.${signatureAsBase64}`;

    // TuneUp the call
    // ------------------------------------------
    let headers = {
        'DPOP': dpopProof,
        'Content-Type': 'application/x-www-form-urlencoded',
        'DPoP-Nonce': dpopNonce
    }
    let fetchOptions = {
        method: 'POST',
        headers: headers,
        body: body.toString()
    }

    // Finally, perform the call
    // ------------------------------------------
    let url = userTokenEndPoint;
    fetch( url, fetchOptions ).then( response => {
        // Process the HTTP Response
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        authServerResponse = data;
        userAccessToken = data.access_token;
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مرحله، متغیر “authServerResponse” (پاسخ از سرور مجوز) باید به شکل زیر باشد:

    {
      "access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.eyJhdWQiOiJkaWQ6d2ViOnZlbHZldGZvb3QudXMtZWFzdC5ob3N0LmJza3kubmV0d29yayIsImlhdCI6MTczNzQ5ODM4NCwiZXhwIjoxNzM3NTAxOTg0LCJzdWIiOiJkaWQ6cGxjOnRqYzI3YWplNHV3eHR3NWFiNnd3bTRrbSIsImp0aSI6InRvay1jYzM0YTYzZjgwNWJjMWQ1MTdhNDNmNzU5YWU3ZjJiNCIsImNuZiI6eyJqa3QiOiJVVW1YVXAwMUxySkctak1WQnJHSG1DZy1FR3UyemRncFBMWjhGZDhYMFlNIn0sImNsaWVudF9pZCI6Imh0dHBzOi8vbWFkcmlsZW55ZXIubmVvY2l0aWVzLm9yZy9ic2t5L29hdXRoL2NsaWVudC1tZXRhZGF0YS5qc29uIiwic2NvcGUiOiJhdHByb3RvIHRyYW5zaXRpb246Z2VuZXJpYyIsImlzcyI6Imh0dHBzOi8vYnNreS5zb2NpYWwifQ.OoKiX0LIofSvCqCsZHKtSa7TrOAdWOlTPapu2EGrSxWeF8qkklaM8HXgtmEPTs1BEGIkol91zz32lE1jI72i9Q",
      "token_type": "DPoP",
      "refresh_token": "ref-5c3ecf03caded355cde56b394dae9d9922fda73434dc02642fcb3e1a5fe2e149",
      "scope": "atproto transition:generic",
      "expires_in": 3599,
      "sub": "did:plc:tjc27aje4uwxtw5ab6wwm4km"
    }
     */
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه: به “مشاهده کنید“چطوره”access_token“، می توانید به JWT

در اینجا ما می رویم!

ما نشانه دسترسی کاربر برای برقراری تماس با Bluesky EndPoints محافظت شده را داریم.از طرف” از کاربر.

تماس های بعدی

از این نقطه به بعد، تمام DPoP-Proofهایی که باید ایجاد شوند (برای تماس های بعدی) باید شامل نه تنها “dpop-nonce“پارامتر، بلکه “atHash“، access_token هش شد.

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------

    // For subsequent calls, we must include the
    // hash of the access token in the DPoP-Proof payload.
    // ------------------------------------------

    // Let's calculate the hash
    let encodedAccessToken = new TextEncoder().encode(userAccessToken);
    let atHash = await crypto.subtle.digest('SHA-256', encodedAccessToken)
    .then(function(hash) {        
        let base = Base64.ToBase64Url(new Uint8Array(hash));
        if (noPadding){
            base = base.replace(/\=+$/, '');
        }    
        return base;
    });

    // Regenerate the UUID.
    let uuid = self.crypto.randomUUID();

    // Add the hash in the DPoP-Proof payload.
    // The "url" is a new one.
    let dpop_proof_payload = {

        // This parameter LINKs the user access token
        // to the call & the application, thru the crypto key
        // ------------------------------------------
        ath: atHash,

        // The method can be "GET" or whatever.
        // ------------------------------------------
        htm: "POST",

        // The "url" should be distinct.
        // ------------------------------------------
        htu: url,

        // The "time stamp" is "now" (UNIX like)
        // ------------------------------------------
        iat: Math.floor(Date.now() / 1000),

        // The brand new uuid.
        // ------------------------------------------
        jti: uuid,

        // The rest of the parameters should be the same
        // ------------------------------------------
        iss: APP_CLIENT_ID,
        nonce: dpopNonce

    };

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با این DPoP-Proof جدید، می توانیم یک ” جدید ایجاد کنیمسرصفحه ها” اعتراض به انجام تماس.

    // ------------------------------------------
    //   Javascript
    // ------------------------------------------

    let headers: {
        'Content-Type': [whichever],
        'Accept': 'application/json',

        // The "Authorization" header now is
        // not a "Bearer" but a "DPoP". 
        // ------------------------------------------
        'Authorization': `DPoP ${userAccessToken}`,

        // The "DPoP-Proof" must be included also
        // in a proper header.
        // ------------------------------------------
        'DPoP': dpopProof
    },
    let fetchOptions = {
        method: 'POST',     // Or "GET", or...
        headers: headers,
        body: body          // Whatever. If needed
    }
    fetch( url, fetchOptions ).then( response => {
        // Process the HTTP Response

        // Normally, the "nonce" should come; to be checked.
        // ------------------------------------------
        dpopNonce = response.headers.get( "dpop-nonce" );
        return response.json();
    }).then( data => {
        // Process the HTTP Response Body
        // Whatever we expect.
    });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کلمات پایانی

البته این فقط یک “شبه جاوا اسکریپت” کد. اگر می خواهید از آن استفاده کنید، توجه داشته باشید که .then(...) توابع هستند “وعده ها“، بنابراین باید بر اساس آن برنامه ریزی کنید.

من هیچ چک یا “کنترل خطا“در کد؛ فقط برای توضیح “مسیر مبارک“، ساده ترین راه. اگر قصد دارید از این کد به عنوان پایه استفاده کنید، به یاد داشته باشید که طبق معمول، تمام بررسی های مورد نیاز و کنترل های خطا را در جریان قرار دهید.

و بالاخره “البتهاستفاده از آن بسیار بسیار بهتر است مشتری رسمی Bluesky TypeScript. شما می توانید در اینجا پیدا کنید کد منبع.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا