core/apps/webapp/app/services/apiAuth.server.ts
Harshith Mullapudi 714399cf41
Feat: OAuth support for external apps (#22)
* Feat: OAuth support for external apps

* Fix: OAuth screen

---------

Co-authored-by: Manoj K <saimanoj58@gmail.com>
2025-07-19 16:44:15 +05:30

133 lines
3.3 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" };
}