mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-03 09:51:39 +00:00
@@ -0,0 +1,30 @@
|
||||
import { GET } from '../.well-known/openid-configuration/route';
|
||||
|
||||
describe('OIDC Discovery Endpoint', () => {
|
||||
it('returns a valid OpenID Connect configuration', async () => {
|
||||
const response = await GET(new Request('https://example.com/.well-known/openid-configuration'));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const config = await response.json();
|
||||
|
||||
// Basic structure validation
|
||||
expect(config).toHaveProperty('issuer');
|
||||
expect(config).toHaveProperty('authorization_endpoint');
|
||||
expect(config).toHaveProperty('token_endpoint');
|
||||
expect(config).toHaveProperty('userinfo_endpoint');
|
||||
expect(config).toHaveProperty('jwks_uri');
|
||||
|
||||
// Validate specific SIOP requirements
|
||||
expect(config.subject_syntax_types_supported).toContain('did');
|
||||
expect(config.id_token_types_supported).toContain('subject-signed_id_token');
|
||||
});
|
||||
|
||||
it('responds with correct CORS headers', async () => {
|
||||
const response = await GET(new Request('https://example.com/.well-known/openid-configuration'));
|
||||
|
||||
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
|
||||
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET');
|
||||
expect(response.headers.get('Content-Type')).toBe('application/json');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { POST } from '../token/route';
|
||||
|
||||
describe('OIDC Token Endpoint', () => {
|
||||
it('handles authorization code token exchange', async () => {
|
||||
const mockRequest = new NextRequest(new URL('https://example.com/token'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
grant_type: 'authorization_code',
|
||||
code: 'valid_code',
|
||||
client_id: 'test_client',
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
code_verifier: 'test_verifier',
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(mockRequest);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const tokens = await response.json();
|
||||
expect(tokens).toHaveProperty('access_token');
|
||||
expect(tokens).toHaveProperty('token_type', 'Bearer');
|
||||
expect(tokens).toHaveProperty('expires_in');
|
||||
});
|
||||
|
||||
it('handles refresh token grant', async () => {
|
||||
const mockRequest = new NextRequest(new URL('https://example.com/token'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: 'valid_refresh_token',
|
||||
client_id: 'test_client',
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(mockRequest);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const tokens = await response.json();
|
||||
expect(tokens).toHaveProperty('access_token');
|
||||
expect(tokens).toHaveProperty('token_type', 'Bearer');
|
||||
});
|
||||
|
||||
it('rejects invalid grant types', async () => {
|
||||
const mockRequest = new NextRequest(new URL('https://example.com/token'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
grant_type: 'invalid_grant',
|
||||
client_id: 'test_client',
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(mockRequest);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
const error = await response.json();
|
||||
expect(error).toHaveProperty('error', 'unsupported_grant_type');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const BRIDGE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
|
||||
interface OIDCAuthorizationParams {
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
scope: string;
|
||||
state?: string;
|
||||
nonce?: string;
|
||||
code_challenge?: string;
|
||||
code_challenge_method?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/oidc/authorize
|
||||
*
|
||||
* OIDC Authorization endpoint that initiates the authorization code flow.
|
||||
* This endpoint proxies the request to the Go bridge handler at /oidc/authorize.
|
||||
*/
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Extract and validate required OIDC parameters
|
||||
const authParams: OIDCAuthorizationParams = {
|
||||
response_type: searchParams.get('response_type') || '',
|
||||
client_id: searchParams.get('client_id') || '',
|
||||
redirect_uri: searchParams.get('redirect_uri') || '',
|
||||
scope: searchParams.get('scope') || '',
|
||||
state: searchParams.get('state') || undefined,
|
||||
nonce: searchParams.get('nonce') || undefined,
|
||||
code_challenge: searchParams.get('code_challenge') || undefined,
|
||||
code_challenge_method: searchParams.get('code_challenge_method') || undefined,
|
||||
};
|
||||
|
||||
// Validate required parameters
|
||||
if (
|
||||
!authParams.response_type ||
|
||||
!authParams.client_id ||
|
||||
!authParams.redirect_uri ||
|
||||
!authParams.scope
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description:
|
||||
'Missing required parameters: response_type, client_id, redirect_uri, or scope',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build query string for the bridge handler
|
||||
const queryParams = new URLSearchParams();
|
||||
Object.entries(authParams).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') {
|
||||
queryParams.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward the request to the Go bridge handler
|
||||
const response = await fetch(`${BRIDGE_URL}/oidc/authorize?${queryParams.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
// Forward authentication and session headers
|
||||
Authorization: request.headers.get('Authorization') || '',
|
||||
Cookie: request.headers.get('Cookie') || '',
|
||||
'User-Agent': request.headers.get('User-Agent') || '',
|
||||
Origin: request.headers.get('Origin') || '',
|
||||
Referer: request.headers.get('Referer') || '',
|
||||
},
|
||||
});
|
||||
|
||||
// Handle different response types from the bridge
|
||||
if (response.status === 302) {
|
||||
// Bridge handler returned a redirect - follow it
|
||||
const location = response.headers.get('Location');
|
||||
if (location) {
|
||||
return NextResponse.redirect(location);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({
|
||||
error: 'authorization_error',
|
||||
error_description: 'Authorization request failed',
|
||||
}));
|
||||
|
||||
console.error('OIDC Authorization error:', response.status, errorData);
|
||||
return NextResponse.json(errorData, { status: response.status });
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
// Return the authorization response with appropriate headers
|
||||
return NextResponse.json(responseData, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
'Access-Control-Allow-Origin': request.headers.get('Origin') || '*',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('OIDC Authorization proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Internal server error during authorization',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/oidc/authorize
|
||||
*
|
||||
* Handle authorization requests sent via POST (form-encoded).
|
||||
*/
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
|
||||
// Extract parameters from form data
|
||||
const authParams: OIDCAuthorizationParams = {
|
||||
response_type: formData.get('response_type')?.toString() || '',
|
||||
client_id: formData.get('client_id')?.toString() || '',
|
||||
redirect_uri: formData.get('redirect_uri')?.toString() || '',
|
||||
scope: formData.get('scope')?.toString() || '',
|
||||
state: formData.get('state')?.toString() || undefined,
|
||||
nonce: formData.get('nonce')?.toString() || undefined,
|
||||
code_challenge: formData.get('code_challenge')?.toString() || undefined,
|
||||
code_challenge_method: formData.get('code_challenge_method')?.toString() || undefined,
|
||||
};
|
||||
|
||||
// Validate required parameters
|
||||
if (
|
||||
!authParams.response_type ||
|
||||
!authParams.client_id ||
|
||||
!authParams.redirect_uri ||
|
||||
!authParams.scope
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description:
|
||||
'Missing required parameters: response_type, client_id, redirect_uri, or scope',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Forward as form data to the bridge handler
|
||||
const bridgeFormData = new FormData();
|
||||
Object.entries(authParams).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') {
|
||||
bridgeFormData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(`${BRIDGE_URL}/oidc/authorize`, {
|
||||
method: 'POST',
|
||||
body: bridgeFormData,
|
||||
headers: {
|
||||
// Forward authentication and session headers
|
||||
Authorization: request.headers.get('Authorization') || '',
|
||||
Cookie: request.headers.get('Cookie') || '',
|
||||
'User-Agent': request.headers.get('User-Agent') || '',
|
||||
Origin: request.headers.get('Origin') || '',
|
||||
Referer: request.headers.get('Referer') || '',
|
||||
},
|
||||
});
|
||||
|
||||
// Handle redirect response
|
||||
if (response.status === 302) {
|
||||
const location = response.headers.get('Location');
|
||||
if (location) {
|
||||
return NextResponse.redirect(location);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({
|
||||
error: 'authorization_error',
|
||||
error_description: 'Authorization request failed',
|
||||
}));
|
||||
|
||||
return NextResponse.json(errorData, { status: response.status });
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return NextResponse.json(responseData);
|
||||
} catch (error) {
|
||||
console.error('OIDC Authorization POST proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Internal server error during authorization',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OPTIONS /api/oidc/authorize
|
||||
*
|
||||
* Handle preflight CORS requests for the authorization endpoint.
|
||||
*/
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Cookie',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const BRIDGE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
|
||||
interface JWK {
|
||||
kty: string;
|
||||
use?: string;
|
||||
kid: string;
|
||||
alg?: string;
|
||||
n?: string; // RSA modulus
|
||||
e?: string; // RSA exponent
|
||||
x?: string; // EC x coordinate
|
||||
y?: string; // EC y coordinate
|
||||
crv?: string; // EC curve
|
||||
}
|
||||
|
||||
interface JWKSet {
|
||||
keys: JWK[];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/oidc/jwks
|
||||
*
|
||||
* OIDC JSON Web Key Set (JWKS) endpoint that returns public keys used for token verification.
|
||||
* This endpoint proxies the request to the Go bridge handler at /oidc/jwks.
|
||||
* The returned keys are used by relying parties to verify JWT tokens issued by this OIDC provider.
|
||||
*/
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
// Forward the request to the Go bridge handler
|
||||
const response = await fetch(`${BRIDGE_URL}/oidc/jwks`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': request.headers.get('User-Agent') || '',
|
||||
Origin: request.headers.get('Origin') || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('OIDC JWKS error:', response.status, response.statusText);
|
||||
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
errorData = {
|
||||
error: 'server_error',
|
||||
error_description: 'Failed to retrieve JSON Web Key Set',
|
||||
};
|
||||
}
|
||||
|
||||
return NextResponse.json(errorData, { status: response.status });
|
||||
}
|
||||
|
||||
const jwks: JWKSet = await response.json();
|
||||
|
||||
// Validate JWKS structure
|
||||
if (!jwks || !Array.isArray(jwks.keys)) {
|
||||
console.error('Invalid JWKS response structure:', jwks);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Invalid JWKS response from server',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate each key in the set
|
||||
const validKeys = jwks.keys.filter((key: JWK) => {
|
||||
// Basic validation for required JWK fields
|
||||
if (!key.kty || !key.kid) {
|
||||
console.warn('Invalid JWK missing required fields:', key);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate key type specific fields
|
||||
if (key.kty === 'RSA' && (!key.n || !key.e)) {
|
||||
console.warn('Invalid RSA JWK missing n or e:', key);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key.kty === 'EC' && (!key.x || !key.y || !key.crv)) {
|
||||
console.warn('Invalid EC JWK missing x, y, or crv:', key);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const validatedJWKS: JWKSet = {
|
||||
keys: validKeys,
|
||||
};
|
||||
|
||||
// Return the JWKS with appropriate headers
|
||||
return NextResponse.json(validatedJWKS, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// JWKS can be cached longer since keys don't change frequently
|
||||
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=43200',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
// Security headers for key endpoint
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('OIDC JWKS proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Internal server error during JWKS retrieval',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OPTIONS /api/oidc/jwks
|
||||
*
|
||||
* Handle preflight CORS requests for the JWKS endpoint.
|
||||
*/
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/oidc/jwks
|
||||
*
|
||||
* Return method not allowed for POST requests to JWKS endpoint.
|
||||
*/
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'method_not_allowed',
|
||||
error_description: 'POST method not allowed for JWKS endpoint. Use GET.',
|
||||
},
|
||||
{
|
||||
status: 405,
|
||||
headers: {
|
||||
Allow: 'GET, OPTIONS',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
export const revalidate = 3600; // Revalidate every hour
|
||||
|
||||
const BRIDGE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
|
||||
interface OIDCTokenRequest {
|
||||
grant_type: string;
|
||||
code?: string;
|
||||
redirect_uri?: string;
|
||||
client_id: string;
|
||||
client_secret?: string;
|
||||
code_verifier?: string;
|
||||
refresh_token?: string;
|
||||
}
|
||||
|
||||
interface OIDCTokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token?: string;
|
||||
id_token?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/oidc/token
|
||||
*
|
||||
* OIDC Token endpoint that exchanges authorization codes for access tokens.
|
||||
* This endpoint proxies the request to the Go bridge handler at /oidc/token.
|
||||
* Supports authorization_code, refresh_token, and client_credentials grant types.
|
||||
*/
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const contentType = request.headers.get('Content-Type') || '';
|
||||
let tokenRequest: OIDCTokenRequest;
|
||||
|
||||
// Parse request based on content type
|
||||
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const formData = await request.formData();
|
||||
tokenRequest = {
|
||||
grant_type: formData.get('grant_type')?.toString() || '',
|
||||
code: formData.get('code')?.toString() || undefined,
|
||||
redirect_uri: formData.get('redirect_uri')?.toString() || undefined,
|
||||
client_id: formData.get('client_id')?.toString() || '',
|
||||
client_secret: formData.get('client_secret')?.toString() || undefined,
|
||||
code_verifier: formData.get('code_verifier')?.toString() || undefined,
|
||||
refresh_token: formData.get('refresh_token')?.toString() || undefined,
|
||||
};
|
||||
} else if (contentType.includes('application/json')) {
|
||||
tokenRequest = await request.json();
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description:
|
||||
'Content-Type must be application/x-www-form-urlencoded or application/json',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!tokenRequest.grant_type || !tokenRequest.client_id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description: 'Missing required parameters: grant_type and client_id',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate grant type specific parameters
|
||||
if (tokenRequest.grant_type === 'authorization_code') {
|
||||
if (!tokenRequest.code || !tokenRequest.redirect_uri) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description:
|
||||
'Missing required parameters for authorization_code grant: code and redirect_uri',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else if (tokenRequest.grant_type === 'refresh_token') {
|
||||
if (!tokenRequest.refresh_token) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description: 'Missing required parameter for refresh_token grant: refresh_token',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare form data for the bridge handler (OIDC typically uses form encoding)
|
||||
const bridgeFormData = new FormData();
|
||||
Object.entries(tokenRequest).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') {
|
||||
bridgeFormData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward the request to the Go bridge handler
|
||||
const response = await fetch(`${BRIDGE_URL}/oidc/token`, {
|
||||
method: 'POST',
|
||||
body: bridgeFormData,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
// Forward client authentication headers
|
||||
Authorization: request.headers.get('Authorization') || '',
|
||||
'User-Agent': request.headers.get('User-Agent') || '',
|
||||
Origin: request.headers.get('Origin') || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
errorData = {
|
||||
error: 'server_error',
|
||||
error_description: 'Token request failed',
|
||||
};
|
||||
}
|
||||
|
||||
console.error('OIDC Token error:', response.status, errorData);
|
||||
return NextResponse.json(errorData, { status: response.status });
|
||||
}
|
||||
|
||||
const tokenResponse: OIDCTokenResponse = await response.json();
|
||||
|
||||
// Return the token response with appropriate headers
|
||||
return NextResponse.json(tokenResponse, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
'Access-Control-Allow-Origin': request.headers.get('Origin') || '*',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('OIDC Token proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Internal server error during token exchange',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OPTIONS /api/oidc/token
|
||||
*
|
||||
* Handle preflight CORS requests for the token endpoint.
|
||||
*/
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/oidc/token
|
||||
*
|
||||
* Return method not allowed for GET requests to token endpoint.
|
||||
*/
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description: 'GET method not allowed for token endpoint. Use POST.',
|
||||
},
|
||||
{
|
||||
status: 405,
|
||||
headers: {
|
||||
Allow: 'POST, OPTIONS',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const BRIDGE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
|
||||
interface OIDCUserInfo {
|
||||
sub: string;
|
||||
name?: string;
|
||||
preferred_username?: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
did?: string;
|
||||
vault_id?: string;
|
||||
updated_at?: number;
|
||||
claims?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/oidc/userinfo
|
||||
*
|
||||
* OIDC UserInfo endpoint that returns user information for a valid access token.
|
||||
* This endpoint proxies the request to the Go bridge handler at /oidc/userinfo.
|
||||
* Requires a valid Bearer token in the Authorization header.
|
||||
*/
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
// Extract access token from Authorization header
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_token',
|
||||
error_description:
|
||||
'Missing or invalid Authorization header. Expected format: Bearer <access_token>',
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Bearer realm="OIDC UserInfo", error="invalid_token"',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Forward the request to the Go bridge handler
|
||||
const response = await fetch(`${BRIDGE_URL}/oidc/userinfo`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
// Forward the Authorization header with the access token
|
||||
Authorization: authHeader,
|
||||
'User-Agent': request.headers.get('User-Agent') || '',
|
||||
Origin: request.headers.get('Origin') || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
// Handle different error statuses
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
errorData = {
|
||||
error: 'invalid_token',
|
||||
error_description: 'Access token is invalid or expired',
|
||||
};
|
||||
break;
|
||||
case 403:
|
||||
errorData = {
|
||||
error: 'insufficient_scope',
|
||||
error_description: 'Access token does not have sufficient scope',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
errorData = {
|
||||
error: 'server_error',
|
||||
error_description: 'UserInfo request failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.error('OIDC UserInfo error:', response.status, errorData);
|
||||
|
||||
const responseHeaders: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
};
|
||||
|
||||
// Add WWW-Authenticate header for 401 responses
|
||||
if (response.status === 401) {
|
||||
responseHeaders['WWW-Authenticate'] = 'Bearer realm="OIDC UserInfo", error="invalid_token"';
|
||||
}
|
||||
|
||||
return NextResponse.json(errorData, {
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
const userInfo: OIDCUserInfo = await response.json();
|
||||
|
||||
// Return the user information with appropriate headers
|
||||
return NextResponse.json(userInfo, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
'Access-Control-Allow-Origin': request.headers.get('Origin') || '*',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('OIDC UserInfo proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Internal server error during userinfo retrieval',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/oidc/userinfo
|
||||
*
|
||||
* OIDC UserInfo endpoint that accepts POST requests with access token in form data.
|
||||
* This is an alternative method for clients that prefer POST over GET.
|
||||
*/
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const contentType = request.headers.get('Content-Type') || '';
|
||||
let accessToken: string | null = null;
|
||||
|
||||
// Extract access token from different sources
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
accessToken = authHeader.substring(7);
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const formData = await request.formData();
|
||||
accessToken = formData.get('access_token')?.toString() || null;
|
||||
} else if (contentType.includes('application/json')) {
|
||||
const body = await request.json();
|
||||
accessToken = body.access_token || null;
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'invalid_token',
|
||||
error_description: 'Access token required in Authorization header or request body',
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Bearer realm="OIDC UserInfo", error="invalid_token"',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Forward the request to the Go bridge handler
|
||||
const response = await fetch(`${BRIDGE_URL}/oidc/userinfo`, {
|
||||
method: 'GET', // Bridge handler expects GET
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'User-Agent': request.headers.get('User-Agent') || '',
|
||||
Origin: request.headers.get('Origin') || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
errorData = {
|
||||
error: 'server_error',
|
||||
error_description: 'UserInfo request failed',
|
||||
};
|
||||
}
|
||||
|
||||
return NextResponse.json(errorData, { status: response.status });
|
||||
}
|
||||
|
||||
const userInfo: OIDCUserInfo = await response.json();
|
||||
return NextResponse.json(userInfo, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('OIDC UserInfo POST proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
error_description: 'Internal server error during userinfo retrieval',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OPTIONS /api/oidc/userinfo
|
||||
*
|
||||
* Handle preflight CORS requests for the userinfo endpoint.
|
||||
*/
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user