fix(auth): respect server username in clients (#25596)

This commit is contained in:
Shoubhit Dash
2026-05-03 19:28:31 +05:30
committed by GitHub
parent 0f1ed19440
commit 22d0563671
11 changed files with 148 additions and 88 deletions
@@ -1,5 +1,5 @@
import { ConfigService } from "@/effect/config-service"
import { Config, Context, Effect, Encoding, Layer, Option, Redacted } from "effect"
import { ServerAuth } from "@/server/auth"
import { Effect, Encoding, Layer, Redacted } from "effect"
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiError, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
@@ -18,41 +18,18 @@ export class Authorization extends HttpApiMiddleware.Service<Authorization>()(
},
) {}
export class ServerAuthConfig extends ConfigService.Service<ServerAuthConfig>()(
"@opencode/ExperimentalHttpApiServerAuthConfig",
{
password: Config.string("OPENCODE_SERVER_PASSWORD").pipe(Config.option),
username: Config.string("OPENCODE_SERVER_USERNAME").pipe(Config.withDefault("opencode")),
},
) {}
function validateCredential<A, E, R>(
effect: Effect.Effect<A, E, R>,
credential: { readonly username: string; readonly password: Redacted.Redacted },
config: Context.Service.Shape<typeof ServerAuthConfig>,
credential: ServerAuth.DecodedCredentials,
config: ServerAuth.Info,
) {
return Effect.gen(function* () {
if (!isAuthRequired(config)) return yield* effect
if (!isCredentialAuthorized(credential, config)) return yield* new HttpApiError.Unauthorized({})
if (!ServerAuth.required(config)) return yield* effect
if (!ServerAuth.authorized(credential, config)) return yield* new HttpApiError.Unauthorized({})
return yield* effect
})
}
function isAuthRequired(config: Context.Service.Shape<typeof ServerAuthConfig>) {
return Option.isSome(config.password) && config.password.value !== ""
}
function isCredentialAuthorized(
credential: { readonly username: string; readonly password: Redacted.Redacted },
config: Context.Service.Shape<typeof ServerAuthConfig>,
) {
return (
Option.isSome(config.password) &&
credential.username === config.username &&
Redacted.value(credential.password) === config.password.value
)
}
function decodeCredential(input: string) {
const emptyCredential = {
username: "",
@@ -78,11 +55,11 @@ function decodeCredential(input: string) {
function validateRawCredential<A, E, R>(
effect: Effect.Effect<A, E, R>,
credential: { readonly username: string; readonly password: Redacted.Redacted },
config: Context.Service.Shape<typeof ServerAuthConfig>,
credential: ServerAuth.DecodedCredentials,
config: ServerAuth.Info,
) {
if (!isAuthRequired(config)) return effect
if (!isCredentialAuthorized(credential, config))
if (!ServerAuth.required(config)) return effect
if (!ServerAuth.authorized(credential, config))
return Effect.succeed(
HttpServerResponse.empty({
status: UNAUTHORIZED,
@@ -94,8 +71,8 @@ function validateRawCredential<A, E, R>(
export const authorizationRouterMiddleware = HttpRouter.middleware()(
Effect.gen(function* () {
const config = yield* ServerAuthConfig
if (!isAuthRequired(config)) return (effect) => effect
const config = yield* ServerAuth.Config
if (!ServerAuth.required(config)) return (effect) => effect
return (effect) =>
Effect.gen(function* () {
@@ -122,7 +99,7 @@ export const authorizationRouterMiddleware = HttpRouter.middleware()(
export const authorizationLayer = Layer.effect(
Authorization,
Effect.gen(function* () {
const config = yield* ServerAuthConfig
const config = yield* ServerAuth.Config
return Authorization.of({
basic: (effect, { credential }) => validateCredential(effect, credential, config),
authToken: (effect, { credential }) =>