Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CREATE TABLE "LoginAttempts" (
"state" TEXT NOT NULL,
"codeVerifier" TEXT NOT NULL,
"identifier" TEXT,
"tokenData" JSONB,
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
Expand Down
1 change: 1 addition & 0 deletions modules/auth_oauth2/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ model LoginAttempts {
codeVerifier String

identifier String?
tokenData Json?

startedAt DateTime @default(now())
expiresAt DateTime
Expand Down
10 changes: 10 additions & 0 deletions modules/auth_oauth2/module.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
"name": "Get Status",
"description": "Check the status of a OAuth login using the flow token. Returns the status of the login flow.",
"public": true
},
"add_to_user": {
"name": "Add OAuth Login to User",
"description": "Use a finished OAuth flow to add the OAuth login to an already-authenticated users.",
"public": true
},
"login_to_user": {
"name": "Login to or Create User with OAuth",
"description": "Use a finished OAuth flow to login to a user, creating a new one if it doesn't exist.",
"public": true
}
},
"errors": {
Expand Down
1 change: 1 addition & 0 deletions modules/auth_oauth2/routes/login_callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export async function handle(
},
data: {
identifier: ident,
tokenData: { ...tokens },
completedAt: new Date(),
},
});
Expand Down
64 changes: 64 additions & 0 deletions modules/auth_oauth2/scripts/add_to_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";

export interface Request {
flowToken: string;
userToken: string;
}

export type Response = ReturnType<ScriptContext["modules"]["authProviders"]["addProviderToUser"]>;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

if (!req.flowToken) throw new RuntimeError("missing_token", { statusCode: 400 });

const { tokens: [flowToken] } = await ctx.modules.tokens.fetchByToken({ tokens: [req.flowToken] });
if (!flowToken) {
throw new RuntimeError("invalid_token", { statusCode: 400 });
}
if (new Date(flowToken.expireAt ?? 0) < new Date()) {
throw new RuntimeError("expired_token", { statusCode: 400 });
}

const flowId = flowToken.meta.flowId;
if (!flowId) throw new RuntimeError("invalid_token", { statusCode: 400 });

const flow = await ctx.db.loginAttempts.findFirst({
where: {
id: flowId,
}
});
if (!flow) throw new RuntimeError("invalid_token", { statusCode: 400 });

if (!flow.identifier || !flow.tokenData) {
throw new RuntimeError("flow_not_complete", { statusCode: 400 });
}

await ctx.modules.users.authenticateToken({ userToken: req.userToken });

const tokenData = flow.tokenData;
if (!tokenData) {
throw new RuntimeError("internal_error", { statusCode: 500 });
}
if (typeof tokenData !== "object") {
throw new RuntimeError("internal_error", { statusCode: 500 });
}
if (Array.isArray(tokenData)) {
throw new RuntimeError("internal_error", { statusCode: 500 });
}

return await ctx.modules.authProviders.addProviderToUser({
userToken: req.userToken,
info: {
providerType: "oauth2",
providerId: flow.providerId,
},
uniqueData: {
identifier: flow.identifier,
},
additionalData: tokenData,
});
}
2 changes: 1 addition & 1 deletion modules/auth_oauth2/scripts/get_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function run(
});
if (!flow) throw new RuntimeError("invalid_token", { statusCode: 400 });

if (flow.identifier) {
if (flow.identifier && flow.tokenData) {
return { status: "complete" };
} else if (new Date(flow.expiresAt) < new Date()) {
return { status: "expired" };
Expand Down
60 changes: 60 additions & 0 deletions modules/auth_oauth2/scripts/login_to_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";

export interface Request {
flowToken: string;
}

export type Response = ReturnType<ScriptContext["modules"]["authProviders"]["getOrCreateUserFromProvider"]>;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

if (!req.flowToken) throw new RuntimeError("missing_token", { statusCode: 400 });

const { tokens: [flowToken] } = await ctx.modules.tokens.fetchByToken({ tokens: [req.flowToken] });
if (!flowToken) {
throw new RuntimeError("invalid_token", { statusCode: 400 });
}
if (new Date(flowToken.expireAt ?? 0) < new Date()) {
throw new RuntimeError("expired_token", { statusCode: 400 });
}

const flowId = flowToken.meta.flowId;
if (!flowId) throw new RuntimeError("invalid_token", { statusCode: 400 });

const flow = await ctx.db.loginAttempts.findFirst({
where: {
id: flowId,
}
});
if (!flow) throw new RuntimeError("invalid_token", { statusCode: 400 });

if (!flow.identifier || !flow.tokenData) {
throw new RuntimeError("flow_not_complete", { statusCode: 400 });
}

const tokenData = flow.tokenData;
if (!tokenData) {
throw new RuntimeError("internal_error", { statusCode: 500 });
}
if (typeof tokenData !== "object") {
throw new RuntimeError("internal_error", { statusCode: 500 });
}
if (Array.isArray(tokenData)) {
throw new RuntimeError("internal_error", { statusCode: 500 });
}

return await ctx.modules.authProviders.getOrCreateUserFromProvider({
info: {
providerType: "oauth2",
providerId: flow.providerId,
},
uniqueData: {
identifier: flow.identifier,
},
additionalData: tokenData,
});
}