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
همانطور که ممکن است در آن پاسخ متوجه شوید، در “سند انجام دادیک کلید ویژه در زیر وجود دارد:
، که به یک 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 نیز یک ورودی ویژه در زیر وجود دارد:
، معمولاً تنها با یک ورودی (در قالب 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
.
خلاصه: برای بازیابی توکن کاربر، باید:
- تماس بگیرید “
token_endpoint
“. - اما، قبل از آن، باید مجوز دریافت کنیم، با تماس با “
authorization_endpoint
“. - و همچنین، قبل از مراحل بالا، باید به سرور بگوییم که قرار است آن عملیات را با استفاده از ” انجام دهیم.
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. شما می توانید در اینجا پیدا کنید کد منبع.