com.microsoft.aad.adal.CordovaAdalPlugin.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.aad.adal.CordovaAdalPlugin.java

Source

/*******************************************************************************
 * Copyright (c) Microsoft Open Technologies, Inc.
 * All Rights Reserved
 * Licensed under the Apache License, Version 2.0.
 * See License.txt in the project root for license information.
 ******************************************************************************/

package com.microsoft.aad.adal;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PermissionHelper;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;

import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import static com.microsoft.aad.adal.SimpleSerialization.tokenItemToJSON;

public class CordovaAdalPlugin extends CordovaPlugin {

    private static final PromptBehavior SHOW_PROMPT_ALWAYS = PromptBehavior.Always;

    private static final int GET_ACCOUNTS_PERMISSION_REQ_CODE = 0;
    private static final String PERMISSION_DENIED_ERROR = "Permissions denied";
    private static final String SECRET_KEY = "com.microsoft.aad.CordovaADAL";

    private final Hashtable<String, AuthenticationContext> contexts = new Hashtable<String, AuthenticationContext>();
    private AuthenticationContext currentContext;
    private CallbackContext callbackContext;

    public CordovaAdalPlugin() {

        // Android API < 18 does not support AndroidKeyStore so ADAL requires
        // some extra work to crete and pass secret key to ADAL.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            try {
                SecretKey secretKey = this.createSecretKey(SECRET_KEY);
                AuthenticationSettings.INSTANCE.setSecretKey(secretKey.getEncoded());
            } catch (Exception e) {
                Log.w("CordovaAdalPlugin", "Unable to create secret key: " + e.getMessage());
            }
        }
    }

    @Override
    public boolean execute(String action, JSONArray args, final CallbackContext callbackContext)
            throws JSONException {

        this.cordova.setActivityResultCallback(this);
        this.callbackContext = callbackContext;

        if (action.equals("createAsync")) {

            // Don't catch JSONException since it is already handled by Cordova
            String authority = args.getString(0);
            // Due to https://github.com/AzureAD/azure-activedirectory-library-for-android/blob/master/src/src/com/microsoft/aad/adal/AuthenticationContext.java#L158
            // AuthenticationContext constructor validates authority by default
            boolean validateAuthority = args.optBoolean(1, true);
            return createAsync(authority, validateAuthority);

        } else if (action.equals("acquireTokenAsync")) {

            String authority = args.getString(0);
            boolean validateAuthority = args.optBoolean(1, true);
            String resourceUrl = args.getString(2);
            String clientId = args.getString(3);
            String redirectUrl = args.getString(4);
            String userId = args.optString(5, null);
            userId = userId.equals("null") ? null : userId;
            String extraQueryParams = args.optString(6, null);
            extraQueryParams = extraQueryParams.equals("null") ? null : extraQueryParams;

            return acquireTokenAsync(authority, validateAuthority, resourceUrl, clientId, redirectUrl, userId,
                    extraQueryParams);

        } else if (action.equals("acquireTokenSilentAsync")) {

            String authority = args.getString(0);
            boolean validateAuthority = args.optBoolean(1, true);
            String resourceUrl = args.getString(2);
            String clientId = args.getString(3);
            String userId = args.getString(4);

            // This is a workaround for Cordova bridge issue. When null us passed from JS side
            // it is being translated to "null" string
            userId = userId.equals("null") ? null : userId;

            return acquireTokenSilentAsync(authority, validateAuthority, resourceUrl, clientId, userId);

        } else if (action.equals("tokenCacheClear")) {

            String authority = args.getString(0);
            boolean validateAuthority = args.optBoolean(1, true);
            return clearTokenCache(authority, validateAuthority);

        } else if (action.equals("tokenCacheReadItems")) {

            String authority = args.getString(0);
            boolean validateAuthority = args.optBoolean(1, true);
            return readTokenCacheItems(authority, validateAuthority);

        } else if (action.equals("tokenCacheDeleteItem")) {

            String authority = args.getString(0);
            boolean validateAuthority = args.optBoolean(1, true);
            String itemAuthority = args.getString(2);
            String resource = args.getString(3);
            resource = resource.equals("null") ? null : resource;
            String clientId = args.getString(4);
            String userId = args.getString(5);
            boolean isMultipleResourceRefreshToken = args.getBoolean(6);

            return deleteTokenCacheItem(authority, validateAuthority, itemAuthority, resource, clientId, userId,
                    isMultipleResourceRefreshToken);
        } else if (action.equals("setUseBroker")) {

            boolean useBroker = args.getBoolean(0);
            return setUseBroker(useBroker);
        }

        return false;
    }

    private boolean createAsync(String authority, boolean validateAuthority) {

        try {
            getOrCreateContext(authority, validateAuthority);
        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        callbackContext.success();
        return true;
    }

    private boolean acquireTokenAsync(String authority, boolean validateAuthority, String resourceUrl,
            String clientId, String redirectUrl, String userId, String extraQueryParams) {

        final AuthenticationContext authContext;
        try {
            authContext = getOrCreateContext(authority, validateAuthority);
        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        if (userId != null) {
            ITokenCacheStore cache = authContext.getCache();
            if (cache instanceof ITokenStoreQuery) {

                List<TokenCacheItem> tokensForUserId = ((ITokenStoreQuery) cache).getTokensForUser(userId);
                if (tokensForUserId.size() > 0) {
                    // Try to acquire alias for specified userId
                    userId = tokensForUserId.get(0).getUserInfo().getDisplayableId();
                }
            }
        }

        authContext.acquireToken(this.cordova.getActivity(), resourceUrl, clientId, redirectUrl, userId,
                SHOW_PROMPT_ALWAYS, extraQueryParams, new DefaultAuthenticationCallback(callbackContext));

        return true;

    }

    private boolean acquireTokenSilentAsync(String authority, boolean validateAuthority, String resourceUrl,
            String clientId, String userId) {

        final AuthenticationContext authContext;
        try {
            authContext = getOrCreateContext(authority, validateAuthority);
        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        authContext.acquireTokenSilent(resourceUrl, clientId, userId,
                new DefaultAuthenticationCallback(callbackContext));
        return true;
    }

    private boolean readTokenCacheItems(String authority, boolean validateAuthority) throws JSONException {

        final AuthenticationContext authContext;
        try {
            authContext = getOrCreateContext(authority, validateAuthority);
        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        JSONArray result = new JSONArray();
        ITokenCacheStore cache = authContext.getCache();

        if (cache instanceof ITokenStoreQuery) {
            Iterator<TokenCacheItem> cacheItems = ((ITokenStoreQuery) cache).getAll();

            while (cacheItems.hasNext()) {
                TokenCacheItem item = cacheItems.next();
                result.put(tokenItemToJSON(item));
            }
        }

        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result));

        return true;
    }

    private boolean deleteTokenCacheItem(String authority, boolean validateAuthority, String itemAuthority,
            String resource, String clientId, String userId, boolean isMultipleResourceRefreshToken) {

        final AuthenticationContext authContext;
        try {
            authContext = getOrCreateContext(authority, validateAuthority);
        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        String key = CacheKey.createCacheKey(itemAuthority, resource, clientId, isMultipleResourceRefreshToken,
                userId);
        authContext.getCache().removeItem(key);

        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
        return true;
    }

    private boolean clearTokenCache(String authority, boolean validateAuthority) {
        final AuthenticationContext authContext;
        try {
            authContext = getOrCreateContext(authority, validateAuthority);
        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        authContext.getCache().removeAll();
        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
        return true;
    }

    private boolean setUseBroker(boolean useBroker) {

        try {
            AuthenticationSettings.INSTANCE.setUseBroker(useBroker);

            // Android 6.0 "Marshmallow" introduced a new permissions model where the user can turn on and off permissions as necessary.
            // This means that applications must handle these permission in run time.
            // http://cordova.apache.org/docs/en/latest/guide/platforms/android/plugin.html#android-permissions
            if (useBroker && Build.VERSION.SDK_INT >= 23 /* Build.VERSION_CODES.M */ ) {

                requestBrokerPermissions();
                // Cordova callback will be handled by requestBrokerPermissions method
                return true;
            }

        } catch (Exception e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage()));
            return true;
        }

        callbackContext.success();
        return true;
    }

    private void requestBrokerPermissions() {

        // USE_CREDENTIALS and MANAGE_ACOUNTS are deprecated and not required
        if (PermissionHelper.hasPermission(this, Manifest.permission.GET_ACCOUNTS)) { // android.permission.GET_ACCOUNTS
            // already granted
            callbackContext.success();
            return;
        }

        PermissionHelper.requestPermission(this, GET_ACCOUNTS_PERMISSION_REQ_CODE,
                Manifest.permission.GET_ACCOUNTS);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (currentContext != null) {
            currentContext.onActivityResult(requestCode, resultCode, data);
        }
    }

    public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults)
            throws JSONException {
        for (int r : grantResults) {
            if (r == PackageManager.PERMISSION_DENIED) {
                this.callbackContext
                        .sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
                return;
            }
        }
        callbackContext.success();
    }

    private AuthenticationContext getOrCreateContext(String authority, boolean validateAuthority)
            throws NoSuchPaddingException, NoSuchAlgorithmException {

        AuthenticationContext result;
        if (!contexts.containsKey(authority)) {
            result = new AuthenticationContext(this.cordova.getActivity(), authority, validateAuthority);
            this.contexts.put(authority, result);
        } else {
            result = contexts.get(authority);
        }

        currentContext = result;
        return result;
    }

    private SecretKey createSecretKey(String key)
            throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithSHA256And256BitAES-CBC-BC");
        SecretKey tempkey = keyFactory
                .generateSecret(new PBEKeySpec(key.toCharArray(), "abcdedfdfd".getBytes("UTF-8"), 100, 256));
        SecretKey secretKey = new SecretKeySpec(tempkey.getEncoded(), "AES");
        return secretKey;
    }
}