core/apps/webapp/app/services/apiAuth.server.ts
Harshith Mullapudi c80303a851
OAuth for core (#28)
* Feat: add integrations access to OAuth apps

* Fix: generalize OAuth flow

---------

Co-authored-by: Manoj K <saimanoj58@gmail.com>
2025-07-23 13:03:13 +05:30

178 lines
4.2 KiB
TypeScript

import { findUserByToken } from "~/models/personal-token.server";
import { oauth2Service } from "~/services/oauth2.server";
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export type ApiAuthenticationResult =
| ApiAuthenticationResultSuccess
| ApiAuthenticationResultFailure;
export type ApiAuthenticationResultSuccess = {
ok: true;
apiKey: string;
type: "PRIVATE" | "OAUTH2";
userId: string;
scopes?: string[];
oneTimeUse?: boolean;
oauth2?: {
clientId: string;
scope: string | null;
};
};
export type ApiAuthenticationResultFailure = {
ok: false;
error: string;
};
/**
* This method is the same as `authenticateApiRequest` but it returns a failure result instead of undefined.
* It should be used from now on to ensure that the API key is always validated and provide a failure result.
*/
export async function authenticateApiRequestWithFailure(
request: Request,
options: { allowPublicKey?: boolean; allowJWT?: boolean } = {},
): Promise<ApiAuthenticationResult> {
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
return {
ok: false,
error: "Invalid API Key",
};
}
const authentication = await authenticateApiKeyWithFailure(apiKey, options);
return authentication;
}
/**
* This method is the same as `authenticateApiKey` but it returns a failure result instead of undefined.
* It should be used from now on to ensure that the API key is always validated and provide a failure result.
*/
export async function authenticateApiKeyWithFailure(
apiKey: string,
options: { allowPublicKey?: boolean; allowJWT?: boolean } = {},
): Promise<ApiAuthenticationResult> {
// First try OAuth2 access token
try {
const accessToken = await oauth2Service.validateAccessToken(apiKey);
if (accessToken) {
return {
ok: true,
apiKey,
type: "OAUTH2",
userId: accessToken.user.id,
scopes: accessToken.scope ? accessToken.scope.split(" ") : undefined,
oauth2: {
clientId: accessToken.client.clientId,
scope: accessToken.scope,
},
};
}
} catch (error) {
// If OAuth2 token validation fails, continue to PAT validation
}
// Fall back to PAT authentication
const result = getApiKeyResult(apiKey);
if (!result) {
return {
ok: false,
error: "Invalid API Key",
};
}
switch (result.type) {
case "PRIVATE": {
const user = await findUserByToken(result.apiKey);
if (!user) {
return {
ok: false,
error: "Invalid API Key",
};
}
return {
ok: true,
...result,
userId: user.userId,
};
}
}
}
export function isSecretApiKey(key: string) {
return key.startsWith("rc_");
}
export function getApiKeyFromRequest(request: Request) {
return getApiKeyFromHeader(request.headers.get("Authorization"));
}
export function getApiKeyFromHeader(authorization?: string | null) {
if (typeof authorization !== "string" || !authorization) {
return;
}
const apiKey = authorization.replace(/^Bearer /, "");
return apiKey;
}
export function getApiKeyResult(apiKey: string): {
apiKey: string;
type: "PRIVATE";
} {
return { apiKey, type: "PRIVATE" };
}
/**
* Authenticate OAuth2 requests specifically
* Returns structured result for OAuth endpoints
*/
export async function authenticateOAuthRequest(
request: Request,
scopes?: string[],
): Promise<{
success: boolean;
user?: { id: string };
clientId?: string;
error?: string;
}> {
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
return {
success: false,
error: "Missing authorization header",
};
}
// Only allow OAuth2 tokens for OAuth API endpoints
try {
const accessToken = await oauth2Service.validateAccessToken(apiKey, scopes);
if (accessToken) {
return {
success: true,
user: { id: accessToken.user.id },
clientId: accessToken.client.clientId,
};
}
} catch (error) {
return {
success: false,
error: "Invalid or expired access token",
};
}
return {
success: false,
error: "Invalid access token",
};
}