Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion layers/identity/server/api/cli/oauth/token.post.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OauthTokenInputSchema, TokenResponseSchema } from 'skilld-protocol/wire'
import { z } from 'zod'
import { defineApiHandler } from '#shared/server/handler'
import { issueSession, presentTokenResponse, sha256Base64Url } from '../../../utils/cli-tokens'
import { getUserById } from '../../../utils/users'
Expand All @@ -14,8 +15,13 @@ interface AuthCodeRow {
used_at: number | null
}

// The CLI names each sign-in with the computer hostname. Only the account owner sees it.
const TokenInputSchema = OauthTokenInputSchema.extend({
device_label: z.string().trim().min(1).max(64).regex(/^\P{Cc}+$/u).optional(),
})

export default defineApiHandler({
schema: OauthTokenInputSchema,
schema: TokenInputSchema,
response: TokenResponseSchema,
handler: async ({ event, body }) => {
const row = await event.context.platform.db.prepare(
Expand Down Expand Up @@ -46,6 +52,7 @@ export default defineApiHandler({
kind: 'oauth',
scopes: row.scopes,
cliVersion: row.cli_version ?? undefined,
deviceLabel: body.device_label,
})

return presentTokenResponse(session, user.login)
Expand Down
29 changes: 29 additions & 0 deletions test/unit/cli-oauth-endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ describe('cli OAuth token endpoint', () => {
})
})

it('stores the device label the CLI sends', async () => {
const verifier = 'd'.repeat(64)
await insertCode({ code: 'code-label-1234567', challenge: await sha256Base64Url(verifier), port: 49155 })

setBody({
code: 'code-label-1234567',
code_verifier: verifier,
redirect_uri: 'http://127.0.0.1:49155/',
device_label: ' work-laptop ',
})
await tokenHandler(event)

expect(sqlite.prepare('SELECT device_label FROM cli_tokens').get()).toEqual({ device_label: 'work-laptop' })
})

it('rejects a device label with control characters', async () => {
const verifier = 'e'.repeat(64)
await insertCode({ code: 'code-ctrl-12345678', challenge: await sha256Base64Url(verifier), port: 49156 })

setBody({
code: 'code-ctrl-12345678',
code_verifier: verifier,
redirect_uri: 'http://127.0.0.1:49156/',
device_label: 'work\u001B[31mlaptop',
})

await expect(tokenHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})

async function insertCode(input: { code: string, challenge: string, port: number }) {
sqlite.prepare(`
INSERT INTO cli_auth_codes (
Expand Down
Loading