feat: fix github auth flow

pull/715/head
Travis Fischer 2025-06-16 08:54:23 +07:00
rodzic f9254b5965
commit ebd3e0928a
10 zmienionych plików z 167 dodań i 14 usunięć

Wyświetl plik

@ -0,0 +1,28 @@
import type { DefaultHonoEnv } from '@agentic/platform-hono'
import type { OpenAPIHono } from '@hono/zod-openapi'
import { assert } from '@agentic/platform-core'
import { authStorage } from './utils'
export function registerV1AuthGitHubOAuthCallback(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.get('auth/github/callback', async (c) => {
const logger = c.get('logger')
const query = c.req.query()
assert(query.state, 400, 'State is required')
const entry = await authStorage.get(['github', query.state, 'redirectUri'])
assert(entry, 400, 'Redirect URI not found')
const redirectUri = entry.redirectUri
assert(entry.redirectUri, 400, 'Redirect URI not found')
const url = new URL(
`${redirectUri}?${new URLSearchParams(query).toString()}`
).toString()
logger.info('GitHub auth callback', query, '=>', url)
return c.redirect(url)
})
}

Wyświetl plik

@ -21,7 +21,7 @@ const route = createRoute({
tags: ['auth'],
operationId: 'exchangeOAuthCodeWithGitHub',
method: 'post',
path: 'auth/github',
path: 'auth/github/exchange',
security: openapiAuthenticatedSecuritySchemas,
request: {
body: {
@ -51,7 +51,7 @@ const route = createRoute({
}
})
export function registerV1AuthExchangeOAuthCodeWithGitHub(
export function registerV1AuthGitHubOAuthExchange(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.openapi(route, async (c) => {

Wyświetl plik

@ -0,0 +1,71 @@
import type { DefaultHonoEnv } from '@agentic/platform-hono'
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
import { env } from '@/lib/env'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponse404,
openapiErrorResponses
} from '@/lib/openapi-utils'
import { authStorage } from './utils'
const route = createRoute({
description: 'Starts a GitHub OAuth flow.',
tags: ['auth'],
operationId: 'initGitHubOAuthFlow',
method: 'get',
path: 'auth/github/init',
security: openapiAuthenticatedSecuritySchemas,
request: {
query: z
.object({
redirect_uri: z.string(),
client_id: z.string().optional(),
scope: z.string().optional()
})
.passthrough()
},
responses: {
302: {
description: 'Redirected to GitHub'
},
...openapiErrorResponses,
...openapiErrorResponse404
}
})
export function registerV1AuthGitHubOAuthInitFlow(
app: OpenAPIHono<DefaultHonoEnv>
) {
return app.openapi(route, async (c) => {
const logger = c.get('logger')
const {
client_id: clientId = env.GITHUB_CLIENT_ID,
scope = 'user:email',
redirect_uri: redirectUri
} = c.req.query()
const state = crypto.randomUUID()
// TODO: unique identifier
await authStorage.set(['github', state, 'redirectUri'], { redirectUri })
const publicRedirectUri = `${env.apiBaseUrl}/v1/auth/github/callback`
const url = new URL('https://github.com/login/oauth/authorize')
url.searchParams.append('client_id', clientId)
url.searchParams.append('scope', scope)
url.searchParams.append('state', state)
url.searchParams.append('redirect_uri', publicRedirectUri)
logger.info('Redirecting to GitHub', {
url: url.toString(),
clientId,
scope,
publicRedirectUri
})
return c.redirect(url.toString())
})
}

Wyświetl plik

@ -0,0 +1,3 @@
import { DrizzleAuthStorage } from '@/lib/drizzle-auth-storage'
export const authStorage = DrizzleAuthStorage()

Wyświetl plik

@ -6,7 +6,9 @@ import type { AuthenticatedHonoEnv } from '@/lib/types'
import * as middleware from '@/lib/middleware'
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
import { registerV1AuthExchangeOAuthCodeWithGitHub } from './auth/github'
import { registerV1AuthGitHubOAuthCallback } from './auth/github-callback'
import { registerV1AuthGitHubOAuthExchange } from './auth/github-exchange'
import { registerV1AuthGitHubOAuthInitFlow } from './auth/github-init'
import { registerV1AuthSignInWithPassword } from './auth/sign-in-with-password'
import { registerV1AuthSignUpWithPassword } from './auth/sign-up-with-password'
import { registerV1AdminConsumersActivateConsumer } from './consumers/admin-activate-consumer'
@ -78,7 +80,9 @@ registerHealthCheck(publicRouter)
// Auth
registerV1AuthSignInWithPassword(publicRouter)
registerV1AuthSignUpWithPassword(publicRouter)
registerV1AuthExchangeOAuthCodeWithGitHub(publicRouter)
registerV1AuthGitHubOAuthExchange(publicRouter)
registerV1AuthGitHubOAuthInitFlow(publicRouter)
registerV1AuthGitHubOAuthCallback(publicRouter)
// Users
registerV1UsersGetUser(privateRouter)

Wyświetl plik

@ -42,11 +42,15 @@ export function parseEnv(inputEnv: Record<string, unknown>) {
)
const isStripeLive = env.STRIPE_SECRET_KEY.startsWith('sk_live_')
const apiBaseUrl = baseEnv.isProd
? 'https://api.agentic.so'
: 'http://localhost:3001'
return {
...baseEnv,
...env,
isStripeLive
isStripeLive,
apiBaseUrl
}
}

Wyświetl plik

@ -10,7 +10,7 @@ import { env } from '@/lib/env'
import * as middleware from '@/lib/middleware'
import { initExitHooks } from './lib/exit-hooks'
import { registerOAuthRedirect } from './oauth-redirect'
// import { registerOAuthRedirect } from './oauth-redirect'
export const app = new OpenAPIHono<DefaultHonoEnv>()
@ -32,7 +32,7 @@ app.use(middleware.accessLogger)
app.use(middleware.responseTime)
// TODO: top-level auth routes
registerOAuthRedirect(app)
// registerOAuthRedirect(app)
// Mount all v1 API routes
app.route('/v1', apiV1)

Wyświetl plik

@ -212,12 +212,10 @@ export class AgenticApiClient {
scope?: string
clientId?: string
}): Promise<string> {
const publicRedirectUri = `${this.apiBaseUrl}/oauth/callback?${new URLSearchParams({ uri: redirectUri }).toString()}`
const url = new URL('https://github.com/login/oauth/authorize')
const url = new URL(`${this.apiBaseUrl}/v1/auth/github/init`)
url.searchParams.append('client_id', clientId)
url.searchParams.append('scope', scope)
url.searchParams.append('redirect_uri', publicRedirectUri)
url.searchParams.append('redirect_uri', redirectUri)
return url.toString()
}
@ -226,7 +224,9 @@ export class AgenticApiClient {
async exchangeOAuthCodeWithGitHub(
json: OperationBody<'exchangeOAuthCodeWithGitHub'>
): Promise<AuthSession> {
this._authSession = await this.ky.post('v1/auth/github', { json }).json()
this._authSession = await this.ky
.post('v1/auth/github/exchange', { json })
.json()
this.onUpdateAuth?.(this._authSession)
return this._authSession

Wyświetl plik

@ -55,7 +55,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/auth/github": {
"/v1/auth/github/exchange": {
parameters: {
query?: never;
header?: never;
@ -64,7 +64,7 @@ export interface paths {
};
get?: never;
put?: never;
/** @description Exchanges GitHub code for auth session. */
/** @description Exchanges a GitHub OAuth code for an Agentic auth session. */
post: operations["exchangeOAuthCodeWithGitHub"];
delete?: never;
options?: never;
@ -72,6 +72,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/auth/github/init": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Starts a GitHub OAuth flow. */
get: operations["initGitHubOAuthFlow"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/users/{userId}": {
parameters: {
query?: never;
@ -1021,6 +1038,32 @@ export interface operations {
404: components["responses"]["404"];
};
};
initGitHubOAuthFlow: {
parameters: {
query: {
redirect_uri: string;
client_id?: string;
scope?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Redirected to GitHub */
302: {
headers: {
[name: string]: unknown;
};
content?: never;
};
400: components["responses"]["400"];
401: components["responses"]["401"];
403: components["responses"]["403"];
404: components["responses"]["404"];
};
};
getUser: {
parameters: {
query?: never;