node.jsからMS Authenticatorをいろいろ制御してみる

## はじめに
　VC(verifiable credential)を試してみようと考えて、スマホの中に入っているアプリの中で一番手軽そうにみえた[Microsoft Authenticator](https://www.microsoft.com/ja-jp/security/mobile-authenticator-app)に手を出したというのが、いじるきっかけ。またMS Authenticatorを取り扱っている記事が多くあるのも魅力の１つ。例えば以下のサイトでは、VCを発行するやりとりが詳しく解説されている。
- [Microsoft Entra Verified ID を使ってみた（②発行編）](https://qiita.com/ToshihiroNozaki/items/ab493131998bcac837c6)

　ただ多くの記事にあるように、AzureのApp serviceでmy webサーバを立ち上げたり、.netを使ってVCを発行するアプリを作るのは、結構敷居が高い。
 
　簡単に試す事ができるように、nodejsだけでms autheticatorにvcを発行できるようコードを書き起こした。ちょうど下の図にあるAPI群をもったコードである。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/a6fb29eb-e5bc-4ec4-2c9d-fb2aab1ba552.png)

## いじってみた結果
- パターンA: 何もいじらない。
   - 「__追加ボタンを押すとVCがスマホに入る__」
- パターンB: ④vc issue requestにpinというフィールドを入れる。
  - 「__確認コードボタンが表示され、入力したあとサイトAでのPIN検証後に、VCがスマホに入る__」
- パターンC: ⑥manifestをいじって、"idTokens"ではなく"accessTokens"にする。
  - 「__アカウントにサインインボタンが表示され、サインイン後Azure directoryの認証結果を取得し、その結果をサイトAで検証した後にVCが追加される__」
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624321/40d0bce8-2661-6e2b-bd85-b9c1a6820d96.png)

### 1. いじった箇所(パターンB)
　vc issuer requestにpinフィールドを設けることで、MS authenticatorにpin入力を促すことができる。仕様は[https://learn.microsoft.com/en-us/entra/verified-id/issuance-request-api](https://learn.microsoft.com/en-us/entra/verified-id/issuance-request-api)に公開されているが、あまり詳しくない。実際には仕様にないhashなるものが送られていたりする。(<small>※ただ調査してみると、hashはPIN認証の際に使われていないので、実質仕様どおりかもしれない。</small>)

``` vc issue requestのpayload部
  {
    "jti":uuid.v4().toUpperCase(),
    ...
    "claims":{},
    "id_token_hint":hint_jwt
    "pin":{"length":6,"type":"numeric","alg":"sha256","iterations":1,"salt":xxx, "hash":zzz}
    ...
  };
```
ここでrequestすると、MS authentocatorはユーザが入力したpinの値をもとに base64( sha256(salt+pin) )を計算し、⑦の処理の際に、その値をissue/..に返してくる。

### 2. いじった箇所(パターンC)
1. manifestをいじる前に、[A guided tour of Microsoft Entra Verified ID](https://www.microsoft.com/en-us/download/details.aspx?id=104494)にあるように、Azure上にtenantを作って、そこにユーザを作り、そのユーザとMS authenticatorを結びつけておく必要がある。
  - create your Azure AD tenant(p23)
  - create a test user(p25)
  - Set up the user for Microsft Authenticator(p26)

結びつけられたら、そのtenant idを```const directory="https://login.microsoftonline.com/<tenant id>/v2.0"```に設定する。
2. あとは送信するmanifestをidTokensからaccessTokensに変更する。

- 変更前のmanifest
``` 
{
    ...
    "display": {
      "locale": "en-US",
      ...
      },
      "consent": {},
      "claims": {},
      },
      "id": "display"
    },
    "input": {
      "issuer": DID,
      "id": "input",
      attestations:{
        "idTokens": [{
          "id": "https://self-issued.me",
          "encrypted": false,
          "claims": [{..},{..}],
          "configuration": "https://self-issued.me",
          ...
        }]
     }
    }
} 
```
- MS authenticatorにサインインを促すために、menifestに変更を加える部分
```
 attestations = {
  "accessTokens": [{
    "id": directory,
     "encrypted": false,
     "claims": [],
     "required": true,
      "configuration": directory,
      "resourceId": "bb2a64ee-5d29-4b07-a491-25806dc854d3",
      "oboScope": "User.Read.All" 
  }]
 };
```
- この変更を加えて、manifestを送ると、⑦でM authenticatorから入力があるときに、Azure DirectoryからのTokenをもらうことができる(※この値で、発行しようとしているVC内容のダブルチェックも可能)

```
{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "T1St-dLTvyWRgxB_676u8krXS-I",
  "kid": "T1St-dLTvyWRgxB_676u8krXS-I"
}.{
  "aud": "bb2a64ee-5d29-4b07-a491-25806dc854d3",
  "iss": "https://sts.windows.net/<tenant id>/",
  "iat": 1701503608,
  "nbf": 1701503608,
  "exp": 1701508690,
  "acr": "1",
  "aio": "ATQAy/8VAAAAcJbmKE6C0R6TG2Fk7KpIMk+pah+RFU0Vd3QZWthaXOtyC+n6aAVkEuDb7gdbzvIY",
  "amr": [
    "pwd"
  ],
  "appid": "4813382a-8fa7-425e-ab75-3b753aab3abb",
  "appidacr": "0",
  "family_name": "afo afo man",
  "given_name": "hoge hoge",
  "ipaddr": "180.53.77.202",
  "name": "Test User 01",
  "oid": "7194b49b-8a45-4dac-80f7-4dc67dae8892",
  "puid": "100320031DE1E391",
  "rh": "0.AWoAJ1cUGcbFuEWSo2otvxZB--5kKrspXQdLpJElgG3IVNNqAEk.",
  "scp": "user_impersonation",
  "sub": "87sg7Rre57ZujnBSn4i5OpRV4tN6fzss-Q6vn90JMB8",
  "tid": "<tenant id>",
  "unique_name": "testuser1@xxx.onmicrosoft.com",
  "upn": "testuser1@xxx.onmicrosoft.com",
  "uti": "-gO-2Ta2f0CswFcXtscCAA",
  "ver": "1.0"
}.signature

```
## 動作確認
#### 1. index.html, server.js, package.jsを以下のフォルダ構成のように配置する
 ```
├── server.js
├── package.json
└── views/
    ├── index.html
```
<details>
<summary>index.htmlのコードを見る</summary>

``` index.html
<!DOCTYPE html>
<html lang='en'>

<head><meta charset='utf-8'></head>
<body>
  <div align=center>
    <div id="title">[ngrok registration]</div>
    <div id="parameter">given name:<input type="text" id="given" value="hogehoge"><br>
    family name:<input type="text" id="family" value="deadbeaf"><br>
    <input type="button" value="発行" onclick="issuerQR()"/><br>
    <p><div id=qrmsg></div>
    <p><div id=qr></div>
  </div>

</body>
<script type="text/javascript">

  function getData(url, type, cb){
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
      switch(xhr.readyState){
        case 4:
          if (xhr.status == 200||xhr.status ==304){
            cb(xhr.response)
          }
      }
    };
    xhr.open("GET", url, false);
    xhr.setRequestHeader('Content-Type', type);
    xhr.send('');
  }

  function sendData(url, data, type, cb){
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
      switch(xhr.readyState){
        case 4:
          if (xhr.status == 200||xhr.status ==304){
            console.log("send success");
            cb(xhr.response)
          }
      }
    };
    xhr.open("POST", url, false);
    xhr.setRequestHeader('Content-Type', type);
    xhr.send(data);
  }

  function issuerQR() {
    let setting={
      "clientName": "ngrok registration",
      "type": "ngrokVerifiedCredential",
      "claims": {"lastName": document.getElementById('family').value, "firstName": document.getElementById('given').value}
    }
    sendData("./.issuer_init", JSON.stringify(setting), 'application/json', 
      function(data){
        target = document.getElementById("qr");
        target.innerHTML = "<img src="+JSON.parse(data).qrcode+" />";
        let interval_id = setInterval(
          function(){
            console.log("request check status");
            istatusCheck(interval_id, JSON.parse(data).sess);
          }, 3000);
      }
    );
  }

  function istatusCheck(clearid, sess){
    getData("./.istatus/"+sess, 'application/json', 
      function(data){
        console.log("status:"+JSON.parse(data).status);
        if (JSON.parse(data).status == "request_retrieved"){
          let msg = "QR code sccand. waiting..";
          if (typeof JSON.parse(data).pin !=="undefined"){ msg +="PIN:"+JSON.parse(data).pin;}
          document.getElementById("qrmsg").innerHTML =msg;
        }else if(JSON.parse(data).status == "issuance_successful"){
          document.getElementById("qrmsg").innerHTML = "finish";
          document.getElementById("qr").innerHTML = "";
          clearInterval(clearid);
        }
      }
    );
  }

  </script>
</html>
```
</details>
<details>
<summary>server.jsのコードを見る</summary>

``` server.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const http = require('http');
const QRCode = require('qrcode');
const crypto = require('crypto');
const elliptic = require('elliptic');
const didJWT = require('did-jwt');
const fs = require('fs');
const uuid = require('uuid');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use('/', express.static(__dirname + '/views/'));
app.use(bodyParser.text({type: '*/*'}));
app.use(bodyParser.raw({type: '*/*'}));

//*******setting********
const port=8888;
let hostname = '127.0.0.1'+":"+port;
const pinenable=false;
const method="self" // "attest"
if (process.argv.length > 2){ hostname = process.argv[2];}
// authentication by azure directory
const directory = "https://login.microsoftonline.com/<tenant id>/v2.0";
//**********************

//*******global variable*******
let DID="did:web:"+hostname;
let task= new Array();
let taskstatus= new Array();
let storageJson=[];
let didweb, didwebkey;
const main = async()=>{
  didwebkey = await init(hostname);
  didweb = didjson(DID, didwebkey, hostname)
}
main();
//******************************

//*******web server*******
const httpServer = http.createServer(app);
const server = httpServer.listen(port, () => {
  console.log('\n web page start: listening on port %s...', server.address().port);
});

app.get('/.*', async function(req,res){
  console.log("\n====GET====");
  console.log("cmd:"+JSON.stringify(req.params));
  console.log("====GET====");

  let ret="";
  let cmd = req.params[0].split('/');

  if (cmd[0] =="well-known" && cmd[1] == "did.json"){
    console.log("access to did.json")
    res.send(didweb);

  } else if (cmd[0] =="well-known" && cmd[1] == "did-configuration.json"){
    console.log("access to did-configuration.json")
    res.send(await makedidconfig());

  } else if (cmd[0] == "issuanceRequests"){
    console.log("start session:"+cmd[1]+", issuanceRequests")
    taskstatus[cmd[1]].vcstatus = "request_retrieved";
    res.send(await makevcrequest(task[cmd[1]]));

  } else if(cmd[0] == "contracts" && cmd[2] == "manifest"){
    console.log("manifest for :"+cmd[1]);
    res.send(await makemanifest(task[cmd[1]],method));

  } else if (cmd[0] =="istatus"){
    ret={"status":taskstatus[cmd[1]].vcstatus};
    if (taskstatus[cmd[1]].vcstatus =="request_retrieved"){
      if (pinenable && (method !="attest")){
         ret.pin=task[cmd[1]].pin;
      }
    }
    res.send(JSON.stringify(ret));

  }else{
    res.send(ret);
  }
});

app.post('/.*', async function(req,res){
  console.log("\n====POST====");
  console.log("cmd:"+JSON.stringify(req.params));
  console.log("====POST====");

  let ret="";
  let cmd = req.params[0].split('/');

  if (cmd[0] =="issuer_init"){
    let [redirecturi, sessNo] = await issuerInit(req.body);
    let inviteUrl = "openid-vc://?request_uri=" + redirecturi;
    QRCode.toDataURL(inviteUrl, function (err, url) {
      let sendData = {"qrcode":url, "sess":sessNo, "url":redirecturi};
      res.send(sendData);
    });

  }else if (cmd[0] == "issue"){
    console.log("post from ms authenticator:")
    let decoded = didJWT.decodeJWT(req.body);
    let token;
    if (method == "attest"){
      token = decoded.payload.attestations.accessTokens[directory]
    }else{
      token = decoded.payload.attestations.idTokens["https://self-issued.me"]
    }
    let sess = cmd[1];
    if ((pinenable && decoded.payload.pin == task[sess].extpin) || (method=="attest")||!pinenable){
      let ret = await createVc(decoded, task[sess]);
      if (task[sess].iss=="did"){
        await saveDid(task[sess].issdid, task[sess].isskey);
      }
      res.send(JSON.stringify(ret));
    }else{
      console.log("pin is wrong, return error!");
      let sendData = {
        "requestId":"","date":"","mscv":"",
        "error":{
        "code":"unauthorized",
        "message":"The requested resource requires authentication",
          "innererror":{
            "code":"Unauthorized",
            "message":"An unhandled error has occurred verifying a token."
          }
        }
      };
       res.send(JSON.stringify(sendData));
    }
  }else if (cmd[0] == "completeIssuance"){
    // server   <--- ms authenticator
    if (req.body.code == 'issuance_successful'){
      console.log("vc issue is success");
    }else{
      console.log("vc issue is failed");
    }
    console.log("state:"+req.body.state);
    taskstatus[req.body.state].vcstatus="issuance_successful";
    res.status(202).send('OK')
  }else{
    res.send(null);
  }
});


//////////////////////
//  support function
///////////////////////

async function init(_hostname){
  let _did = "did:web:"+_hostname;
  let _key = await loadDid(_did);
  if (_key ==null){
    console.log("\n this "+_did+"'s key not registered")
    let [_key, , _document] = await createDid("web", _hostname);
    await saveDid(_did, _key);
    return _key;
  }else{
    console.log("\n this "+_did+"'s key is registered")
    return _key;
  }
}

async function loadDid(_did){
  let key = null;
  try{
    storageJson = JSON.parse(fs.readFileSync('./storage.json'));
    storageJson.forEach((item) =>{
       if (item.did == _did) key = item.key
    })
    return key;
  }catch(err){
    return null;
  }
}

async function saveDid(_did, _key){
  storageJson.push({did:_did, key:_key})
  try{
    fs.writeFileSync("./storage.json", JSON.stringify(storageJson))
  }catch(err){
    console.error(err);
  }
}

async function createDid(_method, _hostname){
  const key = crypto.randomBytes(32).toString("hex");
  const ec = new elliptic.ec('secp256k1');
  const prv = ec.keyFromPrivate(key, 'hex');
  const pub = prv.getPublic();
  const ecjwk = {
    kty:"EC", 
    crv:"secp256k1", 
    "x":pub.x.toBuffer().toString('base64'),
    "y":pub.y.toBuffer().toString('base64')
  };
  const ecjwkpri = {
    kty:"EC", 
    crv:"secp256k1", 
    "x":pub.x.toBuffer().toString('base64'),
    "y":pub.y.toBuffer().toString('base64'),
    "d":prv.getPrivate().toBuffer().toString('base64')
  };
  console.log("--------generate key for did -------------------")
  console.log(` d : ${prv.getPrivate().toBuffer().toString('base64')}`);
  console.log(` x (b64): ${pub.x.toBuffer().toString('base64')}`);
  console.log(` y (b64): ${pub.y.toBuffer().toString('base64')}`);
  console.log("-----------------------------\n")
  let _key={"publicJwk":ecjwk,"privateJwk":ecjwkpri};
  let _document
  let _did
  if (_method == "web"){
    // web method
    _did = "did:web"+_hostname;
    _document =didjson(_did, _key, _hostname);
    console.log("-> "+JSON.stringify(_document));
  }
  return [_key, _did, _document];
}

function createSigner(_method, _key){
  let _signer = didJWT.ES256KSigner(didJWT.hexToBytes(Buffer.from(didwebkey.privateJwk.d, 'base64').toString('hex')))
  return _signer;
}


/////////////////////////
//   did:web
/////////////////////////
function didjson(_did, _key, _hostname){
  let _document = {
    "id":"",
    "@context":["https://www.w3.org/ns/did/v1",{"@base":""}],
    "service":[
      {"id":"#linkeddomains","type":"LinkedDomains","serviceEndpoint":{"origins":""}}
    ],
    "verificationMethod":[
      {"id":"#owner","controller":"",
        "type":"EcdsaSecp256k1VerificationKey2019",
        "publicKeyJwk":{"crv":"secp256k1","kty":"EC","x":"","y":""}
      }
    ],
    "authentication":["#owner"],"assertionMethod":["#owner"]
  };
  _document.id = _did;
  _document["@context"][1]["@base"]=_did;
  _document.service[0].serviceEndpoint.origins = ["https://"+_hostname+"/"];
  _document.verificationMethod[0].controller = _did;
  _document.verificationMethod[0].publicKeyJwk = _key.publicJwk;
  return _document;
}

async function makedidconfig(){
  let didconfig = {
    "@context":"https://identity.foundation/.well-known/contexts/did-configuration-v0.0.jsonld", 
    "linked_dids":[]
  };
  let configPayload = {
    "sub":didweb.id,
    "iss":didweb.id,
    "nbf":1674647331,
    "vc":{
      "@context":[
        "https://www.w3.org/2018/credentials/v1",
        "https://identity.foundation/.well-known/contexts/did-configuration-v0.0.jsonld"
      ],
      "issuer":didweb.id,
      "issuanceDate":"2023-01-25T11:48:51.998Z",
      "expirationDate":"2048-01-25T11:48:51.998Z",
      "type":["VerifiableCredential","DomainLinkageCredential"],
      "credentialSubject":{"id":didweb.id,"origin":didweb.service[0].serviceEndpoint.origins[0]}
    }
  };

  let signer = didJWT.ES256KSigner(didJWT.hexToBytes(Buffer.from(didwebkey.privateJwk.d, 'base64').toString('hex')))
  const configjwt = await didJWT.createJWT(
    configPayload,
      { issuer:DID,expiresIn:3600,signer },
      { alg: 'ES256K', kid: DID+"#owner" }
  )
  didconfig.linked_dids[0]=configjwt;
  return didconfig;
}

////////////////////////////
//  vc issuer
/////////////////////////////

async function issuerInit(_body){
  let sessNo = crypto.randomBytes(8).toString("hex");
  let serviceEndpoint = "https://"+hostname+"/";

  task[sessNo]= {
    "sess":sessNo,
    "userid":crypto.randomBytes(32).toString("base64"),
    "clientName":_body.clientName,
    "type":_body.type,
    "claims":_body.claims,
    "serviceEndpoint":serviceEndpoint,
    "isskey": didwebkey,
    "issdid": DID
  };

  if (pinenable){
    task[sessNo].pin = Math.random().toString(10).slice(-6);
    task[sessNo].pinsalt = crypto.randomBytes(32).toString("hex");
    task[sessNo].pinhash = "qXp47rTIcPN2rfAz/sCKDAVNgFzKG7bTzVnCjWg5suU=";
    let pinsha256 = crypto.createHash('sha256');
    pinsha256.update(task[sessNo].pinsalt+task[sessNo].pin);
    task[sessNo].extpin = pinsha256.digest('base64');
  }
  let redirecturi = task[sessNo].serviceEndpoint+".issuanceRequests/"+sessNo;
  taskstatus[sessNo]={"vcstatus":"wait"};
  return [redirecturi, sessNo];
}

async function makevcrequest(_param){
  let hintbody ={
    "id":uuid.v4(),
    "sub":_param.userid,
    "aud":_param.serviceEndpoint+".issue/"+_param.sess,
    "nonce":crypto.randomBytes(16).toString("base64"),
    "sub_jwk":{
      "crv":"secp256k1",
      "kid":_param.issdid+"#owner",
      "kty":"EC",
      "x":_param.isskey.publicJwk.x,
      "y":_param.isskey.publicJwk.y
    },
    "did":_param.issdid,
    "type":_param.type,
    "credentialSubject":{
      "lastName":_param.claims.lastName,
      "firstName":_param.claims.firstName
    },
    "iss":"https://self-issued.me",
    "jti":uuid.v4().toUpperCase()
  };
  const signer = createSigner(_param.iss, _param.isskey);
  const hint_jwt = await didJWT.createJWT(
    hintbody,
    { issuer:"https://self-issued.me", expiresIn:3600, signer },
    { alg: 'ES256K', kid: _param.issdid+"#owner" }
  );
  if (pinenable){
    hintbody.pin = {"length":6,"type":"numeric","alg":"sha256","iterations":1,"salt":_param.pinsalt, "hash":_param.pinhash};
  }
  let isbody ={
    "jti":uuid.v4().toUpperCase(),
    "response_type":"id_token",
    "response_mode":"post",
    "scope":"openid",
    "nonce":crypto.randomBytes(16).toString("base64"),
    "client_id":_param.issdid,
    "redirect_uri":_param.serviceEndpoint+".completeIssuance",
    "prompt":"create",
    "state":_param.sess,
    "registration":{
      "client_name":_param.clientName,
      "subject_syntax_types_supported":["did:ion"],
      "vp_formats":{
        "jwt_vp":{"alg":["ES256K"]},"jwt_vc":{"alg":["ES256K"]}
      }
    },
    "claims":{
      "vp_token":{
        "presentation_definition":{
          "id":"b1cb4efd-8e33-482f-8dad-4452cbc2a8d1",
          "input_descriptors":[{
             "id":"Sample",
             "schema":[{"uri":"Sample"}],
             "issuance":[{"manifest":_param.serviceEndpoint+".contracts/"+_param.sess+"/manifest"}]
          }]
        }
      }
    },
    "id_token_hint":hint_jwt
  };
  if (pinenable){
    isbody.pin = {"length":6,"type":"numeric","alg":"sha256","iterations":1,"salt":_param.pinsalt, "hash":_param.pinhash};
  }
  const jwt = await didJWT.createJWT(
    isbody,
    { issuer:_param.issdid+"#owner", expiresIn:3600, signer },
    { alg: 'ES256K', kid: _param.issdid+"#owner" }
  )
  return jwt;
}

async function createVc(_decoded, _param){
  let idToken
  if (typeof _decoded.payload.attestations.idTokens !=="undefined"){
    idToken = _decoded.payload.attestations.idTokens["https://self-issued.me"];
  }else{
    idToken = _decoded.payload.attestations.accessTokens[directory];
  }
  console.log("\n-> idToken:");
  const subject = didJWT.decodeJWT(idToken).payload
  let vcbody =  {
    "jti": _decoded.payload.jti,
    "vc": {
      "@context": ["https://www.w3.org/2018/credentials/v1"],
      "type": ["VerifiableCredential"],
      "credentialSubject": {},
      "exchangeService": {
        "id": _param.serviceEndpoint+".card/exchange",
        "type": "PortableIdentityCardServiceExchange2020"
      },
      "sub": _decoded.header.kid
    }
  };
  if (method=="attest"){
    vcbody.vc.credentialSubject.lastName = subject.family_name;
    vcbody.vc.credentialSubject.firstName = subject.given_name;
  }else{
    vcbody.vc.credentialSubject = subject.credentialSubject;
  }
  vcbody.vc.credentialStatus = await makeCredStatus(vcbody.vc, _param.issdid, new Date().toISOString());
  let signer = createSigner(_param.iss, _param.isskey);
  const jwt = await didJWT.createJWT(
    vcbody,
    { issuer:_param.issdid, expiresIn:3600, signer },
    { alg: 'ES256K', kid: _param.issdid+"#owner" }
  )
  const decoded = didJWT.decodeJWT(jwt)
  console.log('\n//// JWT Decoded:\n',decoded)
  let sendData = {"vc":jwt}
  return sendData;
}

async function makeCredStatus(_vc, _iss, _date){
  let credStatus = {
      "id": "https://xxxx.github.io/credential-status/3BOJ1LAFUS#26",
      "type": "StatusList2021Entry",
      "statusPurpose": "revocation",
      "statusListIndex": 26,
      "statusListCredential": "https://xxxx.github.io/credential-status/3BOJ1LAFUS"
    }
  return credStatus;
}

async function makemanifest(_param, _method){

  let manifestbody = {
    "id": _param.sess,
    "display": {
      "locale": "en-US",
      "contract": _param.serviceEndpoint+".contracts/"+_param.sess+"/manifest",
      "card": {
        "title": "ID card",
        "issuedBy": "ngrok regitration",
        "backgroundColor": "#ffffff",
        "textColor": "#000000",
        "logo": {
          "uri": "https://didcustomerplayground.blob.core.windows.net/public/VerifiedCredentialExpert_icon.png",
          "description": "Verified skill Card"
        },
        "description": "Use your verified credential to prove to anyone that you know all about verifiable credentials."
      },
      "consent": {
        "title": "Do you want to get your Verified Credential?",
        "instructions": "Sign in with your account to get your card."
      },
      "claims": {
        "vc.credentialSubject.firstName": {"type": "String","label": "first Name"},
        "vc.credentialSubject.lastName": {"type": "String","label": "last Name"},
      },
      "id": "display"
    },
    "input": {
      "credentialIssuer": _param.serviceEndpoint+".issue/"+_param.sess,
      "issuer": DID,
      "id": "input"
    }
  };

  if (_method == "attest"){
    manifestbody.input.attestations = {
      "accessTokens": [{
        "id": directory,
        "encrypted": false,
        "claims": [],
        "required": true,
        "configuration": directory,
        "resourceId": "bb2a64ee-5d29-4b07-a491-25806dc854d3",
        "oboScope": "User.Read.All" 
      }]
    };
  }else{
    manifestbody.input.attestations = {
      "idTokens": [{
        "id": "https://self-issued.me",
        "encrypted": false,
        "claims": [{
          "claim": "$.lastName",
          "required": true,
          "indexed": false
        },{
          "claim": "$.firstName",
          "required": true,
          "indexed": false
        }],
        "required": true,
        "configuration": "https://self-issued.me",
        "client_id": "",
        "redirect_uri": ""
      }]
    };
  }
  let signer = createSigner("web", didwebkey);
  const jwt = await didJWT.createJWT(
     manifestbody,
     { issuer:DID, expiresIn:3600, signer },
     { alg: 'ES256K', kid: DID+"#owner" }
  )
  const decoded = didJWT.decodeJWT(jwt)
  return JSON.stringify({"token":jwt})
}

```
</details>

<details>
<summary>package.jsonのコードを見る</summary>

``` package.json
{
  "dependencies": {
    "consolidate": "^0.16.0",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "crypto": "^1.0.1",
    "did-jwt": "^6.11.0",
    "did-resolver": "^4.0.1",
    "elliptic": "^6.5.4",
    "express": "^4.18.2",
    "jose": "^4.11.2",
    "ngrok": "^4.3.3",
    "qrcode": "^1.5.1",
    "web-did-resolver": "^2.0.21"
  }
}

```
</details>

#### 2. 関連モジュールをインストールする
```
> npm install
```
#### 3. サーバに割り当てるドメイン名を取得
```
> ngrok http 8888
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://57dd-180-53-77-xxx.ngrok-free.app -> http://localhost:8888
Forwarding                    https://57dd-180-53-77-xxx.ngrok-free.app -> http://localhost:8888
```
#### 4. 取得したドメイン名を引数に、サーバを起動する
```
> nodejs server.js 57dd-180-53-77-xxx.ngrok-free.app
 
 web page start: listening on port 8888...

 this did:web:57dd-180-53-77-xxx.ngrok-free.app's key not registered
--------generate key for did -------------------
 d : 0tr5cu9T9f6Xz0H6Mwv0gwGM3NxSGTAzcOMcHS/sRnk=
 x (b64): 0G5yn2kRKSsPWcWIPQCX9YFJY0k4tOlucU4XR932VA0=
 y (b64): LCk2FkTpjLpvhnfWGOb+sn0drRof2aJl56VFDp8pook=
-----------------------------

-> {"id":"did:web57dd-180-53-77-xxx.ngrok-free.app","@context":["https://www.w3.org/ns/did/v1",{"@base":"did:web57dd-180-53-77-202.ngrok-free.app"}],"service":[{"id":"#linkeddomains","type":"LinkedDomains","serviceEndpoint":{"origins":["https://57dd-180-53-77-202.ngrok-free.app/"]}}],"verificationMethod":[{"id":"#owner","controller":"did:web57dd-180-53-77-xxx.ngrok-free.app","type":"EcdsaSecp256k1VerificationKey2019","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0G5yn2kRKSsPWcWIPQCX9YFJY0k4tOlucU4XR932VA0=","y":"LCk2FkTpjLpvhnfWGOb+sn0drRof2aJl56VFDp8pook="}}],"authentication":["#owner"],"assertionMethod":["#owner"]}
```
#### 5. ブラウザでhttps://57dd-180-53-77-xxx.ngrok-free.app/にアクセス
1. 画面上にある「発行」ボタンを押してQRコードを表示
1. 表示されたQRコードを、スマホでスキャンするとVC発行の手続きが始まる

#### 6. いじる箇所
1. pinを有効にする場合、server.jsの```const pinenable=false;```を```const pinenable=true;```に
1. ms loginを有効にする場合、server.jsの```const method="self";```を```const method="attest";```に。