Skip to content

Commit 34740d3

Browse files
committed
feat(client): add OAuth confidential client endpoints
- Add /api/oauth/client-metadata.json for OAuth client discovery - Add /api/oauth/jwks.json for public key distribution - Update .env.example with all required variables - Add vercel.json for deployment
1 parent 1f1d2bc commit 34740d3

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

client/.env.example

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1-
# Hypergoat API URL
2-
# Change this to your Hypergoat server's address in production
1+
# =============================================================================
2+
# Hypergoat Client Configuration
3+
# =============================================================================
4+
5+
# Backend API URL (the Go server)
6+
# For local development: http://localhost:8080
7+
# For production: https://your-railway-app.up.railway.app
38
NEXT_PUBLIC_API_URL=http://localhost:8080
9+
10+
# Same as above, used for server-side API calls
11+
HYPERGOAT_URL=http://127.0.0.1:8080
12+
13+
# =============================================================================
14+
# OAuth Configuration
15+
# =============================================================================
16+
17+
# Public URL of this client (required for production OAuth)
18+
# This is the Vercel URL where the client is deployed
19+
# Leave empty for local development (uses http://127.0.0.1:3000)
20+
PUBLIC_URL=
21+
22+
# Secret for encrypting session cookies (must be at least 32 characters)
23+
# Generate with: openssl rand -base64 32
24+
COOKIE_SECRET=development-secret-at-least-32-chars!!
25+
26+
# Private JWK for confidential OAuth client (optional)
27+
# For production, you can generate a key pair with:
28+
# npx @atproto/jwk-cli generate --alg ES256
29+
# Then paste the private key JSON here (minified, single line)
30+
# Leave empty to use public client mode (simpler, works for most cases)
31+
ATPROTO_JWK_PRIVATE=
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from "next/server";
2+
import { env } from "@/lib/env";
3+
4+
export const dynamic = "force-dynamic";
5+
6+
export async function GET() {
7+
const publicUrl = env.PUBLIC_URL;
8+
9+
if (!publicUrl) {
10+
return NextResponse.json(
11+
{ error: "PUBLIC_URL not configured" },
12+
{ status: 500 }
13+
);
14+
}
15+
16+
const metadata = {
17+
client_id: `${publicUrl}/api/oauth/client-metadata.json`,
18+
client_name: "Hypergoat Admin",
19+
client_uri: publicUrl,
20+
logo_uri: `${publicUrl}/logo.png`,
21+
tos_uri: `${publicUrl}/terms`,
22+
policy_uri: `${publicUrl}/privacy`,
23+
redirect_uris: [`${publicUrl}/api/oauth/callback`],
24+
grant_types: ["authorization_code", "refresh_token"],
25+
response_types: ["code"],
26+
scope: "atproto transition:generic",
27+
token_endpoint_auth_method: "private_key_jwt",
28+
token_endpoint_auth_signing_alg: "ES256",
29+
jwks_uri: `${publicUrl}/api/oauth/jwks.json`,
30+
application_type: "web",
31+
dpop_bound_access_tokens: true,
32+
};
33+
34+
return NextResponse.json(metadata, {
35+
headers: {
36+
"Content-Type": "application/json",
37+
"Cache-Control": "public, max-age=3600",
38+
},
39+
});
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextResponse } from "next/server";
2+
import { getJwks } from "@/lib/auth/client";
3+
4+
export const dynamic = "force-dynamic";
5+
6+
export async function GET() {
7+
try {
8+
const jwks = await getJwks();
9+
10+
if (!jwks) {
11+
// Return empty keyset if no private key configured (public client mode)
12+
return NextResponse.json(
13+
{ keys: [] },
14+
{
15+
headers: {
16+
"Content-Type": "application/json",
17+
"Cache-Control": "public, max-age=3600",
18+
},
19+
}
20+
);
21+
}
22+
23+
return NextResponse.json(jwks, {
24+
headers: {
25+
"Content-Type": "application/json",
26+
"Cache-Control": "public, max-age=3600",
27+
},
28+
});
29+
} catch (error) {
30+
console.error("Failed to get JWKS:", error);
31+
return NextResponse.json(
32+
{ error: "Failed to get JWKS" },
33+
{ status: 500 }
34+
);
35+
}
36+
}

client/vercel.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "https://openapi.vercel.sh/vercel.json",
3+
"framework": "nextjs"
4+
}

0 commit comments

Comments
 (0)