ავტორიზაციის ნაკადი (Authorization Code + PKCE)
ეს გვერდი აღწერს სრულ ციკლს — გადამისამართებიდან ტოკენების მიღებამდე და განახლებამდე. თუ OIDC ბიბლიოთეკას იყენებთ, ის ამ ნაბიჯების უმეტესობას ავტომატურად გააკეთებს; აქ ჩამოწერილია ის, რაც უნდა იცოდეთ კონფიგურაციისა და შეცდომების დამუშავებისთვის.
ნაბიჯი 1 — PKCE წყვილის გენერაცია
ყოველი შესვლისთვის აგენერირებთ ახალ შემთხვევით code_verifier-ს (43–128 სიმბოლო, [A-Za-z0-9-._~]) და ითვლით code_challenge = BASE64URL(SHA256(code_verifier)). Verifier რჩება თქვენთან (სესიაში), challenge მიდის authorize მოთხოვნაში.
S256 მეთოდი — plain ან challenge-ის გარეშე მოთხოვნა უარყოფილი იქნება invalid_request შეცდომით.
ნაბიჯი 3 — დაბრუნება redirect_uri-ზე
წარმატების შემთხვევაში მომხმარებელი ბრუნდება {redirect_uri}?code=…&state=… მისამართზე. ჯერ შეამოწმეთ state, შემდეგ გაცვალეთ კოდი. კოდი ერთჯერადია და მოკლევადიანი (რამდენიმე წუთი) — გაცვლა დაუყოვნებლივ მოახდინეთ.
შეცდომის შემთხვევაში იმავე მისამართზე მოდის ?error=…&error_description=…&state=…:
| error | როდის | რა უნდა გააკეთოთ |
|---|---|---|
| access_denied | მომხმარებელმა დააჭირა „უკან“ / გააუქმა შესვლა. | დააბრუნეთ მომხმარებელი შესვლის გვერდზე, შეცდომის ჩვენების გარეშე. |
| login_required | prompt=none, ბრაუზერში INEX ID-ის აქტიური სესია არ არის (ან max_age-ზე ძველია). |
გაუშვით ჩვეულებრივი (ინტერაქტიული) შესვლა. |
| account_selection_required | prompt=none, ბრაუზერში რამდენიმე ანგარიშია შესული. |
გაუშვით შესვლა prompt=select_account-ით. |
| invalid_request | აკლია ან არასწორია პარამეტრი (მაგ. PKCE, მეთოდი არაა S256). | შეასწორეთ მოთხოვნა — ეს თქვენი მხარის შეცდომაა. |
| invalid_scope | მოთხოვნილი scope არ არსებობს. | გამოიყენეთ მხოლოდ დოკუმენტირებული scope-ები. |
client_id ან redirect_uri არასწორია, INEX ID უსაფრთხოების მიზნით მომხმარებელს თქვენთან არ აბრუნებს და შეცდომას საკუთარ გვერდზე აჩვენებს.
ნაბიჯი 4 — კოდის გაცვლა ტოკენებზე
{issuer}/oauth/token
თქვენი სერვერიდან აგზავნით POST {issuer}/oauth/token მოთხოვნას, Content-Type: application/x-www-form-urlencoded. Confidential client ავთენტიფიცირდება client_secret-ით — ან body-ში (client_secret_post), ან HTTP Basic ჰედერით (client_secret_basic). Public client secret-ს არ აგზავნის.
| ველი | სავალდებულო | აღწერა |
|---|---|---|
| grant_type | კი | authorization_code. |
| code | კი | callback-ში მიღებული კოდი. |
| redirect_uri | კი | ზუსტად იგივე, რაც authorize მოთხოვნაში იყო. |
| client_id | კი | თქვენი client-ის იდენტიფიკატორი. |
| client_secret | პირობითი | მხოლოდ confidential client-ისთვის (თუ Basic ჰედერს არ იყენებთ). |
| code_verifier | კი | ნაბიჯ 1-ში შენახული verifier. |
პასუხი 200 OK
| ველი | ტიპი | აღწერა |
|---|---|---|
| token_type | string | ყოველთვის Bearer. |
| expires_in | integer | access token-ის სიცოცხლის ხანგრძლივობა წამებში. გამოიყენეთ ეს მნიშვნელობა — ნუ ჩაწერთ კოდში ფიქსირებულ დროს. |
| access_token | string | ტოკენი /userinfo-ის გამოსაძახებლად. |
| refresh_token | string | ახალი access token-ის მისაღებად (იხ. ნაბიჯი 6). ინახეთ მხოლოდ სერვერზე, დაშიფრულად. |
| id_token | string (JWT) | ხელმოწერილი JWT მომხმარებლის იდენტობით. ბრუნდება მაშინ, როცა scope-ში openid იყო. |
{"error": "…", "error_description": "…"}. ყველაზე ხშირი: invalid_grant (კოდი ვადაგასულია, უკვე გამოყენებულია, verifier ან redirect_uri არ ემთხვევა), invalid_client (არასწორი secret), unsupported_grant_type (მხარდაჭერილია მხოლოდ authorization_code და refresh_token).
ნაბიჯი 5 — id_token-ის შემოწმება
id_token არის RS256-ით ხელმოწერილი JWT. ნდობამდე აუცილებლად შეამოწმეთ:
-
ხელმოწერა — საჯარო გასაღებით
{issuer}/oauth/jwks-დან; გასაღები შეარჩიეთ ჰედერისkid-ით. JWKS დაქეშეთ, ხოლო უცნობიkid-ის შემთხვევაში ხელახლა ჩამოტვირთეთ (გასაღების როტაცია). -
iss— ზუსტად ემთხვევა issuer-ს (მაგ.https://id.inex.ge, ბოლოში/-ის გარეშე). -
aud— ემთხვევა თქვენსclient_id-ს. -
exp— ტოკენი ვადაგასული არ არის (id_token მოქმედებს 1 საათი). -
nonce— ემთხვევა authorize მოთხოვნაში გაგზავნილს.
sub
sub არის მომხმარებლის მუდმივი იდენტიფიკატორი (UUID). ის არასოდეს იცვლება და ერთნაირია ყველა client-ისთვის. სწორედ ის უნდა შეინახოთ და გამოიყენოთ მომხმარებლის საიდენტიფიკაციოდ — არა ელ. ფოსტა ან ტელეფონი, რომლებიც შეიძლება შეიცვალოს (ტელეფონი, ამასთან, უნიკალურიც არ არის).
ნაბიჯი 6 — ტოკენის განახლება
access token-ის ვადის ამოწურვისას აგზავნით POST {issuer}/oauth/token-ს: grant_type=refresh_token, refresh_token, client_id (+ client_secret confidential client-ისთვის). პასუხი იგივე ფორმისაა: ახალი access token, ახალი refresh token და ახალი id_token (nonce/auth_time-ის გარეშე).
invalid_grant-ს. თუ ერთსა და იმავე მომხმარებელზე პარალელური მოთხოვნები გაქვთ, განახლება ერთი lock-ით დაიცავით, რომ ორმა პროცესმა ერთი და იგივე refresh token ერთდროულად არ გამოიყენოს.
ჩუმი შემოწმება (prompt=none)
თუ გინდათ მომხმარებელი ავტომატურად შეიყვანოთ, როცა ის INEX ID-ზე უკვე შესულია (მაგ. სხვა INEX-ის სერვისიდან მოსული), გააგზავნეთ ჩვეულებრივი authorize მოთხოვნა prompt=none-ით. ან მაშინვე დაბრუნდება კოდით, ან login_required / account_selection_required შეცდომით — ფორმა მომხმარებელს არ გამოუჩნდება.
import crypto from 'node:crypto';
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
const state = crypto.randomBytes(16).toString('base64url');
const nonce = crypto.randomBytes(16).toString('base64url');
// keep codeVerifier, state and nonce in the user's session
$codeVerifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
GET https://id-staging.inex.ge/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fshop.example.com%2Fauth%2Finex%2Fcallback
&scope=openid%20profile%20email%20phone
&state=STATE
&nonce=NONCE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&ui_locales=ka
GET https://shop.example.com/auth/inex/callback?code=def50200a1...&state=STATE
GET https://shop.example.com/auth/inex/callback?error=access_denied&error_description=The+customer+cancelled+the+sign-in.&state=STATE
curl -X POST https://id-staging.inex.ge/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=def50200a1... \
-d redirect_uri=https://shop.example.com/auth/inex/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=CODE_VERIFIER
{
"token_type": "Bearer",
"expires_in": 86400,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200b7f3...",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9..."
}
{
"iss": "https://id-staging.inex.ge",
"aud": "YOUR_CLIENT_ID",
"sub": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"iat": 1760961600,
"exp": 1760965200,
"auth_time": 1760961590,
"nonce": "NONCE",
"given_name": "Nino",
"family_name": "Beridze",
"name": "Nino Beridze",
"locale": "ka",
"email": "nino@example.com",
"email_verified": true,
"phone_number": "995599123456",
"phone_number_verified": true
}
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://id-staging.inex.ge/oauth/jwks'));
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: 'https://id-staging.inex.ge',
audience: 'YOUR_CLIENT_ID',
});
if (payload.nonce !== session.nonce) throw new Error('nonce mismatch');
const inexUserId = payload.sub; // store it with your user
curl -X POST https://id-staging.inex.ge/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=refresh_token \
-d refresh_token=def50200b7f3... \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET