概要
DiscordなどでイーサリアムのNFTを所有している人だけが入室できるチャンネルなどがあります。チャンネル制御といえばCollab.Landが有名ですが、サポートしているトークンはここで紹介されています。
Symbolがありませんね。それでは作ってしまいましょう。
このブログを参考にさせていただきましたので、詳細はこちらからどうぞ。
ブログではNFTの所有を条件としていますが、この記事では汎用性を考えて秘密鍵を持っていることの証明で認証する手法を紹介します。秘密鍵を知っていることさえ確認できれば、ユーザーがNFTを所有していることはサービス提供側で任意のノードに接続して自由に調べることができます(加えて、Symbolブロックチェーンなら、検証も含めてかなり柔軟にアカウント情報へのアクセスが可能ですが、これはまた別の機会に)。また、専用のウォレットを準備するとハッキングに悪用される可能性もあるため、署名にはSSS(ブラウザ拡張機能)、aLice(Android、iOSアプリ)を使用します。
また、上記ブログの内容について、少し補足しておきます。
生成するURLは2種類
OAuthのURL Generatorで生成するURLは、管理者が使用するbotの生成のためのURL(SCOPES=bot)と、ユーザーが使用するURL(SCOPES=identify)と2種類あります。管理者が使用するURLはbotを作ってしまえば再び使用することはありません。ユーザーが使用するURLは、ユーザーが承認を求める度に使用されることになります。一度承認すればユーザーに権限が付与されるので、ログインごとの認証はDiscord側がよろしくやってくれます。
開発者モード
かなり見つけにくいところにあります。このモードがONになっていなくても機能することは確認できましたが、各ID値を調べるのが面倒になるのでONにしておいた方が無難でしょう。
サーバー設定ではなく「サーバープロフィールを編集」から「アプリの設定」→「詳細設定」を選択すると見つかります。
response_type = token
URL Generatorで生成されたURLのresponse_typeをcodeからtokenに変更するとアクセストークンを直接取得することができますが、これはかなり生存期間の長いトークンなのでブラウザからの問い合わせに使用する場合はcodeのままで、アクセストークンはサーバー間通信のみでやり取りした方がよいように思います。
Discordの設定
リンクの作成
ユーザーにアクセスしてもらうURLは以下のようになります。
https://discord.com/api/oauth2/authorize?client_id=****&response_type=code&redirect_uri=https%3A%2F%***.jp%2Fdiscord%2F&scope=identify&state=*******
このURLにアクセスすると、ユーザー承認画面の後、redirect_uri
に指定されたページでCODEが送信されます。
SSSの場合はCODEを暗号化する必要があるので、サービス提供者側の公開鍵を以下のようにURLの最後に付与しておきます。
&state={サービス提供者の公開鍵}
HTML
DiscordからCODE情報を受け取る画面です。
SSSで署名ボタンとaLiceで署名ボタンを設置します。
この画面でSSSやaLiceを呼び出し、暗号化OR署名したメッセージをサービス側で準備したNodeJSへ送信します。
SSSではsetMessage
での暗号化、aLiceではrequest_sign_utf8
での署名を使用して、秘密鍵を知っていることを証明します。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<h1>Discord Test</h1>
<button id="sss">SSSで署名</button>
<button id="alice">aLiceで署名</button>
<script>
const params = new URLSearchParams(document.location.search.substring(1));
const code = params.get("code");
const verifierPublicKey = params.get("state"); //for sss
// ボタン要素を取得
var sssButton = document.getElementById("sss");
var aliceButton = document.getElementById("alice");
// ボタンがクリックされたときの処理を定義
sssButton.addEventListener("click", function() {
window.SSS.setMessage(code, verifierPublicKey); //相手先の公開鍵
window.SSS.requestSignEncription().then((msg) => {
const payload = {
encryptedCode : msg.payload,
signerPublicKey : window.SSS.activePublicKey,
}
const method = "POST";
const requestOptions = {
method,
headers: {
'Content-Type': 'application/json' // リクエストヘッダーに適切なContent-Typeを指定します
},
body:JSON.stringify(payload)
};
fetch("https://***.jp/node/sss/", requestOptions)
.then(response => {
console.log(response)
});
});
});
aliceButton.addEventListener("click", function() {
const byteArray = encoder.encode(code);
const hexedCode = Array.from(encoder.encode(code))
.map(byte => byte.toString(16)
.padStart(2, '0')).join('').toUpperCase();
const url = Array.from(encoder.encode("https://***.jp/node/alice/")).map(byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase();
location.href = `alice://sign?type=request_sign_utf8&data=${hexedCode}&callback=${url}`;
});
function toHex(str){
const encoder = new TextEncoder('utf-8');
return Array.from(encoder.encode(str)).map(byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase();
}
</script>
<script>
</script>
</body>
</html>
細かい仕様は各アプリのドキュメントを参考してください。
Node.js
署名OR暗号化メッセージを受け取るサービスです。
manageRole権限を持つBOTのclientを生成します。addRolesで権限を付与します。
各ID値は先述のブログより調べて設定してください。
discord.js
import { Client, GatewayIntentBits } from 'discord.js';
import fetch from "node-fetch";
import symbolSdk from "symbol-sdk";
const BOT_TOKEN = '*****';
const SERVER_ID = '*****';
const ROLES_ID = '*****';
const CLIENT_ID = '*****';
const CLIENT_SECRET = '*****';
const client = new Client({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
] });
client.on('ready', async () => {
console.log(`Logged in as ${client.user.tag}!`);
const guild = await client.guilds.fetch(SERVER_ID);
const rols = await guild.roles.fetch();
rols.forEach(role => {
console.log(role.name + ' : ' + role.id);//idが確認できる
})
});
client.login(BOT_TOKEN);
function addRoles(code){
const params = new URLSearchParams();
params.append('client_id', CLIENT_ID);
params.append('client_secret', CLIENT_SECRET);
params.append('grant_type', 'authorization_code');
params.append('redirect_uri', `https://***.jp/discord/`);
params.append('code', code);
const response = await fetch('https://discord.com/api/oauth2/token', { method: "POST", body: params })
const data = await response.json();
const usersRes = await fetch('https://discordapp.com/api/users/@me',
{
method: 'GET',
headers: {
'Authorization': 'Bearer ' + data.access_token,
'Content-Type': 'application/json',
}
}
)
const usersData = await usersRes.json();
const guild = await client.guilds.fetch(SERVER_ID);
const role = await guild.roles.fetch(ROLES_ID);
const member = await guild.members.fetch(usersData.id);
const memberRes = await member.roles.add(role);
return JSON.stringify(memberRes);
}
addRoles
受信したメッセージを以下のURLにPOSTすることでアクセストークンを入手できます。
https://discord.com/api/oauth2/token
次にアクセストークンを以下のURLにGET送信することでユーザー情報を入手できます。
https://discordapp.com/api/users/@me
ユーザー情報を入手することができれば、manageRole権限のあるBOTで対象ユーザーに権限を与えることができます。
SSS
SSSの場合は暗号化されたコードが送信されるため、コードを復号して使用します。
復号に成功することで、ユーザ認証も同時に検証することが可能です。
app.post('/sss/',async function(req, res){
const encryptedCode = req.body.encryptedCode;
const signerPublicKey = req.body.signerPublicKey;
//アカウント設定
const verifierAccount = new symbolSdk.symbol.KeyPair(new symbolSdk.PrivateKey("*****"));
const signerPublicAccount = new symbolSdk.symbol.PublicKey(Uint8Array.from(Buffer.from(signerPublicKey, "hex")));
const verifierMsgEncoder = new symbolSdk.symbol.MessageEncoder(verifierAccount);
const decryptMessageData = verifierMsgEncoder.tryDecode(signerPublicAccount.publicKey, Uint8Array.from(Buffer.from("01" + encryptedCode, "hex")));
if (decryptMessageData.isDecoded) {
const decryptMessage = new TextDecoder().decode(decryptMessageData.message);
const memberRes = addRoles(decryptMessage);
res.send(memberRes);
} else {
console.log("decrypt failed!");
}
});
decryptMessageData.isDecoded
この値がtrueだった場合は、認証成功ですのでsymbol-sdkなどを使用してsignerPublicKey
の所有するNFTを調べに行き、条件を満たしていれば後続のaddRoles(decryptMessage)を実行します。
aLice
aLiceの場合はコードの署名が送信されるため、検証して使用します。
app.get('/alice/',async function(req, res){
const encryptedCode = req.query.original_data;
const signerPublicKey = req.query.pubkey;
const signature = new symbolSdk.Signature(req.query.signature);
const byteArray = Buffer.from(encryptedCode, 'hex');
const decodedString = byteArray.toString('utf-8');
const publicKey = new symbolSdk.symbol.PublicKey(Uint8Array.from(Buffer.from(signerPublicKey, "hex")));
const v = new symbolSdk.symbol.Verifier(publicKey);
const isVerified = v.verify(Buffer.from(decodedString, 'utf-8'), signature);
if (isVerified) {
const memberRes = addRoles(decodedString);
res.send(memberRes);
} else {
console.log("verify failed!");
}
});
isVerified
この値がtrueだった場合は、認証成功ですのでsymbol-sdkなどを使用してsignerPublicKey
の所有するNFTを調べに行き、条件を満たしていれば後続のaddRoles(decodedString)を実行します。
まとめ
最後までは紹介しませんでしたが、これでSymbolでもトークンの所有を条件としたコミュニティスペースの提供が可能になりました。この方法はDiscord専用ではなく、OAuth2.0を提供しているあらゆるサービスで応用可能です。
また、SSSやaLiceなどコミュニティによって十分検証されたツールを用いることによって、秘密鍵が漏洩してしまう可能性も低くなります。またSymbolではマルチシグに関する機能が充実しており、複数のウォレットを使用すれば多要素認証のように秘密鍵を複数端末で分散管理することが可能です。
さらに本記事での手法はトークンの所有と限定しなかったことにより、様々な用途に使用できます。例えばメタデータ上に記録されたDIDであったり、レンタル期間中のネームスペースの所有であったり、あるいは指定トークンをロック中であるかどうかなど、ぜひブロックチェーンの可能性を広げるアイデアにご利用ください。
さいごに
弊社ではSymbolブロックチェーンを活用したNFTやDID、トレーサビリティなどさまざまな調査研究を行っております。 ブロックチェーンを活用した社会実装・実証実験・技術教育などお気軽にご相談ください。