คู่มือ Integration SSO

OAuth 2.0 + OpenID Connect สำหรับระบบ Partner

SSO Horizon · https://sso.doae.go.th

1. ภาพรวม

SSO Horizon เป็นระบบยืนยันตัวตนกลาง (Single Sign-On) ของกรมส่งเสริมการเกษตร ใช้มาตรฐาน OAuth 2.0 + OpenID Connect (OIDC) ให้ระบบภายนอกเชื่อมต่อ login ร่วมกันได้

[User] → กด Login ที่ App ของคุณ → redirect ไป sso.doae.go.th (หน้า login) → user login (+ 2FA ถ้ามี) → redirect กลับ /auth/callback?code=...&state=... → backend แลก code → access_token → ดึง userinfo → สร้าง session ใน App
Production: https://sso.doae.go.th ใช้ Authorization Code Flow เท่านั้น

2. ขั้นตอนเริ่มต้น (Onboarding)

ก่อน integrate ต้องขอ OAuth Client จาก SSO Admin โดยเตรียมข้อมูล:

รายการตัวอย่าง
ชื่อระบบDOAE APP / ระบบรายงาน X
Production URLhttps://app.doae.go.th
Redirect URIhttps://app.doae.go.th/auth/callback
Scopesopenid profile email
ผู้ติดต่อdev@partner.co.th

หลัง Admin อนุมัติ จะได้รับ client_id และ client_secret

หรือขอผ่าน Developer Portal (login SSO ก่อน → เมนู Developer Portal → ขอ OAuth Client → รอ Admin approve)

3. Credentials & Environment

ใส่ใน .env ฝั่ง Server เท่านั้น — ห้าม commit ลง git

.env
# === SSO OAuth2 (ฝั่ง Server — ห้าม commit) ===
SSO_ENABLED=true
SSO_ISSUER=https://sso.doae.go.th
SSO_API_URL=https://sso.doae.go.th
SSO_PORTAL_URL=https://sso.doae.go.th
SSO_CLIENT_ID=<client_id จาก SSO Admin>
SSO_CLIENT_SECRET=<client_secret จาก SSO Admin>
SSO_REDIRECT_URI=https://your-app.doae.go.th/auth/callback
SSO_SCOPES=openid profile email

APP_URL=https://your-app.doae.go.th
สำคัญ: client_secret ห้ามใส่ใน frontend, mobile app หรือ JavaScript ที่ client เห็นได้

4. Endpoints

Base URL ทุก endpoint: https://sso.doae.go.th

MethodPathใช้เมื่อ
GET/.well-known/openid-configurationAuto-config OIDC
GET/oauth/authorizeเริ่ม login — redirect browser
POST/oauth/tokenแลก code → token (backend)
GET/oauth/userinfoดึง profile (Bearer token)
GET/api/auth/meprofile ฉบับเต็ม (Bearer token)
POST/api/auth/refreshRefresh access token
POST/api/auth/logoutRevoke session
GET/healthHealth check

Discovery: https://sso.doae.go.th/.well-known/openid-configuration

หมายเหตุ: path /oauth2/* เป็น legacy — ใช้ /oauth/* เท่านั้น

5. OAuth2 Authorization Code Flow

Step 1 — Redirect ไป Login

สร้าง state แบบ random เก็บใน session แล้ว redirect:

Authorize URL
https://sso.doae.go.th/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fyour-app.doae.go.th%2Fauth%2Fcallback&response_type=code&scope=openid%20profile%20email&state=RANDOM_STATE

Step 2 — Callback รับ Code

GET https://your-app.doae.go.th/auth/callback?code=UUID&state=RANDOM_STATE
  • ตรวจ state ต้องตรงกับ session → ไม่ตรง = 403
  • code ใช้ครั้งเดียว หมดอายุ 5 นาที

Step 3 — แลก Token (Backend Only)

Token Request
POST https://sso.doae.go.th/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://your-app.doae.go.th/auth/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
Response
{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 86400
}

Step 4 — ดึง UserInfo

UserInfo Request
GET https://sso.doae.go.th/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN

6. ข้อมูล User

/oauth/userinfo Response
{
  "sub": "df5ddfbd-2ccf-4eb9-bc66-a843c031b335",
  "id": "df5ddfbd-2ccf-4eb9-bc66-a843c031b335",
  "email": "user@doae.go.th",
  "name": "นายทดสอบ ระบบ",
  "full_name": "นายทดสอบ ระบบ",
  "preferred_username": "user",
  "role": "admin",
  "role_id": "uuid-from-roles-table",
  "organization_id": "uuid",
  "organization_name": "สำนักงานเกษตรอำเภอ...",
  "organization_type": "district",
  "province": "เชียงใหม่",
  "region_id": "uuid-org-unit-region",
  "province_id": "uuid-org-unit-province",
  "district_id": "uuid-org-unit-district",
  "center_id": null,
  "admin_level": "district"
}
Fieldใช้ทำอะไร
sub / idPrimary Key ใน App (sso_user_id) — ใช้ UUID นี้เป็นหลัก
emailอีเมลราชการ @doae.go.th
full_name / nameชื่อ-นามสกุล
roleชื่อ role: user | technical_support | admin | superadmin | developer
role_idUUID จากตาราง roles (primary key ของ role)
organization_idUUID หน่วยงานที่ผู้ใช้สังกัด
organization_nameชื่อหน่วยงาน
organization_typeประเภทหน่วยงาน เช่น region, province, district, center
provinceชื่อจังหวัด (string)
region_idUUID หน่วยงานระดับ สสก (region)
province_idUUID หน่วยงานระดับจังหวัด
district_idUUID หน่วยงานระดับอำเภอ
center_idUUID หน่วยงานระดับศูนย์
admin_levelขอบเขตสิทธิ์แอดมิน — ต้องมอบหมายก่อน: central | region | province | district | center

admin_level — ต้องมอบหมายก่อน

ไม่ได้มาจาก role อัตโนมัติ — ต้องมอบผ่านหน้า มอบหมายสิทธิ์ ใน SSO Portal

ค่าความหมายFilter ข้อมูลด้วยมอบต่อได้
centralส่วนกลางทั้งกรมregion → center
regionสสกregion_idprovince → center
provinceจังหวัดprovince_iddistrict, center
districtอำเภอdistrict_idcenter
centerศูนย์center_id
(ไม่มี field)ยังไม่ถูกมอบหมายorganization_id ของตัวเอง
  • Superadmin มอบให้ Admin ส่วนกลาง / สสก / จังหวัด / อำเภอ / ศูนย์ ได้ก่อน
  • Admin ที่ได้รับมอบ มอบต่อได้เฉพาะระดับต่ำกว่า และเฉพาะหน่วยงานใต้ขอบเขตตน
  • เมื่อมอบแล้ว ระบบตั้ง role=admin ให้ผู้รับ และส่ง admin_level ใน userinfo
  • superadmin เองได้ admin_level=central อัตโนมัติ

/api/auth/me ให้ field ชุดเดียวกัน เพิ่ม: personal_email, work_email, phone, organization_legacy_id

7. JWT Token

  • Algorithm: HS256
  • Access Token TTL: 24 ชั่วโมง (86400 วินาที)
  • Refresh Token TTL: 365 วัน
  • Header: Authorization: Bearer {token}

แนะนำ validate โดยเรียก /oauth/userinfo — ไม่ต้อง verify JWT signature เอง

8. Logout & Refresh

Refresh Token

Refresh Request
POST https://sso.doae.go.th/api/auth/refresh
Content-Type: application/json

{"refresh_token": "YOUR_REFRESH_TOKEN"}
Refresh Response
{
  "success": true,
  "tokens": {
    "access_token": "eyJ...",
    "refresh_token": "eyJ...",
    "token_type": "Bearer",
    "expires_in": 86400
  }
}

Logout

  1. เรียก POST /api/auth/logout ด้วย Bearer token (optional)
  2. ลบ session/cookies ใน App
  3. Redirect ไป https://sso.doae.go.th/login

9. Error Codes

Errorสาเหตุวิธีแก้
invalid_clientclient_id/secret ผิดตรวจ .env
invalid_redirect_uriURI ไม่ตรงที่ลงทะเบียนตรวจ trailing slash, http/https
invalid_grantcode หมดอายุ/ใช้แล้วlogin ใหม่
invalid_scopescope ไม่ได้รับอนุญาตใช้ openid profile email
access_denieduser ปฏิเสธแจ้ง user

10. Security Checklist

ต้องทำ (Must)

  • ✓ client_secret ฝั่ง server เท่านั้น
  • ✓ validate state ทุก callback
  • ✓ แลก code → token ฝั่ง backend
  • ✓ HTTPS ใน production
  • ✓ ใช้ sub (UUID) เป็น user ID
  • ✓ Session cookie: Secure, HttpOnly

ห้ามทำ (Must Not)

  • ✗ client_secret ใน frontend
  • ✗ token ใน URL query string
  • ✗ skip state validation
  • ✗ ใช้ /oauth2/* endpoints

11. ตัวอย่าง Laravel

Laravel
// routes/web.php
Route::get('/auth/login', [SsoController::class, 'login']);
Route::get('/auth/callback', [SsoController::class, 'callback']);
Route::post('/auth/logout', [SsoController::class, 'logout']);

// SsoController — login
$state = Str::random(40);
session(['oauth_state' => $state]);
return redirect('https://sso.doae.go.th/oauth/authorize?' . http_build_query([
    'client_id'     => config('sso.client_id'),
    'redirect_uri'  => config('sso.redirect_uri'),
    'response_type' => 'code',
    'scope'         => 'openid profile email',
    'state'         => $state,
]));

// SsoController — callback
if ($request->state !== session('oauth_state')) abort(403);
$tokens = Http::asForm()->post('https://sso.doae.go.th/oauth/token', [
    'grant_type'    => 'authorization_code',
    'code'          => $request->code,
    'redirect_uri'  => config('sso.redirect_uri'),
    'client_id'     => config('sso.client_id'),
    'client_secret' => config('sso.client_secret'),
])->json();
$user = Http::withToken($tokens['access_token'])
    ->get('https://sso.doae.go.th/oauth/userinfo')->json();
// ใช้ $user['sub'] เป็น sso_user_id ใน DB

12. ตัวอย่าง Node.js / Express

Node.js
app.get('/auth/login', (req, res) => {
  const state = crypto.randomBytes(32).toString('hex');
  req.session.oauthState = state;
  const params = new URLSearchParams({
    client_id: process.env.SSO_CLIENT_ID,
    redirect_uri: process.env.SSO_REDIRECT_URI,
    response_type: 'code',
    scope: 'openid profile email',
    state,
  });
  res.redirect('https://sso.doae.go.th/oauth/authorize?' + params);
});

app.get('/auth/callback', async (req, res) => {
  if (req.query.state !== req.session.oauthState) return res.status(403).send('Invalid state');
  const { data: tokens } = await axios.post('https://sso.doae.go.th/oauth/token',
    new URLSearchParams({
      grant_type: 'authorization_code',
      code: req.query.code,
      redirect_uri: process.env.SSO_REDIRECT_URI,
      client_id: process.env.SSO_CLIENT_ID,
      client_secret: process.env.SSO_CLIENT_SECRET,
    }).toString(),
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
  );
  const { data: user } = await axios.get('https://sso.doae.go.th/oauth/userinfo', {
    headers: { Authorization: 'Bearer ' + tokens.access_token },
  });
  req.session.user = user;
  res.redirect('/dashboard');
});

13. FAQ

Q: API อยู่ domain ไหน?

A: Production ใช้ https://sso.doae.go.th ทุก endpoint (Portal + API + OAuth รวมกัน)

Q: User ต้องมี account SSO ก่อนไหม?

A: ใช่ — สมัครที่ /register หรือ Admin สร้างให้

Q: Primary key ใช้อะไร?

A: sub (UUID) — ไม่ใช้ email อย่างเดียว

Q: province_id / district_id ได้จากไหน?

A: คำนวณจากต้นไม้ organization_units — มี region_id / center_id ด้วย

Q: admin_level คืออะไร?

A: ขอบเขตสิทธิ์ที่มอบหมาย: central | region | province | district | center — ต้องมอบก่อนถึงจะมีใน userinfo

Q: ใครกำหนด admin_level ได้?

A: Superadmin มอบก่อน → Admin มอบต่อให้ สสก/จังหวัด/อำเภอ/ศูนย์ ตามระดับ (หน้ามอบหมายสิทธิ์)

Q: ต้อง implement 2FA เองไหม?

A: ไม่ — SSO จัดการให้ตอน login

Q: เปลี่ยน callback path ได้ไหม?

A: ได้ — แจ้ง SSO Admin เพิ่ม redirect URI ก่อน deploy

Q: ทดสอบ local ยังไง?

A: ใช้ http://localhost:3000/auth/callback (ต้องลงทะเบียน URI นี้กับ Admin)

Q: client_secret หลุดทำยังไง?

A: แจ้ง SSO Admin regenerate ทันที

14. ติดต่อ

SSO Admin / ขอ OAuth Client: helpdesk@doae.go.th

SSO Portal: https://sso.doae.go.th

หน่วยงาน: ศูนย์เทคโนโลยีสารสนเทศและการสื่อสาร กรมส่งเสริมการเกษตร

กรมส่งเสริมการเกษตร · SSO Horizon · อัปเดต มิ.ย. 2026