Skip to content

Commit 64cf3e2

Browse files
authored
Merge pull request #6 from scratch-auth/security-update-1
Security update
2 parents 86f6076 + b4a264b commit 64cf3e2

File tree

13 files changed

+301
-58
lines changed

13 files changed

+301
-58
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/node_modules
2-
.next
2+
.next
3+
/test

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Scratch Auth
1+
# [Scratch Auth](https://scratch-auth.netlify.app)
22

33
## Overview
44

package-lock.json

Lines changed: 14 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"nextra": "^2.13.4",
2929
"nextra-theme-docs": "^2.13.4",
3030
"react": "^18.3.1",
31-
"react-dom": "^18.3.1"
31+
"react-dom": "^18.3.1",
32+
"tailwind-merge": "^2.5.3"
3233
},
3334
"devDependencies": {
3435
"autoprefixer": "^10.4.20",

packages/nextjs/.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
node_modules
1+
/node_modules
22
package-lock.json
3-
test

packages/nextjs/.npmignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
.github
2-
node_modules
2+
/node_modules
33
package-lock.json
44
README.md
5-
test

packages/nextjs/README.md

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,138 @@
1-
# Scratch Auth with Next.js
1+
# [Scratch Auth with Next.js](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart)
22

3-
Scratch Auth is a simple OAuth service for Scratch. It provides developers with an easy-to-understand API and users with a smooth login experience. By using @scratch-auth/nextjs, you can easily implement OAuth functionality into your site.
3+
Scratch Auth is a simple OAuth service for Scratch. It provides developers with an easy-to-understand API and users with a smooth login experience. By using @scratch-auth/nextjs, you can easily implement OAuth functionality into your site.
4+
5+
6+
### [Install](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart#install)
7+
8+
Scratch Auth is composed of a set of NPM packages.
9+
10+
```bash
11+
npm install @scratch-auth/nextjs
12+
```
13+
14+
### [Set your environment variables](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart#set-your-environment-variables)
15+
16+
Add the following keys to your file. These keys can be generated at any time using `openssl rand -hex 32`.
17+
18+
```bash
19+
openssl rand -hex 32
20+
```
21+
22+
```env
23+
SCRATCH_AUTH_SECRET_KEY=***************************************
24+
```
25+
26+
### [Adding a Session Verification Page](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart#adding-a-session-verification-page)
27+
28+
This page will decode the session and verify account information. You need to add this file to the path of the `redirect_url` that you set in `scratch-auth.config.ts`. During the redirect, the session will be set in the `privateCode` parameter.
29+
30+
```tsx filename="app/api/auth/page.tsx"
31+
"use client";
32+
33+
import React, { useEffect } from "react";
34+
import { useRouter, useSearchParams } from "next/navigation";
35+
import { setScratchAuthSession } from "@scratch-auth/nextjs";
36+
37+
export default function AuthPage() {
38+
const router = useRouter();
39+
const searchParams = useSearchParams();
40+
const privateCode = searchParams.get("privateCode");
41+
42+
useEffect(() => {
43+
async function auth() {
44+
console.log(privateCode);
45+
const check = await setScratchAuthSession(privateCode);
46+
console.log("check", check);
47+
if (check) {
48+
console.log("Authentication successful");
49+
} else {
50+
console.error("Authentication failed");
51+
}
52+
router.push("/");
53+
}
54+
auth();
55+
}, [privateCode]);
56+
57+
return (
58+
<div className="flex justify-center items-center w-full h-full min-h-[calc(100dvh-64px)]">
59+
Authenticating Scratch account...
60+
</div>
61+
);
62+
}
63+
```
64+
65+
### [Adding an Authentication Button](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart#adding-a-session-verification-page)
66+
67+
By using Scratch Auth’s pre-built components, you can control the content displayed for logged-in and logged-out users. First, create a header for users to log in or out. To do this, use the following:
68+
69+
- `<LogIned>`: The children of this component are displayed only when the user is logged in.
70+
- `<LogOuted>`: The children of this component are displayed only when the user is logged out.
71+
- `<UserButton />`: A pre-built component with styles ready to display the avatar of the logged-in user’s account.
72+
- `<LogInButton />`: An unstyled component that links to the login page. In this example, since no properties or environment variables are specified for the login URL, the component will link to the login page of the account portal.
73+
74+
```tsx filename="app/page.tsx"
75+
import React from "react";
76+
import {
77+
LogInButton,
78+
LogIned,
79+
LogOuted,
80+
UserButton,
81+
} from "@scratch-auth/nextjs";
82+
83+
export default function Page() {
84+
return (
85+
<div>
86+
<LogOuted>
87+
<LogInButton />
88+
</LogOuted>
89+
<LogIned>
90+
<UserButton />
91+
</LogIned>
92+
</div>
93+
);
94+
}
95+
```
96+
97+
### [Adding the Package Configuration File](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart#adding-the-package-configuration-file)
98+
99+
| Item | Description |
100+
| ------------ | ------------------------------------------- |
101+
| COOKIE_NAME | The name to be stored in the cookie |
102+
| redirect_url | The URL for the session authentication page |
103+
| title | Project title |
104+
| expiration | Session expiration time |
105+
| cn | CN function |
106+
| debug | Debug mode |
107+
108+
```tsx filename="scratch-auth.config.ts"
109+
import { ScratchAuthConfig } from "@scratch-auth/nextjs/src/types";
110+
import { type ClassValue, clsx } from "clsx";
111+
import { twMerge } from "tailwind-merge";
112+
113+
const config: ScratchAuthConfig = {
114+
COOKIE_NAME: "scratch-auth-session",
115+
redirect_url: `http://localhost:3000/api/auth`,
116+
title: `Scratch Auth`,
117+
expiration: 30,
118+
cn: function cn(...inputs: ClassValue[]) {
119+
return twMerge(clsx(inputs));
120+
},
121+
debug: true,
122+
};
123+
124+
export default config;
125+
```
126+
127+
### [Authenticating the Scratch Account](https://scratch-auth.netlify.app/en/docs/nextjs/quickstart#authenticating-the-scratch-account)
128+
129+
Run your project with the following command:
130+
131+
```bash
132+
npm run dev
133+
```
134+
135+
Access the app’s homepage at [http://localhost:3000](http://localhost:3000). Sign
136+
up and create the first user.
137+
138+
</Steps>

packages/nextjs/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@scratch-auth/nextjs",
3-
"version": "0.0.1-beta.8",
3+
"version": "0.0.1-beta.9",
44
"description": "Scratch Auth is a simple OAuth service for Scratch. It provides developers with an easy-to-understand API and users with a smooth login experience. By using @scratch-auth/nextjs, you can easily implement OAuth functionality into your site.",
55
"main": "./src/index.d.ts",
66
"scripts": {
@@ -21,6 +21,12 @@
2121
},
2222
"homepage": "https://scratch-auth.netlify.app",
2323
"license": "MIT",
24+
"keywords": [
25+
"scratch",
26+
"oauth",
27+
"authentication",
28+
"nextjs"
29+
],
2430
"dependencies": {
2531
"@radix-ui/react-avatar": "^1.1.1",
2632
"@radix-ui/react-dropdown-menu": "^2.1.2",

packages/nextjs/src/action.ts

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import crypto from "crypto";
44
import { v4 as uuidv4 } from "uuid";
5-
import { deleteCookie, getCookie, setCookie } from "./cookie/cookie.server";
65
import pkgConfig from "./pkgConfig";
6+
import { cookies } from "next/headers";
7+
import { deleteCookie } from "./cookie/cookie.server";
78

89
const secretKey = process.env.SCRATCH_AUTH_SECRET_KEY!;
910
if (!secretKey) {
@@ -20,30 +21,46 @@ export async function ScratchAuthCalculateHmac(text: string): Promise<string> {
2021
return crypto.createHmac("sha256", secretKey).update(text).digest("hex");
2122
}
2223

23-
// AES暗号化を使って文字列を暗号化
24+
// AES-GCMを使って文字列を暗号化
2425
export async function ScratchAuthEncrypt(text: string): Promise<string> {
25-
const iv = crypto.randomBytes(16); // 初期化ベクトル
26+
const iv = crypto.randomBytes(12);
2627
const cipher = crypto.createCipheriv(
27-
"aes-256-cbc",
28+
"aes-256-gcm",
2829
Buffer.from(secretKey, "hex"),
2930
iv
3031
);
32+
3133
let encrypted = cipher.update(text, "utf8", "hex");
3234
encrypted += cipher.final("hex");
33-
return iv.toString("hex") + ":" + encrypted; // IVと暗号文を結合
35+
36+
const authTag = cipher.getAuthTag().toString("hex");
37+
38+
return `${iv.toString("hex")}:${encrypted}:${authTag}`;
3439
}
3540

36-
// AES復号化を使って文字列を復号化
37-
export async function ScratchAuthDecrypt(text: string): Promise<string> {
38-
const [iv, encrypted] = text.split(":");
39-
const decipher = crypto.createDecipheriv(
40-
"aes-256-cbc",
41-
Buffer.from(secretKey, "hex"),
42-
Buffer.from(iv, "hex")
43-
);
44-
let decrypted = decipher.update(encrypted, "hex", "utf8");
45-
decrypted += decipher.final("utf8");
46-
return decrypted;
41+
// AES-GCMを使って文字列を復号化
42+
export async function ScratchAuthDecrypt(text: string): Promise<string | null> {
43+
try {
44+
const [iv, encrypted, authTag] = text.split(":");
45+
const decipher = crypto.createDecipheriv(
46+
"aes-256-gcm",
47+
Buffer.from(secretKey, "hex"),
48+
Buffer.from(iv, "hex")
49+
);
50+
51+
decipher.setAuthTag(Buffer.from(authTag, "hex"));
52+
53+
let decrypted = decipher.update(encrypted, "hex", "utf8");
54+
decrypted += decipher.final("utf8");
55+
56+
return decrypted;
57+
} catch (error) {
58+
if (pkgConfig.debug) {
59+
console.warn("Decryption failed:", error);
60+
}
61+
await deleteCookie(pkgConfig.COOKIE_NAME);
62+
return null;
63+
}
4764
}
4865

4966
// トークンの検証
@@ -76,16 +93,16 @@ export async function setScratchAuthEncryptedData(
7693
value: string,
7794
days: number
7895
): Promise<void> {
79-
const hmac = await ScratchAuthCalculateHmac(value); // HMACを計算
80-
const encryptedValue = await ScratchAuthEncrypt(value + "|" + hmac); // データとHMACを結合し、暗号化
96+
const hmac = await ScratchAuthCalculateHmac(value);
97+
const encryptedValue = await ScratchAuthEncrypt(value + "|" + hmac);
8198
const expires = new Date();
8299
if (days === -1) {
83-
expires.setFullYear(expires.getFullYear() + 200); // 有効期限を200年後に設定
100+
expires.setFullYear(expires.getFullYear() + 200);
84101
} else {
85-
expires.setDate(expires.getDate() + days); // 指定日数後に期限切れに
102+
expires.setDate(expires.getDate() + days);
86103
}
87104

88-
await setCookie({
105+
cookies().set({
89106
name: content,
90107
value: encryptedValue,
91108
expires: expires,
@@ -97,40 +114,45 @@ export async function setScratchAuthEncryptedData(
97114
export async function getScratchAuthDecryptedSessionId(
98115
content: string
99116
): Promise<string | null> {
100-
const encryptedValue = await getCookie(content); // Cookieから値を取得
117+
const encryptedValue = cookies().get(content);
101118
if (encryptedValue) {
102-
try {
103-
const decryptedValue = await ScratchAuthDecrypt(encryptedValue.value); // 復号化を試みる
104-
const [sessionId, hmac] = decryptedValue.split("|"); // セッションIDとHMACに分割
105-
const calculatedHmac = await ScratchAuthCalculateHmac(sessionId); // HMACを再計算
119+
const decryptedValue = await ScratchAuthDecrypt(encryptedValue.value);
120+
if (decryptedValue) {
121+
const [sessionId, hmac] = decryptedValue.split("|");
122+
const calculatedHmac = await ScratchAuthCalculateHmac(sessionId);
106123
if (calculatedHmac === hmac) {
107-
return sessionId; // 検証が成功した場合はセッションIDを返す
124+
return sessionId;
108125
} else {
109-
console.warn("HMAC does not match. Deleting cookie.");
110-
await deleteCookie(content); // HMACが一致しない場合、Cookieを削除
126+
if (pkgConfig.debug) {
127+
console.warn("HMAC does not match. Deleting cookie.");
128+
}
129+
await deleteCookie(content);
130+
}
131+
} else {
132+
if (pkgConfig.debug) {
133+
console.warn("Decryption failed. Deleting cookie.");
111134
}
112-
} catch (error) {
113-
console.error("Error during decryption:", error);
114-
await deleteCookie(content); // 復号化に失敗した場合、Cookieを削除
115135
}
116136
}
117137
return null;
118138
}
119139

120-
// ユーザー名とIVを取得する新しい関数を作成
140+
// ユーザー名とIVを取得する関数
121141
export async function getScratchAuthUserName(
122142
content: string
123143
): Promise<string | null> {
124-
const encryptedValue = await getCookie(content); // Cookieから値を取得
144+
const encryptedValue = cookies().get(content);
125145
if (encryptedValue) {
126-
try {
127-
const decrypted = await ScratchAuthDecrypt(encryptedValue.value); // 復号化
128-
const [username] = decrypted.split("|"); // ユーザー名を取得
129-
return username; // ユーザー名を返す
130-
} catch (error) {
131-
console.error("Error during decryption:", error);
132-
await deleteCookie(content); // 復号化に失敗した場合、Cookieを削除
146+
const decrypted = await ScratchAuthDecrypt(encryptedValue.value);
147+
if (decrypted) {
148+
const [username] = decrypted.split("|");
149+
return username;
150+
} else {
151+
if (pkgConfig.debug) {
152+
console.warn("Decryption failed. Deleting cookie.");
153+
}
133154
}
134155
}
156+
await deleteCookie(content);
135157
return null;
136158
}

0 commit comments

Comments
 (0)