This repository was archived by the owner on Sep 17, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add auth_oauth
#132
Open
Blckbrry-Pi
wants to merge
2
commits into
main
Choose a base branch
from
07-20-eat_create_the_auth_username_pasword_module
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: add auth_oauth
#132
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export interface Config { | ||
| providers: Record<string, ProviderEndpoints | string>; | ||
| } | ||
|
|
||
| export interface ProviderEndpoints { | ||
| authorization: string; | ||
| token: string; | ||
| userinfo: string; | ||
| scopes: string; | ||
| userinfoKey: string; | ||
| } | ||
15 changes: 15 additions & 0 deletions
15
modules/auth_oauth2/db/migrations/20240701041159_init/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| -- CreateTable | ||
| CREATE TABLE "LoginAttempts" ( | ||
| "id" TEXT NOT NULL, | ||
| "providerId" TEXT NOT NULL, | ||
| "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), | ||
| "invalidatedAt" TIMESTAMP(3), | ||
|
|
||
| CONSTRAINT "LoginAttempts_pkey" PRIMARY KEY ("id") | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Please do not edit this file manually | ||
| # It should be added in your version-control system (i.e. Git) | ||
| provider = "postgresql" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Do not modify this `datasource` block | ||
| datasource db { | ||
| provider = "postgresql" | ||
| url = env("DATABASE_URL") | ||
| } | ||
|
|
||
| model LoginAttempts { | ||
| id String @id @default(uuid()) | ||
|
|
||
| providerId String | ||
| state String | ||
| codeVerifier String | ||
|
|
||
| identifier String? | ||
| tokenData Json? | ||
|
|
||
| startedAt DateTime @default(now()) | ||
| expiresAt DateTime | ||
| completedAt DateTime? | ||
| invalidatedAt DateTime? | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| { | ||
| "name": "OAuth2 Authentication Provider", | ||
| "description": "Authenticate users with OAuth 2.0.", | ||
| "icon": "key", | ||
| "tags": [ | ||
| "core", | ||
| "user", | ||
| "auth" | ||
| ], | ||
| "authors": [ | ||
| "rivet-gg", | ||
| "Skyler Calaman" | ||
| ], | ||
| "status": "beta", | ||
| "dependencies": { | ||
| "rate_limit": {}, | ||
| "identities": {}, | ||
| "users": {}, | ||
| "tokens": {} | ||
| }, | ||
| "routes": { | ||
| "login_callback": { | ||
| "name": "OAuth Redirect Callback", | ||
| "description": "Verify a user's OAuth login and create a session.", | ||
| "method": "GET", | ||
| "pathPrefix": "/callback/" | ||
| } | ||
| }, | ||
| "scripts": { | ||
Blckbrry-Pi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "start_login": { | ||
| "name": "Start Login", | ||
| "description": "Start the OAuth login process. Returns a URL to redirect the user to and a flow token.", | ||
| "public": true | ||
| }, | ||
| "get_login_status": { | ||
| "name": "Get Login Status", | ||
| "description": "Check the status of a OAuth login using the flow token. Returns the status of the login flow.", | ||
| "public": true | ||
| }, | ||
| "complete_add_to_user": { | ||
| "name": "Complete Flow and Add OAuth Login to User", | ||
| "description": "Use a finished OAuth flow to add the OAuth login to an already-authenticated users.", | ||
| "public": true | ||
| }, | ||
| "complete_login_to_user": { | ||
| "name": "Complete Flow and 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": { | ||
| "already_friends": { | ||
| "name": "Already Friends" | ||
| }, | ||
| "friend_request_not_found": { | ||
| "name": "Friend Request Not Found" | ||
| }, | ||
| "friend_request_already_exists": { | ||
| "name": "Friend Request Already Exists" | ||
| }, | ||
| "not_friend_request_recipient": { | ||
| "name": "Not Friend Request Recipient" | ||
| }, | ||
| "friend_request_already_accepted": { | ||
| "name": "Friend Request Already Accepted" | ||
| }, | ||
| "friend_request_already_declined": { | ||
| "name": "Friend Request Already Declined" | ||
| }, | ||
| "cannot_send_to_self": { | ||
| "name": "Cannot Send to Self" | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { | ||
| RouteContext, | ||
| RuntimeError, | ||
| RouteRequest, | ||
| RouteResponse, | ||
| } from "../module.gen.ts"; | ||
|
|
||
| import { getFullConfig } from "../utils/env.ts"; | ||
| import { getClient } from "../utils/client.ts"; | ||
| import { getUserUniqueIdentifier } from "../utils/client.ts"; | ||
| import { Tokens } from "https://deno.land/x/oauth2_client@v1.0.2/mod.ts"; | ||
|
|
||
| import { compareConstantTime, stateToDataStr } from "../utils/state.ts"; | ||
| import { OAUTH_DONE_HTML } from "../utils/pages.ts"; | ||
|
|
||
| export async function handle( | ||
| ctx: RouteContext, | ||
| req: RouteRequest, | ||
| ): Promise<RouteResponse> { | ||
| // Max 5 login attempts per IP per minute | ||
| ctx.modules.rateLimit.throttlePublic({ requests: 5, period: 60 }); | ||
|
|
||
| // Ensure that the provider configurations are valid | ||
| const config = await getFullConfig(ctx.config); | ||
| if (!config) throw new RuntimeError("invalid_config", { statusCode: 500 }); | ||
|
|
||
| // Get the URI that this request was made to | ||
| const uri = new URL(req.url); | ||
|
|
||
| // Get the state from the URI | ||
| const redirectedState = uri.searchParams.get("state"); | ||
| if (!redirectedState) { | ||
| throw new RuntimeError("missing_state", { statusCode: 400 }); | ||
| } | ||
|
|
||
| // Extract the data from the state | ||
| const stateData = await stateToDataStr(config.oauthSecret, redirectedState); | ||
| const { flowId, providerId } = JSON.parse(stateData); | ||
|
|
||
| // Get the login attempt stored in the database | ||
| const loginAttempt = await ctx.db.loginAttempts.findUnique({ | ||
| where: { | ||
| id: flowId, | ||
| }, | ||
| }); | ||
| if (!loginAttempt) throw new RuntimeError("login_not_found", { statusCode: 400 }); | ||
|
|
||
| // Check if the login attempt is valid | ||
| if (loginAttempt.completedAt) { | ||
| throw new RuntimeError("login_already_completed", { statusCode: 400 }); | ||
| } | ||
| if (loginAttempt.invalidatedAt) { | ||
| throw new RuntimeError("login_cancelled", { statusCode: 400 }); | ||
| } | ||
| if (new Date(loginAttempt.expiresAt) < new Date()) { | ||
| throw new RuntimeError("login_expired", { statusCode: 400 }); | ||
| } | ||
|
|
||
| // Check if the provider ID and state match | ||
| const providerIdMatch = compareConstantTime(loginAttempt.providerId, providerId); | ||
| const stateMatch = compareConstantTime(loginAttempt.state, redirectedState); | ||
| if (!providerIdMatch || !stateMatch) throw new RuntimeError("invalid_state", { statusCode: 400 }); | ||
|
|
||
| const { state, codeVerifier } = loginAttempt; | ||
|
|
||
| // Get the provider config | ||
| const provider = config.providers[providerId]; | ||
| if (!provider) throw new RuntimeError("invalid_provider", { statusCode: 400 }); | ||
|
|
||
| // Get the oauth client | ||
| const client = getClient(config, provider.name); | ||
| if (!client.config.redirectUri) throw new RuntimeError("invalid_config", { statusCode: 500 }); | ||
|
|
||
| // Get the user's tokens and sub | ||
| let tokens: Tokens; | ||
| let ident: string; | ||
| try { | ||
| tokens = await client.code.getToken(uri.toString(), { state, codeVerifier }); | ||
| ident = await getUserUniqueIdentifier(tokens.accessToken, provider); | ||
| } catch (e) { | ||
| console.error(e); | ||
| throw new RuntimeError("invalid_oauth_response", { statusCode: 502 }); | ||
| } | ||
|
|
||
| // Update the login attempt | ||
| await ctx.db.loginAttempts.update({ | ||
| where: { | ||
| id: flowId, | ||
| }, | ||
| data: { | ||
| identifier: ident, | ||
| tokenData: { ...tokens }, | ||
| completedAt: new Date(), | ||
| }, | ||
| }); | ||
|
|
||
| return new RouteResponse( | ||
| OAUTH_DONE_HTML, | ||
| { | ||
| status: 200, | ||
| headers: { | ||
| "Content-Type": "text/html", | ||
| }, | ||
| }, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"]["identities"]["link"]>; | ||
|
|
||
| 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.identities.link({ | ||
| userToken: req.userToken, | ||
| info: { | ||
| identityType: "oauth2", | ||
| identityId: flow.providerId, | ||
| }, | ||
| uniqueData: { | ||
| identifier: flow.identifier, | ||
| }, | ||
| additionalData: tokenData, | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"]["identities"]["signInOrSignUp"]>; | ||
|
|
||
| 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.identities.signInOrSignUp({ | ||
| info: { | ||
| identityType: "oauth2", | ||
| identityId: flow.providerId, | ||
| }, | ||
| uniqueData: { | ||
| identifier: flow.identifier, | ||
| }, | ||
| additionalData: tokenData, | ||
| }); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's rename this to
customProvidersand add pre-configured blocks forgoogle,facebook, etc.see the repo i sent an while back for reference on pre-made configs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. This will require some reworking however, and IMO that functionality should be provided by a
googleorfacebookmodule.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok cool let's get the interface stable then by just renaming this to "customProviders" and add presets later