ken_nogi/dbbackup/lib/lineworksAuth.js
Kenichiro NOGI 67cc3da6c8 up
2026-07-13 19:02:04 +09:00

116 lines
4.2 KiB
JavaScript

"use strict";
/*
* LINE WORKS Service AccountのJWT自己署名 → OAuth2アクセストークン取得。
* get-token.js (CLI、掲示板等のService Account対応APIの動作確認用) が使用する。
* Drive/共有ドライブAPIはService Account認証に非対応のため、backup.jsは
* 代わりに lib/lineworksUserAuth.js (User Account認証) を使用する。
* ロジックは元々 get-token.js / lineworks-anythingllm.js にあったものを移設。
*/
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const LW_TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
function resolvePrivateKeyFile() {
return process.env.LW_PRIVATE_KEY_FILE
? path.resolve(process.env.LW_PRIVATE_KEY_FILE)
: path.join(__dirname, "..", "private_20260307184804.key");
}
function resolvePrivateKey() {
if (process.env.LW_PRIVATE_KEY && process.env.LW_PRIVATE_KEY.trim()) {
return process.env.LW_PRIVATE_KEY;
}
const file = resolvePrivateKeyFile();
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
}
function base64UrlEncode(value) {
return Buffer.from(value)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function createJwtAssertion({ clientId, serviceAccount, privateKey }) {
const now = Math.floor(Date.now() / 1000);
const header = { alg: "RS256", typ: "JWT" };
const payload = {
iss: clientId,
sub: serviceAccount,
aud: LW_TOKEN_URL,
iat: now,
exp: now + 300,
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signingInput = `${encodedHeader}.${encodedPayload}`;
const signer = crypto.createSign("RSA-SHA256");
signer.update(signingInput);
signer.end();
const signature = signer
.sign(privateKey)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
return `${signingInput}.${signature}`;
}
// clientId/clientSecret/serviceAccount にコード側のデフォルト値は持たない。
// 必ず .env(または呼び出し元が明示的に渡すoptions)から取得し、無ければ即座にエラーとする。
// scope は呼び出し元(get-token.js / backup.js)ごとに用途が異なるため必須の引数として扱う。
async function getAccessToken(options = {}) {
const clientId = options.clientId || process.env.LW_CLIENT_ID;
const clientSecret = options.clientSecret || process.env.LW_CLIENT_SECRET;
const serviceAccount = options.serviceAccount || process.env.LW_SERVICE_ACCOUNT;
const scope = options.scope !== undefined ? options.scope : process.env.LW_SCOPE;
const privateKey = options.privateKey || resolvePrivateKey();
const missing = [];
if (!clientId) missing.push("LW_CLIENT_ID");
if (!clientSecret) missing.push("LW_CLIENT_SECRET");
if (!serviceAccount) missing.push("LW_SERVICE_ACCOUNT");
if (!privateKey) missing.push(`LW_PRIVATE_KEY または LW_PRIVATE_KEY_FILE (${resolvePrivateKeyFile()} が存在しません)`);
if (missing.length > 0) {
throw new Error(`環境変数が不足しています: ${missing.join(", ")}`);
}
const assertion = createJwtAssertion({ clientId, serviceAccount, privateKey });
const form = new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion,
client_id: clientId,
client_secret: clientSecret,
scope: scope || "",
});
const response = await fetch(LW_TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form,
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`アクセストークン取得失敗: ${response.status} ${detail}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error("アクセストークン取得失敗: access_token が返却されませんでした");
}
return data.access_token;
}
module.exports = { getAccessToken, resolvePrivateKeyFile, resolvePrivateKey };