116 lines
4.2 KiB
JavaScript
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 };
|