mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 17:58:28 +00:00
216 lines
6.0 KiB
TypeScript
216 lines
6.0 KiB
TypeScript
import { type ActionFunctionArgs, json } from "@remix-run/node";
|
|
import {
|
|
oauth2Service,
|
|
OAuth2Errors,
|
|
type OAuth2TokenRequest,
|
|
} from "~/services/oauth2.server";
|
|
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
if (request.method !== "POST") {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_REQUEST,
|
|
error_description: "Only POST method is allowed",
|
|
},
|
|
{ status: 405 },
|
|
);
|
|
}
|
|
|
|
try {
|
|
const contentType = request.headers.get("content-type");
|
|
let body: any;
|
|
let tokenRequest: OAuth2TokenRequest;
|
|
const authHeader = request.headers.get("authorization");
|
|
let basicAuthClientId: string | undefined;
|
|
let basicAuthClientSecret: string | undefined;
|
|
|
|
if (authHeader?.startsWith("Basic ")) {
|
|
try {
|
|
const encoded = authHeader.slice(6); // Remove "Basic " prefix
|
|
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
const [clientId, clientSecret] = decoded.split(":");
|
|
basicAuthClientId = clientId;
|
|
basicAuthClientSecret = clientSecret;
|
|
} catch (error) {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_CLIENT,
|
|
error_description: "Invalid Basic authorization header",
|
|
},
|
|
{ status: 401 },
|
|
);
|
|
}
|
|
}
|
|
|
|
// Support both JSON and form-encoded data
|
|
if (contentType?.includes("application/json")) {
|
|
body = await request.json();
|
|
tokenRequest = {
|
|
grant_type: body.grant_type,
|
|
code: body.code || undefined,
|
|
redirect_uri: body.redirect_uri || undefined,
|
|
client_id: basicAuthClientId || body.client_id,
|
|
client_secret: basicAuthClientSecret || body.client_secret || undefined,
|
|
code_verifier: body.code_verifier || undefined,
|
|
};
|
|
} else {
|
|
// Fall back to form data for compatibility
|
|
const formData = await request.formData();
|
|
body = Object.fromEntries(formData);
|
|
|
|
tokenRequest = {
|
|
grant_type: formData.get("grant_type") as string,
|
|
code: (formData.get("code") as string) || undefined,
|
|
redirect_uri: (formData.get("redirect_uri") as string) || undefined,
|
|
client_id: basicAuthClientId || (formData.get("client_id") as string),
|
|
client_secret:
|
|
basicAuthClientSecret ||
|
|
(formData.get("client_secret") as string) ||
|
|
undefined,
|
|
code_verifier: (formData.get("code_verifier") as string) || undefined,
|
|
};
|
|
}
|
|
|
|
// Validate required parameters
|
|
if (!tokenRequest.grant_type) {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_REQUEST,
|
|
error_description: "Missing required parameters",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Handle authorization code grant
|
|
if (tokenRequest.grant_type === "authorization_code") {
|
|
if (!tokenRequest.code || !tokenRequest.redirect_uri) {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_REQUEST,
|
|
error_description: "Missing code or redirect_uri",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate client
|
|
try {
|
|
await oauth2Service.validateClient(
|
|
tokenRequest.client_id,
|
|
tokenRequest.client_secret,
|
|
);
|
|
} catch (error) {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_CLIENT,
|
|
error_description: "Invalid client credentials",
|
|
},
|
|
{ status: 401 },
|
|
);
|
|
}
|
|
|
|
// Exchange code for tokens
|
|
try {
|
|
const tokens = await oauth2Service.exchangeCodeForTokens({
|
|
code: tokenRequest.code,
|
|
clientId: tokenRequest.client_id,
|
|
redirectUri: tokenRequest.redirect_uri,
|
|
codeVerifier: tokenRequest.code_verifier,
|
|
});
|
|
|
|
return json(tokens);
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : "Unknown error";
|
|
return json(
|
|
{
|
|
error: errorMessage,
|
|
error_description: "Failed to exchange code for tokens",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
}
|
|
|
|
// Handle refresh token grant
|
|
if (tokenRequest.grant_type === "refresh_token") {
|
|
const refreshToken = body.refresh_token;
|
|
|
|
if (!refreshToken) {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_REQUEST,
|
|
error_description: "Missing refresh_token",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate client
|
|
try {
|
|
await oauth2Service.validateClient(
|
|
tokenRequest.client_id,
|
|
tokenRequest.client_secret,
|
|
);
|
|
} catch (error) {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_CLIENT,
|
|
error_description: "Invalid client credentials",
|
|
},
|
|
{ status: 401 },
|
|
);
|
|
}
|
|
|
|
// Refresh access token
|
|
try {
|
|
const tokens = await oauth2Service.refreshAccessToken(
|
|
refreshToken,
|
|
tokenRequest.client_id,
|
|
);
|
|
return json(tokens);
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : "Unknown error";
|
|
return json(
|
|
{
|
|
error: errorMessage,
|
|
error_description: "Failed to refresh access token",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
}
|
|
|
|
// Unsupported grant type
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.UNSUPPORTED_GRANT_TYPE,
|
|
error_description: "Unsupported grant type",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
} catch (error) {
|
|
console.error("OAuth2 token endpoint error:", error);
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.SERVER_ERROR,
|
|
error_description: "Internal server error",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
};
|
|
|
|
// This endpoint only supports POST
|
|
export const loader = () => {
|
|
return json(
|
|
{
|
|
error: OAuth2Errors.INVALID_REQUEST,
|
|
error_description: "Only POST method is allowed",
|
|
},
|
|
{ status: 405 },
|
|
);
|
|
};
|