|
| 1 | +--- |
| 2 | +description: "Kenapa Deserve membawa Basic Auth tapi tidak Bearer, dan cara menyusun auth token untuk skema apa pun." |
| 3 | +--- |
| 4 | + |
| 5 | +# Bearer Auth |
| 6 | + |
| 7 | +Deserve membawa [Basic Auth](/id/middleware/basic-auth) tapi tidak ada middleware Bearer, dan pemisahan antara keduanya adalah intinya. |
| 8 | + |
| 9 | +## Kenapa Tidak Dibawa |
| 10 | + |
| 11 | +Bearer hanyalah sebuah amplop. Header [`Authorization: Bearer <token>`](https://datatracker.ietf.org/doc/html/rfc6750) membawa token, dan apa yang dianggap token valid berubah seiring ekosistem. Satu layanan memverifikasi tanda tangan [JWT](https://datatracker.ietf.org/doc/html/rfc7519), lain mengambil kunci publik berputar dari endpoint [JWKS](https://datatracker.ietf.org/doc/html/rfc7517), lain memanggil API introspeksi untuk token buram, dan algoritma penanda bisa RS256, ES256, atau HS256. |
| 12 | + |
| 13 | +Menanam salah satu pilihan itu akan mengunci setiap proyek ke satu skema. Ketika spesifikasi bergerak atau sebuah tim memutar kunci dengan cara berbeda, jawaban bawaan itu berubah jadi kandang ketimbang bantuan. Jadi keputusannya adalah membiarkan verifikasi terbuka dan membiarkan developer membawa skema yang dibutuhkan kasusnya. |
| 14 | + |
| 15 | +## Kenapa Basic Auth Dibawa Tapi Bearer Tidak |
| 16 | + |
| 17 | +[Basic Auth](/id/middleware/basic-auth) adalah satu skema tetap. Header membawa username dan password base64, pengecekannya adalah perbandingan waktu konstan terhadap sebuah daftar, dan tak ada yang perlu dipilih. Stabilitas itulah yang membuatnya cocok di dalam framework. |
| 18 | + |
| 19 | +Bearer sebaliknya. Format token, tanda tangan, dan sumber kepercayaan semuanya berbeda, jadi tak ada satu pengecekan untuk dibawa. Kedua skema membaca header `Authorization` yang sama, tapi hanya satu yang punya jawaban benar tunggal. |
| 20 | + |
| 21 | +## Bagian yang Sudah Ada |
| 22 | + |
| 23 | +Penjaga token adalah komposisi kecil di atas bagian yang sudah ada: |
| 24 | + |
| 25 | +- **Baca header** - [`ctx.header('authorization')`](/id/core-concepts/context-object#akses-data-request) mengembalikan nilai `Authorization` mentah. |
| 26 | +- **Jalan lebih awal** - [middleware global](/id/middleware/global) berjalan sebelum route handler dan bisa menghentikan request dengan mengembalikan `Response`. |
| 27 | +- **Tolak bersih** - [`ctx.handleError(401, ...)`](/id/core-concepts/context-object#penanganan-error) mengarah lewat [`router.catch()`](/id/error-handling/object-details) saat satu diatur. |
| 28 | +- **Bawa hasilnya** - [`ctx.state`](/id/core-concepts/context-object#berbagi-state) menyerahkan identitas terdekode ke handler di hilir. |
| 29 | + |
| 30 | +## Sebuah Penjaga Bearer |
| 31 | + |
| 32 | +Middleware ini menarik token dari header, memverifikasinya, dan menyimpan hasilnya untuk handler berikutnya. Placeholder `verifyToken` mewakili skema pilihan, sebuah pengecekan JWT, lookup JWKS, atau panggilan introspeksi. |
| 33 | + |
| 34 | +```typescript twoslash |
| 35 | +import type { Context } from '@neabyte/deserve' |
| 36 | +import { Router } from '@neabyte/deserve' |
| 37 | + |
| 38 | +const router = new Router() |
| 39 | +declare function verifyToken(token: string): Promise<{ userId: string } | null> |
| 40 | +// ---cut--- |
| 41 | +router.use(async (ctx, next) => { |
| 42 | + const header = ctx.header('authorization') |
| 43 | + const spaceIndex = header ? header.indexOf(' ') : -1 |
| 44 | + const scheme = spaceIndex > 0 ? header!.slice(0, spaceIndex) : '' |
| 45 | + |
| 46 | + // Tolak apa pun yang bukan Bearer |
| 47 | + if (scheme.toLowerCase() !== 'bearer') { |
| 48 | + return await ctx.handleError(401, new Error('Missing Bearer token')) |
| 49 | + } |
| 50 | + |
| 51 | + // Verifikasi dengan skema pilihan |
| 52 | + const token = header!.slice(spaceIndex + 1).trim() |
| 53 | + const claims = await verifyToken(token) |
| 54 | + if (!claims) { |
| 55 | + return await ctx.handleError(401, new Error('Invalid token')) |
| 56 | + } |
| 57 | + |
| 58 | + // Serahkan identitas ke handler |
| 59 | + ctx.state.userId = claims.userId |
| 60 | + return await next() |
| 61 | +}) |
| 62 | + |
| 63 | +await router.serve(8000) |
| 64 | +``` |
| 65 | + |
| 66 | +Handler lalu membaca identitas langsung dari state, tanpa parsing token sendiri. |
| 67 | + |
| 68 | +```typescript twoslash |
| 69 | +import type { Context } from '@neabyte/deserve' |
| 70 | +// ---cut--- |
| 71 | +export function GET(ctx: Context): Response { |
| 72 | + // Baca apa yang disimpan penjaga |
| 73 | + const userId = ctx.state.userId |
| 74 | + return ctx.send.json({ userId }) |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +## Mengarahkan Kegagalan Lewat Satu Handler |
| 79 | + |
| 80 | +Penjaga di atas mengembalikan `401` dari dalam middleware. Untuk mengirim setiap kegagalan auth lewat satu tempat, bungkus middleware dengan [`WrapMware`](/id/middleware/global#membungkus-middleware-dengan-penanganan-error) dan lempar saat ditolak, lalu bentuk balasannya dengan [`router.catch()`](/id/error-handling/object-details). |
| 81 | + |
| 82 | +```typescript twoslash |
| 83 | +import type { Context } from '@neabyte/deserve' |
| 84 | +import { Router, WrapMware } from '@neabyte/deserve' |
| 85 | + |
| 86 | +const router = new Router() |
| 87 | +declare function verifyToken(token: string): Promise<{ userId: string } | null> |
| 88 | +// ---cut--- |
| 89 | +// Lemparan sampai router.catch saat dibungkus |
| 90 | +const bearer = WrapMware('Bearer', async (ctx: Context, next) => { |
| 91 | + const header = ctx.header('authorization') |
| 92 | + if (!header?.toLowerCase().startsWith('bearer ')) { |
| 93 | + throw new Error('Missing Bearer token') |
| 94 | + } |
| 95 | + const claims = await verifyToken(header.slice(7).trim()) |
| 96 | + if (!claims) { |
| 97 | + throw new Error('Invalid token') |
| 98 | + } |
| 99 | + ctx.state.userId = claims.userId |
| 100 | + return await next() |
| 101 | +}) |
| 102 | + |
| 103 | +// Terapkan penjaga dan bentuk error |
| 104 | +router.use(bearer) |
| 105 | +router.catch((ctx, err) => ctx.send.json({ error: err.error?.message }, { status: 401 })) |
| 106 | +``` |
| 107 | + |
| 108 | +Ini pola pembungkusan yang sama dipakai [Basic Auth](/id/middleware/basic-auth) secara internal, kini membawa pengecekan token alih-alih perbandingan kredensial. |
| 109 | + |
| 110 | +## Menjaga Hanya Sebagian Rute |
| 111 | + |
| 112 | +Penjaga token sering cocok di prefix API sementara halaman publik tetap terbuka. Middleware per-path membatasi pengecekan ke satu prefix, bentuk yang sama ditunjukkan di [middleware global](/id/middleware/global#middleware-per-path). |
| 113 | + |
| 114 | +```typescript twoslash |
| 115 | +import type { Context } from '@neabyte/deserve' |
| 116 | +import { Router } from '@neabyte/deserve' |
| 117 | + |
| 118 | +const router = new Router() |
| 119 | +declare function verifyToken(token: string): Promise<{ userId: string } | null> |
| 120 | +// ---cut--- |
| 121 | +// Jaga hanya rute /api |
| 122 | +router.use('/api', async (ctx, next) => { |
| 123 | + const header = ctx.header('authorization') |
| 124 | + const claims = header?.toLowerCase().startsWith('bearer ') |
| 125 | + ? await verifyToken(header.slice(7).trim()) |
| 126 | + : null |
| 127 | + if (!claims) { |
| 128 | + return await ctx.handleError(401, new Error('Invalid token')) |
| 129 | + } |
| 130 | + ctx.state.userId = claims.userId |
| 131 | + return await next() |
| 132 | +}) |
| 133 | +``` |
| 134 | + |
| 135 | +Untuk session sisi server yang ditandatangani framework alih-alih token, lihat [middleware session](/id/middleware/session). |
0 commit comments