■Zoom - PKCE OAuth とは
既存のOAuthでは、Zoomから提供される"Authorization Code"をサーバサイドで取り扱いする必要がありましたが、PKCE OAuthでは"Code Exchange"を採用することによりSDKクライアント側の実装でAPIリクエストに必要な認証トークンの取得まで実装いただけるようになっています。 また、Zoomユーザのメールアドレス、パスワードでZoomMeetingSDKクラインをご利用いただくことが可能になります。(2022/06/17)
■実装の概要
ネイティブ(iOS, Android, Win, Mac)版のMeetingSDKにおいてZoomMeetingへ接続する際に、ユーザの識別にzakトークン(Zoom Access Token)を使用します。Zakトークンは、Rest_API経由で取得する必要があるため、Rest_APIを実行するための"access_token"(認証トークン)が必要となります。
ユーザに紐ずく"access_token"を取得する為には一旦ブラウザ経由でZoomのサインインページにアクセスしてメールアドレス、パスワードを入力しZoom側で認証後、指定のリダイレクトURL宛に"Authorization Code"を元に生成いただく必要があります。また、サーバサイドで処理は必要ありませんが、実在するリダイレクト先の指定は必要となります。事前にドメインの準備は必要となります。
■実装例
⑴ 初めにアプリを起動する準備をしていきます。
MarketplaceにサインインしてサンプルのSDKをダウンロードしておきます。
*詳しくは:はじめての Zoom Meeting SDK - 準備編)
Android Studioから新しく"Empty Activity"でプロジェクトを作成し事前にダウンロードしておいたAndroid版のSDKをインポートしていきます。
*詳しくは:Import the Zoom SDK libraries
⑵ 識別認証で利用する対になる"code_challenge"と"code_verifier"を生成できるようにします。
public final String createCodeVerifier() {
SecureRandom secureRandom = new SecureRandom();
byte[] code = new byte[32];
secureRandom.nextBytes(code);
code_verifier = Base64.encodeToString(code, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
return code_verifier;
}
public final String createCodeChallenge() throws NoSuchAlgorithmException, UnsupportedEncodingException {
byte[] codeVerifierBytes = createCodeVerifier().getBytes("US-ASCII");
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(codeVerifierBytes);
byte[] codeChallengeBytes = md.digest();
code_challenge = Base64.encodeToString(codeChallengeBytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
return code_challenge;
}
⑶ Zoomのサインインページで "code_challenge"を付随して認証できるようにURIを生成します。
public String createAuthorizeURI() throws UnsupportedEncodingException, NoSuchAlgorithmException {
code_challenge = createCodeChallenge();
Log.d(TAG, "code_challenge: " + code_challenge);
String uriString = <Marketplaceで生成したAdd_URL>;
Uri uri = Uri.parse(uriString)
.buildUpon()
.appendQueryParameter("code_challenge", code_challenge)
.appendQueryParameter("code_challenge_method", "S256")
.build();
return uri.toString();
}
⑷ WebViewを実装し先ほど生成したURIへアクセスできるようします。
ただし、必要なのはZoomへ認証した後の”Authorization Code”のみなのでhandlerを利用してリダイレクト先へアクセスする前にcodeだけを取得して次に繋げます。
try {
url = createAuthorizeURI();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
WebView webView = (WebView) findViewById(R.id.webView1);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
final Uri uri = Uri.parse(url);
return handleUri(uri);
}
private boolean handleUri(final Uri uri) {
Log.i(TAG, "Uri = " + uri);
final String host = uri.getHost();
final String scheme = uri.getScheme();
if (!host.equals(<Marketplaceで指定したリダイレクトURLのドメイン>)) {
return false; // continue loading page mainly for Zoom Authentication screen
} else {
String code = uri.getQueryParameter("code");
Log.d(TAG, "code: " + code);
getAccessToken(code);
return true; // Stop loading once getting Authentication_Code
}
}
});
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
webView.loadUrl(url);
⑸ リダイレクトされた"Authorization Code"を元に"code_verifier"を付随して"access_token"を取得します。
*以下実際のHTTP処理は別HttpRequestClassにて実施しCallbackの様子になります。
*Callbackで取得したaccess_tokenとrefresh_tokenはその後利用できるようにSharedPreferencesに保存しておくと便利です。
public void getAccessToken(String code){
Log.d(TAG, "getAccessToken");
Log.d(TAG, "code_verifier: " + code_verifier);
String Url = "https://zoom.us/oauth/token?code="+ code + "&grant_type=authorization_code&redirect_uri=" + <Marketplaceで指定したURLをencodeした値> + "&code_verifier=" + code_verifier;
String clientid = <Marketplaceで取得した Client ID>;
String clientsecret = <Marketplaceで取得した Client Secret>;
final String baseCode = clientid + ":" + clientsecret;
final String AuthCode = Base64.encodeToString(baseCode.getBytes(), Base64.URL_SAFE | Base64.NO_WRAP);
String AuthType = "Basic";
String RequestMethod = "POST";
String ContentType = "application/x-www-form-urlencoded";
HttpRequestClass httpRequest = new HttpRequestClass();
httpRequest.setOnCallBack(new HttpRequestClass.CallBackTask(){
@Override
public void CallBack(String result) {
super.CallBack(result);
Log.d(TAG, "getAccessToken HttpRequestClass: " + result);
if (isJSONValid(result)){
try {
JSONObject jsonObject = new JSONObject(result);
String access_token = jsonObject.getString("access_token");
String refresh_token = jsonObject.getString("refresh_token");
SharedPreferences.Editor editor = pref.edit();
editor.putString("access_token", access_token);
editor.putString("refresh_token", refresh_token);
editor.commit();
getUserInfo(access_token);
} catch (JSONException e) {
System.out.println("Error " + e.toString());
}
}
}
});
httpRequest.execute(Url, AuthType, AuthCode, RequestMethod, ContentType);
}
⑹ 取得した"access_token"を利用してユーザ情報を取得します。
*以下実際のHTTP処理は別HttpRequestClassにて実施しCallbackの様子になります。
*Callbackで取得した"id"と"表示名"はその後利用できるようにSharedPreferencesに保存しておくと便利です。
public void getUserInfo(String access_token){
Log.d(TAG, "getUserInfo");
String Url = "https://api.zoom.us/v2/users/me";
final String AuthCode = access_token;
String AuthType = "Bearer";
String RequestMethod = "GET";
String ContentType = "application/json";
HttpRequestClass httpRequest = new HttpRequestClass();
httpRequest.setOnCallBack(new HttpRequestClass.CallBackTask(){
@Override
public void CallBack(String result) {
super.CallBack(result);
Log.d(TAG, "getUserInfo HttpRequestClass: " + result);
if (isJSONValid(result)){
try {
JSONObject jsonObject = new JSONObject(result);
String id = jsonObject.getString("id");
String first_name = jsonObject.getString("first_name");
String last_name = jsonObject.getString("last_name");
String display_name = first_name + " " + last_name;
Log.d(TAG, "id: " + id);
SharedPreferences.Editor editor = pref.edit();
editor.putString("id", id);
editor.putString("display_name", display_name);
editor.commit();
finish();
} catch (JSONException e) {
System.out.println("Error " + e.toString());
}
}
}
});
httpRequest.execute(Url, AuthType, AuthCode, RequestMethod, ContentType);
}
⑺ ここまで取得できたところで、最後に"access_token"を利用して"zak"トークンを取得しZoomMeetingを起動します。
注意:"access_token"が失効してしまっている場合には"refresh_token"を利用して別途取得し直す必要があります。
public void getZakToken(String ACCESS_TOKEN){
Log.d(TAG, "getZakToken");
String Url = "https://api.zoom.us/v2/users/me/zak";
final String AuthCode = ACCESS_TOKEN;
String AuthType = "Bearer";
String RequestMethod = "GET";
String ContentType = "application/json";
HttpRequestClass httpRequest = new HttpRequestClass();
httpRequest.setOnCallBack(new HttpRequestClass.CallBackTask(){
@Override
public void CallBack(String result) {
super.CallBack(result);
Log.d(TAG, "getZakToken HttpRequestClass: " + result);
if (isJSONValid(result)){
try {
JSONObject jsonObject = new JSONObject(result);
String ZOOM_ACCESS_TOKEN = jsonObject.getString("token");
Log.d(TAG, "zak_token: " + ZOOM_ACCESS_TOKEN);
prepareZoomMeeting(ZOOM_ACCESS_TOKEN);
} catch (JSONException e) {
System.out.println("Error " + e.toString());
}
}
}
});
httpRequest.execute(Url, AuthType, AuthCode, RequestMethod, ContentType);
}
btn.setOnClickListener( v -> {
Log.d(TAG, "button clicked: " + editTextNum.getText().toString());
MEETING_ID = editTextNum.getText().toString();
pref = this.getSharedPreferences("msdk_settings", Context.MODE_PRIVATE);
USER_ID = pref.getString("id", null);
DISPLAY_NAME = pref.getString("display_name", null);
final String access_token = pref.getString("access_token", null);
ZoomSDK zoomSDK = ZoomSDK.getInstance();
if(zoomSDK.isInitialized()) {
registerMeetingServiceListener();
getZakToken(access_token);
}else{
Log.e(TAG, "zoomSDK is not initialized");
}
});
■サンプル
補足
marketplace.zoom.usへサインインして「Build App」よりSDKappを登録した上では、別途Android版SDKをダウンロードする必要があります。
MarketplaceからダウンロードしたSDKのzipファイル内からそれぞれ「commonlib」、「mobilertc」に含まれるファイルをサンプル内のフォルダへコピーしてからAndroidStudioで開いでください。
また、Contrants.classにinterfeceを用意していますので、Marketaplceで採取、指定しただいたクレデンシャル、リダイレクトURLを入力してからBuildするようにしてください。
Marketplaceで指定するリダイレクトURL先での処理の必要はありませんが、何らか応答できるページを持っている必要があります。
■その他参考資料
PKCE OAuth tutorial
https://marketplace.zoom.us/docs/sdk/native-sdks/android/build-an-app/pkce
OAuth with Zoom
https://marketplace.zoom.us/docs/guides/auth/oauth
はじめての Zoom Meeting SDK - 準備編
https://qiita.com/yosuke-sawamura/items/de69e73e47335cd61d68