Add initial library
This commit is contained in:
parent
7cecc1d042
commit
e8a82796e0
6 changed files with 180 additions and 0 deletions
25
package.json
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "sf-auth-nextjs-middleware",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Nextjs middleware and callback route for sf-auth",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"default": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"nextjs",
|
||||||
|
"middleware",
|
||||||
|
"authentication",
|
||||||
|
"sf-auth"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": ">=13.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/callback.ts
Normal file
87
src/callback.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { NextResponse, type NextRequest } from "next/server";
|
||||||
|
|
||||||
|
import { AUTH_VALIDATE_URL, DEFAULT_COOKIE_NAMES } from "./constants";
|
||||||
|
import type { SfAuthCookieNames } from "./middleware";
|
||||||
|
|
||||||
|
export type SfAuthCallbackOptions = {
|
||||||
|
redirectTo: string;
|
||||||
|
cookieNames?: SfAuthCookieNames;
|
||||||
|
validateEndpoint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidateResponse = {
|
||||||
|
valid: boolean;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveCookieNames = (cookieNames?: SfAuthCookieNames) => ({
|
||||||
|
userId: cookieNames?.userId ?? DEFAULT_COOKIE_NAMES.userId,
|
||||||
|
username: cookieNames?.username ?? DEFAULT_COOKIE_NAMES.username
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorResponse = (message: string, status = 400) =>
|
||||||
|
new NextResponse(message, {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/plain; charset=utf-8"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSfAuthCallbackRoute = (
|
||||||
|
options: SfAuthCallbackOptions
|
||||||
|
) => {
|
||||||
|
const cookieNames = resolveCookieNames(options.cookieNames);
|
||||||
|
const validateUrl = options.validateEndpoint ?? AUTH_VALIDATE_URL;
|
||||||
|
|
||||||
|
return async (request: NextRequest) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const userId = url.searchParams.get("user_id");
|
||||||
|
const username = url.searchParams.get("username");
|
||||||
|
const key = url.searchParams.get("key");
|
||||||
|
|
||||||
|
if (!userId || !username || !key) {
|
||||||
|
return errorResponse("Missing required query parameters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let validateResponse: ValidateResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(validateUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ user_id: userId, key })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return errorResponse("Failed to validate credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
validateResponse = (await response.json()) as ValidateResponse;
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse("Unable to validate credentials.", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateResponse.valid || validateResponse.user_id !== userId) {
|
||||||
|
return errorResponse("Invalid credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectTarget = new URL(options.redirectTo, request.url);
|
||||||
|
const response = NextResponse.redirect(redirectTarget);
|
||||||
|
|
||||||
|
response.cookies.set(cookieNames.userId, userId, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
response.cookies.set(cookieNames.username, username, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
path: "/"
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
};
|
||||||
10
src/constants.ts
Normal file
10
src/constants.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const DEFAULT_COOKIE_NAMES = {
|
||||||
|
userId: "sf_user_id",
|
||||||
|
username: "sf_username"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const AUTH_REDIRECT_BASE_URL =
|
||||||
|
"https://snazzyfellas.com/api/redirect/authenticate";
|
||||||
|
|
||||||
|
export const AUTH_VALIDATE_URL =
|
||||||
|
"https://snazzyfellas.com/api/redirect/validate";
|
||||||
5
src/index.ts
Normal file
5
src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { createSfAuthMiddleware } from "./middleware";
|
||||||
|
export type { SfAuthMiddlewareOptions, SfAuthCookieNames } from "./middleware";
|
||||||
|
|
||||||
|
export { createSfAuthCallbackRoute } from "./callback";
|
||||||
|
export type { SfAuthCallbackOptions } from "./callback";
|
||||||
38
src/middleware.ts
Normal file
38
src/middleware.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextResponse, type NextRequest } from "next/server";
|
||||||
|
|
||||||
|
import { AUTH_REDIRECT_BASE_URL, DEFAULT_COOKIE_NAMES } from "./constants";
|
||||||
|
|
||||||
|
export type SfAuthCookieNames = {
|
||||||
|
userId?: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SfAuthMiddlewareOptions = {
|
||||||
|
cookieNames?: SfAuthCookieNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveCookieNames = (cookieNames?: SfAuthCookieNames) => ({
|
||||||
|
userId: cookieNames?.userId ?? DEFAULT_COOKIE_NAMES.userId,
|
||||||
|
username: cookieNames?.username ?? DEFAULT_COOKIE_NAMES.username
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSfAuthMiddleware = (
|
||||||
|
redirectUri: string,
|
||||||
|
options: SfAuthMiddlewareOptions = {}
|
||||||
|
) => {
|
||||||
|
const cookieNames = resolveCookieNames(options.cookieNames);
|
||||||
|
const redirectUrl = `${AUTH_REDIRECT_BASE_URL}?redirect_uri=${encodeURIComponent(
|
||||||
|
redirectUri
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
return (request: NextRequest) => {
|
||||||
|
const userId = request.cookies.get(cookieNames.userId)?.value;
|
||||||
|
const username = request.cookies.get(cookieNames.username)?.value;
|
||||||
|
|
||||||
|
if (!userId || !username) {
|
||||||
|
return NextResponse.redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
};
|
||||||
|
};
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue