Skip to content

着せ替えアプリとの接続

自社アプリに着せ替えアプリへのリンクを張り、ID連携を実装することで、エンドユーザは着せ替え操作を簡単に行えるようになります。本ページでは、自社アプリがWebアプリケーションであることを想定したリンクの張り方とID連携の実装について説明します。

Info

本ID連携はOAuth認証方式に則っていますが、RESTful API のみを利用している場合はアクセストークンは必要ありません。信頼できる経路で正規ユーザ(着せ替えアプリのユーザ)のアバターIDを取得し、自社アプリのユーザと紐付けるために利用しています。

全体の流れ

エンドユーザから見たときの全体の流れは、Unity SDKの方と同じフローです。

実装

最低、Avatar Play と連携または起動するためのUIを自社アプリ内に設置する必要があります。

UI

Avatar Play へのリンク(ボタン、テキストリンク、アイコンなど)を、自社アプリの設定画面などに配置します。

ボタン、テキストリンク

ボタンやテキストリンクを使う場合は、「Avatar Play を使う」という文言を利用してください。

ボタンリンク

アイコン

以下のアイコン画像を利用できます。

  1. 120x120ピクセルのアイコン
  2. 180x180ピクセルのアイコン

以下のように、アイコンの近くに「Avatar Play」という文言を配置してください。

アイコンリンク

サーバ実装箇所

以下の箇所をサーバ側で実装する必要があります。

  1. リンクの設置
  2. ID連携のリダイレクトURIへ戻った時

リンクの設置

ID連携済みのユーザの場合は「起動用リンク」を、ID連携していないユーザの場合は「ID連携用リンク」を設置します。ユーザーのステータス(ゲストなのかどうか)は、自社アプリ側で管理するか、Avatar API経由で取得できます。Avatar API に問い合わせた結果、「guest」プロパティが true であった場合はID連携していないゲストユーザですので、「ID連携用リンク」に誘導します。リンクはHTML内に埋め込むことも、自社アプリのエンドポイントからリダイレクトさせて誘導することも可能です。

起動用リンクの作成

OpenUrl API を呼び出し、「起動用リンク」を取得します。

$ curl "https://api.avatarplay3d.com/avatar/v1/OpenUrl?key={APIキー}"
{
 "url": "https://link.avatarplay3d.com/?...",
}
func open(w http.ResponseWriter, r *http.Request) {
    // リクエストURL
    requestURL := fmt.Sprintf("https://api.avatarplay3d.com/avatar/v1/OpenUrl?key=%s", "{APIキー}")

    // リクエスト
    res, err := http.Get(requestURL)
    if err != nil {
        http.Error(w, "api failed", http.StatusInternalServerError)
        return
    }

    // レスポンスボディ(JSON)をパース
    apiResponse := struct {
        URL string `json:"url"`
    }{}
    if err := json.NewDecoder(res.Body).Decode(&apiResponse); err != nil {
        http.Error(w, "decode failed", http.StatusInternalServerError)
        return
    }

    // redirect
    http.Redirect(w, r, apiResponse.URL, http.StatusFound)
}
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // リクエストURL
        String requestUrl = String.format("https://api.avatarplay3d.com/avatar/v1/OpenUrl?key=%s", "{APIキー}");

        String res;
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(requestUrl).openConnection();

            // リクエスト
            StringBuffer sb = new StringBuffer();
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                  sb.append(line);
                }
            } finally {
                if (reader != null) {
                    reader.close();
                }
            }

            res = sb.toString();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }

        JSONObject o = new JSONObject(res);
        response.sendRedirect(o.getString("url"));
    }
ID連携用リンクの作成

ConnectUrl API を呼び出して連携用のURLを取得します。ConnectUrl API の呼び出しには、以下の3つのパラメータを生成する必要があります。

redirectUri

アプリから戻ったときに呼び出される自社アプリのエンドポイントです。末尾に「token」パラメータが付与されたURLにユーザが戻ってきます。

scope

取得するパーミッションをカンマ区切りで連結した文字列です。

codeChallenge

以下のパラメータを連結した文字列から作られる、ハッシュ値です。

パラメータ名 説明 サンプル値
appID アプリID。自社アプリに割り振られたIDです。Avatar Play 担当者より共有されます。 1234567890
responseType レスポンスタイプ。「grant_token」という文字で固定です。 grant_token
redirectUri 前述の redirectUri と同じ値です。 http://example.com/oauth_callback
scope 前述の scope と同じ値です。 AvatarLoad,GrantItem
requestDate リクエスト時のUnixタイムスタンプ(秒)です。ユーザと紐付けてサーバ側に一時的に保持しておきます。後述の AccessToken API で必要になります。 1552893948
apiKey APIキーを指定します。 abcdefghi...
codeVerifier コード検証用の乱数です。48文字以上のランダム文字列を生成し、ユーザと紐付けてサーバ側に一時的に保持しておきます。後述の AccessToken API で必要になります。 qwertyuio...

これらの値を、半角コロン「:」区切りで上述の順に連結し、SHA256でハッシュ化したものが codeChallenge です。

実装
# パラメータを作ります。
$ APP_ID={アプリID}
$ RESPONSE_TYPE=grant_token
$ REDIRECT_URI=https://example.com/callback
$ SCOPE=AvatarLoad,ItemGrant
$ REQUEST_DATE=`date +%s`
$ API_KEY={APIキー}
$ CODE_VERIFIER=`cat /dev/urandom | base64 | fold -w 48 | head -n 1`

# codeChallengeを作ります。
$ CODE_CHALLENGE=`echo -n "$APP_ID:$RESPONSE_TYPE:$REDIRECT_URI:$SCOPE:$REQUEST_DATE:$API_KEY:$CODE_VERIFIER" | shasum -a 256 | cut -d' ' -f 1`

# パラメータをURLエンコードします。
$ ENC_REDIRECT_URI=`ruby -r cgi -e "puts CGI.escape(\"$REDIRECT_URI\")"`
$ ENC_SCOPE=`ruby -r cgi -e "puts CGI.escape(\"$SCOPE\")"`
$ ENC_CODE_CHALLENGE=`ruby -r cgi -e "puts CGI.escape(\"$CODE_CHALLENGE\")"`

# ConnectUrl API を呼び出します。
$ URL="https://api.avatarplay3d.com/avatar/v1/ConnectUrl?key=$API_KEY&redirectUri=$ENC_REDIRECT_URI&scope=$ENC_SCOPE&codeChallenge=$ENC_CODE_CHALLENGE"
$ curl $URL
{
 "url": "https://sdk.avatarplay3d.com/fit/lp?..."
}
func connect(w http.ResponseWriter, r *http.Request) {
    // リダイレクトURI
    redirectURI = "https://example.com/callback"

    // スコープ
    scope = "AvatarLoad"

    // リクエスト日時
    requestDate = time.Now().Unix()

    // codeVerifier
    codeVerifierBytes := make([]byte, 48)
    if _, err := io.ReadFull(rand.Reader, codeVerifierBytes); err != nil {
        http.Error(w, "code verifier generate failed", http.StatusInternalServerError)
        return
    }
    codeVerifier = hex.EncodeToString(codeVerifierBytes)

    // リダイレクトURI、スコープ、リクエスト日時、codeVerifier はユーザーと紐付けてキャッシュなどに保存しておく

    // codeChallenge
    codeChallenge := fmt.Sprintf(
        "%s:%s:%s:%s:%d:%s:%s",
        "{アプリID}",
        "grant_token",
        redirectURI,
        scope,
        requestDate,
        "{APIキー}",
        codeVerifier,
    )
    codeChallenge = fmt.Sprintf("%x", sha256.Sum256([]byte(codeChallenge)))

    // リクエストURL
    requestParams := url.Values{}
    requestParams.Add("key", "{APIキー}")
    requestParams.Add("redirectUri", redirectURI)
    requestParams.Add("scope", scope)
    requestParams.Add("codeChallenge", codeChallenge)
    requestURL := fmt.Sprintf("https://api.avatarplay3d.com/avatar/v1/ConnectUrl?%s", requestParams.Encode())

    // リクエスト
    res, err := http.Get(requestURL)
    if err != nil {
        http.Error(w, "api failed", http.StatusInternalServerError)
        return
    }

    // レスポンスボディ(JSON)をパース
    apiResponse := struct {
        URL string `json:"url"`
    }{}
    if err := json.NewDecoder(res.Body).Decode(&apiResponse); err != nil {
        http.Error(w, "decode failed", http.StatusInternalServerError)
        return
    }

    // リダイレクト
    http.Redirect(w, r, apiResponse.URL, http.StatusFound)
}
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // リダイレクトURI
        redirectUri = "https://example.com/callback";

        // スコープ
        scope = "AvatarLoad";

        // リクエスト日時
        requestDate = System.currentTimeMillis() / 1000L;

        // codeVerifier
        StringBuffer sbCodeVerifier = new StringBuffer();
        String alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
        for (int i = 0; i < 48; i++) {
            sbCodeVerifier.append(alphabet.charAt((int)(alphabet.length()*Math.random())));
        }
        codeVerifier = sbCodeVerifier.toString();

        // リダイレクトURI、スコープ、リクエスト日時、codeVerifier はユーザーと紐付けてキャッシュなどに保存しておく

        // codeChallenge
        String codeChallenge = String.format(
                "%s:%s:%s:%s:%d:%s:%s",
                "{アプリID}",
                "grant_token",
                redirectUri,
                scope,
                requestDate,
                "{APIキー}",
                codeVerifier);

        MessageDigest sha256;
        try {
            sha256 = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new ServletException(e);
        }
        codeChallenge = String.format("%040x", new BigInteger(1, sha256.digest(codeChallenge.getBytes())));

        // リクエストURL
        String requestUrl = String.format(
                "https://api.avatarplay3d.com/avatar/v1/ConnectUrl?key=%s&redirectUri=%s&scope=%s&codeChallenge=%s",
                "{APIキー}",
                redirectUri,
                scope,
                codeChallenge);

        String res;
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(requestUrl).openConnection();

            StringBuffer sb = new StringBuffer();
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                  sb.append(line);
                }
            } finally {
                if (reader != null) {
                    reader.close();
                }
            }

            res = sb.toString();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }

        JSONObject o = new JSONObject(res);
        response.sendRedirect(o.getString("url"));
    }

リダイレクトURIへ戻り、アバターIDを取得

ユーザは着せ替えアプリをインストールし、自社アプリへの利用許可に同意した後、redirectUri へ遷移します。自社アプリ側では redirectUri に付与された「token」パラメータを取得し、redirectUri, scope, requestDate, codeVerifier と一緒に AccessToken API へ引き渡してアバターIDを取得します。

$ TOKEN={デコードされたtokenパラメータの値}
$ ENC_TOKEN=`ruby -r cgi -e "puts CGI.escape(\"$TOKEN\")"`
$ ENC_CODE_VERIFIER=`ruby -r cgi -e "puts CGI.escape(\"$CODE_VERIFIER\")"`
$ URL="https://api.avatarplay3d.com/avatar/v1/AccessToken?key=$API_KEY&redirectUri=$ENC_REDIRECT_URI&scope=$ENC_SCOPE&requestDate=$REQUEST_DATE&codeVerifier=$ENC_CODE_VERIFIER&token=$ENC_TOKEN"
$ curl $URL
{
 "avatarId": "1234567890",
 "accessToken": "affs3mqcdwqtlrjahxe6hy0cic6g0xfdwqi4hsnkq487i73o",
 "scope": "AvatarLoad,GrantItem",
 "tokenType": "Bearer"
}
func callback(w http.ResponseWriter, r *http.Request) {
    // 渡されたトークン
    token := r.URL.Query().Get("token")

    // リクエストURL
    requestURL := fmt.Sprintf("https://api.avatarplay3d.com/avatar/v1/AccessToken?key=%s", "{APIキー}")

    // リクエストボディ
    requestParams := url.Values{}
    requestParams.Add("token", token)
    requestParams.Add("redirectUri", redirectURI)
    requestParams.Add("scope", scope)
    requestParams.Add("requestDate", strconv.FormatInt(requestDate, 10))
    requestParams.Add("codeVerifier", codeVerifier)
    requestBody := strings.NewReader(requestParams.Encode())

    // リクエスト
    req, err := http.NewRequest("POST", requestURL, requestBody)
    if err != nil {
        http.Error(w, "new request failed", http.StatusInternalServerError)
        return
    }

    // URLエンコードされたボディ。ボディがJSONの場合は「application/json」を指定する
    req.Header.Add("content-type", "application/x-www-form-urlencoded")

    client := &http.Client{}
    res, err := client.Do(req)
    if err != nil {
        http.Error(w, "api failed", http.StatusInternalServerError)
        return
    }

    // レスポンスボディ(JSON)をパース
    apiResponse := struct {
        AvatarID json.Number `json:"avatarId"`
    }{}
    if err := json.NewDecoder(res.Body).Decode(&apiResponse); err != nil {
        log.Printf("decode failed: %+v", err)
        http.Error(w, "decode failed", http.StatusInternalServerError)
        return
    }

    // 得られたアバターIDと自社アプリのユーザを紐付ける
    avatarID, _ = apiResponse.AvatarID.Int64()
}
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // 渡されたトークン
        String token = request.getParameter("token");

        // リクエストURL
        String requestUrl = String.format("https://api.avatarplay3d.com/avatar/v1/AccessToken?key=%s", "{APIキー}");

        // リクエストボディ
        String requestBody = String.format(
                "token=%s&redirectUri=%s&scope=%s&requestDate=%d&codeVerifier=%s",
                URLEncoder.encode(token, "UTF-8"),
                URLEncoder.encode(redirectUri, "UTF-8"),
                URLEncoder.encode(scope, "UTF-8"),
                requestDate,
                codeVerifier);

        String res;
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(requestUrl).openConnection();
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);

            // URLエンコードされたボディ。ボディがJSONの場合は「application/json」を指定する
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            // ボディ
            OutputStreamWriter writer = null;
            try {
                writer = new OutputStreamWriter(new BufferedOutputStream(conn.getOutputStream()));
                writer.write(requestBody);
            } finally {
                if (writer != null) {
                    writer.close();
                }
            }

            // リクエスト
            StringBuffer sb = new StringBuffer();
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                  sb.append(line);
                }
            } finally {
                if (reader != null) {
                    reader.close();
                }
            }

            res = sb.toString();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }

        // 得られたアバターIDと自社アプリのユーザを紐付ける
        JSONObject o = new JSONObject(res);
        avatarId = o.getLong("avatarId");
    }

ここで得られる「avatarId」の値が正規ユーザのアバターIDとなるため、自社アプリのユーザと紐づけ、以降ユーザのアバターIDとして利用します。RESTful API のみを利用する場合、アクセストークンは必要ないため破棄します。サーバ側で一時的に保持していた requestDate, codeVerifier は破棄します。

再連携(オプション)

着せ替えアプリと連携済みのユーザに対し、再度ID連携を行うためのリンクを設置することもできます。連携専用のUI(ボタンなど)を設置し、上述と同様に ConnectUrl, AccessToken API を使ってID連携を実装します。