Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/demo_web/src/types/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export type LoginMethod =
| "x"
| "discord"
| "apple"
| "email";
| "email"
| "github";
40 changes: 40 additions & 0 deletions backend/oko_api/server/src/api/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Result } from "@oko-wallet/stdlib-js";
import type { SocialLoginGithubVerifyUserResponse } from "@oko-wallet/oko-types/social_login";

export const GITHUB_SOCIAL_LOGIN_TOKEN_URL =
"https://github.com/login/oauth/access_token";
export const GITHUB_USER_INFO_URL = "https://api.github.com/user";
export const GITHUB_CLIENT_ID = "PLACEHOLDER_GITHUB_CLIENT_ID";

export async function getGithubUserInfo(accessToken: string): Promise<
Result<
SocialLoginGithubVerifyUserResponse,
{
status: number;
text: string;
}
>
> {
const res = await fetch(GITHUB_USER_INFO_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.github+json",
},
});

if (res.ok) {
return {
success: true,
data: await res.json(),
};
}

return {
success: false,
err: {
status: res.status,
text: await res.text(),
},
};
}
65 changes: 65 additions & 0 deletions backend/oko_api/server/src/middleware/auth/github_auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { Request, Response, NextFunction } from "express";
import type { AuthType } from "@oko-wallet/oko-types/auth";

import { validateAccessTokenOfGithub } from "@oko-wallet-api/middleware/auth/github_auth/validate";
import type { OAuthLocals } from "@oko-wallet-api/middleware/auth/types";

export interface GithubAuthenticatedRequest<T = any> extends Request {
body: T;
}

export async function githubAuthMiddleware(
req: GithubAuthenticatedRequest,
res: Response<unknown, OAuthLocals>,
next: NextFunction,
) {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith("Bearer ")) {
res
.status(401)
.json({ error: "Authorization header with Bearer token required" });
return;
}

const accessToken = authHeader.substring(7).trim();

try {
const result = await validateAccessTokenOfGithub(accessToken);

if (!result.success) {
res.status(401).json({ error: result.err });
return;
}

if (!result.data) {
res.status(500).json({
error: "Internal server error: Token info missing after validation",
});
return;
}

if (result.data.id == null) {
res.status(401).json({
error: "Can't get id from GitHub token",
});
return;
}

res.locals.oauth_user = {
type: "github" as AuthType,
user_identifier: `github_${result.data.id}`,
email: result.data.email ?? undefined,
name: result.data.login,
metadata: result.data as unknown as Record<string, unknown>,
};

next();
return;
} catch (err: unknown) {
res.status(500).json({
error: `Token validation failed: ${err instanceof Error ? err.message : String(err)}`,
});
return;
}
}
34 changes: 34 additions & 0 deletions backend/oko_api/server/src/middleware/auth/github_auth/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Result } from "@oko-wallet/stdlib-js";
import type { SocialLoginGithubVerifyUserResponse } from "@oko-wallet/oko-types/social_login";

import { getGithubUserInfo } from "@oko-wallet-api/api/github";

export async function validateAccessTokenOfGithub(
accessToken: string,
): Promise<Result<SocialLoginGithubVerifyUserResponse, string>> {
try {
const res = await getGithubUserInfo(accessToken);
if (!res.success) {
if (res.err.status === 429) {
return {
success: false,
err: "Too Many Requests",
};
}
return {
success: false,
err: `Invalid token: ${res.err.text}`,
};
}

return {
success: true,
data: res.data,
};
} catch (error: unknown) {
return {
success: false,
err: `Token validation failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
12 changes: 11 additions & 1 deletion backend/oko_api/server/src/middleware/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
discordAuthMiddleware,
type DiscordAuthenticatedRequest,
} from "@oko-wallet-api/middleware/auth/discord_auth";
import {
githubAuthMiddleware,
type GithubAuthenticatedRequest,
} from "@oko-wallet-api/middleware/auth/github_auth";
import type {
OAuthBody,
OAuthLocals,
Expand Down Expand Up @@ -59,9 +63,15 @@ export async function oauthMiddleware(
res,
next,
);
case "github":
return githubAuthMiddleware(
req as GithubAuthenticatedRequest,
res,
next,
);
default:
res.status(400).json({
error: `Invalid auth_type: ${authType}. Must be 'google', 'auth0', 'x', or 'telegram'`,
error: `Invalid auth_type: ${authType}. Must be 'google', 'auth0', 'x', 'telegram', 'discord', or 'github'`,
});
return;
}
Expand Down
150 changes: 150 additions & 0 deletions backend/oko_api/server/src/routes/social_login_v1/get_github_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { Response, Request } from "express";
import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response";
import type {
SocialLoginGithubBody,
SocialLoginGithubResponse,
} from "@oko-wallet/oko-types/social_login";
import { registry } from "@oko-wallet/oko-api-openapi";
import { ErrorResponseSchema } from "@oko-wallet/oko-api-openapi/common";
import {
SocialLoginGithubRequestSchema,
SocialLoginGithubSuccessResponseSchema,
} from "@oko-wallet/oko-api-openapi/social_login";

import {
GITHUB_CLIENT_ID,
GITHUB_SOCIAL_LOGIN_TOKEN_URL,
} from "@oko-wallet-api/api/github";

registry.registerPath({
method: "post",
path: "/social-login/v1/github/get-token",
tags: ["Social Login"],
summary: "Get GitHub access token",
description:
"Exchange authorization code for a GitHub access token using PKCE",
request: {
body: {
required: true,
content: {
"application/json": {
schema: SocialLoginGithubRequestSchema,
},
},
},
},
responses: {
200: {
description: "Successfully retrieved access token",
content: {
"application/json": {
schema: SocialLoginGithubSuccessResponseSchema,
},
},
},
400: {
description: "Invalid request",
content: {
"application/json": {
schema: ErrorResponseSchema,
},
},
},
500: {
description: "Server error",
content: {
"application/json": {
schema: ErrorResponseSchema,
},
},
},
},
});

export async function getGithubToken(
req: Request<any, any, SocialLoginGithubBody>,
res: Response<OkoApiResponse<SocialLoginGithubResponse>>,
) {
const body = req.body;

if (!body.code || !body.code_verifier || !body.redirect_uri) {
res.status(400).json({
success: false,
code: "INVALID_REQUEST",
msg: "Code, code_verifier, or redirect_uri is not set",
});
return;
}

const clientSecret = process.env.GITHUB_CLIENT_SECRET;
if (!clientSecret) {
res.status(500).json({
success: false,
code: "UNKNOWN_ERROR",
msg: "GitHub client secret is not configured",
});
return;
}

try {
const reqBody = new URLSearchParams({
code: body.code,
grant_type: "authorization_code",
client_id: GITHUB_CLIENT_ID,
client_secret: clientSecret,
redirect_uri: body.redirect_uri,
code_verifier: body.code_verifier,
});

const response = await fetch(GITHUB_SOCIAL_LOGIN_TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: reqBody,
});

if (response.status === 200) {
const data: SocialLoginGithubResponse & {
error?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why couldn't this be part of the type definition?

error_description?: string;
} = await response.json();

if (data.error) {
res.status(400).json({
success: false,
code: "UNKNOWN_ERROR",
msg: `${data.error}: ${data.error_description}`,
});
return;
}

res.status(200).json({
success: true,
data: {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in,
token_type: data.token_type,
scope: data.scope,
},
});
return;
}

res.status(response.status).json({
success: false,
code: "UNKNOWN_ERROR",
msg: await response.text(),
});
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Failed to exchange GitHub token";
res.status(500).json({
success: false,
code: "UNKNOWN_ERROR",
msg: message,
});
}
}
7 changes: 7 additions & 0 deletions backend/oko_api/server/src/routes/social_login_v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { userJwtMiddleware } from "@oko-wallet-api/middleware/auth/keplr_auth";
import express from "express";

import { getXToken } from "./get_x_token";
import { getGithubToken } from "./get_github_token";
import { verifyXUser } from "./verify_x_user";
import { rateLimitMiddleware } from "@oko-wallet-api/middleware/rate_limit";
import { saveReferral } from "./save_referral";
Expand All @@ -16,6 +17,12 @@ export function makeSocialLoginRouter() {
getXToken,
);

router.post(
"/github/get-token",
rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }),
getGithubToken,
);

router.get(
"/x/verify-user",
rateLimitMiddleware({ windowSeconds: 60, maxRequests: 10 }),
Expand Down
Loading