/* AuthorizationProfile.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.profile; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; import org.deviceconnect.android.localoauth.AccessTokenData; import org.deviceconnect.android.localoauth.AccessTokenScope; import org.deviceconnect.android.localoauth.ClientData; import org.deviceconnect.android.localoauth.ConfirmAuthParams; import org.deviceconnect.android.localoauth.LocalOAuth2Main; import org.deviceconnect.android.localoauth.PublishAccessTokenListener; import org.deviceconnect.android.localoauth.exception.AuthorizationException; import org.deviceconnect.android.message.DConnectMessageService; import org.deviceconnect.android.message.MessageUtils; import org.deviceconnect.android.profile.api.DConnectApi; import org.deviceconnect.message.DConnectMessage; import org.deviceconnect.profile.AuthorizationProfileConstants; import org.restlet.ext.oauth.PackageInfoOAuth; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; /** * Authorization プロファイル. * * <p> * Local OAuthの認可機能を提供するAPI.<br> * </p> * * @author NTT DOCOMO, INC. */ public class AuthorizationProfile extends DConnectProfile implements AuthorizationProfileConstants { /** ロックオブジェクト. */ private final Object mLockObj = new Object(); /** * プロファイルプロバイダー. */ private final DConnectProfileProvider mProvider; /** * 指定されたプロファイルプロバイダーをもつAuthorizationプロファイルを生成する. * * @param provider プロファイルプロバイダー */ public AuthorizationProfile(final DConnectProfileProvider provider) { mProvider = provider; addApi(mGrantApi); addApi(mCreateAccessTokenApi); } /** * デバイスプラグインのサポートするすべてのプロファイル名の配列を取得する. * @return プロファイル名の配列 */ private String[] getAllProfileNames() { List<DConnectProfile> profiles = mProvider.getProfileList(); String[] names = new String[profiles.size()]; for (int i = 0; i < names.length; i++) { names[i] = profiles.get(i).getProfileName(); } return names; } @Override public final String getProfileName() { return PROFILE_NAME; } @Override public boolean onRequest(final Intent request, final Intent response) { // Local OAuthを使用しない場合にはNot Supportを返却する DConnectMessageService service = (DConnectMessageService) getContext(); if (!service.isUseLocalOAuth()) { MessageUtils.setNotSupportProfileError(response); return true; } return super.onRequest(request, response); } /** * Authorization Grant API. */ private final DConnectApi mGrantApi = new DConnectApi() { @Override public String getAttribute() { return ATTRIBUTE_GRANT; } @Override public Method getMethod() { return Method.GET; } @Override public boolean onRequest(final Intent request, final Intent response) { new Thread(new Runnable() { @Override public void run() { try { createClient(request, response); } catch (Exception e) { MessageUtils.setAuthorizationError(response, e.getMessage()); } sendResponse(response); } }).start(); return false; } }; /** * Authorization Create Access Token API. */ private final DConnectApi mCreateAccessTokenApi = new DConnectApi() { @Override public String getAttribute() { return ATTRIBUTE_ACCESS_TOKEN; } @Override public Method getMethod() { return Method.GET; } @Override public boolean onRequest(final Intent request, final Intent response) { new Thread(new Runnable() { @Override public void run() { try { getAccessToken(request, response); } catch (AuthorizationException e) { MessageUtils.setAuthorizationError(response, e.getMessage()); } catch (UnsupportedEncodingException e) { MessageUtils.setInvalidRequestParameterError(response, e.getMessage()); } catch (IllegalArgumentException e) { MessageUtils.setInvalidRequestParameterError(response, e.getMessage()); } catch (IllegalStateException e) { MessageUtils.setInvalidRequestParameterError(response, e.getMessage()); } catch (Exception e) { MessageUtils.setUnknownError(response, e.getMessage()); } sendResponse(response); } }).start(); return false; } }; /** * Clientデータを作成する. * * @param request リクエスト * @param response レスポンス */ private void createClient(final Intent request, final Intent response) { String packageName = request.getStringExtra(AuthorizationProfile.PARAM_PACKAGE); String serviceId = request.getStringExtra(DConnectProfile.PARAM_SERVICE_ID); if (packageName == null) { MessageUtils.setInvalidRequestParameterError(response); } else { // Local OAuthでクライアント作成 PackageInfoOAuth packageInfo = new PackageInfoOAuth(packageName, serviceId); try { ClientData client = LocalOAuth2Main.createClient(packageInfo); if (client != null) { response.putExtra(DConnectMessage.EXTRA_RESULT, DConnectMessage.RESULT_OK); response.putExtra(AuthorizationProfile.PARAM_CLIENT_ID, client.getClientId()); } else { MessageUtils.setAuthorizationError(response, "Cannot create a client."); } } catch (AuthorizationException e) { MessageUtils.setAuthorizationError(response, e.getMessage()); } catch (IllegalArgumentException e) { MessageUtils.setInvalidRequestParameterError(response, e.getMessage()); } } } /** * アクセストークンの取得の処理を行う. * * @param request リクエスト * @param response レスポンス * * @throws AuthorizationException 認証に失敗した場合に発生 * @throws UnsupportedEncodingException 文字のエンコードに失敗した場合に発生 */ private void getAccessToken(final Intent request, final Intent response) throws AuthorizationException, UnsupportedEncodingException { String serviceId = request.getStringExtra(DConnectMessage.EXTRA_SERVICE_ID); String clientId = request.getStringExtra(AuthorizationProfile.PARAM_CLIENT_ID); String[] scopes = parseScopes(request.getStringExtra(AuthorizationProfile.PARAM_SCOPE)); if (scopes == null) { scopes = getAllProfileNames(); } String applicationName; PackageManager pm = getContext().getPackageManager(); try { PackageInfo info = pm.getPackageInfo(getContext().getPackageName(), 0); ApplicationInfo ai = info.applicationInfo; applicationName = (String) pm.getApplicationLabel(ai); } catch (NameNotFoundException e) { applicationName = request.getStringExtra(AuthorizationProfile.PARAM_APPLICATION_NAME); } // TODO _typeからアプリorデバイスプラグインかを判別できる? ConfirmAuthParams params = new ConfirmAuthParams.Builder().context(getContext()).serviceId(serviceId) .clientId(clientId).scopes(scopes).applicationName(applicationName) .isForDevicePlugin(true) .build(); // Local OAuthでAccessTokenを作成する。 final AccessTokenData[] token = new AccessTokenData[1]; LocalOAuth2Main.confirmPublishAccessToken(params, new PublishAccessTokenListener() { @Override public void onReceiveAccessToken(final AccessTokenData accessTokenData) { token[0] = accessTokenData; synchronized (mLockObj) { mLockObj.notifyAll(); } } @Override public void onReceiveException(final Exception exception) { token[0] = null; synchronized (mLockObj) { mLockObj.notifyAll(); } } }); // ユーザからのレスポンスを待つ if (token[0] == null) { waitForResponse(); } // アクセストークンの確認 if (token[0] != null && token[0].getAccessToken() != null) { response.putExtra(DConnectMessage.EXTRA_RESULT, DConnectMessage.RESULT_OK); response.putExtra(AuthorizationProfile.PARAM_ACCESS_TOKEN, token[0].getAccessToken()); AccessTokenScope[] atScopes = token[0].getScopes(); if (atScopes != null) { List<Bundle> s = new ArrayList<>(); AccessTokenScope minScope = null; for (AccessTokenScope scope : atScopes) { Bundle b = new Bundle(); b.putString(PARAM_SCOPE, scope.getScope()); b.putLong(PARAM_EXPIRE_PERIOD, scope.getExpirePeriod()); s.add(b); if (minScope == null || (minScope.getExpirePeriod() > scope.getExpirePeriod())) { minScope = scope; } } response.putExtra(PARAM_SCOPES, s.toArray(new Bundle[s.size()])); // NOTE: GotAPI 1.0対応 if (minScope != null) { response.putExtra(PARAM_EXPIRE, token[0].getTimestamp() + minScope.getExpirePeriod()); } } } else { MessageUtils.setAuthorizationError(response, "Cannot create a access token."); } } /** * レスポンスが返ってくるまでの間スレッドを停止する. * タイムアウトは設定していない。 */ private void waitForResponse() { synchronized (mLockObj) { try { mLockObj.wait(); } catch (InterruptedException e) { mLogger.warning("InterruptedException occurred in waitForResponse."); } } } /** * スコープを分割して、配列に変換します. * @param scope スコープ * @return 分割されたスコープ. scopeが<code>null</code>または空文字の場合は<code>null</code> */ private String[] parseScopes(final String scope) { if (scope == null) { return null; } String[] scopes = scope.split(","); for (int i = 0; i < scopes.length; i++) { String s = scopes[i].trim(); if (s.equals("")) { return null; } scopes[i] = s; } return scopes; } }